From b263e04408e559e563c295d26a782dd6700fb211 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 6 Jul 2020 22:58:31 -0400 Subject: [PATCH] WIP - styles Globals --- demo/collections/Admin.js | 2 +- demo/globals/GlobalWithPolicies.js | 1 - src/client/components/views/Global/Default.js | 147 ++++++++++++++ src/client/components/views/Global/index.js | 90 +++++---- src/client/components/views/Global/index.scss | 186 ++++++++++++++++++ .../views/collections/Edit/Default.js | 5 +- src/collections/operations/update.js | 2 +- src/globals/buildModel.js | 15 +- src/globals/init.js | 1 - src/globals/operations/findOne.js | 12 +- src/globals/operations/update.js | 22 +-- src/globals/requestHandlers/findOne.js | 5 +- src/globals/requestHandlers/update.js | 6 +- src/globals/routes.js | 3 +- src/utilities/getPropSubset.js | 1 - src/utilities/secureConfig.js | 33 ---- 16 files changed, 418 insertions(+), 113 deletions(-) create mode 100644 src/client/components/views/Global/Default.js create mode 100644 src/client/components/views/Global/index.scss delete mode 100644 src/utilities/getPropSubset.js delete mode 100644 src/utilities/secureConfig.js diff --git a/demo/collections/Admin.js b/demo/collections/Admin.js index d0b5679b1b..4f6b2d631e 100644 --- a/demo/collections/Admin.js +++ b/demo/collections/Admin.js @@ -21,7 +21,7 @@ module.exports = { admin: () => true, }, auth: { - tokenExpiration: 300, + tokenExpiration: 7200, useAPIKey: true, secureCookie: process.env.NODE_ENV === 'production', }, diff --git a/demo/globals/GlobalWithPolicies.js b/demo/globals/GlobalWithPolicies.js index a7c721989a..ad43109be8 100644 --- a/demo/globals/GlobalWithPolicies.js +++ b/demo/globals/GlobalWithPolicies.js @@ -12,7 +12,6 @@ module.exports = { name: 'title', label: 'Site Title', type: 'text', - localized: true, maxLength: 100, required: true, }, diff --git a/src/client/components/views/Global/Default.js b/src/client/components/views/Global/Default.js new file mode 100644 index 0000000000..ff61fa946a --- /dev/null +++ b/src/client/components/views/Global/Default.js @@ -0,0 +1,147 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import format from 'date-fns/format'; +import config from 'payload/config'; +import Eyebrow from '../../elements/Eyebrow'; +import Form from '../../forms/Form'; +import PreviewButton from '../../elements/PreviewButton'; +import FormSubmit from '../../forms/Submit'; +import RenderFields from '../../forms/RenderFields'; +import CopyToClipboard from '../../elements/CopyToClipboard'; +import * as fieldTypes from '../../forms/field-types'; +import LeaveWithoutSaving from '../../modals/LeaveWithoutSaving'; + +import './index.scss'; + +const { serverURL, routes: { api } } = config; + +const baseClass = 'global-edit'; + +const DefaultGlobalView = (props) => { + const { + global, data, onSave, permissions, + } = props; + + const { + slug, + fields, + preview, + label, + } = global; + + const apiURL = `${serverURL}${api}/globals/${slug}`; + const action = `${serverURL}${api}/globals/${slug}`; + const hasSavePermission = permissions?.update?.permission; + + return ( +
+
+
+ + +
+
+

+ Edit + {' '} + {label} +

+
+ (!field.position || (field.position && field.position !== 'sidebar'))} + fieldTypes={fieldTypes} + fieldSchema={fields} + initialData={data} + customComponentsPath={`${slug}.fields.`} + /> +
+
+
+
+ + {hasSavePermission && ( + Save + )} +
+ {data && ( +
+ + API URL + {' '} + + + + {apiURL} + +
+ )} +
+ field.position === 'sidebar'} + position="sidebar" + fieldTypes={fieldTypes} + fieldSchema={fields} + initialData={data} + customComponentsPath={`${slug}.fields.`} + /> +
+ {data && ( +
    + {data && ( + <> + {data.updatedAt && ( +
  • +
    Last Modified
    +
    {format(new Date(data.updatedAt), 'MMMM do yyyy, h:mma')}
    +
  • + )} + + )} +
+ )} +
+
+
+ ); +}; + +DefaultGlobalView.defaultProps = { + data: undefined, +}; + +DefaultGlobalView.propTypes = { + global: PropTypes.shape({ + label: PropTypes.string.isRequired, + slug: PropTypes.string, + fields: PropTypes.arrayOf(PropTypes.shape({})), + preview: PropTypes.func, + }).isRequired, + data: PropTypes.shape({ + updatedAt: PropTypes.string, + }), + onSave: PropTypes.func.isRequired, + permissions: PropTypes.shape({ + update: PropTypes.shape({ + permission: PropTypes.bool, + }), + fields: PropTypes.shape({}), + }).isRequired, +}; + +export default DefaultGlobalView; diff --git a/src/client/components/views/Global/index.js b/src/client/components/views/Global/index.js index 5c372647dc..73a1bcdff7 100644 --- a/src/client/components/views/Global/index.js +++ b/src/client/components/views/Global/index.js @@ -1,68 +1,76 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; +import { useHistory, useLocation } from 'react-router-dom'; import config from 'payload/config'; import { useStepNav } from '../../elements/StepNav'; import usePayloadAPI from '../../../hooks/usePayloadAPI'; -import Form from '../../forms/Form'; -import RenderFields from '../../forms/RenderFields'; -import * as fieldTypes from '../../forms/field-types'; +import { useUser } from '../../data/User'; -const { - serverURL, - routes: { - admin, - api, - }, -} = config; +import RenderCustomComponent from '../../utilities/RenderCustomComponent'; +import DefaultGlobal from './Default'; -const baseClass = 'global-edit'; +const { serverURL, routes: { admin, api } } = config; -const Global = (props) => { - const { global: { slug, label, fields } } = props; +const GlobalView = (props) => { + const { state: locationState } = useLocation(); + const history = useHistory(); const { setStepNav } = useStepNav(); + const { permissions } = useUser(); + + const { global } = props; + + const { + slug, + label, + } = global; + + const onSave = (json) => { + history.push(`${admin}/globals/${global.slug}`, { + status: { + message: json.message, + type: 'success', + }, + data: json.doc, + }); + }; const [{ data }] = usePayloadAPI( `${serverURL}${api}/globals/${slug}`, { initialParams: { 'fallback-locale': 'null' } }, ); + const dataToRender = locationState?.data || data; + useEffect(() => { - setStepNav([{ - url: `${admin}/globals/${slug}`, + const nav = [{ label, - }]); - }, [setStepNav, slug, label]); + }]; + + setStepNav(nav); + }, [setStepNav, label]); + + const globalPermissions = permissions?.[slug]; return ( -
-
-

- Edit - {' '} - {label} -

-
-
- - -
+ ); }; -Global.propTypes = { +GlobalView.propTypes = { global: PropTypes.shape({ - label: PropTypes.string, - slug: PropTypes.string, + label: PropTypes.string.isRequired, + slug: PropTypes.string.isRequired, fields: PropTypes.arrayOf(PropTypes.shape({})), }).isRequired, }; -export default Global; +export default GlobalView; diff --git a/src/client/components/views/Global/index.scss b/src/client/components/views/Global/index.scss new file mode 100644 index 0000000000..495816a66c --- /dev/null +++ b/src/client/components/views/Global/index.scss @@ -0,0 +1,186 @@ +@import '../../../scss/styles.scss'; + +.global-edit { + width: 100%; + + &__form { + display: flex; + align-items: stretch; + } + + &__main { + min-width: 0; + } + + &__header { + h1 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + &__collection-actions { + list-style: none; + margin: 0; + padding: base(1.5) 0 base(.5); + display: flex; + + li { + margin-right: base(.75); + } + } + + &__edit { + background: white; + padding: base(5) base(6) base(6); + } + + &__sidebar { + padding-top: base(3); + padding-bottom: base(1); + width: base(22); + display: flex; + flex-direction: column; + min-width: 0; + } + + &__collection-actions, + &__document-actions, + &__meta, + &__sidebar-fields, + &__api-url { + padding-left: base(1.5); + } + + &__document-actions { + @include blur-bg; + position: sticky; + top: 0; + z-index: 2; + padding-right: $baseline; + } + + &__document-actions--with-preview { + display: flex; + + > * { + width: calc(50% - #{base(.5)}); + } + + > *:first-child { + margin-right: base(.5); + } + + > *:last-child { + margin-left: base(.5); + } + + .form-submit { + .btn { + width: 100%; + padding-left: base(2); + padding-right: base(2); + } + } + } + + &__api-url { + margin-bottom: base(1.5); + padding-right: base(1.5); + + a { + display: block; + overflow: hidden; + text-overflow: ellipsis; + } + } + + &__meta { + margin: 0; + padding-top: $baseline; + list-style: none; + + li { + margin-bottom: base(.5); + } + } + + &__label { + color: $color-gray; + } + + &__collection-actions, + &__api-url { + a, button { + cursor: pointer; + font-weight: 600; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } + + &--is-editing { + .collection-edit__sidebar { + padding-top: 0; + } + } + + @include large-break { + &__edit { + padding: base(3.5); + } + } + + @include mid-break { + &__sidebar { + width: unset; + } + + &__form { + display: block; + } + + &__edit { + padding: $baseline; + margin-bottom: $baseline; + } + + &__document-actions { + position: fixed; + bottom: 0; + left: 0; + right: 0; + top: auto; + } + + &__document-actions, + &__meta, + &__sidebar-fields, + &__api-url { + padding-left: $baseline; + } + + &__api-url { + margin-bottom: base(.5); + } + + &__collection-actions { + margin-top: base(.5); + padding-left: $baseline; + padding-bottom: 0; + order: 1; + + li { + margin: 0 base(.5) 0 0; + } + } + + &__sidebar { + padding-bottom: base(4); + } + } +} diff --git a/src/client/components/views/collections/Edit/Default.js b/src/client/components/views/collections/Edit/Default.js index d863c9934b..7ebdcc01f3 100644 --- a/src/client/components/views/collections/Edit/Default.js +++ b/src/client/components/views/collections/Edit/Default.js @@ -39,7 +39,7 @@ const DefaultEditView = (props) => { const apiURL = `${serverURL}${api}/${slug}/${id}`; let action = `${serverURL}${api}/${slug}${isEditing ? `/${id}` : ''}`; - const hasSavePermission = (isEditing && permissions?.update?.permission) || (!isEditing && permissions?.update?.permission); + const hasSavePermission = (isEditing && permissions?.update?.permission) || (!isEditing && permissions?.create?.permission); if (auth && !isEditing) { action = `${action}/register`; @@ -170,7 +170,6 @@ const DefaultEditView = (props) => { DefaultEditView.defaultProps = { isEditing: false, data: undefined, - onSave: null, }; DefaultEditView.propTypes = { @@ -191,7 +190,7 @@ DefaultEditView.propTypes = { updatedAt: PropTypes.string, createdAt: PropTypes.string, }), - onSave: PropTypes.func, + onSave: PropTypes.func.isRequired, permissions: PropTypes.shape({ create: PropTypes.shape({ permission: PropTypes.bool, diff --git a/src/collections/operations/update.js b/src/collections/operations/update.js index abe5be00c5..eed1b31876 100644 --- a/src/collections/operations/update.js +++ b/src/collections/operations/update.js @@ -154,7 +154,7 @@ const update = async (args) => { } // ///////////////////////////////////// - // 7. Return updated document + // 9. Return updated document // ///////////////////////////////////// return doc; diff --git a/src/globals/buildModel.js b/src/globals/buildModel.js index ae6e69e30f..6b6ade840e 100644 --- a/src/globals/buildModel.js +++ b/src/globals/buildModel.js @@ -1,6 +1,11 @@ const mongoose = require('mongoose'); const autopopulate = require('mongoose-autopopulate'); -const mongooseHidden = require('mongoose-hidden'); +const mongooseHidden = require('mongoose-hidden')({ + hidden: { + _id: true, __v: true, + }, + applyRecursively: true, +}); const buildSchema = require('../mongoose/buildSchema'); const localizationPlugin = require('../localization/plugin'); @@ -9,18 +14,12 @@ const buildModel = (config) => { const globalsSchema = new mongoose.Schema({}, { discriminatorKey: 'globalType', timestamps: false }) .plugin(localizationPlugin, config.localization) .plugin(autopopulate) - .plugin(mongooseHidden()); + .plugin(mongooseHidden); const Globals = mongoose.model('globals', globalsSchema); Object.values(config.globals).forEach((globalConfig) => { const globalSchema = buildSchema(globalConfig.fields); - - globalSchema - .plugin(localizationPlugin, config.localization) - .plugin(autopopulate) - .plugin(mongooseHidden()); - Globals.discriminator(globalConfig.slug, globalSchema); }); diff --git a/src/globals/init.js b/src/globals/init.js index 397cde7849..a1bd492187 100644 --- a/src/globals/init.js +++ b/src/globals/init.js @@ -1,5 +1,4 @@ const buildModel = require('./buildModel'); -const sanitize = require('./sanitize'); const routes = require('./routes'); function initGlobals() { diff --git a/src/globals/operations/findOne.js b/src/globals/operations/findOne.js index 5cd38360f1..e2f1630de7 100644 --- a/src/globals/operations/findOne.js +++ b/src/globals/operations/findOne.js @@ -55,14 +55,18 @@ const findOne = async (args) => { } let result = await Model.findOne({ globalType: slug }); + let data = {}; - if (!result) throw new NotFound(); + if (!result) { + result = {}; + } else { + if (locale && result.setLocale) { + result.setLocale(locale, fallbackLocale); + } - if (locale && result.setLocale) { - result.setLocale(locale, fallbackLocale); + data = result.toJSON({ virtuals: true }); } - let data = result.toJSON({ virtuals: true }); // ///////////////////////////////////// // 4. Execute field-level hooks and policies diff --git a/src/globals/operations/update.js b/src/globals/operations/update.js index 17326245a1..c63bacaa85 100644 --- a/src/globals/operations/update.js +++ b/src/globals/operations/update.js @@ -29,7 +29,7 @@ const update = async (args) => { let global = await Model.findOne({ globalType: slug }); if (!global) { - global = new Model(); + global = new Model({ globalType: slug }); } if (locale && global.setLocale) { @@ -39,7 +39,7 @@ const update = async (args) => { const globalJSON = global.toJSON({ virtuals: true }); // ///////////////////////////////////// - // 2. Execute before global hook + // 3. Execute before global hook // ///////////////////////////////////// const { beforeUpdate } = args.config.hooks; @@ -49,37 +49,37 @@ const update = async (args) => { } // ///////////////////////////////////// - // 3. Merge updates into existing data + // 4. Merge updates into existing data // ///////////////////////////////////// options.data = deepmerge(globalJSON, options.data, { arrayMerge: overwriteMerge }); // ///////////////////////////////////// - // 4. Execute field-level hooks, policies, and validation + // 5. Execute field-level hooks, policies, and validation // ///////////////////////////////////// options.data = await performFieldOperations(args.config, { ...options, hook: 'beforeUpdate', operationName: 'update' }); // ///////////////////////////////////// - // 4. Perform database operation + // 6. Perform database operation // ///////////////////////////////////// - Object.assign(global, { ...options.data, globalType: slug }); + Object.assign(global, options.data); - global.save(); + await global.save(); global = global.toJSON({ virtuals: true }); // ///////////////////////////////////// - // 5. Execute field-level hooks and policies + // 7. Execute field-level hooks and policies // ///////////////////////////////////// - global = performFieldOperations(args.config, { + global = await performFieldOperations(args.config, { ...options, data: global, hook: 'afterRead', operationName: 'read', }); // ///////////////////////////////////// - // 6. Execute after global hook + // 8. Execute after global hook // ///////////////////////////////////// const { afterUpdate } = args.config.hooks; @@ -89,7 +89,7 @@ const update = async (args) => { } // ///////////////////////////////////// - // 7. Return global + // 9. Return global // ///////////////////////////////////// return global; diff --git a/src/globals/requestHandlers/findOne.js b/src/globals/requestHandlers/findOne.js index 932ae2456e..2b6fb6ee10 100644 --- a/src/globals/requestHandlers/findOne.js +++ b/src/globals/requestHandlers/findOne.js @@ -1,8 +1,7 @@ const httpStatus = require('http-status'); -const formatErrorResponse = require('../../express/responses/formatError'); const { findOne } = require('../operations'); -const findOneHandler = (Model, config) => async (req, res) => { +const findOneHandler = (Model, config) => async (req, res, next) => { try { const { slug } = config; @@ -16,7 +15,7 @@ const findOneHandler = (Model, config) => async (req, res) => { return res.status(httpStatus.OK).json(result); } catch (error) { - return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error)); + return next(error); } }; diff --git a/src/globals/requestHandlers/update.js b/src/globals/requestHandlers/update.js index 675a7b67e1..f40b455a7d 100644 --- a/src/globals/requestHandlers/update.js +++ b/src/globals/requestHandlers/update.js @@ -1,8 +1,7 @@ const httpStatus = require('http-status'); -const formatErrorResponse = require('../../express/responses/formatError'); const { update } = require('../operations'); -const updateHandler = (Model, config) => async (req, res) => { +const updateHandler = (Model, config) => async (req, res, next) => { try { const { slug } = config; @@ -12,11 +11,12 @@ const updateHandler = (Model, config) => async (req, res) => { config, slug, depth: req.query.depth, + data: req.body, }); return res.status(httpStatus.OK).json({ message: 'Global saved successfully.', result }); } catch (error) { - return res.status(error.status || httpStatus.INTERNAL_SERVER_ERROR).json(formatErrorResponse(error)); + return next(error); } }; diff --git a/src/globals/routes.js b/src/globals/routes.js index c242f8c807..ec2016bac5 100644 --- a/src/globals/routes.js +++ b/src/globals/routes.js @@ -10,8 +10,7 @@ const registerGlobals = (globalConfigs, Globals) => { router .route(`/globals/${global.slug}`) .get(findOne(Globals, global)) - .post(update(Globals, global)) - .put(update(Globals, global)); + .post(update(Globals, global)); }); return router; diff --git a/src/utilities/getPropSubset.js b/src/utilities/getPropSubset.js deleted file mode 100644 index 07635e9650..0000000000 --- a/src/utilities/getPropSubset.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = (keys, obj) => keys.reduce((a, c) => ({ ...a, [c]: obj[c] }), {}); diff --git a/src/utilities/secureConfig.js b/src/utilities/secureConfig.js deleted file mode 100644 index b0f4b0d4de..0000000000 --- a/src/utilities/secureConfig.js +++ /dev/null @@ -1,33 +0,0 @@ -const deepCopyObject = require('../utilities/deepCopyObject'); - -const recursivelySecure = (object) => { - const newObject = deepCopyObject(object); - - delete newObject.hooks; - delete newObject.components; - delete newObject.policies; - - if (newObject.fields) { - newObject.fields.forEach((field, i) => { - newObject.fields[i] = recursivelySecure(field); - }); - } - - return newObject; -}; - -const secureConfig = (insecureConfig) => { - const config = deepCopyObject(insecureConfig); - - delete config.secret; - - recursivelySecure(config); - - config.collections.forEach((collection, i) => { - config.collections[i] = recursivelySecure(collection); - }); - - return config; -}; - -module.exports = secureConfig;