From 2ae33b603abaec4ff80261a465781f508b4a1e06 Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Thu, 11 Aug 2022 11:07:46 -0400 Subject: [PATCH] feat: WIP tab compatible with traverseFields --- src/fields/config/schema.ts | 2 +- src/fields/config/types.ts | 17 ++-- src/fields/hooks/afterChange/promise.ts | 48 +++++++--- src/fields/hooks/afterRead/promise.ts | 59 +++++++----- src/fields/hooks/beforeChange/promise.ts | 77 +++++++++------- src/fields/hooks/beforeValidate/promise.ts | 54 +++++++---- src/mongoose/buildSchema.ts | 9 +- test/fields/collections/Tabs/index.ts | 100 +++++++++++++++------ test/fields/int.spec.ts | 20 +++++ 9 files changed, 267 insertions(+), 119 deletions(-) diff --git a/src/fields/config/schema.ts b/src/fields/config/schema.ts index 5242bc7a09..aa0f4d3fb7 100644 --- a/src/fields/config/schema.ts +++ b/src/fields/config/schema.ts @@ -181,7 +181,7 @@ export const collapsible = baseField.keys({ admin: baseAdminFields.default(), }); -const tab = joi.object({ +const tab = baseField.keys({ name: joi.string().when('localized', { is: joi.exist(), then: joi.required() }), localized: joi.boolean(), label: joi.string().required(), diff --git a/src/fields/config/types.ts b/src/fields/config/types.ts index b3b6c3fcb5..70b726712d 100644 --- a/src/fields/config/types.ts +++ b/src/fields/config/types.ts @@ -181,18 +181,14 @@ export type CollapsibleField = Omit & { export type TabsAdmin = Omit; -type BaseTab = { +type TabBase = { fields: Field[] description?: Description } -type NamedTab = BaseTab & { - name: string - localized?: boolean - label?: string -} +export type NamedTab = TabBase & FieldBase -type UnnamedTab = BaseTab & { +export type UnnamedTab = TabBase & Omit & { label: string localized?: never } @@ -205,6 +201,10 @@ export type TabsField = Omit & { admin?: TabsAdmin } +type TabAsField = Tab & { + type: 'tab' +}; + export type UIField = { name: string label?: string @@ -363,6 +363,7 @@ export type Field = | RowField | CollapsibleField | TabsField + | TabAsField | UIField; export type FieldAffectingData = @@ -468,7 +469,7 @@ export function tabHasName(tab: Tab): tab is NamedTab { return 'name' in tab; } -export function fieldIsLocalized(field: Field): boolean { +export function fieldIsLocalized(field: Field | Tab): boolean { return 'localized' in field && field.localized; } diff --git a/src/fields/hooks/afterChange/promise.ts b/src/fields/hooks/afterChange/promise.ts index 12f8a93721..d0a5459fa2 100644 --- a/src/fields/hooks/afterChange/promise.ts +++ b/src/fields/hooks/afterChange/promise.ts @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ import { PayloadRequest } from '../../../express/types'; -import { Field, fieldAffectsData } from '../../config/types'; +import { Field, fieldAffectsData, tabHasName } from '../../config/types'; import { traverseFields } from './traverseFields'; type Args = { @@ -125,20 +125,40 @@ export const promise = async ({ break; } - case 'tabs': { - const promises = []; - field.tabs.forEach((tab) => { - promises.push(traverseFields({ - data, - doc, - fields: tab.fields, - operation, - req, - siblingData: siblingData || {}, - siblingDoc: { ...siblingDoc }, - })); + case 'tab': { + let tabSiblingData; + let tabSiblingDoc; + if (tabHasName(field)) { + tabSiblingData = siblingData[field.name] || {}; + tabSiblingDoc = siblingDoc[field.name]; + } else { + tabSiblingData = siblingData || {}; + tabSiblingDoc = { ...siblingDoc }; + } + + await traverseFields({ + data, + doc, + fields: field.fields, + operation, + req, + siblingData: tabSiblingData, + siblingDoc: tabSiblingDoc, + }); + + break; + } + + case 'tabs': { + await traverseFields({ + data, + doc, + fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), + operation, + req, + siblingData: siblingData || {}, + siblingDoc: { ...siblingDoc }, }); - await Promise.all(promises); break; } diff --git a/src/fields/hooks/afterRead/promise.ts b/src/fields/hooks/afterRead/promise.ts index b9143b8691..908c05ec1b 100644 --- a/src/fields/hooks/afterRead/promise.ts +++ b/src/fields/hooks/afterRead/promise.ts @@ -275,28 +275,45 @@ export const promise = async ({ break; } - case 'tabs': { - field.tabs.forEach((tab) => { - let tabDoc = siblingDoc; - if (tabHasName(tab)) { - tabDoc = siblingDoc[tab.name] as Record; - if (typeof siblingDoc[tab.name] !== 'object') tabDoc = {}; - } + case 'tab': { + let tabDoc = siblingDoc; + if (tabHasName(field)) { + tabDoc = siblingDoc[field.name] as Record; + if (typeof siblingDoc[field.name] !== 'object') tabDoc = {}; + } - traverseFields({ - currentDepth, - depth, - doc, - fieldPromises, - fields: tab.fields, - findMany, - flattenLocales, - overrideAccess, - populationPromises, - req, - siblingDoc: tabDoc, - showHiddenFields, - }); + await traverseFields({ + currentDepth, + depth, + doc, + fieldPromises, + fields: field.fields, + findMany, + flattenLocales, + overrideAccess, + populationPromises, + req, + siblingDoc: tabDoc, + showHiddenFields, + }); + + break; + } + + case 'tabs': { + traverseFields({ + currentDepth, + depth, + doc, + fieldPromises, + fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), + findMany, + flattenLocales, + overrideAccess, + populationPromises, + req, + siblingDoc, + showHiddenFields, }); break; } diff --git a/src/fields/hooks/beforeChange/promise.ts b/src/fields/hooks/beforeChange/promise.ts index cb21e96417..8f4ae44b3c 100644 --- a/src/fields/hooks/beforeChange/promise.ts +++ b/src/fields/hooks/beforeChange/promise.ts @@ -281,38 +281,55 @@ export const promise = async ({ break; } - case 'tabs': { - const promises = []; - field.tabs.forEach((tab) => { - let tabPath = path; - let tabSiblingData = siblingData; - let tabSiblingDoc = siblingDoc; - let tabSiblingDocWithLocales = siblingDocWithLocales; - if (tabHasName(tab)) { - tabPath = `${path}${tab.name}.`; - tabSiblingData = typeof siblingData[tab.name] === 'object' ? siblingData[tab.name] as Record : {}; - tabSiblingDoc = typeof siblingDoc[tab.name] === 'object' ? siblingDoc[tab.name] as Record : {}; - tabSiblingDocWithLocales = typeof siblingDocWithLocales[tab.name] === 'object' ? siblingDocWithLocales[tab.name] as Record : {}; - } - promises.push(traverseFields({ - data, - doc, - docWithLocales, - errors, - fields: tab.fields, - id, - mergeLocaleActions, - operation, - path: tabPath, - req, - siblingData: tabSiblingData, - siblingDoc: tabSiblingDoc, - siblingDocWithLocales: tabSiblingDocWithLocales, - skipValidation: skipValidationFromHere, - })); + case 'tab': { + let tabPath = path; + let tabSiblingData = siblingData; + let tabSiblingDoc = siblingDoc; + let tabSiblingDocWithLocales = siblingDocWithLocales; + if (tabHasName(field)) { + tabPath = `${path}${field.name}.`; + tabSiblingData = typeof siblingData[field.name] === 'object' ? siblingData[field.name] as Record : {}; + tabSiblingDoc = typeof siblingDoc[field.name] === 'object' ? siblingDoc[field.name] as Record : {}; + tabSiblingDocWithLocales = typeof siblingDocWithLocales[field.name] === 'object' ? siblingDocWithLocales[field.name] as Record : {}; + } + + await traverseFields({ + data, + doc, + docWithLocales, + errors, + fields: field.fields, + id, + mergeLocaleActions, + operation, + path: tabPath, + req, + siblingData: tabSiblingData, + siblingDoc: tabSiblingDoc, + siblingDocWithLocales: tabSiblingDocWithLocales, + skipValidation: skipValidationFromHere, }); - await Promise.all(promises); + break; + } + + case 'tabs': { + await traverseFields({ + data, + doc, + docWithLocales, + errors, + fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), + id, + mergeLocaleActions, + operation, + path, + req, + siblingData, + siblingDoc, + siblingDocWithLocales, + skipValidation: skipValidationFromHere, + }); break; } diff --git a/src/fields/hooks/beforeValidate/promise.ts b/src/fields/hooks/beforeValidate/promise.ts index 01e1920e14..3c154845c0 100644 --- a/src/fields/hooks/beforeValidate/promise.ts +++ b/src/fields/hooks/beforeValidate/promise.ts @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ import { PayloadRequest } from '../../../express/types'; -import { Field, fieldAffectsData, valueIsValueWithRelation } from '../../config/types'; +import { Field, fieldAffectsData, tabHasName, valueIsValueWithRelation } from '../../config/types'; import { traverseFields } from './traverseFields'; type Args = { @@ -265,22 +265,44 @@ export const promise = async ({ break; } - case 'tabs': { - const promises = []; - field.tabs.forEach((tab) => { - promises.push(traverseFields({ - data, - doc, - fields: tab.fields, - id, - operation, - overrideAccess, - req, - siblingData, - siblingDoc, - })); + case 'tab': { + let tabSiblingData; + let tabSiblingDoc; + if (tabHasName(field)) { + tabSiblingData = typeof siblingData[field.name] === 'object' ? siblingData[field.name] : {}; + tabSiblingDoc = typeof siblingDoc[field.name] === 'object' ? siblingDoc[field.name] : {}; + } else { + tabSiblingData = siblingData; + tabSiblingDoc = siblingDoc; + } + + await traverseFields({ + data, + doc, + fields: field.fields, + id, + operation, + overrideAccess, + req, + siblingData: tabSiblingData, + siblingDoc: tabSiblingDoc, + }); + + break; + } + + case 'tabs': { + await traverseFields({ + data, + doc, + fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), + id, + operation, + overrideAccess, + req, + siblingData, + siblingDoc, }); - await Promise.all(promises); break; } diff --git a/src/mongoose/buildSchema.ts b/src/mongoose/buildSchema.ts index c27759a408..d9cc9f7ca6 100644 --- a/src/mongoose/buildSchema.ts +++ b/src/mongoose/buildSchema.ts @@ -25,10 +25,11 @@ import { RichTextField, RowField, SelectField, + Tab, tabHasName, TabsField, TextareaField, - TextField, + TextField, UnnamedTab, UploadField, } from '../fields/config/types'; import sortableFieldTypes from '../fields/sortableFieldTypes'; @@ -54,7 +55,7 @@ const formatBaseSchema = (field: NonPresentationalField, buildSchemaOptions: Bui index: field.index || field.unique || false, }); -const localizeSchema = (field: NonPresentationalField, schema, localization) => { +const localizeSchema = (field: NonPresentationalField | Tab, schema, localization) => { if (fieldIsLocalized(field) && localization && Array.isArray(localization.locales)) { return { type: localization.locales.reduce((localeSchema, locale) => ({ @@ -338,10 +339,10 @@ const fieldToSchemaMap = { }; schema.add({ - [tab.name]: localizeSchema(field, baseSchema, config.localization), + [tab.name]: localizeSchema(tab, baseSchema, config.localization), }); } else { - tab.fields.forEach((subField: Field) => { + (tab as UnnamedTab).fields.forEach((subField: Field) => { const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]; if (addFieldSchema) { diff --git a/test/fields/collections/Tabs/index.ts b/test/fields/collections/Tabs/index.ts index dcf713eab5..e82d8d0b38 100644 --- a/test/fields/collections/Tabs/index.ts +++ b/test/fields/collections/Tabs/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-param-reassign */ import type { CollectionConfig } from '../../../../src/collections/config/types'; import { blocksField, blocksFieldSeedData } from '../Blocks'; import { UIField } from './UIField'; @@ -10,6 +11,9 @@ export const localizedTextValue = 'localized text'; const TabsFields: CollectionConfig = { slug: tabsSlug, + access: { + read: () => true, + }, versions: true, fields: [ { @@ -148,27 +152,74 @@ const TabsFields: CollectionConfig = { description: 'This tab is localized and requires a name', fields: [ { - name: 'array', - labels: { - singular: 'Item', - plural: 'Items', - }, - type: 'array', - required: true, - fields: [ - { - name: 'text', - type: 'text', - required: true, - }, - ], + name: 'text', + type: 'text', }, + ], + }, + { + name: 'accessControlTab', + label: 'Access Control Tab', + access: { + read: () => false, + }, + description: 'This tab is cannot be read', + fields: [ { name: 'text', type: 'text', }, ], }, + { + name: 'hooksTab', + label: 'Hooks Tab', + hooks: { + beforeValidate: [ + ({ data }) => { + data.hooksTab.beforeValidate = true; + return data.hooksTab; + }, + ], + beforeChange: [ + ({ data }) => { + data.hooksTab.beforeChange = true; + return data.hooksTab; + }, + ], + afterChange: [ + ({ data }) => { + data.hooksTab.afterChange = true; + return data.hooksTab; + }, + ], + afterRead: [ + ({ data }) => { + data.hooksTab.afterRead = true; + return data.hooksTab; + }, + ], + }, + description: 'This tab has hooks', + fields: [ + { + name: 'beforeValidate', + type: 'checkbox', + }, + { + name: 'beforeChange', + type: 'checkbox', + }, + { + name: 'afterChange', + type: 'checkbox', + }, + { + name: 'afterRead', + type: 'checkbox', + }, + ], + }, ], }, { @@ -250,20 +301,19 @@ export const tabsDoc = { ], text: namedTabText, }, + text: 'localized', localizedTab: { - array: [ - { - text: "Hello, I'm the first row, in a named tab", - }, - { - text: 'Second row here, in a named tab', - }, - { - text: 'Here is some data for the third row, in a named tab', - }, - ], text: localizedTextValue, }, + accessControlTab: { + text: 'cannot be read', + }, + hooksTab: { + beforeValidate: false, + beforeChange: false, + afterChange: false, + afterRead: false, + }, textarea: 'Here is some text that goes in a textarea', anotherText: 'Super tired of writing this text', textInRow: 'hello', diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index 58a72019b7..0855b68599 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -315,6 +315,26 @@ describe('Fields', () => { expect(document.localizedTab.en.text).toStrictEqual(localizedTextValue); }); + it('should allow access control on a named tab', async () => { + document = await payload.findByID({ + collection: tabsSlug, + id: document.id, + overrideAccess: false, + }); + expect(document.accessControlTab).toBeUndefined(); + }); + + it('should allow hooks on a named tab', async () => { + document = await payload.findByID({ + collection: tabsSlug, + id: document.id, + }); + expect(document.hooksTab.beforeValidate).toBe(true); + expect(document.hooksTab.beforeChange).toBe(true); + expect(document.hooksTab.afterChange).toBe(true); + expect(document.hooksTab.afterRead).toBe(true); + }); + it('should return empty object for groups when no data present', async () => { const doc = await payload.create({ collection: groupFieldsSlug,