From c3ac2f8ae4f7721bd04c729947eda595e07f9d36 Mon Sep 17 00:00:00 2001 From: Gautam Singh Date: Mon, 27 Apr 2020 03:33:07 +0530 Subject: [PATCH 1/8] create singleton list plugin --- packages/list-plugins/index.js | 2 ++ .../list-plugins/lib/limiting/singleton.js | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 packages/list-plugins/lib/limiting/singleton.js diff --git a/packages/list-plugins/index.js b/packages/list-plugins/index.js index e81094640e8..576a059afa5 100644 --- a/packages/list-plugins/index.js +++ b/packages/list-plugins/index.js @@ -1,5 +1,6 @@ const { atTracking, createdAt, updatedAt } = require('./lib/tracking/atTracking'); const { byTracking, createdBy, updatedBy } = require('./lib/tracking/byTracking'); +const { singleton } = require('./lib/limiting/singleton'); module.exports = { atTracking, @@ -8,4 +9,5 @@ module.exports = { byTracking, createdBy, updatedBy, + singleton, }; diff --git a/packages/list-plugins/lib/limiting/singleton.js b/packages/list-plugins/lib/limiting/singleton.js new file mode 100644 index 00000000000..16e8cd0c4db --- /dev/null +++ b/packages/list-plugins/lib/limiting/singleton.js @@ -0,0 +1,24 @@ +const { composeHook } = require('../utils'); + +exports.singleton = () => ({ hooks = {}, adminConfig = {}, ...rest }, { listKey, keystone }) => { + const newResolveInput = async ({ resolvedData, operation }) => { + if (operation === 'create') { + const list = keystone.getListByKey(listKey); + const query = `{${list.gqlNames.listQueryMetaName} { count }}`; + const { + data: { [list.gqlNames.listQueryMetaName]: listQuery } = {}, + errors, + } = await keystone.executeQuery(query); + if (errors) { + throw errors; + } + if (listQuery && listQuery.count && listQuery.count > 0) { + throw new Error(`ItemLimit reached, This Singleton list can not add more item`); + } + } + return resolvedData; + }; + const originalResolveInput = hooks.resolveInput; + hooks.resolveInput = composeHook(originalResolveInput, newResolveInput); + return { hooks, adminConfig: { ...adminConfig, singleton: true }, ...rest }; +}; From 660866c8364554e09103b8fccbd54f2e9aaac66b Mon Sep 17 00:00:00 2001 From: Gautam Singh Date: Mon, 27 Apr 2020 04:17:14 +0530 Subject: [PATCH 2/8] prevent delete by setting access control. --- .../list-plugins/lib/limiting/singleton.js | 16 ++++++++++--- packages/list-plugins/lib/utils.js | 24 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/list-plugins/lib/limiting/singleton.js b/packages/list-plugins/lib/limiting/singleton.js index 16e8cd0c4db..52936143c2a 100644 --- a/packages/list-plugins/lib/limiting/singleton.js +++ b/packages/list-plugins/lib/limiting/singleton.js @@ -1,6 +1,9 @@ -const { composeHook } = require('../utils'); +const { composeHook, composeAccess } = require('../utils'); -exports.singleton = () => ({ hooks = {}, adminConfig = {}, ...rest }, { listKey, keystone }) => { +exports.singleton = () => ( + { hooks = {}, access = {}, adminConfig = {}, ...rest }, + { listKey, keystone } +) => { const newResolveInput = async ({ resolvedData, operation }) => { if (operation === 'create') { const list = keystone.getListByKey(listKey); @@ -18,7 +21,14 @@ exports.singleton = () => ({ hooks = {}, adminConfig = {}, ...rest }, { listKey, } return resolvedData; }; + + const listAccess = composeAccess(access, { delete: false }, keystone.defaultAccess.list); const originalResolveInput = hooks.resolveInput; hooks.resolveInput = composeHook(originalResolveInput, newResolveInput); - return { hooks, adminConfig: { ...adminConfig, singleton: true }, ...rest }; + return { + access: listAccess, + hooks, + adminConfig: { ...adminConfig, singleton: true }, + ...rest, + }; }; diff --git a/packages/list-plugins/lib/utils.js b/packages/list-plugins/lib/utils.js index f27958d8f4a..47607052f19 100644 --- a/packages/list-plugins/lib/utils.js +++ b/packages/list-plugins/lib/utils.js @@ -5,3 +5,27 @@ exports.composeHook = (originalHook, newHook) => async params => { } return newHook({ ...params, resolvedData }); }; + +exports.composeAccess = (originalAccess, newAccess = {}) => { + if (typeof originalAccess === 'undefined') { + return { + ...newAccess, + }; + } + + const isShorthand = typeof originalAccess === 'boolean'; + if (isShorthand) { + return { + create: originalAccess, + read: originalAccess, + update: originalAccess, + delete: originalAccess, + auth: originalAccess, + ...newAccess, + }; + } + return { + ...originalAccess, + ...newAccess, + }; +}; From e9c0dc06a4018c92c930c8b4ce7cda9b7fea707e Mon Sep 17 00:00:00 2001 From: Gautam Singh Date: Fri, 22 May 2020 02:51:58 +0530 Subject: [PATCH 3/8] update ui fr singleton list --- .../client/pages/Item/AddNewItem.js | 7 +++-- .../client/pages/Item/ItemTitle.js | 26 +++++++++++-------- .../app-admin-ui/client/pages/List/index.js | 7 ++++- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/app-admin-ui/client/pages/Item/AddNewItem.js b/packages/app-admin-ui/client/pages/Item/AddNewItem.js index 95e16caea46..136f94ed86f 100644 --- a/packages/app-admin-ui/client/pages/Item/AddNewItem.js +++ b/packages/app-admin-ui/client/pages/Item/AddNewItem.js @@ -7,10 +7,13 @@ import { useList } from '../../providers/List'; const AddNewItem = () => { let { - list: { access }, + list: { + access, + adminConfig: { singleton = false }, + }, openCreateItemModal, } = useList(); - if (!access.create) return null; + if (!access.create || singleton) return null; const cypressId = 'item-page-create-button'; return ( diff --git a/packages/app-admin-ui/client/pages/Item/ItemTitle.js b/packages/app-admin-ui/client/pages/Item/ItemTitle.js index 81b6a06efda..0fdfdc0d940 100644 --- a/packages/app-admin-ui/client/pages/Item/ItemTitle.js +++ b/packages/app-admin-ui/client/pages/Item/ItemTitle.js @@ -29,17 +29,21 @@ export const ItemTitle = memo(function ItemTitle({ titleText }) { {titleText} -
- - {list.label} - - -
+ {!list.adminConfig.singleton ? ( +
+ + {list.label} + + +
+ ) : ( +
+ )} {itemHeaderActions ? ( itemHeaderActions() ) : ( diff --git a/packages/app-admin-ui/client/pages/List/index.js b/packages/app-admin-ui/client/pages/List/index.js index 7c1e9e64050..514571ef908 100644 --- a/packages/app-admin-ui/client/pages/List/index.js +++ b/packages/app-admin-ui/client/pages/List/index.js @@ -32,6 +32,7 @@ import Search from './Search'; import Management, { ManageToolbar } from './Management'; import { useListFilter, useListSelect, useListSort, useListUrlState } from './dataHooks'; import { captureSuspensePromises } from '@keystonejs/utils'; +import { useAdminMeta } from '../../providers/AdminMeta'; export function ListLayout(props) { const { items, itemCount, queryErrors, query } = props; @@ -225,7 +226,7 @@ const ListPage = props => { queryErrorsParsed, query, } = useList(); - + const { adminPath } = useAdminMeta(); const history = useHistory(); const location = useLocation(); @@ -278,6 +279,10 @@ const ListPage = props => { ); } + if (list.adminConfig.singleton && itemCount > 0) { + history.push(`${adminPath}/${list.path}/${items[0].id}`); + } + // Success // ------------------------------ return ( From c5dafe2fb39a1210be90b74b82a7677dd6f46001 Mon Sep 17 00:00:00 2001 From: Gautam Singh Date: Mon, 27 Apr 2020 04:33:10 +0530 Subject: [PATCH 4/8] add readme and changeset --- .changeset/wise-icons-shake.md | 10 ++++++++++ packages/list-plugins/README.md | 19 ++++++++++++++++++- packages/list-plugins/singleton.md | 21 +++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 .changeset/wise-icons-shake.md create mode 100644 packages/list-plugins/singleton.md diff --git a/.changeset/wise-icons-shake.md b/.changeset/wise-icons-shake.md new file mode 100644 index 00000000000..0b2afa55b6d --- /dev/null +++ b/.changeset/wise-icons-shake.md @@ -0,0 +1,10 @@ +--- +'@keystonejs/app-admin-ui': minor +'@keystonejs/list-plugins': major +--- + +* Added `singleton` list plugin which prevents creating more items for a list or delete the only item in the list. + +* Updated `admin-ui` to + - Redirect to the only item if there is an item in singleton list + - Hide `Search` field, `Back` and `AddNew` buttons on item details page diff --git a/packages/list-plugins/README.md b/packages/list-plugins/README.md index 99d9a447319..d32a30a3583 100644 --- a/packages/list-plugins/README.md +++ b/packages/list-plugins/README.md @@ -52,7 +52,7 @@ const { createdAt, updatedAt } = require('@keystonejs/list-plugins'); _Note_: The API is the same. -## byTracking +# byTracking Adds `createdBy` and `updatedBy` fields to a list. These fields are read-only by will be updated automatically when items are created or updated. @@ -103,3 +103,20 @@ const { createdBy, updatedBy } = require('@keystonejs/list-plugins'); ``` _Note_: The API is the same. + +# singleton + +This plugin makes a list singleton by allowing only one item in the list. Useful for list which must contain only one items. + +## Usage + +```js +const { singleton } = require('@keystonejs/list-plugins'); + +keystone.createList('ListWithPlugin', { + fields: {...}, + plugins: [ + singleton({...}), + ], +}); +``` diff --git a/packages/list-plugins/singleton.md b/packages/list-plugins/singleton.md new file mode 100644 index 00000000000..4cc44ea0fbc --- /dev/null +++ b/packages/list-plugins/singleton.md @@ -0,0 +1,21 @@ + + +# singleton Plugin + +This plugin makes a list singleton by allowing only one item in the list. Useful for list which must contain only one items. + +## Usage + +```js +const { singleton } = require('@keystonejs/list-plugins'); + +keystone.createList('ListWithPlugin', { + fields: {...}, + plugins: [ + singleton({...}), + ], +}); +``` From 097814a4af0db18ac9c00391c8ea9197a9248d31 Mon Sep 17 00:00:00 2001 From: Gautam Singh Date: Fri, 22 May 2020 08:24:08 +0530 Subject: [PATCH 5/8] review comment - use fullpath on list, move it to useEffect --- packages/app-admin-ui/client/pages/List/index.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/app-admin-ui/client/pages/List/index.js b/packages/app-admin-ui/client/pages/List/index.js index 514571ef908..7327e847769 100644 --- a/packages/app-admin-ui/client/pages/List/index.js +++ b/packages/app-admin-ui/client/pages/List/index.js @@ -226,7 +226,6 @@ const ListPage = props => { queryErrorsParsed, query, } = useList(); - const { adminPath } = useAdminMeta(); const history = useHistory(); const location = useLocation(); @@ -248,6 +247,12 @@ const ListPage = props => { } }, []); + useEffect(() => { + if (list.adminConfig.singleton && itemCount > 0) { + history.replace(`${list.fullPath}/${items[0].id}`); + } + }, [itemCount]); + // Error // ------------------------------ // Only show error page if there is no data @@ -279,10 +284,6 @@ const ListPage = props => { ); } - if (list.adminConfig.singleton && itemCount > 0) { - history.push(`${adminPath}/${list.path}/${items[0].id}`); - } - // Success // ------------------------------ return ( From 85448f09405e39ba9387090dfe21e042d32e0783 Mon Sep 17 00:00:00 2001 From: Gautam Singh Date: Fri, 22 May 2020 08:38:23 +0530 Subject: [PATCH 6/8] update readme --- packages/list-plugins/README.md | 2 +- packages/list-plugins/singleton.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/list-plugins/README.md b/packages/list-plugins/README.md index d32a30a3583..183a224cb20 100644 --- a/packages/list-plugins/README.md +++ b/packages/list-plugins/README.md @@ -116,7 +116,7 @@ const { singleton } = require('@keystonejs/list-plugins'); keystone.createList('ListWithPlugin', { fields: {...}, plugins: [ - singleton({...}), + singleton(), ], }); ``` diff --git a/packages/list-plugins/singleton.md b/packages/list-plugins/singleton.md index 4cc44ea0fbc..d947b608b34 100644 --- a/packages/list-plugins/singleton.md +++ b/packages/list-plugins/singleton.md @@ -15,7 +15,7 @@ const { singleton } = require('@keystonejs/list-plugins'); keystone.createList('ListWithPlugin', { fields: {...}, plugins: [ - singleton({...}), + singleton(), ], }); ``` From 643b02aa805d788ed3cc0d2753b778b9c89f39e0 Mon Sep 17 00:00:00 2001 From: Gautam Singh Date: Fri, 22 May 2020 09:52:46 +0530 Subject: [PATCH 7/8] lint error --- packages/app-admin-ui/client/pages/List/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app-admin-ui/client/pages/List/index.js b/packages/app-admin-ui/client/pages/List/index.js index 7327e847769..5d6789374cd 100644 --- a/packages/app-admin-ui/client/pages/List/index.js +++ b/packages/app-admin-ui/client/pages/List/index.js @@ -32,7 +32,6 @@ import Search from './Search'; import Management, { ManageToolbar } from './Management'; import { useListFilter, useListSelect, useListSort, useListUrlState } from './dataHooks'; import { captureSuspensePromises } from '@keystonejs/utils'; -import { useAdminMeta } from '../../providers/AdminMeta'; export function ListLayout(props) { const { items, itemCount, queryErrors, query } = props; From b3473831c5d3d0bdd0f3894a2b13c9f262b93e65 Mon Sep 17 00:00:00 2001 From: Gautam Singh Date: Fri, 22 May 2020 13:05:01 +0530 Subject: [PATCH 8/8] revert UI changes to make it as part of different PR. --- .changeset/wise-icons-shake.md | 5 ---- .../client/pages/Item/AddNewItem.js | 7 ++--- .../client/pages/Item/ItemTitle.js | 26 ++++++++----------- .../app-admin-ui/client/pages/List/index.js | 7 +---- 4 files changed, 14 insertions(+), 31 deletions(-) diff --git a/.changeset/wise-icons-shake.md b/.changeset/wise-icons-shake.md index 0b2afa55b6d..0455f194bbc 100644 --- a/.changeset/wise-icons-shake.md +++ b/.changeset/wise-icons-shake.md @@ -1,10 +1,5 @@ --- -'@keystonejs/app-admin-ui': minor '@keystonejs/list-plugins': major --- * Added `singleton` list plugin which prevents creating more items for a list or delete the only item in the list. - -* Updated `admin-ui` to - - Redirect to the only item if there is an item in singleton list - - Hide `Search` field, `Back` and `AddNew` buttons on item details page diff --git a/packages/app-admin-ui/client/pages/Item/AddNewItem.js b/packages/app-admin-ui/client/pages/Item/AddNewItem.js index 136f94ed86f..95e16caea46 100644 --- a/packages/app-admin-ui/client/pages/Item/AddNewItem.js +++ b/packages/app-admin-ui/client/pages/Item/AddNewItem.js @@ -7,13 +7,10 @@ import { useList } from '../../providers/List'; const AddNewItem = () => { let { - list: { - access, - adminConfig: { singleton = false }, - }, + list: { access }, openCreateItemModal, } = useList(); - if (!access.create || singleton) return null; + if (!access.create) return null; const cypressId = 'item-page-create-button'; return ( diff --git a/packages/app-admin-ui/client/pages/Item/ItemTitle.js b/packages/app-admin-ui/client/pages/Item/ItemTitle.js index 0fdfdc0d940..81b6a06efda 100644 --- a/packages/app-admin-ui/client/pages/Item/ItemTitle.js +++ b/packages/app-admin-ui/client/pages/Item/ItemTitle.js @@ -29,21 +29,17 @@ export const ItemTitle = memo(function ItemTitle({ titleText }) { {titleText} - {!list.adminConfig.singleton ? ( -
- - {list.label} - - -
- ) : ( -
- )} +
+ + {list.label} + + +
{itemHeaderActions ? ( itemHeaderActions() ) : ( diff --git a/packages/app-admin-ui/client/pages/List/index.js b/packages/app-admin-ui/client/pages/List/index.js index 5d6789374cd..7c1e9e64050 100644 --- a/packages/app-admin-ui/client/pages/List/index.js +++ b/packages/app-admin-ui/client/pages/List/index.js @@ -225,6 +225,7 @@ const ListPage = props => { queryErrorsParsed, query, } = useList(); + const history = useHistory(); const location = useLocation(); @@ -246,12 +247,6 @@ const ListPage = props => { } }, []); - useEffect(() => { - if (list.adminConfig.singleton && itemCount > 0) { - history.replace(`${list.fullPath}/${items[0].id}`); - } - }, [itemCount]); - // Error // ------------------------------ // Only show error page if there is no data