import React, { Fragment } from 'react' // import PropTypes from 'prop-types' import TextField from 'material-ui/TextField' import Typography from 'material-ui/Typography' import IconButton from 'material-ui/IconButton' import EditIcon from 'material-ui-icons/ModeEdit' import ResetIcon from 'material-ui-icons/Restore' import Save from 'material-ui-icons/Save' import CircularProgress from 'material-ui/Progress/CircularProgress' // import View from '..' import PrismaCmsComponent, { PrismaCmsComponentError, } from '@prisma-cms/component' import { EditableObjectEditorProps, // EditableObjectMutateProps, EditableObjectProps, EditableObjectState, EditableObjectSaveResult, EditableObjectProcessorResponse, } from './interfaces' export * from './interfaces' const SaveIcon: React.ComponentType = () => { return ( ) } export class EditableObject< P extends EditableObjectProps = EditableObjectProps, S extends EditableObjectState = EditableObjectState > extends PrismaCmsComponent { // static propTypes = { // ...View.propTypes, // mutate: PropTypes.func, // mutation: PropTypes.object, // _dirty: PropTypes.object, // errorDelay: PropTypes.number.isRequired, // SaveIcon: PropTypes.func.isRequired, // ResetIcon: PropTypes.func.isRequired, // EditIcon: PropTypes.func.isRequired, // cacheKey: PropTypes.string, // cacheKeyPrefix: PropTypes.string.isRequired, // } static defaultProps = { ...PrismaCmsComponent.defaultProps, errorDelay: 5000, SaveIcon, ResetIcon, EditIcon, cacheKeyPrefix: 'item_', } constructor(props: P) { super(props) this.getObjectWithMutations = this.getObjectWithMutations.bind(this) this.getMutation = this.getMutation.bind(this) this.save = this.save.bind(this) this.startEdit = this.startEdit.bind(this) this.getEditor = this.getEditor.bind(this) this.inEditMode = this.inEditMode.bind(this) this.getButtons = this.getButtons.bind(this) this.state = { ...this.state, inEditMode: false, notifications: [], loading: false, } } UNSAFE_componentWillMount(): void { this.initCache() // deprecated // super.UNSAFE_componentWillMount && super.UNSAFE_componentWillMount() } getStorage = () => { return this.context.localStorage ?? global.localStorage } initCache(): void { const { _dirty = null } = this.props const cache = this.getCache() Object.assign(this.state, { _dirty: _dirty || cache ? { ..._dirty, ...cache, } : undefined, }) } getCacheKey(): string | null | undefined { const { cacheKey, cacheKeyPrefix } = this.props const id = this.getObject()?.id return cacheKey !== undefined ? cacheKey : id ? `${cacheKeyPrefix}${id}` : null } setCache(data: Record | null): boolean { const localStorage = this.getStorage() if (!localStorage) { return false } let cacheData const key = this.getCacheKey() if (key) { if (data) { try { cacheData = JSON.stringify(data) } catch (error) { console.error(error) return false } localStorage.setItem(key, cacheData) } else { localStorage.removeItem(key) } } return true } getCache(): Record | null | undefined { const localStorage = this.getStorage() let cacheData const key = this.getCacheKey() if (key && localStorage) { try { cacheData = localStorage.getItem(key) if (cacheData) { cacheData = JSON.parse(cacheData) } } catch (error) { console.error(error) return } } return cacheData } clearCache(): void { const localStorage = this.getStorage() const key = this.getCacheKey() if (key && localStorage) { localStorage.removeItem(key) } } onSave = (result: EditableObjectSaveResult): (() => void) => { // let callback = () => undefined const { onSave } = this.props const callback = async () => { await this.resetStore() if (onSave) { onSave(result) } return undefined } return callback } async save(): Promise { const { _dirty } = this.state return await this.saveObject(_dirty) .then((result: EditableObjectSaveResult) => { if (result && !(result instanceof Error)) { this.clearCache() this.setState( { _dirty: null, inEditMode: false, }, this.onSave(result) ) } return result }) .catch((error: Error) => { console.error('Save error', error) return error }) } async saveObject(data: P['_dirty']): Promise { const options = this.getMutation(data) return this.mutate(options) } async mutate(props: Record): Promise { const { loading } = this.state // const { client } = this.context if (loading) { return } const { mutate: mutateProp, mutation } = { ...this.props, ...props, } let mutate = mutateProp return new Promise((resolve, reject) => { if (props && mutation && !props.mutation) { props.mutation = mutation } if (!mutate) { const { client } = this.context mutate = client.mutate } if (!mutate) { throw new Error('Mutate is not defined') } this.setState( { loading: true, }, async () => { const newState = { loading: false, } let result: EditableObjectSaveResult if (mutate) { result = await mutate(props) .then(async (result) => { if (!(result instanceof Error)) { // const { data: resultData } = result || {} // const { response } = resultData || {} const resultData: EditableObjectProcessorResponse = result?.data const response = resultData.response // const { success: successProp, message, errors = null } = response; let success = response?.success const message = response?.message const errors = response?.errors?.map(({ key, message, ...other }) => { const error: PrismaCmsComponentError = new Error(message) /** * For Fields compability */ Object.assign(error, { key, name: key, ...other, }) return error }) ?? [] Object.assign(newState, { errors, }) if (success === undefined) { success = true } if (!success) { this.addError(message || 'Request error') return reject(result) } // else { // await this.resetStore() // } } return result }) .catch((error) => { const message = (error.message && error.message.replace(/^GraphQL error: */, '')) || '' this.addError(message) return error }) } this.setState(newState, () => { return resolve(result) }) return } ) }) } resetStore = async (): Promise => { const { apiClientResetStore } = this.context if (apiClientResetStore) { return apiClientResetStore.call(this) } else { const { client } = this.context // if (!client.queryManager.fetchQueryRejectFns.size) { if (!client['queryManager'].fetchCancelFns.size) { await client.resetStore().catch(console.error) } } } getMutation(data: P['_dirty']) { const variables = this.getMutationVariables(data) return { variables, } } getMutationVariables(data: P['_dirty']): { where?: { id: string } data: P['_dirty'] } { const object = this.getObjectWithMutations() const id = object?.id const where = id ? { id } : undefined return { where, data, } } startEdit(): void { this.setState({ inEditMode: true, }) } // resetEdit = (): Promise => { // return new Promise((resolve) => { // this.clearCache() // this.setState( // { // inEditMode: false, // _dirty: null, // }, // () => { // resolve(); // } // ) // }) // } // resetEdit = (): Promise => { // return new Promise((resolve) => { // this.clearCache() // this.setState( // { // inEditMode: false, // _dirty: null, // }, // () => { // resolve(); // } // ) // }) // } resetEdit = () => { // this.clearCache() this.updateObject(null) // this.setState( // { // inEditMode: false, // _dirty: null, // }, // () => { // resolve(); // } // ) } inEditMode(): boolean { const { inEditMode, _dirty } = this.state return inEditMode || _dirty ? true : false } isDirty(): boolean { return this.state._dirty ? true : false } // updateObject(data: P['_dirty']): void { // const { _dirty = {} } = this.state // const localStorage = this.getStorage() // const newData = Object.assign({ ..._dirty }, data) // const key = this.getCacheKey() // if (key && newData && localStorage) { // localStorage.setItem(this.getCacheKey(), JSON.stringify(newData)) // } // /** // * Переделал, так как ТС ругался на ридонли, // * но надо будет понаблюдать. Ранее так делалось, чтобы получить изменения // * в стейте сразу же. Вероятно надо будет как-то вернуть. // */ // // this.state._dirty = newData // // this.forceUpdate() // this.setState({ // _dirty: newData, // }) // } prepareDirty(data: P['_dirty']) { const newDirty = super.prepareDirty(data) return newDirty } // prepareNewState(state: { // _dirty: P["_dirty"] // }) { prepareNewState( state: Pick | S | null ): Pick | S | null { // TODO Fix types const newState = super.prepareNewState(state) as S | null if (newState) { if (newState._dirty) { const key = this.getCacheKey() const localStorage = this.getStorage() if (key && localStorage) { localStorage.setItem( this.getCacheKey(), JSON.stringify(newState._dirty) ) } } else { newState.inEditMode = false this.clearCache() } } return newState } getEditor(props: EditableObjectEditorProps): React.ReactNode { const { Editor = TextField, name, helperText, onFocus, fullWidth = true, label, ...other } = props const object = this.getObjectWithMutations() if (!object) { return null } const value = object[name] || '' const errors = this.state.errors // const error = errors ? errors.find((n) => n.key === name) : '' const error = errors ? errors.find((n) => n.name === name) : '' const helperTextMessage = (error && error.message) || helperText return Editor ? ( { // if (error) { // const index = errors.indexOf(error) // if (index !== -1) { // errors.splice(index, 1) // this.setState({ // errors, // }) // } // } // return onFocus ? onFocus(event) : null // }} fullWidth={fullWidth} {...other} // TODO: call both onFocus and this.onFocusBind onFocus={onFocus || this.onFocusBind} /> ) : null } getTextField( props: EditableObjectEditorProps = { name: '', Editor: TextField } ): React.ReactNode { props = { Editor: TextField, autoComplete: 'off', ...props, } return this.getEditor(props) } // TODO add generic // getObjectWithMutations(): Partial | null | undefined { getObjectWithMutations(): P['object'] | undefined { const object = this.getObject() // if (!object) { // return object // } const { _dirty } = this.state if (_dirty) { const draftObject: typeof object = { ...object } return Object.assign(draftObject, _dirty) } else { return object } } getButtons(): Array | React.ReactNode { const inEditMode = this.inEditMode() const isDirty = this.isDirty() const buttons = [] if (this.canEdit()) { if (inEditMode) { buttons.push(this.renderResetButton()) if (isDirty) { buttons.push(this.renderSaveButton()) } } else { buttons.push(this.renderEditButton()) } } return buttons && buttons.length ? buttons : null } renderResetButton(): React.ReactNode { const ResetIcon = this.props.ResetIcon as | React.ElementType | null | undefined return ResetIcon ? ( ) : null } renderSaveButton(): React.ReactNode { const SaveIcon = this.props.SaveIcon as React.ElementType | null | undefined const { loading } = this.state return ( {loading ? : SaveIcon ? : null} ) } renderEditButton(): React.ReactNode { const EditIcon = this.props.EditIcon as React.ElementType | null | undefined return EditIcon ? ( ) : null } getTitle() { return this.getObjectWithMutations()?.name } renderHeader(): React.ReactNode { return ( {this.getTitle()} {this.getButtons()} ) } renderEmpty(): React.ReactNode { return null } renderDefaultView(): React.ReactNode { return null } renderEditableView(): React.ReactNode { return null } closeError(error: Record): void { Object.assign(error, { open: false, }) this.forceUpdate() } // TODO: Move in @prisma-cms/component with checking _dirty canEdit() { const object = this.getObjectWithMutations() ?? null if (object) { const { id: objectId, CreatedBy } = object const { id: currentUserId, sudo } = this.getCurrentUser() || {} const { id: createdById } = CreatedBy || {} if (objectId) { if (sudo || (createdById && createdById === currentUserId)) { return true } } else { return true } } return false } render(): React.ReactNode { const { loading } = this.props let output = null // const object = this.getObject() const object = this.getObjectWithMutations() if (!object) { if (loading) { return null } else { output = this.renderEmpty() } } else { const inEditMode = this.canEdit() && this.inEditMode() let content = null if (inEditMode) { content = this.renderEditableView() } else { content = this.renderDefaultView() } output = ( {this.renderHeader()} {content} ) } return ( {output} {this.renderErrors()} ) } } /** * For backward compatibility */ export const EditableView = EditableObject export default EditableObject