mongaku/mongaku

View on GitHub
src/views/EditRecord.js

Summary

Maintainability
D
2 days
Test Coverage
// @flow

const React = require("react");

const FixedStringEdit = require("./types/edit/FixedString.js");
const LinkedRecordEdit = require("./types/edit/LinkedRecord.js");
const NameEdit = require("./types/edit/Name.js");
const SimpleDateEdit = require("./types/edit/SimpleDate.js");
const YearRangeEdit = require("./types/edit/YearRange.js");
const Select = require("./shared/Select.js");

import type {Context, ModelType, Source} from "./types.js";
const {childContextTypes} = require("./Wrapper.js");

type Props = {
    title: string,
    dynamicValues: {},
    globalFacets?: {
        [name: string]: Array<{
            text: string,
        }>,
    },
    mode: "create" | "edit" | "clone",
    record?: Record,
    type: string,
    source?: string,
    sources?: Array<Source>,
};

type Record = {
    _id?: string,
    id?: string,
    type: string,
    title?: string,
    imageModels: Array<ImageType>,
    getTitle: string,
    getEditURL: string,
    getCloneURL: string,
    getCreateURL: string,
    getRemoveImageURL: string,
};

type ImageType = {
    _id: string,
    getOriginalURL: string,
    getThumbURL: string,
};

const Image = (
    {
        image,
        record,
        title,
    }: Props & {
        image: ImageType,
        title: string,
    },
    {gettext}: Context,
) => (
    <div className="img col-md-3 col-xs-6 col-sm-4" key={image._id}>
        <div className="img-wrap">
            <a href={image.getOriginalURL} target="_blank">
                <img
                    src={image.getThumbURL}
                    alt={title}
                    title={title}
                    className="img-responsive center-block"
                />
            </a>
        </div>

        <div className="details reduced">
            <form
                action={record && record.getRemoveImageURL}
                method="POST"
                encType="multipart/form-data"
            >
                <input type="hidden" name="image" value={image._id} />

                <button type="submit" className="btn btn-danger btn-xs">
                    <span
                        className="glyphicon glyphicon-remove"
                        aria-hidden="true"
                    />{" "}
                    {gettext("Remove Image")}
                </button>
            </form>
        </div>
    </div>
);

Image.contextTypes = childContextTypes;

const Title = ({title}: {title: string}) => (
    <div
        className="col-xs-12 text-center"
        style={{
            borderBottom: "1px solid lightgrey",
            paddingBottom: 10,
            marginBottom: 15,
        }}
    >
        <h1 className="panel-title">{title}</h1>
    </div>
);

const Images = (props: Props & {title: string}) => {
    const {record, title} = props;

    return (
        <div className="col-xs-12 text-center">
            <div>
                {record &&
                    record.imageModels.map((image, i) => (
                        <Image {...props} key={i} image={image} title={title} />
                    ))}
            </div>
        </div>
    );
};

const ImageForm = (props, {gettext}: Context) => (
    <tr>
        <th className="text-right">{gettext("Add Images")}</th>
        <td>
            <input
                type="file"
                name="images"
                className="form-control"
                multiple
            />
        </td>
    </tr>
);

ImageForm.contextTypes = childContextTypes;

class IDForm extends React.Component<
    Props & {
        curSource: string,
        onValid: (state: boolean) => void,
    },
    {
        unused: boolean,
    },
> {
    constructor(props) {
        super(props);
        this.state = {
            unused: false,
        };
    }

    componentDidUpdate(prevProps) {
        if (prevProps.curSource !== this.props.curSource) {
            this.checkID();
        }
    }

    context: Context;
    currentID: string;
    setUnused(unused) {
        this.props.onValid(unused);
        this.setState({unused});
    }

    checkID() {
        const id = this.currentID;
        const {type, curSource} = this.props;

        if (!id) {
            return this.setUnused(false);
        }

        fetch(`/${type}/${curSource}/${id}/json`, {
            credentials: "same-origin",
        }).then(res => {
            this.setUnused(res.status !== 200);
        });
    }

    handleInput(e: SyntheticInputEvent<HTMLInputElement>) {
        this.currentID = e.target.value;
        this.checkID();
    }

    render() {
        const {record, type, curSource} = this.props;
        const {gettext, options} = this.context;
        const {unused} = this.state;

        if (options.types[type].autoID || (record && record._id)) {
            return null;
        }

        return (
            <tr className={unused ? "has-success" : "has-error"}>
                <th className="text-right">
                    <label className="control-label">{gettext("ID")}</label>
                </th>
                <td>
                    <input
                        type="text"
                        name="id"
                        className="form-control"
                        defaultValue={record && record.id}
                        onInput={e => this.handleInput(e)}
                        disabled={!curSource}
                    />
                </td>
            </tr>
        );
    }
}

IDForm.contextTypes = childContextTypes;

const HiddenSourceID = ({id}: {id: string}) => (
    <tr style={{display: "none"}}>
        <td>
            <input type="hidden" name="source" value={id} />
        </td>
    </tr>
);

const SourceForm = (
    {
        source,
        sources,
        onSourceChange,
    }: Props & {
        onSourceChange: (curSource: string) => void,
    },
    {gettext}: Context,
) => {
    if (source) {
        return <HiddenSourceID id={source} />;
    }

    if (!sources) {
        return null;
    }

    if (sources.length === 1) {
        return <HiddenSourceID id={sources[0]._id} />;
    }

    return (
        <tr>
            <th className="text-right">
                <label className="control-label">{gettext("Source")}</label>
            </th>
            <td>
                <Select
                    name="source"
                    value={sources[0]._id}
                    options={sources.map(source => ({
                        value: source._id,
                        label: source.getFullName,
                    }))}
                    clearable={false}
                    onChange={source => {
                        if (typeof source === "string") {
                            onSourceChange(source);
                        }
                    }}
                />
            </td>
        </tr>
    );
};

SourceForm.contextTypes = childContextTypes;

const TypeEdit = ({
    name,
    type,
    value,
    allValues,
    typeSchema,
}: {
    name: string,
    type: string,
    value?: any,
    allValues: Array<any>,
    typeSchema: ModelType,
}) => {
    const {multiple} = typeSchema;

    if (typeSchema.type === "Dimension") {
        return null;
    } else if (typeSchema.type === "FixedString") {
        const expectedValues = typeSchema.values || {};
        let values = Object.keys(expectedValues).map(id => ({
            id,
            name: expectedValues[id].name,
        }));
        const fixed = values.length !== 0;

        if (!fixed) {
            values = allValues.map(text => ({
                id: text,
                name: text,
            }));
        }

        return (
            <FixedStringEdit
                name={name}
                type={type}
                value={value}
                values={values}
                fixed={fixed}
                multiple={multiple}
            />
        );
    } else if (typeSchema.type === "LinkedRecord") {
        return (
            <LinkedRecordEdit
                name={name}
                type={type}
                value={value}
                multiple={multiple}
                recordType={typeSchema.recordType}
                placeholder={typeSchema.placeholder}
            />
        );
    } else if (typeSchema.type === "Location") {
        return null;
    } else if (typeSchema.type === "Name") {
        return (
            <NameEdit
                name={name}
                type={type}
                value={value}
                multiple={multiple}
                names={allValues}
            />
        );
    } else if (typeSchema.type === "SimpleDate") {
        return <SimpleDateEdit name={name} type={type} value={value} />;
    } else if (typeSchema.type === "SimpleNumber") {
        return (
            <FixedStringEdit
                name={name}
                type={type}
                value={value && String(value)}
            />
        );
    } else if (typeSchema.type === "SimpleString") {
        return (
            <FixedStringEdit
                name={name}
                type={type}
                value={value}
                multiline={typeSchema.multiline}
            />
        );
    } else if (typeSchema.type === "URL") {
        return <FixedStringEdit name={name} type={type} value={value} />;
    } else if (typeSchema.type === "YearRange") {
        return <YearRangeEdit name={name} type={type} value={value} />;
    }

    return null;
};

class Contents extends React.Component<
    Props,
    {
        showPrivate: boolean,
        valid: boolean,
        curSource: string,
    },
> {
    constructor(props) {
        super(props);
        this.state = {
            showPrivate: false,
            valid: props.mode === "edit",
            curSource: props.source || props.sources[0]._id,
        };
    }

    componentDidMount() {
        const {showPrivate} = window.localStorage;
        this.setState({showPrivate}); // eslint-disable-line react/no-did-mount-set-state
    }

    context: Context;
    togglePrivate(e: SyntheticInputEvent<HTMLInputElement>) {
        const showPrivate = e.target.checked;
        if (showPrivate) {
            window.localStorage.showPrivate = showPrivate;
        } else {
            delete window.localStorage.showPrivate;
        }
        this.setState({showPrivate});
    }

    render() {
        const {mode, type, globalFacets, dynamicValues} = this.props;
        const {gettext, options} = this.context;
        const {model} = options.types[type];
        const types = Object.keys(model);
        const canBePrivate = mode === "edit";
        let privateToggle = null;

        const fields = types.map(modelType => {
            const typeSchema = model[modelType];
            const dynamicValue = dynamicValues[modelType];
            const values = ((globalFacets && globalFacets[modelType]) || [])
                .map(bucket => bucket.text)
                .sort();
            const isPrivate =
                canBePrivate && !this.state.showPrivate && typeSchema.private;

            return (
                <tr key={modelType}>
                    <th className="text-right">{typeSchema.title}</th>
                    <td {...(isPrivate ? {"data-private": "true"} : {})}>
                        <TypeEdit
                            name={modelType}
                            type={type}
                            value={dynamicValue}
                            allValues={values}
                            typeSchema={typeSchema}
                        />
                    </td>
                </tr>
            );
        });

        if (canBePrivate) {
            privateToggle = (
                <tr>
                    <th />
                    <td>
                        <label>
                            <input
                                type="checkbox"
                                className="toggle-private"
                                defaultChecked={this.state.showPrivate}
                                onChange={e => this.togglePrivate(e)}
                            />{" "}
                            {gettext("Show private fields.")}
                        </label>
                    </td>
                </tr>
            );
        }

        return (
            <tbody>
                {!options.types[type].noImages && <ImageForm {...this.props} />}
                <SourceForm
                    {...this.props}
                    onSourceChange={curSource => this.setState({curSource})}
                />
                <IDForm
                    {...this.props}
                    curSource={this.state.curSource}
                    onValid={valid => this.setState({valid})}
                />
                {fields}
                {privateToggle}
                <SubmitButtons {...this.props} valid={this.state.valid} />
            </tbody>
        );
    }
}

Contents.contextTypes = childContextTypes;

const SubmitButtons = (props: Props & {valid: boolean}) => {
    const {mode, valid} = props;

    return (
        <tr>
            <th />
            <td>
                <SubmitButton {...props} valid={valid} />
                <span className="pull-right">
                    {mode === "edit" && <DeleteButton {...props} />}
                </span>
            </td>
        </tr>
    );
};

const SubmitButton = (props: Props & {valid: boolean}, {gettext}: Context) => {
    const {mode, valid} = props;
    let buttonText = gettext("Update");

    if (mode === "create") {
        buttonText = gettext("Create");
    } else if (mode === "clone") {
        buttonText = gettext("Clone");
    }

    return (
        <input
            type="submit"
            value={buttonText}
            className="btn btn-primary"
            disabled={!valid}
        />
    );
};

SubmitButton.contextTypes = childContextTypes;

const DeleteButton = (props: Props, {gettext}: Context) => {
    const deleteText = gettext("Are you sure you wish to delete this?");
    return (
        <input
            type="submit"
            name="removeRecord"
            value={gettext("Delete")}
            className="btn btn-danger pull-right"
            onClick={e => {
                // eslint-disable-next-line no-alert
                if (!confirm(deleteText)) {
                    e.preventDefault();
                }
            }}
        />
    );
};

DeleteButton.contextTypes = childContextTypes;

const CloneButton = ({record}: Props, {gettext}: Context) => (
    <div className="row">
        <div
            className="col-xs-12"
            style={{
                marginBottom: 15,
                overflow: "auto",
            }}
        >
            <a
                href={record && record.getCloneURL}
                className="btn btn-primary pull-right"
            >
                {gettext("Clone Record")}
            </a>
        </div>
    </div>
);

CloneButton.contextTypes = childContextTypes;

const EditRecord = (props: Props) => {
    const {record, mode, title} = props;
    const postURL = record
        ? record._id
            ? record.getEditURL
            : record.getCreateURL
        : "";

    return (
        <div>
            {mode === "edit" && <CloneButton {...props} />}
            <div className="row">
                <div className="col-md-12 imageholder">
                    <Title title={title} />
                    <Images {...props} title={title} />
                    <form
                        action={postURL}
                        method="POST"
                        encType="multipart/form-data"
                    >
                        <div className="responsive-table">
                            <table className="table table-hover">
                                <Contents {...props} />
                            </table>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    );
};

module.exports = EditRecord;