import {autobind} from "core-decorators"; import {some, values} from "lodash"; import {action, Lambda, observable, reaction, runInAction} from "mobx"; import * as React from "react"; import {PanelButtonsProps} from "focus-components/panel"; import {applicationStore} from "../application"; import {messageStore} from "../message"; import {Field} from "./field"; import {StoreNode, toFlatValues} from "./store"; import {EntityField} from "./types"; import {createViewModel, ViewModel} from "./view-model"; import { autocompleteSelectFor, AutocompleteSelectOptions, autocompleteTextFor, AutocompleteTextOptions, BaseOptions, displayFor, fieldFor, fieldForWith, FieldOptions, selectFor, SelectOptions, stringFor, textFor, TextOptions } from "./field-helpers"; import {form, loading} from "./style/auto-form.css"; /** Options additionnelles de l'AutoForm. */ export interface AutoFormOptions { /** Pour ajouter une classe particulière sur le formulaire. */ className?: string; /** ViewModel externe de `storeData`, s'il y a besoin d'externaliser le state interne du formulaire. */ entity?: E & ViewModel; /** Par défaut: true */ hasForm?: boolean; /** Défaut: false */ initiallyEditing?: boolean; } /** Config de services à fournir à AutoForm. */ export interface ServiceConfig { delete?: (entity: {}) => Promise; getLoadParams?: () => any[]; load?: (...args: any[]) => Promise<{}>; save: (entity: {}) => Promise<{}>; } /** Classe de base pour un composant Focus avec un formulaire. */ @autobind export abstract class AutoForm> extends React.Component { // On ne peut pas injecter le contexte dans le form (héritage...) donc on va le chercher directement. static contextTypes = {classNames: React.PropTypes.object}; context: {classNames: {[key: string]: {[key: string]: any}}}; readonly entity: E & ViewModel; readonly services: ServiceConfig; readonly storeData: E; @observable errors: Record = {}; @observable fields: Record = {}; @observable isEdit: boolean; @observable isLoading = false; private className: string; private hasForm: boolean; private isCustomEntity: boolean; private loadDisposer?: Lambda; /** * Initialise le formulaire. * @param props Les props du composant. * @param storeData L'EntityStoreData de base du formulaire. * @param services La config de services pour le formulaire ({delete?, getLoadParams, load, save}). * @param options Options additionnelles. */ constructor(props: P, storeData: E, services: ServiceConfig, {entity, className, hasForm, initiallyEditing}: AutoFormOptions = {}) { super(props); this.storeData = storeData; this.services = services; this.entity = entity || createViewModel(storeData); this.isCustomEntity = entity !== undefined; this.isEdit = initiallyEditing || false; this.hasForm = hasForm !== undefined ? hasForm : true; this.className = className || ""; if (services.getLoadParams) { this.loadDisposer = reaction(services.getLoadParams, this.load, {compareStructural: true}); } } componentWillMount() { this.entity.subscribe(); this.load(); } componentWillUnmount() { if (!this.isCustomEntity) { this.entity.unsubscribe(); } if (this.loadDisposer) { this.loadDisposer(); } } /** Change le mode du formulaire. */ @action toggleEdit(isEdit: boolean) { this.isEdit = isEdit; if (isEdit) { applicationStore.changeMode("edit", "consult"); } else { this.entity.reset(); applicationStore.changeMode("consult", "edit"); } } /** Appelle le service de suppression. */ @action async delete() { if (this.services.delete) { this.isLoading = true; await this.services.delete(toFlatValues(this.entity)); messageStore.addSuccessMessage("detail.deleted"); runInAction(() => { this.isLoading = false; this.storeData.clear(); this.onFormDeleted(); }); } } /** Appele le service de chargement */ @action async load() { const {getLoadParams, load} = this.services; if (getLoadParams && load) { const params = getLoadParams(); if (params) { this.isLoading = true; this.storeData.clear(); const data = await load(...params); runInAction(() => { this.storeData.set(data); this.isLoading = false; this.onFormLoaded(); }); } } } /** Appelle le service de sauvegarde. */ @action async save() { if (this.validate()) { this.isLoading = true; try { const data = await this.services.save(toFlatValues(this.entity)); messageStore.addSuccessMessage("detail.saved"); runInAction(() => { this.isLoading = false; this.isEdit = false; this.storeData.set(data); this.onFormSaved(); }); } catch (e) { runInAction(() => { this.isLoading = false; if (e.fields) { this.errors = e.fields || {}; } }); } } } onFormDeleted() { // A éventuellement surcharger. } onFormLoaded() { // A éventuellement surcharger. } onFormSaved() { // A éventuellement surcharger. } /** Valide les différents champs du formulaire. */ @action validate() { for (const field in this.fields) { this.fields[field]!.showError = true; } return !some(values(this.fields), field => field && field.error); } /** Récupère les props à fournir à un Panel pour relier ses boutons au formulaire. */ getPanelButtonProps(): PanelButtonsProps { return { editing: this.isEdit, getUserInput: () => ({}), // Pas besoin de passer l'input il est déjà dans le state du formulaire. save: this.save, toggleEdit: this.toggleEdit, }; } abstract renderContent(): React.ReactElement | null; render() { const contextClassName = this.context && this.context.classNames && this.context.classNames["form"] || ""; if (this.hasForm) { return (
{ e.preventDefault(); this.save(); }} >
{this.renderContent()}
); } else { return this.renderContent(); } } /** * Crée un champ de type AutocompleteSelect. * @param field La définition de champ. * @param options Les options du champ. */ autocompleteSelectFor(field: EntityField, options: AutocompleteSelectOptions) { return autocompleteSelectFor(field, this.setFieldOptions(field, options)); } /** * Crée un champ de type AutocompleteText. * @param field La définition de champ. * @param options Les options du champ. */ autocompleteTextFor(field: EntityField, options: AutocompleteTextOptions) { return autocompleteTextFor(field, this.setFieldOptions(field, options)); } /** * Crée un champ standard en lecture seule. * @param field La définition de champ. * @param options Les options du champ. */ displayFor(field: EntityField, options?: BaseOptions & {[key: string]: any}) { return displayFor(field, options); } /** * Crée un champ standard. * @param field La définition de champ. * @param options Les options du champ. */ fieldFor(field: EntityField, options: BaseOptions & {[key: string]: any} = {}) { return fieldFor(field, this.setFieldOptions(field, options)); } /** * Crée un champ avec des composants personnalisés. * @param field La définition de champ. * @param options Les options du champ. */ fieldForWith(field: EntityField, options: FieldOptions & DisplayProps & FieldProps & InputProps) { return fieldForWith(field, this.setFieldOptions(field, options)); } /** * Crée un champ avec résolution de référence. * @param field La définition de champ. * @param listName Le nom de la liste de référence. * @param options Les options du champ. */ selectFor(field: EntityField, values: {code?: T, id?: T}[], options: SelectOptions = {}) { return selectFor(field, values, this.setFieldOptions(field, options)); } /** * Récupère le texte correspondant à un champ. * @param field La définition de champ. * @param options Les options du champ. */ stringFor(field: EntityField, options: TextOptions = {}) { return stringFor(field, options); } /** * Affiche un champ sous format texte. * @param field La définition de champ. * @param options Les options du champ. */ textFor(field: EntityField, options: TextOptions = {}) { return textFor(field, options); } private setFieldOptions(field: EntityField, options: {[key: string]: any}) { options["ref"] = (f: Field) => this.fields[field.$entity.translationKey] = f; options["error"] = this.errors[field.$entity.translationKey]; if (options["isEdit"] === undefined) { options["isEdit"] = this.isEdit; } if ((this.entity as any)[field.$entity.name]) { options["onChange"] = options["onChange"] || action((value: any) => (this.entity as any)[field.$entity.name].value = value ); } return options as any; } }