@prisma-cms/front-editor
Version:
650 lines (519 loc) • 16.4 kB
JavaScript
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react/forbid-foreign-prop-types */
import React from 'react'
import PropTypes from 'prop-types'
import EditorComponent from '../../../EditorComponent'
import { EditableObject as ApolloEditableObject } from 'apollo-cms'
import { ObjectContext } from '../../Connectors/Connector/ListView'
import { EditableObjectContext } from '../../../context'
import Query from '../../Connectors/Query'
// import { graphql } from 'react-apollo';
import gql from 'graphql-tag'
import { parse } from 'graphql'
import pathToRegexp from 'path-to-regexp'
import IconButton from 'material-ui/IconButton'
import CircularProgress from 'material-ui/Progress/CircularProgress'
import DeleteIcon from 'material-ui-icons/Delete'
// TODO Fix for new EditableObject
export class Editable extends ApolloEditableObject {
static propTypes = {
// eslint-disable-next-line react/forbid-foreign-prop-types
...ApolloEditableObject.propTypes,
show_header: PropTypes.bool.isRequired,
extendQuery: PropTypes.func.isRequired,
getQueryNameFromQuery: PropTypes.func.isRequired,
query_components: PropTypes.array.isRequired,
DeleteIcon: PropTypes.func.isRequired,
deletable_object: PropTypes.bool.isRequired,
on_delete_redirect_url: PropTypes.string,
data: PropTypes.object,
}
static defaultProps = {
...ApolloEditableObject.defaultProps,
show_header: true,
DeleteIcon,
deletable_object: false,
}
constructor(props) {
super(props)
this.delete = this.delete.bind(this)
}
updateObject(data) {
for (const i in data) {
const value = data[i]
if (value === '') {
data[i] = null
}
}
return super.updateObject(data)
}
async mutate(props, method) {
/**
Prepare Mutation
*/
const {
query_components: children,
getQueryNameFromQuery,
extendQuery,
} = this.props
const queries = {}
const { id: objectId } = this.getObject() || {}
children &&
children.length &&
children
.filter((n) => n)
.map((n) => {
const { type, props } = n
if (type === Query) {
const { query } = (props && props.props) || {}
if (query) {
const queryName = getQueryNameFromQuery(query)
if (queryName) {
queries[queryName] = query
}
}
}
return null
})
if (!method) {
method = objectId ? 'update' : 'create'
}
let mutation = queries && method ? queries[method] : null
if (!mutation) {
const error = new Error('Can not get mutation')
return this.addError(error)
}
/**
Eof Prepare Mutation
*/
try {
const extendedQuery = extendQuery(mutation)
mutation = gql(extendedQuery)
} catch (error) {
console.error(error)
}
return super.mutate({
...props,
mutation,
// mutation: extendedQuery(mutation),
})
}
renderEditableView() {
const { children } = this.props
return children
}
renderDefaultView() {
const { children } = this.props
return children
}
renderHeader() {
const { show_header } = this.props
return show_header ? super.renderHeader() : null
}
getButtons() {
const { id: objectId } = this.getObject() || {}
const buttons = super.getButtons() || []
if (this.inEditMode() && objectId) {
buttons.push(this.renderDeleteButton())
}
return buttons
}
renderDeleteButton() {
const { DeleteIcon, deletable_object } = this.props
const { loading } = this.state
return deletable_object ? (
<IconButton key="delete" onClick={this.delete} disabled={loading}>
{loading ? <CircularProgress /> : <DeleteIcon />}
</IconButton>
) : null
}
delete() {
const { id: objectId } = this.getObject() || {}
if (objectId) {
if (window.confirm('Удалить данный объект?')) {
return this.mutate(
{
variables: {
where: {
id: objectId,
},
},
},
'delete'
).then((r) => {
const { on_delete_redirect_url } = this.props
if (on_delete_redirect_url) {
const {
router: { history },
} = this.context
history.push(decodeURIComponent(on_delete_redirect_url))
}
return r
})
}
}
}
render() {
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_dirty: _dirty_null,
...other
} = this.props
const { loading, _dirty, errors, notifications } = this.state
const fieldErrors = {}
if (errors && errors.length) {
errors.map(({ key, message }) => {
fieldErrors[key] = message
return null
})
}
const object = this.getObjectWithMutations()
return (
<EditableObjectContext.Provider
value={{
// getCacheKey: this.getCacheKey,
updateObject: this.updateObject,
getEditor: this.getEditor,
inEditMode: this.inEditMode(),
canEdit: this.canEdit(),
getObjectWithMutations: this.getObjectWithMutations,
getObject: this.getObject,
getButtons: this.getButtons,
save: this.save,
mutate: this.mutate,
loading,
_dirty,
errors,
fieldErrors,
notifications,
...other,
}}
>
<ObjectContext.Provider
value={{
object: this.getObjectWithMutations(),
}}
>
{super.render()}
</ObjectContext.Provider>
</EditableObjectContext.Provider>
)
}
}
class EditableObject extends EditorComponent {
static propTypes = {
...EditorComponent.propTypes,
deletable_object: PropTypes.bool.isRequired,
/**
* Если новый объект создается как дочерний от другого объекта,
* то можно указать имя родителя, чтобы сформировать конструкцию [parent_name]: {connect: parent_id}.
* Важно! Если выставить это свойство, будет создан именно новый объект,
* даже если это имеющийся уже объект (объект будет перетерт).
*/
create_as_a_child_of: PropTypes.string,
/**
* При рендеринге создает новый ключ для рендеринга Editable.
* Это удобно, когда надо обновить данные
* при сохранении нового объекта без перезагрузки страницы.
* Важно! В режиме редактирования шаблонов передача меняемого key
* ломает вывод элементов, поэтому этот параметр следует использовать очень осторожно.
*/
random_key: PropTypes.bool,
}
static defaultProps = {
...EditorComponent.defaultProps,
style: {
...EditorComponent.defaultProps.style,
flexBasis: '100%',
},
/**
* УРЛ, куда редиректить при создании нового объекта
*/
on_create_redirect_url: undefined,
/**
* Куда редиректить при успешном удалении элемента
*/
on_delete_redirect_url: undefined,
cache_key: undefined,
cache_key_prefix: undefined,
new_object_cache_key: undefined,
show_header: true,
hide_wrapper_in_default_mode: true,
deletable_object: false,
create_as_a_child_of: undefined,
random_key: false,
}
static Name = 'EditableObject'
static help_url =
'https://front-editor.prisma-cms.com/topics/editableobject.html'
constructor(props) {
super(props)
this.state = {
...this.state,
object_key: Math.random(),
}
this.onCreateObject = this.onCreateObject.bind(this)
this.onSaveObject = this.onSaveObject.bind(this)
}
onBeforeDrop = () => {
return
}
renderPanelView(content) {
return super.renderPanelView(
content || <div className={'panelEditableObject'}>EditableObject</div>
)
}
getEditableClass() {
return Editable
}
/**
* Позволяет переопределить редактируемый объект,
* например, чтобы создавать новый внутри имеющегося
*/
prepareEditableObject(object) {
const { create_as_a_child_of } = this.getComponentProps(this)
return create_as_a_child_of ? {} : object
}
/**
* Этот метод не модифицирует сам редактируемые объект,
* а только формирует параметры для класса Editable
*/
prepareObject(context) {
return {
_dirty: this.getDirty(context),
}
}
getDirty(context) {
let { _dirty } = context
const { object } = context
const { create_as_a_child_of } = this.getComponentProps(this)
const { id: objectId } = object || {}
if (create_as_a_child_of) {
_dirty = _dirty
? {
..._dirty,
}
: {}
Object.assign(_dirty, {
[create_as_a_child_of]: {
connect: {
id: objectId,
},
},
})
}
return _dirty
}
onCreateObject(result) {
const { on_create_redirect_url } = this.getComponentProps(this)
if (on_create_redirect_url) {
const { response } = result.data || {}
const { data: object } = response || {}
if (object) {
const toPath = pathToRegexp.compile(on_create_redirect_url)
try {
const url = toPath(object, { noValidate: true })
if (url) {
const {
router: { history },
} = this.context
history.push(decodeURIComponent(url))
}
} catch (error) {
console.error(error)
}
}
}
return
}
onSaveObject(result) {
const { random_key } = this.getComponentProps(this)
if (random_key) {
this.setState({
object_key: Math.random(),
})
}
return this.onCreateObject(result)
}
// onUpdateObject(result) {
// return result;
// }
/**
* Расширяем запрос
*/
extendQueryBind = (Query) => this.extendQuery(Query)
extendQuery(Query) {
const { schema } = this.context
if (Query && schema) {
/**
* Проходим запрос на предмет директив в фрагментах
*/
const parsedQuery = parse(Query)
if (parsedQuery && schema) {
const { types } = schema
const { definitions } = parsedQuery
if (definitions && definitions.length) {
definitions.reduceRight((current, definition) => {
const {
kind,
directives,
selectionSet: {
loc: { end },
},
typeCondition,
} = definition
if (kind === 'FragmentDefinition' && typeCondition) {
const needAutoloadFields =
directives &&
directives.find(
(n) =>
n && n.name && n.name.value === 'prismaCmsFragmentAllFields'
)
? true
: false
if (needAutoloadFields) {
const {
// kind,
name: { value: type },
} = typeCondition
if (type) {
const field = types.find((n) => {
const { kind, name } = n
return kind === 'OBJECT' && name === type
})
if (field) {
let { fields } = field
fields = fields.filter((n) => {
return n && n.name && this.isScalar(n) ? true : false
})
/**
* Если были получены скалярные поля,
* добавляем их в запрос
*/
if (fields.length) {
const fieldsList =
'\n' + fields.map(({ name }) => name).join('\n') + '\n'
const position = end - 1
Query = [
Query.slice(0, position),
fieldsList,
Query.slice(position),
].join('')
}
}
}
}
}
return current
}, [])
}
}
}
return Query
}
isScalar(field) {
const {
type: { kind, ofType },
} = field
if (kind === 'SCALAR') {
return true
} else if (kind === 'ENUM') {
return true
} else if ((kind === 'NON_NULL' || kind === 'LIST') && ofType) {
return this.isScalar({
type: ofType,
})
} else {
return false
}
}
getQueryNameFromQuery(query) {
try {
const parsedSchema = parse(query)
if (parsedSchema) {
const { definitions } = parsedSchema
if (definitions) {
const OperationDefinition = definitions.find(
(n) => n.kind === 'OperationDefinition'
)
if (OperationDefinition) {
const { value } = OperationDefinition.name || {}
return value
}
}
}
} catch (error) {
console.error(error)
}
}
renderChildren() {
// const {
// inEditMode,
// } = this.getEditorContext();
const children = super.renderChildren()
const {
on_create_redirect_url,
props,
data,
components,
style,
cache_key,
cache_key_prefix,
new_object_cache_key,
object,
random_key,
// _dirty,
...other
} = this.getComponentProps(this)
const { object_key } = this.state
const Editable = this.getEditableClass()
return (
<ObjectContext.Consumer key="editable_object">
{(context) => {
const { object, loading } = context
/**
Если объекта нет и еще выполняется загрузка,
прерываем рендерер.
Иначе объект будет инициализирован как новый, то есть в режиме редактирования со свойством _dirty
*/
if (!object && loading) {
return null
}
/**
* Здесь есть возможность переопределить объект
*/
const editableObject = this.prepareEditableObject(object)
const { id: objectId } = editableObject || {}
const cacheKey =
cache_key !== undefined
? cache_key
: new_object_cache_key && !objectId
? new_object_cache_key
: undefined
const cacheKeyPrefix = cache_key_prefix
return (
<Editable
key={random_key ? object_key : undefined}
// data={{
// object: object || {},
// }}
object={editableObject}
extendQuery={this.extendQueryBind}
// extendQuery={(query) => this.extendQuery(query)}
getQueryNameFromQuery={this.getQueryNameFromQuery}
query_components={children}
onSave={!objectId ? this.onSaveObject : null}
cacheKey={cacheKey}
cacheKeyPrefix={cacheKeyPrefix}
{...this.prepareObject(context)}
{...other}
>
{children}
</Editable>
)
}}
</ObjectContext.Consumer>
)
}
}
export default EditableObject