vdelacou/iblis-ui

View on GitHub
src/components/form_components/managed_simple_list_form/index.tsx

Summary

Maintainability
D
1 day
Test Coverage
import { Grid, IconButton, Menu, MenuItem, Tooltip, Typography, WithTheme, withTheme } from '@material-ui/core';
import { Cancel, DeleteForever, Done, MoreVert } from '@material-ui/icons';
import * as React from 'react';
import { Field, InjectedFormProps } from 'redux-form';
import { IblisTextField } from '../../../components';
import { style } from './style';

export interface ManagedSimpleListFormValues {
    /**
     * The id of the entity
     */
    entityId?: string | number;
    /**
     * The name of the entity
     */
    entityName?: string;
}

export interface ManagedSimpleListFormProps {
    /**
     * The initial form values
     */
    initValues: ManagedSimpleListFormValues;
    /**
     * The text to display on edit menu button
     * @default Edit
     */
    editLabel?: string;
    /**
     * The text to display on delete menu button
     * @default Delete
     */
    deleteLabel?: string;
    /**
     * The text to display on tooltip on icon for confirm edit
     * @default Edit
     */
    editConfirmLabel?: string;
    /**
     * The text to display on tooltip on icon for cancel edit
     * @default Cancel
     */
    editCancelLabel?: string;
    /**
     * The text to display on tooltip on icon for confirm delete
     * @default Delete
     */
    deleteConfirmLabel?: string;
    /**
     * The text to display on tooltip on icon for cancel delete
     * @default Cancel
     */
    deleteCancelLabel?: string;
    /**
     * The message to show to confirm to really delete a entity
     * @default Do you confirm you want to delete this?
     */
    confirmDeleteLabel?: string;
    /**
     * To show in the menu the edit button
     * @default true
     */
    hasEdit?: boolean;
    /**
     * To show in the menu the deletee button
     * @default true
     */
    hasDelete?: boolean;
    /**
     * To show to user that the action is loading
     * @default false
     */
    isLoading?: boolean;
    /**
     * To show error if validation function is not satisfied
     */
    validateFunctions: Array<(value: string, allValues: ManagedSimpleListFormValues, props: ManagedSimpleListFormProps) => string | undefined>;
    /**
     * The component to display if needed at the left of the form
     */
    leftComponent?: React.ReactNode;
    /**
     * The height of the component
     * @default 60
     */
    componentHeight?: number;
    /**
     * A list of label and action to add to the menu
     * @default []
     */
    menuAction?: Array<{
        /**
         * The label to display on the menu
         */
        label: string;
        /**
         * If the menu is disabled
         * @default false
         */
        disabled?: boolean;
        /**
         * The Function to call to when click on menu
         */
        action(id: string | number): void;
    }>;
    /*
     * The Function to call to edit entity
     */
    editAction(values: ManagedSimpleListFormValues): void;
    /*
     * The Function to call to delete entity
     */
    deleteAction(id: string | number): void;
}

class ManagedSimpleListFormState {
    /**
     * on which element show the menu. if undefined the menu is not show
     */
    readonly element?: EventTarget & HTMLElement = undefined;
    /**
     * When we display the form to allow edit
     * @default false
     */
    readonly editItem: boolean = false;
    /**
     * When we display the button to confirm delete
     * @default false
     */
    readonly deleteItem: boolean = false;
}

class ManagedSimpleListFormBase extends
    React.PureComponent<ManagedSimpleListFormProps & InjectedFormProps<ManagedSimpleListFormValues, ManagedSimpleListFormProps> & WithTheme, ManagedSimpleListFormState> {

    readonly state = new ManagedSimpleListFormState();

    focusInputField = (input?: HTMLInputElement) => {
        //  dirty hack to force focus (need to find a way to make it cleaner)
        setTimeout(() => { if (input) { input.focus(); } }, 100);
    }

    submitForm = (values: ManagedSimpleListFormValues) => {
        this.props.editAction(values);
        this.setState(() => { return { editItem: false }; });
    }

    delete = () => {
        if (this.props.initValues && this.props.initValues.entityId) {
            this.props.deleteAction(this.props.initValues.entityId);
            this.setState(() => { return { deleteItem: false }; });
        }
    }

    renderMenu = (
        editLabel: string, deleteLabel: string, hasEdit: boolean, hasDelete: boolean,
        optionalMenu: Array<{ label: string; disabled?: boolean; action(id: string | number): void }> = [], element?: HTMLElement) => {
        return (
            <Menu
                elevation={1}
                anchorEl={element}
                open={Boolean(element)}
                disableEnforceFocus={true}
                disableRestoreFocus={true}
                onClose={() => { this.setState(() => { return { element: undefined }; }); }}
                anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
            >
                {this.renderEditMenuItem(editLabel, hasEdit)}
                {this.renderDeleteMenuItem(deleteLabel, hasDelete)}
                {this.renderOptionalMenuItem(optionalMenu)}
            </Menu >
        );
    }

    renderEditMenuItem = (editLabel: string, hasEdit: boolean) => {
        if (hasEdit) {
            return (
                <MenuItem onClick={() => { this.setState(() => { return { editItem: true, element: undefined }; }); }}>
                    {editLabel}
                </MenuItem>
            );
        } else {
            return null;
        }
    }

    renderDeleteMenuItem = (deleteLabel: string, hasDelete: boolean) => {
        if (hasDelete) {
            return (
                <MenuItem onClick={() => { this.setState(() => { return { deleteItem: true, element: undefined }; }); }}>
                    {deleteLabel}
                </MenuItem>
            );
        } else {
            return null;
        }
    }

    renderOptionalMenuItem = (optionalMenu: Array<{ label: string; disabled?: boolean; action(id: string | number): void }> = []) => {
        if (optionalMenu.length !== 0) {
            return optionalMenu.map((menu, index) => {
                if (this.props.initValues && this.props.initValues.entityId) {
                    const id = this.props.initValues.entityId;
                    return (
                        <MenuItem disabled={menu.disabled} key={index} onClick={() => menu.action(id)}>
                            {menu.label}
                        </MenuItem>
                    );
                } else {
                    return null;
                }
            });
        } else {
            return null;
        }
    }

    renderValue = (editItem: boolean, deleteItem: boolean, confirmDeleteLabel: string) => {
        if (deleteItem) {
            return (
                <Typography variant="subheading" color="primary" style={style(this.props.theme).valueContainer}  >
                    {confirmDeleteLabel}
                </Typography>
            );
        }
        if (!editItem) {
            return (
                <Typography variant="subheading" color={this.props.isLoading ? 'textSecondary' : 'default'} style={style(this.props.theme).valueContainer} noWrap={true} >
                    {this.props.initValues && this.props.initValues.entityName}
                </Typography >
            );
        } else {
            return (
                <div style={style(this.props.theme).formContainer}>
                    <Field
                        type="text"
                        name="entityName"
                        component={IblisTextField}
                        disabled={this.props.submitting || this.props.isLoading}
                        validate={this.props.validateFunctions}
                        required={true}
                        autoFocus={true}
                        withRef={true}
                        inputRef={(ref: HTMLInputElement) => this.focusInputField(ref)}
                    />
                </div>
            );
        }
    }

    renderIconMenu = (
        editItem: boolean, deleteItem: boolean, //
        editConfirmLabel: string, editCancelLabel: string, deleteConfirmLabel: string, deleteCancelLabel: string, //
        isLoading: boolean, hasEdit: boolean, hasDelete: boolean, optionalMenu: Array<{ label: string; disabled?: boolean; action(id: string | number): void }> = [],
    ) => {
        if (deleteItem) {
            return (
                <div >
                    <Tooltip title={deleteConfirmLabel} >
                        <IconButton
                            disabled={isLoading}
                            onClick={() => { this.delete(); }}
                            color={'inherit'}
                        >
                            <DeleteForever color={'primary'} />
                        </IconButton>
                    </Tooltip>
                    <Tooltip title={deleteCancelLabel}>
                        <IconButton
                            disabled={isLoading}
                            onClick={() => this.setState(() => { return { deleteItem: false }; })}
                        >
                            <Cancel />
                        </IconButton>
                    </Tooltip>
                </div >
            );
        }
        if (editItem) {
            return (
                <div>
                    <Tooltip title={editConfirmLabel}>
                        <IconButton
                            disabled={isLoading}
                            type="submit"
                        >
                            <Done color={'primary'} />
                        </IconButton>
                    </Tooltip>
                    <Tooltip title={editCancelLabel}>
                        <IconButton
                            disabled={isLoading}
                            onClick={() => this.setState(() => { return { editItem: false }; })}
                        >
                            <Cancel />
                        </IconButton>
                    </Tooltip>
                </div>
            );
        }
        if (hasEdit || hasDelete || (optionalMenu.length > 0)) {
            return (
                <div >
                    <IconButton
                        disabled={isLoading}
                        onClick={(event: React.MouseEvent<HTMLElement>) => { const el = event.currentTarget; this.setState(() => { return { element: el }; }); }}
                    >
                        <MoreVert />
                    </IconButton>
                </div >
            );
        }
        return null;
    }

    render(): React.ReactNode {

        const {
            editLabel = 'Edit', deleteLabel = 'Delete', confirmDeleteLabel = 'Do you confirm you want to delete this?', leftComponent, componentHeight = 60,
            isLoading = false, menuAction = [], hasEdit = true, hasDelete = true, theme }
            = this.props;
        const editCfrmLbl = this.props.editConfirmLabel ? this.props.editConfirmLabel : 'Edit';
        const editCncLbl = this.props.editCancelLabel ? this.props.editCancelLabel : 'Cancel';
        const dltCfrmLbl = this.props.deleteConfirmLabel ? this.props.deleteConfirmLabel : 'Delete';
        const dltCncLbl = this.props.deleteCancelLabel ? this.props.deleteCancelLabel : 'Cancel';
        const { editItem, deleteItem, element } = this.state;

        if (this.props.initValues && !this.props.initialized) {
            this.props.initialize(this.props.initValues);
        }
        return (
            <div>
                <form
                    onSubmit={this.props.handleSubmit(this.submitForm)}
                    noValidate={true}
                >
                    <Grid container={true} alignItems={'center'}>
                        <Grid item={true} xs={8} sm={10} >
                            <div style={style(theme, componentHeight).mainContainer}>
                                <div
                                    style={leftComponent ? style(theme, componentHeight).leftComponentContainerPresent : style(theme, componentHeight).leftComponentContainer}
                                >
                                    {leftComponent}
                                </div>
                                <div style={style(theme, componentHeight).renderValueContainerFlex} >
                                    <div style={style(theme, componentHeight).renderValueContainer}>
                                        {this.renderValue(editItem, deleteItem, confirmDeleteLabel)}
                                    </div>
                                </div>
                            </div>
                        </Grid>
                        <Grid item={true} xs={4} sm={2} style={style(this.props.theme).iconMenuContainer}>
                            {this.renderIconMenu(editItem, deleteItem, editCfrmLbl, editCncLbl, dltCfrmLbl, dltCncLbl, isLoading, hasEdit, hasDelete, menuAction)}
                        </Grid>
                    </Grid>
                </form>
                {/* Render Menu */}
                {this.renderMenu(editLabel, deleteLabel, hasEdit, hasDelete, menuAction, element)}
            </div >
        );
    }
}

const ManagedSimpleListFormWithTheme: React.ComponentType<ManagedSimpleListFormProps & InjectedFormProps<ManagedSimpleListFormValues, ManagedSimpleListFormProps>> =
    withTheme()(ManagedSimpleListFormBase);

/**
 * A simple form for update or delete an entity with only one element
 * If no edit or delete button asked, then the menu is not displayed
 */
export const ManagedSimpleListForm: React.ComponentType<ManagedSimpleListFormProps & InjectedFormProps<ManagedSimpleListFormValues, ManagedSimpleListFormProps>> =
    (ManagedSimpleListFormWithTheme);