From c08f0958ceff6661609885a89d00b6cb2449611b Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Fri, 27 Oct 2023 08:39:43 +0000 Subject: [PATCH 01/37] feat: moved createForm, listForms into HCMS --- apps/api/graphql/src/index.ts | 5 +- .../src/operations/form/index.ts | 118 +------------ .../CmsFormBuilderStorage.ts | 96 +++++++++++ .../FormBuilderContextSetup.ts | 107 ++++++++++++ .../createFormBuilderContext.ts | 19 +++ .../createFormBuilderPlugins.ts | 20 +++ .../createGraphQLSchemaPlugin.ts | 7 + .../cmsFormBuilderStorage/creteModelField.ts | 38 +++++ .../src/cmsFormBuilderStorage/modelFactory.ts | 26 +++ .../models/form.model.ts | 112 ++++++++++++ packages/api-form-builder/src/index.ts | 17 +- .../src/plugins/crud/forms.crud.ts | 17 +- .../src/plugins/crud/index.ts | 160 +++++++++--------- .../api-form-builder/src/plugins/graphql.ts | 14 +- .../src/plugins/graphql/form.ts | 17 +- .../src/plugins/graphql/formSettings.ts | 13 +- packages/api-form-builder/src/types.ts | 13 +- .../src/crud/contentEntry.crud.ts | 2 +- 18 files changed, 558 insertions(+), 243 deletions(-) create mode 100644 packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts create mode 100644 packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts create mode 100644 packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderContext.ts create mode 100644 packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderPlugins.ts create mode 100644 packages/api-form-builder/src/cmsFormBuilderStorage/createGraphQLSchemaPlugin.ts create mode 100644 packages/api-form-builder/src/cmsFormBuilderStorage/creteModelField.ts create mode 100644 packages/api-form-builder/src/cmsFormBuilderStorage/modelFactory.ts create mode 100644 packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts diff --git a/apps/api/graphql/src/index.ts b/apps/api/graphql/src/index.ts index 7afb7bb31c1..3cc55cb71bc 100644 --- a/apps/api/graphql/src/index.ts +++ b/apps/api/graphql/src/index.ts @@ -22,9 +22,9 @@ import { createFileModelModifier } from "@webiny/api-file-manager"; import { createFileManagerStorageOperations } from "@webiny/api-file-manager-ddb"; +import { createFormBuilderContext, createFormBuilderGraphQL } from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import logsPlugins from "@webiny/handler-logs"; import fileManagerS3 from "@webiny/api-file-manager-s3"; -import { createFormBuilder } from "@webiny/api-form-builder"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createHeadlessCmsContext, createHeadlessCmsGraphQL } from "@webiny/api-headless-cms"; import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb"; @@ -88,7 +88,8 @@ export const handler = createHandler({ pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), - createFormBuilder({ + createFormBuilderGraphQL(), + createFormBuilderContext({ storageOperations: createFormBuilderStorageOperations({ documentClient }) diff --git a/packages/api-form-builder-so-ddb/src/operations/form/index.ts b/packages/api-form-builder-so-ddb/src/operations/form/index.ts index 3e7282c2a12..f6fb5eaa62e 100644 --- a/packages/api-form-builder-so-ddb/src/operations/form/index.ts +++ b/packages/api-form-builder-so-ddb/src/operations/form/index.ts @@ -152,46 +152,10 @@ export const createFormStorageOperations = ( }; }; - const createForm = async ( - params: FormBuilderStorageOperationsCreateFormParams - ): Promise => { - const { form } = params; - - const revisionKeys = createRevisionKeys(form); - const latestKeys = createLatestKeys(form); - const gsiKeys = createGSIKeys(form); - - const items = [ - entity.putBatch({ - ...form, - ...revisionKeys, - ...gsiKeys, - TYPE: createFormType() - }), - entity.putBatch({ - ...form, - ...latestKeys, - TYPE: createFormLatestType() - }) - ]; - - try { - await batchWriteAll({ - table, - items - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not insert form data into table.", - ex.code || "CREATE_FORM_ERROR", - { - revisionKeys, - latestKeys, - form - } - ); - } - return form; + const createForm = () => { + throw new Error( + "api-form-builder-ddb does not implement the Form Builder storage operations." + ); }; const createFormFrom = async ( @@ -357,76 +321,10 @@ export const createFormStorageOperations = ( } }; - const listForms = async ( - params: FormBuilderStorageOperationsListFormsParams - ): Promise => { - const { sort, limit, where: initialWhere, after } = params; - - const queryAllParams: QueryAllParams = { - entity, - partitionKey: createFormLatestPartitionKey(initialWhere), - options: { - gte: " " - } - }; - - let results; - try { - results = await queryAll(queryAllParams); - } catch (ex) { - throw new WebinyError( - ex.message || "Could list forms.", - ex.code || "LIST_FORMS_ERROR", - { - where: initialWhere, - partitionKey: queryAllParams.partitionKey - } - ); - } - const totalCount = results.length; - - const where: Partial = { - ...initialWhere - }; - /** - * We need to remove conditions so we do not filter by them again. - */ - delete where.tenant; - delete where.locale; - - const filteredItems = filterItems({ - plugins, - items: results, - where, - fields: formDynamoDbFields - }); - - const sortedItems = sortItems({ - items: filteredItems, - sort, - fields: formDynamoDbFields - }); - - const start = parseInt(decodeCursor(after) || "0") || 0; - const hasMoreItems = totalCount > start + limit; - const end = limit > totalCount + start + limit ? undefined : start + limit; - const items = sortedItems.slice(start, end); - /** - * Although we do not need a cursor here, we will use it as such to keep it standardized. - * Number is simply encoded. - */ - const cursor = items.length > 0 ? encodeCursor(start + limit) : null; - - const meta = { - hasMoreItems, - totalCount, - cursor - }; - - return { - items, - meta - }; + const listForms = () => { + throw new Error( + "api-form-builder-ddb does not implement the Form Builder storage operations." + ); }; const listFormRevisions = async ( diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts new file mode 100644 index 00000000000..df651fd0333 --- /dev/null +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts @@ -0,0 +1,96 @@ +import { CmsEntry, CmsModel, HeadlessCms } from "@webiny/api-headless-cms/types"; +import { Security } from "@webiny/api-security/types"; +import { FbForm, FormBuilderStorageOperationsListFormsParams } from "~/types"; + +interface ModelContext { + tenant: string; + locale: string; +} + +export class CmsFormBuilderStorage { + private readonly cms: HeadlessCms; + private readonly security: Security; + private readonly model: CmsModel; + + static async create(params: { formModel: CmsModel; cms: HeadlessCms; security: Security }) { + return new CmsFormBuilderStorage(params.formModel, params.cms, params.security); + } + + private constructor(formModel: CmsModel, cms: HeadlessCms, security: Security) { + this.model = formModel; + this.cms = cms; + this.security = security; + } + + private modelWithContext({ tenant, locale }: ModelContext): CmsModel { + return { ...this.model, tenant, locale }; + } + + createForm = async ({ form }: { form: FbForm }) => { + const model = this.modelWithContext(form); + const entry = await this.security.withoutAuthorization(() => { + return this.cms.createEntry(model, { ...form }); + }); + return this.getFormFieldValues(entry); + }; + + getForm = async ({ where }: FormBuilderStorageOperationsListFormsParams) => { + const { id, tenant, locale } = where; + const model = this.modelWithContext({ tenant, locale }); + const entry = await this.security.withoutAuthorization(() => { + return this.cms.getEntry(model, { where: { entryId: id, latest: true } }); + }); + return entry ? this.getFormFieldValues(entry) : null; + }; + + listFormRevisions = async (params: FormBuilderStorageOperationsListFormsParams) => { + const { + where: { tenant, locale, id } + } = params; + const model = this.modelWithContext({ tenant, locale }); + + const [entries, meta] = await this.security.withoutAuthorization(async () => { + return await this.cms.listLatestEntries(model, { + after: params.after, + limit: params.limit, + sort: params.sort, + where: { + entryId: id + } + }); + }); + + return [entries.map(entry => this.getFormFieldValues(entry)), meta]; + }; + + listForms = async (params: FormBuilderStorageOperationsListFormsParams) => { + const tenant = params.where.tenant; + const locale = params.where.locale; + + const model = this.modelWithContext({ tenant, locale }); + + const [entries, meta] = await this.security.withoutAuthorization(async () => { + return await this.cms.listLatestEntries(model, { + after: params.after, + limit: params.limit, + sort: params.sort, + where: {} + }); + }); + + return [entries.map(entry => this.getFormFieldValues(entry)), meta]; + }; + + private getFormFieldValues(entry: CmsEntry) { + return { + id: entry.entryId, + createdBy: entry.createdBy, + createdOn: entry.createdOn, + savedOn: entry.savedOn, + locale: entry.locale, + tenant: entry.tenant, + webinyVersion: entry.webinyVersion, + ...entry.values + } as FbForm; + } +} diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts new file mode 100644 index 00000000000..652be6febc4 --- /dev/null +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts @@ -0,0 +1,107 @@ +import { createFormBuilder } from "~/index"; +import { FormBuilderContext, FbFormPermission } from "~/types"; +import WebinyError from "@webiny/error"; +import { SecurityPermission } from "@webiny/api-security/types"; +import { createFormBuilderPlugins } from "./createFormBuilderPlugins"; +import { CmsModelPlugin } from "@webiny/api-headless-cms"; +import { CmsFormBuilderStorage } from "./CmsFormBuilderStorage"; +import { AppPermissions } from "@webiny/api-security/utils/AppPermissions"; + +class FormsPermissions extends AppPermissions {} + +export class FormBuilderContextSetup { + private readonly context: FormBuilderContext; + + constructor(context: FormBuilderContext) { + this.context = context; + } + + async setupContext(storageOperations: any) { + const formStorageOps = await this.context.security.withoutAuthorization(() => { + return this.setupCmsStorageOperations(); + }); + + if (formStorageOps) { + storageOperations = { + ...storageOperations, + ...formStorageOps + }; + } + + const formPermissions = new FormsPermissions({ + getIdentity: this.context.security.getIdentity, + getPermissions: () => this.context.security.getPermissions("fb.form"), + fullAccessPermissionName: "fb.*" + }); + + return createFormBuilder({ + storageOperations, + formsPermissions: this.formsPermissions, + getTenant: () => this.context.tenancy.getCurrentTenant(), + getLocale: this.getLocale.bind(this), + context: this.context + }); + } + + private getLocale() { + const locale = this.context.i18n.getContentLocale(); + if (!locale) { + throw new WebinyError( + "Missing locale on context.i18n locale in File Manager API.", + "LOCALE_ERROR" + ); + } + return locale; + } + + private getIdentity() { + return this.context.security.getIdentity(); + } + + private getTenantId() { + return this.context.tenancy.getCurrentTenant().id; + } + + private async getPermissions( + name: string + ): Promise { + return this.context.security.getPermissions(name); + } + + private basePermissionsArgs = { + getIdentity: this.getIdentity.bind(this), + fullAccessPermissionName: "fb.*" + }; + + private formsPermissions = new FormsPermissions({ + ...this.basePermissionsArgs, + getPermissions: () => this.context.security.getPermissions("fb.form") + }); + + private async setupCmsStorageOperations() { + // This registers code plugins (model group, models) + const { groupPlugin, formModelDefinition } = createFormBuilderPlugins(); + + // Finally, register all plugins + this.context.plugins.register([groupPlugin, new CmsModelPlugin(formModelDefinition)]); + const formModel = await this.getModel("fbForm"); + + return await CmsFormBuilderStorage.create({ + formModel, + cms: this.context.cms, + security: this.context.security + }); + } + + private async getModel(modelId: string) { + const model = await this.context.cms.getModel(modelId); + if (!model) { + throw new WebinyError({ + code: "MODEL_NOT_FOUND", + message: `Content model "${modelId}" was not found!` + }); + } + + return model; + } +} diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderContext.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderContext.ts new file mode 100644 index 00000000000..3b581b05cda --- /dev/null +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderContext.ts @@ -0,0 +1,19 @@ +import { ContextPlugin } from "@webiny/api"; +import { FormBuilderContext } from "~/types"; +import { FormBuilderContextSetup } from "./FormBuilderContextSetup"; +import { createGraphQLSchemaPlugin } from "./createGraphQLSchemaPlugin"; + +export const createFormBuilderContext = ({ storageOperations }: { storageOperations: any }) => { + const plugin = new ContextPlugin(async context => { + const fbContext = new FormBuilderContextSetup(context); + await fbContext.setupContext(storageOperations); + }); + + plugin.name = "form.builder-createContext"; + + return plugin; +}; + +export const createFormBuilderGraphQL = () => { + return createGraphQLSchemaPlugin(); +}; diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderPlugins.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderPlugins.ts new file mode 100644 index 00000000000..5839dfe5834 --- /dev/null +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderPlugins.ts @@ -0,0 +1,20 @@ +import { CmsGroupPlugin } from "@webiny/api-headless-cms"; +import { createFormDataModelDefinition } from "./models/form.model"; + +export const createFormBuilderPlugins = () => { + const groupId = "contentModelGroup_fb"; + + const groupPlugin = new CmsGroupPlugin({ + id: groupId, + slug: "formBuilder", + name: "Form Builder", + description: "Group for Form Builder models", + icon: "", + isPrivate: true + }); + + return { + groupPlugin, + formModelDefinition: createFormDataModelDefinition() + }; +}; diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/createGraphQLSchemaPlugin.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/createGraphQLSchemaPlugin.ts new file mode 100644 index 00000000000..9eacfca6c4b --- /dev/null +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/createGraphQLSchemaPlugin.ts @@ -0,0 +1,7 @@ +import { createFormBuilderSettingsSchema } from "~/plugins/graphql/formSettings"; +import { createFormSchema } from "~/plugins/graphql/form"; +import { createBaseSchema } from "~/plugins/graphql"; + +export const createGraphQLSchemaPlugin = () => { + return [createBaseSchema(), createFormBuilderSettingsSchema(), createFormSchema()]; +}; diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/creteModelField.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/creteModelField.ts new file mode 100644 index 00000000000..e351829b554 --- /dev/null +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/creteModelField.ts @@ -0,0 +1,38 @@ +import { CmsModelField } from "@webiny/api-headless-cms/types"; +import camelCase from "lodash/camelCase"; + +export interface CreateModelFieldParams + extends Omit { + fieldId?: string; +} + +export const createModelField = (params: CreateModelFieldParams): CmsModelField => { + const { + label, + fieldId: initialFieldId, + type, + settings = {}, + listValidation = [], + validation = [], + multipleValues = false, + predefinedValues = { + values: [], + enabled: false + } + } = params; + + const fieldId = initialFieldId ? camelCase(initialFieldId) : camelCase(label); + + return { + id: fieldId, + storageId: `${type}@${fieldId}`, + fieldId, + label, + type, + settings, + listValidation, + validation, + multipleValues, + predefinedValues + }; +} \ No newline at end of file diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/modelFactory.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/modelFactory.ts new file mode 100644 index 00000000000..2937689e08c --- /dev/null +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/modelFactory.ts @@ -0,0 +1,26 @@ +import { CmsModelPlugin, CmsPrivateModelFull, createCmsModel } from "@webiny/api-headless-cms"; +import { CmsGroup } from "@webiny/api-headless-cms/types"; + +interface Params { + group: Pick; + /** + * Locale and tenant do not need to be defined. + * In that case model is not bound to any locale or tenant. + * You can bind it to locale, tenant, both or none. + */ + locale?: string; + tenant?: string; + modelDefinition: Omit; +} + +export const modelFactory = (params: Params): CmsModelPlugin => { + const { group, locale, tenant, modelDefinition } = params; + + return createCmsModel({ + group, + locale, + tenant, + ...modelDefinition, + noValidate: true + }); +}; \ No newline at end of file diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts new file mode 100644 index 00000000000..41f4b014ff1 --- /dev/null +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts @@ -0,0 +1,112 @@ +import { createModelField } from "../creteModelField"; +import { CmsModelField } from "@webiny/api-headless-cms/types"; + +const required = () => { + return { + name: "required", + message: "Value is required." + }; +}; + +const nameField = () => { + return createModelField({ + label: "Name", + type: "text", + validation: [required()] + }); +}; + +const versionField = () => { + return createModelField({ + label: "Version", + type: "text", + validation: [required()] + }); +}; + +const publishedField = () => { + return createModelField({ + label: "Published", + type: "text", + validation: [required()] + }); +}; + +const statusField = () => { + return createModelField({ + label: "Status", + type: "text", + validation: [required()] + }); +}; + +const fieldIdField = () => { + return createModelField({ + label: "FieldId", + type: "text", + validation: [required()] + }); +}; + +const fieldsField = (fields: CmsModelField[]) => { + return createModelField({ + label: "Fields", + type: "object", + validation: [required()], + multipleValues: true, + settings: { + fields + } + }); +}; + +// const stepLayoutField = () => { +// return createModelField({ +// label: "Layout", +// type: "object", +// validation: [required()], +// multipleValues: true +// }); +// }; + +const stepTitleField = () => { + return createModelField({ + label: "FieldId", + type: "text", + validation: [required()] + }); +}; + +const stepsField = (fields: CmsModelField[]) => { + return createModelField({ + label: "Steps", + type: "object", + validation: [required()], + multipleValues: true, + settings: { + fields + } + }); +}; + +const DEFAULT_FIELDS = ["name", "version", "published", "status", "fields", "steps"]; + +export const createFormDataModelDefinition = (): any => { + return { + name: "FbForm", + modelId: "fbForm", + titleFieldId: "name", + layout: DEFAULT_FIELDS.map(field => [field]), + fields: [ + nameField(), + versionField(), + publishedField(), + statusField(), + fieldsField([fieldIdField()]), + stepsField([stepTitleField()]) + ], + description: "Form Builder - Form builder create data model", + isPrivate: true, + noValidate: true + }; +}; diff --git a/packages/api-form-builder/src/index.ts b/packages/api-form-builder/src/index.ts index 53cc151ac07..18c93784914 100644 --- a/packages/api-form-builder/src/index.ts +++ b/packages/api-form-builder/src/index.ts @@ -1,24 +1,23 @@ -import createCruds from "./plugins/crud"; -import graphql from "./plugins/graphql"; +import { setupFormBuilderContext } from "./plugins/crud"; import triggerHandlers from "./plugins/triggers"; import validators from "./plugins/validators"; -import formsGraphQL from "./plugins/graphql/form"; -import formSettingsGraphQL from "./plugins/graphql/formSettings"; import formBuilderPrerenderingPlugins from "~/plugins/prerenderingHooks"; -import { FormBuilderStorageOperations } from "~/types"; +import { FormBuilderStorageOperations, FormBuilderContext } from "~/types"; +import { FormsPermissions } from "./plugins/crud/permissions/FormsPermissions"; export interface CreateFormBuilderParams { storageOperations: FormBuilderStorageOperations; + formsPermissions: FormsPermissions; + getTenant: any; + getLocale: any; + context: FormBuilderContext; } export const createFormBuilder = (params: CreateFormBuilderParams) => { return [ - createCruds(params), - graphql, + setupFormBuilderContext(params), triggerHandlers, validators, - formsGraphQL, - formSettingsGraphQL, formBuilderPrerenderingPlugins() ]; }; diff --git a/packages/api-form-builder/src/plugins/crud/forms.crud.ts b/packages/api-form-builder/src/plugins/crud/forms.crud.ts index e750165325c..cca92372c52 100644 --- a/packages/api-form-builder/src/plugins/crud/forms.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/forms.crud.ts @@ -194,9 +194,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { } try { - const { items } = await this.storageOperations.listForms(listFormParams); - - return items; + return await this.storageOperations.listForms(listFormParams); } catch (ex) { throw new WebinyError( ex.message || "Could not list all forms by given params", @@ -214,14 +212,16 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { await this.getForm(`${pid}#${revisionNumber}`, options); try { - return await this.storageOperations.listFormRevisions({ + const result = (await this.storageOperations.listFormRevisions({ where: { - id, + id: `${pid}#${revisionNumber}`, tenant: getTenant().id, locale: getLocale().code }, sort: ["version_ASC"] - }); + })) as unknown as FbForm[][]; + + return result[0]; } catch (ex) { throw new WebinyError( ex.message || "Could not list form revisions.", @@ -367,10 +367,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { await onFormBeforeCreate.publish({ form }); - const result = await this.storageOperations.createForm({ - input, - form - }); + const result = await this.storageOperations.createForm({ form, input }); await onFormAfterCreate.publish({ form: result }); diff --git a/packages/api-form-builder/src/plugins/crud/index.ts b/packages/api-form-builder/src/plugins/crud/index.ts index a4df2a2e981..55ddb3c8de5 100644 --- a/packages/api-form-builder/src/plugins/crud/index.ts +++ b/packages/api-form-builder/src/plugins/crud/index.ts @@ -1,5 +1,4 @@ -import { FormBuilderContext, FormBuilderStorageOperations } from "~/types"; -import { ContextPlugin } from "@webiny/api"; +import { FormBuilderStorageOperations } from "~/types"; import { createSystemCrud } from "~/plugins/crud/system.crud"; import { createSettingsCrud } from "~/plugins/crud/settings.crud"; import { createFormsCrud } from "~/plugins/crud/forms.crud"; @@ -12,97 +11,96 @@ export interface CreateFormBuilderCrudParams { storageOperations: FormBuilderStorageOperations; } -export default (params: CreateFormBuilderCrudParams) => { - const { storageOperations } = params; +export const setupFormBuilderContext = async (params: any) => { + const { storageOperations, context } = params; - return new ContextPlugin(async context => { - const getLocale = () => { - const locale = context.i18n.getContentLocale(); - if (!locale) { - throw new WebinyError( - "Missing locale on context.i18n locale in API Form Builder.", - "LOCALE_ERROR" - ); - } - return locale; - }; - - const getIdentity = () => { - return context.security.getIdentity(); - }; - - const getTenant = () => { - return context.tenancy.getCurrentTenant(); - }; - - if (storageOperations.beforeInit) { - try { - await storageOperations.beforeInit(context); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not run before init in Form Builder storage operations.", - ex.code || "STORAGE_OPERATIONS_BEFORE_INIT_ERROR", - { - ...ex - } - ); - } + const getLocale = () => { + const locale = context.i18n.getContentLocale(); + if (!locale) { + throw new WebinyError( + "Missing locale on context.i18n locale in API Form Builder.", + "LOCALE_ERROR" + ); } + return locale; + }; - const basePermissionsArgs = { - getIdentity, - fullAccessPermissionName: "fb.*" - }; - - const formsPermissions = new FormsPermissions({ - ...basePermissionsArgs, - getPermissions: () => context.security.getPermissions("fb.form") - }); - - const settingsPermissions = new SettingsPermissions({ - ...basePermissionsArgs, - getPermissions: () => context.security.getPermissions("fb.settings") - }); + const getIdentity = () => { + return context.security.getIdentity(); + }; - context.formBuilder = { - storageOperations, - ...createSystemCrud({ - getIdentity, - getTenant, - getLocale, - context - }), - ...createSettingsCrud({ - getTenant, - getLocale, - settingsPermissions, - context - }), - ...createFormsCrud({ - getTenant, - getLocale, - formsPermissions, - context - }), - ...createSubmissionsCrud({ - context, - formsPermissions - }) - }; + const getTenant = () => { + return context.tenancy.getCurrentTenant(); + }; - if (!storageOperations.init) { - return; - } + if (storageOperations.beforeInit) { try { - await storageOperations.init(context); + await storageOperations.beforeInit(context); } catch (ex) { throw new WebinyError( - ex.message || "Could not run init in Form Builder storage operations.", - ex.code || "STORAGE_OPERATIONS_INIT_ERROR", + ex.message || "Could not run before init in Form Builder storage operations.", + ex.code || "STORAGE_OPERATIONS_BEFORE_INIT_ERROR", { ...ex } ); } + } + + const basePermissionsArgs = { + getIdentity, + fullAccessPermissionName: "fb.*" + }; + + const formsPermissions = new FormsPermissions({ + ...basePermissionsArgs, + getPermissions: () => context.security.getPermissions("fb.form") + }); + + const settingsPermissions = new SettingsPermissions({ + ...basePermissionsArgs, + getPermissions: () => context.security.getPermissions("fb.settings") }); + + context.formBuilder = { + storageOperations, + ...createSystemCrud({ + getIdentity, + getTenant, + getLocale, + context + }), + ...createSettingsCrud({ + getTenant, + getLocale, + settingsPermissions, + context + }), + ...createFormsCrud({ + getTenant, + getLocale, + formsPermissions, + context + }), + ...createSubmissionsCrud({ + context, + formsPermissions + }) + }; + + if (!storageOperations.init) { + return; + } + + try { + await storageOperations.init(context); + } catch (ex) { + throw new WebinyError( + ex.message || "Could not run init in Form Builder storage operations.", + ex.code || "STORAGE_OPERATIONS_INIT_ERROR", + { + ...ex + } + ); + } }; diff --git a/packages/api-form-builder/src/plugins/graphql.ts b/packages/api-form-builder/src/plugins/graphql.ts index c14043d04d0..5eb706b8f62 100644 --- a/packages/api-form-builder/src/plugins/graphql.ts +++ b/packages/api-form-builder/src/plugins/graphql.ts @@ -1,13 +1,11 @@ -import { GraphQLSchemaPlugin } from "@webiny/handler-graphql/types"; +import { GraphQLSchemaPlugin } from "@webiny/handler-graphql"; import { ErrorResponse, Response } from "@webiny/handler-graphql"; import { FormBuilderContext } from "~/types"; const emptyResolver = () => ({}); -const plugin: GraphQLSchemaPlugin = { - type: "graphql-schema", - name: "graphql-schema-formBuilder", - schema: { +export const createBaseSchema = () => { + const baseSchema = new GraphQLSchemaPlugin({ typeDefs: /* GraphQL */ ` type FbBooleanResponse { data: Boolean @@ -86,7 +84,7 @@ const plugin: GraphQLSchemaPlugin = { } } } - } -}; + }); -export default plugin; + return baseSchema; +}; diff --git a/packages/api-form-builder/src/plugins/graphql/form.ts b/packages/api-form-builder/src/plugins/graphql/form.ts index b81bcca0a09..3c91ef05bb8 100644 --- a/packages/api-form-builder/src/plugins/graphql/form.ts +++ b/packages/api-form-builder/src/plugins/graphql/form.ts @@ -6,14 +6,13 @@ import { NotFoundResponse, Response } from "@webiny/handler-graphql/responses"; -import { GraphQLSchemaPlugin } from "@webiny/handler-graphql/types"; import { sanitizeFormSubmissionData, flattenSubmissionMeta } from "~/plugins/crud/utils"; import { FormBuilderContext, FbFormField } from "~/types"; import { mdbid } from "@webiny/utils"; +import { GraphQLSchemaPlugin } from "@webiny/handler-graphql"; -const plugin: GraphQLSchemaPlugin = { - type: "graphql-schema", - schema: { +export const createFormSchema = () => { + const formSchema = new GraphQLSchemaPlugin({ typeDefs: /* GraphQL */ ` enum FbFormStatusEnum { published @@ -378,9 +377,9 @@ const plugin: GraphQLSchemaPlugin = { }, listForms: async (_, __, { formBuilder }) => { try { - const forms = await formBuilder.listForms(); + const [data, meta] = await formBuilder.listForms(); - return new ListResponse(forms); + return new ListResponse(data, meta); } catch (e) { return new ErrorResponse(e); } @@ -661,7 +660,7 @@ const plugin: GraphQLSchemaPlugin = { } } } - } -}; + }); -export default plugin; + return formSchema; +}; diff --git a/packages/api-form-builder/src/plugins/graphql/formSettings.ts b/packages/api-form-builder/src/plugins/graphql/formSettings.ts index 17737092e83..a98d52beb22 100644 --- a/packages/api-form-builder/src/plugins/graphql/formSettings.ts +++ b/packages/api-form-builder/src/plugins/graphql/formSettings.ts @@ -1,10 +1,9 @@ -import { GraphQLSchemaPlugin } from "@webiny/handler-graphql/types"; +import { GraphQLSchemaPlugin } from "@webiny/handler-graphql"; import { ErrorResponse, Response } from "@webiny/handler-graphql/responses"; import { FormBuilderContext } from "~/types"; -const plugin: GraphQLSchemaPlugin = { - type: "graphql-schema", - schema: { +export const createFormBuilderSettingsSchema = () => { + const formBuilderSettingGraphQL = new GraphQLSchemaPlugin({ typeDefs: /* GraphQL */ ` type FbReCaptchaSettings { enabled: Boolean @@ -65,7 +64,7 @@ const plugin: GraphQLSchemaPlugin = { } } } - } -}; + }); -export default plugin; + return formBuilderSettingGraphQL; +} diff --git a/packages/api-form-builder/src/types.ts b/packages/api-form-builder/src/types.ts index 8d69c9ba340..fc87d16bb65 100644 --- a/packages/api-form-builder/src/types.ts +++ b/packages/api-form-builder/src/types.ts @@ -223,7 +223,7 @@ export interface OnFormAfterUnpublishTopicParams { export interface FormsCRUD { getForm(id: string, options?: FormBuilderGetFormOptions): Promise; getFormStats(id: string): Promise; - listForms(): Promise; + listForms(): Promise; createForm(data: FormCreateInput): Promise; updateForm(id: string, data: Partial): Promise; deleteForm(id: string): Promise; @@ -586,14 +586,15 @@ export interface FormBuilderStorageOperationsListFormRevisionsParams { * @category StorageOperations * @category StorageOperationsParams */ -export interface FormBuilderStorageOperationsListFormsResponse { - items: FbForm[]; - meta: { + +export type FormBuilderStorageOperationsListFormsResponse = [ + FbForm[], + { hasMoreItems: boolean; cursor: string | null; totalCount: number; - }; -} + } +]; /** * @category StorageOperations diff --git a/packages/api-headless-cms/src/crud/contentEntry.crud.ts b/packages/api-headless-cms/src/crud/contentEntry.crud.ts index 9c67b97ca65..8f010d16094 100644 --- a/packages/api-headless-cms/src/crud/contentEntry.crud.ts +++ b/packages/api-headless-cms/src/crud/contentEntry.crud.ts @@ -189,7 +189,7 @@ interface DeleteEntryParams { const createEntryId = (input: CreateCmsEntryInput) => { let entryId = mdbid(); if (input.id) { - if (input.id.match(/^([a-zA-Z0-9])([a-zA-Z0-9\-]+)([a-zA-Z0-9])$/) === null) { + if (input.id.match(/^([a-zA-Z0-9])([a-zA-Z0-9\-#]+)([a-zA-Z0-9])$/) === null) { throw new WebinyError( "The provided ID is not valid. It must be a string which can be A-Z, a-z, 0-9, - and it cannot start or end with a -.", "INVALID_ID", From 02af88f4eae13b2717335b3931ae96e82cba079d Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Wed, 1 Nov 2023 14:16:51 +0000 Subject: [PATCH 02/37] feat: moved submission methods, revision methods, deleteForm into HCMS --- .../src/operations/form/index.ts | 5 +- .../src/operations/form/index.ts | 299 +----------------- .../src/operations/submission/index.ts | 167 +--------- .../CmsFormBuilderStorage.ts | 69 ++-- .../CmsSubmissionsStorage.ts | 106 +++++++ .../FormBuilderContextSetup.ts | 30 +- .../ListSubmissionsWhereProcessor.ts | 31 ++ .../createFormBuilderPlugins.ts | 6 +- .../cmsFormBuilderStorage/creteModelField.ts | 4 +- .../models/form.model.ts | 296 +++++++++++++++-- .../models/submission.model.ts | 157 +++++++++ .../src/plugins/crud/forms.crud.ts | 38 +-- .../src/plugins/crud/submissions.crud.ts | 2 +- packages/api-form-builder/src/types.ts | 10 +- .../src/crud/contentEntry.crud.ts | 2 +- .../src/graphqlFields/index.ts | 4 +- .../src/graphqlFields/json.ts | 35 ++ packages/api-headless-cms/src/types.ts | 1 + .../src/components/Form/FormRender.tsx | 5 + 19 files changed, 736 insertions(+), 531 deletions(-) create mode 100644 packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts create mode 100644 packages/api-form-builder/src/cmsFormBuilderStorage/ListSubmissionsWhereProcessor.ts create mode 100644 packages/api-form-builder/src/cmsFormBuilderStorage/models/submission.model.ts create mode 100644 packages/api-headless-cms/src/graphqlFields/json.ts diff --git a/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts b/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts index d23ab1341f2..cbe09bdb08b 100644 --- a/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts +++ b/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts @@ -456,10 +456,7 @@ export const createFormStorageOperations = ( cursor: items.length > 0 ? encodeCursor(hits[items.length - 1].sort) || null : null }; - return { - items, - meta - }; + return [items, meta]; }; const listFormRevisions = async ( diff --git a/packages/api-form-builder-so-ddb/src/operations/form/index.ts b/packages/api-form-builder-so-ddb/src/operations/form/index.ts index f6fb5eaa62e..ff0659eabb1 100644 --- a/packages/api-form-builder-so-ddb/src/operations/form/index.ts +++ b/packages/api-form-builder-so-ddb/src/operations/form/index.ts @@ -1,16 +1,9 @@ import WebinyError from "@webiny/error"; import { FbForm, - FormBuilderStorageOperationsCreateFormFromParams, - FormBuilderStorageOperationsCreateFormParams, - FormBuilderStorageOperationsDeleteFormParams, - FormBuilderStorageOperationsDeleteFormRevisionParams, FormBuilderStorageOperationsGetFormParams, FormBuilderStorageOperationsListFormRevisionsParams, FormBuilderStorageOperationsListFormRevisionsParamsWhere, - FormBuilderStorageOperationsListFormsParams, - FormBuilderStorageOperationsListFormsResponse, - FormBuilderStorageOperationsPublishFormParams, FormBuilderStorageOperationsUnpublishFormParams, FormBuilderStorageOperationsUpdateFormParams } from "@webiny/api-form-builder/types"; @@ -28,15 +21,8 @@ import { FormBuilderFormStorageOperations } from "~/types"; import { FormDynamoDbFieldPlugin } from "~/plugins/FormDynamoDbFieldPlugin"; -import { decodeCursor, encodeCursor } from "@webiny/db-dynamodb/utils/cursor"; import { get } from "@webiny/db-dynamodb/utils/get"; -type DbRecord = T & { - PK: string; - SK: string; - TYPE: string; -}; - interface Keys { PK: string; SK: string; @@ -158,49 +144,10 @@ export const createFormStorageOperations = ( ); }; - const createFormFrom = async ( - params: FormBuilderStorageOperationsCreateFormFromParams - ): Promise => { - const { form, original, latest } = params; - - const revisionKeys = createRevisionKeys(form); - const latestKeys = createLatestKeys(form); - const gsiKeys = createGSIKeys(form); - - const items = [ - entity.putBatch({ - ...form, - ...revisionKeys, - ...gsiKeys, - TYPE: createFormType() - }), - entity.putBatch({ - ...form, - ...latestKeys, - TYPE: createFormLatestType() - }) - ]; - - try { - await batchWriteAll({ - table, - items - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not create form data in the table, from existing form.", - ex.code || "CREATE_FORM_FROM_ERROR", - { - revisionKeys, - latestKeys, - original, - form, - latest - } - ); - } - - return form; + const createFormFrom = async () => { + throw new Error( + "api-form-builder-ddb does not implement the Form Builder storage operations." + ); }; const updateForm = async ( @@ -383,236 +330,22 @@ export const createFormStorageOperations = ( }); }; - const deleteForm = async ( - params: FormBuilderStorageOperationsDeleteFormParams - ): Promise => { - const { form } = params; - let items: any[]; - /** - * This will find all form records. - */ - const queryAllParams: QueryAllParams = { - entity, - partitionKey: createFormPartitionKey(form), - options: { - beginsWith: form.formId - } - }; - try { - items = await queryAll(queryAllParams); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not query forms and submissions by given params.", - ex.code || "QUERY_FORM_AND_SUBMISSIONS_ERROR", - { - partitionKey: queryAllParams.partitionKey, - options: queryAllParams.options - } - ); - } - - let hasLatestPublishedRecord = false; - - const deleteItems = items.map(item => { - if (!hasLatestPublishedRecord && item.published) { - hasLatestPublishedRecord = true; - } - return entity.deleteBatch({ - PK: item.PK, - SK: item.SK - }); - }); - if (hasLatestPublishedRecord) { - deleteItems.push(entity.deleteBatch(createLatestPublishedKeys(items[0]))); - } - - deleteItems.push(entity.deleteBatch(createLatestKeys(items[0]))); - - try { - await batchWriteAll({ - table, - items: deleteItems - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not delete form and it's submissions.", - ex.code || "DELETE_FORM_AND_SUBMISSIONS_ERROR" - ); - } - return form; + const deleteForm = async () => { + throw new Error( + "api-form-builder-ddb does not implement the Form Builder storage operations." + ); }; - /** - * We need to: - * - delete current revision - * - get previously published revision and update the record if it exists or delete if it does not - * - update latest record if current one is the latest - */ - const deleteFormRevision = async ( - params: FormBuilderStorageOperationsDeleteFormRevisionParams - ): Promise => { - const { form, revisions, previous } = params; - - const revisionKeys = createRevisionKeys(form); - const latestKeys = createLatestKeys(form); - - const latestForm = revisions[0]; - const latestPublishedForm = revisions.find(rev => rev.published === true); - - const isLatest = latestForm ? latestForm.id === form.id : false; - const isLatestPublished = latestPublishedForm ? latestPublishedForm.id === form.id : false; - - const items = [entity.deleteBatch(revisionKeys)]; - if (isLatest || isLatestPublished) { - /** - * Sort out the latest published record. - */ - if (isLatestPublished) { - const previouslyPublishedForm = revisions - .filter(f => !!f.publishedOn && f.version !== form.version) - .sort((a, b) => { - return ( - new Date(b.publishedOn as string).getTime() - - new Date(a.publishedOn as string).getTime() - ); - }) - .shift(); - if (previouslyPublishedForm) { - items.push( - entity.putBatch({ - ...previouslyPublishedForm, - ...createLatestPublishedKeys(previouslyPublishedForm), - GSI1_PK: null, - GSI1_SK: null, - TYPE: createFormLatestPublishedType() - }) - ); - } else { - items.push(entity.deleteBatch(createLatestPublishedKeys(form))); - } - } - /** - * Sort out the latest record. - */ - if (isLatest) { - items.push( - entity.putBatch({ - ...previous, - ...latestKeys, - GSI1_PK: null, - GSI1_SK: null, - TYPE: createFormLatestType() - }) - ); - } - } - /** - * Now save the batch data. - */ - try { - await batchWriteAll({ - table, - items - }); - return form; - } catch (ex) { - throw new WebinyError( - ex.message || "Could not delete form revision from table.", - ex.code || "DELETE_FORM_REVISION_ERROR", - { - form, - latestForm, - revisionKeys, - latestKeys - } - ); - } + const deleteFormRevision = async () => { + throw new Error( + "api-form-builder-ddb does not implement the Form Builder storage operations." + ); }; - /** - * We need to save form in: - * - regular form record - * - latest published form record - * - latest form record - if form is latest one - * - elasticsearch latest form record - */ - const publishForm = async ( - params: FormBuilderStorageOperationsPublishFormParams - ): Promise => { - const { form, original } = params; - - const revisionKeys = createRevisionKeys(form); - - const latestKeys = createLatestKeys(form); - - const latestPublishedKeys = createLatestPublishedKeys(form); - - const gsiKeys = { - GSI1_PK: createFormGSIPartitionKey(form), - GSI1_SK: createGSISortKey(form.version) - }; - - const { locale, tenant, formId } = form; - - const latestForm = await getForm({ - where: { - formId, - tenant, - locale, - latest: true - } - }); - - const isLatestForm = latestForm ? latestForm.id === form.id : false; - /** - * Update revision and latest published records - */ - const items = [ - entity.putBatch({ - ...form, - ...revisionKeys, - ...gsiKeys, - TYPE: createFormType() - }), - entity.putBatch({ - ...form, - ...latestPublishedKeys, - TYPE: createFormLatestPublishedType() - }) - ]; - /** - * Update the latest form as well - */ - if (isLatestForm) { - items.push( - entity.putBatch({ - ...form, - ...latestKeys, - TYPE: createFormLatestType() - }) - ); - } - - try { - await batchWriteAll({ - table, - items - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not publish form.", - ex.code || "PUBLISH_FORM_ERROR", - { - form, - original, - latestForm, - revisionKeys, - latestKeys, - latestPublishedKeys - } - ); - } - return form; + const publishForm = async () => { + throw new Error( + "api-form-builder-ddb does not implement the Form Builder storage operations." + ); }; /** diff --git a/packages/api-form-builder-so-ddb/src/operations/submission/index.ts b/packages/api-form-builder-so-ddb/src/operations/submission/index.ts index 4a07186a5e1..22cb442e434 100644 --- a/packages/api-form-builder-so-ddb/src/operations/submission/index.ts +++ b/packages/api-form-builder-so-ddb/src/operations/submission/index.ts @@ -1,11 +1,7 @@ import { FbSubmission, - FormBuilderStorageOperationsCreateSubmissionParams, FormBuilderStorageOperationsDeleteSubmissionParams, - FormBuilderStorageOperationsGetSubmissionParams, - FormBuilderStorageOperationsListSubmissionsParams, - FormBuilderStorageOperationsListSubmissionsResponse, - FormBuilderStorageOperationsUpdateSubmissionParams + FormBuilderStorageOperationsGetSubmissionParams } from "@webiny/api-form-builder/types"; import { Entity, Table } from "dynamodb-toolbox"; import WebinyError from "@webiny/error"; @@ -16,11 +12,6 @@ import { } from "~/types"; import { cleanupItem } from "@webiny/db-dynamodb/utils/cleanup"; import { parseIdentifier } from "@webiny/utils"; -import { queryAll, QueryAllParams } from "@webiny/db-dynamodb/utils/query"; -import { decodeCursor, encodeCursor } from "@webiny/db-dynamodb/utils/cursor"; -import { sortItems } from "@webiny/db-dynamodb/utils/sort"; -import { filterItems } from "@webiny/db-dynamodb/utils/filter"; -import { FormSubmissionDynamoDbFieldPlugin } from "~/plugins/FormSubmissionDynamoDbFieldPlugin"; import { get } from "@webiny/db-dynamodb/utils/get"; export interface CreateSubmissionStorageOperationsParams { @@ -32,7 +23,7 @@ export interface CreateSubmissionStorageOperationsParams { export const createSubmissionStorageOperations = ( params: CreateSubmissionStorageOperationsParams ): FormBuilderSubmissionStorageOperations => { - const { entity, plugins } = params; + const { entity } = params; const createSubmissionPartitionKey = ( params: FormBuilderSubmissionStorageOperationsCreatePartitionKeyParams @@ -47,70 +38,19 @@ export const createSubmissionStorageOperations = ( return id; }; - const createSubmissionType = () => { - return "fb.formSubmission"; - }; - - const createSubmission = async ( - params: FormBuilderStorageOperationsCreateSubmissionParams - ): Promise => { - const { submission, form } = params; - const keys = { - PK: createSubmissionPartitionKey(form), - SK: createSubmissionSortKey(submission.id) - }; - - try { - await entity.put({ - ...submission, - ...keys, - TYPE: createSubmissionType() - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not create form submission in the DynamoDB.", - ex.code || "UPDATE_FORM_SUBMISSION_ERROR", - { - submission, - form, - keys - } - ); - } - - return submission; + const createSubmission = () => { + throw new Error( + "api-form-builder-ddb does not implement the Form Builder storage operations." + ); }; - const updateSubmission = async ( - params: FormBuilderStorageOperationsUpdateSubmissionParams - ): Promise => { - const { submission, form, original } = params; - const keys = { - PK: createSubmissionPartitionKey(form), - SK: createSubmissionSortKey(submission.id) - }; - - try { - await entity.put({ - ...submission, - ...keys, - TYPE: createSubmissionType() - }); - return submission; - } catch (ex) { - throw new WebinyError( - ex.message || "Could not update form submission in the DynamoDB.", - ex.code || "UPDATE_FORM_SUBMISSION_ERROR", - { - submission, - original, - form, - keys - } - ); - } + const updateSubmission = () => { + throw new Error( + "api-form-builder-ddb does not implement the Form Builder storage operations." + ); }; + // Skipped when moving backend to HCMS. const deleteSubmission = async ( params: FormBuilderStorageOperationsDeleteSubmissionParams ): Promise => { @@ -138,90 +78,13 @@ export const createSubmissionStorageOperations = ( return submission; }; - const listSubmissions = async ( - params: FormBuilderStorageOperationsListSubmissionsParams - ): Promise => { - const { where: initialWhere, sort, limit = 100000, after } = params; - - const { tenant, locale, formId } = initialWhere; - - const where: Partial = { - ...initialWhere - }; - /** - * We need to remove conditions so we do not filter by them again. - */ - delete where.tenant; - delete where.locale; - delete where.formId; - - const queryAllParams: QueryAllParams = { - entity, - partitionKey: createSubmissionPartitionKey({ - tenant, - locale, - formId - }), - options: { - gte: " ", - reverse: true - } - }; - - let results; - try { - results = await queryAll(queryAllParams); - } catch (ex) { - throw new WebinyError( - ex.message || "Could list form submissions.", - ex.code || "LIST_SUBMISSIONS_ERROR", - { - where: initialWhere, - partitionKey: queryAllParams.partitionKey - } - ); - } - - const fields = plugins.byType( - FormSubmissionDynamoDbFieldPlugin.type + const listSubmissions = () => { + throw new Error( + "api-form-builder-ddb does not implement the Form Builder storage operations." ); - - const filteredSubmissions = filterItems({ - plugins, - items: results, - where, - fields - }); - - const sortedSubmissions = sortItems({ - items: filteredSubmissions, - sort, - fields - }); - - const totalCount = sortedSubmissions.length; - const start = parseInt(decodeCursor(after) || "0") || 0; - const hasMoreItems = totalCount > start + limit; - const end = limit > totalCount + start + limit ? undefined : start + limit; - const items = sortedSubmissions.slice(start, end); - /** - * Although we do not need a cursor here, we will use it as such to keep it standardized. - * Number is simply encoded. - */ - const cursor = items.length > 0 ? encodeCursor(start + limit) : null; - - const meta = { - hasMoreItems, - totalCount, - cursor - }; - - return { - items, - meta - }; }; + // Skipped when moving backend to HCMS. const getSubmission = async ( params: FormBuilderStorageOperationsGetSubmissionParams ): Promise => { diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts index df651fd0333..c0a58e1ce09 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts @@ -1,6 +1,10 @@ import { CmsEntry, CmsModel, HeadlessCms } from "@webiny/api-headless-cms/types"; import { Security } from "@webiny/api-security/types"; -import { FbForm, FormBuilderStorageOperationsListFormsParams } from "~/types"; +import { + FbForm, + FormBuilderStorageOperationsListFormsParams, + FormBuilderStorageOperationsListFormRevisionsParams +} from "~/types"; interface ModelContext { tenant: string; @@ -12,12 +16,12 @@ export class CmsFormBuilderStorage { private readonly security: Security; private readonly model: CmsModel; - static async create(params: { formModel: CmsModel; cms: HeadlessCms; security: Security }) { - return new CmsFormBuilderStorage(params.formModel, params.cms, params.security); + static async create(params: { model: CmsModel; cms: HeadlessCms; security: Security }) { + return new CmsFormBuilderStorage(params.model, params.cms, params.security); } - private constructor(formModel: CmsModel, cms: HeadlessCms, security: Security) { - this.model = formModel; + private constructor(model: CmsModel, cms: HeadlessCms, security: Security) { + this.model = model; this.cms = cms; this.security = security; } @@ -37,30 +41,21 @@ export class CmsFormBuilderStorage { getForm = async ({ where }: FormBuilderStorageOperationsListFormsParams) => { const { id, tenant, locale } = where; const model = this.modelWithContext({ tenant, locale }); - const entry = await this.security.withoutAuthorization(() => { - return this.cms.getEntry(model, { where: { entryId: id, latest: true } }); + const entry = await this.security.withoutAuthorization(async () => { + return await this.cms.getEntryById(model, id || ""); }); return entry ? this.getFormFieldValues(entry) : null; }; - listFormRevisions = async (params: FormBuilderStorageOperationsListFormsParams) => { + listFormRevisions = async (params: FormBuilderStorageOperationsListFormRevisionsParams) => { const { - where: { tenant, locale, id } + where: { tenant, locale, formId } } = params; const model = this.modelWithContext({ tenant, locale }); - - const [entries, meta] = await this.security.withoutAuthorization(async () => { - return await this.cms.listLatestEntries(model, { - after: params.after, - limit: params.limit, - sort: params.sort, - where: { - entryId: id - } - }); + const entries = await this.security.withoutAuthorization(async () => { + return await this.cms.getEntryRevisions(model, `${formId}#0001`); }); - - return [entries.map(entry => this.getFormFieldValues(entry)), meta]; + return [entries.map(entry => this.getFormFieldValues(entry))]; }; listForms = async (params: FormBuilderStorageOperationsListFormsParams) => { @@ -81,15 +76,45 @@ export class CmsFormBuilderStorage { return [entries.map(entry => this.getFormFieldValues(entry)), meta]; }; + // [WIP] + createFormFrom = async (params: any) => { + const { form } = params; + const model = this.modelWithContext(form); + const entry = await this.security.withoutAuthorization(async () => { + return await this.cms.createEntryRevisionFrom(model, form.id, {}); + }); + return entry ? this.getFormFieldValues(entry) : null; + }; + + deleteForm = async ({ form }: { form: FbForm }) => { + const model = this.modelWithContext(form); + + await this.security.withoutAuthorization(async () => { + return await this.cms.deleteEntry(model, form.id, { + force: true + }); + }); + }; + + // [WIP] + deleteFormRevision = async ({ form }: { form: FbForm }) => { + const model = this.modelWithContext(form); + + await this.security.withoutAuthorization(async () => { + return await this.cms.deleteEntryRevision(model, form.id); + }); + }; + private getFormFieldValues(entry: CmsEntry) { return { - id: entry.entryId, + id: entry.id, createdBy: entry.createdBy, createdOn: entry.createdOn, savedOn: entry.savedOn, locale: entry.locale, tenant: entry.tenant, webinyVersion: entry.webinyVersion, + version: entry.version, ...entry.values } as FbForm; } diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts new file mode 100644 index 00000000000..2adc3036c4a --- /dev/null +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts @@ -0,0 +1,106 @@ +import omit from "lodash/omit"; + +import { CmsEntry, CmsModel, HeadlessCms } from "@webiny/api-headless-cms/types"; +import { Security } from "@webiny/api-security/types"; + +import { ListSubmissionsWhereProcessor } from "~/cmsFormBuilderStorage/ListSubmissionsWhereProcessor"; +import { + FormBuilderStorageOperationsCreateSubmissionParams, + FormBuilderStorageOperationsUpdateSubmissionParams, + FormBuilderStorageOperationsListSubmissionsParams +} from "~/types"; + +interface ModelContext { + tenant: string; + locale: string; +} + +export class CmsSubmissionsStorage { + private readonly cms: HeadlessCms; + private readonly security: Security; + private readonly model: CmsModel; + private readonly submissionsWhereProcessor: ListSubmissionsWhereProcessor; + + static async create(params: { model: CmsModel; cms: HeadlessCms; security: Security }) { + return new CmsSubmissionsStorage(params.model, params.cms, params.security); + } + + private constructor(model: CmsModel, cms: HeadlessCms, security: Security) { + this.model = model; + this.cms = cms; + this.security = security; + this.submissionsWhereProcessor = new ListSubmissionsWhereProcessor(); + } + + private modelWithContext({ tenant, locale }: ModelContext): CmsModel { + return { ...this.model, tenant, locale }; + } + + listSubmissions = async (params: FormBuilderStorageOperationsListSubmissionsParams) => { + const tenant = params.where.tenant; + const locale = params.where.locale; + + const model = this.modelWithContext({ tenant, locale }); + + const [entries, meta] = await this.security.withoutAuthorization(async () => { + const where = this.submissionsWhereProcessor.process(params.where); + return await this.cms.listLatestEntries(model, { + after: params.after, + limit: params.limit, + sort: params.sort, + where + }); + }); + + return { items: entries.map(entry => this.getSubmissionValues(entry)), meta }; + }; + + createSubmission = async ({ + submission + }: FormBuilderStorageOperationsCreateSubmissionParams) => { + const model = this.modelWithContext(submission); + const entry = await this.security.withoutAuthorization(() => { + return this.cms.createEntry(model, { ...submission }); + }); + + return this.getSubmissionValues(entry); + }; + + updateSubmission = async ({ + submission + }: FormBuilderStorageOperationsUpdateSubmissionParams) => { + const model = this.modelWithContext(submission); + + return await this.security.withoutAuthorization(async () => { + const entry = await this.cms.getEntry(model, { + where: { entryId: submission.id, latest: true } + }); + + const values = omit(submission, [ + "id", + "createdOn", + "createdBy", + "tenant", + "locale", + "webinyVersion" + ]); + + const updatedEntry = await this.cms.updateEntry(model, entry.id, values); + + return this.getSubmissionValues(updatedEntry); + }); + }; + + private getSubmissionValues(entry: CmsEntry) { + return { + id: entry.entryId, + createdBy: entry.createdBy, + createdOn: entry.createdOn, + savedOn: entry.savedOn, + locale: entry.locale, + tenant: entry.tenant, + webinyVersion: entry.webinyVersion, + ...entry.values + }; + } +} diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts index 652be6febc4..0f581fc9d70 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts @@ -5,6 +5,7 @@ import { SecurityPermission } from "@webiny/api-security/types"; import { createFormBuilderPlugins } from "./createFormBuilderPlugins"; import { CmsModelPlugin } from "@webiny/api-headless-cms"; import { CmsFormBuilderStorage } from "./CmsFormBuilderStorage"; +import { CmsSubmissionsStorage } from "./CmsSubmissionsStorage"; import { AppPermissions } from "@webiny/api-security/utils/AppPermissions"; class FormsPermissions extends AppPermissions {} @@ -80,17 +81,30 @@ export class FormBuilderContextSetup { private async setupCmsStorageOperations() { // This registers code plugins (model group, models) - const { groupPlugin, formModelDefinition } = createFormBuilderPlugins(); + const { groupPlugin, formModelDefinition, submissionModelDefinition } = + createFormBuilderPlugins(); // Finally, register all plugins - this.context.plugins.register([groupPlugin, new CmsModelPlugin(formModelDefinition)]); + this.context.plugins.register([ + groupPlugin, + new CmsModelPlugin(formModelDefinition), + new CmsModelPlugin(submissionModelDefinition) + ]); const formModel = await this.getModel("fbForm"); - - return await CmsFormBuilderStorage.create({ - formModel, - cms: this.context.cms, - security: this.context.security - }); + const submissionModel = await this.getModel("fbSubmission"); + + return { + ...(await CmsFormBuilderStorage.create({ + model: formModel, + cms: this.context.cms, + security: this.context.security + })), + ...(await CmsSubmissionsStorage.create({ + model: submissionModel, + cms: this.context.cms, + security: this.context.security + })) + }; } private async getModel(modelId: string) { diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/ListSubmissionsWhereProcessor.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/ListSubmissionsWhereProcessor.ts new file mode 100644 index 00000000000..8057e76e5e4 --- /dev/null +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/ListSubmissionsWhereProcessor.ts @@ -0,0 +1,31 @@ +import { CmsEntryListWhere } from "@webiny/api-headless-cms/types"; +import { FormBuilderStorageOperationsListSubmissionsParams } from "~/types"; + +type StandardSubmissionKey = keyof FormBuilderStorageOperationsListSubmissionsParams["where"]; +type CmsEntryListWhereKey = keyof CmsEntryListWhere; + +export class ListSubmissionsWhereProcessor { + private readonly skipKeys = ["tenant", "locale", "formId"]; + private readonly keyMap: Partial> = { + id_in: "entryId_in" + }; + + process(input: FormBuilderStorageOperationsListSubmissionsParams["where"]): CmsEntryListWhere { + const where: CmsEntryListWhere = input.formId ? { form: { parent: input.formId } } : {}; + + Object.keys(input) + .filter(key => !this.skipKeys.includes(key)) + .forEach(key => { + const remappedKey = this.keyMap[key as StandardSubmissionKey]; + const value = input[key as StandardSubmissionKey]; + + if (remappedKey && value !== undefined) { + where[remappedKey] = value; + } else if (value !== undefined) { + where[key] = value; + } + }); + + return where; + } +} diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderPlugins.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderPlugins.ts index 5839dfe5834..0d72e0df969 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderPlugins.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderPlugins.ts @@ -1,5 +1,6 @@ import { CmsGroupPlugin } from "@webiny/api-headless-cms"; import { createFormDataModelDefinition } from "./models/form.model"; +import { createSubmissionDataModelDefinition } from "./models/submission.model"; export const createFormBuilderPlugins = () => { const groupId = "contentModelGroup_fb"; @@ -15,6 +16,9 @@ export const createFormBuilderPlugins = () => { return { groupPlugin, - formModelDefinition: createFormDataModelDefinition() + formModelDefinition: createFormDataModelDefinition(groupPlugin.contentModelGroup), + submissionModelDefinition: createSubmissionDataModelDefinition( + groupPlugin.contentModelGroup + ) }; }; diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/creteModelField.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/creteModelField.ts index e351829b554..3d19f63da17 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/creteModelField.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/creteModelField.ts @@ -21,7 +21,7 @@ export const createModelField = (params: CreateModelFieldParams): CmsModelField } } = params; - const fieldId = initialFieldId ? camelCase(initialFieldId) : camelCase(label); + const fieldId = initialFieldId || camelCase(label); return { id: fieldId, @@ -35,4 +35,4 @@ export const createModelField = (params: CreateModelFieldParams): CmsModelField multipleValues, predefinedValues }; -} \ No newline at end of file +}; diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts index 41f4b014ff1..d3b4af9ceda 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts @@ -1,5 +1,5 @@ import { createModelField } from "../creteModelField"; -import { CmsModelField } from "@webiny/api-headless-cms/types"; +import { CmsModelField, CmsModelGroup } from "@webiny/api-headless-cms/types"; const required = () => { return { @@ -8,17 +8,18 @@ const required = () => { }; }; -const nameField = () => { +const formIdField = () => { return createModelField({ - label: "Name", + label: "Form ID", + fieldId: "formId", type: "text", validation: [required()] }); }; -const versionField = () => { +const nameField = () => { return createModelField({ - label: "Version", + label: "Name", type: "text", validation: [required()] }); @@ -40,15 +41,154 @@ const statusField = () => { }); }; +const field_IdField = () => { + return createModelField({ + label: "ID", + fieldId: "_id", + type: "text" + }); +}; + const fieldIdField = () => { return createModelField({ label: "FieldId", + type: "text" + }); +}; + +const fieldTypeField = () => { + return createModelField({ + label: "Type", + type: "text", + validation: [required()] + }); +}; + +const fieldNameField = () => { + return createModelField({ + label: "Name", type: "text", validation: [required()] }); }; -const fieldsField = (fields: CmsModelField[]) => { +const fieldLabelField = () => { + return createModelField({ + label: "Label", + type: "text", + validation: [required()] + }); +}; + +const fieldPlaceholderTextField = () => { + return createModelField({ + label: "PlaceholderText", + type: "text" + }); +}; + +const fieldHelpTextField = () => { + return createModelField({ + label: "HelpText", + type: "text" + }); +}; + +const fieldOptionsLabelField = () => { + return createModelField({ + label: "Label", + type: "text" + }); +}; + +const fieldOptionsValueField = () => { + return createModelField({ + label: "Value", + type: "text" + }); +}; + +const fieldOptionsField = (fields: CmsModelField[]) => { + return createModelField({ + label: "Options", + type: "object", + validation: [required()], + multipleValues: true, + settings: { + fields + } + }); +}; + +const fieldValidationNameField = () => { + return createModelField({ + label: "Name", + type: "text", + validation: [required()] + }); +}; + +const fieldValidationMessageField = () => { + return createModelField({ + label: "Message", + type: "text", + validation: [required()] + }); +}; + +const fieldValidationSettingsValuesField = () => { + return createModelField({ + label: "Values", + type: "object", + multipleValues: true + }); +}; + +const fieldValidationSettingsValueField = () => { + return createModelField({ + label: "Value", + type: "text" + }); +}; + +const fieldValidationSettingsPresetField = () => { + return createModelField({ + label: "Preset", + type: "text" + }); +}; + +const fieldValidationSettingsField = (fields: CmsModelField[]) => { + return createModelField({ + label: "Settings", + type: "object", + validation: [required()], + settings: { + fields + } + }); +}; + +const fieldValidationField = (fields: CmsModelField[]) => { + return createModelField({ + label: "Validation", + type: "object", + validation: [required()], + multipleValues: true, + settings: { + fields + } + }); +}; + +const fieldSettingsField = () => { + return createModelField({ + label: "Settings", + type: "object" + }); +}; + +export const fieldsField = (fields: CmsModelField[]) => { return createModelField({ label: "Fields", type: "object", @@ -60,24 +200,24 @@ const fieldsField = (fields: CmsModelField[]) => { }); }; -// const stepLayoutField = () => { -// return createModelField({ -// label: "Layout", -// type: "object", -// validation: [required()], -// multipleValues: true -// }); -// }; +const stepLayoutField = () => { + return createModelField({ + label: "Layout", + type: "json", + validation: [required()], + multipleValues: true + }); +}; const stepTitleField = () => { return createModelField({ - label: "FieldId", + label: "Title", type: "text", validation: [required()] }); }; -const stepsField = (fields: CmsModelField[]) => { +export const stepsField = (fields: CmsModelField[]) => { return createModelField({ label: "Steps", type: "object", @@ -89,24 +229,138 @@ const stepsField = (fields: CmsModelField[]) => { }); }; -const DEFAULT_FIELDS = ["name", "version", "published", "status", "fields", "steps"]; +const settingsReCaptchaEnabledField = () => { + return createModelField({ + label: "Enabled", + type: "text" + }); +}; + +const settingsReCaptchaErrorMessageField = () => { + return createModelField({ + label: "ErrorMessage", + type: "text" + }); +}; + +const settingsReCaptchaField = (fields: CmsModelField[]) => { + return createModelField({ + label: "reCaptcha", + type: "object", + validation: [required()], + settings: { + fields + } + }); +}; + +const settingsLayoutRendererField = () => { + return createModelField({ + label: "Renderer", + type: "text" + }); +}; + +const settingsLayoutField = (fields: CmsModelField[]) => { + return createModelField({ + label: "Layout", + type: "object", + settings: { + fields + } + }); +}; + +const settingsSubmitButtonLabelField = () => { + return createModelField({ + label: "SubmitButtonLabel", + type: "text" + }); +}; + +const settingsFullWidthSubmitButtonField = () => { + return createModelField({ + label: "FullWidthSubmitButton", + type: "text" + }); +}; + +const settingsSuccessMessageField = () => { + return createModelField({ + label: "SuccessMessage", + type: "text" + }); +}; + +const settingsTermsOfServiceMessageField = () => { + return createModelField({ + label: "TermsOfServiceMessage", + type: "text" + }); +}; + +const settingsField = (fields: CmsModelField[]) => { + return createModelField({ + label: "Settings", + type: "object", + settings: { + fields + } + }); +}; + +const DEFAULT_FIELDS = ["formId", "name", "published", "status", "fields", "steps", "settings"]; + +const SETTINGS_FIELDS: CmsModelField[] = [ + settingsLayoutField([settingsLayoutRendererField()]), + settingsSubmitButtonLabelField(), + settingsFullWidthSubmitButtonField(), + settingsSuccessMessageField(), + settingsTermsOfServiceMessageField(), + settingsReCaptchaField([settingsReCaptchaEnabledField(), settingsReCaptchaErrorMessageField()]) +]; + +export const FIELD_FIELDS = [ + field_IdField(), + fieldIdField(), + fieldTypeField(), + fieldNameField(), + fieldLabelField(), + fieldPlaceholderTextField(), + fieldHelpTextField(), + fieldOptionsField([fieldOptionsLabelField(), fieldOptionsValueField()]), + fieldValidationField([ + fieldValidationNameField(), + fieldValidationMessageField(), + fieldValidationSettingsField([ + fieldValidationSettingsValuesField(), + fieldValidationSettingsValueField(), + fieldValidationSettingsPresetField() + ]) + ]), + fieldSettingsField() +]; + +export const STEP_FIELDS = [stepTitleField(), stepLayoutField()]; -export const createFormDataModelDefinition = (): any => { +export const createFormDataModelDefinition = (group: CmsModelGroup): any => { return { name: "FbForm", modelId: "fbForm", titleFieldId: "name", layout: DEFAULT_FIELDS.map(field => [field]), fields: [ + formIdField(), nameField(), - versionField(), publishedField(), statusField(), - fieldsField([fieldIdField()]), - stepsField([stepTitleField()]) + fieldsField(FIELD_FIELDS), + stepsField(STEP_FIELDS), + settingsField(SETTINGS_FIELDS) ], description: "Form Builder - Form builder create data model", isPrivate: true, + group, noValidate: true }; }; diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/submission.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/submission.model.ts new file mode 100644 index 00000000000..4d1c1ccd151 --- /dev/null +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/submission.model.ts @@ -0,0 +1,157 @@ +import { CmsModelField, CmsModelGroup } from "@webiny/api-headless-cms/types"; +import { CmsPrivateModelFull } from "@webiny/api-headless-cms"; + +import { fieldsField, stepsField, FIELD_FIELDS, STEP_FIELDS } from "./form.model"; +import { createModelField } from "../creteModelField"; + +const required = () => { + return { + name: "required", + message: "Value is required." + }; +}; + +const dataField = () => { + return createModelField({ + label: "Data", + fieldId: "data", + type: "json", + validation: [required()] + }); +}; + +const metaIpField = () => { + return createModelField({ + label: "IP", + fieldId: "ip", + type: "text" + }); +}; + +const metaSubmittedOnField = () => { + return createModelField({ + label: "Submitted On", + fieldId: "submittedOn", + type: "text" + }); +}; + +const metaUrlLocationField = () => { + return createModelField({ + label: "Location", + fieldId: "location", + type: "text" + }); +}; + +const metaUrlQueryField = () => { + return createModelField({ + label: "Query", + fieldId: "query", + type: "json" + }); +}; + +const metaUrlField = (fields: CmsModelField[]) => { + return createModelField({ + label: "URL", + fieldId: "url", + type: "text", + settings: { + fields, + layout: fields.map(field => [field.storageId]) + } + }); +}; + +const metaField = (fields: CmsModelField[]) => { + return createModelField({ + label: "Meta", + fieldId: "meta", + type: "object", + settings: { + fields, + layout: fields.map(field => [field.storageId]) + } + }); +}; + +const formIdField = () => { + return createModelField({ + label: "ID", + fieldId: "id", + type: "text", + validation: [required()] + }); +}; + +const formParentField = () => { + return createModelField({ + label: "Parent", + fieldId: "parent", + type: "text", + validation: [required()] + }); +}; + +const versionField = () => { + return createModelField({ + label: "Version", + fieldId: "version", + type: "text", + validation: [required()] + }); +}; + +const formField = (fields: CmsModelField[]) => { + return createModelField({ + label: "Form", + fieldId: "form", + type: "object", + validation: [required()], + settings: { + fields, + layout: fields.map(field => [field.storageId]) + } + }); +}; + +const logsField = () => { + return createModelField({ + label: "Logs", + fieldId: "logs", + type: "json", + multipleValues: true + }); +}; + +const DEFAULT_FIELDS = ["data", "meta", "form"]; + +export const createSubmissionDataModelDefinition = (group: CmsModelGroup): CmsPrivateModelFull => { + return { + name: "FbSubmission", + modelId: "fbSubmission", + titleFieldId: "", + group, + layout: DEFAULT_FIELDS.map(field => [field]), + fields: [ + dataField(), + metaField([ + metaIpField(), + metaSubmittedOnField(), + metaUrlField([metaUrlLocationField(), metaUrlQueryField()]) + ]), + formField([ + formIdField(), + formParentField(), + versionField(), + fieldsField(FIELD_FIELDS), + stepsField(STEP_FIELDS) + ]), + logsField() + ], + description: "Form Builder - Submission content model", + isPrivate: true, + noValidate: true + }; +}; diff --git a/packages/api-form-builder/src/plugins/crud/forms.crud.ts b/packages/api-form-builder/src/plugins/crud/forms.crud.ts index cca92372c52..5d1abe6b21e 100644 --- a/packages/api-form-builder/src/plugins/crud/forms.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/forms.crud.ts @@ -147,9 +147,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { * as a resolver to an `FbForm` GraphQL type, and we already check permissions * and ownership when resolving the form in `getForm`. */ - const revisions = await this.getFormRevisions(id, { - auth: false - }); + const revisions = await this.getFormRevisions(id); /** * Then calculate the stats @@ -206,15 +204,11 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { ); } }, - async getFormRevisions(this: FormBuilder, id, options) { - // Just get the form first, to check if it exists and if user has access to it. - const [pid, revisionNumber = "0001"] = id.split("#"); - await this.getForm(`${pid}#${revisionNumber}`, options); - + async getFormRevisions(this: FormBuilder, id) { try { const result = (await this.storageOperations.listFormRevisions({ where: { - id: `${pid}#${revisionNumber}`, + formId: id, tenant: getTenant().id, locale: getLocale().code }, @@ -308,15 +302,11 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { */ const formId = mdbid(); const version = 1; - const id = createIdentifier({ - id: formId, - version - }); const slug = `${slugify(data.name)}-${formId}`.toLowerCase(); const form: FbForm = { - id, + id: formId, formId, locale: getLocale().code, tenant: getTenant().id, @@ -492,7 +482,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { const revisions = await this.storageOperations.listFormRevisions({ where: { - formId: formFormId, + formId: formFormId || "", tenant: form.tenant, locale: form.locale }, @@ -513,11 +503,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { previous, revisions }); - await this.storageOperations.deleteFormRevision({ - form, - previous, - revisions - }); + await this.storageOperations.deleteFormRevision({ form }); await onFormRevisionAfterDelete.publish({ form, previous, @@ -537,11 +523,11 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { }, async publishForm(this: FormBuilder, id) { await formsPermissions.ensure({ rwd: "r", pw: "p" }); - + const [pid, revisionNumber = "0001"] = id.split("#"); /** * getForm checks for existence of the form. */ - const original = await this.getForm(id, { + const original = await this.getForm(`${pid}#${revisionNumber}`, { auth: false }); @@ -630,12 +616,10 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { const original = await this.getForm(id, { auth: false }); - const originalFormFormId = original.formId || (original.id.split("#").pop() as string); - const latest = await this.storageOperations.getForm({ where: { - formId: originalFormFormId, + id, latest: true, tenant: original.tenant, locale: original.locale @@ -689,9 +673,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { form }); const result = await this.storageOperations.createFormFrom({ - original, - latest, - form + form: latest }); await onFormRevisionAfterCreate.publish({ original, diff --git a/packages/api-form-builder/src/plugins/crud/submissions.crud.ts b/packages/api-form-builder/src/plugins/crud/submissions.crud.ts index 5e858ed1490..c2f9e0bb8b4 100644 --- a/packages/api-form-builder/src/plugins/crud/submissions.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/submissions.crud.ts @@ -382,7 +382,7 @@ export const createSubmissionsCrud = (params: CreateSubmissionsCrudParams): Subm const data = await new models.FormSubmissionUpdateDataModel().populate(input); data.validate(); - const updatedData = data.toJSON(); + const updatedData = await data.toJSON(); const submissionId = input.id; diff --git a/packages/api-form-builder/src/types.ts b/packages/api-form-builder/src/types.ts index fc87d16bb65..14e6c6a14ae 100644 --- a/packages/api-form-builder/src/types.ts +++ b/packages/api-form-builder/src/types.ts @@ -232,7 +232,7 @@ export interface FormsCRUD { createFormRevision(fromRevisionId: string): Promise; incrementFormViews(id: string): Promise; incrementFormSubmissions(id: string): Promise; - getFormRevisions(id: string, options?: FormBuilderGetFormRevisionsOptions): Promise; + getFormRevisions(id: string): Promise; getPublishedFormRevisionById(revisionId: string): Promise; getLatestPublishedFormRevision(formId: string): Promise; deleteFormRevision(id: string): Promise; @@ -567,7 +567,7 @@ export interface FormBuilderStorageOperationsListFormsParams { */ export interface FormBuilderStorageOperationsListFormRevisionsParamsWhere { id?: string; - formId?: string; + formId: string; version_not?: number; publishedOn_not?: string | null; tenant: string; @@ -610,8 +610,6 @@ export interface FormBuilderStorageOperationsCreateFormParams { * @category StorageOperationsParams */ export interface FormBuilderStorageOperationsCreateFormFromParams { - original: FbForm; - latest: FbForm; form: FbForm; } @@ -765,9 +763,7 @@ export interface FormBuilderFormStorageOperations { /** * Delete the single form revision. */ - deleteFormRevision( - params: FormBuilderStorageOperationsDeleteFormRevisionParams - ): Promise; + deleteFormRevision({ form }: { form: FbForm }): Promise; publishForm(params: FormBuilderStorageOperationsPublishFormParams): Promise; unpublishForm(params: FormBuilderStorageOperationsUnpublishFormParams): Promise; } diff --git a/packages/api-headless-cms/src/crud/contentEntry.crud.ts b/packages/api-headless-cms/src/crud/contentEntry.crud.ts index 8f010d16094..2da60da3429 100644 --- a/packages/api-headless-cms/src/crud/contentEntry.crud.ts +++ b/packages/api-headless-cms/src/crud/contentEntry.crud.ts @@ -76,7 +76,7 @@ import { I18NLocale } from "@webiny/api-i18n/types"; import { filterAsync } from "~/utils/filterAsync"; import { EntriesPermissions } from "~/utils/permissions/EntriesPermissions"; import { ModelsPermissions } from "~/utils/permissions/ModelsPermissions"; -import { NotAuthorizedError } from "@webiny/api-security/"; +import { NotAuthorizedError } from "@webiny/api-security"; import { ROOT_FOLDER } from "~/constants"; export const STATUS_DRAFT = CONTENT_ENTRY_STATUS.DRAFT; diff --git a/packages/api-headless-cms/src/graphqlFields/index.ts b/packages/api-headless-cms/src/graphqlFields/index.ts index 4de7e77f8d4..57e56c917ef 100644 --- a/packages/api-headless-cms/src/graphqlFields/index.ts +++ b/packages/api-headless-cms/src/graphqlFields/index.ts @@ -8,6 +8,7 @@ import { createRichTextField } from "./richText"; import { createFileField } from "./file"; import { createObjectField } from "./object"; import { createDynamicZoneField } from "~/graphqlFields/dynamicZone"; +import { createJSONField } from "./json"; import { CmsModelFieldToGraphQLPlugin } from "~/types"; export const createGraphQLFields = (): CmsModelFieldToGraphQLPlugin[] => [ @@ -20,5 +21,6 @@ export const createGraphQLFields = (): CmsModelFieldToGraphQLPlugin[] => [ createRichTextField(), createFileField(), createObjectField(), - createDynamicZoneField() + createDynamicZoneField(), + createJSONField() ]; diff --git a/packages/api-headless-cms/src/graphqlFields/json.ts b/packages/api-headless-cms/src/graphqlFields/json.ts new file mode 100644 index 00000000000..e68c006d80d --- /dev/null +++ b/packages/api-headless-cms/src/graphqlFields/json.ts @@ -0,0 +1,35 @@ +import { CmsModelFieldToGraphQLPlugin } from "~/types"; +import { createGraphQLInputField } from "./helpers"; + +export const createJSONField = (): CmsModelFieldToGraphQLPlugin => { + return { + name: "cms-model-field-to-graphql-json", + type: "cms-model-field-to-graphql", + fieldType: "json", + isSortable: false, + isSearchable: false, + read: { + createTypeField({ field }) { + if (field.multipleValues) { + return `${field.fieldId}: [JSON]`; + } + + return `${field.fieldId}: JSON`; + }, + createGetFilters({ field }) { + return `${field.fieldId}: JSON`; + } + }, + manage: { + createTypeField({ field }) { + if (field.multipleValues) { + return `${field.fieldId}: [JSON]`; + } + return `${field.fieldId}: JSON`; + }, + createInputField({ field }) { + return createGraphQLInputField(field, "JSON"); + } + } + }; +}; diff --git a/packages/api-headless-cms/src/types.ts b/packages/api-headless-cms/src/types.ts index b166675c6fb..61f8d3cebb7 100644 --- a/packages/api-headless-cms/src/types.ts +++ b/packages/api-headless-cms/src/types.ts @@ -169,6 +169,7 @@ export type CmsModelFieldType = | "rich-text" | "text" | "dynamicZone" + | "json" | string; /** * A definition for content model field. This type exists on the app side as well. diff --git a/packages/app-form-builder/src/components/Form/FormRender.tsx b/packages/app-form-builder/src/components/Form/FormRender.tsx index e5e8aa13766..19320c7a795 100644 --- a/packages/app-form-builder/src/components/Form/FormRender.tsx +++ b/packages/app-form-builder/src/components/Form/FormRender.tsx @@ -161,6 +161,11 @@ const FormRender: React.FC = props => { fields.forEach(field => { const fieldId = field.fieldId; + // Remove after form model fields id fix + if (!field.settings) { + return; + } + if ( fieldId && "defaultValue" in field.settings && From be282459885d95743e5690f5d50dff2c36eae9cf Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Wed, 1 Nov 2023 14:29:33 +0000 Subject: [PATCH 03/37] fix: fixed build errors --- .../src/operations/form/index.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts b/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts index cbe09bdb08b..eca42038f45 100644 --- a/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts +++ b/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts @@ -179,7 +179,7 @@ export const createFormStorageOperations = ( const createFormFrom = async ( params: FormBuilderStorageOperationsCreateFormFromParams ): Promise => { - const { form, original, latest } = params; + const { form } = params; const revisionKeys = { PK: createFormPartitionKey(form), @@ -217,9 +217,7 @@ export const createFormStorageOperations = ( { revisionKeys, latestKeys, - original, - form, - latest + form } ); } @@ -242,9 +240,7 @@ export const createFormStorageOperations = ( ex.code || "CREATE_FORM_FROM_ERROR", { latestKeys, - form, - latest, - original + form } ); } From 8144935e99daad590539e00ef168e7730f5dff5f Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Wed, 1 Nov 2023 15:01:58 +0000 Subject: [PATCH 04/37] feat: added locked form model field --- .../CmsFormBuilderStorage.ts | 3 +-- .../models/form.model.ts | 21 ++++++++++++++++++- .../src/graphql/crud/forms.crud.ts | 2 +- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts index c0a58e1ce09..80da42621e8 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts @@ -53,7 +53,7 @@ export class CmsFormBuilderStorage { } = params; const model = this.modelWithContext({ tenant, locale }); const entries = await this.security.withoutAuthorization(async () => { - return await this.cms.getEntryRevisions(model, `${formId}#0001`); + return await this.cms.getEntryRevisions(model, formId); }); return [entries.map(entry => this.getFormFieldValues(entry))]; }; @@ -76,7 +76,6 @@ export class CmsFormBuilderStorage { return [entries.map(entry => this.getFormFieldValues(entry)), meta]; }; - // [WIP] createFormFrom = async (params: any) => { const { form } = params; const model = this.modelWithContext(form); diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts index d3b4af9ceda..d1062abf418 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts @@ -41,6 +41,15 @@ const statusField = () => { }); }; +const lockedField = () => { + return createModelField({ + label: "Locked", + fieldId: "locked", + type: "text", + validation: [required()] + }); +}; + const field_IdField = () => { return createModelField({ label: "ID", @@ -309,7 +318,16 @@ const settingsField = (fields: CmsModelField[]) => { }); }; -const DEFAULT_FIELDS = ["formId", "name", "published", "status", "fields", "steps", "settings"]; +const DEFAULT_FIELDS = [ + "formId", + "name", + "published", + "status", + "locked", + "fields", + "steps", + "settings" +]; const SETTINGS_FIELDS: CmsModelField[] = [ settingsLayoutField([settingsLayoutRendererField()]), @@ -354,6 +372,7 @@ export const createFormDataModelDefinition = (group: CmsModelGroup): any => { nameField(), publishedField(), statusField(), + lockedField(), fieldsField(FIELD_FIELDS), stepsField(STEP_FIELDS), settingsField(SETTINGS_FIELDS) diff --git a/packages/api-page-builder-import-export/src/graphql/crud/forms.crud.ts b/packages/api-page-builder-import-export/src/graphql/crud/forms.crud.ts index 76bded0d5ce..fa7548537bc 100644 --- a/packages/api-page-builder-import-export/src/graphql/crud/forms.crud.ts +++ b/packages/api-page-builder-import-export/src/graphql/crud/forms.crud.ts @@ -96,7 +96,7 @@ export default new ContextPlugin(context => { ) { formIds = []; - const forms = await context.formBuilder.listForms(); + const [forms] = await context.formBuilder.listForms(); // Save form ids forms.forEach(form => formIds.push(form.id)); From 44f7f109f8333dbb69cc8f0b58a9cd97345febce Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Wed, 1 Nov 2023 16:01:45 +0000 Subject: [PATCH 05/37] feat: moved deleteFormRevision into HCMS --- apps/api/graphql/src/index.ts | 5 ++++- .../src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts | 3 +-- .../src/cmsFormBuilderStorage/modelFactory.ts | 2 +- packages/api-form-builder/src/plugins/crud/forms.crud.ts | 8 ++++---- .../api-form-builder/src/plugins/graphql/formSettings.ts | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/api/graphql/src/index.ts b/apps/api/graphql/src/index.ts index 3cc55cb71bc..e04d3a30b95 100644 --- a/apps/api/graphql/src/index.ts +++ b/apps/api/graphql/src/index.ts @@ -22,7 +22,10 @@ import { createFileModelModifier } from "@webiny/api-file-manager"; import { createFileManagerStorageOperations } from "@webiny/api-file-manager-ddb"; -import { createFormBuilderContext, createFormBuilderGraphQL } from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; +import { + createFormBuilderContext, + createFormBuilderGraphQL +} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import logsPlugins from "@webiny/handler-logs"; import fileManagerS3 from "@webiny/api-file-manager-s3"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts index 80da42621e8..63444245cce 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts @@ -55,7 +55,7 @@ export class CmsFormBuilderStorage { const entries = await this.security.withoutAuthorization(async () => { return await this.cms.getEntryRevisions(model, formId); }); - return [entries.map(entry => this.getFormFieldValues(entry))]; + return entries.map(entry => this.getFormFieldValues(entry)); }; listForms = async (params: FormBuilderStorageOperationsListFormsParams) => { @@ -95,7 +95,6 @@ export class CmsFormBuilderStorage { }); }; - // [WIP] deleteFormRevision = async ({ form }: { form: FbForm }) => { const model = this.modelWithContext(form); diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/modelFactory.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/modelFactory.ts index 2937689e08c..731dfb919f3 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/modelFactory.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/modelFactory.ts @@ -23,4 +23,4 @@ export const modelFactory = (params: Params): CmsModelPlugin => { ...modelDefinition, noValidate: true }); -}; \ No newline at end of file +}; diff --git a/packages/api-form-builder/src/plugins/crud/forms.crud.ts b/packages/api-form-builder/src/plugins/crud/forms.crud.ts index 5d1abe6b21e..55ef9ed61a1 100644 --- a/packages/api-form-builder/src/plugins/crud/forms.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/forms.crud.ts @@ -206,16 +206,16 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { }, async getFormRevisions(this: FormBuilder, id) { try { - const result = (await this.storageOperations.listFormRevisions({ + const result = await this.storageOperations.listFormRevisions({ where: { formId: id, tenant: getTenant().id, locale: getLocale().code }, sort: ["version_ASC"] - })) as unknown as FbForm[][]; + }); - return result[0]; + return result; } catch (ex) { throw new WebinyError( ex.message || "Could not list form revisions.", @@ -490,7 +490,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { }); const previous = revisions.find(rev => rev.version < form.version) || null; - if (!previous && revisions.length === 1) { + if (revisions.length === 1) { /** * Means we're deleting the last revision, so we need to delete the whole form. */ diff --git a/packages/api-form-builder/src/plugins/graphql/formSettings.ts b/packages/api-form-builder/src/plugins/graphql/formSettings.ts index a98d52beb22..814b6b7b4f0 100644 --- a/packages/api-form-builder/src/plugins/graphql/formSettings.ts +++ b/packages/api-form-builder/src/plugins/graphql/formSettings.ts @@ -67,4 +67,4 @@ export const createFormBuilderSettingsSchema = () => { }); return formBuilderSettingGraphQL; -} +}; From 9eba393356f8d5846fdaf798b2434d698117eed3 Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Wed, 1 Nov 2023 16:34:41 +0000 Subject: [PATCH 06/37] fix: removed unused form builder context methods --- .../FormBuilderContextSetup.ts | 25 +++---------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts index 0f581fc9d70..5e98d21d351 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts @@ -1,7 +1,6 @@ import { createFormBuilder } from "~/index"; import { FormBuilderContext, FbFormPermission } from "~/types"; import WebinyError from "@webiny/error"; -import { SecurityPermission } from "@webiny/api-security/types"; import { createFormBuilderPlugins } from "./createFormBuilderPlugins"; import { CmsModelPlugin } from "@webiny/api-headless-cms"; import { CmsFormBuilderStorage } from "./CmsFormBuilderStorage"; @@ -29,16 +28,16 @@ export class FormBuilderContextSetup { }; } - const formPermissions = new FormsPermissions({ - getIdentity: this.context.security.getIdentity, + const formsPermissions = new FormsPermissions({ + getIdentity: this.getIdentity.bind(this), getPermissions: () => this.context.security.getPermissions("fb.form"), fullAccessPermissionName: "fb.*" }); return createFormBuilder({ storageOperations, - formsPermissions: this.formsPermissions, - getTenant: () => this.context.tenancy.getCurrentTenant(), + formsPermissions, + getTenant: this.getTenantId.bind(this), getLocale: this.getLocale.bind(this), context: this.context }); @@ -63,22 +62,6 @@ export class FormBuilderContextSetup { return this.context.tenancy.getCurrentTenant().id; } - private async getPermissions( - name: string - ): Promise { - return this.context.security.getPermissions(name); - } - - private basePermissionsArgs = { - getIdentity: this.getIdentity.bind(this), - fullAccessPermissionName: "fb.*" - }; - - private formsPermissions = new FormsPermissions({ - ...this.basePermissionsArgs, - getPermissions: () => this.context.security.getPermissions("fb.form") - }); - private async setupCmsStorageOperations() { // This registers code plugins (model group, models) const { groupPlugin, formModelDefinition, submissionModelDefinition } = From 63714c9623182c51b09fed695f6677321df270d5 Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Fri, 3 Nov 2023 09:15:49 +0000 Subject: [PATCH 07/37] feat: add updateForm, dynamic schema for submissions --- apps/api/graphql/src/index.ts | 2 +- .../api-form-builder-so-ddb-es/src/index.ts | 8 +- .../src/operations/form/index.ts | 956 +----------------- packages/api-form-builder-so-ddb/src/index.ts | 6 +- .../src/operations/form/index.ts | 417 +------- .../CmsFormBuilderStorage.ts | 39 +- .../CmsSubmissionsStorage.ts | 14 +- .../ListSubmissionsWhereProcessor.ts | 31 - .../createFormBuilderContext.ts | 2 +- .../createGraphQLSchemaPlugin.ts | 49 +- .../isInstallationPending.ts | 15 + .../models/submission.model.ts | 12 +- .../src/plugins/crud/forms.crud.ts | 13 +- .../graphql/createSubmissionsTypeDefs.ts | 95 ++ .../src/plugins/graphql/form.ts | 245 +---- .../src/plugins/graphql/submissionsSchema.ts | 192 ++++ packages/api-form-builder/src/types.ts | 17 +- .../ddb-es/apps/api/graphql/src/index.ts | 13 +- 18 files changed, 483 insertions(+), 1643 deletions(-) delete mode 100644 packages/api-form-builder/src/cmsFormBuilderStorage/ListSubmissionsWhereProcessor.ts create mode 100644 packages/api-form-builder/src/cmsFormBuilderStorage/isInstallationPending.ts create mode 100644 packages/api-form-builder/src/plugins/graphql/createSubmissionsTypeDefs.ts create mode 100644 packages/api-form-builder/src/plugins/graphql/submissionsSchema.ts diff --git a/apps/api/graphql/src/index.ts b/apps/api/graphql/src/index.ts index e04d3a30b95..7e16044eb75 100644 --- a/apps/api/graphql/src/index.ts +++ b/apps/api/graphql/src/index.ts @@ -91,12 +91,12 @@ export const handler = createHandler({ pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), - createFormBuilderGraphQL(), createFormBuilderContext({ storageOperations: createFormBuilderStorageOperations({ documentClient }) }), + createFormBuilderGraphQL(), createApwGraphQL(), createApwPageBuilderContext({ storageOperations: createApwSaStorageOperations({ documentClient }) diff --git a/packages/api-form-builder-so-ddb-es/src/index.ts b/packages/api-form-builder-so-ddb-es/src/index.ts index cbfcb3fd23c..e5c138c5603 100644 --- a/packages/api-form-builder-so-ddb-es/src/index.ts +++ b/packages/api-form-builder-so-ddb-es/src/index.ts @@ -179,13 +179,7 @@ export const createFormBuilderStorageOperations: FormBuilderStorageOperationsFac table, entity: entities.settings }), - ...createFormStorageOperations({ - elasticsearch, - table, - entity: entities.form, - esEntity: entities.esForm, - plugins - }), + ...createFormStorageOperations(), ...createSubmissionStorageOperations({ elasticsearch, table, diff --git a/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts b/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts index eca42038f45..8095fffd664 100644 --- a/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts +++ b/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts @@ -1,34 +1,8 @@ -import { - FbForm, - FormBuilderStorageOperationsCreateFormFromParams, - FormBuilderStorageOperationsCreateFormParams, - FormBuilderStorageOperationsDeleteFormParams, - FormBuilderStorageOperationsDeleteFormRevisionParams, - FormBuilderStorageOperationsGetFormParams, - FormBuilderStorageOperationsListFormRevisionsParams, - FormBuilderStorageOperationsListFormRevisionsParamsWhere, - FormBuilderStorageOperationsListFormsParams, - FormBuilderStorageOperationsListFormsResponse, - FormBuilderStorageOperationsPublishFormParams, - FormBuilderStorageOperationsUnpublishFormParams, - FormBuilderStorageOperationsUpdateFormParams -} from "@webiny/api-form-builder/types"; import { Entity, Table } from "dynamodb-toolbox"; import { Client } from "@elastic/elasticsearch"; -import { queryAll, QueryAllParams } from "@webiny/db-dynamodb/utils/query"; -import WebinyError from "@webiny/error"; -import { cleanupItem } from "@webiny/db-dynamodb/utils/cleanup"; -import { batchWriteAll } from "@webiny/db-dynamodb/utils/batchWrite"; -import { configurations } from "~/configurations"; -import { filterItems } from "@webiny/db-dynamodb/utils/filter"; -import fields from "./fields"; -import { sortItems } from "@webiny/db-dynamodb/utils/sort"; -import { parseIdentifier, zeroPad } from "@webiny/utils"; -import { createElasticsearchBody, createFormElasticType } from "./elasticsearchBody"; -import { decodeCursor, encodeCursor } from "@webiny/api-elasticsearch"; +import { parseIdentifier } from "@webiny/utils"; import { PluginsContainer } from "@webiny/plugins"; import { FormBuilderFormCreateKeyParams, FormBuilderFormStorageOperations } from "~/types"; -import { ElasticsearchSearchResponse } from "@webiny/api-elasticsearch/types"; export type DbRecord = T & { PK: string; @@ -44,37 +18,7 @@ export interface CreateFormStorageOperationsParams { plugins: PluginsContainer; } -type FbFormElastic = Omit & { - __type: string; -}; - -const getESDataForLatestRevision = (form: FbForm): FbFormElastic => ({ - __type: createFormElasticType(), - id: form.id, - createdOn: form.createdOn, - savedOn: form.savedOn, - name: form.name, - slug: form.slug, - published: form.published, - publishedOn: form.publishedOn, - version: form.version, - locked: form.locked, - status: form.status, - createdBy: form.createdBy, - ownedBy: form.ownedBy, - tenant: form.tenant, - locale: form.locale, - webinyVersion: form.webinyVersion, - formId: form.formId -}); - -export const createFormStorageOperations = ( - params: CreateFormStorageOperationsParams -): FormBuilderFormStorageOperations => { - const { entity, esEntity, table, plugins, elasticsearch } = params; - - const formDynamoDbFields = fields(); - +export const createFormStorageOperations = (): FormBuilderFormStorageOperations => { const createFormPartitionKey = (params: FormBuilderFormCreateKeyParams): string => { const { tenant, locale, id: targetId } = params; @@ -83,880 +27,64 @@ export const createFormStorageOperations = ( return `T#${tenant}#L#${locale}#FB#F#${id}`; }; - const createRevisionSortKey = (value: string | number | undefined): string => { - const version = - typeof value === "number" ? Number(value) : (parseIdentifier(value).version as number); - return `REV#${zeroPad(version)}`; + const createForm = () => { + throw new Error( + "api-form-builder-ddb-es does not implement the Form Builder storage operations." + ); }; - const createLatestSortKey = (): string => { - return "L"; + const createFormFrom = async () => { + throw new Error( + "api-form-builder-ddb-es does not implement the Form Builder storage operations." + ); }; - const createLatestPublishedSortKey = (): string => { - return "LP"; + const updateForm = async () => { + throw new Error( + "api-form-builder-ddb-es does not implement the Form Builder storage operations." + ); }; - const createFormType = (): string => { - return "fb.form"; + const getForm = async () => { + throw new Error( + "api-form-builder-ddb-es does not implement the Form Builder storage operations." + ); }; - const createFormLatestType = (): string => { - return "fb.form.latest"; + const listForms = () => { + throw new Error( + "api-form-builder-ddb-es does not implement the Form Builder storage operations." + ); }; - const createFormLatestPublishedType = (): string => { - return "fb.form.latestPublished"; - }; - - const createForm = async ( - params: FormBuilderStorageOperationsCreateFormParams - ): Promise => { - const { form } = params; - - const revisionKeys = { - PK: createFormPartitionKey(form), - SK: createRevisionSortKey(form.id) - }; - const latestKeys = { - PK: createFormPartitionKey(form), - SK: createLatestSortKey() - }; - - const items = [ - entity.putBatch({ - ...form, - TYPE: createFormType(), - ...revisionKeys - }), - entity.putBatch({ - ...form, - TYPE: createFormLatestType(), - ...latestKeys - }) - ]; - - try { - await batchWriteAll({ - table, - items - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not insert form data into regular table.", - ex.code || "CREATE_FORM_ERROR", - { - revisionKeys, - latestKeys, - form - } - ); - } - try { - const { index } = configurations.es({ - tenant: form.tenant, - locale: form.locale - }); - await esEntity.put({ - index, - data: getESDataForLatestRevision(form), - TYPE: createFormType(), - ...latestKeys - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not insert form data into Elasticsearch table.", - ex.code || "CREATE_FORM_ERROR", - { - latestKeys, - form - } - ); - } - return form; - }; - - const createFormFrom = async ( - params: FormBuilderStorageOperationsCreateFormFromParams - ): Promise => { - const { form } = params; - - const revisionKeys = { - PK: createFormPartitionKey(form), - SK: createRevisionSortKey(form.version) - }; - - const latestKeys = { - PK: createFormPartitionKey(form), - SK: createLatestSortKey() - }; - - const items = [ - entity.putBatch({ - ...form, - ...revisionKeys, - TYPE: createFormType() - }), - entity.putBatch({ - ...form, - ...latestKeys, - TYPE: createFormLatestType() - }) - ]; - - try { - await batchWriteAll({ - table, - items - }); - } catch (ex) { - throw new WebinyError( - ex.message || - "Could not create form data in the regular table, from existing form.", - ex.code || "CREATE_FORM_FROM_ERROR", - { - revisionKeys, - latestKeys, - form - } - ); - } - - try { - const { index } = configurations.es({ - tenant: form.tenant, - locale: form.locale - }); - await esEntity.put({ - index, - data: getESDataForLatestRevision(form), - TYPE: createFormLatestType(), - ...latestKeys - }); - } catch (ex) { - throw new WebinyError( - ex.message || - "Could not create form in the Elasticsearch table, from existing form.", - ex.code || "CREATE_FORM_FROM_ERROR", - { - latestKeys, - form - } - ); - } - return form; + const listFormRevisions = async () => { + throw new Error( + "api-form-builder-ddb-es does not implement the Form Builder storage operations." + ); }; - const updateForm = async ( - params: FormBuilderStorageOperationsUpdateFormParams - ): Promise => { - const { form, original } = params; - - const revisionKeys = { - PK: createFormPartitionKey(form), - SK: createRevisionSortKey(form.id) - }; - const latestKeys = { - PK: createFormPartitionKey(form), - SK: createLatestSortKey() - }; - - const { formId, tenant, locale } = form; - - const latestForm = await getForm({ - where: { - formId, - tenant, - locale, - latest: true - } - }); - const isLatestForm = latestForm ? latestForm.id === form.id : false; - - const items = [ - entity.putBatch({ - ...form, - TYPE: createFormType(), - ...revisionKeys - }) - ]; - if (isLatestForm) { - items.push( - entity.putBatch({ - ...form, - TYPE: createFormLatestType(), - ...latestKeys - }) - ); - } - try { - await batchWriteAll({ - table, - items - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not update form data in the regular table.", - ex.code || "UPDATE_FORM_ERROR", - { - revisionKeys, - latestKeys, - original, - form, - latestForm - } - ); - } - /** - * No need to go further if its not latest form. - */ - if (!isLatestForm) { - return form; - } - - try { - const { index } = configurations.es({ - tenant: form.tenant, - locale: form.locale - }); - await esEntity.put({ - index, - data: getESDataForLatestRevision(form), - TYPE: createFormLatestType(), - ...latestKeys - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not update form data in the Elasticsearch table.", - ex.code || "UPDATE_FORM_ERROR", - { - latestKeys, - form, - latestForm, - original - } - ); - } - return form; + const deleteForm = async () => { + throw new Error( + "api-form-builder-ddb-es does not implement the Form Builder storage operations." + ); }; - const getForm = async ( - params: FormBuilderStorageOperationsGetFormParams - ): Promise => { - const { where } = params; - const { id, formId, latest, published, version, tenant, locale } = where; - if (latest && published) { - throw new WebinyError("Cannot have both latest and published params."); - } - let sortKey: string; - if (latest) { - sortKey = createLatestSortKey(); - } else if (published && !version) { - /** - * Because of the specifics how DynamoDB works, we must not load the published record if version is sent. - */ - sortKey = createLatestPublishedSortKey(); - } else if (id || version) { - sortKey = createRevisionSortKey(version || id); - } else { - throw new WebinyError( - "Missing parameter to create a sort key.", - "MISSING_WHERE_PARAMETER", - { - where - } - ); - } - - const keys = { - PK: createFormPartitionKey({ - tenant, - locale, - id: (formId || id) as string - }), - SK: sortKey - }; - - try { - const result = await entity.get(keys); - if (!result || !result.Item) { - return null; - } - return cleanupItem(entity, result.Item); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not get form by keys.", - ex.code || "GET_FORM_ERROR", - { - keys - } - ); - } + const deleteFormRevision = async () => { + throw new Error( + "api-form-builder-ddb-esdoes not implement the Form Builder storage operations." + ); }; - const listForms = async ( - params: FormBuilderStorageOperationsListFormsParams - ): Promise => { - const { sort, limit, where, after } = params; - - const body = createElasticsearchBody({ - plugins, - sort, - limit: limit + 1, - where, - after: decodeCursor(after) as any - }); - - const esConfig = configurations.es({ - tenant: where.tenant, - locale: where.locale - }); - - const query = { - ...esConfig, - body - }; - - let response: ElasticsearchSearchResponse; - try { - response = await elasticsearch.search(query); - } catch (ex) { - throw new WebinyError( - ex.message || "Could list forms.", - ex.code || "LIST_FORMS_ERROR", - { - where, - query - } - ); - } - - const { hits, total } = response.body.hits; - const items = hits.map(item => item._source); - - const hasMoreItems = items.length > limit; - if (hasMoreItems) { - /** - * Remove the last item from results, we don't want to include it. - */ - items.pop(); - } - /** - * Cursor is the `sort` value of the last item in the array. - * https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#search-after - */ - - const meta = { - hasMoreItems, - totalCount: total.value, - cursor: items.length > 0 ? encodeCursor(hits[items.length - 1].sort) || null : null - }; - - return [items, meta]; + const publishForm = async () => { + throw new Error( + "api-form-builder-ddb-es does not implement the Form Builder storage operations." + ); }; - const listFormRevisions = async ( - params: FormBuilderStorageOperationsListFormRevisionsParams - ): Promise => { - const { where: initialWhere, sort } = params; - const { id, formId, tenant, locale } = initialWhere; - const queryAllParams: QueryAllParams = { - entity, - partitionKey: createFormPartitionKey({ - tenant, - locale, - id: (id || formId) as string - }), - options: { - beginsWith: "REV#" - } - }; - - let items: FbForm[] = []; - try { - items = await queryAll(queryAllParams); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not query forms by given params.", - ex.code || "QUERY_FORMS_ERROR", - { - partitionKey: queryAllParams.partitionKey, - options: queryAllParams.options - } - ); - } - const where: Partial = { - ...initialWhere, - id: undefined, - formId: undefined - }; - const filteredItems = filterItems({ - plugins, - items, - where, - fields: formDynamoDbFields - }); - if (!sort || sort.length === 0) { - return filteredItems; - } - return sortItems({ - items: filteredItems, - sort, - fields: formDynamoDbFields - }); - }; - - const deleteForm = async ( - params: FormBuilderStorageOperationsDeleteFormParams - ): Promise => { - const { form } = params; - let items: any[]; - /** - * This will find all form and submission records. - */ - const queryAllParams = { - entity, - partitionKey: createFormPartitionKey(form), - options: { - gte: " " - } - }; - try { - items = await queryAll(queryAllParams); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not query forms and submissions by given params.", - ex.code || "QUERY_FORM_AND_SUBMISSIONS_ERROR", - { - partitionKey: queryAllParams.partitionKey, - options: queryAllParams.options - } - ); - } - - const deleteItems = items.map(item => { - return entity.deleteBatch({ - PK: item.PK, - SK: item.SK - }); - }); - try { - await batchWriteAll({ - table, - items: deleteItems - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not delete form and it's submissions.", - ex.code || "DELETE_FORM_AND_SUBMISSIONS_ERROR" - ); - } - - const latestKeys = { - PK: createFormPartitionKey(form), - SK: createLatestSortKey() - }; - try { - await esEntity.delete(latestKeys); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not delete latest form record from Elasticsearch.", - ex.code || "DELETE_FORM_ERROR", - { - latestKeys - } - ); - } - return form; - }; - /** - * We need to: - * - delete current revision - * - get previously published revision and update the record if it exists or delete if it does not - * - update latest record if current one is the latest - */ - const deleteFormRevision = async ( - params: FormBuilderStorageOperationsDeleteFormRevisionParams - ): Promise => { - const { form, revisions, previous } = params; - - const revisionKeys = { - PK: createFormPartitionKey(form), - SK: createRevisionSortKey(form.id) - }; - - const latestKeys = { - PK: createFormPartitionKey(form), - SK: createLatestSortKey() - }; - - const latestForm = revisions[0]; - const latestPublishedForm = revisions.find(rev => rev.published === true); - - const isLatest = latestForm ? latestForm.id === form.id : false; - const isLatestPublished = latestPublishedForm ? latestPublishedForm.id === form.id : false; - - const items = [entity.deleteBatch(revisionKeys)]; - let esDataItem = undefined; - - if (isLatest || isLatestPublished) { - /** - * Sort out the latest published record. - */ - if (isLatestPublished) { - const previouslyPublishedForm = revisions - .filter(f => !!f.publishedOn && f.version !== form.version) - .sort((a, b) => { - return ( - new Date(b.publishedOn as string).getTime() - - new Date(a.publishedOn as string).getTime() - ); - }) - .shift(); - if (previouslyPublishedForm) { - items.push( - entity.putBatch({ - ...previouslyPublishedForm, - PK: createFormPartitionKey(previouslyPublishedForm), - SK: createLatestPublishedSortKey(), - TYPE: createFormLatestPublishedType() - }) - ); - } else { - items.push( - entity.deleteBatch({ - PK: createFormPartitionKey(form), - SK: createLatestPublishedSortKey() - }) - ); - } - } - /** - * Sort out the latest record. - */ - if (isLatest && previous) { - items.push( - entity.putBatch({ - ...previous, - ...latestKeys, - TYPE: createFormLatestType() - }) - ); - - const { index } = configurations.es({ - tenant: previous.tenant, - locale: previous.locale - }); - - esDataItem = { - index, - ...latestKeys, - data: getESDataForLatestRevision(previous) - }; - } - } - /** - * Now save the batch data. - */ - try { - await batchWriteAll({ - table, - items - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not delete form revision from regular table.", - ex.code || "DELETE_FORM_REVISION_ERROR", - { - form, - latestForm, - revisionKeys, - latestKeys - } - ); - } - /** - * And then the Elasticsearch data, if any. - */ - if (!esDataItem) { - return form; - } - try { - await esEntity.put(esDataItem); - return form; - } catch (ex) { - throw new WebinyError( - ex.message || "Could not delete form from to the Elasticsearch table.", - ex.code || "DELETE_FORM_REVISION_ERROR", - { - form, - latestForm, - revisionKeys, - latestKeys - } - ); - } - }; - - /** - * We need to save form in: - * - regular form record - * - latest published form record - * - latest form record - if form is latest one - * - elasticsearch latest form record - */ - const publishForm = async ( - params: FormBuilderStorageOperationsPublishFormParams - ): Promise => { - const { form, original } = params; - - const revisionKeys = { - PK: createFormPartitionKey(form), - SK: createRevisionSortKey(form.version) - }; - - const latestKeys = { - PK: createFormPartitionKey(form), - SK: createLatestSortKey() - }; - - const latestPublishedKeys = { - PK: createFormPartitionKey(form), - SK: createLatestPublishedSortKey() - }; - - const { locale, tenant, formId } = form; - - const latestForm = await getForm({ - where: { - formId, - tenant, - locale, - latest: true - } - }); - - const isLatestForm = latestForm ? latestForm.id === form.id : false; - /** - * Update revision and latest published records - */ - const items = [ - entity.putBatch({ - ...form, - ...revisionKeys, - TYPE: createFormType() - }), - entity.putBatch({ - ...form, - ...latestPublishedKeys, - TYPE: createFormLatestPublishedType() - }) - ]; - /** - * Update the latest form as well - */ - if (isLatestForm) { - items.push( - entity.putBatch({ - ...form, - ...latestKeys, - TYPE: createFormLatestType() - }) - ); - } - - try { - await batchWriteAll({ - table, - items - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not publish form.", - ex.code || "PUBLISH_FORM_ERROR", - { - form, - original, - latestForm, - revisionKeys, - latestKeys, - latestPublishedKeys - } - ); - } - if (!isLatestForm) { - return form; - } - const { index } = configurations.es({ - tenant: form.tenant, - locale: form.locale - }); - const esData = getESDataForLatestRevision(form); - try { - await esEntity.put({ - ...latestKeys, - index, - TYPE: createFormLatestType(), - data: esData - }); - return form; - } catch (ex) { - throw new WebinyError( - ex.message || "Could not publish form to the Elasticsearch.", - ex.code || "PUBLISH_FORM_ERROR", - { - form, - original, - latestForm, - revisionKeys, - latestKeys, - latestPublishedKeys - } - ); - } - }; - - /** - * We need to: - * - update form revision record - * - if latest published (LP) is current form, find the previously published record and update LP if there is some previously published, delete otherwise - * - if is latest update the Elasticsearch record - */ - const unpublishForm = async ( - params: FormBuilderStorageOperationsUnpublishFormParams - ): Promise => { - const { form, original } = params; - - const revisionKeys = { - PK: createFormPartitionKey(form), - SK: createRevisionSortKey(form.version) - }; - - const latestKeys = { - PK: createFormPartitionKey(form), - SK: createLatestSortKey() - }; - - const latestPublishedKeys = { - PK: createFormPartitionKey(form), - SK: createLatestPublishedSortKey() - }; - - const { formId, tenant, locale } = form; - - const latestForm = await getForm({ - where: { - formId, - tenant, - locale, - latest: true - } - }); - - const latestPublishedForm = await getForm({ - where: { - formId, - tenant, - locale, - published: true - } - }); - - const isLatest = latestForm ? latestForm.id === form.id : false; - const isLatestPublished = latestPublishedForm ? latestPublishedForm.id === form.id : false; - - const items = [ - entity.putBatch({ - ...form, - ...revisionKeys, - TYPE: createFormType() - }) - ]; - let esData: any = undefined; - if (isLatest) { - esData = getESDataForLatestRevision(form); - } - /** - * In case previously published revision exists, replace current one with that one. - * And if it does not, delete the record. - */ - if (isLatestPublished) { - const revisions = await listFormRevisions({ - where: { - formId, - tenant, - locale, - version_not: form.version, - publishedOn_not: null - }, - sort: ["savedOn_DESC"] - }); - - const previouslyPublishedRevision = revisions.shift(); - if (previouslyPublishedRevision) { - items.push( - entity.putBatch({ - ...previouslyPublishedRevision, - ...latestPublishedKeys, - TYPE: createFormLatestPublishedType() - }) - ); - } else { - items.push(entity.deleteBatch(latestPublishedKeys)); - } - } - - try { - await batchWriteAll({ - table, - items - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not unpublish form.", - ex.code || "UNPUBLISH_FORM_ERROR", - { - form, - original, - latestForm, - revisionKeys, - latestKeys, - latestPublishedKeys - } - ); - } - /** - * No need to go further in case of non-existing Elasticsearch data. - */ - if (!esData) { - return form; - } - const { index } = configurations.es({ - tenant: form.tenant, - locale: form.locale - }); - try { - await esEntity.put({ - ...latestKeys, - index, - TYPE: createFormLatestType(), - data: esData - }); - return form; - } catch (ex) { - throw new WebinyError( - ex.message || "Could not unpublish form from the Elasticsearch.", - ex.code || "UNPUBLISH_FORM_ERROR", - { - form, - original, - latestForm, - revisionKeys, - latestKeys, - latestPublishedKeys - } - ); - } + const unpublishForm = async () => { + throw new Error( + "api-form-builder-ddb-es does not implement the Form Builder storage operations." + ); }; return { diff --git a/packages/api-form-builder-so-ddb/src/index.ts b/packages/api-form-builder-so-ddb/src/index.ts index 1f530b61e49..944b6d31400 100644 --- a/packages/api-form-builder-so-ddb/src/index.ts +++ b/packages/api-form-builder-so-ddb/src/index.ts @@ -108,11 +108,7 @@ export const createFormBuilderStorageOperations: FormBuilderStorageOperationsFac table, entity: entities.settings }), - ...createFormStorageOperations({ - table, - entity: entities.form, - plugins - }), + ...createFormStorageOperations(), ...createSubmissionStorageOperations({ table, entity: entities.submission, diff --git a/packages/api-form-builder-so-ddb/src/operations/form/index.ts b/packages/api-form-builder-so-ddb/src/operations/form/index.ts index ff0659eabb1..44ee4843a2b 100644 --- a/packages/api-form-builder-so-ddb/src/operations/form/index.ts +++ b/packages/api-form-builder-so-ddb/src/operations/form/index.ts @@ -1,143 +1,19 @@ -import WebinyError from "@webiny/error"; -import { - FbForm, - FormBuilderStorageOperationsGetFormParams, - FormBuilderStorageOperationsListFormRevisionsParams, - FormBuilderStorageOperationsListFormRevisionsParamsWhere, - FormBuilderStorageOperationsUnpublishFormParams, - FormBuilderStorageOperationsUpdateFormParams -} from "@webiny/api-form-builder/types"; import { Entity, Table } from "dynamodb-toolbox"; -import { queryAll, QueryAllParams } from "@webiny/db-dynamodb/utils/query"; -import { cleanupItem } from "@webiny/db-dynamodb/utils/cleanup"; -import { batchWriteAll } from "@webiny/db-dynamodb/utils/batchWrite"; -import { filterItems } from "@webiny/db-dynamodb/utils/filter"; -import { sortItems } from "@webiny/db-dynamodb/utils/sort"; -import { createIdentifier, parseIdentifier } from "@webiny/utils"; import { PluginsContainer } from "@webiny/plugins"; -import { - FormBuilderFormCreateGSIPartitionKeyParams, - FormBuilderFormCreatePartitionKeyParams, - FormBuilderFormStorageOperations -} from "~/types"; -import { FormDynamoDbFieldPlugin } from "~/plugins/FormDynamoDbFieldPlugin"; -import { get } from "@webiny/db-dynamodb/utils/get"; - -interface Keys { - PK: string; - SK: string; -} - -interface FormLatestSortKeyParams { - id?: string; - formId?: string; -} - -interface GsiKeys { - GSI1_PK: string; - GSI1_SK: string; -} - +import { FormBuilderFormCreatePartitionKeyParams, FormBuilderFormStorageOperations } from "~/types"; export interface CreateFormStorageOperationsParams { entity: Entity; table: Table; plugins: PluginsContainer; } -export const createFormStorageOperations = ( - params: CreateFormStorageOperationsParams -): FormBuilderFormStorageOperations => { - const { entity, table, plugins } = params; - - const formDynamoDbFields = plugins.byType( - FormDynamoDbFieldPlugin.type - ); - +export const createFormStorageOperations = (): FormBuilderFormStorageOperations => { const createFormPartitionKey = (params: FormBuilderFormCreatePartitionKeyParams): string => { const { tenant, locale } = params; return `T#${tenant}#L#${locale}#FB#F`; }; - const createFormLatestPartitionKey = ( - params: FormBuilderFormCreatePartitionKeyParams - ): string => { - return `${createFormPartitionKey(params)}#L`; - }; - - const createFormLatestPublishedPartitionKey = ( - params: FormBuilderFormCreatePartitionKeyParams - ): string => { - return `${createFormPartitionKey(params)}#LP`; - }; - - const createFormGSIPartitionKey = ( - params: FormBuilderFormCreateGSIPartitionKeyParams - ): string => { - const { tenant, locale, id: targetId } = params; - const { id } = parseIdentifier(targetId); - - return `T#${tenant}#L#${locale}#FB#F#${id}`; - }; - - const createRevisionSortKey = ({ id }: { id: string }): string => { - return `${id}`; - }; - - const createFormLatestSortKey = ({ id, formId }: FormLatestSortKeyParams): string => { - const value = parseIdentifier(id || formId); - return value.id; - }; - - const createLatestPublishedSortKey = ({ id, formId }: FormLatestSortKeyParams): string => { - const value = parseIdentifier(id || formId); - return value.id; - }; - - const createGSISortKey = (version: number) => { - return `${version}`; - }; - - const createFormType = (): string => { - return "fb.form"; - }; - - const createFormLatestType = (): string => { - return "fb.form.latest"; - }; - - const createFormLatestPublishedType = (): string => { - return "fb.form.latestPublished"; - }; - - const createRevisionKeys = (form: FbForm): Keys => { - return { - PK: createFormPartitionKey(form), - SK: createRevisionSortKey(form) - }; - }; - - const createLatestKeys = (form: FbForm): Keys => { - return { - PK: createFormLatestPartitionKey(form), - SK: createFormLatestSortKey(form) - }; - }; - - const createLatestPublishedKeys = (form: FbForm): Keys => { - return { - PK: createFormLatestPublishedPartitionKey(form), - SK: createLatestPublishedSortKey(form) - }; - }; - - const createGSIKeys = (form: FbForm): GsiKeys => { - return { - GSI1_PK: createFormGSIPartitionKey(form), - GSI1_SK: createGSISortKey(form.version) - }; - }; - const createForm = () => { throw new Error( "api-form-builder-ddb does not implement the Form Builder storage operations." @@ -150,122 +26,16 @@ export const createFormStorageOperations = ( ); }; - const updateForm = async ( - params: FormBuilderStorageOperationsUpdateFormParams - ): Promise => { - const { form, original } = params; - - const revisionKeys = createRevisionKeys(form); - const latestKeys = createLatestKeys(form); - const gsiKeys = createGSIKeys(form); - - const { formId, tenant, locale } = form; - - const latestForm = await getForm({ - where: { - formId, - tenant, - locale, - latest: true - } - }); - const isLatestForm = latestForm ? latestForm.id === form.id : false; - - const items = [ - entity.putBatch({ - ...form, - ...revisionKeys, - ...gsiKeys, - TYPE: createFormType() - }) - ]; - if (isLatestForm) { - items.push( - entity.putBatch({ - ...form, - ...latestKeys, - TYPE: createFormLatestType() - }) - ); - } - try { - await batchWriteAll({ - table, - items - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not update form data in the table.", - ex.code || "UPDATE_FORM_ERROR", - { - revisionKeys, - latestKeys, - original, - form, - latestForm - } - ); - } - - return form; + const updateForm = async () => { + throw new Error( + "api-form-builder-ddb does not implement the Form Builder storage operations." + ); }; - const getForm = async ( - params: FormBuilderStorageOperationsGetFormParams - ): Promise => { - const { where } = params; - const { id, formId, latest, published, version } = where; - if (latest && published) { - throw new WebinyError("Cannot have both latest and published params."); - } - let partitionKey: string; - let sortKey: string; - if (latest) { - partitionKey = createFormLatestPartitionKey(where); - sortKey = createFormLatestSortKey(where); - } else if (published && !version) { - /** - * Because of the specifics how DynamoDB works, we must not load the published record if version is sent. - */ - partitionKey = createFormLatestPublishedPartitionKey(where); - sortKey = createLatestPublishedSortKey(where); - } else if (id || version) { - partitionKey = createFormPartitionKey(where); - sortKey = createRevisionSortKey({ - id: - id || - createIdentifier({ - id: formId as string, - version: version as number - }) - }); - } else { - throw new WebinyError( - "Missing parameter to create a sort key.", - "MISSING_WHERE_PARAMETER", - { - where - } - ); - } - - const keys = { - PK: partitionKey, - SK: sortKey - }; - - try { - const item = await get({ entity, keys }); - return cleanupItem(entity, item); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not get form by keys.", - ex.code || "GET_FORM_ERROR", - { - keys - } - ); - } + const getForm = async () => { + throw new Error( + "api-form-builder-ddb does not implement the Form Builder storage operations." + ); }; const listForms = () => { @@ -274,60 +44,10 @@ export const createFormStorageOperations = ( ); }; - const listFormRevisions = async ( - params: FormBuilderStorageOperationsListFormRevisionsParams - ): Promise => { - const { where: initialWhere, sort } = params; - const { id, formId, tenant, locale } = initialWhere; - - const queryAllParams: QueryAllParams = { - entity, - partitionKey: createFormGSIPartitionKey({ - tenant, - locale, - id: formId || id - }), - options: { - index: "GSI1", - gte: " " - } - }; - - let items: FbForm[] = []; - try { - items = await queryAll(queryAllParams); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not query forms by given params.", - ex.code || "QUERY_FORMS_ERROR", - { - partitionKey: queryAllParams.partitionKey, - options: queryAllParams.options - } - ); - } - const where: Partial = { - ...initialWhere - }; - /** - * We need to remove conditions so we do not filter by them again. - */ - delete where.id; - delete where.formId; - delete where.tenant; - delete where.locale; - - const filteredItems = filterItems({ - plugins, - items, - where, - fields: formDynamoDbFields - }); - return sortItems({ - items: filteredItems, - sort, - fields: formDynamoDbFields - }); + const listFormRevisions = async () => { + throw new Error( + "api-form-builder-ddb does not implement the Form Builder storage operations." + ); }; const deleteForm = async () => { @@ -348,111 +68,10 @@ export const createFormStorageOperations = ( ); }; - /** - * We need to: - * - update form revision record - * - if latest published (LP) is current form, find the previously published record and update LP if there is some previously published, delete otherwise - * - if is latest update the Elasticsearch record - */ - const unpublishForm = async ( - params: FormBuilderStorageOperationsUnpublishFormParams - ): Promise => { - const { form, original } = params; - - const revisionKeys = createRevisionKeys(form); - const latestKeys = createLatestKeys(form); - const latestPublishedKeys = createLatestPublishedKeys(form); - const gsiKeys = createGSIKeys(form); - - const { formId, tenant, locale } = form; - - const latestForm = await getForm({ - where: { - formId, - tenant, - locale, - latest: true - } - }); - - const latestPublishedForm = await getForm({ - where: { - formId, - tenant, - locale, - published: true - } - }); - - const isLatest = latestForm ? latestForm.id === form.id : false; - const isLatestPublished = latestPublishedForm ? latestPublishedForm.id === form.id : false; - - const items = [ - entity.putBatch({ - ...form, - ...revisionKeys, - ...gsiKeys, - TYPE: createFormType() - }) - ]; - - if (isLatest) { - entity.putBatch({ - ...form, - ...latestKeys, - TYPE: createFormLatestType() - }); - } - /** - * In case previously published revision exists, replace current one with that one. - * And if it does not, delete the record. - */ - if (isLatestPublished) { - const revisions = await listFormRevisions({ - where: { - formId, - tenant, - locale, - version_not: form.version, - publishedOn_not: null - }, - sort: ["savedOn_DESC"] - }); - - const previouslyPublishedRevision = revisions.shift(); - if (previouslyPublishedRevision) { - items.push( - entity.putBatch({ - ...previouslyPublishedRevision, - ...latestPublishedKeys, - TYPE: createFormLatestPublishedType() - }) - ); - } else { - items.push(entity.deleteBatch(latestPublishedKeys)); - } - } - - try { - await batchWriteAll({ - table, - items - }); - return form; - } catch (ex) { - throw new WebinyError( - ex.message || "Could not unpublish form.", - ex.code || "UNPUBLISH_FORM_ERROR", - { - form, - original, - latestForm, - revisionKeys, - latestKeys, - latestPublishedKeys - } - ); - } + const unpublishForm = async () => { + throw new Error( + "api-form-builder-ddb does not implement the Form Builder storage operations." + ); }; return { diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts index 63444245cce..1470f2a63e2 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts @@ -3,7 +3,9 @@ import { Security } from "@webiny/api-security/types"; import { FbForm, FormBuilderStorageOperationsListFormsParams, - FormBuilderStorageOperationsListFormRevisionsParams + FormBuilderStorageOperationsListFormRevisionsParams, + FormBuilderStorageOperationsUpdateFormParams, + FormBuilderStorageOperationsDeleteFormRevisionParams } from "~/types"; interface ModelContext { @@ -32,36 +34,38 @@ export class CmsFormBuilderStorage { createForm = async ({ form }: { form: FbForm }) => { const model = this.modelWithContext(form); + const entry = await this.security.withoutAuthorization(() => { return this.cms.createEntry(model, { ...form }); }); + return this.getFormFieldValues(entry); }; getForm = async ({ where }: FormBuilderStorageOperationsListFormsParams) => { const { id, tenant, locale } = where; const model = this.modelWithContext({ tenant, locale }); + const entry = await this.security.withoutAuthorization(async () => { return await this.cms.getEntryById(model, id || ""); }); + return entry ? this.getFormFieldValues(entry) : null; }; listFormRevisions = async (params: FormBuilderStorageOperationsListFormRevisionsParams) => { - const { - where: { tenant, locale, formId } - } = params; + const { tenant, locale, formId } = params.where; const model = this.modelWithContext({ tenant, locale }); + const entries = await this.security.withoutAuthorization(async () => { return await this.cms.getEntryRevisions(model, formId); }); + return entries.map(entry => this.getFormFieldValues(entry)); }; listForms = async (params: FormBuilderStorageOperationsListFormsParams) => { - const tenant = params.where.tenant; - const locale = params.where.locale; - + const { id, tenant, locale, ...restWhere } = params.where; const model = this.modelWithContext({ tenant, locale }); const [entries, meta] = await this.security.withoutAuthorization(async () => { @@ -69,7 +73,7 @@ export class CmsFormBuilderStorage { after: params.after, limit: params.limit, sort: params.sort, - where: {} + where: { entryId: id, ...restWhere } }); }); @@ -79,9 +83,11 @@ export class CmsFormBuilderStorage { createFormFrom = async (params: any) => { const { form } = params; const model = this.modelWithContext(form); + const entry = await this.security.withoutAuthorization(async () => { return await this.cms.createEntryRevisionFrom(model, form.id, {}); }); + return entry ? this.getFormFieldValues(entry) : null; }; @@ -95,7 +101,7 @@ export class CmsFormBuilderStorage { }); }; - deleteFormRevision = async ({ form }: { form: FbForm }) => { + deleteFormRevision = async ({ form }: FormBuilderStorageOperationsDeleteFormRevisionParams) => { const model = this.modelWithContext(form); await this.security.withoutAuthorization(async () => { @@ -103,6 +109,21 @@ export class CmsFormBuilderStorage { }); }; + updateForm = async ({ + form, + input, + meta, + options + }: FormBuilderStorageOperationsUpdateFormParams) => { + const model = this.modelWithContext(form); + + const entry = await this.security.withoutAuthorization(async () => { + return await this.cms.updateEntry(model, form.id, input, meta, options); + }); + + return entry ? this.getFormFieldValues(entry) : null; + }; + private getFormFieldValues(entry: CmsEntry) { return { id: entry.id, diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts index 2adc3036c4a..338ca5921c6 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts @@ -3,7 +3,6 @@ import omit from "lodash/omit"; import { CmsEntry, CmsModel, HeadlessCms } from "@webiny/api-headless-cms/types"; import { Security } from "@webiny/api-security/types"; -import { ListSubmissionsWhereProcessor } from "~/cmsFormBuilderStorage/ListSubmissionsWhereProcessor"; import { FormBuilderStorageOperationsCreateSubmissionParams, FormBuilderStorageOperationsUpdateSubmissionParams, @@ -19,7 +18,6 @@ export class CmsSubmissionsStorage { private readonly cms: HeadlessCms; private readonly security: Security; private readonly model: CmsModel; - private readonly submissionsWhereProcessor: ListSubmissionsWhereProcessor; static async create(params: { model: CmsModel; cms: HeadlessCms; security: Security }) { return new CmsSubmissionsStorage(params.model, params.cms, params.security); @@ -29,7 +27,6 @@ export class CmsSubmissionsStorage { this.model = model; this.cms = cms; this.security = security; - this.submissionsWhereProcessor = new ListSubmissionsWhereProcessor(); } private modelWithContext({ tenant, locale }: ModelContext): CmsModel { @@ -37,18 +34,18 @@ export class CmsSubmissionsStorage { } listSubmissions = async (params: FormBuilderStorageOperationsListSubmissionsParams) => { - const tenant = params.where.tenant; - const locale = params.where.locale; - + const { id_in, formId, tenant, locale } = params.where; const model = this.modelWithContext({ tenant, locale }); const [entries, meta] = await this.security.withoutAuthorization(async () => { - const where = this.submissionsWhereProcessor.process(params.where); return await this.cms.listLatestEntries(model, { after: params.after, limit: params.limit, sort: params.sort, - where + where: { + entryId_in: id_in, + form: { parent: formId } + } }); }); @@ -59,6 +56,7 @@ export class CmsSubmissionsStorage { submission }: FormBuilderStorageOperationsCreateSubmissionParams) => { const model = this.modelWithContext(submission); + const entry = await this.security.withoutAuthorization(() => { return this.cms.createEntry(model, { ...submission }); }); diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/ListSubmissionsWhereProcessor.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/ListSubmissionsWhereProcessor.ts deleted file mode 100644 index 8057e76e5e4..00000000000 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/ListSubmissionsWhereProcessor.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { CmsEntryListWhere } from "@webiny/api-headless-cms/types"; -import { FormBuilderStorageOperationsListSubmissionsParams } from "~/types"; - -type StandardSubmissionKey = keyof FormBuilderStorageOperationsListSubmissionsParams["where"]; -type CmsEntryListWhereKey = keyof CmsEntryListWhere; - -export class ListSubmissionsWhereProcessor { - private readonly skipKeys = ["tenant", "locale", "formId"]; - private readonly keyMap: Partial> = { - id_in: "entryId_in" - }; - - process(input: FormBuilderStorageOperationsListSubmissionsParams["where"]): CmsEntryListWhere { - const where: CmsEntryListWhere = input.formId ? { form: { parent: input.formId } } : {}; - - Object.keys(input) - .filter(key => !this.skipKeys.includes(key)) - .forEach(key => { - const remappedKey = this.keyMap[key as StandardSubmissionKey]; - const value = input[key as StandardSubmissionKey]; - - if (remappedKey && value !== undefined) { - where[remappedKey] = value; - } else if (value !== undefined) { - where[key] = value; - } - }); - - return where; - } -} diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderContext.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderContext.ts index 3b581b05cda..b7be02c408d 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderContext.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderContext.ts @@ -9,7 +9,7 @@ export const createFormBuilderContext = ({ storageOperations }: { storageOperati await fbContext.setupContext(storageOperations); }); - plugin.name = "form.builder-createContext"; + plugin.name = "form-builder.createContext"; return plugin; }; diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/createGraphQLSchemaPlugin.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/createGraphQLSchemaPlugin.ts index 9eacfca6c4b..e15a8c7127e 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/createGraphQLSchemaPlugin.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/createGraphQLSchemaPlugin.ts @@ -1,7 +1,54 @@ +import { ContextPlugin } from "@webiny/api"; +import { GraphQLSchemaPlugin } from "@webiny/handler-graphql"; +import { CmsModel } from "@webiny/api-headless-cms/types"; +import { createFieldTypePluginRecords } from "@webiny/api-headless-cms/graphql/schema/createFieldTypePluginRecords"; +import { createGraphQLSchemaPluginFromFieldPlugins } from "@webiny/api-headless-cms/utils/getSchemaFromFieldPlugins"; + +import { isInstallationPending } from "~/cmsFormBuilderStorage/isInstallationPending"; import { createFormBuilderSettingsSchema } from "~/plugins/graphql/formSettings"; import { createFormSchema } from "~/plugins/graphql/form"; import { createBaseSchema } from "~/plugins/graphql"; +import { createSubmissionsSchema } from "~/plugins/graphql/submissionsSchema"; +import { FormBuilderContext } from "~/types"; export const createGraphQLSchemaPlugin = () => { - return [createBaseSchema(), createFormBuilderSettingsSchema(), createFormSchema()]; + return [ + createBaseSchema(), + createFormBuilderSettingsSchema(), + createFormSchema(), + // Submission schema is generated dynamically, based on a CMS model, so we need to + // register it from a ContextPlugin, to perform additional bootstrap. + new ContextPlugin(async context => { + if (isInstallationPending(context)) { + return; + } + + await context.security.withoutAuthorization(async () => { + const submissionModel = (await context.cms.getModel("fbSubmission")) as CmsModel; + const models = await context.cms.listModels(); + const fieldPlugins = createFieldTypePluginRecords(context.plugins); + /** + * We need to register all plugins for all the CMS fields. + */ + const plugins = createGraphQLSchemaPluginFromFieldPlugins({ + models, + type: "manage", + fieldTypePlugins: fieldPlugins, + createPlugin: ({ schema, type, fieldType }) => { + const plugin = new GraphQLSchemaPlugin(schema); + plugin.name = `fb.graphql.schema.${type}.field.${fieldType}`; + return plugin; + } + }); + + const graphQlPlugin = createSubmissionsSchema({ + model: submissionModel, + models, + plugins: fieldPlugins + }); + + context.plugins.register([...plugins, graphQlPlugin]); + }); + }) + ]; }; diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/isInstallationPending.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/isInstallationPending.ts new file mode 100644 index 00000000000..7ab12f1d503 --- /dev/null +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/isInstallationPending.ts @@ -0,0 +1,15 @@ +import { FormBuilderContext } from "~/types"; + +type CheckInstallationParams = Pick; +export const isInstallationPending = ({ tenancy, i18n }: CheckInstallationParams): boolean => { + /** + * In case of a fresh webiny project "tenant" and "locale" won't be there until + * installation is completed. So, we need to skip "storage" creation till then. + */ + const tenant = tenancy.getCurrentTenant(); + if (!tenant) { + return true; + } + + return !i18n.getContentLocale(); +}; diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/submission.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/submission.model.ts index 4d1c1ccd151..50ff015ae38 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/models/submission.model.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/submission.model.ts @@ -56,7 +56,7 @@ const metaUrlField = (fields: CmsModelField[]) => { return createModelField({ label: "URL", fieldId: "url", - type: "text", + type: "object", settings: { fields, layout: fields.map(field => [field.storageId]) @@ -85,6 +85,15 @@ const formIdField = () => { }); }; +const formNameField = () => { + return createModelField({ + label: "Name", + fieldId: "name", + type: "text", + validation: [required()] + }); +}; + const formParentField = () => { return createModelField({ label: "Parent", @@ -143,6 +152,7 @@ export const createSubmissionDataModelDefinition = (group: CmsModelGroup): CmsPr ]), formField([ formIdField(), + formNameField(), formParentField(), versionField(), fieldsField(FIELD_FIELDS), diff --git a/packages/api-form-builder/src/plugins/crud/forms.crud.ts b/packages/api-form-builder/src/plugins/crud/forms.crud.ts index 55ef9ed61a1..835ac17c893 100644 --- a/packages/api-form-builder/src/plugins/crud/forms.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/forms.crud.ts @@ -410,9 +410,10 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { original }); const result = await this.storageOperations.updateForm({ - input: data, form, - original + input, + meta: {}, + options: {} }); await onFormAfterUpdate.publish({ form, @@ -710,8 +711,8 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { try { await this.storageOperations.updateForm({ - original, - form + form, + input: {} }); } catch (ex) { throw new WebinyError( @@ -743,8 +744,8 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { try { await this.storageOperations.updateForm({ - original, - form + form, + input: {} }); } catch (ex) { throw new WebinyError( diff --git a/packages/api-form-builder/src/plugins/graphql/createSubmissionsTypeDefs.ts b/packages/api-form-builder/src/plugins/graphql/createSubmissionsTypeDefs.ts new file mode 100644 index 00000000000..f389bec65db --- /dev/null +++ b/packages/api-form-builder/src/plugins/graphql/createSubmissionsTypeDefs.ts @@ -0,0 +1,95 @@ +import { CmsFieldTypePlugins, CmsModel } from "@webiny/api-headless-cms/types"; +import { renderFields } from "@webiny/api-headless-cms/utils/renderFields"; +import { renderSortEnum } from "@webiny/api-headless-cms/utils/renderSortEnum"; + +export interface CreateSubmissionsTypeDefsParams { + model: CmsModel; + models: CmsModel[]; + plugins: CmsFieldTypePlugins; +} + +export const createSubmissionsTypeDefs = (params: CreateSubmissionsTypeDefsParams): string => { + const { model, models, plugins: fieldTypePlugins } = params; + const { fields } = model; + + const fieldTypes = renderFields({ + models, + model, + fields, + type: "manage", + fieldTypePlugins + }); + + const sortEnumRender = renderSortEnum({ + model, + fields: model.fields, + fieldTypePlugins, + sorterPlugins: [] + }); + + return /* GraphQL */ ` + ${fieldTypes.map(f => f.typeDefs).join("\n")} + + type FbFormSubmission { + id: ID! + savedOn: DateTime! + createdOn: DateTime! + createdBy: FmCreatedBy! + ${fieldTypes.map(f => f.fields).join("\n")} + } + + enum FbSubmissionSort { + ${sortEnumRender} + } + + type FbFormSubmissionResponse { + data: FbFormSubmission + error: FbError + } + + type FbListSubmissionsMeta { + cursor: String + hasMoreItems: Boolean + totalCount: Int + } + + type FbFormSubmissionsListResponse { + data: [FbFormSubmission] + meta: FbListSubmissionsMeta + error: FbError + } + + type FbExportFormSubmissionsFile { + src: String + key: String + } + + type FbExportFormSubmissionsResponse { + data: FbExportFormSubmissionsFile + error: FbError + } + + extend type FbQuery { + # List form submissions for specific Form + listFormSubmissions( + form: ID! + sort: [FbSubmissionSort!] + limit: Int + after: String + ): FbFormSubmissionsListResponse + } + + extend type FbMutation { + # Submits a form + createFormSubmission( + revision: ID! + data: JSON! + reCaptchaResponseToken: String + meta: JSON + ): FbFormSubmissionResponse + + # Export submissions as a CSV file + exportFormSubmissions(form: ID!): FbExportFormSubmissionsResponse + } + `; +}; diff --git a/packages/api-form-builder/src/plugins/graphql/form.ts b/packages/api-form-builder/src/plugins/graphql/form.ts index 3c91ef05bb8..786db8ac2df 100644 --- a/packages/api-form-builder/src/plugins/graphql/form.ts +++ b/packages/api-form-builder/src/plugins/graphql/form.ts @@ -1,14 +1,10 @@ -import { parseAsync } from "json2csv"; -import { format } from "date-fns"; import { ErrorResponse, ListResponse, NotFoundResponse, Response } from "@webiny/handler-graphql/responses"; -import { sanitizeFormSubmissionData, flattenSubmissionMeta } from "~/plugins/crud/utils"; -import { FormBuilderContext, FbFormField } from "~/types"; -import { mdbid } from "@webiny/utils"; +import { FormBuilderContext } from "~/types"; import { GraphQLSchemaPlugin } from "@webiny/handler-graphql"; export const createFormSchema = () => { @@ -205,72 +201,11 @@ export const createFormSchema = () => { error: FbError } - type FbSubmissionFormData { - id: ID - parent: ID - name: String - version: Int - fields: [FbFormFieldType] - steps: [FbFormStepType] - } - - type FbFormSubmission { - id: ID - data: JSON - meta: FbSubmissionMeta - form: FbSubmissionFormData - } - - type FbSubmissionMetaUrl { - location: String - query: JSON - } - - type FbSubmissionMeta { - ip: String - submittedOn: DateTime - url: FbSubmissionMetaUrl - } - - type FbListSubmissionsMeta { - cursor: String - hasMoreItems: Boolean - totalCount: Int - } - - type FbFormSubmissionsListResponse { - data: [FbFormSubmission] - meta: FbListSubmissionsMeta - error: FbError - } - - type FbFormSubmissionResponse { - data: FbFormSubmission - error: FbError - } - type FbFormRevisionsResponse { data: [FbForm] error: FbError } - type FbExportFormSubmissionsFile { - src: String - key: String - } - - type FbExportFormSubmissionsResponse { - data: FbExportFormSubmissionsFile - error: FbError - } - - enum FbSubmissionSort { - createdOn_ASC - createdOn_DESC - savedOn_ASC - savedOn_DESC - } - extend type FbQuery { # Get form (can be published or not, requires authorization ) getForm(revision: ID!): FbFormResponse @@ -283,14 +218,6 @@ export const createFormSchema = () => { # List forms (returns a list of latest revision) listForms: FbFormListResponse - - # List form submissions for specific Form - listFormSubmissions( - form: ID! - sort: [FbSubmissionSort!] - limit: Int - after: String - ): FbFormSubmissionsListResponse } extend type FbMutation { @@ -316,17 +243,6 @@ export const createFormSchema = () => { # Logs a view of a form saveFormView(revision: ID!): FbSaveFormViewResponse - - # Submits a form - createFormSubmission( - revision: ID! - data: JSON! - reCaptchaResponseToken: String - meta: JSON - ): FbFormSubmissionResponse - - # Export submissions as a CSV file - exportFormSubmissions(form: ID!): FbExportFormSubmissionsResponse } `, resolvers: { @@ -408,18 +324,6 @@ export const createFormSchema = () => { } return new Response(form); - }, - listFormSubmissions: async (_, args: any, { formBuilder }) => { - try { - const { form, ...options } = args; - const [submissions, meta] = await formBuilder.listFormSubmissions( - form, - options - ); - return new ListResponse(submissions, meta); - } catch (err) { - return new ErrorResponse(err); - } } }, FbMutation: { @@ -510,153 +414,6 @@ export const createFormSchema = () => { } catch (e) { return new ErrorResponse(e); } - }, - createFormSubmission: async (_: any, args: any, { formBuilder }) => { - const { revision, data, reCaptchaResponseToken, meta = {} } = args; - - try { - const formSubmission = await formBuilder.createFormSubmission( - revision, - reCaptchaResponseToken, - data, - meta - ); - - return new Response(formSubmission); - } catch (e) { - return new ErrorResponse(e); - } - }, - exportFormSubmissions: async (_: any, args: any, { formBuilder, fileManager }) => { - const { form } = args; - - try { - await formBuilder.onFormSubmissionsBeforeExport.publish({ form }); - const [submissions] = await formBuilder.listFormSubmissions(form, { - limit: 10000 - }); - - if (submissions.length === 0) { - return new NotFoundResponse("No form submissions found."); - } - - /** - * Get all revisions of the form. - */ - const revisions = await formBuilder.getFormRevisions(form); - const publishedRevisions = revisions.filter(r => r.published); - - const rows: Record[] = []; - const fields: Record = {}; - const fieldsData: FbFormField[] = []; - - /** - * First extract all distinct fields across all form submissions. - */ - for (let i = 0; i < publishedRevisions.length; i++) { - const revision = publishedRevisions[i]; - for (let j = 0; j < revision.fields.length; j++) { - const field = revision.fields[j]; - if (!fields[field.fieldId]) { - fieldsData.push(field); - fields[field.fieldId] = field.label; - - if (field?.options && field?.options?.length > 0) { - fields[`${field.fieldId}_label`] = `${field.label} (Label)`; - } - } - } - } - - /** - * Add meta fields. - */ - for (let i = 0; i < submissions.length; i++) { - const flattenedSubmissionMeta = flattenSubmissionMeta( - submissions[i].meta.url || {}, - "meta_url" - ); - - for (const metaKey in flattenedSubmissionMeta) { - if (!fields[metaKey]) { - fields[metaKey] = metaKey; - } - } - } - - /** - * Build rows. - */ - for (let i = 0; i < submissions.length; i++) { - const submissionData = sanitizeFormSubmissionData( - fieldsData, - submissions[i].data - ); - - const flattenedSubmissionMeta = flattenSubmissionMeta( - submissions[i].meta.url || {}, - "meta_url" - ); - for (const metaKey in flattenedSubmissionMeta) { - submissionData[metaKey] = flattenedSubmissionMeta[metaKey]; - } - - const row: Record = {}; - - row["Date submitted (UTC)"] = format( - new Date(submissions[i].createdOn), - "yyyy-MM-dd HH:mm:ss" - ); - - Object.keys(fields).map(fieldId => { - if (fieldId in submissionData) { - const value = submissionData[fieldId]; - // Remove brackets from arrays; - row[fields[fieldId]] = Array.isArray(value) - ? value.map(item => `"${item}"`).join(", ") - : value; - } else { - row[fields[fieldId]] = "N/A"; - } - }); - rows.push(row); - } - - /** - * Save CSV file and return its URL to the client. - */ - const csv = await parseAsync(rows, { - fields: ["Date submitted (UTC)", ...Object.values(fields)] - }); - - const buffer = Buffer.from(csv); - const id = mdbid(); - - const fileData = { - buffer, - id, - size: buffer.length, - name: "form_submissions_export.csv", - key: `${id}/form_submissions_export.csv`, - type: "text/csv", - keyPrefix: "form-submissions", - hideInFileManager: true - }; - - const { key } = await fileManager.storage.upload(fileData); - - const settings = await fileManager.getSettings(); - - const result = { - key, - src: (settings?.srcPrefix || "") + key - }; - await formBuilder.onFormSubmissionsAfterExport.publish({ result }); - - return new Response(result); - } catch (e) { - return new ErrorResponse(e); - } } } } diff --git a/packages/api-form-builder/src/plugins/graphql/submissionsSchema.ts b/packages/api-form-builder/src/plugins/graphql/submissionsSchema.ts new file mode 100644 index 00000000000..697a078140f --- /dev/null +++ b/packages/api-form-builder/src/plugins/graphql/submissionsSchema.ts @@ -0,0 +1,192 @@ +import { parseAsync } from "json2csv"; +import { format } from "date-fns"; + +import { mdbid } from "@webiny/utils"; +import { + ErrorResponse, + GraphQLSchemaPlugin, + ListResponse, + NotFoundResponse, + Response +} from "@webiny/handler-graphql"; + +import { sanitizeFormSubmissionData, flattenSubmissionMeta } from "~/plugins/crud/utils"; +import { + createSubmissionsTypeDefs, + CreateSubmissionsTypeDefsParams +} from "~/plugins/graphql/createSubmissionsTypeDefs"; +import { FormBuilderContext, FbFormField } from "~/types"; + +export const createSubmissionsSchema = (params: CreateSubmissionsTypeDefsParams) => { + const submissionsGraphQL = new GraphQLSchemaPlugin({ + typeDefs: createSubmissionsTypeDefs(params), + resolvers: { + FbQuery: { + listFormSubmissions: async (_, args: any, { formBuilder }) => { + try { + const { form, ...options } = args; + const [submissions, meta] = await formBuilder.listFormSubmissions( + form, + options + ); + return new ListResponse(submissions, meta); + } catch (err) { + return new ErrorResponse(err); + } + } + }, + FbMutation: { + createFormSubmission: async (_: any, args: any, { formBuilder }) => { + const { revision, data, reCaptchaResponseToken, meta = {} } = args; + + try { + const formSubmission = await formBuilder.createFormSubmission( + revision, + reCaptchaResponseToken, + data, + meta + ); + + return new Response(formSubmission); + } catch (e) { + return new ErrorResponse(e); + } + }, + exportFormSubmissions: async (_: any, args: any, { formBuilder, fileManager }) => { + const { form } = args; + + try { + await formBuilder.onFormSubmissionsBeforeExport.publish({ form }); + const [submissions] = await formBuilder.listFormSubmissions(form, { + limit: 10000 + }); + + if (submissions.length === 0) { + return new NotFoundResponse("No form submissions found."); + } + + /** + * Get all revisions of the form. + */ + const revisions = await formBuilder.getFormRevisions(form); + const publishedRevisions = revisions.filter(r => r.published); + + const rows: Record[] = []; + const fields: Record = {}; + const fieldsData: FbFormField[] = []; + + /** + * First extract all distinct fields across all form submissions. + */ + for (let i = 0; i < publishedRevisions.length; i++) { + const revision = publishedRevisions[i]; + for (let j = 0; j < revision.fields.length; j++) { + const field = revision.fields[j]; + if (!fields[field.fieldId]) { + fieldsData.push(field); + fields[field.fieldId] = field.label; + + if (field?.options && field?.options?.length > 0) { + fields[`${field.fieldId}_label`] = `${field.label} (Label)`; + } + } + } + } + + /** + * Add meta fields. + */ + for (let i = 0; i < submissions.length; i++) { + const flattenedSubmissionMeta = flattenSubmissionMeta( + submissions[i].meta.url || {}, + "meta_url" + ); + + for (const metaKey in flattenedSubmissionMeta) { + if (!fields[metaKey]) { + fields[metaKey] = metaKey; + } + } + } + + /** + * Build rows. + */ + for (let i = 0; i < submissions.length; i++) { + const submissionData = sanitizeFormSubmissionData( + fieldsData, + submissions[i].data + ); + + const flattenedSubmissionMeta = flattenSubmissionMeta( + submissions[i].meta.url || {}, + "meta_url" + ); + for (const metaKey in flattenedSubmissionMeta) { + submissionData[metaKey] = flattenedSubmissionMeta[metaKey]; + } + + const row: Record = {}; + + row["Date submitted (UTC)"] = format( + new Date(submissions[i].createdOn), + "yyyy-MM-dd HH:mm:ss" + ); + + Object.keys(fields).map(fieldId => { + if (fieldId in submissionData) { + const value = submissionData[fieldId]; + // Remove brackets from arrays; + row[fields[fieldId]] = Array.isArray(value) + ? value.map(item => `"${item}"`).join(", ") + : value; + } else { + row[fields[fieldId]] = "N/A"; + } + }); + rows.push(row); + } + + /** + * Save CSV file and return its URL to the client. + */ + const csv = await parseAsync(rows, { + fields: ["Date submitted (UTC)", ...Object.values(fields)] + }); + + const buffer = Buffer.from(csv); + const id = mdbid(); + + const fileData = { + buffer, + id, + size: buffer.length, + name: "form_submissions_export.csv", + key: `${id}/form_submissions_export.csv`, + type: "text/csv", + keyPrefix: "form-submissions", + hideInFileManager: true + }; + + const { key } = await fileManager.storage.upload(fileData); + + const settings = await fileManager.getSettings(); + + const result = { + key, + src: (settings?.srcPrefix || "") + key + }; + await formBuilder.onFormSubmissionsAfterExport.publish({ result }); + + return new Response(result); + } catch (e) { + return new ErrorResponse(e); + } + } + } + } + }); + submissionsGraphQL.name = "fb.graphql.submissions"; + + return submissionsGraphQL; +}; diff --git a/packages/api-form-builder/src/types.ts b/packages/api-form-builder/src/types.ts index 14e6c6a14ae..2a060b93e63 100644 --- a/packages/api-form-builder/src/types.ts +++ b/packages/api-form-builder/src/types.ts @@ -618,9 +618,10 @@ export interface FormBuilderStorageOperationsCreateFormFromParams { * @category StorageOperationsParams */ export interface FormBuilderStorageOperationsUpdateFormParams { - input?: Record; - original: FbForm; form: FbForm; + input: Record; + meta?: Record; + options?: Record; } /** @@ -636,14 +637,6 @@ export interface FormBuilderStorageOperationsDeleteFormParams { * @category StorageOperationsParams */ export interface FormBuilderStorageOperationsDeleteFormRevisionParams { - /** - * Method always receives all the revisions of given form ordered by version_DESC. - */ - revisions: FbForm[]; - /** - * Previous revision of the current form. Always the first lesser available version. - */ - previous: FbForm | null; form: FbForm; } @@ -763,7 +756,9 @@ export interface FormBuilderFormStorageOperations { /** * Delete the single form revision. */ - deleteFormRevision({ form }: { form: FbForm }): Promise; + deleteFormRevision( + params: FormBuilderStorageOperationsDeleteFormRevisionParams + ): Promise; publishForm(params: FormBuilderStorageOperationsPublishFormParams): Promise; unpublishForm(params: FormBuilderStorageOperationsUnpublishFormParams): Promise; } diff --git a/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/index.ts b/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/index.ts index 1b0f57697a4..9ef73b9fc43 100644 --- a/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/index.ts +++ b/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/index.ts @@ -22,10 +22,13 @@ import elasticsearchClientContext, { } from "@webiny/api-elasticsearch"; import { createFileManagerContext, createFileManagerGraphQL } from "@webiny/api-file-manager"; import { createFileManagerStorageOperations } from "@webiny/api-file-manager-ddb"; +import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; +import { + createFormBuilderContext, + createFormBuilderGraphQL +} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import logsPlugins from "@webiny/handler-logs"; import fileManagerS3 from "@webiny/api-file-manager-s3"; -import { createFormBuilder } from "@webiny/api-form-builder"; -import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb-es"; import { createHeadlessCmsContext, createHeadlessCmsGraphQL } from "@webiny/api-headless-cms"; import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb-es"; import { createAco } from "@webiny/api-aco"; @@ -99,12 +102,12 @@ export const handler = createHandler({ pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), - createFormBuilder({ + createFormBuilderContext({ storageOperations: createFormBuilderStorageOperations({ - documentClient, - elasticsearch: elasticsearchClient + documentClient }) }), + createFormBuilderGraphQL(), createGzipCompression(), createApwGraphQL(), createApwPageBuilderContext({ From cb042fbf32a2fdcb2de88f2b524a16e4f3ca35c8 Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Fri, 3 Nov 2023 16:10:53 +0000 Subject: [PATCH 08/37] feat: moved publishRevision, unpublishRevision into HCMS, dynamic schema for forms --- .../CmsFormBuilderStorage.ts | 36 +- .../createGraphQLSchemaPlugin.ts | 18 +- .../models/form.model.ts | 191 ++++++-- .../src/plugins/crud/forms.crud.ts | 32 +- .../plugins/graphql/createFormsTypeDefs.ts | 178 ++++++++ .../src/plugins/graphql/form.ts | 423 ------------------ .../src/plugins/graphql/formsSchema.ts | 194 ++++++++ packages/api-form-builder/src/types.ts | 2 + 8 files changed, 582 insertions(+), 492 deletions(-) create mode 100644 packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts delete mode 100644 packages/api-form-builder/src/plugins/graphql/form.ts create mode 100644 packages/api-form-builder/src/plugins/graphql/formsSchema.ts diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts index 1470f2a63e2..900954befbb 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts @@ -5,7 +5,9 @@ import { FormBuilderStorageOperationsListFormsParams, FormBuilderStorageOperationsListFormRevisionsParams, FormBuilderStorageOperationsUpdateFormParams, - FormBuilderStorageOperationsDeleteFormRevisionParams + FormBuilderStorageOperationsDeleteFormRevisionParams, + FormBuilderStorageOperationsPublishFormParams, + FormBuilderStorageOperationsUnpublishFormParams } from "~/types"; interface ModelContext { @@ -85,7 +87,15 @@ export class CmsFormBuilderStorage { const model = this.modelWithContext(form); const entry = await this.security.withoutAuthorization(async () => { - return await this.cms.createEntryRevisionFrom(model, form.id, {}); + return await this.cms.createEntryRevisionFrom(model, form.id, { + status: "draft", + published: false, + locked: false, + stats: { + submissions: 0, + views: 0 + } + }); }); return entry ? this.getFormFieldValues(entry) : null; @@ -124,6 +134,28 @@ export class CmsFormBuilderStorage { return entry ? this.getFormFieldValues(entry) : null; }; + publishForm = async (params: FormBuilderStorageOperationsPublishFormParams) => { + const { form, input } = params; + const model = this.modelWithContext(form); + + const entry = await this.security.withoutAuthorization(async () => { + return await this.cms.updateEntry(model, form.id, input); + }); + + return entry ? this.getFormFieldValues(entry) : null; + }; + + unpublishForm = async (params: FormBuilderStorageOperationsUnpublishFormParams) => { + const { form, input } = params; + const model = this.modelWithContext(form); + + const entry = await this.security.withoutAuthorization(async () => { + return await this.cms.updateEntry(model, form.id, input); + }); + + return entry ? this.getFormFieldValues(entry) : null; + }; + private getFormFieldValues(entry: CmsEntry) { return { id: entry.id, diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/createGraphQLSchemaPlugin.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/createGraphQLSchemaPlugin.ts index e15a8c7127e..90b8684fa17 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/createGraphQLSchemaPlugin.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/createGraphQLSchemaPlugin.ts @@ -6,16 +6,15 @@ import { createGraphQLSchemaPluginFromFieldPlugins } from "@webiny/api-headless- import { isInstallationPending } from "~/cmsFormBuilderStorage/isInstallationPending"; import { createFormBuilderSettingsSchema } from "~/plugins/graphql/formSettings"; -import { createFormSchema } from "~/plugins/graphql/form"; import { createBaseSchema } from "~/plugins/graphql"; import { createSubmissionsSchema } from "~/plugins/graphql/submissionsSchema"; +import { createFormsSchema } from "~/plugins/graphql/formsSchema"; import { FormBuilderContext } from "~/types"; export const createGraphQLSchemaPlugin = () => { return [ createBaseSchema(), createFormBuilderSettingsSchema(), - createFormSchema(), // Submission schema is generated dynamically, based on a CMS model, so we need to // register it from a ContextPlugin, to perform additional bootstrap. new ContextPlugin(async context => { @@ -25,6 +24,7 @@ export const createGraphQLSchemaPlugin = () => { await context.security.withoutAuthorization(async () => { const submissionModel = (await context.cms.getModel("fbSubmission")) as CmsModel; + const formsModel = (await context.cms.getModel("fbForm")) as CmsModel; const models = await context.cms.listModels(); const fieldPlugins = createFieldTypePluginRecords(context.plugins); /** @@ -41,13 +41,23 @@ export const createGraphQLSchemaPlugin = () => { } }); - const graphQlPlugin = createSubmissionsSchema({ + const formsGraphQlPlugin = createFormsSchema({ + model: formsModel, + models, + plugins: fieldPlugins + }); + + const submissionsGraphQlPlugin = createSubmissionsSchema({ model: submissionModel, models, plugins: fieldPlugins }); - context.plugins.register([...plugins, graphQlPlugin]); + context.plugins.register([ + ...plugins, + formsGraphQlPlugin, + submissionsGraphQlPlugin + ]); }); }) ]; diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts index d1062abf418..c4bf51d3e2d 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts @@ -41,6 +41,49 @@ const statusField = () => { }); }; +const statsViewsField = () => { + return createModelField({ + label: "Views", + type: "number" + }); +}; + +const statsSubmissionsField = () => { + return createModelField({ + label: "Submissions", + type: "number" + }); +}; + +const conversionRateStatsSubmissionsField = () => { + return createModelField({ + label: "Conversion Rate", + fieldId: "conversionRate", + type: "number" + }); +}; + +const statsField = (fields: CmsModelField[]) => { + return createModelField({ + label: "Stats", + type: "object", + settings: { + fields + } + }); +}; + +const overallStatsField = (fields: CmsModelField[]) => { + return createModelField({ + label: "Overall Stats", + fieldId: "overallStats", + type: "object", + settings: { + fields + } + }); +}; + const lockedField = () => { return createModelField({ label: "Locked", @@ -145,36 +188,11 @@ const fieldValidationMessageField = () => { }); }; -const fieldValidationSettingsValuesField = () => { - return createModelField({ - label: "Values", - type: "object", - multipleValues: true - }); -}; - -const fieldValidationSettingsValueField = () => { - return createModelField({ - label: "Value", - type: "text" - }); -}; - -const fieldValidationSettingsPresetField = () => { - return createModelField({ - label: "Preset", - type: "text" - }); -}; - -const fieldValidationSettingsField = (fields: CmsModelField[]) => { +const fieldValidationSettingsField = () => { return createModelField({ label: "Settings", - type: "object", - validation: [required()], - settings: { - fields - } + type: "json", + validation: [required()] }); }; @@ -193,7 +211,7 @@ const fieldValidationField = (fields: CmsModelField[]) => { const fieldSettingsField = () => { return createModelField({ label: "Settings", - type: "object" + type: "json" }); }; @@ -245,10 +263,43 @@ const settingsReCaptchaEnabledField = () => { }); }; +const settingsReCaptchaSettingsEnabledField = () => { + return createModelField({ + label: "Enabled", + type: "boolean" + }); +}; + +const settingsReCaptchaSettingsSecretKeyField = () => { + return createModelField({ + label: "Secret Key", + fieldId: "secretKey", + type: "text" + }); +}; + +const settingsReCaptchaSettingsSiteKeyField = () => { + return createModelField({ + label: "Site Key", + fieldId: "siteKey", + type: "text" + }); +}; + +const settingsReCaptchaSettingsField = (fields: CmsModelField[]) => { + return createModelField({ + label: "Settings", + type: "object", + settings: { + fields + } + }); +}; + const settingsReCaptchaErrorMessageField = () => { return createModelField({ label: "ErrorMessage", - type: "text" + type: "json" }); }; @@ -290,21 +341,48 @@ const settingsSubmitButtonLabelField = () => { const settingsFullWidthSubmitButtonField = () => { return createModelField({ label: "FullWidthSubmitButton", - type: "text" + type: "boolean" }); }; const settingsSuccessMessageField = () => { return createModelField({ label: "SuccessMessage", + type: "json" + }); +}; + +const settingsTermsOfServiceMessageEnabledField = () => { + return createModelField({ + label: "Enabled", + fieldId: "enabled", + type: "boolean" + }); +}; + +const settingsTermsOfServiceMessageMessageField = () => { + return createModelField({ + label: "Message", + fieldId: "message", + type: "json" + }); +}; + +const settingsTermsOfServiceMessageErrorMessageField = () => { + return createModelField({ + label: "Error Message", + fieldId: "errorMessage", type: "text" }); }; -const settingsTermsOfServiceMessageField = () => { +const settingsTermsOfServiceMessageField = (fields: CmsModelField[]) => { return createModelField({ label: "TermsOfServiceMessage", - type: "text" + type: "object", + settings: { + fields + } }); }; @@ -318,15 +396,25 @@ const settingsField = (fields: CmsModelField[]) => { }); }; +const triggersField = () => { + return createModelField({ + label: "Triggers", + type: "json" + }); +}; + const DEFAULT_FIELDS = [ "formId", "name", "published", "status", + "stats", + "overallStats", "locked", "fields", "steps", - "settings" + "settings", + "triggers" ]; const SETTINGS_FIELDS: CmsModelField[] = [ @@ -334,8 +422,20 @@ const SETTINGS_FIELDS: CmsModelField[] = [ settingsSubmitButtonLabelField(), settingsFullWidthSubmitButtonField(), settingsSuccessMessageField(), - settingsTermsOfServiceMessageField(), - settingsReCaptchaField([settingsReCaptchaEnabledField(), settingsReCaptchaErrorMessageField()]) + settingsTermsOfServiceMessageField([ + settingsTermsOfServiceMessageEnabledField(), + settingsTermsOfServiceMessageMessageField(), + settingsTermsOfServiceMessageErrorMessageField() + ]), + settingsReCaptchaField([ + settingsReCaptchaEnabledField(), + settingsReCaptchaSettingsField([ + settingsReCaptchaSettingsEnabledField(), + settingsReCaptchaSettingsSecretKeyField(), + settingsReCaptchaSettingsSiteKeyField() + ]), + settingsReCaptchaErrorMessageField() + ]) ]; export const FIELD_FIELDS = [ @@ -350,11 +450,7 @@ export const FIELD_FIELDS = [ fieldValidationField([ fieldValidationNameField(), fieldValidationMessageField(), - fieldValidationSettingsField([ - fieldValidationSettingsValuesField(), - fieldValidationSettingsValueField(), - fieldValidationSettingsPresetField() - ]) + fieldValidationSettingsField() ]), fieldSettingsField() ]; @@ -372,10 +468,21 @@ export const createFormDataModelDefinition = (group: CmsModelGroup): any => { nameField(), publishedField(), statusField(), + statsField([ + statsViewsField(), + statsSubmissionsField(), + conversionRateStatsSubmissionsField() + ]), + overallStatsField([ + statsViewsField(), + statsSubmissionsField(), + conversionRateStatsSubmissionsField() + ]), lockedField(), fieldsField(FIELD_FIELDS), stepsField(STEP_FIELDS), - settingsField(SETTINGS_FIELDS) + settingsField(SETTINGS_FIELDS), + triggersField() ], description: "Form Builder - Form builder create data model", isPrivate: true, diff --git a/packages/api-form-builder/src/plugins/crud/forms.crud.ts b/packages/api-form-builder/src/plugins/crud/forms.crud.ts index 835ac17c893..e055026f62c 100644 --- a/packages/api-form-builder/src/plugins/crud/forms.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/forms.crud.ts @@ -227,7 +227,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { } }, async getPublishedFormRevisionById(this: FormBuilder, id) { - const [formId, version] = id.split("#"); + const [version] = id.split("#"); if (!version) { throw new WebinyError("There is no version in given ID value.", "VERSION_ERROR", { id @@ -238,8 +238,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { try { form = await this.storageOperations.getForm({ where: { - formId, - version: Number(version), + id, published: true, tenant: getTenant().id, locale: getLocale().code @@ -260,16 +259,11 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { return form; }, async getLatestPublishedFormRevision(this: FormBuilder, id) { - /** - * Make sure we have a unique form ID, and not a revision ID - */ - const [formId] = id.split("#"); - let form: FbForm | null = null; try { form = await this.storageOperations.getForm({ where: { - formId, + id, published: true, tenant: getTenant().id, locale: getLocale().code @@ -398,10 +392,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { const form: FbForm = { ...original, - ...data, - savedOn: new Date().toISOString(), - tenant: getTenant().id, - webinyVersion: context.WEBINY_VERSION + ...data }; try { @@ -538,10 +529,8 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { ...original, published: true, publishedOn: new Date().toISOString(), - locked: true, - savedOn: new Date().toISOString(), status: getStatus({ published: true, locked: true }), - tenant: getTenant().id, + locked: true, webinyVersion: context.WEBINY_VERSION }; @@ -551,7 +540,8 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { }); const result = await this.storageOperations.publishForm({ original, - form + form, + input: form }); await onFormAfterPublish.publish({ form @@ -583,7 +573,6 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { published: false, savedOn: new Date().toISOString(), status: getStatus({ published: false, locked: true }), - tenant: getTenant().id, webinyVersion: context.WEBINY_VERSION }; @@ -593,7 +582,8 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { }); const result = await this.storageOperations.unpublishForm({ original, - form + form, + input: form }); await onFormAfterUnpublish.publish({ form: result @@ -712,7 +702,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { try { await this.storageOperations.updateForm({ form, - input: {} + input: form }); } catch (ex) { throw new WebinyError( @@ -745,7 +735,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { try { await this.storageOperations.updateForm({ form, - input: {} + input: form }); } catch (ex) { throw new WebinyError( diff --git a/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts b/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts new file mode 100644 index 00000000000..7d4c1c2f869 --- /dev/null +++ b/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts @@ -0,0 +1,178 @@ +import { CmsFieldTypePlugins, CmsModel, CmsModelField } from "@webiny/api-headless-cms/types"; +import { renderFields } from "@webiny/api-headless-cms/utils/renderFields"; +import { renderInputFields } from "@webiny/api-headless-cms/utils/renderInputFields"; + +export interface CreateFormsTypeDefsParams { + model: CmsModel; + models: CmsModel[]; + plugins: CmsFieldTypePlugins; +} + +const removeFieldRequiredValidation = (field: CmsModelField) => { + if (field.validation) { + field.validation = field.validation.filter(validation => validation.name !== "required"); + } + if (field.listValidation) { + field.listValidation = field.listValidation.filter(v => v.name !== "required"); + } + return field; +}; + +const createUpdateFields = (fields: CmsModelField[]): CmsModelField[] => { + return fields.reduce((collection, field) => { + collection.push(removeFieldRequiredValidation({ ...field })); + return collection; + }, []); +}; + +export const createFormsTypeDefs = (params: CreateFormsTypeDefsParams): string => { + const { model, models, plugins: fieldTypePlugins } = params; + const { fields } = model; + + const fieldTypes = renderFields({ + models, + model, + fields, + type: "manage", + fieldTypePlugins + }); + + const inputFields = renderInputFields({ + models, + model, + fields, + fieldTypePlugins + }); + + const excludeFormFields = [ + "formId", + "published", + "status", + "stats", + "overallStats", + "locked", + "fields", + "steps", + "settings", + "triggers" + ]; + + const inputCreateFields = renderInputFields({ + models, + model, + fields: model.fields.filter(field => !excludeFormFields.includes(field.fieldId)), + fieldTypePlugins + }); + + const excludeUpdateFormFields = [ + "formId", + "published", + "status", + "stats", + "overallStats", + "locked" + ]; + + const inputUpdateFields = renderInputFields({ + models, + model, + fields: createUpdateFields( + model.fields.filter(field => !excludeUpdateFormFields.includes(field.fieldId)) + ), + fieldTypePlugins + }); + + return /* GraphQL */ ` + ${fieldTypes.map(f => f.typeDefs).join("\n")} + + enum FbFormStatusEnum { + published + draft + locked + } + + type FbFormUser { + id: String + displayName: String + type: String + } + + type FbForm { + id: ID! + createdBy: FbFormUser! + ownedBy: FbFormUser! + createdOn: DateTime! + savedOn: DateTime! + version: Number! + ${fieldTypes.map(f => f.fields).join("\n")} + } + + ${inputFields.map(f => f.typeDefs).join("\n")} + + input FbCreateFormInput { + ${inputCreateFields.map(f => f.fields).join("\n")} + } + + input FbUpdateFormInput { + ${inputUpdateFields.map(f => f.fields).join("\n")} + } + + type FbFormResponse { + data: FbForm + error: FbError + } + + type FbFormListResponse { + data: [FbForm] + error: FbError + } + + type FbFormRevisionsResponse { + data: [FbForm] + error: FbError + } + + type FbSaveFormViewResponse { + error: FbError + } + + extend type FbQuery { + # Get form (can be published or not, requires authorization ) + getForm(revision: ID!): FbFormResponse + + # List forms (returns a list of latest revision) + listForms: FbFormListResponse + + # Get form revisions + getFormRevisions(id: ID!): FbFormRevisionsResponse + + # Get published form by exact revision ID, or parent form ID (public access) + getPublishedForm(revision: ID, parent: ID): FbFormResponse + } + + extend type FbMutation { + createForm(data: FbCreateFormInput!): FbFormResponse + + # Create a new revision from an existing revision + createRevisionFrom(revision: ID!): FbFormResponse + + # Update revision + updateRevision(revision: ID!, data: FbUpdateFormInput!): FbFormResponse + + # Delete form and all of its revisions + deleteForm(id: ID!): FbDeleteResponse + + # Delete a single revision + deleteRevision(revision: ID!): FbDeleteResponse + + # Publish revision + publishRevision(revision: ID!): FbFormResponse + + # Unpublish revision + unpublishRevision(revision: ID!): FbFormResponse + + # Logs a view of a form + saveFormView(revision: ID!): FbSaveFormViewResponse + } + `; +}; diff --git a/packages/api-form-builder/src/plugins/graphql/form.ts b/packages/api-form-builder/src/plugins/graphql/form.ts deleted file mode 100644 index 786db8ac2df..00000000000 --- a/packages/api-form-builder/src/plugins/graphql/form.ts +++ /dev/null @@ -1,423 +0,0 @@ -import { - ErrorResponse, - ListResponse, - NotFoundResponse, - Response -} from "@webiny/handler-graphql/responses"; -import { FormBuilderContext } from "~/types"; -import { GraphQLSchemaPlugin } from "@webiny/handler-graphql"; - -export const createFormSchema = () => { - const formSchema = new GraphQLSchemaPlugin({ - typeDefs: /* GraphQL */ ` - enum FbFormStatusEnum { - published - draft - locked - } - - type FbFormUser { - id: String - displayName: String - type: String - } - - type FbForm { - id: ID! - formId: ID! - createdBy: FbFormUser! - ownedBy: FbFormUser! - createdOn: DateTime! - savedOn: DateTime! - publishedOn: DateTime - version: Int! - name: String! - slug: String! - fields: [FbFormFieldType!]! - steps: [FbFormStepType!]! - settings: FbFormSettingsType! - triggers: JSON - published: Boolean! - locked: Boolean! - status: FbFormStatusEnum! - stats: FbFormStatsType! - overallStats: FbFormStatsType! - } - - type FbFieldOptionsType { - label: String - value: String - } - - input FbFormStepInput { - title: String - layout: [[String]] - } - - input FbFieldOptionsInput { - label: String - value: String - } - - input FbFieldValidationInput { - name: String! - message: String - settings: JSON - } - - type FbFieldValidationType { - name: String! - message: String - settings: JSON - } - - type FbFormStepType { - title: String - layout: [[String]] - } - - type FbFormFieldType { - _id: ID! - fieldId: String! - type: String! - name: String! - label: String - placeholderText: String - helpText: String - options: [FbFieldOptionsType] - validation: [FbFieldValidationType] - settings: JSON - } - - input FbFormFieldInput { - _id: ID! - fieldId: String! - type: String! - name: String! - label: String - placeholderText: String - helpText: String - options: [FbFieldOptionsInput] - validation: [FbFieldValidationInput] - settings: JSON - } - - type FbFormSettingsLayoutType { - renderer: String - } - - type FbTermsOfServiceMessage { - enabled: Boolean - message: JSON - errorMessage: String - } - - type FbFormReCaptchaSettings { - enabled: Boolean - siteKey: String - secretKey: String - } - - type FbReCaptcha { - enabled: Boolean - errorMessage: JSON - settings: FbFormReCaptchaSettings - } - - type FbFormSettingsType { - layout: FbFormSettingsLayoutType - submitButtonLabel: String - fullWidthSubmitButton: Boolean - successMessage: JSON - termsOfServiceMessage: FbTermsOfServiceMessage - reCaptcha: FbReCaptcha - } - - type FbFormStatsType { - views: Int - submissions: Int - conversionRate: Float - } - - input FbFormReCaptchaSettingsInput { - enabled: Boolean - siteKey: String - secretKey: String - } - - input FbReCaptchaInput { - enabled: Boolean - errorMessage: JSON - settings: FbFormReCaptchaSettingsInput - } - - input FbTermsOfServiceMessageInput { - enabled: Boolean - message: JSON - errorMessage: String - } - - input FbFormSettingsLayoutInput { - renderer: String - } - - input FbFormSettingsInput { - layout: FbFormSettingsLayoutInput - submitButtonLabel: String - fullWidthSubmitButton: Boolean - successMessage: JSON - termsOfServiceMessage: FbTermsOfServiceMessageInput - reCaptcha: FbReCaptchaInput - } - - input FbUpdateFormInput { - name: String - fields: [FbFormFieldInput] - steps: [FbFormStepInput] - settings: FbFormSettingsInput - triggers: JSON - } - - input FbFormSortInput { - name: Int - publishedOn: Int - } - - input FbCreateFormInput { - name: String! - } - - type FbFormResponse { - data: FbForm - error: FbError - } - - type FbFormListResponse { - data: [FbForm] - error: FbError - } - - type FbSaveFormViewResponse { - error: FbError - } - - type FbFormRevisionsResponse { - data: [FbForm] - error: FbError - } - - extend type FbQuery { - # Get form (can be published or not, requires authorization ) - getForm(revision: ID!): FbFormResponse - - # Get form revisions - getFormRevisions(id: ID!): FbFormRevisionsResponse - - # Get published form by exact revision ID, or parent form ID (public access) - getPublishedForm(revision: ID, parent: ID): FbFormResponse - - # List forms (returns a list of latest revision) - listForms: FbFormListResponse - } - - extend type FbMutation { - createForm(data: FbCreateFormInput!): FbFormResponse - - # Create a new revision from an existing revision - createRevisionFrom(revision: ID!): FbFormResponse - - # Update revision - updateRevision(revision: ID!, data: FbUpdateFormInput!): FbFormResponse - - # Publish revision - publishRevision(revision: ID!): FbFormResponse - - # Unpublish revision - unpublishRevision(revision: ID!): FbFormResponse - - # Delete form and all of its revisions - deleteForm(id: ID!): FbDeleteResponse - - # Delete a single revision - deleteRevision(revision: ID!): FbDeleteResponse - - # Logs a view of a form - saveFormView(revision: ID!): FbSaveFormViewResponse - } - `, - resolvers: { - FbForm: { - overallStats: async (form, _, { formBuilder }) => { - try { - return await formBuilder.getFormStats(form.id); - } catch (ex) { - console.log(`Could not fetch form "${form.id}" stats.`); - console.log(ex.message); - } - return { - views: 0, - submissions: 0, - conversionRate: 0 - }; - }, - settings: async (form, _, { formBuilder }) => { - const settings = await formBuilder.getSettings({ auth: false }); - - return { - ...form.settings, - reCaptcha: { - ...form.settings.reCaptcha, - settings: settings ? settings.reCaptcha : null - } - }; - } - }, - FbQuery: { - getForm: async (_, args: any, { formBuilder }) => { - try { - const form = await formBuilder.getForm(args.revision); - - return new Response(form); - } catch (e) { - return new ErrorResponse(e); - } - }, - getFormRevisions: async (_, args: any, { formBuilder }) => { - try { - const revisions = await formBuilder.getFormRevisions(args.id); - - return new Response(revisions); - } catch (e) { - return new ErrorResponse(e); - } - }, - listForms: async (_, __, { formBuilder }) => { - try { - const [data, meta] = await formBuilder.listForms(); - - return new ListResponse(data, meta); - } catch (e) { - return new ErrorResponse(e); - } - }, - getPublishedForm: async (_, args: any, { formBuilder }) => { - if (!args.revision && !args.parent) { - return new NotFoundResponse("Revision ID or Form ID missing."); - } - - let form; - - if (args.revision) { - /** - * This fetches the exact revision specified by revision ID - */ - form = await formBuilder.getPublishedFormRevisionById(args.revision); - } else if (args.parent) { - /** - * This fetches the latest published revision for given parent form - */ - form = await formBuilder.getLatestPublishedFormRevision(args.parent); - } - - if (!form) { - return new NotFoundResponse("The requested form was not found."); - } - - return new Response(form); - } - }, - FbMutation: { - /** - * Creates a new form - */ - createForm: async (_, args: any, { formBuilder }) => { - try { - const form = await formBuilder.createForm(args.data); - - return new Response(form); - } catch (e) { - return new ErrorResponse(e); - } - }, - /** - * Deletes the entire form with all of its revisions - */ - deleteForm: async (_, args: any, { formBuilder }) => { - try { - await formBuilder.deleteForm(args.id); - - return new Response(true); - } catch (e) { - return new ErrorResponse(e); - } - }, - /** - * Creates a revision from the given revision - */ - createRevisionFrom: async (_, args: any, { formBuilder }) => { - try { - const form = await formBuilder.createFormRevision(args.revision); - return new Response(form); - } catch (e) { - return new ErrorResponse(e); - } - }, - /** - * Updates revision - */ - updateRevision: async (_, args: any, { formBuilder }) => { - try { - const form = await formBuilder.updateForm(args.revision, args.data); - return new Response(form); - } catch (e) { - return new ErrorResponse(e); - } - }, - /** - * Publish revision (must be given an exact revision ID to publish) - */ - publishRevision: async (_, { revision }, { formBuilder }) => { - try { - const form = await formBuilder.publishForm(revision); - - return new Response(form); - } catch (e) { - return new ErrorResponse(e); - } - }, - unpublishRevision: async (_, args: any, { formBuilder }) => { - try { - const form = await formBuilder.unpublishForm(args.revision); - - return new Response(form); - } catch (e) { - return new ErrorResponse(e); - } - }, - /** - * Delete a revision - */ - deleteRevision: async (_, args: any, { formBuilder }) => { - try { - await formBuilder.deleteFormRevision(args.revision); - - return new Response(true); - } catch (e) { - return new ErrorResponse(e); - } - }, - saveFormView: async (_, args: any, { formBuilder }) => { - try { - const form = await formBuilder.incrementFormViews(args.revision); - - return new Response(form); - } catch (e) { - return new ErrorResponse(e); - } - } - } - } - }); - - return formSchema; -}; diff --git a/packages/api-form-builder/src/plugins/graphql/formsSchema.ts b/packages/api-form-builder/src/plugins/graphql/formsSchema.ts new file mode 100644 index 00000000000..cfca433480a --- /dev/null +++ b/packages/api-form-builder/src/plugins/graphql/formsSchema.ts @@ -0,0 +1,194 @@ +import { + ErrorResponse, + GraphQLSchemaPlugin, + ListResponse, + NotFoundResponse, + Response +} from "@webiny/handler-graphql"; + +import { + createFormsTypeDefs, + CreateFormsTypeDefsParams +} from "~/plugins/graphql/createFormsTypeDefs"; +import { FormBuilderContext } from "~/types"; + +export const createFormsSchema = (params: CreateFormsTypeDefsParams) => { + const formsGraphQL = new GraphQLSchemaPlugin({ + typeDefs: createFormsTypeDefs(params), + resolvers: { + FbForm: { + overallStats: async (form, _, { formBuilder }) => { + try { + return await formBuilder.getFormStats(form.id); + } catch (ex) { + console.log(`Could not fetch form "${form.id}" stats.`); + console.log(ex.message); + } + return { + views: 0, + submissions: 0, + conversionRate: 0 + }; + }, + settings: async (form, _, { formBuilder }) => { + const settings = await formBuilder.getSettings({ auth: false }); + + return { + ...form.settings, + reCaptcha: { + ...form.settings.reCaptcha, + settings: settings ? settings.reCaptcha : null + } + }; + } + }, + FbQuery: { + getForm: async (_, args: any, { formBuilder }) => { + try { + const form = await formBuilder.getForm(args.revision); + + return new Response(form); + } catch (e) { + return new ErrorResponse(e); + } + }, + getFormRevisions: async (_, args: any, { formBuilder }) => { + try { + const revisions = await formBuilder.getFormRevisions(args.id); + + return new Response(revisions); + } catch (e) { + return new ErrorResponse(e); + } + }, + listForms: async (_, __, { formBuilder }) => { + try { + const [data, meta] = await formBuilder.listForms(); + + return new ListResponse(data, meta); + } catch (e) { + return new ErrorResponse(e); + } + }, + getPublishedForm: async (_, args: any, { formBuilder }) => { + if (!args.revision && !args.parent) { + return new NotFoundResponse("Revision ID or Form ID missing."); + } + + let form; + + if (args.revision) { + /** + * This fetches the exact revision specified by revision ID + */ + form = await formBuilder.getPublishedFormRevisionById(args.revision); + } else if (args.parent) { + /** + * This fetches the latest published revision for given parent form + */ + form = await formBuilder.getLatestPublishedFormRevision(args.parent); + } + + if (!form) { + return new NotFoundResponse("The requested form was not found."); + } + + return new Response(form); + } + }, + FbMutation: { + /** + * Creates a new form + */ + createForm: async (_, args: any, { formBuilder }) => { + try { + const form = await formBuilder.createForm(args.data); + + return new Response(form); + } catch (e) { + return new ErrorResponse(e); + } + }, + /** + * Deletes the entire form with all of its revisions + */ + deleteForm: async (_, args: any, { formBuilder }) => { + try { + await formBuilder.deleteForm(args.id); + + return new Response(true); + } catch (e) { + return new ErrorResponse(e); + } + }, + /** + * Creates a revision from the given revision + */ + createRevisionFrom: async (_, args: any, { formBuilder }) => { + try { + const form = await formBuilder.createFormRevision(args.revision); + return new Response(form); + } catch (e) { + return new ErrorResponse(e); + } + }, + /** + * Updates revision + */ + updateRevision: async (_, args: any, { formBuilder }) => { + try { + const form = await formBuilder.updateForm(args.revision, args.data); + return new Response(form); + } catch (e) { + return new ErrorResponse(e); + } + }, + /** + * Publish revision (must be given an exact revision ID to publish) + */ + publishRevision: async (_, { revision }, { formBuilder }) => { + try { + const form = await formBuilder.publishForm(revision); + + return new Response(form); + } catch (e) { + return new ErrorResponse(e); + } + }, + unpublishRevision: async (_, args: any, { formBuilder }) => { + try { + const form = await formBuilder.unpublishForm(args.revision); + + return new Response(form); + } catch (e) { + return new ErrorResponse(e); + } + }, + /** + * Delete a revision + */ + deleteRevision: async (_, args: any, { formBuilder }) => { + try { + await formBuilder.deleteFormRevision(args.revision); + + return new Response(true); + } catch (e) { + return new ErrorResponse(e); + } + }, + saveFormView: async (_, args: any, { formBuilder }) => { + try { + const form = await formBuilder.incrementFormViews(args.revision); + + return new Response(form); + } catch (e) { + return new ErrorResponse(e); + } + } + } + } + }); + formsGraphQL.name = "fb.graphql.forms"; + + return formsGraphQL; +}; diff --git a/packages/api-form-builder/src/types.ts b/packages/api-form-builder/src/types.ts index 2a060b93e63..d69c7e5f8dc 100644 --- a/packages/api-form-builder/src/types.ts +++ b/packages/api-form-builder/src/types.ts @@ -647,6 +647,7 @@ export interface FormBuilderStorageOperationsDeleteFormRevisionParams { export interface FormBuilderStorageOperationsPublishFormParams { original: FbForm; form: FbForm; + input: Record; } /** @@ -656,6 +657,7 @@ export interface FormBuilderStorageOperationsPublishFormParams { export interface FormBuilderStorageOperationsUnpublishFormParams { original: FbForm; form: FbForm; + input: Record; } /** From 9c04f9b47a494307534a8a835068532533cac374 Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Tue, 7 Nov 2023 13:54:05 +0000 Subject: [PATCH 09/37] fix: imports/exports, refactor cms storages --- .../pageBuilder/export/combine/src/index.ts | 22 ++- .../pageBuilder/export/process/src/index.ts | 4 +- .../pageBuilder/import/create/src/index.ts | 22 ++- .../pageBuilder/import/process/src/index.ts | 4 +- ...rmBuilderStorage.ts => CmsFormsStorage.ts} | 180 +++++++++++------- .../CmsSubmissionsStorage.ts | 31 +-- .../FormBuilderContextSetup.ts | 97 +++++----- .../createFormBuilderContext.ts | 8 +- .../models/form.model.ts | 8 +- packages/api-form-builder/src/index.ts | 2 - .../src/plugins/crud/forms.crud.ts | 66 ++----- .../src/plugins/crud/index.ts | 49 +++-- .../src/plugins/crud/submissions.crud.ts | 12 +- .../src/plugins/graphql/formsSchema.ts | 2 +- packages/api-form-builder/src/types.ts | 31 +-- .../ddb/apps/api/graphql/src/index.ts | 8 +- .../pageBuilder/export/combine/src/index.ts | 22 ++- .../pageBuilder/export/process/src/index.ts | 4 +- .../pageBuilder/import/create/src/index.ts | 22 ++- .../pageBuilder/import/process/src/index.ts | 4 +- .../pageBuilder/export/combine/src/index.ts | 22 ++- .../pageBuilder/export/process/src/index.ts | 4 +- .../pageBuilder/import/create/src/index.ts | 22 ++- .../pageBuilder/import/process/src/index.ts | 4 +- 24 files changed, 398 insertions(+), 252 deletions(-) rename packages/api-form-builder/src/cmsFormBuilderStorage/{CmsFormBuilderStorage.ts => CmsFormsStorage.ts} (57%) diff --git a/apps/api/pageBuilder/export/combine/src/index.ts b/apps/api/pageBuilder/export/combine/src/index.ts index b5b72356424..ce54d91e674 100644 --- a/apps/api/pageBuilder/export/combine/src/index.ts +++ b/apps/api/pageBuilder/export/combine/src/index.ts @@ -3,7 +3,12 @@ import { createHandler } from "@webiny/handler-aws/raw"; import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import i18nContentPlugins from "@webiny/api-i18n-content/plugins"; -import { createFormBuilder } from "@webiny/api-form-builder"; +import { CmsParametersPlugin, createHeadlessCmsContext } from "@webiny/api-headless-cms"; +import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb"; +import { + createFormBuilderContext, + createFormBuilderGraphQL +} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createPageBuilderGraphQL, @@ -40,17 +45,30 @@ export const handler = createHandler({ i18nPlugins(), i18nDynamoDbStorageOperations(), i18nContentPlugins(), + new CmsParametersPlugin(async context => { + const locale = context.i18n.getCurrentLocale("content")?.code || "en-US"; + return { + type: "manage", + locale + }; + }), + createHeadlessCmsContext({ + storageOperations: createHeadlessCmsStorageOperations({ + documentClient + }) + }), createPageBuilderContext({ storageOperations: createPageBuilderStorageOperations({ documentClient }) }), createPageBuilderGraphQL(), - createFormBuilder({ + createFormBuilderContext({ storageOperations: createFormBuilderStorageOperations({ documentClient }) }), + createFormBuilderGraphQL(), pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), diff --git a/apps/api/pageBuilder/export/process/src/index.ts b/apps/api/pageBuilder/export/process/src/index.ts index 463881ed03a..3e253c466a1 100644 --- a/apps/api/pageBuilder/export/process/src/index.ts +++ b/apps/api/pageBuilder/export/process/src/index.ts @@ -4,7 +4,7 @@ import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import i18nContentPlugins from "@webiny/api-i18n-content/plugins"; import { createPageBuilderContext } from "@webiny/api-page-builder/graphql"; -import { createFormBuilder } from "@webiny/api-form-builder"; +import { createFormBuilderContext } from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createStorageOperations as createPageBuilderStorageOperations } from "@webiny/api-page-builder-so-ddb"; import pageBuilderImportExportPlugins from "@webiny/api-page-builder-import-export/graphql"; @@ -61,7 +61,7 @@ export const handler = createHandler({ documentClient }) }), - createFormBuilder({ + createFormBuilderContext({ storageOperations: createFormBuilderStorageOperations({ documentClient }) diff --git a/apps/api/pageBuilder/import/create/src/index.ts b/apps/api/pageBuilder/import/create/src/index.ts index 5f35af4467b..c1050b61a54 100644 --- a/apps/api/pageBuilder/import/create/src/index.ts +++ b/apps/api/pageBuilder/import/create/src/index.ts @@ -3,7 +3,12 @@ import { createHandler } from "@webiny/handler-aws/raw"; import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import i18nContentPlugins from "@webiny/api-i18n-content/plugins"; -import { createFormBuilder } from "@webiny/api-form-builder"; +import { CmsParametersPlugin, createHeadlessCmsContext } from "@webiny/api-headless-cms"; +import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb"; +import { + createFormBuilderContext, + createFormBuilderGraphQL +} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createPageBuilderGraphQL, @@ -38,17 +43,30 @@ export const handler = createHandler({ i18nPlugins(), i18nDynamoDbStorageOperations(), i18nContentPlugins(), + new CmsParametersPlugin(async context => { + const locale = context.i18n.getCurrentLocale("content")?.code || "en-US"; + return { + type: "manage", + locale + }; + }), + createHeadlessCmsContext({ + storageOperations: createHeadlessCmsStorageOperations({ + documentClient + }) + }), createPageBuilderContext({ storageOperations: createPageBuilderStorageOperations({ documentClient }) }), createPageBuilderGraphQL(), - createFormBuilder({ + createFormBuilderContext({ storageOperations: createFormBuilderStorageOperations({ documentClient }) }), + createFormBuilderGraphQL(), pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), diff --git a/apps/api/pageBuilder/import/process/src/index.ts b/apps/api/pageBuilder/import/process/src/index.ts index 0e38e5fd8c8..9f72006d99a 100644 --- a/apps/api/pageBuilder/import/process/src/index.ts +++ b/apps/api/pageBuilder/import/process/src/index.ts @@ -3,7 +3,7 @@ import { createHandler } from "@webiny/handler-aws/raw"; import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import i18nContentPlugins from "@webiny/api-i18n-content/plugins"; -import { createFormBuilder } from "@webiny/api-form-builder"; +import { createFormBuilderContext } from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createPageBuilderContext } from "@webiny/api-page-builder/graphql"; import { createStorageOperations as createPageBuilderStorageOperations } from "@webiny/api-page-builder-so-ddb"; @@ -64,7 +64,7 @@ export const handler = createHandler({ documentClient }) }), - createFormBuilder({ + createFormBuilderContext({ storageOperations: createFormBuilderStorageOperations({ documentClient }) diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts similarity index 57% rename from packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts rename to packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts index 900954befbb..f6cc7873897 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormBuilderStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts @@ -1,5 +1,8 @@ import { CmsEntry, CmsModel, HeadlessCms } from "@webiny/api-headless-cms/types"; +import WebinyError from "@webiny/error"; import { Security } from "@webiny/api-security/types"; +import { createIdentifier } from "@webiny/utils"; + import { FbForm, FormBuilderStorageOperationsListFormsParams, @@ -7,7 +10,13 @@ import { FormBuilderStorageOperationsUpdateFormParams, FormBuilderStorageOperationsDeleteFormRevisionParams, FormBuilderStorageOperationsPublishFormParams, - FormBuilderStorageOperationsUnpublishFormParams + FormBuilderStorageOperationsUnpublishFormParams, + FormBuilderStorageOperationsListFormsResponse, + FormBuilderStorageOperationsCreateFormParams, + FormBuilderStorageOperationsCreateFormFromParams, + FormBuilderStorageOperationsDeleteFormParams, + FormBuilderStorageOperationsGetFormParams, + FormBuilderFormStorageOperations } from "~/types"; interface ModelContext { @@ -15,13 +24,13 @@ interface ModelContext { locale: string; } -export class CmsFormBuilderStorage { +export class CmsFormsStorage implements FormBuilderFormStorageOperations { private readonly cms: HeadlessCms; private readonly security: Security; private readonly model: CmsModel; static async create(params: { model: CmsModel; cms: HeadlessCms; security: Security }) { - return new CmsFormBuilderStorage(params.model, params.cms, params.security); + return new CmsFormsStorage(params.model, params.cms, params.security); } private constructor(model: CmsModel, cms: HeadlessCms, security: Security) { @@ -34,55 +43,65 @@ export class CmsFormBuilderStorage { return { ...this.model, tenant, locale }; } - createForm = async ({ form }: { form: FbForm }) => { - const model = this.modelWithContext(form); - - const entry = await this.security.withoutAuthorization(() => { - return this.cms.createEntry(model, { ...form }); - }); - - return this.getFormFieldValues(entry); - }; - - getForm = async ({ where }: FormBuilderStorageOperationsListFormsParams) => { - const { id, tenant, locale } = where; + async getForm(params: FormBuilderStorageOperationsGetFormParams): Promise { + const { + id, + formId: initialFormId, + version, + published, + latest, + tenant, + locale + } = params.where; const model = this.modelWithContext({ tenant, locale }); + const formId = initialFormId || id?.split("#").shift() || ""; const entry = await this.security.withoutAuthorization(async () => { - return await this.cms.getEntryById(model, id || ""); + if (latest) { + const [entries] = await this.cms.listLatestEntries(model, { + where: { entryId: formId } + }); + + return entries[0]; + } else if (published && !version) { + const entries = (await this.cms.getEntryRevisions(model, formId)) + .filter(entryItem => entryItem.values.published) + .sort((a, b) => b.version - a.version); + + return entries[0]; + } else if (id || version) { + return await this.cms.getEntryById( + model, + id || + createIdentifier({ + id: formId as string, + version: version as number + }) + ); + } else { + throw new WebinyError("Missing parameter to get form", "MISSING_WHERE_PARAMETER", { + where: params.where + }); + } }); return entry ? this.getFormFieldValues(entry) : null; - }; - - listFormRevisions = async (params: FormBuilderStorageOperationsListFormRevisionsParams) => { - const { tenant, locale, formId } = params.where; - const model = this.modelWithContext({ tenant, locale }); - - const entries = await this.security.withoutAuthorization(async () => { - return await this.cms.getEntryRevisions(model, formId); - }); - - return entries.map(entry => this.getFormFieldValues(entry)); - }; + } - listForms = async (params: FormBuilderStorageOperationsListFormsParams) => { - const { id, tenant, locale, ...restWhere } = params.where; - const model = this.modelWithContext({ tenant, locale }); + async createForm(params: FormBuilderStorageOperationsCreateFormParams): Promise { + const { form } = params; + const model = this.modelWithContext(form); - const [entries, meta] = await this.security.withoutAuthorization(async () => { - return await this.cms.listLatestEntries(model, { - after: params.after, - limit: params.limit, - sort: params.sort, - where: { entryId: id, ...restWhere } - }); + const entry = await this.security.withoutAuthorization(() => { + return this.cms.createEntry(model, { ...form }); }); - return [entries.map(entry => this.getFormFieldValues(entry)), meta]; - }; + return this.getFormFieldValues(entry); + } - createFormFrom = async (params: any) => { + async createFormFrom( + params: FormBuilderStorageOperationsCreateFormFromParams + ): Promise { const { form } = params; const model = this.modelWithContext(form); @@ -98,10 +117,22 @@ export class CmsFormBuilderStorage { }); }); - return entry ? this.getFormFieldValues(entry) : null; - }; + return this.getFormFieldValues(entry); + } - deleteForm = async ({ form }: { form: FbForm }) => { + async updateForm(params: FormBuilderStorageOperationsUpdateFormParams): Promise { + const { form, input, meta, options } = params; + const model = this.modelWithContext(form); + + const entry = await this.security.withoutAuthorization(async () => { + return await this.cms.updateEntry(model, form.id, input, meta, options); + }); + + return this.getFormFieldValues(entry); + } + + async deleteForm(params: FormBuilderStorageOperationsDeleteFormParams): Promise { + const { form } = params; const model = this.modelWithContext(form); await this.security.withoutAuthorization(async () => { @@ -109,32 +140,51 @@ export class CmsFormBuilderStorage { force: true }); }); - }; + } - deleteFormRevision = async ({ form }: FormBuilderStorageOperationsDeleteFormRevisionParams) => { + async deleteFormRevision( + params: FormBuilderStorageOperationsDeleteFormRevisionParams + ): Promise { + const { form } = params; const model = this.modelWithContext(form); await this.security.withoutAuthorization(async () => { return await this.cms.deleteEntryRevision(model, form.id); }); - }; - - updateForm = async ({ - form, - input, - meta, - options - }: FormBuilderStorageOperationsUpdateFormParams) => { - const model = this.modelWithContext(form); + } - const entry = await this.security.withoutAuthorization(async () => { - return await this.cms.updateEntry(model, form.id, input, meta, options); + async listForms( + params: FormBuilderStorageOperationsListFormsParams + ): Promise { + const { id, tenant, locale, ...restWhere } = params.where; + const model = this.modelWithContext({ tenant, locale }); + + const [entries, meta] = await this.security.withoutAuthorization(async () => { + return await this.cms.listLatestEntries(model, { + after: params.after, + limit: params.limit, + sort: params.sort, + where: { entryId: id, ...restWhere } + }); }); - return entry ? this.getFormFieldValues(entry) : null; - }; + return [entries.map(entry => this.getFormFieldValues(entry)), meta]; + } + + async listFormRevisions( + params: FormBuilderStorageOperationsListFormRevisionsParams + ): Promise { + const { tenant, locale, formId } = params.where; + const model = this.modelWithContext({ tenant, locale }); + + const entries = await this.security.withoutAuthorization(async () => { + return await this.cms.getEntryRevisions(model, formId); + }); - publishForm = async (params: FormBuilderStorageOperationsPublishFormParams) => { + return entries.map(entry => this.getFormFieldValues(entry)); + } + + async publishForm(params: FormBuilderStorageOperationsPublishFormParams): Promise { const { form, input } = params; const model = this.modelWithContext(form); @@ -142,10 +192,10 @@ export class CmsFormBuilderStorage { return await this.cms.updateEntry(model, form.id, input); }); - return entry ? this.getFormFieldValues(entry) : null; - }; + return this.getFormFieldValues(entry); + } - unpublishForm = async (params: FormBuilderStorageOperationsUnpublishFormParams) => { + async unpublishForm(params: FormBuilderStorageOperationsUnpublishFormParams): Promise { const { form, input } = params; const model = this.modelWithContext(form); @@ -153,8 +203,8 @@ export class CmsFormBuilderStorage { return await this.cms.updateEntry(model, form.id, input); }); - return entry ? this.getFormFieldValues(entry) : null; - }; + return this.getFormFieldValues(entry); + } private getFormFieldValues(entry: CmsEntry) { return { diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts index 338ca5921c6..d598bbe7df1 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts @@ -6,7 +6,8 @@ import { Security } from "@webiny/api-security/types"; import { FormBuilderStorageOperationsCreateSubmissionParams, FormBuilderStorageOperationsUpdateSubmissionParams, - FormBuilderStorageOperationsListSubmissionsParams + FormBuilderStorageOperationsListSubmissionsParams, + FbSubmission } from "~/types"; interface ModelContext { @@ -33,7 +34,7 @@ export class CmsSubmissionsStorage { return { ...this.model, tenant, locale }; } - listSubmissions = async (params: FormBuilderStorageOperationsListSubmissionsParams) => { + async listSubmissions(params: FormBuilderStorageOperationsListSubmissionsParams) { const { id_in, formId, tenant, locale } = params.where; const model = this.modelWithContext({ tenant, locale }); @@ -50,11 +51,9 @@ export class CmsSubmissionsStorage { }); return { items: entries.map(entry => this.getSubmissionValues(entry)), meta }; - }; + } - createSubmission = async ({ - submission - }: FormBuilderStorageOperationsCreateSubmissionParams) => { + async createSubmission({ submission }: FormBuilderStorageOperationsCreateSubmissionParams) { const model = this.modelWithContext(submission); const entry = await this.security.withoutAuthorization(() => { @@ -62,11 +61,9 @@ export class CmsSubmissionsStorage { }); return this.getSubmissionValues(entry); - }; + } - updateSubmission = async ({ - submission - }: FormBuilderStorageOperationsUpdateSubmissionParams) => { + async updateSubmission({ submission }: FormBuilderStorageOperationsUpdateSubmissionParams) { const model = this.modelWithContext(submission); return await this.security.withoutAuthorization(async () => { @@ -87,18 +84,26 @@ export class CmsSubmissionsStorage { return this.getSubmissionValues(updatedEntry); }); - }; + } + + async getSubmission() { + return null; + } + + async deleteSubmission() { + return null; + } private getSubmissionValues(entry: CmsEntry) { return { id: entry.entryId, - createdBy: entry.createdBy, + ownedBy: entry.createdBy, createdOn: entry.createdOn, savedOn: entry.savedOn, locale: entry.locale, tenant: entry.tenant, webinyVersion: entry.webinyVersion, ...entry.values - }; + } as FbSubmission; } } diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts index 5e98d21d351..f46787b4ef4 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts @@ -1,11 +1,12 @@ -import { createFormBuilder } from "~/index"; -import { FormBuilderContext, FbFormPermission } from "~/types"; +import { CmsModelPlugin } from "@webiny/api-headless-cms"; +import { AppPermissions } from "@webiny/api-security/utils/AppPermissions"; import WebinyError from "@webiny/error"; + +import { createFormBuilder } from "~/index"; import { createFormBuilderPlugins } from "./createFormBuilderPlugins"; -import { CmsModelPlugin } from "@webiny/api-headless-cms"; -import { CmsFormBuilderStorage } from "./CmsFormBuilderStorage"; +import { CmsFormsStorage } from "./CmsFormsStorage"; import { CmsSubmissionsStorage } from "./CmsSubmissionsStorage"; -import { AppPermissions } from "@webiny/api-security/utils/AppPermissions"; +import { FormBuilderContext, FbFormPermission, FormBuilderStorageOperations } from "~/types"; class FormsPermissions extends AppPermissions {} @@ -16,17 +17,30 @@ export class FormBuilderContextSetup { this.context = context; } - async setupContext(storageOperations: any) { - const formStorageOps = await this.context.security.withoutAuthorization(() => { - return this.setupCmsStorageOperations(); + async setupContext(storageOperations: FormBuilderStorageOperations) { + // This registers code plugins (model group, models) + const { groupPlugin, formModelDefinition, submissionModelDefinition } = + createFormBuilderPlugins(); + + // Finally, register all plugins + this.context.plugins.register([ + groupPlugin, + new CmsModelPlugin(formModelDefinition), + new CmsModelPlugin(submissionModelDefinition) + ]); + + const formsStorageOps = await this.context.security.withoutAuthorization(() => { + return this.setupFormsCmsStorageOperations(); + }); + const submissionsStorageOps = await this.context.security.withoutAuthorization(() => { + return this.setupSubmissionsCmsStorageOperations(); }); - if (formStorageOps) { - storageOperations = { - ...storageOperations, - ...formStorageOps - }; - } + storageOperations = { + ...storageOperations, + forms: formsStorageOps, + submissions: submissionsStorageOps + }; const formsPermissions = new FormsPermissions({ getIdentity: this.getIdentity.bind(this), @@ -37,57 +51,32 @@ export class FormBuilderContextSetup { return createFormBuilder({ storageOperations, formsPermissions, - getTenant: this.getTenantId.bind(this), - getLocale: this.getLocale.bind(this), context: this.context }); } - private getLocale() { - const locale = this.context.i18n.getContentLocale(); - if (!locale) { - throw new WebinyError( - "Missing locale on context.i18n locale in File Manager API.", - "LOCALE_ERROR" - ); - } - return locale; - } - private getIdentity() { return this.context.security.getIdentity(); } - private getTenantId() { - return this.context.tenancy.getCurrentTenant().id; + private async setupFormsCmsStorageOperations() { + const model = await this.getModel("fbForm"); + + return await CmsFormsStorage.create({ + model, + cms: this.context.cms, + security: this.context.security + }); } - private async setupCmsStorageOperations() { - // This registers code plugins (model group, models) - const { groupPlugin, formModelDefinition, submissionModelDefinition } = - createFormBuilderPlugins(); + private async setupSubmissionsCmsStorageOperations() { + const model = await this.getModel("fbSubmission"); - // Finally, register all plugins - this.context.plugins.register([ - groupPlugin, - new CmsModelPlugin(formModelDefinition), - new CmsModelPlugin(submissionModelDefinition) - ]); - const formModel = await this.getModel("fbForm"); - const submissionModel = await this.getModel("fbSubmission"); - - return { - ...(await CmsFormBuilderStorage.create({ - model: formModel, - cms: this.context.cms, - security: this.context.security - })), - ...(await CmsSubmissionsStorage.create({ - model: submissionModel, - cms: this.context.cms, - security: this.context.security - })) - }; + return await CmsSubmissionsStorage.create({ + model, + cms: this.context.cms, + security: this.context.security + }); } private async getModel(modelId: string) { diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderContext.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderContext.ts index b7be02c408d..0ff07e7d520 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderContext.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderContext.ts @@ -1,9 +1,13 @@ import { ContextPlugin } from "@webiny/api"; -import { FormBuilderContext } from "~/types"; +import { FormBuilderContext, FormBuilderStorageOperations } from "~/types"; import { FormBuilderContextSetup } from "./FormBuilderContextSetup"; import { createGraphQLSchemaPlugin } from "./createGraphQLSchemaPlugin"; -export const createFormBuilderContext = ({ storageOperations }: { storageOperations: any }) => { +type CreateFormBuilderContextParams = { + storageOperations: FormBuilderStorageOperations; +}; + +export const createFormBuilderContext = ({ storageOperations }: CreateFormBuilderContextParams) => { const plugin = new ContextPlugin(async context => { const fbContext = new FormBuilderContextSetup(context); await fbContext.setupContext(storageOperations); diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts index c4bf51d3e2d..051155e3ffe 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts @@ -1,6 +1,8 @@ -import { createModelField } from "../creteModelField"; +import { CmsPrivateModelFull } from "@webiny/api-headless-cms"; import { CmsModelField, CmsModelGroup } from "@webiny/api-headless-cms/types"; +import { createModelField } from "../creteModelField"; + const required = () => { return { name: "required", @@ -28,7 +30,7 @@ const nameField = () => { const publishedField = () => { return createModelField({ label: "Published", - type: "text", + type: "boolean", validation: [required()] }); }; @@ -457,7 +459,7 @@ export const FIELD_FIELDS = [ export const STEP_FIELDS = [stepTitleField(), stepLayoutField()]; -export const createFormDataModelDefinition = (group: CmsModelGroup): any => { +export const createFormDataModelDefinition = (group: CmsModelGroup): CmsPrivateModelFull => { return { name: "FbForm", modelId: "fbForm", diff --git a/packages/api-form-builder/src/index.ts b/packages/api-form-builder/src/index.ts index 18c93784914..cc821dd426c 100644 --- a/packages/api-form-builder/src/index.ts +++ b/packages/api-form-builder/src/index.ts @@ -8,8 +8,6 @@ import { FormsPermissions } from "./plugins/crud/permissions/FormsPermissions"; export interface CreateFormBuilderParams { storageOperations: FormBuilderStorageOperations; formsPermissions: FormsPermissions; - getTenant: any; - getLocale: any; context: FormBuilderContext; } diff --git a/packages/api-form-builder/src/plugins/crud/forms.crud.ts b/packages/api-form-builder/src/plugins/crud/forms.crud.ts index e055026f62c..bbf28895840 100644 --- a/packages/api-form-builder/src/plugins/crud/forms.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/forms.crud.ts @@ -114,7 +114,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { let form: FbForm | null = null; try { - form = await this.storageOperations.getForm({ + form = await this.storageOperations.forms.getForm({ where: { id, tenant: getTenant().id, @@ -192,7 +192,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { } try { - return await this.storageOperations.listForms(listFormParams); + return await this.storageOperations.forms.listForms(listFormParams); } catch (ex) { throw new WebinyError( ex.message || "Could not list all forms by given params", @@ -206,7 +206,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { }, async getFormRevisions(this: FormBuilder, id) { try { - const result = await this.storageOperations.listFormRevisions({ + const result = await this.storageOperations.forms.listFormRevisions({ where: { formId: id, tenant: getTenant().id, @@ -226,42 +226,10 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { ); } }, - async getPublishedFormRevisionById(this: FormBuilder, id) { - const [version] = id.split("#"); - if (!version) { - throw new WebinyError("There is no version in given ID value.", "VERSION_ERROR", { - id - }); - } - - let form: FbForm | null = null; - try { - form = await this.storageOperations.getForm({ - where: { - id, - published: true, - tenant: getTenant().id, - locale: getLocale().code - } - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not load published form revision by ID.", - ex.code || "GET_PUBLISHED_FORM_BY_ID_ERROR", - { - id - } - ); - } - if (!form) { - throw new NotFoundError(`Form "${id}" was not found!`); - } - return form; - }, async getLatestPublishedFormRevision(this: FormBuilder, id) { let form: FbForm | null = null; try { - form = await this.storageOperations.getForm({ + form = await this.storageOperations.forms.getForm({ where: { id, published: true, @@ -351,7 +319,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { await onFormBeforeCreate.publish({ form }); - const result = await this.storageOperations.createForm({ form, input }); + const result = await this.storageOperations.forms.createForm({ form, input }); await onFormAfterCreate.publish({ form: result }); @@ -372,7 +340,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { await updateData.validate(); const data = await updateData.toJSON({ onlyDirty: true }); - const original = await this.storageOperations.getForm({ + const original = await this.storageOperations.forms.getForm({ where: { id, tenant: getTenant().id, @@ -400,7 +368,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { form, original }); - const result = await this.storageOperations.updateForm({ + const result = await this.storageOperations.forms.updateForm({ form, input, meta: {}, @@ -426,7 +394,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { async deleteForm(this: FormBuilder, id) { await formsPermissions.ensure({ rwd: "d" }); - const form = await this.storageOperations.getForm({ + const form = await this.storageOperations.forms.getForm({ where: { id, tenant: getTenant().id, @@ -444,7 +412,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { await onFormBeforeDelete.publish({ form }); - await this.storageOperations.deleteForm({ + await this.storageOperations.forms.deleteForm({ form }); await onFormAfterDelete.publish({ @@ -472,7 +440,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { const formFormId = form.formId || form.id.split("#").pop(); - const revisions = await this.storageOperations.listFormRevisions({ + const revisions = await this.storageOperations.forms.listFormRevisions({ where: { formId: formFormId || "", tenant: form.tenant, @@ -495,7 +463,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { previous, revisions }); - await this.storageOperations.deleteFormRevision({ form }); + await this.storageOperations.forms.deleteFormRevision({ form }); await onFormRevisionAfterDelete.publish({ form, previous, @@ -538,7 +506,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { await onFormBeforePublish.publish({ form }); - const result = await this.storageOperations.publishForm({ + const result = await this.storageOperations.forms.publishForm({ original, form, input: form @@ -580,7 +548,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { await onFormBeforeUnpublish.publish({ form }); - const result = await this.storageOperations.unpublishForm({ + const result = await this.storageOperations.forms.unpublishForm({ original, form, input: form @@ -608,7 +576,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { auth: false }); const originalFormFormId = original.formId || (original.id.split("#").pop() as string); - const latest = await this.storageOperations.getForm({ + const latest = await this.storageOperations.forms.getForm({ where: { id, latest: true, @@ -663,7 +631,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { latest, form }); - const result = await this.storageOperations.createFormFrom({ + const result = await this.storageOperations.forms.createFormFrom({ form: latest }); await onFormRevisionAfterCreate.publish({ @@ -700,7 +668,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { }; try { - await this.storageOperations.updateForm({ + await this.storageOperations.forms.updateForm({ form, input: form }); @@ -733,7 +701,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { }; try { - await this.storageOperations.updateForm({ + await this.storageOperations.forms.updateForm({ form, input: form }); diff --git a/packages/api-form-builder/src/plugins/crud/index.ts b/packages/api-form-builder/src/plugins/crud/index.ts index 55ddb3c8de5..12e710059d9 100644 --- a/packages/api-form-builder/src/plugins/crud/index.ts +++ b/packages/api-form-builder/src/plugins/crud/index.ts @@ -1,4 +1,4 @@ -import { FormBuilderStorageOperations } from "~/types"; +import { FormBuilderStorageOperations, FormBuilderContext } from "~/types"; import { createSystemCrud } from "~/plugins/crud/system.crud"; import { createSettingsCrud } from "~/plugins/crud/settings.crud"; import { createFormsCrud } from "~/plugins/crud/forms.crud"; @@ -9,9 +9,10 @@ import { SettingsPermissions } from "~/plugins/crud/permissions/SettingsPermissi export interface CreateFormBuilderCrudParams { storageOperations: FormBuilderStorageOperations; + context: FormBuilderContext; } -export const setupFormBuilderContext = async (params: any) => { +export const setupFormBuilderContext = async (params: CreateFormBuilderCrudParams) => { const { storageOperations, context } = params; const getLocale = () => { @@ -33,18 +34,26 @@ export const setupFormBuilderContext = async (params: any) => { return context.tenancy.getCurrentTenant(); }; - if (storageOperations.beforeInit) { - try { + try { + if (storageOperations.beforeInit) { await storageOperations.beforeInit(context); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not run before init in Form Builder storage operations.", - ex.code || "STORAGE_OPERATIONS_BEFORE_INIT_ERROR", - { - ...ex - } - ); } + + if (storageOperations.forms.beforeInit) { + await storageOperations.forms.beforeInit(context); + } + + if (storageOperations.submissions.beforeInit) { + await storageOperations.submissions.beforeInit(context); + } + } catch (ex) { + throw new WebinyError( + ex.message || "Could not run before init in Form Builder storage operations.", + ex.code || "STORAGE_OPERATIONS_BEFORE_INIT_ERROR", + { + ...ex + } + ); } const basePermissionsArgs = { @@ -88,12 +97,18 @@ export const setupFormBuilderContext = async (params: any) => { }) }; - if (!storageOperations.init) { - return; - } - try { - await storageOperations.init(context); + if (storageOperations.init) { + await storageOperations.init(context); + } + + if (storageOperations.forms.init) { + await storageOperations.forms.init(context); + } + + if (storageOperations.submissions.init) { + await storageOperations.submissions.init(context); + } } catch (ex) { throw new WebinyError( ex.message || "Could not run init in Form Builder storage operations.", diff --git a/packages/api-form-builder/src/plugins/crud/submissions.crud.ts b/packages/api-form-builder/src/plugins/crud/submissions.crud.ts index c2f9e0bb8b4..5d068794633 100644 --- a/packages/api-form-builder/src/plugins/crud/submissions.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/submissions.crud.ts @@ -99,7 +99,7 @@ export const createSubmissionsCrud = (params: CreateSubmissionsCrudParams): Subm }; try { - const { items } = await this.storageOperations.listSubmissions( + const { items } = await this.storageOperations.submissions.listSubmissions( listSubmissionsParams ); @@ -156,7 +156,9 @@ export const createSubmissionsCrud = (params: CreateSubmissionsCrudParams): Subm }; try { - const result = await this.storageOperations.listSubmissions(listSubmissionsParams); + const result = await this.storageOperations.submissions.listSubmissions( + listSubmissionsParams + ); return [result.items, result.meta]; } catch (ex) { @@ -308,7 +310,7 @@ export const createSubmissionsCrud = (params: CreateSubmissionsCrudParams): Subm form, submission }); - await this.storageOperations.createSubmission({ + await this.storageOperations.submissions.createSubmission({ input: modelData, form, submission @@ -411,7 +413,7 @@ export const createSubmissionsCrud = (params: CreateSubmissionsCrudParams): Subm original, submission }); - await this.storageOperations.updateSubmission({ + await this.storageOperations.submissions.updateSubmission({ input: updatedData, form, original, @@ -448,7 +450,7 @@ export const createSubmissionsCrud = (params: CreateSubmissionsCrudParams): Subm form, submission }); - await this.storageOperations.deleteSubmission({ + await this.storageOperations.submissions.deleteSubmission({ form, submission }); diff --git a/packages/api-form-builder/src/plugins/graphql/formsSchema.ts b/packages/api-form-builder/src/plugins/graphql/formsSchema.ts index cfca433480a..6bf9e6007ff 100644 --- a/packages/api-form-builder/src/plugins/graphql/formsSchema.ts +++ b/packages/api-form-builder/src/plugins/graphql/formsSchema.ts @@ -81,7 +81,7 @@ export const createFormsSchema = (params: CreateFormsTypeDefsParams) => { /** * This fetches the exact revision specified by revision ID */ - form = await formBuilder.getPublishedFormRevisionById(args.revision); + form = await formBuilder.getForm(args.revision, { auth: false }); } else if (args.parent) { /** * This fetches the latest published revision for given parent form diff --git a/packages/api-form-builder/src/types.ts b/packages/api-form-builder/src/types.ts index d69c7e5f8dc..37c833dbbf3 100644 --- a/packages/api-form-builder/src/types.ts +++ b/packages/api-form-builder/src/types.ts @@ -233,7 +233,6 @@ export interface FormsCRUD { incrementFormViews(id: string): Promise; incrementFormSubmissions(id: string): Promise; getFormRevisions(id: string): Promise; - getPublishedFormRevisionById(revisionId: string): Promise; getLatestPublishedFormRevision(formId: string): Promise; deleteFormRevision(id: string): Promise; /** @@ -742,27 +741,27 @@ export interface FormBuilderSettingsStorageOperations { */ export interface FormBuilderFormStorageOperations { getForm(params: FormBuilderStorageOperationsGetFormParams): Promise; - listForms( - params: FormBuilderStorageOperationsListFormsParams - ): Promise; - listFormRevisions( - params: FormBuilderStorageOperationsListFormRevisionsParams - ): Promise; createForm(params: FormBuilderStorageOperationsCreateFormParams): Promise; createFormFrom(params: FormBuilderStorageOperationsCreateFormFromParams): Promise; updateForm(params: FormBuilderStorageOperationsUpdateFormParams): Promise; /** * Delete all form revisions + latest + published. */ - deleteForm(params: FormBuilderStorageOperationsDeleteFormParams): Promise; + deleteForm(params: FormBuilderStorageOperationsDeleteFormParams): Promise; /** * Delete the single form revision. */ - deleteFormRevision( - params: FormBuilderStorageOperationsDeleteFormRevisionParams - ): Promise; + deleteFormRevision(params: FormBuilderStorageOperationsDeleteFormRevisionParams): Promise; + listForms( + params: FormBuilderStorageOperationsListFormsParams + ): Promise; + listFormRevisions( + params: FormBuilderStorageOperationsListFormRevisionsParams + ): Promise; publishForm(params: FormBuilderStorageOperationsPublishFormParams): Promise; unpublishForm(params: FormBuilderStorageOperationsUnpublishFormParams): Promise; + beforeInit?: (context: FormBuilderContext) => Promise; + init?: (context: FormBuilderContext) => Promise; } /** @@ -791,16 +790,18 @@ export interface FormBuilderSubmissionStorageOperations { ): Promise; deleteSubmission( params: FormBuilderStorageOperationsDeleteSubmissionParams - ): Promise; + ): Promise; + beforeInit?: (context: FormBuilderContext) => Promise; + init?: (context: FormBuilderContext) => Promise; } /** * @category StorageOperations */ export interface FormBuilderStorageOperations extends FormBuilderSystemStorageOperations, - FormBuilderSettingsStorageOperations, - FormBuilderFormStorageOperations, - FormBuilderSubmissionStorageOperations { + FormBuilderSettingsStorageOperations { + forms: FormBuilderFormStorageOperations; + submissions: FormBuilderSubmissionStorageOperations; beforeInit?: (context: FormBuilderContext) => Promise; init?: (context: FormBuilderContext) => Promise; } diff --git a/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/index.ts b/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/index.ts index ebbb2fb0adc..9ffcc06ac81 100644 --- a/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/index.ts +++ b/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/index.ts @@ -20,7 +20,10 @@ import { createFileManagerContext, createFileManagerGraphQL } from "@webiny/api- import { createFileManagerStorageOperations } from "@webiny/api-file-manager-ddb"; import logsPlugins from "@webiny/handler-logs"; import fileManagerS3 from "@webiny/api-file-manager-s3"; -import { createFormBuilder } from "@webiny/api-form-builder"; +import { + createFormBuilderContext, + createFormBuilderGraphQL +} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createHeadlessCmsContext, createHeadlessCmsGraphQL } from "@webiny/api-headless-cms"; import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb"; @@ -86,11 +89,12 @@ export const handler = createHandler({ pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), - createFormBuilder({ + createFormBuilderContext({ storageOperations: createFormBuilderStorageOperations({ documentClient }) }), + createFormBuilderGraphQL(), createApwGraphQL(), createApwPageBuilderContext({ storageOperations: createApwSaStorageOperations({ documentClient }) diff --git a/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/export/combine/src/index.ts b/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/export/combine/src/index.ts index 92f17764c0d..d9268e54b68 100644 --- a/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/export/combine/src/index.ts +++ b/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/export/combine/src/index.ts @@ -2,7 +2,12 @@ import { DocumentClient } from "aws-sdk/clients/dynamodb"; import { createHandler } from "@webiny/handler-aws/raw"; import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; -import { createFormBuilder } from "@webiny/api-form-builder"; +import { CmsParametersPlugin, createHeadlessCmsContext } from "@webiny/api-headless-cms"; +import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb-es"; +import { + createFormBuilderContext, + createFormBuilderGraphQL +} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb-es"; import { createPageBuilderGraphQL, @@ -46,6 +51,18 @@ export const handler = createHandler({ securityPlugins({ documentClient }), i18nPlugins(), i18nDynamoDbStorageOperations(), + new CmsParametersPlugin(async context => { + const locale = context.i18n.getCurrentLocale("content")?.code || "en-US"; + return { + type: "manage", + locale + }; + }), + createHeadlessCmsContext({ + storageOperations: createHeadlessCmsStorageOperations({ + documentClient + }) + }), createPageBuilderContext({ storageOperations: createPageBuilderStorageOperations({ documentClient, @@ -53,12 +70,13 @@ export const handler = createHandler({ }) }), createPageBuilderGraphQL(), - createFormBuilder({ + createFormBuilderContext({ storageOperations: createFormBuilderStorageOperations({ documentClient, elasticsearch: elasticsearchClient }) }), + createFormBuilderGraphQL(), pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), diff --git a/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/export/process/src/index.ts b/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/export/process/src/index.ts index 4c762119d85..cc136a7b862 100644 --- a/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/export/process/src/index.ts +++ b/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/export/process/src/index.ts @@ -2,7 +2,7 @@ import { DocumentClient } from "aws-sdk/clients/dynamodb"; import { createHandler } from "@webiny/handler-aws/raw"; import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; -import { createFormBuilder } from "@webiny/api-form-builder"; +import { createFormBuilderContext } from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb-es"; import { createPageBuilderContext } from "@webiny/api-page-builder/graphql"; import { createStorageOperations as createPageBuilderStorageOperations } from "@webiny/api-page-builder-so-ddb-es"; @@ -73,7 +73,7 @@ export const handler = createHandler({ elasticsearch: elasticsearchClient }) }), - createFormBuilder({ + createFormBuilderContext({ storageOperations: createFormBuilderStorageOperations({ documentClient, elasticsearch: elasticsearchClient diff --git a/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/import/create/src/index.ts b/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/import/create/src/index.ts index 4e11910e7e7..2d8ef3fba78 100644 --- a/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/import/create/src/index.ts +++ b/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/import/create/src/index.ts @@ -2,7 +2,12 @@ import { DocumentClient } from "aws-sdk/clients/dynamodb"; import { createHandler } from "@webiny/handler-aws/raw"; import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; -import { createFormBuilder } from "@webiny/api-form-builder"; +import { CmsParametersPlugin, createHeadlessCmsContext } from "@webiny/api-headless-cms"; +import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb-es"; +import { + createFormBuilderContext, + createFormBuilderGraphQL +} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb-es"; import { createPageBuilderGraphQL, @@ -48,6 +53,18 @@ export const handler = createHandler({ securityPlugins({ documentClient }), i18nPlugins(), i18nDynamoDbStorageOperations(), + new CmsParametersPlugin(async context => { + const locale = context.i18n.getCurrentLocale("content")?.code || "en-US"; + return { + type: "manage", + locale + }; + }), + createHeadlessCmsContext({ + storageOperations: createHeadlessCmsStorageOperations({ + documentClient + }) + }), createPageBuilderContext({ storageOperations: createPageBuilderStorageOperations({ documentClient, @@ -55,12 +72,13 @@ export const handler = createHandler({ }) }), createPageBuilderGraphQL(), - createFormBuilder({ + createFormBuilderContext({ storageOperations: createFormBuilderStorageOperations({ documentClient, elasticsearch: elasticsearchClient }) }), + createFormBuilderGraphQL(), pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), diff --git a/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/import/process/src/index.ts b/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/import/process/src/index.ts index 6767d6f3328..b03b39f7a92 100644 --- a/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/import/process/src/index.ts +++ b/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/import/process/src/index.ts @@ -2,7 +2,7 @@ import { DocumentClient } from "aws-sdk/clients/dynamodb"; import { createHandler } from "@webiny/handler-aws/raw"; import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; -import { createFormBuilder } from "@webiny/api-form-builder"; +import { createFormBuilderContext } from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb-es"; import { createPageBuilderContext } from "@webiny/api-page-builder/graphql"; import { createStorageOperations as createPageBuilderStorageOperations } from "@webiny/api-page-builder-so-ddb-es"; @@ -77,7 +77,7 @@ export const handler = createHandler({ elasticsearch: elasticsearchClient }) }), - createFormBuilder({ + createFormBuilderContext({ storageOperations: createFormBuilderStorageOperations({ documentClient, elasticsearch: elasticsearchClient diff --git a/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/export/combine/src/index.ts b/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/export/combine/src/index.ts index b5b72356424..ce54d91e674 100644 --- a/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/export/combine/src/index.ts +++ b/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/export/combine/src/index.ts @@ -3,7 +3,12 @@ import { createHandler } from "@webiny/handler-aws/raw"; import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import i18nContentPlugins from "@webiny/api-i18n-content/plugins"; -import { createFormBuilder } from "@webiny/api-form-builder"; +import { CmsParametersPlugin, createHeadlessCmsContext } from "@webiny/api-headless-cms"; +import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb"; +import { + createFormBuilderContext, + createFormBuilderGraphQL +} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createPageBuilderGraphQL, @@ -40,17 +45,30 @@ export const handler = createHandler({ i18nPlugins(), i18nDynamoDbStorageOperations(), i18nContentPlugins(), + new CmsParametersPlugin(async context => { + const locale = context.i18n.getCurrentLocale("content")?.code || "en-US"; + return { + type: "manage", + locale + }; + }), + createHeadlessCmsContext({ + storageOperations: createHeadlessCmsStorageOperations({ + documentClient + }) + }), createPageBuilderContext({ storageOperations: createPageBuilderStorageOperations({ documentClient }) }), createPageBuilderGraphQL(), - createFormBuilder({ + createFormBuilderContext({ storageOperations: createFormBuilderStorageOperations({ documentClient }) }), + createFormBuilderGraphQL(), pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), diff --git a/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/export/process/src/index.ts b/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/export/process/src/index.ts index b6b94c393cc..f30877258e9 100644 --- a/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/export/process/src/index.ts +++ b/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/export/process/src/index.ts @@ -3,7 +3,7 @@ import { createHandler } from "@webiny/handler-aws/raw"; import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import i18nContentPlugins from "@webiny/api-i18n-content/plugins"; -import { createFormBuilder } from "@webiny/api-form-builder"; +import { createFormBuilderContext } from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createPageBuilderContext } from "@webiny/api-page-builder/graphql"; import { createStorageOperations as createPageBuilderStorageOperations } from "@webiny/api-page-builder-so-ddb"; @@ -63,7 +63,7 @@ export const handler = createHandler({ documentClient }) }), - createFormBuilder({ + createFormBuilderContext({ storageOperations: createFormBuilderStorageOperations({ documentClient }) diff --git a/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/import/create/src/index.ts b/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/import/create/src/index.ts index 5f35af4467b..c1050b61a54 100644 --- a/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/import/create/src/index.ts +++ b/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/import/create/src/index.ts @@ -3,7 +3,12 @@ import { createHandler } from "@webiny/handler-aws/raw"; import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import i18nContentPlugins from "@webiny/api-i18n-content/plugins"; -import { createFormBuilder } from "@webiny/api-form-builder"; +import { CmsParametersPlugin, createHeadlessCmsContext } from "@webiny/api-headless-cms"; +import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb"; +import { + createFormBuilderContext, + createFormBuilderGraphQL +} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createPageBuilderGraphQL, @@ -38,17 +43,30 @@ export const handler = createHandler({ i18nPlugins(), i18nDynamoDbStorageOperations(), i18nContentPlugins(), + new CmsParametersPlugin(async context => { + const locale = context.i18n.getCurrentLocale("content")?.code || "en-US"; + return { + type: "manage", + locale + }; + }), + createHeadlessCmsContext({ + storageOperations: createHeadlessCmsStorageOperations({ + documentClient + }) + }), createPageBuilderContext({ storageOperations: createPageBuilderStorageOperations({ documentClient }) }), createPageBuilderGraphQL(), - createFormBuilder({ + createFormBuilderContext({ storageOperations: createFormBuilderStorageOperations({ documentClient }) }), + createFormBuilderGraphQL(), pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), diff --git a/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/import/process/src/index.ts b/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/import/process/src/index.ts index 833c005e6e6..40c58734d21 100644 --- a/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/import/process/src/index.ts +++ b/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/import/process/src/index.ts @@ -3,7 +3,7 @@ import { createHandler } from "@webiny/handler-aws/raw"; import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import i18nContentPlugins from "@webiny/api-i18n-content/plugins"; -import { createFormBuilder } from "@webiny/api-form-builder"; +import { createFormBuilderContext } from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createPageBuilderContext } from "@webiny/api-page-builder/graphql"; import { createStorageOperations as createPageBuilderStorageOperations } from "@webiny/api-page-builder-so-ddb"; @@ -65,7 +65,7 @@ export const handler = createHandler({ documentClient }) }), - createFormBuilder({ + createFormBuilderContext({ storageOperations: createFormBuilderStorageOperations({ documentClient }) From 1d4a590affa9d06d0a3920d1cdcbfec18c4e2667 Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Tue, 7 Nov 2023 14:18:06 +0000 Subject: [PATCH 10/37] fix: fix ddb packages types --- packages/api-form-builder-so-ddb-es/src/index.ts | 4 ++-- packages/api-form-builder-so-ddb-es/src/types.ts | 4 ++-- packages/api-form-builder-so-ddb/src/index.ts | 4 ++-- packages/api-form-builder-so-ddb/src/types.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/api-form-builder-so-ddb-es/src/index.ts b/packages/api-form-builder-so-ddb-es/src/index.ts index e5c138c5603..4e6a0fb2379 100644 --- a/packages/api-form-builder-so-ddb-es/src/index.ts +++ b/packages/api-form-builder-so-ddb-es/src/index.ts @@ -179,8 +179,8 @@ export const createFormBuilderStorageOperations: FormBuilderStorageOperationsFac table, entity: entities.settings }), - ...createFormStorageOperations(), - ...createSubmissionStorageOperations({ + forms: createFormStorageOperations(), + submissions: createSubmissionStorageOperations({ elasticsearch, table, entity: entities.submission, diff --git a/packages/api-form-builder-so-ddb-es/src/types.ts b/packages/api-form-builder-so-ddb-es/src/types.ts index 56edf611121..046d954d404 100644 --- a/packages/api-form-builder-so-ddb-es/src/types.ts +++ b/packages/api-form-builder-so-ddb-es/src/types.ts @@ -88,9 +88,9 @@ export type Entities = "form" | "esForm" | "submission" | "esSubmission" | "syst export interface FormBuilderStorageOperations extends BaseFormBuilderStorageOperations, FormBuilderSettingsStorageOperations, - FormBuilderSubmissionStorageOperations, - FormBuilderFormStorageOperations, FormBuilderSystemStorageOperations { + forms: FormBuilderFormStorageOperations; + submissions: FormBuilderSubmissionStorageOperations; getTable(): Table; getEsTable(): Table; getEntities(): Record>; diff --git a/packages/api-form-builder-so-ddb/src/index.ts b/packages/api-form-builder-so-ddb/src/index.ts index 944b6d31400..9c37b4c98d3 100644 --- a/packages/api-form-builder-so-ddb/src/index.ts +++ b/packages/api-form-builder-so-ddb/src/index.ts @@ -108,8 +108,8 @@ export const createFormBuilderStorageOperations: FormBuilderStorageOperationsFac table, entity: entities.settings }), - ...createFormStorageOperations(), - ...createSubmissionStorageOperations({ + forms: createFormStorageOperations(), + submissions: createSubmissionStorageOperations({ table, entity: entities.submission, plugins diff --git a/packages/api-form-builder-so-ddb/src/types.ts b/packages/api-form-builder-so-ddb/src/types.ts index 0933281573c..d8fd1cf3f6d 100644 --- a/packages/api-form-builder-so-ddb/src/types.ts +++ b/packages/api-form-builder-so-ddb/src/types.ts @@ -89,9 +89,9 @@ export type Entities = "form" | "submission" | "system" | "settings"; export interface FormBuilderStorageOperations extends BaseFormBuilderStorageOperations, FormBuilderSettingsStorageOperations, - FormBuilderSubmissionStorageOperations, - FormBuilderFormStorageOperations, FormBuilderSystemStorageOperations { + forms: FormBuilderFormStorageOperations; + submissions: FormBuilderSubmissionStorageOperations; getTable(): Table; getEntities(): Record>; } From 96f0783b198e4c6b0c05f10d5b06767d8b7fb710 Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Tue, 7 Nov 2023 14:52:10 +0000 Subject: [PATCH 11/37] fix: fix dependencies --- apps/api/pageBuilder/export/combine/package.json | 2 ++ apps/api/pageBuilder/export/combine/tsconfig.json | 6 ++++++ apps/api/pageBuilder/import/create/package.json | 2 ++ apps/api/pageBuilder/import/create/tsconfig.json | 6 ++++++ yarn.lock | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/apps/api/pageBuilder/export/combine/package.json b/apps/api/pageBuilder/export/combine/package.json index 36f3f8852ba..5b48f11e6dc 100644 --- a/apps/api/pageBuilder/export/combine/package.json +++ b/apps/api/pageBuilder/export/combine/package.json @@ -8,6 +8,8 @@ "dependencies": { "@webiny/api-form-builder": "0.0.0", "@webiny/api-form-builder-so-ddb": "0.0.0", + "@webiny/api-headless-cms": "0.0.0", + "@webiny/api-headless-cms-ddb": "0.0.0", "@webiny/api-i18n": "0.0.0", "@webiny/api-i18n-content": "0.0.0", "@webiny/api-i18n-ddb": "0.0.0", diff --git a/apps/api/pageBuilder/export/combine/tsconfig.json b/apps/api/pageBuilder/export/combine/tsconfig.json index 053e9cc6fe1..d135212256d 100644 --- a/apps/api/pageBuilder/export/combine/tsconfig.json +++ b/apps/api/pageBuilder/export/combine/tsconfig.json @@ -4,6 +4,8 @@ "references": [ { "path": "../../../../../packages/api-form-builder/tsconfig.build.json" }, { "path": "../../../../../packages/api-form-builder-so-ddb/tsconfig.build.json" }, + { "path": "../../../../../packages/api-headless-cms/tsconfig.build.json" }, + { "path": "../../../../../packages/api-headless-cms-ddb/tsconfig.build.json" }, { "path": "../../../../../packages/api-i18n/tsconfig.build.json" }, { "path": "../../../../../packages/api-i18n-content/tsconfig.build.json" }, { "path": "../../../../../packages/api-i18n-ddb/tsconfig.build.json" }, @@ -29,6 +31,10 @@ "../../../../../packages/api-form-builder-so-ddb/src/*" ], "@webiny/api-form-builder-so-ddb": ["../../../../../packages/api-form-builder-so-ddb/src"], + "@webiny/api-headless-cms/*": ["../../../../../packages/api-headless-cms/src/*"], + "@webiny/api-headless-cms": ["../../../../../packages/api-headless-cms/src"], + "@webiny/api-headless-cms-ddb/*": ["../../../../../packages/api-headless-cms-ddb/src/*"], + "@webiny/api-headless-cms-ddb": ["../../../../../packages/api-headless-cms-ddb/src"], "@webiny/api-i18n/*": ["../../../../../packages/api-i18n/src/*"], "@webiny/api-i18n": ["../../../../../packages/api-i18n/src"], "@webiny/api-i18n-content/*": ["../../../../../packages/api-i18n-content/src/*"], diff --git a/apps/api/pageBuilder/import/create/package.json b/apps/api/pageBuilder/import/create/package.json index a166feffc14..efbfaf52038 100644 --- a/apps/api/pageBuilder/import/create/package.json +++ b/apps/api/pageBuilder/import/create/package.json @@ -8,6 +8,8 @@ "dependencies": { "@webiny/api-form-builder": "0.0.0", "@webiny/api-form-builder-so-ddb": "0.0.0", + "@webiny/api-headless-cms": "0.0.0", + "@webiny/api-headless-cms-ddb": "0.0.0", "@webiny/api-i18n": "0.0.0", "@webiny/api-i18n-content": "0.0.0", "@webiny/api-i18n-ddb": "0.0.0", diff --git a/apps/api/pageBuilder/import/create/tsconfig.json b/apps/api/pageBuilder/import/create/tsconfig.json index 053e9cc6fe1..d135212256d 100644 --- a/apps/api/pageBuilder/import/create/tsconfig.json +++ b/apps/api/pageBuilder/import/create/tsconfig.json @@ -4,6 +4,8 @@ "references": [ { "path": "../../../../../packages/api-form-builder/tsconfig.build.json" }, { "path": "../../../../../packages/api-form-builder-so-ddb/tsconfig.build.json" }, + { "path": "../../../../../packages/api-headless-cms/tsconfig.build.json" }, + { "path": "../../../../../packages/api-headless-cms-ddb/tsconfig.build.json" }, { "path": "../../../../../packages/api-i18n/tsconfig.build.json" }, { "path": "../../../../../packages/api-i18n-content/tsconfig.build.json" }, { "path": "../../../../../packages/api-i18n-ddb/tsconfig.build.json" }, @@ -29,6 +31,10 @@ "../../../../../packages/api-form-builder-so-ddb/src/*" ], "@webiny/api-form-builder-so-ddb": ["../../../../../packages/api-form-builder-so-ddb/src"], + "@webiny/api-headless-cms/*": ["../../../../../packages/api-headless-cms/src/*"], + "@webiny/api-headless-cms": ["../../../../../packages/api-headless-cms/src"], + "@webiny/api-headless-cms-ddb/*": ["../../../../../packages/api-headless-cms-ddb/src/*"], + "@webiny/api-headless-cms-ddb": ["../../../../../packages/api-headless-cms-ddb/src"], "@webiny/api-i18n/*": ["../../../../../packages/api-i18n/src/*"], "@webiny/api-i18n": ["../../../../../packages/api-i18n/src"], "@webiny/api-i18n-content/*": ["../../../../../packages/api-i18n-content/src/*"], diff --git a/yarn.lock b/yarn.lock index 2f07ea5da46..6f9bf4569d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18047,6 +18047,8 @@ __metadata: dependencies: "@webiny/api-form-builder": 0.0.0 "@webiny/api-form-builder-so-ddb": 0.0.0 + "@webiny/api-headless-cms": 0.0.0 + "@webiny/api-headless-cms-ddb": 0.0.0 "@webiny/api-i18n": 0.0.0 "@webiny/api-i18n-content": 0.0.0 "@webiny/api-i18n-ddb": 0.0.0 @@ -18104,6 +18106,8 @@ __metadata: dependencies: "@webiny/api-form-builder": 0.0.0 "@webiny/api-form-builder-so-ddb": 0.0.0 + "@webiny/api-headless-cms": 0.0.0 + "@webiny/api-headless-cms-ddb": 0.0.0 "@webiny/api-i18n": 0.0.0 "@webiny/api-i18n-content": 0.0.0 "@webiny/api-i18n-ddb": 0.0.0 From 59d07f6a1ecd53952cc955ba3202ca8a6288aca2 Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Tue, 7 Nov 2023 16:52:59 +0000 Subject: [PATCH 12/37] fix: fixed api-form-builder tests --- .../__tests__/formsSecurity.test.ts | 4 +-- .../__tests__/useGqlHandler.ts | 11 +++++--- .../cmsFormBuilderStorage/CmsFormsStorage.ts | 2 ++ .../models/form.model.ts | 26 ++++++++++++++++--- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/api-form-builder/__tests__/formsSecurity.test.ts b/packages/api-form-builder/__tests__/formsSecurity.test.ts index 6f702f70abf..f52f3591a62 100644 --- a/packages/api-form-builder/__tests__/formsSecurity.test.ts +++ b/packages/api-form-builder/__tests__/formsSecurity.test.ts @@ -280,7 +280,7 @@ describe("Forms Security Test", () => { ...mock, steps: [ { - title: "", + title: "Step 1", layout: [] } ] @@ -294,7 +294,7 @@ describe("Forms Security Test", () => { ...new MockResponse({ prefix: `new-updated-form-`, id: formId }), steps: [ { - title: "", + title: "Step 1", layout: [] } ] diff --git a/packages/api-form-builder/__tests__/useGqlHandler.ts b/packages/api-form-builder/__tests__/useGqlHandler.ts index e2f03ade537..d073cae6571 100644 --- a/packages/api-form-builder/__tests__/useGqlHandler.ts +++ b/packages/api-form-builder/__tests__/useGqlHandler.ts @@ -7,7 +7,10 @@ import { createFileManagerContext, createFileManagerGraphQL } from "@webiny/api- import i18nContext from "@webiny/api-i18n/graphql/context"; import { mockLocalesPlugins } from "@webiny/api-i18n/graphql/testing"; import { SecurityIdentity } from "@webiny/api-security/types"; -import { createFormBuilder } from "~/index"; +import { + createFormBuilderContext, + createFormBuilderGraphQL +} from "~/cmsFormBuilderStorage/createFormBuilderContext"; // Graphql import { INSTALL as INSTALL_FILE_MANAGER } from "./graphql/fileManagerSettings"; import { @@ -45,6 +48,7 @@ import { HeadlessCmsStorageOperations } from "@webiny/api-headless-cms/types"; import { CmsParametersPlugin, createHeadlessCmsContext } from "@webiny/api-headless-cms"; import { FormBuilderStorageOperations } from "~/types"; import { createPageBuilderContext } from "@webiny/api-page-builder"; +import { PageBuilderStorageOperations } from "@webiny/api-page-builder/types"; export interface UseGqlHandlerParams { permissions?: SecurityPermission[]; @@ -65,7 +69,7 @@ export default (params: UseGqlHandlerParams = {}) => { const { permissions, identity, plugins = [] } = params; const i18nStorage = getStorageOps("i18n"); const fileManagerStorage = getStorageOps("fileManager"); - const pageBuilderStorage = getStorageOps("pageBuilder"); + const pageBuilderStorage = getStorageOps("pageBuilder"); const formBuilderStorage = getStorageOps("formBuilder"); const cmsStorage = getStorageOps("cms"); @@ -95,7 +99,7 @@ export default (params: UseGqlHandlerParams = {}) => { }), createFileManagerGraphQL(), - createFormBuilder({ + createFormBuilderContext({ storageOperations: formBuilderStorage.storageOperations }), { @@ -123,6 +127,7 @@ export default (params: UseGqlHandlerParams = {}) => { // dummy } }, + createFormBuilderGraphQL(), ...plugins ] }); diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts index f6cc7873897..32df94b0a37 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts @@ -109,6 +109,7 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { return await this.cms.createEntryRevisionFrom(model, form.id, { status: "draft", published: false, + publishedOn: null, locked: false, stats: { submissions: 0, @@ -216,6 +217,7 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { tenant: entry.tenant, webinyVersion: entry.webinyVersion, version: entry.version, + ownedBy: entry.ownedBy, ...entry.values } as FbForm; } diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts index 051155e3ffe..6033f2e8ef1 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts @@ -90,7 +90,7 @@ const lockedField = () => { return createModelField({ label: "Locked", fieldId: "locked", - type: "text", + type: "boolean", validation: [required()] }); }; @@ -405,6 +405,22 @@ const triggersField = () => { }); }; +const publishedOnField = () => { + return createModelField({ + label: "Published On", + fieldId: "publishedOn", + type: "datetime" + }); +}; + +const slugField = () => { + return createModelField({ + label: "Slug", + fieldId: "slug", + type: "text" + }); +}; + const DEFAULT_FIELDS = [ "formId", "name", @@ -416,7 +432,9 @@ const DEFAULT_FIELDS = [ "fields", "steps", "settings", - "triggers" + "triggers", + "publishedOn", + "slug" ]; const SETTINGS_FIELDS: CmsModelField[] = [ @@ -484,7 +502,9 @@ export const createFormDataModelDefinition = (group: CmsModelGroup): CmsPrivateM fieldsField(FIELD_FIELDS), stepsField(STEP_FIELDS), settingsField(SETTINGS_FIELDS), - triggersField() + triggersField(), + publishedOnField(), + slugField() ], description: "Form Builder - Form builder create data model", isPrivate: true, From 37603f332b368e773fe288fa641243f3b3918c9d Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Wed, 8 Nov 2023 09:57:58 +0000 Subject: [PATCH 13/37] fix: fixed api-audit-logs tests --- packages/api-audit-logs/__tests__/helpers/handlerCore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api-audit-logs/__tests__/helpers/handlerCore.ts b/packages/api-audit-logs/__tests__/helpers/handlerCore.ts index 4467369cebd..3a18e61bc9a 100644 --- a/packages/api-audit-logs/__tests__/helpers/handlerCore.ts +++ b/packages/api-audit-logs/__tests__/helpers/handlerCore.ts @@ -16,7 +16,7 @@ import { AuditLogsContext } from "~/types"; import { createAco } from "@webiny/api-aco"; import { createAuditLogs } from "~/index"; import { createContextPlugin } from "@webiny/handler"; -import { createFormBuilder } from "@webiny/api-form-builder"; +import { createFormBuilderContext } from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import { FormBuilderStorageOperations } from "@webiny/api-form-builder/types"; import { FileManagerStorageOperations } from "@webiny/api-file-manager/types"; import { PageBuilderStorageOperations } from "@webiny/api-page-builder/types"; @@ -143,7 +143,7 @@ export const createHandlerCore = (params?: CreateHandlerCoreParams) => { createFileManagerContext({ storageOperations: fileManagerStorage.storageOperations }), - createFormBuilder({ + createFormBuilderContext({ storageOperations: formBuilderStorage.storageOperations }), createHeadlessCmsGraphQL(), From 6e6360cd4905c52d0c5f85fb2b81edcc5e784919 Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Wed, 8 Nov 2023 12:42:10 +0000 Subject: [PATCH 14/37] fix: add interface implementation to CmsSubmissionsStorage class --- .../src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts index d598bbe7df1..addd65d963a 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts @@ -7,6 +7,7 @@ import { FormBuilderStorageOperationsCreateSubmissionParams, FormBuilderStorageOperationsUpdateSubmissionParams, FormBuilderStorageOperationsListSubmissionsParams, + FormBuilderSubmissionStorageOperations, FbSubmission } from "~/types"; @@ -15,7 +16,7 @@ interface ModelContext { locale: string; } -export class CmsSubmissionsStorage { +export class CmsSubmissionsStorage implements FormBuilderSubmissionStorageOperations { private readonly cms: HeadlessCms; private readonly security: Security; private readonly model: CmsModel; From eb371aa5984c94d304049af8c166f8e56b2f8ac6 Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Wed, 8 Nov 2023 15:37:04 +0000 Subject: [PATCH 15/37] feat: moved deleteSubmission and getSubmission into HCMS --- .../api-form-builder-so-ddb-es/src/index.ts | 8 +- .../src/operations/form/index.ts | 20 +- .../src/operations/submission/index.ts | 332 ++---------------- packages/api-form-builder-so-ddb/src/index.ts | 6 +- .../src/operations/form/index.ts | 20 +- .../src/operations/submission/index.ts | 78 +--- .../CmsSubmissionsStorage.ts | 26 +- packages/api-form-builder/src/types.ts | 4 +- 8 files changed, 78 insertions(+), 416 deletions(-) diff --git a/packages/api-form-builder-so-ddb-es/src/index.ts b/packages/api-form-builder-so-ddb-es/src/index.ts index 4e6a0fb2379..aa67089e3a3 100644 --- a/packages/api-form-builder-so-ddb-es/src/index.ts +++ b/packages/api-form-builder-so-ddb-es/src/index.ts @@ -180,12 +180,6 @@ export const createFormBuilderStorageOperations: FormBuilderStorageOperationsFac entity: entities.settings }), forms: createFormStorageOperations(), - submissions: createSubmissionStorageOperations({ - elasticsearch, - table, - entity: entities.submission, - esEntity: entities.esSubmission, - plugins - }) + submissions: createSubmissionStorageOperations() }; }; diff --git a/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts b/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts index 8095fffd664..a4f64475880 100644 --- a/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts +++ b/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts @@ -29,61 +29,61 @@ export const createFormStorageOperations = (): FormBuilderFormStorageOperations const createForm = () => { throw new Error( - "api-form-builder-ddb-es does not implement the Form Builder storage operations." + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." ); }; const createFormFrom = async () => { throw new Error( - "api-form-builder-ddb-es does not implement the Form Builder storage operations." + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." ); }; const updateForm = async () => { throw new Error( - "api-form-builder-ddb-es does not implement the Form Builder storage operations." + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." ); }; const getForm = async () => { throw new Error( - "api-form-builder-ddb-es does not implement the Form Builder storage operations." + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." ); }; const listForms = () => { throw new Error( - "api-form-builder-ddb-es does not implement the Form Builder storage operations." + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." ); }; const listFormRevisions = async () => { throw new Error( - "api-form-builder-ddb-es does not implement the Form Builder storage operations." + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." ); }; const deleteForm = async () => { throw new Error( - "api-form-builder-ddb-es does not implement the Form Builder storage operations." + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." ); }; const deleteFormRevision = async () => { throw new Error( - "api-form-builder-ddb-esdoes not implement the Form Builder storage operations." + "api-form-builder-so-ddb-esdoes not implement the Form Builder storage operations." ); }; const publishForm = async () => { throw new Error( - "api-form-builder-ddb-es does not implement the Form Builder storage operations." + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." ); }; const unpublishForm = async () => { throw new Error( - "api-form-builder-ddb-es does not implement the Form Builder storage operations." + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." ); }; diff --git a/packages/api-form-builder-so-ddb-es/src/operations/submission/index.ts b/packages/api-form-builder-so-ddb-es/src/operations/submission/index.ts index 3685605c3ed..2677541e893 100644 --- a/packages/api-form-builder-so-ddb-es/src/operations/submission/index.ts +++ b/packages/api-form-builder-so-ddb-es/src/operations/submission/index.ts @@ -1,31 +1,11 @@ -import { - FbSubmission, - FormBuilderStorageOperationsCreateSubmissionParams, - FormBuilderStorageOperationsDeleteSubmissionParams, - FormBuilderStorageOperationsGetSubmissionParams, - FormBuilderStorageOperationsListSubmissionsParams, - FormBuilderStorageOperationsListSubmissionsResponse, - FormBuilderStorageOperationsUpdateSubmissionParams -} from "@webiny/api-form-builder/types"; import { Entity, Table } from "dynamodb-toolbox"; import { Client } from "@elastic/elasticsearch"; -import WebinyError from "@webiny/error"; -import { batchReadAll } from "@webiny/db-dynamodb/utils/batchRead"; -import { sortItems } from "@webiny/db-dynamodb/utils/sort"; -import { createLimit, encodeCursor, decodeCursor } from "@webiny/api-elasticsearch"; -import { - createElasticsearchBody, - createSubmissionElasticType -} from "~/operations/submission/elasticsearchBody"; import { PluginsContainer } from "@webiny/plugins"; import { FormBuilderSubmissionStorageOperations, FormBuilderSubmissionStorageOperationsCreatePartitionKeyParams } from "~/types"; -import { configurations } from "~/configurations"; -import { cleanupItem } from "@webiny/db-dynamodb/utils/cleanup"; import { parseIdentifier } from "@webiny/utils"; -import { ElasticsearchSearchResponse } from "@webiny/api-elasticsearch/types"; export interface CreateSubmissionStorageOperationsParams { entity: Entity; @@ -35,11 +15,7 @@ export interface CreateSubmissionStorageOperationsParams { plugins: PluginsContainer; } -export const createSubmissionStorageOperations = ( - params: CreateSubmissionStorageOperationsParams -): FormBuilderSubmissionStorageOperations => { - const { entity, esEntity, table, elasticsearch, plugins } = params; - +export const createSubmissionStorageOperations = (): FormBuilderSubmissionStorageOperations => { const createSubmissionPartitionKey = ( params: FormBuilderSubmissionStorageOperationsCreatePartitionKeyParams ) => { @@ -53,300 +29,34 @@ export const createSubmissionStorageOperations = ( return `FS#${id}`; }; - const createSubmissionType = () => { - return "fb.formSubmission"; - }; - - const createSubmission = async ( - params: FormBuilderStorageOperationsCreateSubmissionParams - ): Promise => { - const { submission, form } = params; - const keys = { - PK: createSubmissionPartitionKey(form), - SK: createSubmissionSortKey(submission.id) - }; - - try { - await entity.put({ - ...submission, - ...keys, - TYPE: createSubmissionType() - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not create form submission in the DynamoDB.", - ex.code || "UPDATE_FORM_SUBMISSION_ERROR", - { - submission, - form, - keys - } - ); - } - - try { - const { index } = configurations.es({ - tenant: form.tenant, - locale: form.locale - }); - await esEntity.put({ - index, - data: { - ...submission, - __type: createSubmissionElasticType() - }, - TYPE: createSubmissionType(), - ...keys - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not create form submission in the Elasticsearch.", - ex.code || "UPDATE_FORM_SUBMISSION_ERROR", - { - submission, - form, - keys - } - ); - } - - return submission; + const createSubmission = async () => { + throw new Error( + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." + ); }; - /** - * We do not save the data in the Elasticsearch because there is no need for that. - */ - const updateSubmission = async ( - params: FormBuilderStorageOperationsUpdateSubmissionParams - ): Promise => { - const { submission, form, original } = params; - const keys = { - PK: createSubmissionPartitionKey(form), - SK: createSubmissionSortKey(submission.id) - }; - try { - await entity.put({ - ...submission, - ...keys, - TYPE: createSubmissionType() - }); - return submission; - } catch (ex) { - throw new WebinyError( - ex.message || "Could not update form submission in the DynamoDB.", - ex.code || "UPDATE_FORM_SUBMISSION_ERROR", - { - submission, - original, - form, - keys - } - ); - } + const updateSubmission = async () => { + throw new Error( + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." + ); }; - const deleteSubmission = async ( - params: FormBuilderStorageOperationsDeleteSubmissionParams - ): Promise => { - const { submission, form } = params; - - const keys = { - PK: createSubmissionPartitionKey(form), - SK: createSubmissionSortKey(submission.id) - }; - - try { - await entity.delete(keys); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not delete form submission from DynamoDB.", - ex.code || "DELETE_FORM_SUBMISSION_ERROR", - { - submission, - form, - keys - } - ); - } - - try { - await esEntity.delete(keys); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not delete form submission from Elasticsearch.", - ex.code || "DELETE_FORM_SUBMISSION_ERROR", - { - submission, - form, - keys - } - ); - } - - return submission; - }; - - /** - * - * We are using this method because it is faster to fetch the exact data from the DynamoDB than Elasticsearch. - * - * @internal - */ - const listSubmissionsByIds = async ( - params: FormBuilderStorageOperationsListSubmissionsParams - ): Promise => { - const { where, sort } = params; - const items = (where.id_in || []).map(id => { - return entity.getBatch({ - PK: createSubmissionPartitionKey({ - ...where - }), - SK: createSubmissionSortKey(id) - }); - }); - - let results: FbSubmission[] = []; - - try { - results = await batchReadAll({ - table, - items - }); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not batch read form submissions.", - ex.code || "BATCH_READ_SUBMISSIONS_ERROR", - { - where, - sort - } - ); - } - /** - * We need to remove empty results because it is a possibility that batch read returned null for non-existing record. - */ - const submissions = results.filter(Boolean).map(submission => { - return cleanupItem(entity, submission); - }) as FbSubmission[]; - if (!sort) { - return submissions; - } - return sortItems({ - items: submissions, - sort, - fields: [] - }); + const deleteSubmission = async () => { + throw new Error( + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." + ); }; - const listSubmissions = async ( - params: FormBuilderStorageOperationsListSubmissionsParams - ): Promise => { - const { where, sort = [], limit: initialLimit, after } = params; - - if (where.id_in) { - const items = await listSubmissionsByIds(params); - - return { - items, - meta: { - hasMoreItems: false, - cursor: null, - totalCount: items.length - } - }; - } - - const limit = createLimit(initialLimit); - - const body = createElasticsearchBody({ - plugins, - sort, - limit: limit + 1, - where, - after: decodeCursor(after) as any - }); - - const esConfig = configurations.es({ - tenant: where.tenant, - locale: where.locale - }); - - const query = { - ...esConfig, - body - }; - - let response: ElasticsearchSearchResponse; - try { - response = await elasticsearch.search(query); - } catch (ex) { - throw new WebinyError( - ex.message || "Could list form submissions.", - ex.code || "LIST_SUBMISSIONS_ERROR", - { - where, - query - } - ); - } - - const { hits, total } = response.body.hits; - const items = hits.map(item => item._source); - - const hasMoreItems = items.length > limit; - if (hasMoreItems) { - /** - * Remove the last item from results, we don't want to include it. - */ - items.pop(); - } - /** - * Cursor is the `sort` value of the last item in the array. - * https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#search-after - */ - const meta = { - hasMoreItems, - totalCount: total.value, - cursor: items.length > 0 ? encodeCursor(hits[items.length - 1].sort) || null : null - }; - - return { - items, - meta - }; + const listSubmissions = async () => { + throw new Error( + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." + ); }; - const getSubmission = async ( - params: FormBuilderStorageOperationsGetSubmissionParams - ): Promise => { - const { where } = params; - - const keys = { - PK: createSubmissionPartitionKey({ - ...where, - formId: where.formId as string - }), - SK: createSubmissionSortKey(where.id) - }; - - try { - const result = await entity.get(keys); - - if (!result || !result.Item) { - return null; - } - - return cleanupItem(entity, result.Item); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not oad submission.", - ex.code || "GET_SUBMISSION_ERROR", - { - where, - keys - } - ); - } + const getSubmission = async () => { + throw new Error( + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." + ); }; return { diff --git a/packages/api-form-builder-so-ddb/src/index.ts b/packages/api-form-builder-so-ddb/src/index.ts index 9c37b4c98d3..6ff4beb3abe 100644 --- a/packages/api-form-builder-so-ddb/src/index.ts +++ b/packages/api-form-builder-so-ddb/src/index.ts @@ -109,10 +109,6 @@ export const createFormBuilderStorageOperations: FormBuilderStorageOperationsFac entity: entities.settings }), forms: createFormStorageOperations(), - submissions: createSubmissionStorageOperations({ - table, - entity: entities.submission, - plugins - }) + submissions: createSubmissionStorageOperations() }; }; diff --git a/packages/api-form-builder-so-ddb/src/operations/form/index.ts b/packages/api-form-builder-so-ddb/src/operations/form/index.ts index 44ee4843a2b..fb7baa66a86 100644 --- a/packages/api-form-builder-so-ddb/src/operations/form/index.ts +++ b/packages/api-form-builder-so-ddb/src/operations/form/index.ts @@ -16,61 +16,61 @@ export const createFormStorageOperations = (): FormBuilderFormStorageOperations const createForm = () => { throw new Error( - "api-form-builder-ddb does not implement the Form Builder storage operations." + "api-form-builder-so-ddb does not implement the Form Builder storage operations." ); }; const createFormFrom = async () => { throw new Error( - "api-form-builder-ddb does not implement the Form Builder storage operations." + "api-form-builder-so-ddb does not implement the Form Builder storage operations." ); }; const updateForm = async () => { throw new Error( - "api-form-builder-ddb does not implement the Form Builder storage operations." + "api-form-builder-so-ddb does not implement the Form Builder storage operations." ); }; const getForm = async () => { throw new Error( - "api-form-builder-ddb does not implement the Form Builder storage operations." + "api-form-builder-so-ddb does not implement the Form Builder storage operations." ); }; const listForms = () => { throw new Error( - "api-form-builder-ddb does not implement the Form Builder storage operations." + "api-form-builder-so-ddb does not implement the Form Builder storage operations." ); }; const listFormRevisions = async () => { throw new Error( - "api-form-builder-ddb does not implement the Form Builder storage operations." + "api-form-builder-so-ddb does not implement the Form Builder storage operations." ); }; const deleteForm = async () => { throw new Error( - "api-form-builder-ddb does not implement the Form Builder storage operations." + "api-form-builder-so-ddb does not implement the Form Builder storage operations." ); }; const deleteFormRevision = async () => { throw new Error( - "api-form-builder-ddb does not implement the Form Builder storage operations." + "api-form-builder-so-ddb does not implement the Form Builder storage operations." ); }; const publishForm = async () => { throw new Error( - "api-form-builder-ddb does not implement the Form Builder storage operations." + "api-form-builder-so-ddb does not implement the Form Builder storage operations." ); }; const unpublishForm = async () => { throw new Error( - "api-form-builder-ddb does not implement the Form Builder storage operations." + "api-form-builder-so-ddb does not implement the Form Builder storage operations." ); }; diff --git a/packages/api-form-builder-so-ddb/src/operations/submission/index.ts b/packages/api-form-builder-so-ddb/src/operations/submission/index.ts index 22cb442e434..eda9095c692 100644 --- a/packages/api-form-builder-so-ddb/src/operations/submission/index.ts +++ b/packages/api-form-builder-so-ddb/src/operations/submission/index.ts @@ -1,18 +1,10 @@ -import { - FbSubmission, - FormBuilderStorageOperationsDeleteSubmissionParams, - FormBuilderStorageOperationsGetSubmissionParams -} from "@webiny/api-form-builder/types"; import { Entity, Table } from "dynamodb-toolbox"; -import WebinyError from "@webiny/error"; import { PluginsContainer } from "@webiny/plugins"; import { FormBuilderSubmissionStorageOperations, FormBuilderSubmissionStorageOperationsCreatePartitionKeyParams } from "~/types"; -import { cleanupItem } from "@webiny/db-dynamodb/utils/cleanup"; import { parseIdentifier } from "@webiny/utils"; -import { get } from "@webiny/db-dynamodb/utils/get"; export interface CreateSubmissionStorageOperationsParams { entity: Entity; @@ -20,11 +12,7 @@ export interface CreateSubmissionStorageOperationsParams { plugins: PluginsContainer; } -export const createSubmissionStorageOperations = ( - params: CreateSubmissionStorageOperationsParams -): FormBuilderSubmissionStorageOperations => { - const { entity } = params; - +export const createSubmissionStorageOperations = (): FormBuilderSubmissionStorageOperations => { const createSubmissionPartitionKey = ( params: FormBuilderSubmissionStorageOperationsCreatePartitionKeyParams ) => { @@ -40,74 +28,32 @@ export const createSubmissionStorageOperations = ( const createSubmission = () => { throw new Error( - "api-form-builder-ddb does not implement the Form Builder storage operations." + "api-form-builder-so-ddb does not implement the Form Builder storage operations." ); }; const updateSubmission = () => { throw new Error( - "api-form-builder-ddb does not implement the Form Builder storage operations." + "api-form-builder-so-ddb does not implement the Form Builder storage operations." ); }; - // Skipped when moving backend to HCMS. - const deleteSubmission = async ( - params: FormBuilderStorageOperationsDeleteSubmissionParams - ): Promise => { - const { submission, form } = params; - - const keys = { - PK: createSubmissionPartitionKey(form), - SK: createSubmissionSortKey(submission.id) - }; - - try { - await entity.delete(keys); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not delete form submission from DynamoDB.", - ex.code || "DELETE_FORM_SUBMISSION_ERROR", - { - submission, - form, - keys - } - ); - } - - return submission; + const deleteSubmission = async () => { + throw new Error( + "api-form-builder-so-ddb does not implement the Form Builder storage operations." + ); }; const listSubmissions = () => { throw new Error( - "api-form-builder-ddb does not implement the Form Builder storage operations." + "api-form-builder-so-ddb does not implement the Form Builder storage operations." ); }; - // Skipped when moving backend to HCMS. - const getSubmission = async ( - params: FormBuilderStorageOperationsGetSubmissionParams - ): Promise => { - const { where } = params; - - const keys = { - PK: createSubmissionPartitionKey(where), - SK: createSubmissionSortKey(where.id) - }; - - try { - const item = await get({ entity, keys }); - return cleanupItem(entity, item); - } catch (ex) { - throw new WebinyError( - ex.message || "Could not oad submission.", - ex.code || "GET_SUBMISSION_ERROR", - { - where, - keys - } - ); - } + const getSubmission = async () => { + throw new Error( + "api-form-builder-so-ddb does not implement the Form Builder storage operations." + ); }; return { diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts index addd65d963a..e12c8541c6e 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts @@ -8,6 +8,8 @@ import { FormBuilderStorageOperationsUpdateSubmissionParams, FormBuilderStorageOperationsListSubmissionsParams, FormBuilderSubmissionStorageOperations, + FormBuilderStorageOperationsDeleteSubmissionParams, + FormBuilderStorageOperationsGetSubmissionParams, FbSubmission } from "~/types"; @@ -87,12 +89,28 @@ export class CmsSubmissionsStorage implements FormBuilderSubmissionStorageOperat }); } - async getSubmission() { - return null; + async getSubmission(params: FormBuilderStorageOperationsGetSubmissionParams) { + const { where } = params; + const model = this.modelWithContext(where); + + return await this.security.withoutAuthorization(async () => { + const entry = await this.cms.getEntry(model, { + where: { entryId: where.id, latest: true } + }); + + return this.getSubmissionValues(entry); + }); } - async deleteSubmission() { - return null; + async deleteSubmission(params: FormBuilderStorageOperationsDeleteSubmissionParams) { + const { submission } = params; + const model = this.modelWithContext(submission); + + return await this.security.withoutAuthorization(async () => { + return this.cms.deleteEntry(model, submission.id, { + force: true + }); + }); } private getSubmissionValues(entry: CmsEntry) { diff --git a/packages/api-form-builder/src/types.ts b/packages/api-form-builder/src/types.ts index 37c833dbbf3..1218af8b641 100644 --- a/packages/api-form-builder/src/types.ts +++ b/packages/api-form-builder/src/types.ts @@ -788,9 +788,7 @@ export interface FormBuilderSubmissionStorageOperations { updateSubmission( params: FormBuilderStorageOperationsUpdateSubmissionParams ): Promise; - deleteSubmission( - params: FormBuilderStorageOperationsDeleteSubmissionParams - ): Promise; + deleteSubmission(params: FormBuilderStorageOperationsDeleteSubmissionParams): Promise; beforeInit?: (context: FormBuilderContext) => Promise; init?: (context: FormBuilderContext) => Promise; } From f8747c1a82cf7bda671b2a0ab32ee0991d580d11 Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Thu, 23 Nov 2023 10:56:23 +0000 Subject: [PATCH 16/37] fix: implemented requested changes --- apps/api/graphql/src/index.ts | 8 +- .../pageBuilder/export/combine/src/index.ts | 8 +- .../pageBuilder/export/process/src/index.ts | 4 +- .../pageBuilder/import/create/src/index.ts | 8 +- .../pageBuilder/import/process/src/index.ts | 4 +- .../__tests__/helpers/handlerCore.ts | 4 +- .../api-form-builder-so-ddb-es/src/index.ts | 112 +------------ .../src/operations/form/index.ts | 31 +--- .../src/operations/submission/index.ts | 42 +---- .../api-form-builder-so-ddb-es/src/types.ts | 26 +-- packages/api-form-builder-so-ddb/src/index.ts | 48 +----- .../src/operations/form/fields.ts | 47 ------ .../src/operations/form/index.ts | 18 +- .../src/operations/submission/fields.ts | 12 -- .../src/operations/submission/index.ts | 38 +---- packages/api-form-builder-so-ddb/src/types.ts | 24 +-- .../__tests__/useGqlHandler.ts | 10 +- .../cmsFormBuilderStorage/CmsFormsStorage.ts | 61 ++++--- .../CmsSubmissionsStorage.ts | 14 -- .../FormBuilderContextSetup.ts | 4 +- .../createFormBuilderBasicContext.ts | 21 +++ ...creteModelField.ts => createModelField.ts} | 0 .../models/form.model.ts | 19 +-- .../models/submission.model.ts | 4 +- packages/api-form-builder/src/index.ts | 19 +-- .../src/plugins/crud/forms.crud.ts | 59 +++---- .../src/plugins/crud/forms.models.ts | 155 ------------------ .../src/plugins/crud/index.ts | 44 ----- .../src/plugins/crud/settings.crud.ts | 29 ++-- .../src/plugins/crud/settings.models.ts | 49 +++--- .../src/plugins/crud/submissions.crud.ts | 29 +--- .../plugins/crud/utils/createFormSettings.ts | 23 +++ .../src/plugins/crud/utils/index.ts | 1 + .../src/plugins/graphql/formsSchema.ts | 6 +- packages/api-form-builder/src/types.ts | 37 +---- .../src/crud/contentEntry.crud.ts | 2 +- .../src/components/Form/FormRender.tsx | 5 - .../ddb-es/apps/api/graphql/src/index.ts | 10 +- .../ddb/apps/api/graphql/src/index.ts | 8 +- .../pageBuilder/export/combine/src/index.ts | 8 +- .../pageBuilder/export/process/src/index.ts | 4 +- .../pageBuilder/import/create/src/index.ts | 8 +- .../pageBuilder/import/process/src/index.ts | 4 +- .../pageBuilder/export/combine/src/index.ts | 8 +- .../pageBuilder/export/process/src/index.ts | 4 +- .../pageBuilder/import/create/src/index.ts | 8 +- .../pageBuilder/import/process/src/index.ts | 4 +- 47 files changed, 239 insertions(+), 852 deletions(-) delete mode 100644 packages/api-form-builder-so-ddb/src/operations/form/fields.ts delete mode 100644 packages/api-form-builder-so-ddb/src/operations/submission/fields.ts create mode 100644 packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderBasicContext.ts rename packages/api-form-builder/src/cmsFormBuilderStorage/{creteModelField.ts => createModelField.ts} (100%) delete mode 100644 packages/api-form-builder/src/plugins/crud/forms.models.ts create mode 100644 packages/api-form-builder/src/plugins/crud/utils/createFormSettings.ts diff --git a/apps/api/graphql/src/index.ts b/apps/api/graphql/src/index.ts index 171e39608f6..086519ea8af 100644 --- a/apps/api/graphql/src/index.ts +++ b/apps/api/graphql/src/index.ts @@ -22,12 +22,9 @@ import { createFileModelModifier } from "@webiny/api-file-manager"; import { createFileManagerStorageOperations } from "@webiny/api-file-manager-ddb"; -import { - createFormBuilderContext, - createFormBuilderGraphQL -} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import logsPlugins from "@webiny/handler-logs"; import fileManagerS3 from "@webiny/api-file-manager-s3"; +import { createFormBuilder } from "@webiny/api-form-builder"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createHeadlessCmsContext, createHeadlessCmsGraphQL } from "@webiny/api-headless-cms"; import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb"; @@ -88,12 +85,11 @@ export const handler = createHandler({ pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), - createFormBuilderContext({ + createFormBuilder({ storageOperations: createFormBuilderStorageOperations({ documentClient }) }), - createFormBuilderGraphQL(), createApwGraphQL(), createApwPageBuilderContext({ storageOperations: createApwSaStorageOperations({ documentClient }) diff --git a/apps/api/pageBuilder/export/combine/src/index.ts b/apps/api/pageBuilder/export/combine/src/index.ts index 992009d828c..08fa83da13e 100644 --- a/apps/api/pageBuilder/export/combine/src/index.ts +++ b/apps/api/pageBuilder/export/combine/src/index.ts @@ -5,10 +5,7 @@ import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import i18nContentPlugins from "@webiny/api-i18n-content/plugins"; import { CmsParametersPlugin, createHeadlessCmsContext } from "@webiny/api-headless-cms"; import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb"; -import { - createFormBuilderContext, - createFormBuilderGraphQL -} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; +import { createFormBuilder } from "@webiny/api-form-builder"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createPageBuilderContext, @@ -60,12 +57,11 @@ export const handler = createHandler({ }) }), createPageBuilderGraphQL(), - createFormBuilderContext({ + createFormBuilder({ storageOperations: createFormBuilderStorageOperations({ documentClient }) }), - createFormBuilderGraphQL(), pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), diff --git a/apps/api/pageBuilder/export/process/src/index.ts b/apps/api/pageBuilder/export/process/src/index.ts index e01d617977d..8127ff21c18 100644 --- a/apps/api/pageBuilder/export/process/src/index.ts +++ b/apps/api/pageBuilder/export/process/src/index.ts @@ -4,7 +4,7 @@ import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import i18nContentPlugins from "@webiny/api-i18n-content/plugins"; import { createPageBuilderContext } from "@webiny/api-page-builder/graphql"; -import { createFormBuilderContext } from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; +import { createFormBuilder } from "@webiny/api-form-builder"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createStorageOperations as createPageBuilderStorageOperations } from "@webiny/api-page-builder-so-ddb"; import pageBuilderImportExportPlugins from "@webiny/api-page-builder-import-export/graphql"; @@ -58,7 +58,7 @@ export const handler = createHandler({ documentClient }) }), - createFormBuilderContext({ + createFormBuilder({ storageOperations: createFormBuilderStorageOperations({ documentClient }) diff --git a/apps/api/pageBuilder/import/create/src/index.ts b/apps/api/pageBuilder/import/create/src/index.ts index 3288a07c69f..7959e811f0b 100644 --- a/apps/api/pageBuilder/import/create/src/index.ts +++ b/apps/api/pageBuilder/import/create/src/index.ts @@ -5,10 +5,7 @@ import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import i18nContentPlugins from "@webiny/api-i18n-content/plugins"; import { CmsParametersPlugin, createHeadlessCmsContext } from "@webiny/api-headless-cms"; import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb"; -import { - createFormBuilderContext, - createFormBuilderGraphQL -} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; +import { createFormBuilder } from "@webiny/api-form-builder"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createPageBuilderGraphQL, @@ -58,12 +55,11 @@ export const handler = createHandler({ }) }), createPageBuilderGraphQL(), - createFormBuilderContext({ + createFormBuilder({ storageOperations: createFormBuilderStorageOperations({ documentClient }) }), - createFormBuilderGraphQL(), pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), diff --git a/apps/api/pageBuilder/import/process/src/index.ts b/apps/api/pageBuilder/import/process/src/index.ts index a1ff711691f..2e3ddcd5192 100644 --- a/apps/api/pageBuilder/import/process/src/index.ts +++ b/apps/api/pageBuilder/import/process/src/index.ts @@ -3,7 +3,7 @@ import { createHandler } from "@webiny/handler-aws/raw"; import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import i18nContentPlugins from "@webiny/api-i18n-content/plugins"; -import { createFormBuilderContext } from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; +import { createFormBuilder } from "@webiny/api-form-builder"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createPageBuilderContext } from "@webiny/api-page-builder/graphql"; import { createStorageOperations as createPageBuilderStorageOperations } from "@webiny/api-page-builder-so-ddb"; @@ -61,7 +61,7 @@ export const handler = createHandler({ documentClient }) }), - createFormBuilderContext({ + createFormBuilder({ storageOperations: createFormBuilderStorageOperations({ documentClient }) diff --git a/packages/api-audit-logs/__tests__/helpers/handlerCore.ts b/packages/api-audit-logs/__tests__/helpers/handlerCore.ts index 3a18e61bc9a..4467369cebd 100644 --- a/packages/api-audit-logs/__tests__/helpers/handlerCore.ts +++ b/packages/api-audit-logs/__tests__/helpers/handlerCore.ts @@ -16,7 +16,7 @@ import { AuditLogsContext } from "~/types"; import { createAco } from "@webiny/api-aco"; import { createAuditLogs } from "~/index"; import { createContextPlugin } from "@webiny/handler"; -import { createFormBuilderContext } from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; +import { createFormBuilder } from "@webiny/api-form-builder"; import { FormBuilderStorageOperations } from "@webiny/api-form-builder/types"; import { FileManagerStorageOperations } from "@webiny/api-file-manager/types"; import { PageBuilderStorageOperations } from "@webiny/api-page-builder/types"; @@ -143,7 +143,7 @@ export const createHandlerCore = (params?: CreateHandlerCoreParams) => { createFileManagerContext({ storageOperations: fileManagerStorage.storageOperations }), - createFormBuilderContext({ + createFormBuilder({ storageOperations: formBuilderStorage.storageOperations }), createHeadlessCmsGraphQL(), diff --git a/packages/api-form-builder-so-ddb-es/src/index.ts b/packages/api-form-builder-so-ddb-es/src/index.ts index aa67089e3a3..7c69199b332 100644 --- a/packages/api-form-builder-so-ddb-es/src/index.ts +++ b/packages/api-form-builder-so-ddb-es/src/index.ts @@ -1,11 +1,6 @@ -import dynamoDbValueFilters from "@webiny/db-dynamodb/plugins/filters"; -import formElasticsearchFields from "./operations/form/elasticsearchFields"; -import submissionElasticsearchFields from "./operations/submission/elasticsearchFields"; import WebinyError from "@webiny/error"; import { FormBuilderStorageOperationsFactory, ENTITIES } from "~/types"; import { createTable } from "~/definitions/table"; -import { createFormEntity } from "~/definitions/form"; -import { createSubmissionEntity } from "~/definitions/submission"; import { createSystemEntity } from "~/definitions/system"; import { createSettingsEntity } from "~/definitions/settings"; import { createSystemStorageOperations } from "~/operations/system"; @@ -13,27 +8,6 @@ import { createSubmissionStorageOperations } from "~/operations/submission"; import { createSettingsStorageOperations } from "~/operations/settings"; import { createFormStorageOperations } from "~/operations/form"; import { createElasticsearchTable } from "~/definitions/tableElasticsearch"; -import { PluginsContainer } from "@webiny/plugins"; -import { createElasticsearchEntity } from "~/definitions/elasticsearch"; -import { - CompressionPlugin, - ElasticsearchQueryBuilderOperatorPlugin -} from "@webiny/api-elasticsearch"; -import { elasticsearchIndexPlugins } from "~/elasticsearch/indices"; -import { createElasticsearchIndex } from "~/elasticsearch/createElasticsearchIndex"; -import { FormBuilderContext } from "@webiny/api-form-builder/types"; -import { - FormDynamoDbFieldPlugin, - FormElasticsearchBodyModifierPlugin, - FormElasticsearchFieldPlugin, - FormElasticsearchIndexPlugin, - FormElasticsearchQueryModifierPlugin, - FormElasticsearchSortModifierPlugin, - SubmissionElasticsearchBodyModifierPlugin, - SubmissionElasticsearchFieldPlugin, - SubmissionElasticsearchQueryModifierPlugin, - SubmissionElasticsearchSortModifierPlugin -} from "~/plugins"; const reservedFields = ["PK", "SK", "index", "data", "TYPE", "__type", "GSI1_PK", "GSI1_SK"]; @@ -49,14 +23,7 @@ const isReserved = (name: string): void => { export * from "./plugins"; export const createFormBuilderStorageOperations: FormBuilderStorageOperationsFactory = params => { - const { - attributes, - table: tableName, - esTable: esTableName, - documentClient, - elasticsearch, - plugins: userPlugins - } = params; + const { attributes, table: tableName, esTable: esTableName, documentClient } = params; if (attributes) { Object.values(attributes).forEach(attrs => { @@ -64,29 +31,6 @@ export const createFormBuilderStorageOperations: FormBuilderStorageOperationsFac }); } - const plugins = new PluginsContainer([ - /** - * User defined plugins. - */ - userPlugins || [], - /** - * Elasticsearch field definitions for the submission record. - */ - submissionElasticsearchFields(), - /** - * Elasticsearch field definitions for the form record. - */ - formElasticsearchFields(), - /** - * DynamoDB filter plugins for the where conditions. - */ - dynamoDbValueFilters(), - /** - * Built-in Elasticsearch index plugins - */ - elasticsearchIndexPlugins() - ]); - const table = createTable({ tableName, documentClient @@ -101,16 +45,6 @@ export const createFormBuilderStorageOperations: FormBuilderStorageOperationsFac /** * Regular entities. */ - form: createFormEntity({ - entityName: ENTITIES.FORM, - table, - attributes: attributes ? attributes[ENTITIES.FORM] : {} - }), - submission: createSubmissionEntity({ - entityName: ENTITIES.SUBMISSION, - table, - attributes: attributes ? attributes[ENTITIES.SUBMISSION] : {} - }), system: createSystemEntity({ entityName: ENTITIES.SYSTEM, table, @@ -120,54 +54,10 @@ export const createFormBuilderStorageOperations: FormBuilderStorageOperationsFac entityName: ENTITIES.SETTINGS, table, attributes: attributes ? attributes[ENTITIES.SETTINGS] : {} - }), - /** - * Elasticsearch entities. - */ - esForm: createElasticsearchEntity({ - entityName: ENTITIES.ES_FORM, - table: esTable, - attributes: attributes ? attributes[ENTITIES.ES_FORM] : {} - }), - esSubmission: createElasticsearchEntity({ - entityName: ENTITIES.ES_SUBMISSION, - table: esTable, - attributes: attributes ? attributes[ENTITIES.ES_SUBMISSION] : {} }) }; return { - beforeInit: async (context: FormBuilderContext) => { - const types: string[] = [ - // Elasticsearch - CompressionPlugin.type, - ElasticsearchQueryBuilderOperatorPlugin.type, - // Form Builder - FormDynamoDbFieldPlugin.type, - FormElasticsearchBodyModifierPlugin.type, - FormElasticsearchFieldPlugin.type, - FormElasticsearchIndexPlugin.type, - FormElasticsearchQueryModifierPlugin.type, - FormElasticsearchSortModifierPlugin.type, - SubmissionElasticsearchBodyModifierPlugin.type, - SubmissionElasticsearchFieldPlugin.type, - SubmissionElasticsearchQueryModifierPlugin.type, - SubmissionElasticsearchSortModifierPlugin.type - ]; - for (const type of types) { - plugins.mergeByType(context.plugins, type); - } - }, - init: async (context: FormBuilderContext) => { - context.i18n.locales.onLocaleBeforeCreate.subscribe(async ({ locale, tenant }) => { - await createElasticsearchIndex({ - elasticsearch, - plugins, - tenant, - locale: locale.code - }); - }); - }, getTable: () => table, getEsTable: () => esTable, getEntities: () => entities, diff --git a/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts b/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts index d810ffdc7f8..e6f596353a2 100644 --- a/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts +++ b/packages/api-form-builder-so-ddb-es/src/operations/form/index.ts @@ -1,32 +1,6 @@ -import { Entity, Table } from "dynamodb-toolbox"; -import { Client } from "@elastic/elasticsearch"; -import { parseIdentifier } from "@webiny/utils"; -import { PluginsContainer } from "@webiny/plugins"; -import { FormBuilderFormCreateKeyParams, FormBuilderFormStorageOperations } from "~/types"; - -export type DbRecord = T & { - PK: string; - SK: string; - TYPE: string; -}; - -export interface CreateFormStorageOperationsParams { - entity: Entity; - esEntity: Entity; - table: Table; - elasticsearch: Client; - plugins: PluginsContainer; -} +import { FormBuilderFormStorageOperations } from "@webiny/api-form-builder/types"; export const createFormStorageOperations = (): FormBuilderFormStorageOperations => { - const createFormPartitionKey = (params: FormBuilderFormCreateKeyParams): string => { - const { tenant, locale, id: targetId } = params; - - const { id } = parseIdentifier(targetId); - - return `T#${tenant}#L#${locale}#FB#F#${id}`; - }; - const createForm = () => { throw new Error( "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." @@ -97,7 +71,6 @@ export const createFormStorageOperations = (): FormBuilderFormStorageOperations deleteForm, deleteFormRevision, publishForm, - unpublishForm, - createFormPartitionKey + unpublishForm }; }; diff --git a/packages/api-form-builder-so-ddb-es/src/operations/submission/index.ts b/packages/api-form-builder-so-ddb-es/src/operations/submission/index.ts index 984c9c1998c..1492e40121d 100644 --- a/packages/api-form-builder-so-ddb-es/src/operations/submission/index.ts +++ b/packages/api-form-builder-so-ddb-es/src/operations/submission/index.ts @@ -1,35 +1,6 @@ - -import { Entity, Table } from "dynamodb-toolbox"; -import { Client } from "@elastic/elasticsearch"; -import { PluginsContainer } from "@webiny/plugins"; -import { - FormBuilderSubmissionStorageOperations, - FormBuilderSubmissionStorageOperationsCreatePartitionKeyParams -} from "~/types"; -import { parseIdentifier } from "@webiny/utils"; - -export interface CreateSubmissionStorageOperationsParams { - entity: Entity; - esEntity: Entity; - table: Table; - elasticsearch: Client; - plugins: PluginsContainer; -} +import { FormBuilderSubmissionStorageOperations } from "@webiny/api-form-builder/types"; export const createSubmissionStorageOperations = (): FormBuilderSubmissionStorageOperations => { - const createSubmissionPartitionKey = ( - params: FormBuilderSubmissionStorageOperationsCreatePartitionKeyParams - ) => { - const { tenant, locale, formId } = params; - - const { id } = parseIdentifier(formId); - - return `T#${tenant}#L#${locale}#FB#F#${id}`; - }; - const createSubmissionSortKey = (id: string) => { - return `FS#${id}`; - }; - const createSubmission = async () => { throw new Error( "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." @@ -54,19 +25,10 @@ export const createSubmissionStorageOperations = (): FormBuilderSubmissionStorag ); }; - const getSubmission = async () => { - throw new Error( - "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." - ); - }; - return { createSubmission, deleteSubmission, updateSubmission, - listSubmissions, - getSubmission, - createSubmissionPartitionKey, - createSubmissionSortKey + listSubmissions }; }; diff --git a/packages/api-form-builder-so-ddb-es/src/types.ts b/packages/api-form-builder-so-ddb-es/src/types.ts index 541e96cfd34..2342c54d5b4 100644 --- a/packages/api-form-builder-so-ddb-es/src/types.ts +++ b/packages/api-form-builder-so-ddb-es/src/types.ts @@ -1,15 +1,14 @@ import { FormBuilderStorageOperations as BaseFormBuilderStorageOperations, FormBuilderSystemStorageOperations as BaseFormBuilderSystemStorageOperations, - FormBuilderSubmissionStorageOperations as BaseFormBuilderSubmissionStorageOperations, + FormBuilderSubmissionStorageOperations, FormBuilderSettingsStorageOperations as BaseFormBuilderSettingsStorageOperations, - FormBuilderFormStorageOperations as BaseFormBuilderFormStorageOperations + FormBuilderFormStorageOperations } from "@webiny/api-form-builder/types"; import { DynamoDBClient } from "@webiny/aws-sdk/client-dynamodb"; -import { Entity, Table } from "@webiny/db-dynamodb/toolbox"; +import { Entity } from "@webiny/db-dynamodb/toolbox"; import { AttributeDefinition } from "@webiny/db-dynamodb/toolbox"; import { Client } from "@elastic/elasticsearch"; -import { PluginCollection } from "@webiny/plugins/types"; export type Attributes = Record; @@ -28,7 +27,6 @@ export interface FormBuilderStorageOperationsFactoryParams { table?: string; esTable?: string; attributes?: Record; - plugins?: PluginCollection; } export interface FormBuilderSystemCreateKeysParams { @@ -45,25 +43,12 @@ export interface FormBuilderFormCreateKeyParams { tenant: string; locale: string; } - -export interface FormBuilderFormStorageOperations extends BaseFormBuilderFormStorageOperations { - createFormPartitionKey: (params: FormBuilderFormCreateKeyParams) => string; -} - export interface FormBuilderSubmissionStorageOperationsCreatePartitionKeyParams { tenant: string; locale: string; formId: string; } -export interface FormBuilderSubmissionStorageOperations - extends BaseFormBuilderSubmissionStorageOperations { - createSubmissionPartitionKey: ( - params: FormBuilderSubmissionStorageOperationsCreatePartitionKeyParams - ) => string; - createSubmissionSortKey: (id: string) => string; -} - export interface FormBuilderSettingsStorageOperationsCreatePartitionKeyParams { tenant: string; locale: string; @@ -77,7 +62,7 @@ export interface FormBuilderSettingsStorageOperations createSettingsSortKey: () => string; } -export type Entities = "form" | "esForm" | "submission" | "esSubmission" | "system" | "settings"; +export type Entities = "system" | "settings"; export interface FormBuilderStorageOperations extends BaseFormBuilderStorageOperations, @@ -85,8 +70,7 @@ export interface FormBuilderStorageOperations FormBuilderSystemStorageOperations { forms: FormBuilderFormStorageOperations; submissions: FormBuilderSubmissionStorageOperations; - getTable(): Table; - getEsTable(): Table; + getEntities(): Record>; } export interface FormBuilderStorageOperationsFactory { diff --git a/packages/api-form-builder-so-ddb/src/index.ts b/packages/api-form-builder-so-ddb/src/index.ts index 6ff4beb3abe..220a0a0e59d 100644 --- a/packages/api-form-builder-so-ddb/src/index.ts +++ b/packages/api-form-builder-so-ddb/src/index.ts @@ -1,19 +1,12 @@ -import dynamoDbValueFilters from "@webiny/db-dynamodb/plugins/filters"; -import formSubmissionFields from "~/operations/submission/fields"; -import formFields from "~/operations/form/fields"; import WebinyError from "@webiny/error"; import { FormBuilderStorageOperationsFactory, ENTITIES } from "~/types"; import { createTable } from "~/definitions/table"; -import { createFormEntity } from "~/definitions/form"; -import { createSubmissionEntity } from "~/definitions/submission"; import { createSystemEntity } from "~/definitions/system"; import { createSettingsEntity } from "~/definitions/settings"; import { createSystemStorageOperations } from "~/operations/system"; import { createSubmissionStorageOperations } from "~/operations/submission"; import { createSettingsStorageOperations } from "~/operations/settings"; import { createFormStorageOperations } from "~/operations/form"; -import { PluginsContainer } from "@webiny/plugins"; -import { FormDynamoDbFieldPlugin, FormSubmissionDynamoDbFieldPlugin } from "~/plugins"; const reservedFields = ["PK", "SK", "index", "data", "TYPE", "__type", "GSI1_PK", "GSI1_SK"]; @@ -29,7 +22,7 @@ const isReserved = (name: string): void => { export * from "./plugins"; export const createFormBuilderStorageOperations: FormBuilderStorageOperationsFactory = params => { - const { attributes, table: tableName, documentClient, plugins: userPlugins } = params; + const { attributes, table: tableName, documentClient } = params; if (attributes) { Object.values(attributes).forEach(attrs => { @@ -37,26 +30,6 @@ export const createFormBuilderStorageOperations: FormBuilderStorageOperationsFac }); } - const plugins = new PluginsContainer([ - /** - * User defined plugins. - */ - userPlugins || [], - /** - * Form submission DynamoDB fields. - */ - formSubmissionFields(), - /** - * Form DynamoDB fields. - */ - formFields(), - - /** - * DynamoDB filter plugins for the where conditions. - */ - dynamoDbValueFilters() - ]); - const table = createTable({ tableName, documentClient @@ -66,16 +39,6 @@ export const createFormBuilderStorageOperations: FormBuilderStorageOperationsFac /** * Regular entities. */ - form: createFormEntity({ - entityName: ENTITIES.FORM, - table, - attributes: attributes ? attributes[ENTITIES.FORM] : {} - }), - submission: createSubmissionEntity({ - entityName: ENTITIES.SUBMISSION, - table, - attributes: attributes ? attributes[ENTITIES.SUBMISSION] : {} - }), system: createSystemEntity({ entityName: ENTITIES.SYSTEM, table, @@ -89,15 +52,6 @@ export const createFormBuilderStorageOperations: FormBuilderStorageOperationsFac }; return { - beforeInit: async context => { - const types: string[] = [ - FormDynamoDbFieldPlugin.type, - FormSubmissionDynamoDbFieldPlugin.type - ]; - for (const type of types) { - plugins.mergeByType(context.plugins, type); - } - }, getTable: () => table, getEntities: () => entities, ...createSystemStorageOperations({ diff --git a/packages/api-form-builder-so-ddb/src/operations/form/fields.ts b/packages/api-form-builder-so-ddb/src/operations/form/fields.ts deleted file mode 100644 index d3ab95a7b4e..00000000000 --- a/packages/api-form-builder-so-ddb/src/operations/form/fields.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { FormDynamoDbFieldPlugin } from "~/plugins/FormDynamoDbFieldPlugin"; - -export default () => [ - new FormDynamoDbFieldPlugin({ - field: "publishedOn", - type: "date" - }), - new FormDynamoDbFieldPlugin({ - field: "createdOn", - type: "date" - }), - new FormDynamoDbFieldPlugin({ - field: "savedOn", - type: "date" - }), - new FormDynamoDbFieldPlugin({ - field: "name" - }), - new FormDynamoDbFieldPlugin({ - field: "slug" - }), - new FormDynamoDbFieldPlugin({ - field: "locale" - }), - new FormDynamoDbFieldPlugin({ - field: "tenant" - }), - new FormDynamoDbFieldPlugin({ - field: "published", - type: "boolean" - }), - new FormDynamoDbFieldPlugin({ - field: "status" - }), - new FormDynamoDbFieldPlugin({ - field: "version", - type: "number" - }), - new FormDynamoDbFieldPlugin({ - field: "ownedBy", - path: "ownedBy.id" - }), - new FormDynamoDbFieldPlugin({ - field: "createdBy", - path: "createdBy.id" - }) -]; diff --git a/packages/api-form-builder-so-ddb/src/operations/form/index.ts b/packages/api-form-builder-so-ddb/src/operations/form/index.ts index 7eea2985eb3..25b8bac04a2 100644 --- a/packages/api-form-builder-so-ddb/src/operations/form/index.ts +++ b/packages/api-form-builder-so-ddb/src/operations/form/index.ts @@ -1,19 +1,6 @@ -import { Entity, Table } from "dynamodb-toolbox"; -import { PluginsContainer } from "@webiny/plugins"; -import { FormBuilderFormCreatePartitionKeyParams, FormBuilderFormStorageOperations } from "~/types"; -export interface CreateFormStorageOperationsParams { - entity: Entity; - table: Table; - plugins: PluginsContainer; -} +import { FormBuilderFormStorageOperations } from "@webiny/api-form-builder/types"; export const createFormStorageOperations = (): FormBuilderFormStorageOperations => { - const createFormPartitionKey = (params: FormBuilderFormCreatePartitionKeyParams): string => { - const { tenant, locale } = params; - - return `T#${tenant}#L#${locale}#FB#F`; - }; - const createForm = () => { throw new Error( "api-form-builder-so-ddb does not implement the Form Builder storage operations." @@ -84,7 +71,6 @@ export const createFormStorageOperations = (): FormBuilderFormStorageOperations deleteForm, deleteFormRevision, publishForm, - unpublishForm, - createFormPartitionKey + unpublishForm }; }; diff --git a/packages/api-form-builder-so-ddb/src/operations/submission/fields.ts b/packages/api-form-builder-so-ddb/src/operations/submission/fields.ts deleted file mode 100644 index 48aa15e615e..00000000000 --- a/packages/api-form-builder-so-ddb/src/operations/submission/fields.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { FormSubmissionDynamoDbFieldPlugin } from "~/plugins/FormSubmissionDynamoDbFieldPlugin"; - -export default () => [ - new FormSubmissionDynamoDbFieldPlugin({ - field: "createdOn", - type: "date" - }), - new FormSubmissionDynamoDbFieldPlugin({ - field: "savedOn", - type: "date" - }) -]; diff --git a/packages/api-form-builder-so-ddb/src/operations/submission/index.ts b/packages/api-form-builder-so-ddb/src/operations/submission/index.ts index b4f877cfa0e..5923b70850a 100644 --- a/packages/api-form-builder-so-ddb/src/operations/submission/index.ts +++ b/packages/api-form-builder-so-ddb/src/operations/submission/index.ts @@ -1,31 +1,6 @@ -import { Entity, Table } from "dynamodb-toolbox"; -import { PluginsContainer } from "@webiny/plugins"; -import { - FormBuilderSubmissionStorageOperations, - FormBuilderSubmissionStorageOperationsCreatePartitionKeyParams -} from "~/types"; -import { parseIdentifier } from "@webiny/utils"; - -export interface CreateSubmissionStorageOperationsParams { - entity: Entity; - table: Table; - plugins: PluginsContainer; -} +import { FormBuilderSubmissionStorageOperations } from "@webiny/api-form-builder/types"; export const createSubmissionStorageOperations = (): FormBuilderSubmissionStorageOperations => { - const createSubmissionPartitionKey = ( - params: FormBuilderSubmissionStorageOperationsCreatePartitionKeyParams - ) => { - const { tenant, locale, formId } = params; - - const { id } = parseIdentifier(formId); - - return `T#${tenant}#L#${locale}#FB#FS#${id}`; - }; - const createSubmissionSortKey = (id: string) => { - return id; - }; - const createSubmission = () => { throw new Error( "api-form-builder-so-ddb does not implement the Form Builder storage operations." @@ -50,19 +25,10 @@ export const createSubmissionStorageOperations = (): FormBuilderSubmissionStorag ); }; - const getSubmission = async () => { - throw new Error( - "api-form-builder-so-ddb does not implement the Form Builder storage operations." - ); - }; - return { createSubmission, deleteSubmission, updateSubmission, - listSubmissions, - getSubmission, - createSubmissionPartitionKey, - createSubmissionSortKey + listSubmissions }; }; diff --git a/packages/api-form-builder-so-ddb/src/types.ts b/packages/api-form-builder-so-ddb/src/types.ts index a4066d4125c..40561090727 100644 --- a/packages/api-form-builder-so-ddb/src/types.ts +++ b/packages/api-form-builder-so-ddb/src/types.ts @@ -1,14 +1,13 @@ import { FormBuilderStorageOperations as BaseFormBuilderStorageOperations, FormBuilderSystemStorageOperations as BaseFormBuilderSystemStorageOperations, - FormBuilderSubmissionStorageOperations as BaseFormBuilderSubmissionStorageOperations, + FormBuilderSubmissionStorageOperations, FormBuilderSettingsStorageOperations as BaseFormBuilderSettingsStorageOperations, - FormBuilderFormStorageOperations as BaseFormBuilderFormStorageOperations + FormBuilderFormStorageOperations } from "@webiny/api-form-builder/types"; import { DynamoDBClient } from "@webiny/aws-sdk/client-dynamodb"; -import { Entity, Table } from "@webiny/db-dynamodb/toolbox"; +import { Entity } from "@webiny/db-dynamodb/toolbox"; import { AttributeDefinition } from "@webiny/db-dynamodb/toolbox"; -import { Plugin } from "@webiny/plugins"; export type Attributes = Record; @@ -23,7 +22,6 @@ export interface FormBuilderStorageOperationsFactoryParams { documentClient: DynamoDBClient; table?: string; attributes?: Record; - plugins?: Plugin; } export interface FormBuilderSystemCreateKeysParams { @@ -46,25 +44,12 @@ export interface FormBuilderFormCreateGSIPartitionKeyParams { tenant: string; locale: string; } - -export interface FormBuilderFormStorageOperations extends BaseFormBuilderFormStorageOperations { - createFormPartitionKey: (params: FormBuilderFormCreatePartitionKeyParams) => string; -} - export interface FormBuilderSubmissionStorageOperationsCreatePartitionKeyParams { tenant: string; locale: string; formId: string; } -export interface FormBuilderSubmissionStorageOperations - extends BaseFormBuilderSubmissionStorageOperations { - createSubmissionPartitionKey: ( - params: FormBuilderSubmissionStorageOperationsCreatePartitionKeyParams - ) => string; - createSubmissionSortKey: (id: string) => string; -} - export interface FormBuilderSettingsStorageOperationsCreatePartitionKeyParams { tenant: string; locale: string; @@ -78,7 +63,7 @@ export interface FormBuilderSettingsStorageOperations createSettingsSortKey: () => string; } -export type Entities = "form" | "submission" | "system" | "settings"; +export type Entities = "system" | "settings"; export interface FormBuilderStorageOperations extends BaseFormBuilderStorageOperations, @@ -86,7 +71,6 @@ export interface FormBuilderStorageOperations FormBuilderSystemStorageOperations { forms: FormBuilderFormStorageOperations; submissions: FormBuilderSubmissionStorageOperations; - getTable(): Table; getEntities(): Record>; } diff --git a/packages/api-form-builder/__tests__/useGqlHandler.ts b/packages/api-form-builder/__tests__/useGqlHandler.ts index 2b063ad6992..3cf7e9aa79a 100644 --- a/packages/api-form-builder/__tests__/useGqlHandler.ts +++ b/packages/api-form-builder/__tests__/useGqlHandler.ts @@ -6,11 +6,8 @@ import graphqlHandlerPlugins from "@webiny/handler-graphql"; import { createFileManagerContext, createFileManagerGraphQL } from "@webiny/api-file-manager"; import i18nContext from "@webiny/api-i18n/graphql/context"; import { mockLocalesPlugins } from "@webiny/api-i18n/graphql/testing"; -import { SecurityIdentity } from "@webiny/api-security/types"; -import { - createFormBuilderContext, - createFormBuilderGraphQL -} from "~/cmsFormBuilderStorage/createFormBuilderContext"; +import { SecurityIdentity, SecurityPermission } from "@webiny/api-security/types"; +import { createFormBuilder } from "~/index"; // Graphql import { INSTALL as INSTALL_FILE_MANAGER } from "./graphql/fileManagerSettings"; import { @@ -99,7 +96,7 @@ export default (params: UseGqlHandlerParams = {}) => { }), createFileManagerGraphQL(), - createFormBuilderContext({ + createFormBuilder({ storageOperations: formBuilderStorage.storageOperations }), { @@ -127,7 +124,6 @@ export default (params: UseGqlHandlerParams = {}) => { // dummy } }, - createFormBuilderGraphQL(), ...plugins ] }); diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts index 32df94b0a37..37dc865aed6 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts @@ -1,7 +1,7 @@ -import { CmsEntry, CmsModel, HeadlessCms } from "@webiny/api-headless-cms/types"; +import { CmsEntry, CmsEntryValues, CmsModel, HeadlessCms } from "@webiny/api-headless-cms/types"; import WebinyError from "@webiny/error"; import { Security } from "@webiny/api-security/types"; -import { createIdentifier } from "@webiny/utils"; +import { createIdentifier, parseIdentifier } from "@webiny/utils"; import { FbForm, @@ -43,6 +43,17 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { return { ...this.model, tenant, locale }; } + private async getSortedFormRevisions( + model: CmsModel, + formId: string + ): Promise> { + const entries = (await this.cms.getEntryRevisions(model, formId)) + .filter(entryItem => entryItem.values.published) + .sort((a, b) => b.version - a.version); + + return entries[0]; + } + async getForm(params: FormBuilderStorageOperationsGetFormParams): Promise { const { id, @@ -54,30 +65,28 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { locale } = params.where; const model = this.modelWithContext({ tenant, locale }); - const formId = initialFormId || id?.split("#").shift() || ""; + const formId = initialFormId || parseIdentifier(id).id; const entry = await this.security.withoutAuthorization(async () => { if (latest) { - const [entries] = await this.cms.listLatestEntries(model, { - where: { entryId: formId } + const entry = await this.cms.getEntry(model, { + where: { entryId: formId, latest: true } }); - return entries[0]; + return entry; } else if (published && !version) { - const entries = (await this.cms.getEntryRevisions(model, formId)) - .filter(entryItem => entryItem.values.published) - .sort((a, b) => b.version - a.version); + const entry = await this.getSortedFormRevisions(model, formId); - return entries[0]; + return entry; } else if (id || version) { - return await this.cms.getEntryById( - model, - id || - createIdentifier({ - id: formId as string, - version: version as number - }) - ); + const fallbackId = createIdentifier({ + id: formId, + version: version || 1 + }); + + return await this.cms.getEntryById(model, id || fallbackId); + } else if (latest && published) { + throw new WebinyError("Cannot have both latest and published params."); } else { throw new WebinyError("Missing parameter to get form", "MISSING_WHERE_PARAMETER", { where: params.where @@ -122,11 +131,11 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { } async updateForm(params: FormBuilderStorageOperationsUpdateFormParams): Promise { - const { form, input, meta, options } = params; + const { form } = params; const model = this.modelWithContext(form); const entry = await this.security.withoutAuthorization(async () => { - return await this.cms.updateEntry(model, form.id, input, meta, options); + return await this.cms.updateEntry(model, form.id, form); }); return this.getFormFieldValues(entry); @@ -157,7 +166,7 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { async listForms( params: FormBuilderStorageOperationsListFormsParams ): Promise { - const { id, tenant, locale, ...restWhere } = params.where; + const { tenant, locale, ...restWhere } = params.where; const model = this.modelWithContext({ tenant, locale }); const [entries, meta] = await this.security.withoutAuthorization(async () => { @@ -165,7 +174,7 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { after: params.after, limit: params.limit, sort: params.sort, - where: { entryId: id, ...restWhere } + where: restWhere }); }); @@ -186,22 +195,22 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { } async publishForm(params: FormBuilderStorageOperationsPublishFormParams): Promise { - const { form, input } = params; + const { form } = params; const model = this.modelWithContext(form); const entry = await this.security.withoutAuthorization(async () => { - return await this.cms.updateEntry(model, form.id, input); + return await this.cms.updateEntry(model, form.id, form); }); return this.getFormFieldValues(entry); } async unpublishForm(params: FormBuilderStorageOperationsUnpublishFormParams): Promise { - const { form, input } = params; + const { form } = params; const model = this.modelWithContext(form); const entry = await this.security.withoutAuthorization(async () => { - return await this.cms.updateEntry(model, form.id, input); + return await this.cms.updateEntry(model, form.id, form); }); return this.getFormFieldValues(entry); diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts index e12c8541c6e..6f83cd7e847 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts @@ -9,7 +9,6 @@ import { FormBuilderStorageOperationsListSubmissionsParams, FormBuilderSubmissionStorageOperations, FormBuilderStorageOperationsDeleteSubmissionParams, - FormBuilderStorageOperationsGetSubmissionParams, FbSubmission } from "~/types"; @@ -89,19 +88,6 @@ export class CmsSubmissionsStorage implements FormBuilderSubmissionStorageOperat }); } - async getSubmission(params: FormBuilderStorageOperationsGetSubmissionParams) { - const { where } = params; - const model = this.modelWithContext(where); - - return await this.security.withoutAuthorization(async () => { - const entry = await this.cms.getEntry(model, { - where: { entryId: where.id, latest: true } - }); - - return this.getSubmissionValues(entry); - }); - } - async deleteSubmission(params: FormBuilderStorageOperationsDeleteSubmissionParams) { const { submission } = params; const model = this.modelWithContext(submission); diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts index f46787b4ef4..16caee4fbba 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts @@ -2,7 +2,7 @@ import { CmsModelPlugin } from "@webiny/api-headless-cms"; import { AppPermissions } from "@webiny/api-security/utils/AppPermissions"; import WebinyError from "@webiny/error"; -import { createFormBuilder } from "~/index"; +import { createFormBuilderBasicContext } from "./createFormBuilderBasicContext"; import { createFormBuilderPlugins } from "./createFormBuilderPlugins"; import { CmsFormsStorage } from "./CmsFormsStorage"; import { CmsSubmissionsStorage } from "./CmsSubmissionsStorage"; @@ -48,7 +48,7 @@ export class FormBuilderContextSetup { fullAccessPermissionName: "fb.*" }); - return createFormBuilder({ + return createFormBuilderBasicContext({ storageOperations, formsPermissions, context: this.context diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderBasicContext.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderBasicContext.ts new file mode 100644 index 00000000000..9d6d67d0e5b --- /dev/null +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderBasicContext.ts @@ -0,0 +1,21 @@ +import { FormBuilderStorageOperations, FormBuilderContext } from "~/types"; +import { FormsPermissions } from "../plugins/crud/permissions/FormsPermissions"; +import { setupFormBuilderContext } from "../plugins/crud"; +import triggerHandlers from "../plugins/triggers"; +import validators from "../plugins/validators"; +import formBuilderPrerenderingPlugins from "~/plugins/prerenderingHooks"; + +export interface CreateFormBuilderParams { + storageOperations: FormBuilderStorageOperations; + formsPermissions: FormsPermissions; + context: FormBuilderContext; +} + +export const createFormBuilderBasicContext = (params: CreateFormBuilderParams) => { + return [ + setupFormBuilderContext(params), + triggerHandlers, + validators, + formBuilderPrerenderingPlugins() + ]; +}; diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/creteModelField.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/createModelField.ts similarity index 100% rename from packages/api-form-builder/src/cmsFormBuilderStorage/creteModelField.ts rename to packages/api-form-builder/src/cmsFormBuilderStorage/createModelField.ts diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts index 6033f2e8ef1..08579656ac8 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts @@ -1,7 +1,7 @@ import { CmsPrivateModelFull } from "@webiny/api-headless-cms"; import { CmsModelField, CmsModelGroup } from "@webiny/api-headless-cms/types"; -import { createModelField } from "../creteModelField"; +import { createModelField } from "../createModelField"; const required = () => { return { @@ -99,14 +99,16 @@ const field_IdField = () => { return createModelField({ label: "ID", fieldId: "_id", - type: "text" + type: "text", + validation: [required()] }); }; const fieldIdField = () => { return createModelField({ label: "FieldId", - type: "text" + type: "text", + validation: [required()] }); }; @@ -166,7 +168,6 @@ const fieldOptionsField = (fields: CmsModelField[]) => { return createModelField({ label: "Options", type: "object", - validation: [required()], multipleValues: true, settings: { fields @@ -185,16 +186,14 @@ const fieldValidationNameField = () => { const fieldValidationMessageField = () => { return createModelField({ label: "Message", - type: "text", - validation: [required()] + type: "text" }); }; const fieldValidationSettingsField = () => { return createModelField({ label: "Settings", - type: "json", - validation: [required()] + type: "json" }); }; @@ -202,7 +201,6 @@ const fieldValidationField = (fields: CmsModelField[]) => { return createModelField({ label: "Validation", type: "object", - validation: [required()], multipleValues: true, settings: { fields @@ -261,7 +259,7 @@ export const stepsField = (fields: CmsModelField[]) => { const settingsReCaptchaEnabledField = () => { return createModelField({ label: "Enabled", - type: "text" + type: "boolean" }); }; @@ -309,7 +307,6 @@ const settingsReCaptchaField = (fields: CmsModelField[]) => { return createModelField({ label: "reCaptcha", type: "object", - validation: [required()], settings: { fields } diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/submission.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/submission.model.ts index 50ff015ae38..ff3c1040066 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/models/submission.model.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/submission.model.ts @@ -2,7 +2,7 @@ import { CmsModelField, CmsModelGroup } from "@webiny/api-headless-cms/types"; import { CmsPrivateModelFull } from "@webiny/api-headless-cms"; import { fieldsField, stepsField, FIELD_FIELDS, STEP_FIELDS } from "./form.model"; -import { createModelField } from "../creteModelField"; +import { createModelField } from "../createModelField"; const required = () => { return { @@ -32,7 +32,7 @@ const metaSubmittedOnField = () => { return createModelField({ label: "Submitted On", fieldId: "submittedOn", - type: "text" + type: "datetime" }); }; diff --git a/packages/api-form-builder/src/index.ts b/packages/api-form-builder/src/index.ts index cc821dd426c..d1ecd60ec43 100644 --- a/packages/api-form-builder/src/index.ts +++ b/packages/api-form-builder/src/index.ts @@ -1,9 +1,9 @@ -import { setupFormBuilderContext } from "./plugins/crud"; -import triggerHandlers from "./plugins/triggers"; -import validators from "./plugins/validators"; -import formBuilderPrerenderingPlugins from "~/plugins/prerenderingHooks"; import { FormBuilderStorageOperations, FormBuilderContext } from "~/types"; import { FormsPermissions } from "./plugins/crud/permissions/FormsPermissions"; +import { + createFormBuilderContext, + createFormBuilderGraphQL +} from "./cmsFormBuilderStorage/createFormBuilderContext"; export interface CreateFormBuilderParams { storageOperations: FormBuilderStorageOperations; @@ -11,11 +11,8 @@ export interface CreateFormBuilderParams { context: FormBuilderContext; } -export const createFormBuilder = (params: CreateFormBuilderParams) => { - return [ - setupFormBuilderContext(params), - triggerHandlers, - validators, - formBuilderPrerenderingPlugins() - ]; +export const createFormBuilder = (storageOperations: { + storageOperations: FormBuilderStorageOperations; +}) => { + return [createFormBuilderContext(storageOperations), createFormBuilderGraphQL()]; }; diff --git a/packages/api-form-builder/src/plugins/crud/forms.crud.ts b/packages/api-form-builder/src/plugins/crud/forms.crud.ts index bbf28895840..72f1f161fbe 100644 --- a/packages/api-form-builder/src/plugins/crud/forms.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/forms.crud.ts @@ -1,6 +1,5 @@ import slugify from "slugify"; import { NotFoundError } from "@webiny/handler-graphql"; -import * as models from "./forms.models"; import { FbForm, FbFormStats, @@ -26,9 +25,9 @@ import { import WebinyError from "@webiny/error"; import { Tenant } from "@webiny/api-tenancy/types"; import { I18NLocale } from "@webiny/api-i18n/types"; -import { createIdentifier, mdbid } from "@webiny/utils"; +import { createIdentifier, mdbid, parseIdentifier } from "@webiny/utils"; import { createTopic } from "@webiny/pubsub"; -import { getStatus } from "./utils"; +import { getStatus, createFormSettings } from "./utils"; import { FormsPermissions } from "~/plugins/crud/permissions/FormsPermissions"; export interface CreateFormsCrudParams { @@ -254,10 +253,6 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { async createForm(this: FormBuilder, input) { await formsPermissions.ensure({ rwd: "w" }); const identity = context.security.getIdentity(); - const dataModel = new models.FormCreateDataModel().populate(input); - await dataModel.validate(); - - const data = await dataModel.toJSON(); /** * Forms are identified by a common parent ID + Revision number @@ -265,7 +260,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { const formId = mdbid(); const version = 1; - const slug = `${slugify(data.name)}-${formId}`.toLowerCase(); + const slug = `${slugify(input.name)}-${formId}`.toLowerCase(); const form: FbForm = { id: formId, @@ -284,7 +279,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { displayName: identity.displayName, type: identity.type }, - name: data.name, + name: input.name, slug, version, locked: false, @@ -310,7 +305,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { layout: [] } ], - settings: await new models.FormSettingsModel().toJSON(), + settings: createFormSettings(), triggers: null, webinyVersion: context.WEBINY_VERSION }; @@ -319,7 +314,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { await onFormBeforeCreate.publish({ form }); - const result = await this.storageOperations.forms.createForm({ form, input }); + const result = await this.storageOperations.forms.createForm({ form }); await onFormAfterCreate.publish({ form: result }); @@ -336,9 +331,6 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { }, async updateForm(this: FormBuilder, id, input) { await formsPermissions.ensure({ rwd: "w" }); - const updateData = new models.FormUpdateDataModel().populate(input); - await updateData.validate(); - const data = await updateData.toJSON({ onlyDirty: true }); const original = await this.storageOperations.forms.getForm({ where: { @@ -360,7 +352,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { const form: FbForm = { ...original, - ...data + ...input }; try { @@ -369,10 +361,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { original }); const result = await this.storageOperations.forms.updateForm({ - form, - input, - meta: {}, - options: {} + form }); await onFormAfterUpdate.publish({ form, @@ -384,7 +373,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { ex.message || "Could not update form.", ex.code || "UPDATE_FORM_ERROR", { - input: data, + input, form, original } @@ -438,11 +427,11 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { await formsPermissions.ensure({ owns: form.ownedBy }); - const formFormId = form.formId || form.id.split("#").pop(); + const { id: formId } = parseIdentifier(form.id); const revisions = await this.storageOperations.forms.listFormRevisions({ where: { - formId: formFormId || "", + formId, tenant: form.tenant, locale: form.locale }, @@ -483,11 +472,17 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { }, async publishForm(this: FormBuilder, id) { await formsPermissions.ensure({ rwd: "r", pw: "p" }); - const [pid, revisionNumber = "0001"] = id.split("#"); + + const { id: pid, version } = parseIdentifier(id); + const formId = createIdentifier({ + id: pid, + version: version || 1 + }); + /** * getForm checks for existence of the form. */ - const original = await this.getForm(`${pid}#${revisionNumber}`, { + const original = await this.getForm(formId, { auth: false }); @@ -507,9 +502,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { form }); const result = await this.storageOperations.forms.publishForm({ - original, - form, - input: form + form }); await onFormAfterPublish.publish({ form @@ -549,9 +542,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { form }); const result = await this.storageOperations.forms.unpublishForm({ - original, - form, - input: form + form }); await onFormAfterUnpublish.publish({ form: result @@ -575,7 +566,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { const original = await this.getForm(id, { auth: false }); - const originalFormFormId = original.formId || (original.id.split("#").pop() as string); + const { id: originalFormFormId } = parseIdentifier(original.id); const latest = await this.storageOperations.forms.getForm({ where: { id, @@ -669,8 +660,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { try { await this.storageOperations.forms.updateForm({ - form, - input: form + form }); } catch (ex) { throw new WebinyError( @@ -702,8 +692,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { try { await this.storageOperations.forms.updateForm({ - form, - input: form + form }); } catch (ex) { throw new WebinyError( diff --git a/packages/api-form-builder/src/plugins/crud/forms.models.ts b/packages/api-form-builder/src/plugins/crud/forms.models.ts deleted file mode 100644 index e5df2a70abb..00000000000 --- a/packages/api-form-builder/src/plugins/crud/forms.models.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { validation } from "@webiny/validation"; -/** - * Package @commodo/fields does not have types. - */ -// @ts-ignore -import { boolean, fields, string, withFields, number } from "@commodo/fields"; -/** - * Package commodo-fields-object does not have types. - */ -// @ts-ignore -import { object } from "commodo-fields-object"; - -export const FormFieldsModel = withFields({ - _id: string({ validation: validation.create("required") }), - type: string({ validation: validation.create("required") }), - name: string({ validation: validation.create("required") }), - fieldId: string({ validation: validation.create("required") }), - /** - * Note: We've replaced "i18nString()" with "string()" - */ - label: string({ validation: validation.create("required") }), - helpText: string({}), - placeholderText: string({}), - options: fields({ - list: true, - value: [], - instanceOf: withFields({ - label: string({}), - value: string({ value: "" }) - })() - }), - validation: fields({ - list: true, - value: [], - instanceOf: withFields({ - name: string({ validation: validation.create("required") }), - message: string({}), - settings: object({ value: {} }) - })() - }), - settings: object({ value: {} }) -})(); - -export const FormStepsModel = withFields({ - steps: fields({ - value: {}, - instanceOf: withFields({ - title: string(), - layout: object({ value: [] }) - })() - }) -})(); - -export const FormSettingsModel = withFields({ - layout: fields({ - value: {}, - instanceOf: withFields({ - renderer: string({ value: "default" }) - })() - }), - /** - * Note: We've replaced "i18nString()" with "string()" - */ - submitButtonLabel: string({}), - fullWidthSubmitButton: boolean(), - /** - * Note: We've replaced "i18nObject()" with "object()" - */ - successMessage: object(), - termsOfServiceMessage: fields({ - instanceOf: withFields({ - message: object(), - errorMessage: string({}), - enabled: boolean() - })() - }), - reCaptcha: fields({ - value: {}, - instanceOf: withFields({ - enabled: boolean(), - /** - * Note: We've replaced "i18nString()" with "string()" - */ - errorMessage: string({ - value: "Please verify that you are not a robot." - }) - })() - }) -})(); - -export const FormCreateDataModel = withFields({ - name: string({ validation: validation.create("required") }) -})(); - -export const FormUpdateDataModel = withFields({ - name: string({}), - fields: fields({ - list: true, - value: [], - instanceOf: FormFieldsModel - }), - steps: object({ instanceOf: FormStepsModel, value: {} }), - settings: fields({ instanceOf: FormSettingsModel, value: {} }), - triggers: object() -})(); - -export const FormSubmissionCreateDataModel = withFields({ - data: object({ validation: validation.create("required") }), - meta: fields({ - value: {}, - instanceOf: withFields({ - ip: string({}), - submittedOn: string({ - value: new Date().toISOString() - }), - url: fields({ - value: {}, - instanceOf: withFields({ - location: string(), - query: object() - })() - }) - })() - }), - form: fields({ - instanceOf: withFields({ - id: string({ validation: validation.create("required") }), - parent: string({ validation: validation.create("required") }), - name: string({ validation: validation.create("required") }), - version: number({ validation: validation.create("required") }), - steps: object({ instanceOf: FormStepsModel, value: {} }), - fields: fields({ - list: true, - value: [], - instanceOf: FormFieldsModel - }) - })() - }) -})(); - -export const FormSubmissionUpdateDataModel = withFields({ - id: string({ validation: validation.create("required") }), - logs: fields({ - list: true, - value: [], - instanceOf: withFields({ - type: string({ - validation: validation.create("required,in:error:warning:info:success") - }), - message: string(), - data: object(), - createdOn: string({ value: new Date().toISOString() }) - })() - }) -})(); diff --git a/packages/api-form-builder/src/plugins/crud/index.ts b/packages/api-form-builder/src/plugins/crud/index.ts index 12e710059d9..9ccbc7ab57c 100644 --- a/packages/api-form-builder/src/plugins/crud/index.ts +++ b/packages/api-form-builder/src/plugins/crud/index.ts @@ -34,28 +34,6 @@ export const setupFormBuilderContext = async (params: CreateFormBuilderCrudParam return context.tenancy.getCurrentTenant(); }; - try { - if (storageOperations.beforeInit) { - await storageOperations.beforeInit(context); - } - - if (storageOperations.forms.beforeInit) { - await storageOperations.forms.beforeInit(context); - } - - if (storageOperations.submissions.beforeInit) { - await storageOperations.submissions.beforeInit(context); - } - } catch (ex) { - throw new WebinyError( - ex.message || "Could not run before init in Form Builder storage operations.", - ex.code || "STORAGE_OPERATIONS_BEFORE_INIT_ERROR", - { - ...ex - } - ); - } - const basePermissionsArgs = { getIdentity, fullAccessPermissionName: "fb.*" @@ -96,26 +74,4 @@ export const setupFormBuilderContext = async (params: CreateFormBuilderCrudParam formsPermissions }) }; - - try { - if (storageOperations.init) { - await storageOperations.init(context); - } - - if (storageOperations.forms.init) { - await storageOperations.forms.init(context); - } - - if (storageOperations.submissions.init) { - await storageOperations.submissions.init(context); - } - } catch (ex) { - throw new WebinyError( - ex.message || "Could not run init in Form Builder storage operations.", - ex.code || "STORAGE_OPERATIONS_INIT_ERROR", - { - ...ex - } - ); - } }; diff --git a/packages/api-form-builder/src/plugins/crud/settings.crud.ts b/packages/api-form-builder/src/plugins/crud/settings.crud.ts index d0999ec84ca..cf60519df46 100644 --- a/packages/api-form-builder/src/plugins/crud/settings.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/settings.crud.ts @@ -17,6 +17,8 @@ import { I18NLocale } from "@webiny/api-i18n/types"; import { NotFoundError } from "@webiny/handler-graphql"; import { createTopic } from "@webiny/pubsub"; import { SettingsPermissions } from "./permissions/SettingsPermissions"; +import { removeUndefinedValues } from "@webiny/utils/removeUndefinedValues"; +import { createZodError } from "@webiny/utils/createZodError"; export interface CreateSettingsCrudParams { getTenant: () => Tenant; @@ -84,10 +86,13 @@ export const createSettingsCrud = (params: CreateSettingsCrudParams): SettingsCR return settings; }, async createSettings(this: FormBuilder, input) { - const formBuilderSettings = new models.CreateDataModel().populate(input); - await formBuilderSettings.validate(); + const result = await models.CreateDataModel().safeParseAsync(input); - const data = await formBuilderSettings.toJSON(); + if (!result.success) { + throw createZodError(result.error); + } + + const data = removeUndefinedValues(result.data); const original = await this.getSettings({ auth: false }); if (original) { @@ -133,10 +138,14 @@ export const createSettingsCrud = (params: CreateSettingsCrudParams): SettingsCR async updateSettings(this: FormBuilder, data) { await settingsPermissions.ensure(); - const updatedData = new models.UpdateDataModel().populate(data); - await updatedData.validate(); + const result = await models.UpdateDataModel().safeParseAsync(data); + + if (!result.success) { + throw createZodError(result.error); + } + + const updatedData: any = removeUndefinedValues(result.data); - const newSettings = await updatedData.toJSON({ onlyDirty: true }); const original = await this.getSettings(); if (!original) { throw new NotFoundError(`"Form Builder" settings not found!`); @@ -145,12 +154,12 @@ export const createSettingsCrud = (params: CreateSettingsCrudParams): SettingsCR /** * Assign specific properties, just to be sure nothing else gets in the record. */ - const settings = Object.keys(newSettings).reduce( - (collection, key) => { - if (newSettings[key] === undefined) { + const settings = Object.keys(updatedData).reduce( + (collection, key: string) => { + if (updatedData[key as keyof Settings] === undefined) { return collection; } - collection[key as keyof Settings] = newSettings[key]; + collection[key as keyof Settings] = updatedData[key]; return collection; }, { diff --git a/packages/api-form-builder/src/plugins/crud/settings.models.ts b/packages/api-form-builder/src/plugins/crud/settings.models.ts index df6c42a18e7..11268195342 100644 --- a/packages/api-form-builder/src/plugins/crud/settings.models.ts +++ b/packages/api-form-builder/src/plugins/crud/settings.models.ts @@ -1,30 +1,23 @@ -import { validation } from "@webiny/validation"; -/** - * Package @commodo/fields does not have types. - */ -// @ts-ignore -import { withFields, string, boolean, fields } from "@commodo/fields"; +import zod from "zod"; -export const CreateDataModel = withFields({ - domain: string(), - reCaptcha: fields({ - value: {}, - instanceOf: withFields({ - enabled: boolean(), - siteKey: string({ validation: validation.create("maxLength:100") }), - secretKey: string({ validation: validation.create("maxLength:100") }) - })() - }) -})(); +export const CreateDataModel = () => { + return zod.object({ + domain: zod.string(), + reCaptcha: zod.object({ + enabled: zod.boolean(), + siteKey: zod.string().max(100), + secretKey: zod.string().max(100) + }) + }); +}; -export const UpdateDataModel = withFields({ - domain: string(), - reCaptcha: fields({ - value: {}, - instanceOf: withFields({ - enabled: boolean(), - siteKey: string({ validation: validation.create("maxLength:100") }), - secretKey: string({ validation: validation.create("maxLength:100") }) - })() - }) -})(); +export const UpdateDataModel = () => { + return zod.object({ + domain: zod.string(), + reCaptcha: zod.object({ + enabled: zod.boolean(), + siteKey: zod.string().max(100), + secretKey: zod.string().max(100) + }) + }); +}; diff --git a/packages/api-form-builder/src/plugins/crud/submissions.crud.ts b/packages/api-form-builder/src/plugins/crud/submissions.crud.ts index 5d068794633..984ccc9e69b 100644 --- a/packages/api-form-builder/src/plugins/crud/submissions.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/submissions.crud.ts @@ -1,7 +1,6 @@ import fetch from "node-fetch"; import pick from "lodash/pick"; import WebinyError from "@webiny/error"; -import * as models from "~/plugins/crud/forms.models"; import { FbForm, FbFormTriggerHandlerPlugin, @@ -24,7 +23,7 @@ import { NotFoundError } from "@webiny/handler-graphql"; import { NotAuthorizedError } from "@webiny/api-security"; import { createTopic } from "@webiny/pubsub"; import { sanitizeFormSubmissionData } from "~/plugins/crud/utils/sanitizeFormSubmissionData"; -import { mdbid } from "@webiny/utils"; +import { mdbid, parseIdentifier } from "@webiny/utils"; import { FormsPermissions } from "~/plugins/crud/permissions/FormsPermissions"; interface CreateSubmissionsCrudParams { @@ -271,11 +270,9 @@ export const createSubmissionsCrud = (params: CreateSubmissionsCrudParams): Subm }; } - /** - * Use model for data validation and default values. - */ - const formFormId = form.formId || form.id.split("#").pop(); - const submissionModel = new models.FormSubmissionCreateDataModel().populate({ + const { id: formFormId } = parseIdentifier(form.id); + + const submissionModel = { data, meta, form: { @@ -286,12 +283,9 @@ export const createSubmissionsCrud = (params: CreateSubmissionsCrudParams): Subm fields: form.fields, steps: form.steps } - }); - - await submissionModel.validate(); + }; - const modelData: Pick = - await submissionModel.toJSON(); + const modelData: Pick = submissionModel; const submission: FbSubmission = { ...modelData, @@ -381,11 +375,6 @@ export const createSubmissionsCrud = (params: CreateSubmissionsCrudParams): Subm return submission; }, async updateSubmission(this: FormBuilder, formId, input) { - const data = await new models.FormSubmissionUpdateDataModel().populate(input); - data.validate(); - - const updatedData = await data.toJSON(); - const submissionId = input.id; const form = await this.getForm(formId, { @@ -403,7 +392,7 @@ export const createSubmissionsCrud = (params: CreateSubmissionsCrudParams): Subm const submission: FbSubmission = { ...original, tenant: form.tenant, - logs: updatedData.logs, + logs: input.logs, webinyVersion: context.WEBINY_VERSION }; @@ -414,7 +403,7 @@ export const createSubmissionsCrud = (params: CreateSubmissionsCrudParams): Subm submission }); await this.storageOperations.submissions.updateSubmission({ - input: updatedData, + input, form, original, submission @@ -430,7 +419,7 @@ export const createSubmissionsCrud = (params: CreateSubmissionsCrudParams): Subm ex.message || "Could not update form submission.", ex.code || "UPDATE_SUBMISSION_ERROR", { - input: updatedData, + input, original, submission, form: formId diff --git a/packages/api-form-builder/src/plugins/crud/utils/createFormSettings.ts b/packages/api-form-builder/src/plugins/crud/utils/createFormSettings.ts new file mode 100644 index 00000000000..89ae1732545 --- /dev/null +++ b/packages/api-form-builder/src/plugins/crud/utils/createFormSettings.ts @@ -0,0 +1,23 @@ +/** + * Creates default settings values for form. + */ +export const createFormSettings = () => { + return { + layout: { + renderer: "default" + }, + submitButtonLabel: null, + fullWidthSubmitButton: null, + successMessage: null, + termsOfServiceMessage: null, + reCaptcha: { + enabled: null, + errorMessage: "Please verify that you are not a robot.", + settings: { + enabled: null, + siteKey: null, + secretKey: null + } + } + }; +}; diff --git a/packages/api-form-builder/src/plugins/crud/utils/index.ts b/packages/api-form-builder/src/plugins/crud/utils/index.ts index b89863bcfd9..6e682217f5f 100644 --- a/packages/api-form-builder/src/plugins/crud/utils/index.ts +++ b/packages/api-form-builder/src/plugins/crud/utils/index.ts @@ -1,3 +1,4 @@ export * from "./flattenSubmissionMeta"; export * from "./getStatus"; export * from "./sanitizeFormSubmissionData"; +export * from "./createFormSettings"; diff --git a/packages/api-form-builder/src/plugins/graphql/formsSchema.ts b/packages/api-form-builder/src/plugins/graphql/formsSchema.ts index 6bf9e6007ff..bc7e5e87416 100644 --- a/packages/api-form-builder/src/plugins/graphql/formsSchema.ts +++ b/packages/api-form-builder/src/plugins/graphql/formsSchema.ts @@ -10,14 +10,14 @@ import { createFormsTypeDefs, CreateFormsTypeDefsParams } from "~/plugins/graphql/createFormsTypeDefs"; -import { FormBuilderContext } from "~/types"; +import { FbForm, FormBuilderContext } from "~/types"; export const createFormsSchema = (params: CreateFormsTypeDefsParams) => { const formsGraphQL = new GraphQLSchemaPlugin({ typeDefs: createFormsTypeDefs(params), resolvers: { FbForm: { - overallStats: async (form, _, { formBuilder }) => { + overallStats: async (form: FbForm, _, { formBuilder }) => { try { return await formBuilder.getFormStats(form.id); } catch (ex) { @@ -30,7 +30,7 @@ export const createFormsSchema = (params: CreateFormsTypeDefsParams) => { conversionRate: 0 }; }, - settings: async (form, _, { formBuilder }) => { + settings: async (form: FbForm, _, { formBuilder }) => { const settings = await formBuilder.getSettings({ auth: false }); return { diff --git a/packages/api-form-builder/src/types.ts b/packages/api-form-builder/src/types.ts index 1218af8b641..65f261401f2 100644 --- a/packages/api-form-builder/src/types.ts +++ b/packages/api-form-builder/src/types.ts @@ -2,6 +2,7 @@ import { Plugin } from "@webiny/plugins/types"; import { TenancyContext } from "@webiny/api-tenancy/types"; import { SecurityPermission } from "@webiny/api-security/types"; import { FileManagerContext } from "@webiny/api-file-manager/types"; +import { CmsEntryListWhere } from "@webiny/api-headless-cms/types"; import { I18NContext } from "@webiny/api-i18n/types"; import { Topic } from "@webiny/pubsub/types"; @@ -132,7 +133,7 @@ interface FormCreateInput { interface FormUpdateInput { name: string; - fields: Record[]; + fields: FbFormField[]; steps: FbFormStep[]; settings: Record; triggers: Record | null; @@ -356,7 +357,6 @@ export interface FbSubmission { name: string; version: number; fields: Record[]; - layout: string[][]; steps: FbFormStep[]; }; logs: Record[]; @@ -540,21 +540,16 @@ export interface FormBuilderStorageOperationsGetFormParams { }; } +export interface FormBuilderStorageOperationsListFormsWhereParams extends CmsEntryListWhere { + tenant: string; + locale: string; +} /** * @category StorageOperations * @category StorageOperationsParams */ export interface FormBuilderStorageOperationsListFormsParams { - where: { - id?: string; - version?: number; - slug?: string; - published?: boolean; - ownedBy?: string; - latest?: boolean; - tenant: string; - locale: string; - }; + where: FormBuilderStorageOperationsListFormsWhereParams; after: string | null; limit: number; sort: string[]; @@ -565,7 +560,6 @@ export interface FormBuilderStorageOperationsListFormsParams { * @category StorageOperationsParams */ export interface FormBuilderStorageOperationsListFormRevisionsParamsWhere { - id?: string; formId: string; version_not?: number; publishedOn_not?: string | null; @@ -600,7 +594,6 @@ export type FormBuilderStorageOperationsListFormsResponse = [ * @category StorageOperationsParams */ export interface FormBuilderStorageOperationsCreateFormParams { - input: Record; form: FbForm; } @@ -618,9 +611,6 @@ export interface FormBuilderStorageOperationsCreateFormFromParams { */ export interface FormBuilderStorageOperationsUpdateFormParams { form: FbForm; - input: Record; - meta?: Record; - options?: Record; } /** @@ -644,9 +634,7 @@ export interface FormBuilderStorageOperationsDeleteFormRevisionParams { * @category StorageOperationsParams */ export interface FormBuilderStorageOperationsPublishFormParams { - original: FbForm; form: FbForm; - input: Record; } /** @@ -654,9 +642,7 @@ export interface FormBuilderStorageOperationsPublishFormParams { * @category StorageOperationsParams */ export interface FormBuilderStorageOperationsUnpublishFormParams { - original: FbForm; form: FbForm; - input: Record; } /** @@ -760,8 +746,6 @@ export interface FormBuilderFormStorageOperations { ): Promise; publishForm(params: FormBuilderStorageOperationsPublishFormParams): Promise; unpublishForm(params: FormBuilderStorageOperationsUnpublishFormParams): Promise; - beforeInit?: (context: FormBuilderContext) => Promise; - init?: (context: FormBuilderContext) => Promise; } /** @@ -776,9 +760,6 @@ export interface FormBuilderStorageOperationsListSubmissionsResponse { * @category StorageOperations */ export interface FormBuilderSubmissionStorageOperations { - getSubmission( - params: FormBuilderStorageOperationsGetSubmissionParams - ): Promise; listSubmissions( params: FormBuilderStorageOperationsListSubmissionsParams ): Promise; @@ -789,8 +770,6 @@ export interface FormBuilderSubmissionStorageOperations { params: FormBuilderStorageOperationsUpdateSubmissionParams ): Promise; deleteSubmission(params: FormBuilderStorageOperationsDeleteSubmissionParams): Promise; - beforeInit?: (context: FormBuilderContext) => Promise; - init?: (context: FormBuilderContext) => Promise; } /** * @category StorageOperations @@ -800,6 +779,4 @@ export interface FormBuilderStorageOperations FormBuilderSettingsStorageOperations { forms: FormBuilderFormStorageOperations; submissions: FormBuilderSubmissionStorageOperations; - beforeInit?: (context: FormBuilderContext) => Promise; - init?: (context: FormBuilderContext) => Promise; } diff --git a/packages/api-headless-cms/src/crud/contentEntry.crud.ts b/packages/api-headless-cms/src/crud/contentEntry.crud.ts index 2da60da3429..5fd73b022ac 100644 --- a/packages/api-headless-cms/src/crud/contentEntry.crud.ts +++ b/packages/api-headless-cms/src/crud/contentEntry.crud.ts @@ -189,7 +189,7 @@ interface DeleteEntryParams { const createEntryId = (input: CreateCmsEntryInput) => { let entryId = mdbid(); if (input.id) { - if (input.id.match(/^([a-zA-Z0-9])([a-zA-Z0-9\-#]+)([a-zA-Z0-9])$/) === null) { + if (input.id.match(/^([a-zA-Z0-9])([a-zA-Z0-9\-]+)([a-zA-Z0-9])$/) === null) { throw new WebinyError( "The provided ID is not valid. It must be a string which can be A-Z, a-z, 0-9, - and it cannot start or end with a -.", "INVALID_ID", diff --git a/packages/app-form-builder/src/components/Form/FormRender.tsx b/packages/app-form-builder/src/components/Form/FormRender.tsx index 40d8421c3d9..45309e84d76 100644 --- a/packages/app-form-builder/src/components/Form/FormRender.tsx +++ b/packages/app-form-builder/src/components/Form/FormRender.tsx @@ -161,11 +161,6 @@ const FormRender: React.FC = props => { fields.forEach(field => { const fieldId = field.fieldId; - // Remove after form model fields id fix - if (!field.settings) { - return; - } - if ( fieldId && "defaultValue" in field.settings && diff --git a/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/index.ts b/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/index.ts index 1d027d69625..cdd3152ce2b 100644 --- a/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/index.ts +++ b/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/index.ts @@ -22,13 +22,10 @@ import elasticsearchClientContext, { } from "@webiny/api-elasticsearch"; import { createFileManagerContext, createFileManagerGraphQL } from "@webiny/api-file-manager"; import { createFileManagerStorageOperations } from "@webiny/api-file-manager-ddb"; -import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; -import { - createFormBuilderContext, - createFormBuilderGraphQL -} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; import logsPlugins from "@webiny/handler-logs"; import fileManagerS3 from "@webiny/api-file-manager-s3"; +import { createFormBuilder } from "@webiny/api-form-builder"; +import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createHeadlessCmsContext, createHeadlessCmsGraphQL } from "@webiny/api-headless-cms"; import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb-es"; import { createAco } from "@webiny/api-aco"; @@ -99,12 +96,11 @@ export const handler = createHandler({ pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), - createFormBuilderContext({ + createFormBuilder({ storageOperations: createFormBuilderStorageOperations({ documentClient }) }), - createFormBuilderGraphQL(), createGzipCompression(), createApwGraphQL(), createApwPageBuilderContext({ diff --git a/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/index.ts b/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/index.ts index 65c8ce44bc2..73bcb5608d5 100644 --- a/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/index.ts +++ b/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/index.ts @@ -20,10 +20,7 @@ import { createFileManagerContext, createFileManagerGraphQL } from "@webiny/api- import { createFileManagerStorageOperations } from "@webiny/api-file-manager-ddb"; import logsPlugins from "@webiny/handler-logs"; import fileManagerS3 from "@webiny/api-file-manager-s3"; -import { - createFormBuilderContext, - createFormBuilderGraphQL -} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; +import { createFormBuilder } from "@webiny/api-form-builder"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createHeadlessCmsContext, createHeadlessCmsGraphQL } from "@webiny/api-headless-cms"; import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb"; @@ -86,12 +83,11 @@ export const handler = createHandler({ pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), - createFormBuilderContext({ + createFormBuilder({ storageOperations: createFormBuilderStorageOperations({ documentClient }) }), - createFormBuilderGraphQL(), createApwGraphQL(), createApwPageBuilderContext({ storageOperations: createApwSaStorageOperations({ documentClient }) diff --git a/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/export/combine/src/index.ts b/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/export/combine/src/index.ts index 687c29119b9..7cfe8de0861 100644 --- a/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/export/combine/src/index.ts +++ b/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/export/combine/src/index.ts @@ -4,10 +4,7 @@ import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import { CmsParametersPlugin, createHeadlessCmsContext } from "@webiny/api-headless-cms"; import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb-es"; -import { - createFormBuilderContext, - createFormBuilderGraphQL -} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; +import { createFormBuilder } from "@webiny/api-form-builder"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb-es"; import { createPageBuilderGraphQL, @@ -67,13 +64,12 @@ export const handler = createHandler({ }) }), createPageBuilderGraphQL(), - createFormBuilderContext({ + createFormBuilder({ storageOperations: createFormBuilderStorageOperations({ documentClient, elasticsearch: elasticsearchClient }) }), - createFormBuilderGraphQL(), pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), diff --git a/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/export/process/src/index.ts b/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/export/process/src/index.ts index af8e1f9e90c..e28e9377f0e 100644 --- a/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/export/process/src/index.ts +++ b/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/export/process/src/index.ts @@ -2,7 +2,7 @@ import { getDocumentClient } from "@webiny/aws-sdk/client-dynamodb"; import { createHandler } from "@webiny/handler-aws/raw"; import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; -import { createFormBuilderContext } from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; +import { createFormBuilder } from "@webiny/api-form-builder"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb-es"; import { createPageBuilderContext } from "@webiny/api-page-builder/graphql"; import { createStorageOperations as createPageBuilderStorageOperations } from "@webiny/api-page-builder-so-ddb-es"; @@ -70,7 +70,7 @@ export const handler = createHandler({ elasticsearch: elasticsearchClient }) }), - createFormBuilderContext({ + createFormBuilder({ storageOperations: createFormBuilderStorageOperations({ documentClient, elasticsearch: elasticsearchClient diff --git a/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/import/create/src/index.ts b/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/import/create/src/index.ts index cba5a5f92f0..1f187cdca92 100644 --- a/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/import/create/src/index.ts +++ b/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/import/create/src/index.ts @@ -4,10 +4,7 @@ import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import { CmsParametersPlugin, createHeadlessCmsContext } from "@webiny/api-headless-cms"; import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb-es"; -import { - createFormBuilderContext, - createFormBuilderGraphQL -} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; +import { createFormBuilder } from "@webiny/api-form-builder"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb-es"; import { createPageBuilderGraphQL, @@ -69,13 +66,12 @@ export const handler = createHandler({ }) }), createPageBuilderGraphQL(), - createFormBuilderContext({ + createFormBuilder({ storageOperations: createFormBuilderStorageOperations({ documentClient, elasticsearch: elasticsearchClient }) }), - createFormBuilderGraphQL(), pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), diff --git a/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/import/process/src/index.ts b/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/import/process/src/index.ts index d683394a312..1b290e373d6 100644 --- a/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/import/process/src/index.ts +++ b/packages/serverless-cms-aws/handlers/ddb-es/api/pageBuilder/import/process/src/index.ts @@ -2,7 +2,7 @@ import { getDocumentClient } from "@webiny/aws-sdk/client-dynamodb"; import { createHandler } from "@webiny/handler-aws/raw"; import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; -import { createFormBuilderContext } from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; +import { createFormBuilder } from "@webiny/api-form-builder"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb-es"; import { createPageBuilderContext } from "@webiny/api-page-builder/graphql"; import { createStorageOperations as createPageBuilderStorageOperations } from "@webiny/api-page-builder-so-ddb-es"; @@ -74,7 +74,7 @@ export const handler = createHandler({ elasticsearch: elasticsearchClient }) }), - createFormBuilderContext({ + createFormBuilder({ storageOperations: createFormBuilderStorageOperations({ documentClient, elasticsearch: elasticsearchClient diff --git a/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/export/combine/src/index.ts b/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/export/combine/src/index.ts index 581a7dcd8a1..475c0d3cfb4 100644 --- a/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/export/combine/src/index.ts +++ b/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/export/combine/src/index.ts @@ -5,10 +5,7 @@ import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import i18nContentPlugins from "@webiny/api-i18n-content/plugins"; import { CmsParametersPlugin, createHeadlessCmsContext } from "@webiny/api-headless-cms"; import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb"; -import { - createFormBuilderContext, - createFormBuilderGraphQL -} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; +import { createFormBuilder } from "@webiny/api-form-builder"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createPageBuilderGraphQL, @@ -60,12 +57,11 @@ export const handler = createHandler({ }) }), createPageBuilderGraphQL(), - createFormBuilderContext({ + createFormBuilder({ storageOperations: createFormBuilderStorageOperations({ documentClient }) }), - createFormBuilderGraphQL(), pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), diff --git a/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/export/process/src/index.ts b/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/export/process/src/index.ts index eb9291a8fb0..160d709ff5d 100644 --- a/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/export/process/src/index.ts +++ b/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/export/process/src/index.ts @@ -3,7 +3,7 @@ import { createHandler } from "@webiny/handler-aws/raw"; import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import i18nContentPlugins from "@webiny/api-i18n-content/plugins"; -import { createFormBuilderContext } from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; +import { createFormBuilder } from "@webiny/api-form-builder"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createPageBuilderContext } from "@webiny/api-page-builder/graphql"; import { createStorageOperations as createPageBuilderStorageOperations } from "@webiny/api-page-builder-so-ddb"; @@ -60,7 +60,7 @@ export const handler = createHandler({ documentClient }) }), - createFormBuilderContext({ + createFormBuilder({ storageOperations: createFormBuilderStorageOperations({ documentClient }) diff --git a/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/import/create/src/index.ts b/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/import/create/src/index.ts index 3288a07c69f..7959e811f0b 100644 --- a/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/import/create/src/index.ts +++ b/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/import/create/src/index.ts @@ -5,10 +5,7 @@ import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import i18nContentPlugins from "@webiny/api-i18n-content/plugins"; import { CmsParametersPlugin, createHeadlessCmsContext } from "@webiny/api-headless-cms"; import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb"; -import { - createFormBuilderContext, - createFormBuilderGraphQL -} from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; +import { createFormBuilder } from "@webiny/api-form-builder"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createPageBuilderGraphQL, @@ -58,12 +55,11 @@ export const handler = createHandler({ }) }), createPageBuilderGraphQL(), - createFormBuilderContext({ + createFormBuilder({ storageOperations: createFormBuilderStorageOperations({ documentClient }) }), - createFormBuilderGraphQL(), pageBuilderImportExportPlugins({ storageOperations: createPageBuilderImportExportStorageOperations({ documentClient }) }), diff --git a/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/import/process/src/index.ts b/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/import/process/src/index.ts index 72444840e89..954a1970eb3 100644 --- a/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/import/process/src/index.ts +++ b/packages/serverless-cms-aws/handlers/ddb/api/pageBuilder/import/process/src/index.ts @@ -3,7 +3,7 @@ import { createHandler } from "@webiny/handler-aws/raw"; import i18nPlugins from "@webiny/api-i18n/graphql"; import i18nDynamoDbStorageOperations from "@webiny/api-i18n-ddb"; import i18nContentPlugins from "@webiny/api-i18n-content/plugins"; -import { createFormBuilderContext } from "@webiny/api-form-builder/cmsFormBuilderStorage/createFormBuilderContext"; +import { createFormBuilder } from "@webiny/api-form-builder"; import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; import { createPageBuilderContext } from "@webiny/api-page-builder/graphql"; import { createStorageOperations as createPageBuilderStorageOperations } from "@webiny/api-page-builder-so-ddb"; @@ -62,7 +62,7 @@ export const handler = createHandler({ documentClient }) }), - createFormBuilderContext({ + createFormBuilder({ storageOperations: createFormBuilderStorageOperations({ documentClient }) From dd4e2b240806bcf1ee87cc974d84ef2c771138a6 Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Thu, 23 Nov 2023 13:04:26 +0000 Subject: [PATCH 17/37] fix: fixed issue with packages --- packages/api-form-builder-so-ddb-es/package.json | 1 - packages/api-form-builder-so-ddb-es/tsconfig.build.json | 1 - packages/api-form-builder-so-ddb-es/tsconfig.json | 3 --- packages/api-form-builder-so-ddb/package.json | 4 +--- packages/api-form-builder-so-ddb/tsconfig.build.json | 2 -- packages/api-form-builder-so-ddb/tsconfig.json | 6 ------ packages/api-form-builder/package.json | 5 ++--- yarn.lock | 6 +----- 8 files changed, 4 insertions(+), 24 deletions(-) diff --git a/packages/api-form-builder-so-ddb-es/package.json b/packages/api-form-builder-so-ddb-es/package.json index e5b2c0e153c..fd7802abaa5 100644 --- a/packages/api-form-builder-so-ddb-es/package.json +++ b/packages/api-form-builder-so-ddb-es/package.json @@ -30,7 +30,6 @@ "@webiny/db-dynamodb": "0.0.0", "@webiny/error": "0.0.0", "@webiny/plugins": "0.0.0", - "@webiny/utils": "0.0.0", "elastic-ts": "^0.8.0" }, "devDependencies": { diff --git a/packages/api-form-builder-so-ddb-es/tsconfig.build.json b/packages/api-form-builder-so-ddb-es/tsconfig.build.json index 43a13221c2c..ed29f175a16 100644 --- a/packages/api-form-builder-so-ddb-es/tsconfig.build.json +++ b/packages/api-form-builder-so-ddb-es/tsconfig.build.json @@ -8,7 +8,6 @@ { "path": "../db-dynamodb/tsconfig.build.json" }, { "path": "../error/tsconfig.build.json" }, { "path": "../plugins/tsconfig.build.json" }, - { "path": "../utils/tsconfig.build.json" }, { "path": "../api-dynamodb-to-elasticsearch/tsconfig.build.json" }, { "path": "../handler-aws/tsconfig.build.json" }, { "path": "../handler-db/tsconfig.build.json" } diff --git a/packages/api-form-builder-so-ddb-es/tsconfig.json b/packages/api-form-builder-so-ddb-es/tsconfig.json index dac411a6981..891a76ac1aa 100644 --- a/packages/api-form-builder-so-ddb-es/tsconfig.json +++ b/packages/api-form-builder-so-ddb-es/tsconfig.json @@ -8,7 +8,6 @@ { "path": "../db-dynamodb" }, { "path": "../error" }, { "path": "../plugins" }, - { "path": "../utils" }, { "path": "../api-dynamodb-to-elasticsearch" }, { "path": "../handler-aws" }, { "path": "../handler-db" } @@ -32,8 +31,6 @@ "@webiny/error": ["../error/src"], "@webiny/plugins/*": ["../plugins/src/*"], "@webiny/plugins": ["../plugins/src"], - "@webiny/utils/*": ["../utils/src/*"], - "@webiny/utils": ["../utils/src"], "@webiny/api-dynamodb-to-elasticsearch/*": ["../api-dynamodb-to-elasticsearch/src/*"], "@webiny/api-dynamodb-to-elasticsearch": ["../api-dynamodb-to-elasticsearch/src"], "@webiny/handler-aws/*": ["../handler-aws/src/*"], diff --git a/packages/api-form-builder-so-ddb/package.json b/packages/api-form-builder-so-ddb/package.json index 54a3d1483cd..2ee7539d4f0 100644 --- a/packages/api-form-builder-so-ddb/package.json +++ b/packages/api-form-builder-so-ddb/package.json @@ -25,9 +25,7 @@ "@webiny/api-form-builder": "0.0.0", "@webiny/aws-sdk": "0.0.0", "@webiny/db-dynamodb": "0.0.0", - "@webiny/error": "0.0.0", - "@webiny/plugins": "0.0.0", - "@webiny/utils": "0.0.0" + "@webiny/error": "0.0.0" }, "devDependencies": { "@babel/cli": "^7.22.6", diff --git a/packages/api-form-builder-so-ddb/tsconfig.build.json b/packages/api-form-builder-so-ddb/tsconfig.build.json index ac484fb0fbf..c49df1e6cef 100644 --- a/packages/api-form-builder-so-ddb/tsconfig.build.json +++ b/packages/api-form-builder-so-ddb/tsconfig.build.json @@ -6,8 +6,6 @@ { "path": "../aws-sdk/tsconfig.build.json" }, { "path": "../db-dynamodb/tsconfig.build.json" }, { "path": "../error/tsconfig.build.json" }, - { "path": "../plugins/tsconfig.build.json" }, - { "path": "../utils/tsconfig.build.json" }, { "path": "../handler-db/tsconfig.build.json" } ], "compilerOptions": { diff --git a/packages/api-form-builder-so-ddb/tsconfig.json b/packages/api-form-builder-so-ddb/tsconfig.json index 2f22c781d6b..3fbdf7cfcdb 100644 --- a/packages/api-form-builder-so-ddb/tsconfig.json +++ b/packages/api-form-builder-so-ddb/tsconfig.json @@ -6,8 +6,6 @@ { "path": "../aws-sdk" }, { "path": "../db-dynamodb" }, { "path": "../error" }, - { "path": "../plugins" }, - { "path": "../utils" }, { "path": "../handler-db" } ], "compilerOptions": { @@ -25,10 +23,6 @@ "@webiny/db-dynamodb": ["../db-dynamodb/src"], "@webiny/error/*": ["../error/src/*"], "@webiny/error": ["../error/src"], - "@webiny/plugins/*": ["../plugins/src/*"], - "@webiny/plugins": ["../plugins/src"], - "@webiny/utils/*": ["../utils/src/*"], - "@webiny/utils": ["../utils/src"], "@webiny/handler-db/*": ["../handler-db/src/*"], "@webiny/handler-db": ["../handler-db/src"] }, diff --git a/packages/api-form-builder/package.json b/packages/api-form-builder/package.json index 7552c664e1f..e66884afb8f 100644 --- a/packages/api-form-builder/package.json +++ b/packages/api-form-builder/package.json @@ -18,7 +18,6 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.22.6", - "@commodo/fields": "1.1.2-beta.20", "@webiny/api": "0.0.0", "@webiny/api-file-manager": "0.0.0", "@webiny/api-i18n": "0.0.0", @@ -34,13 +33,13 @@ "@webiny/pubsub": "0.0.0", "@webiny/utils": "0.0.0", "@webiny/validation": "0.0.0", - "commodo-fields-object": "^1.0.6", "date-fns": "^2.22.1", "got": "^9.6.0", "json2csv": "^4.5.2", "lodash": "^4.17.11", "node-fetch": "^2.6.1", - "slugify": "^1.2.9" + "slugify": "^1.2.9", + "zod": "^3.21.4" }, "devDependencies": { "@babel/cli": "^7.22.6", diff --git a/yarn.lock b/yarn.lock index 4d46639e47c..a5e645933f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14119,7 +14119,6 @@ __metadata: "@webiny/handler-db": 0.0.0 "@webiny/plugins": 0.0.0 "@webiny/project-utils": 0.0.0 - "@webiny/utils": 0.0.0 csvtojson: ^2.0.10 elastic-ts: ^0.8.0 jest: ^29.5.0 @@ -14145,9 +14144,7 @@ __metadata: "@webiny/db-dynamodb": 0.0.0 "@webiny/error": 0.0.0 "@webiny/handler-db": 0.0.0 - "@webiny/plugins": 0.0.0 "@webiny/project-utils": 0.0.0 - "@webiny/utils": 0.0.0 csvtojson: ^2.0.10 jest: ^29.5.0 jest-dynalite: ^3.2.0 @@ -14166,7 +14163,6 @@ __metadata: "@babel/preset-env": ^7.22.7 "@babel/preset-typescript": ^7.22.5 "@babel/runtime": ^7.22.6 - "@commodo/fields": 1.1.2-beta.20 "@types/got": ^9.6.12 "@types/json2csv": ^4.5.1 "@types/node-fetch": ^2.6.1 @@ -14189,7 +14185,6 @@ __metadata: "@webiny/pubsub": 0.0.0 "@webiny/utils": 0.0.0 "@webiny/validation": 0.0.0 - commodo-fields-object: ^1.0.6 csvtojson: ^2.0.10 date-fns: ^2.22.1 fs-extra: ^9.1.0 @@ -14203,6 +14198,7 @@ __metadata: slugify: ^1.2.9 ttypescript: ^1.5.12 typescript: 4.7.4 + zod: ^3.21.4 languageName: unknown linkType: soft From 725f6732b7061e72eec8b6a3edfe3cfabb8639ae Mon Sep 17 00:00:00 2001 From: Vitalii Nobis Date: Mon, 27 Nov 2023 10:28:19 +0000 Subject: [PATCH 18/37] fix: merged with next, resolved conflicts and type issues --- .adiorc.js | 24 +- .eslintrc.js | 11 +- .github/workflows/cleanup/aws-nuke.yml | 2 + .github/workflows/pullRequests.yml | 23 +- .../pullRequestsCommandCypressTest.yml | 532 ------------------ .github/workflows/versionApproval.yml | 90 +++ .github/workflows/wac/pullRequests.wac.ts | 20 +- apps/admin/src/App.fm.tsx | 4 +- .../src/plugins/formBuilder/richTextEditor.ts | 8 +- .../src/plugins/headlessCMS/richTextEditor.ts | 8 +- .../src/plugins/pageBuilder/richTextEditor.ts | 8 +- apps/admin/src/plugins/scaffolds/index.ts | 2 +- apps/api/graphql/src/index.ts | 3 +- .../__tests__/handler/index.ts | 1 - .../__tests__/snapshots/customAppsSchema.ts | 7 + .../__tests__/snapshots/defaultAppsSchema.ts | 7 + .../api-admin-users/src/createAdminUsers.ts | 4 +- .../src/createAdminUsers/users.validation.ts | 4 +- .../api-admin-users/src/graphql/user.gql.ts | 6 +- packages/api-admin-users/src/index.ts | 2 +- .../scheduleAction/crud.cms_entry.test.ts | 2 +- .../scheduleAction/crud.page.test.ts | 2 +- .../__tests__/utils/createGraphQlHandler.ts | 6 +- .../scheduler/createScheduleActionMethods.ts | 2 +- .../changeRequestStorageOperations.ts | 3 +- .../commentStorageOperations.ts | 3 +- .../contentReviewStorageOperations.ts | 3 +- .../reviewerStorageOperations.ts | 3 +- .../workflowStorageOperations.ts | 3 +- .../__tests__/helpers/handlerCore.ts | 1 - .../src/utils/mimeTypes.ts | 4 +- .../__tests__/file.customDates.test.ts | 58 ++ .../__tests__/file.customIdentities.test.ts | 107 ++++ .../__tests__/file.lifecycle.test.ts | 10 +- .../__tests__/mocks/file.sdl.ts | 22 + .../__tests__/utils/tenancySecurity.ts | 14 +- .../__tests__/utils/useGqlHandler.ts | 8 +- .../src/cmsFileStorage/CmsFilesStorage.ts | 10 +- .../src/createFileManager/files.crud.ts | 18 +- .../filevalidation.disabled.ts | 54 -- .../src/createFileManager/settings.crud.ts | 2 +- .../src/graphql/baseSchema.ts | 1 + .../src/graphql/createFilesTypeDefs.ts | 17 +- .../src/graphql/filesSchema.ts | 14 +- .../src/modelModifier/CmsModelModifier.ts | 7 +- packages/api-file-manager/src/types.ts | 7 +- packages/api-file-manager/src/types/file.ts | 1 + .../cmsFormBuilderStorage/CmsFormsStorage.ts | 2 +- .../src/plugins/crud/forms.crud.ts | 4 +- packages/api-form-builder/src/types.ts | 2 +- .../plugins/elasticsearchSortModifier.test.ts | 3 - .../src/helpers/entryIndexHelpers.ts | 10 +- .../elasticsearch/filtering/populated.ts | 2 +- .../operations/entry/filtering/filter.test.ts | 2 +- .../__tests__/contentAPI/aco/setup/plugins.ts | 1 - .../contentAPI/contentEntry.withId.test.ts | 109 +--- .../contentEntryCustomDates.test.ts | 175 ++++++ .../contentEntryCustomIdentities.test.s.ts | 138 +++++ .../contentAPI/contentEntryMetaField.test.ts | 39 +- .../contentAPI/entryPagination.test.ts | 22 +- .../__tests__/contentAPI/filtering.test.ts | 2 +- .../resolvers.apiKey.manage.test.ts | 1 - .../contentAPI/resolvers.manage.test.ts | 2 +- .../contentAPI/resolvers.read.test.ts | 10 +- .../contentAPI/snapshots/category.manage.ts | 17 +- .../contentAPI/snapshots/category.read.ts | 7 + .../contentAPI/snapshots/page.manage.ts | 17 +- .../contentAPI/snapshots/page.read.ts | 7 + .../contentAPI/snapshots/product.manage.ts | 17 +- .../contentAPI/snapshots/product.read.ts | 7 + .../contentAPI/snapshots/review.manage.ts | 17 +- .../contentAPI/snapshots/review.read.ts | 7 + .../mocks/fieldIdStorageConverter.ts | 2 + .../storageOperations/entries.test.ts | 34 +- .../fieldUniqueValues.test.ts | 13 +- .../__tests__/storageOperations/helpers.ts | 34 -- .../__tests__/testHelpers/helpers.ts | 3 - .../__tests__/testHelpers/plugins.ts | 1 - .../__tests__/testHelpers/tenancySecurity.ts | 14 +- .../testHelpers/useCategoryManageHandler.ts | 18 +- .../testHelpers/useGraphQLHandler.ts | 14 +- .../__tests__/testHelpers/useHandler.ts | 2 + .../src/crud/contentEntry.crud.ts | 132 +++-- .../src/graphql/schema/baseSchema.ts | 13 + .../graphql/schema/createFieldResolvers.ts | 2 +- .../src/graphql/schema/createManageSDL.ts | 10 +- .../schema/resolvers/manage/resolvePublish.ts | 7 +- .../api-headless-cms/src/graphqlFields/ref.ts | 2 +- .../src/plugins/CmsModelPlugin.ts | 2 +- packages/api-headless-cms/src/types.ts | 43 +- packages/api-headless-cms/src/utils/date.ts | 29 + .../src/utils/renderListFilterFields.ts | 7 + .../locales/LocalesStorageOperations.ts | 2 +- packages/api-i18n/__tests__/useGqlHandler.ts | 3 - .../graphql/resolvers/searchLocaleCodes.ts | 2 +- packages/api-mailer/src/graphql/settings.ts | 4 +- .../src/transports/createSmtpTransport.ts | 2 +- .../src/utils/PageBuilderCrudDecorators.ts | 3 +- .../src/client.ts | 3 +- .../graphql/crud/importExportTasks.crud.ts | 4 +- .../utils/extractAndUploadZipFileContents.ts | 2 +- .../__tests__/useHandler.ts | 4 - .../src/operations/pageTemplate/index.ts | 13 +- .../src/operations/pageTemplate/index.ts | 13 +- .../__tests__/graphql/pageFullUrl.test.ts | 5 +- .../__tests__/graphql/pages.deletion.test.ts | 6 +- .../__tests__/graphql/pages.test.ts | 10 +- .../graphql/pagesGetPublished.test.ts | 7 +- .../graphql/pagesListingLatest.test.ts | 14 +- .../__tests__/graphql/simple.pages.test.ts | 26 - .../__tests__/graphql/utils/waitPage.ts | 24 - .../graphql/crud/menus/prepareMenuItems.ts | 27 +- .../src/graphql/crud/pageTemplates.crud.ts | 2 +- .../src/graphql/crud/system.crud.ts | 4 +- .../api-page-builder/src/graphql/types.ts | 23 +- .../src/installation/createInstallationZip.ts | 2 +- .../handlers/render/linkPreloading.test.ts | 6 +- .../render/handlers/render/renderUrl.test.ts | 6 +- .../src/render/renderUrl.ts | 6 +- .../src/createAdminUsersHooks.ts | 6 +- .../src/createAuthenticator.ts | 2 +- .../__tests__/graphql/parallelQueries.ts | 49 ++ .../api-security/__tests__/identity.test.ts | 2 +- packages/api-security/__tests__/login.test.ts | 2 +- .../__tests__/mocks/customAuthenticator.ts | 6 +- .../__tests__/parallelQueries.test.ts | 31 + .../aacl/customPermissionsFiltering.test.ts | 3 +- .../wcp/aacl/mocks/customAuthenticator.ts | 4 +- .../wcp/aacl/mocks/customAuthorizer.ts | 3 +- .../__tests__/withoutAuthorization.test.ts | 67 +-- packages/api-security/src/createSecurity.ts | 41 +- .../createSecurity/createApiKeysMethods.ts | 4 +- .../src/createSecurity/createGroupsMethods.ts | 6 +- .../src/createSecurity/createTeamsMethods.ts | 4 +- .../src/plugins/tenantLinkAuthorization.ts | 2 +- packages/api-security/src/types.ts | 16 - packages/api/src/Context.ts | 2 +- .../QueryBuilderDrawer/QueryBuilderDrawer.tsx | 2 +- packages/app-admin-cognito/src/index.tsx | 2 +- .../app-admin-okta/src/OktaSignInWidget.tsx | 2 +- .../app-admin-rmwc/src/modules/Layout.tsx | 14 +- .../src/modules/Overlays/OmniSearch.tsx | 6 +- .../src/modules/Overlays/index.tsx | 4 - .../createAuthentication.tsx | 2 +- .../src/base/providers/TelemetryProvider.tsx | 4 - .../src/components/MultiImageUpload.tsx | 4 +- packages/app-admin/src/hooks/index.ts | 1 + packages/app-admin/src/hooks/useIsMounted.ts | 17 + packages/app-admin/src/hooks/useShiftKey.ts | 1 - .../src/plugins/globalSearch/SearchBar.tsx | 5 +- .../src/plugins/globalSearch/styled.ts | 3 +- .../src/ui/elements/AccordionElement.tsx | 2 +- .../src/ui/elements/ButtonElement.tsx | 2 +- .../form/FileManagerElement/styled.ts | 5 +- .../src/ui/elements/form/FormFieldElement.tsx | 2 +- .../ChangeRequest/ApwFile.tsx | 2 +- .../components/WorkflowTitle.tsx | 2 +- .../src/MultiPartUploadStrategy.ts | 4 +- packages/app-file-manager-s3/src/index.ts | 2 +- packages/app-file-manager/jest.config.js | 5 + packages/app-file-manager/package.json | 3 +- .../ActionEdit/ActionEdit.styled.tsx | 36 ++ .../BulkActions/ActionEdit/ActionEdit.tsx | 134 +++++ .../ActionEdit/ActionEdit.types.ts | 3 + .../ActionEdit/ActionEditPresenter.test.ts | 187 ++++++ .../ActionEdit/ActionEditPresenter.ts | 79 +++ .../BatchEditorDialog/AddOperation.tsx | 23 + .../BatchEditorDialog/BatchEditor.tsx | 80 +++ .../BatchEditorDialog/BatchEditorDialog.tsx | 74 +++ .../BatchEditorDialogPresenter.test.ts | 400 +++++++++++++ .../BatchEditorDialogPresenter.tsx | 224 ++++++++ .../BatchEditorDialog/FieldRenderer.tsx | 49 ++ .../BatchEditorDialog/Operation.tsx | 56 ++ .../BatchEditorDialog/RemoveOperation.tsx | 23 + .../ActionEdit/BatchEditorDialog/index.tsx | 1 + .../ActionEdit/GraphQLInputMapper.test.ts | 70 +++ .../ActionEdit/GraphQLInputMapper.ts | 42 ++ .../BulkActions/ActionEdit/domain/Batch.ts | 48 ++ .../ActionEdit/domain/BatchMapper.ts | 13 + .../BulkActions/ActionEdit/domain/Field.ts | 78 +++ .../ActionEdit/domain/FieldMapper.ts | 23 + .../BulkActions/ActionEdit/domain/index.ts | 4 + .../BulkActions/ActionEdit/index.tsx | 1 + .../src/components/BulkActions/index.tsx | 1 + .../components/FileDetails/FileDetails.tsx | 2 +- .../FileDetails/components/BaseFields.tsx | 2 +- .../FileDetails/components/Extensions.tsx | 2 +- .../src/components/Grid/File.tsx | 2 +- .../FileManagerView/FileManagerView.tsx | 2 +- .../src/modules/FileManagerRenderer/index.tsx | 3 +- .../FileTypes/fileImage/EditAction.tsx | 4 +- .../Context/useFormEditorFactory.tsx | 44 +- .../components/FormEditor/DragPreview.tsx | 4 +- .../admin/components/FormEditor/Draggable.tsx | 5 - .../FormEditor/DropZone/Horizontal.tsx | 4 +- .../FormEditor/DropZone/Vertical.tsx | 4 +- .../components/FormEditor/FormEditorApp.tsx | 2 - .../EditTab/EditFieldDialog/ValidatorsTab.tsx | 204 ++++--- .../plugins/editor/defaultBar/Name/Name.tsx | 2 +- .../editor/formFieldValidators/pattern.tsx | 2 - .../formFields/components/OptionsList.tsx | 2 +- .../EditFieldOptionDialog.tsx | 2 +- .../components/GeneralSettings.tsx | 5 - .../components/TermsOfServiceSettings.tsx | 11 +- .../createTermsOfServiceComponent.tsx | 2 +- packages/app-form-builder/src/types.ts | 35 +- .../src/plugins/Playground.tsx | 6 +- .../src/plugins/index.tsx | 2 +- .../admin/components/DropZone/Horizontal.tsx | 4 +- .../admin/components/DropZone/Vertical.tsx | 4 +- .../FieldEditor/FieldEditorContext.tsx | 5 +- .../plugins/editor/defaultBar/Name/Name.tsx | 2 +- .../dateTime/DateTimeWithTimezone.tsx | 2 - .../plugins/fieldRenderers/dateTime/utils.tsx | 2 - .../richText/richTextInputs.tsx | 5 - .../src/admin/plugins/icons.tsx | 1 - .../src/admin/views/contentModels/cache.ts | 14 +- .../src/views/settings/Settings.tsx | 2 +- .../src/components/Editor/Editor.tsx | 2 +- .../src/components/RecoilExternal.ts | 4 +- .../src/contexts/EditorState.tsx | 14 +- .../app-page-builder-editor/src/index.tsx | 6 +- .../src/components/Elements.tsx | 1 - .../src/contexts/Renderer.tsx | 2 +- .../attributes/animation/initializeAos.ts | 2 +- .../renderers/embeds/components/OEmbed.tsx | 2 +- .../src/renderers/embeds/pinterest.tsx | 2 - .../src/renderers/embeds/twitter.tsx | 2 +- .../createTermsOfServiceComponent.tsx | 2 +- .../defaultImagesListComponent.tsx | 2 +- .../src/renderers/paragraph.tsx | 2 +- .../PageBlocks/BlocksGateway.ts | 18 +- .../PageBlocks/BlocksRepository.ts | 87 +-- .../AdminPageBuilder/PageBlocks/Loading.ts | 4 +- .../PageBlocks/usePageBlocks.ts | 7 +- .../src/admin/plugins/icons/index.tsx | 5 +- .../src/admin/utils/createElementPlugin.tsx | 4 +- .../views/Categories/CategoriesDialog.tsx | 2 +- .../MenusForm/MenuItems/MenuItemRenderer.tsx | 2 +- .../MenusForm/MenuItems/MenuItemsList.tsx | 2 +- .../admin/views/Pages/PageTemplatesDialog.tsx | 2 +- .../src/admin/views/Pages/cache.ts | 2 +- .../config/editorBar/Title/Title.tsx | 2 +- .../editor/components/Editor/DragPreview.tsx | 2 +- .../editor/components/MediumEditor/index.ts | 2 +- .../ElementControlsOverlay.tsx | 2 +- .../ElementControlsOverlayBorders.tsx | 2 +- .../contexts/EventActionHandlerProvider.tsx | 4 +- .../app-page-builder/src/editor/helpers.ts | 18 +- .../src/editor/hooks/useRefreshBlock.ts | 74 ++- .../elementSettings/components/Action.tsx | 3 + .../variable/MultipleImageVariableInput.tsx | 2 +- .../plugins/elements/accordion/index.tsx | 2 +- .../plugins/elements/carousel/index.tsx | 2 +- .../elements/code/codesandbox/index.tsx | 2 +- .../elements/embeds/soundcloud/index.tsx | 4 +- .../plugins/elements/embeds/vimeo/index.tsx | 4 +- .../plugins/elements/embeds/youtube/index.tsx | 4 +- .../editor/plugins/elements/grid/PeGrid.tsx | 4 + .../editor/plugins/elements/grid/index.tsx | 2 +- .../editor/plugins/elements/heading/index.tsx | 2 +- .../imagesList/ImagesListImagesSettings.tsx | 2 +- .../elements/paragraph/PeParagraph.tsx | 2 +- .../plugins/elements/paragraph/index.tsx | 2 +- .../elements/social/instagram/index.tsx | 2 +- .../elements/social/pinterest/index.tsx | 4 +- .../plugins/elements/social/twitter/index.tsx | 4 +- .../editor/plugins/elements/tabs/index.tsx | 2 +- .../WebsiteSettings/usePbWebsiteSettings.ts | 4 - .../ElementSettingsTabContentPlugin.tsx | 5 +- .../config/editorBar/Title/Title.tsx | 2 +- .../src/pageEditor/helpers.ts | 6 +- .../src/render/components/OEmbed.tsx | 2 +- .../elements/embeds/instagram/index.tsx | 2 +- .../elements/imagesList/components/Slider.tsx | 42 -- .../ElementSettingsTabContentPlugin.tsx | 5 +- .../config/editorBar/Title/Title.tsx | 2 +- .../src/index.tsx | 3 +- .../src/utils/createApolloClient.ts | 4 +- .../app/src/apollo-client/InMemoryCache.ts | 2 +- packages/app/src/plugins/index.tsx | 2 +- .../src/githubActions/index.ts | 2 +- .../essentials/code/webiny.config.ts | 4 +- .../src/index.ts | 2 +- .../graphql/src/plugins/scaffolds/index.ts | 2 - .../src/index.ts | 2 +- packages/commodo/src/compose.ts | 2 +- packages/commodo/src/fields-date.ts | 2 +- packages/commodo/src/fields-float.ts | 2 +- packages/commodo/src/fields-int.ts | 2 +- packages/commodo/src/fields-object.ts | 2 +- packages/commodo/src/fields.ts | 2 +- packages/commodo/src/hooks.ts | 2 +- packages/commodo/src/name.ts | 2 +- packages/commodo/src/pipe.ts | 2 +- packages/commodo/src/repropose.ts | 2 +- .../src/plugins/formBuilder/richTextEditor.ts | 8 +- .../src/plugins/headlessCMS/richTextEditor.ts | 8 +- .../src/plugins/pageBuilder/richTextEditor.ts | 8 +- .../apps/admin/src/plugins/scaffolds/index.ts | 1 - .../graphql/src/plugins/scaffolds/index.ts | 1 - .../graphql/src/plugins/scaffolds/index.ts | 1 - packages/db-dynamodb/src/BatchProcess.ts | 225 -------- packages/db-dynamodb/src/DynamoDbDriver.ts | 3 +- packages/db-dynamodb/src/QueryGenerator.ts | 61 -- .../src/operators/comparison/beginsWith.ts | 14 - .../src/operators/comparison/between.ts | 21 - .../src/operators/comparison/eq.ts | 24 - .../src/operators/comparison/gt.ts | 14 - .../src/operators/comparison/gte.ts | 14 - .../src/operators/comparison/lt.ts | 14 - .../src/operators/comparison/lte.ts | 14 - packages/db-dynamodb/src/operators/index.ts | 21 - .../db-dynamodb/src/operators/logical/and.ts | 58 -- .../db-dynamodb/src/operators/logical/or.ts | 58 -- .../createKeyConditionExpressionArgs.ts | 47 -- .../src/statements/processStatement.ts | 17 - packages/db/src/index.ts | 2 +- packages/form/src/Form.tsx | 2 +- .../src/builtInTypes/RefInputScalar.ts | 2 +- .../src/createGraphQLHandler.ts | 8 +- .../src/createGraphQLSchema.ts | 4 +- .../handler-graphql/src/interceptConsole.ts | 10 +- packages/handler/src/Context.ts | 6 +- packages/i18n/src/I18n.ts | 2 +- packages/i18n/src/extractor/extract.ts | 2 +- .../ioc/__tests__/useCases/models.test.ts | 6 +- .../__tests__/setup/setupEnv.ts | 2 +- .../__tests__/utils/toDom.ts | 2 +- packages/lexical-nodes/src/ImageNode.tsx | 5 +- packages/lexical-nodes/src/ListNode.ts | 3 +- .../src/utils/styleObjectToString.ts | 2 +- .../migrations/5.36.0/001/ddb/001.test.ts | 2 +- .../testing/elasticsearch/client.ts | 8 +- .../elasticsearch/getElasticsearchClient.ts | 2 +- .../project-utils/testing/presets/index.js | 2 +- packages/pubsub/src/index.ts | 4 +- .../pulumi-aws/src/apps/api/ApiFileManager.ts | 2 +- .../pulumi-aws/src/apps/api/ApiPageBuilder.ts | 2 - .../src/apps/website/WebsitePrerendering.ts | 2 +- .../src/utils/lambdaEnvVariables.ts | 2 +- .../__tests__/cases/app-config/Module.tsx | 5 +- .../react-properties/__tests__/setupEnv.ts | 3 +- .../src/context/RouterContext.tsx | 2 +- .../storybook-utils/src/CodeBlock/index.tsx | 2 +- .../storybook-utils/src/Markdown/index.tsx | 2 +- packages/telemetry/react.d.ts | 2 + packages/ui/src/AutoComplete/AutoComplete.tsx | 3 +- .../ui/src/AutoComplete/MultiAutoComplete.tsx | 4 +- packages/ui/src/Checkbox/Checkbox.tsx | 1 - packages/ui/src/ColorPicker/ColorPicker.tsx | 2 +- packages/ui/src/ImageEditor/ImageEditor.tsx | 4 +- .../ui/src/ImageEditor/toolbar/filter.tsx | 16 +- .../ui/src/ImageUpload/MultiImageUpload.tsx | 2 +- packages/ui/src/ImageUpload/styled.ts | 7 +- packages/ui/src/Input/Input.tsx | 2 +- .../ui/src/Input/__tests__/Input.test.tsx | 2 +- .../ui/src/List/DataList/DataList.stories.tsx | 2 +- packages/ui/src/Mosaic/Mosaic.tsx | 2 +- packages/ui/src/Radio/Radio.tsx | 1 - packages/ui/src/Select/Select.tsx | 2 +- packages/ui/src/Tooltip/Tooltip.tsx | 2 +- packages/utils/src/generateId.ts | 2 +- packages/utils/src/mdbid.ts | 2 +- packages/validation/src/validators/numeric.ts | 2 +- scripts/listPackagesWithTests.js | 3 + scripts/prepublishOnly/src/prepublishOnly.ts | 7 +- .../src/resolvePackageVersion.ts | 3 +- scripts/release/Release.js | 10 +- scripts/release/index.js | 10 +- webiny.config.ts | 2 + webiny.project.ts | 4 +- yarn.lock | 8 + 373 files changed, 3560 insertions(+), 2485 deletions(-) delete mode 100644 .github/workflows/pullRequestsCommandCypressTest.yml create mode 100644 .github/workflows/versionApproval.yml create mode 100644 packages/api-file-manager/__tests__/file.customDates.test.ts create mode 100644 packages/api-file-manager/__tests__/file.customIdentities.test.ts delete mode 100644 packages/api-file-manager/src/createFileManager/filevalidation.disabled.ts create mode 100644 packages/api-headless-cms/__tests__/contentAPI/contentEntryCustomDates.test.ts create mode 100644 packages/api-headless-cms/__tests__/contentAPI/contentEntryCustomIdentities.test.s.ts create mode 100644 packages/api-headless-cms/src/utils/date.ts delete mode 100644 packages/api-page-builder/__tests__/graphql/utils/waitPage.ts create mode 100644 packages/api-security/__tests__/graphql/parallelQueries.ts create mode 100644 packages/api-security/__tests__/parallelQueries.test.ts create mode 100644 packages/app-admin/src/hooks/useIsMounted.ts create mode 100644 packages/app-file-manager/jest.config.js create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEdit.styled.tsx create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEdit.tsx create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEdit.types.ts create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEditPresenter.test.ts create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEditPresenter.ts create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/AddOperation.tsx create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditor.tsx create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditorDialog.tsx create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditorDialogPresenter.test.ts create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditorDialogPresenter.tsx create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/FieldRenderer.tsx create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/Operation.tsx create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/RemoveOperation.tsx create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/index.tsx create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/GraphQLInputMapper.test.ts create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/GraphQLInputMapper.ts create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/Batch.ts create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/BatchMapper.ts create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/Field.ts create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/FieldMapper.ts create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/index.ts create mode 100644 packages/app-file-manager/src/components/BulkActions/ActionEdit/index.tsx delete mode 100644 packages/app-page-builder/src/render/plugins/elements/imagesList/components/Slider.tsx delete mode 100644 packages/db-dynamodb/src/BatchProcess.ts delete mode 100644 packages/db-dynamodb/src/QueryGenerator.ts delete mode 100644 packages/db-dynamodb/src/operators/comparison/beginsWith.ts delete mode 100644 packages/db-dynamodb/src/operators/comparison/between.ts delete mode 100644 packages/db-dynamodb/src/operators/comparison/eq.ts delete mode 100644 packages/db-dynamodb/src/operators/comparison/gt.ts delete mode 100644 packages/db-dynamodb/src/operators/comparison/gte.ts delete mode 100644 packages/db-dynamodb/src/operators/comparison/lt.ts delete mode 100644 packages/db-dynamodb/src/operators/comparison/lte.ts delete mode 100644 packages/db-dynamodb/src/operators/index.ts delete mode 100644 packages/db-dynamodb/src/operators/logical/and.ts delete mode 100644 packages/db-dynamodb/src/operators/logical/or.ts delete mode 100644 packages/db-dynamodb/src/statements/createKeyConditionExpressionArgs.ts delete mode 100644 packages/db-dynamodb/src/statements/processStatement.ts create mode 100644 packages/telemetry/react.d.ts diff --git a/.adiorc.js b/.adiorc.js index 58ca505c9db..3b4052f9737 100644 --- a/.adiorc.js +++ b/.adiorc.js @@ -1,4 +1,4 @@ -const get = require("lodash.get"); +const get = require("lodash/get"); const getWorkspaces = require("get-yarn-workspaces"); const path = require("path"); @@ -22,23 +22,25 @@ module.exports = { }, ignore: { src: [ + "~tests", + "~", + "async_hooks", + "aws-sdk", + "buffer", + "child_process", + "crypto", + "events", + "follow-redirects", + "fs", "http", - "path", "https", - "follow-redirects", - "child_process", "os", - "buffer", - "fs", + "path", "readline", "stream", "util", - "events", - "crypto", "url", - "worker_threads", - "~tests", - "~" + "worker_threads" ], dependencies: [ "@babel/runtime", diff --git a/.eslintrc.js b/.eslintrc.js index d049be95938..1ffda1e0bec 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -29,8 +29,15 @@ module.exports = { "import/no-unresolved": 0, // [2, { commonjs: true, amd: true }], "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/ban-ts-ignore": "off", + "@typescript-eslint/ban-ts-comment": [ + 2, + { + "ts-check": true, + "ts-ignore": "allow-with-description", + "ts-nocheck": "allow-with-description", + "ts-expect-error": false + } + ], "@typescript-eslint/ban-types": "off", "@typescript-eslint/no-use-before-define": 0, "@typescript-eslint/no-unused-vars": getNoUnusedVars(), diff --git a/.github/workflows/cleanup/aws-nuke.yml b/.github/workflows/cleanup/aws-nuke.yml index 5804c617518..9605cf40217 100644 --- a/.github/workflows/cleanup/aws-nuke.yml +++ b/.github/workflows/cleanup/aws-nuke.yml @@ -16,6 +16,8 @@ accounts: - "s3://webiny-ci" IAMRole: - "GitHubActionsWebinyJs" + IAMRolePolicyAttachment: + - "GitHubActionsWebinyJs -> AdministratorAccess" resource-types: # These resource types will be destroyed. diff --git a/.github/workflows/pullRequests.yml b/.github/workflows/pullRequests.yml index 3d62e514bb9..70c224a4152 100644 --- a/.github/workflows/pullRequests.yml +++ b/.github/workflows/pullRequests.yml @@ -3,10 +3,7 @@ # and run "github-actions-wac build" (or "ghawac build") to regenerate this file. # For more information, run "github-actions-wac --help". name: Pull Requests -'on': - pull_request: - branches: - - next +'on': pull_request jobs: validateWorkflows: name: Validate workflows @@ -18,12 +15,28 @@ jobs: run: npx github-actions-wac validate validateCommits: name: Validate commit messages + if: github.base_ref != 'dev' steps: - uses: actions/setup-node@v3 with: node-version: 18 - uses: actions/checkout@v3 - - uses: webiny/action-conventional-commits@v1.1.0 + - uses: webiny/action-conventional-commits@v1.2.0 + runs-on: ubuntu-latest + env: + NODE_OPTIONS: '--max_old_space_size=4096' + YARN_ENABLE_IMMUTABLE_INSTALLS: false + validateCommitsDev: + name: Validate commit messages (dev branch, 'feat' commits not allowed) + if: github.base_ref == 'dev' + steps: + - uses: actions/setup-node@v3 + with: + node-version: 18 + - uses: actions/checkout@v3 + - uses: webiny/action-conventional-commits@v1.2.0 + with: + allowed-commit-types: fix,docs,style,refactor,test,build,perf,ci,chore,revert,merge,wip runs-on: ubuntu-latest env: NODE_OPTIONS: '--max_old_space_size=4096' diff --git a/.github/workflows/pullRequestsCommandCypressTest.yml b/.github/workflows/pullRequestsCommandCypressTest.yml deleted file mode 100644 index 5afcc347c82..00000000000 --- a/.github/workflows/pullRequestsCommandCypressTest.yml +++ /dev/null @@ -1,532 +0,0 @@ -# This file was automatically generated by github-actions-wac. -# DO NOT MODIFY IT BY HAND. Instead, modify the source *.wac.ts file(s) -# and run "github-actions-wac build" (or "ghawac build") to regenerate this file. -# For more information, run "github-actions-wac --help". -name: Pull Requests Command - Cypress (TEST) -'on': issue_comment -env: - NODE_OPTIONS: '--max_old_space_size=4096' - AWS_REGION: eu-central-1 -jobs: - validateWorkflows: - name: Validate workflows - runs-on: ubuntu-latest - steps: - - name: Install dependencies - run: yarn --immutable - - name: Validate - run: npx github-actions-wac validate - checkComment: - name: Check comment for /cypress_test - if: ${{ github.event.issue.pull_request }} - steps: - - uses: actions/setup-node@v3 - with: - node-version: 18 - - name: Check for Command - id: command - uses: xt0rted/slash-command-action@v2 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - command: cypress_test - reaction: 'true' - reaction-type: eyes - allow-edits: 'false' - permission-level: write - - name: Create comment - uses: peter-evans/create-or-update-comment@v2 - with: - issue-number: ${{ github.event.issue.number }} - body: >- - Cypress E2E tests have been initiated (for more information, click - [here](https://github.com/webiny/webiny-js/actions/runs/${{ - github.run_id }})). :sparkles: - runs-on: ubuntu-latest - env: - NODE_OPTIONS: '--max_old_space_size=4096' - YARN_ENABLE_IMMUTABLE_INSTALLS: false - e2e-wby-cms-ddb-init: - needs: checkComment - name: E2E (DDB) - Init - outputs: - day: ${{ steps.get-day.outputs.day }} - ts: ${{ steps.get-timestamp.outputs.ts }} - cypress-folders: ${{ steps.list-cypress-folders.outputs.cypress-folders }} - steps: - - uses: actions/setup-node@v3 - with: - node-version: 18 - - uses: actions/checkout@v3 - - name: Install Hub Utility - run: sudo apt-get install -y hub - - name: Checkout Pull Request - working-directory: '' - run: hub pr checkout ${{ github.event.issue.number }} - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - - name: Get day of the month - id: get-day - run: >- - echo "day=$(node --eval "console.log(new Date().getDate())")" >> - $GITHUB_OUTPUT - - name: Get timestamp - id: get-timestamp - run: >- - echo "ts=$(node --eval "console.log(new Date().getTime())")" >> - $GITHUB_OUTPUT - - name: List Cypress tests folders - id: list-cypress-folders - run: >- - echo "cypress-folders=$(node scripts/listCypressTestsFolders.js)" >> - $GITHUB_OUTPUT - runs-on: ubuntu-latest - env: - NODE_OPTIONS: '--max_old_space_size=4096' - YARN_ENABLE_IMMUTABLE_INSTALLS: false - e2e-wby-cms-ddb-project-setup: - needs: e2e-wby-cms-ddb-init - name: E2E (DDB) - Project setup - outputs: - cypress-config: ${{ steps.save-cypress-config.outputs.cypress-config }} - environment: next - env: - NODE_OPTIONS: '--max_old_space_size=4096' - YARN_ENABLE_IMMUTABLE_INSTALLS: 'false' - CYPRESS_MAILOSAUR_API_KEY: ${{ secrets.CYPRESS_MAILOSAUR_API_KEY }} - PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }} - PULUMI_SECRETS_PROVIDER: ${{ secrets.PULUMI_SECRETS_PROVIDER }} - WEBINY_PULUMI_BACKEND: >- - ${{ secrets.WEBINY_PULUMI_BACKEND }}${{ - needs.e2e-wby-cms-ddb-init.outputs.ts }}_ddb - steps: - - uses: actions/setup-node@v3 - with: - node-version: 18 - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: arn:aws:iam::726952677045:role/GitHubActionsWebinyJs - aws-region: eu-central-1 - - uses: actions/checkout@v3 - with: - path: dev - - name: Install Hub Utility - run: sudo apt-get install -y hub - - name: Checkout Pull Request - working-directory: dev - run: hub pr checkout ${{ github.event.issue.number }} - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - - uses: actions/cache@v3 - id: yarn-cache - with: - path: dev/.yarn/cache - key: yarn-${{ runner.os }}-${{ hashFiles('dev/**/yarn.lock') }} - - uses: actions/cache@v3 - id: cached-packages - with: - path: dev/.webiny/cached-packages - key: >- - ${{ runner.os }}-${{ needs.e2e-wby-cms-ddb-init.outputs.day }}-${{ - secrets.RANDOM_CACHE_KEY_SUFFIX }} - - name: Install dependencies - working-directory: dev - run: yarn --immutable - - name: Build packages - working-directory: dev - run: yarn build:quick - - uses: actions/cache@v3 - id: packages-cache - with: - path: dev/.webiny/cached-packages - key: packages-cache-${{ needs.e2e-wby-cms-ddb-init.outputs.ts }} - - name: Start Verdaccio local server - working-directory: dev - run: ' yarn add pm2 verdaccio && npx pm2 start verdaccio -- -c .verdaccio.yaml' - - name: Configure NPM to use local registry - run: npm config set registry http://localhost:4873 - - name: Set git email - run: git config --global user.email "webiny-bot@webiny.com" - - name: Set git username - run: git config --global user.name "webiny-bot" - - name: Create ".npmrc" file in the project root, with a dummy auth token - working-directory: dev - run: echo '//localhost:4873/:_authToken="dummy-auth-token"' > .npmrc - - name: Version and publish to Verdaccio - working-directory: dev - run: yarn release --type=verdaccio - - name: Create verdaccio-files artifact - uses: actions/upload-artifact@v3 - with: - name: verdaccio-files-ddb - retention-days: 1 - path: | - dev/.verdaccio/ - dev/.verdaccio.yaml - - name: Create directory - run: mkdir xyz - - name: Disable Webiny telemetry - run: > - mkdir ~/.webiny && echo '{ "id": "ci", "telemetry": false }' > - ~/.webiny/config - - name: Create a new Webiny project - working-directory: xyz - run: > - npx create-webiny-project@local-npm test-project --tag local-npm - --no-interactive --assign-to-yarnrc - '{"npmRegistryServer":"http://localhost:4873","unsafeHttpWhitelist":["localhost"]}' - --template-options '{"region":"${{ env.AWS_REGION - }}","storageOperations":"ddb"}' - - name: Print CLI version - working-directory: xyz/test-project - run: yarn webiny --version - - name: Create project-files artifact - uses: actions/upload-artifact@v3 - with: - name: project-files-ddb - retention-days: 1 - path: | - xyz/test-project/ - !xyz/test-project/node_modules/**/* - !xyz/test-project/**/node_modules/**/* - !xyz/test-project/.yarn/cache/**/* - - name: Deploy Core - working-directory: xyz/test-project - run: yarn webiny deploy apps/core --env dev - - name: Deploy API - working-directory: xyz/test-project - run: yarn webiny deploy apps/api --env dev - - name: Deploy Admin Area - working-directory: xyz/test-project - run: yarn webiny deploy apps/admin --env dev - - name: Deploy Website - working-directory: xyz/test-project - run: yarn webiny deploy apps/website --env dev - - name: Create Cypress config - working-directory: dev - run: yarn setup-cypress --projectFolder ../xyz/test-project - - name: Save Cypress config - id: save-cypress-config - working-directory: dev - run: >- - echo "cypress-config=$(cat cypress-tests/cypress.config.ts | tr -d - '\t\n\r')" >> $GITHUB_OUTPUT - - name: Cypress - run installation wizard test - working-directory: dev/cypress-tests - run: >- - yarn cypress run --browser chrome --spec - "cypress/e2e/adminInstallation/**/*.cy.js" - runs-on: ubuntu-latest - permissions: - id-token: write - e2e-wby-cms-ddb-cypress-tests: - name: >- - ${{ matrix.cypress-folder }} (ddb, ${{ matrix.os }}, Node v${{ matrix.node - }}) - needs: - - e2e-wby-cms-ddb-init - - e2e-wby-cms-ddb-project-setup - strategy: - fail-fast: false - matrix: - os: - - ubuntu-latest - node: - - 18 - cypress-folder: ${{ fromJson(needs.e2e-wby-cms-ddb-init.outputs.cypress-folders) }} - environment: next - env: - NODE_OPTIONS: '--max_old_space_size=4096' - YARN_ENABLE_IMMUTABLE_INSTALLS: 'false' - CYPRESS_MAILOSAUR_API_KEY: ${{ secrets.CYPRESS_MAILOSAUR_API_KEY }} - PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }} - PULUMI_SECRETS_PROVIDER: ${{ secrets.PULUMI_SECRETS_PROVIDER }} - WEBINY_PULUMI_BACKEND: >- - ${{ secrets.WEBINY_PULUMI_BACKEND }}${{ - needs.e2e-wby-cms-ddb-init.outputs.ts }}_ddb - steps: - - uses: actions/setup-node@v3 - with: - node-version: 18 - - uses: actions/checkout@v3 - with: - path: dev - - name: Install Hub Utility - run: sudo apt-get install -y hub - - name: Checkout Pull Request - working-directory: dev - run: hub pr checkout ${{ github.event.issue.number }} - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - - uses: actions/cache@v3 - with: - path: dev/.webiny/cached-packages - key: packages-cache-${{ needs.e2e-wby-cms-ddb-init.outputs.ts }} - - uses: actions/cache@v3 - with: - path: dev/.yarn/cache - key: yarn-${{ runner.os }}-${{ hashFiles('dev/**/yarn.lock') }} - - name: Install dependencies - working-directory: dev - run: yarn --immutable - - name: Build packages - working-directory: dev - run: yarn build:quick - - name: Set up Cypress config - working-directory: dev - run: >- - echo '${{ needs.e2e-wby-cms-ddb-project-setup.outputs.cypress-config - }}' > cypress-tests/cypress.config.ts - - name: Cypress - run "${{ matrix.cypress-folder }}" tests - working-directory: dev/cypress-tests - timeout-minutes: 40 - run: >- - yarn cypress run --browser chrome --spec "${{ matrix.cypress-folder - }}" - runs-on: ubuntu-latest - e2e-wby-cms-ddb-es-init: - needs: checkComment - name: E2E (DDB-ES) - Init - outputs: - day: ${{ steps.get-day.outputs.day }} - ts: ${{ steps.get-timestamp.outputs.ts }} - cypress-folders: ${{ steps.list-cypress-folders.outputs.cypress-folders }} - steps: - - uses: actions/setup-node@v3 - with: - node-version: 18 - - uses: actions/checkout@v3 - - name: Install Hub Utility - run: sudo apt-get install -y hub - - name: Checkout Pull Request - working-directory: '' - run: hub pr checkout ${{ github.event.issue.number }} - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - - name: Get day of the month - id: get-day - run: >- - echo "day=$(node --eval "console.log(new Date().getDate())")" >> - $GITHUB_OUTPUT - - name: Get timestamp - id: get-timestamp - run: >- - echo "ts=$(node --eval "console.log(new Date().getTime())")" >> - $GITHUB_OUTPUT - - name: List Cypress tests folders - id: list-cypress-folders - run: >- - echo "cypress-folders=$(node scripts/listCypressTestsFolders.js)" >> - $GITHUB_OUTPUT - runs-on: ubuntu-latest - env: - NODE_OPTIONS: '--max_old_space_size=4096' - YARN_ENABLE_IMMUTABLE_INSTALLS: false - e2e-wby-cms-ddb-es-project-setup: - needs: e2e-wby-cms-ddb-es-init - name: E2E (DDB-ES) - Project setup - outputs: - cypress-config: ${{ steps.save-cypress-config.outputs.cypress-config }} - environment: next - env: - NODE_OPTIONS: '--max_old_space_size=4096' - YARN_ENABLE_IMMUTABLE_INSTALLS: 'false' - CYPRESS_MAILOSAUR_API_KEY: ${{ secrets.CYPRESS_MAILOSAUR_API_KEY }} - PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }} - PULUMI_SECRETS_PROVIDER: ${{ secrets.PULUMI_SECRETS_PROVIDER }} - WEBINY_PULUMI_BACKEND: >- - ${{ secrets.WEBINY_PULUMI_BACKEND }}${{ - needs.e2e-wby-cms-ddb-es-init.outputs.ts }}_ddb - AWS_ELASTIC_SEARCH_DOMAIN_NAME: ${{ secrets.AWS_ELASTIC_SEARCH_DOMAIN_NAME }} - ELASTIC_SEARCH_ENDPOINT: ${{ secrets.ELASTIC_SEARCH_ENDPOINT }} - ELASTIC_SEARCH_INDEX_PREFIX: ${{ needs.e2e-wby-cms-ddb-es-init.outputs.ts }}_ - steps: - - uses: actions/setup-node@v3 - with: - node-version: 18 - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: arn:aws:iam::726952677045:role/GitHubActionsWebinyJs - aws-region: eu-central-1 - - uses: actions/checkout@v3 - with: - path: dev - - name: Install Hub Utility - run: sudo apt-get install -y hub - - name: Checkout Pull Request - working-directory: dev - run: hub pr checkout ${{ github.event.issue.number }} - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - - uses: actions/cache@v3 - id: yarn-cache - with: - path: dev/.yarn/cache - key: yarn-${{ runner.os }}-${{ hashFiles('dev/**/yarn.lock') }} - - uses: actions/cache@v3 - id: cached-packages - with: - path: dev/.webiny/cached-packages - key: >- - ${{ runner.os }}-${{ needs.e2e-wby-cms-ddb-es-init.outputs.day - }}-${{ secrets.RANDOM_CACHE_KEY_SUFFIX }} - - name: Install dependencies - working-directory: dev - run: yarn --immutable - - name: Build packages - working-directory: dev - run: yarn build:quick - - uses: actions/cache@v3 - id: packages-cache - with: - path: dev/.webiny/cached-packages - key: packages-cache-${{ needs.e2e-wby-cms-ddb-es-init.outputs.ts }} - - name: Start Verdaccio local server - working-directory: dev - run: ' yarn add pm2 verdaccio && npx pm2 start verdaccio -- -c .verdaccio.yaml' - - name: Configure NPM to use local registry - run: npm config set registry http://localhost:4873 - - name: Set git email - run: git config --global user.email "webiny-bot@webiny.com" - - name: Set git username - run: git config --global user.name "webiny-bot" - - name: Create ".npmrc" file in the project root, with a dummy auth token - working-directory: dev - run: echo '//localhost:4873/:_authToken="dummy-auth-token"' > .npmrc - - name: Version and publish to Verdaccio - working-directory: dev - run: yarn release --type=verdaccio - - name: Create verdaccio-files artifact - uses: actions/upload-artifact@v3 - with: - name: verdaccio-files-ddb-es - retention-days: 1 - path: | - dev/.verdaccio/ - dev/.verdaccio.yaml - - name: Create directory - run: mkdir xyz - - name: Disable Webiny telemetry - run: > - mkdir ~/.webiny && echo '{ "id": "ci", "telemetry": false }' > - ~/.webiny/config - - name: Create a new Webiny project - working-directory: xyz - run: > - npx create-webiny-project@local-npm test-project --tag local-npm - --no-interactive --assign-to-yarnrc - '{"npmRegistryServer":"http://localhost:4873","unsafeHttpWhitelist":["localhost"]}' - --template-options '{"region":"${{ env.AWS_REGION - }}","storageOperations":"ddb-es"}' - - name: Print CLI version - working-directory: xyz/test-project - run: yarn webiny --version - - name: Create project-files artifact - uses: actions/upload-artifact@v3 - with: - name: project-files-ddb-es - retention-days: 1 - path: | - xyz/test-project/ - !xyz/test-project/node_modules/**/* - !xyz/test-project/**/node_modules/**/* - !xyz/test-project/.yarn/cache/**/* - - name: Deploy Core - working-directory: xyz/test-project - run: yarn webiny deploy apps/core --env dev - - name: Deploy API - working-directory: xyz/test-project - run: yarn webiny deploy apps/api --env dev - - name: Deploy Admin Area - working-directory: xyz/test-project - run: yarn webiny deploy apps/admin --env dev - - name: Deploy Website - working-directory: xyz/test-project - run: yarn webiny deploy apps/website --env dev - - name: Create Cypress config - working-directory: dev - run: yarn setup-cypress --projectFolder ../xyz/test-project - - name: Save Cypress config - id: save-cypress-config - working-directory: dev - run: >- - echo "cypress-config=$(cat cypress-tests/cypress.config.ts | tr -d - '\t\n\r')" >> $GITHUB_OUTPUT - - name: Cypress - run installation wizard test - working-directory: dev/cypress-tests - run: >- - yarn cypress run --browser chrome --spec - "cypress/e2e/adminInstallation/**/*.cy.js" - runs-on: ubuntu-latest - permissions: - id-token: write - e2e-wby-cms-ddb-es-cypress-tests: - name: >- - ${{ matrix.cypress-folder }} (ddb-es, ${{ matrix.os }}, Node v${{ - matrix.node }}) - needs: - - e2e-wby-cms-ddb-es-init - - e2e-wby-cms-ddb-es-project-setup - strategy: - fail-fast: false - matrix: - os: - - ubuntu-latest - node: - - 18 - cypress-folder: ${{ fromJson(needs.e2e-wby-cms-ddb-es-init.outputs.cypress-folders) }} - environment: next - env: - NODE_OPTIONS: '--max_old_space_size=4096' - YARN_ENABLE_IMMUTABLE_INSTALLS: 'false' - CYPRESS_MAILOSAUR_API_KEY: ${{ secrets.CYPRESS_MAILOSAUR_API_KEY }} - PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }} - PULUMI_SECRETS_PROVIDER: ${{ secrets.PULUMI_SECRETS_PROVIDER }} - WEBINY_PULUMI_BACKEND: >- - ${{ secrets.WEBINY_PULUMI_BACKEND }}${{ - needs.e2e-wby-cms-ddb-es-init.outputs.ts }}_ddb - AWS_ELASTIC_SEARCH_DOMAIN_NAME: ${{ secrets.AWS_ELASTIC_SEARCH_DOMAIN_NAME }} - ELASTIC_SEARCH_ENDPOINT: ${{ secrets.ELASTIC_SEARCH_ENDPOINT }} - ELASTIC_SEARCH_INDEX_PREFIX: ${{ needs.e2e-wby-cms-ddb-es-init.outputs.ts }}_ - steps: - - uses: actions/setup-node@v3 - with: - node-version: 18 - - uses: actions/checkout@v3 - with: - path: dev - - name: Install Hub Utility - run: sudo apt-get install -y hub - - name: Checkout Pull Request - working-directory: dev - run: hub pr checkout ${{ github.event.issue.number }} - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - - uses: actions/cache@v3 - with: - path: dev/.webiny/cached-packages - key: packages-cache-${{ needs.e2e-wby-cms-ddb-es-init.outputs.ts }} - - uses: actions/cache@v3 - with: - path: dev/.yarn/cache - key: yarn-${{ runner.os }}-${{ hashFiles('dev/**/yarn.lock') }} - - name: Install dependencies - working-directory: dev - run: yarn --immutable - - name: Build packages - working-directory: dev - run: yarn build:quick - - name: Set up Cypress config - working-directory: dev - run: >- - echo '${{ - needs.e2e-wby-cms-ddb-es-project-setup.outputs.cypress-config }}' > - cypress-tests/cypress.config.ts - - name: Cypress - run "${{ matrix.cypress-folder }}" tests - working-directory: dev/cypress-tests - timeout-minutes: 40 - run: >- - yarn cypress run --browser chrome --spec "${{ matrix.cypress-folder - }}" - runs-on: ubuntu-latest diff --git a/.github/workflows/versionApproval.yml b/.github/workflows/versionApproval.yml new file mode 100644 index 00000000000..5855041fc1a --- /dev/null +++ b/.github/workflows/versionApproval.yml @@ -0,0 +1,90 @@ +name: Version Approval + +on: + workflow_dispatch: + repository_dispatch: + types: [release-with-approval] + +env: + NODE_OPTIONS: --max_old_space_size=4096 + GH_TOKEN: ${{ secrets.GH_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + YARN_ENABLE_IMMUTABLE_INSTALLS: false + +jobs: + init: + name: Init + runs-on: ubuntu-latest + outputs: + day: ${{ steps.get-day.outputs.day }} + ts: ${{ steps.get-timestamp.outputs.ts }} + steps: + - uses: actions/setup-node@v3 + with: + node-version: 18 + + - uses: actions/checkout@v3 + + - name: Get day of the month + id: get-day + run: echo "day=$(node --eval "console.log(new Date().getDate())")" >> $GITHUB_OUTPUT + + - name: Get timestamp + id: get-timestamp + run: echo "ts=$(node --eval "console.log(new Date().getTime())")" >> $GITHUB_OUTPUT + versioning: + needs: init + name: Determine Release Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.determine-release-version.outputs.version }} + steps: + - uses: actions/setup-node@v3 + with: + node-version: 18 + + - uses: actions/checkout@v3 + + - uses: actions/cache@v3 + id: yarn-cache + with: + path: .yarn/cache + key: yarn-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} + + - uses: actions/cache@v3 + id: global-daily-packages-cache + with: + path: .webiny/cached-packages + key: ${{ runner.os }}-${{ needs.init.outputs.day }}-${{ secrets.RANDOM_CACHE_KEY_SUFFIX }} + + - name: Install dependencies + run: yarn --immutable + + - name: Build packages + run: yarn build:quick + + - uses: actions/cache@v3 + with: + path: .webiny/cached-packages + key: packages-cache-${{ needs.init.outputs.ts }} + + - name: Version packages + id: determine-release-version + shell: bash + run: | + echo "Creating version.txt" + touch version.txt + echo "Versioning packages..." + yarn release --type=${{ github.event.client_payload.type }} --tag=${{ github.event.client_payload.tag }} --version=${{ github.event.client_payload.version }} --createGithubRelease=${{ github.event.client_payload.createGithubRelease }} --printVersion | tee version.txt + echo "Setting output version: $(tail -n 1 version.txt)" + echo "version=$(tail -n 1 version.txt)" >> $GITHUB_OUTPUT + + npm-release: + needs: [init, versioning] + name: Release ${{ needs.versioning.outputs.version }} ("${{ github.event.client_payload.tag }}") + runs-on: webiny-build-packages + environment: release + steps: + - uses: actions/setup-node@v3 + with: + node-version: 18 diff --git a/.github/workflows/wac/pullRequests.wac.ts b/.github/workflows/wac/pullRequests.wac.ts index 5823fe92bec..cd18bc3083c 100644 --- a/.github/workflows/wac/pullRequests.wac.ts +++ b/.github/workflows/wac/pullRequests.wac.ts @@ -78,12 +78,28 @@ const createJestTestsJob = (storage: string | null) => { export const pullRequests = createWorkflow({ name: "Pull Requests", - on: { pull_request: { branches: ["next"] } }, + on: "pull_request", jobs: { validateWorkflows: createValidateWorkflowsJob(), validateCommits: createJob({ name: "Validate commit messages", - steps: [{ uses: "webiny/action-conventional-commits@v1.1.0" }] + if: "github.base_ref != 'dev'", + steps: [{ uses: "webiny/action-conventional-commits@v1.2.0" }] + }), + // Don't allow "feat" commits to be merged into "dev" branch. + validateCommitsDev: createJob({ + name: "Validate commit messages (dev branch, 'feat' commits not allowed)", + if: "github.base_ref == 'dev'", + steps: [ + { + uses: "webiny/action-conventional-commits@v1.2.0", + with: { + // If dev, use "dev" commit types, otherwise use "next" commit types. + "allowed-commit-types": + "fix,docs,style,refactor,test,build,perf,ci,chore,revert,merge,wip" + } + } + ] }), init: createJob({ name: "Init", diff --git a/apps/admin/src/App.fm.tsx b/apps/admin/src/App.fm.tsx index 7022843eb5f..63b73ca5be8 100644 --- a/apps/admin/src/App.fm.tsx +++ b/apps/admin/src/App.fm.tsx @@ -1,6 +1,6 @@ import React, { useCallback } from "react"; import { Admin, createComponentPlugin } from "@webiny/app-serverless-cms"; -import { FileManagerRenderer, FileManagerFileItem, OverlayLayout } from "@webiny/app-admin"; +import { FileManagerFileItem, FileManagerRenderer, OverlayLayout } from "@webiny/app-admin"; import { Cognito } from "@webiny/app-admin-users-cognito"; import "./App.scss"; @@ -24,7 +24,7 @@ const CustomFileManager = createComponentPlugin(FileManagerRenderer, () => { return ( props.onClose && props.onClose()}> - {/* @ts-ignore */} + {/* @ts-expect-error */} ); diff --git a/apps/admin/src/plugins/formBuilder/richTextEditor.ts b/apps/admin/src/plugins/formBuilder/richTextEditor.ts index 96de4f24eca..77eb207d3ec 100644 --- a/apps/admin/src/plugins/formBuilder/richTextEditor.ts +++ b/apps/admin/src/plugins/formBuilder/richTextEditor.ts @@ -1,13 +1,13 @@ /** * Package @editorjs/* is missing types. */ -// @ts-ignore +// @ts-expect-error import Delimiter from "@editorjs/delimiter"; -// @ts-ignore +// @ts-expect-error import Quote from "@editorjs/quote"; -// @ts-ignore +// @ts-expect-error import List from "@editorjs/list"; -// @ts-ignore +// @ts-expect-error import Underline from "@editorjs/underline"; import Image from "@webiny/app-admin/components/RichTextEditor/tools/image"; import TextColor from "@webiny/app-admin/components/RichTextEditor/tools/textColor"; diff --git a/apps/admin/src/plugins/headlessCMS/richTextEditor.ts b/apps/admin/src/plugins/headlessCMS/richTextEditor.ts index 24bdaa40760..4c5066bf443 100644 --- a/apps/admin/src/plugins/headlessCMS/richTextEditor.ts +++ b/apps/admin/src/plugins/headlessCMS/richTextEditor.ts @@ -1,13 +1,13 @@ /** * Package @editorjs/* is missing types. */ -// @ts-ignore +// @ts-expect-error import Delimiter from "@editorjs/delimiter"; -// @ts-ignore +// @ts-expect-error import Quote from "@editorjs/quote"; -// @ts-ignore +// @ts-expect-error import List from "@editorjs/list"; -// @ts-ignore +// @ts-expect-error import Underline from "@editorjs/underline"; import Image from "@webiny/app-admin/components/RichTextEditor/tools/image"; import TextColor from "@webiny/app-admin/components/RichTextEditor/tools/textColor"; diff --git a/apps/admin/src/plugins/pageBuilder/richTextEditor.ts b/apps/admin/src/plugins/pageBuilder/richTextEditor.ts index 684b3227ef1..b71d0269e6d 100644 --- a/apps/admin/src/plugins/pageBuilder/richTextEditor.ts +++ b/apps/admin/src/plugins/pageBuilder/richTextEditor.ts @@ -1,13 +1,13 @@ /** * Package @editorjs/* is missing types. */ -// @ts-ignore +// @ts-expect-error import Delimiter from "@editorjs/delimiter"; -// @ts-ignore +// @ts-expect-error import Quote from "@editorjs/quote"; -// @ts-ignore +// @ts-expect-error import List from "@editorjs/list"; -// @ts-ignore +// @ts-expect-error import Underline from "@editorjs/underline"; import Image from "@webiny/app-admin/components/RichTextEditor/tools/image"; import TextColor from "@webiny/app-admin/components/RichTextEditor/tools/textColor"; diff --git a/apps/admin/src/plugins/scaffolds/index.ts b/apps/admin/src/plugins/scaffolds/index.ts index d199ea25576..208d5c46f29 100644 --- a/apps/admin/src/plugins/scaffolds/index.ts +++ b/apps/admin/src/plugins/scaffolds/index.ts @@ -1,4 +1,4 @@ // This file is automatically updated via various scaffolding utilities. -// @ts-ignore +// @ts-expect-error export default () => []; diff --git a/apps/api/graphql/src/index.ts b/apps/api/graphql/src/index.ts index 086519ea8af..381a073c181 100644 --- a/apps/api/graphql/src/index.ts +++ b/apps/api/graphql/src/index.ts @@ -105,7 +105,8 @@ export const handler = createHandler({ type: "text", renderer: { name: "text-input" - } + }, + bulkEdit: true }); modifier.addField({ diff --git a/packages/api-aco-so-ddb-es/__tests__/handler/index.ts b/packages/api-aco-so-ddb-es/__tests__/handler/index.ts index cc3495865ca..a67dff152cd 100644 --- a/packages/api-aco-so-ddb-es/__tests__/handler/index.ts +++ b/packages/api-aco-so-ddb-es/__tests__/handler/index.ts @@ -15,7 +15,6 @@ import elasticsearchClientContextPlugin, { createGzipCompression, getElasticsearchOperators } from "@webiny/api-elasticsearch"; -// @ts-ignore import { simulateStream } from "@webiny/project-utils/testing/dynamodb"; import { createEventHandler as createDynamoDBToElasticsearchEventHandler } from "@webiny/api-dynamodb-to-elasticsearch"; diff --git a/packages/api-aco/__tests__/snapshots/customAppsSchema.ts b/packages/api-aco/__tests__/snapshots/customAppsSchema.ts index 358272b24f4..d3c544edaac 100644 --- a/packages/api-aco/__tests__/snapshots/customAppsSchema.ts +++ b/packages/api-aco/__tests__/snapshots/customAppsSchema.ts @@ -199,6 +199,13 @@ export const createCustomAppsSchemaSnapshot = () => { savedOn_lte: DateTime savedOn_between: [DateTime!] savedOn_not_between: [DateTime!] + publishedOn: DateTime + publishedOn_gt: DateTime + publishedOn_gte: DateTime + publishedOn_lt: DateTime + publishedOn_lte: DateTime + publishedOn_between: [DateTime!] + publishedOn_not_between: [DateTime!] createdBy: String createdBy_not: String createdBy_in: [String!] diff --git a/packages/api-aco/__tests__/snapshots/defaultAppsSchema.ts b/packages/api-aco/__tests__/snapshots/defaultAppsSchema.ts index 2a5f14dd718..13e45e3207c 100644 --- a/packages/api-aco/__tests__/snapshots/defaultAppsSchema.ts +++ b/packages/api-aco/__tests__/snapshots/defaultAppsSchema.ts @@ -173,6 +173,13 @@ export const createDefaultAppsSchemaSnapshot = () => { savedOn_lte: DateTime savedOn_between: [DateTime!] savedOn_not_between: [DateTime!] + publishedOn: DateTime + publishedOn_gt: DateTime + publishedOn_gte: DateTime + publishedOn_lt: DateTime + publishedOn_lte: DateTime + publishedOn_between: [DateTime!] + publishedOn_not_between: [DateTime!] createdBy: String createdBy_not: String createdBy_in: [String!] diff --git a/packages/api-admin-users/src/createAdminUsers.ts b/packages/api-admin-users/src/createAdminUsers.ts index a7494d0815b..5b2a913b8be 100644 --- a/packages/api-admin-users/src/createAdminUsers.ts +++ b/packages/api-admin-users/src/createAdminUsers.ts @@ -126,7 +126,7 @@ export const createAdminUsers = ({ /** * TODO @ts-refactor figure out better way to type this */ - // @ts-ignore + // @ts-expect-error async createUser(this: AdminUsers, data) { await checkPermission(); @@ -284,7 +284,7 @@ export const createAdminUsers = ({ /** * TODO @ts-refactor figure out better way to type this */ - // @ts-ignore + // @ts-expect-error async updateUser(this: AdminUsers, id, data) { await checkPermission(); diff --git a/packages/api-admin-users/src/createAdminUsers/users.validation.ts b/packages/api-admin-users/src/createAdminUsers/users.validation.ts index febfae9316a..8d72e8a2c49 100644 --- a/packages/api-admin-users/src/createAdminUsers/users.validation.ts +++ b/packages/api-admin-users/src/createAdminUsers/users.validation.ts @@ -1,12 +1,12 @@ /** * Package @commodo/fields does not have types */ -// @ts-ignore +// @ts-expect-error import { string, withFields } from "@commodo/fields"; /** * Package commodo-fields-object does not have types */ -// @ts-ignore +// @ts-expect-error import { object } from "commodo-fields-object"; import { validation } from "@webiny/validation"; import { AdminUsers } from "~/types"; diff --git a/packages/api-admin-users/src/graphql/user.gql.ts b/packages/api-admin-users/src/graphql/user.gql.ts index 4d12f8150f4..aecb4af84a1 100644 --- a/packages/api-admin-users/src/graphql/user.gql.ts +++ b/packages/api-admin-users/src/graphql/user.gql.ts @@ -1,7 +1,3 @@ -/** - * Package md5 does not have types. - */ -// @ts-ignore import md5 from "md5"; import { ErrorResponse, @@ -90,7 +86,7 @@ export default (params: CreateUserGraphQlPluginsParams) => { * What happens if tenant has no parent? * Or is the getUser.where.tenant optional parameter? In that case, remove comments and make tenant param optional */ - // @ts-ignore + // @ts-expect-error tenant: tenant.parent } }); diff --git a/packages/api-admin-users/src/index.ts b/packages/api-admin-users/src/index.ts index f76af6eb2fd..efaeadc9951 100644 --- a/packages/api-admin-users/src/index.ts +++ b/packages/api-admin-users/src/index.ts @@ -22,7 +22,7 @@ export default ({ storageOperations }: Config) => { * TODO @ts-refactor @pavel * When creating users, is it possible there is no tenant defined? */ - // @ts-ignore + // @ts-expect-error return tenant ? tenant.id : undefined; }; diff --git a/packages/api-apw/__tests__/scheduleAction/crud.cms_entry.test.ts b/packages/api-apw/__tests__/scheduleAction/crud.cms_entry.test.ts index 1452e1d061e..dd70eebd1e0 100644 --- a/packages/api-apw/__tests__/scheduleAction/crud.cms_entry.test.ts +++ b/packages/api-apw/__tests__/scheduleAction/crud.cms_entry.test.ts @@ -98,7 +98,7 @@ describe("Schedule action CRUD Test - CMS Entry type", () => { */ let updateItemResultWithError; try { - // @ts-ignore + // @ts-expect-error updateItemResultWithError = await scheduleActionCrud.update(scheduledAction.id, { action: ApwScheduleActionTypes.UNPUBLISH }); diff --git a/packages/api-apw/__tests__/scheduleAction/crud.page.test.ts b/packages/api-apw/__tests__/scheduleAction/crud.page.test.ts index 6cac467bb1e..7227d2907af 100644 --- a/packages/api-apw/__tests__/scheduleAction/crud.page.test.ts +++ b/packages/api-apw/__tests__/scheduleAction/crud.page.test.ts @@ -94,7 +94,7 @@ describe("Schedule action CRUD Test - Page type", () => { */ let updateItemResultWithError; try { - // @ts-ignore + // @ts-expect-error updateItemResultWithError = await scheduleActionCrud.update(scheduledAction.id, { action: ApwScheduleActionTypes.UNPUBLISH }); diff --git a/packages/api-apw/__tests__/utils/createGraphQlHandler.ts b/packages/api-apw/__tests__/utils/createGraphQlHandler.ts index 6313cf42ed8..8701ffca6a3 100644 --- a/packages/api-apw/__tests__/utils/createGraphQlHandler.ts +++ b/packages/api-apw/__tests__/utils/createGraphQlHandler.ts @@ -142,7 +142,11 @@ export const createGraphQlHandler = (params: GQLHandlerCallableParams) => { new CmsParametersPlugin(async context => { const locale = context.i18n.getContentLocale()?.code || "en-US"; return { - // @ts-ignore + /** + * This will be fixed with type augmenting. + * Currently, request.params.type is unknown. + */ + // @ts-expect-error type: context.request?.params?.type || "read", locale }; diff --git a/packages/api-apw/src/scheduler/createScheduleActionMethods.ts b/packages/api-apw/src/scheduler/createScheduleActionMethods.ts index 0da644e5011..dca550a2717 100644 --- a/packages/api-apw/src/scheduler/createScheduleActionMethods.ts +++ b/packages/api-apw/src/scheduler/createScheduleActionMethods.ts @@ -1,7 +1,7 @@ /** * Package @commodo/fields does not have types. */ -// @ts-ignore +// @ts-expect-error import { string, withFields } from "@commodo/fields"; import { validation } from "@webiny/validation"; import { mdbid } from "@webiny/utils"; diff --git a/packages/api-apw/src/storageOperations/changeRequestStorageOperations.ts b/packages/api-apw/src/storageOperations/changeRequestStorageOperations.ts index 3784e10a0bd..6df376d125e 100644 --- a/packages/api-apw/src/storageOperations/changeRequestStorageOperations.ts +++ b/packages/api-apw/src/storageOperations/changeRequestStorageOperations.ts @@ -100,7 +100,8 @@ export const createChangeRequestStorageOperations = ( const entry = await security.withoutAuthorization(async () => { return cms.updateEntry(model, params.id, { ...existingEntry, - ...params.data + ...params.data, + savedOn: new Date() }); }); return getFieldValues({ diff --git a/packages/api-apw/src/storageOperations/commentStorageOperations.ts b/packages/api-apw/src/storageOperations/commentStorageOperations.ts index e81caf5c30d..f6e434a6f40 100644 --- a/packages/api-apw/src/storageOperations/commentStorageOperations.ts +++ b/packages/api-apw/src/storageOperations/commentStorageOperations.ts @@ -104,7 +104,8 @@ export const createCommentStorageOperations = ({ const entry = await security.withoutAuthorization(async () => { return cms.updateEntry(model, params.id, { ...existingEntry, - ...params.data + ...params.data, + savedOn: new Date() }); }); diff --git a/packages/api-apw/src/storageOperations/contentReviewStorageOperations.ts b/packages/api-apw/src/storageOperations/contentReviewStorageOperations.ts index 0dd70860138..9ee9569e097 100644 --- a/packages/api-apw/src/storageOperations/contentReviewStorageOperations.ts +++ b/packages/api-apw/src/storageOperations/contentReviewStorageOperations.ts @@ -68,7 +68,8 @@ export const createContentReviewStorageOperations = ({ const entry = await security.withoutAuthorization(async () => { return cms.updateEntry(model, params.id, { ...existingEntry, - ...params.data + ...params.data, + savedOn: new Date() }); }); return getFieldValues(entry, baseFields); diff --git a/packages/api-apw/src/storageOperations/reviewerStorageOperations.ts b/packages/api-apw/src/storageOperations/reviewerStorageOperations.ts index c846c3bf4a1..857aabf5961 100644 --- a/packages/api-apw/src/storageOperations/reviewerStorageOperations.ts +++ b/packages/api-apw/src/storageOperations/reviewerStorageOperations.ts @@ -64,7 +64,8 @@ export const createReviewerStorageOperations = ({ const entry = await security.withoutAuthorization(async () => { return cms.updateEntry(model, params.id, { ...existingEntry, - ...params.data + ...params.data, + savedOn: new Date() }); }); return getFieldValues(entry, baseFields); diff --git a/packages/api-apw/src/storageOperations/workflowStorageOperations.ts b/packages/api-apw/src/storageOperations/workflowStorageOperations.ts index 8b75c1083e5..1a5470ba82e 100644 --- a/packages/api-apw/src/storageOperations/workflowStorageOperations.ts +++ b/packages/api-apw/src/storageOperations/workflowStorageOperations.ts @@ -87,7 +87,8 @@ export const createWorkflowStorageOperations = ( const existingEntry = await getWorkflow({ id: params.id }); const input = { ...existingEntry, - ...params.data + ...params.data, + savedOn: new Date() }; const data = formatReviewersForRefInput( input as CreateApwWorkflowParams, diff --git a/packages/api-audit-logs/__tests__/helpers/handlerCore.ts b/packages/api-audit-logs/__tests__/helpers/handlerCore.ts index 4467369cebd..b5e22a2101f 100644 --- a/packages/api-audit-logs/__tests__/helpers/handlerCore.ts +++ b/packages/api-audit-logs/__tests__/helpers/handlerCore.ts @@ -108,7 +108,6 @@ export const createHandlerCore = (params?: CreateHandlerCoreParams) => { id: apiKey, name: apiKey, tenant: tenant.id, - // @ts-ignore permissions: identity?.permissions || [], token, createdBy: { diff --git a/packages/api-file-manager-s3/src/utils/mimeTypes.ts b/packages/api-file-manager-s3/src/utils/mimeTypes.ts index 3f1f0c747bc..799de8c16bc 100644 --- a/packages/api-file-manager-s3/src/utils/mimeTypes.ts +++ b/packages/api-file-manager-s3/src/utils/mimeTypes.ts @@ -1,6 +1,6 @@ -// @ts-ignore +// @ts-expect-error import vendorTypes from "mime/types/other"; -// @ts-ignore +// @ts-expect-error import standardTypes from "mime/types/standard"; /** diff --git a/packages/api-file-manager/__tests__/file.customDates.test.ts b/packages/api-file-manager/__tests__/file.customDates.test.ts new file mode 100644 index 00000000000..a6688fb1358 --- /dev/null +++ b/packages/api-file-manager/__tests__/file.customDates.test.ts @@ -0,0 +1,58 @@ +import useGqlHandler from "./utils/useGqlHandler"; +import { fileAData, ids } from "./mocks/files"; + +describe("file custom dates", () => { + const { createFile, updateFile } = useGqlHandler(); + + it("should create and update file with custom dates", async () => { + const [createResponse] = await createFile( + { + data: { + ...fileAData, + createdOn: "1995-01-01T00:00:00.000Z", + savedOn: "1995-01-01T00:00:00.000Z" + } + }, + ["createdOn", "savedOn"] + ); + expect(createResponse).toEqual({ + data: { + fileManager: { + createFile: { + data: { + ...fileAData, + createdOn: "1995-01-01T00:00:00.000Z", + savedOn: "1995-01-01T00:00:00.000Z" + }, + error: null + } + } + } + }); + + const [updateResponse] = await updateFile( + { + id: ids.A, + data: { + createdOn: "2005-01-01T00:00:00.000Z", + savedOn: "2005-01-01T00:00:00.000Z" + } + }, + ["createdOn", "savedOn"] + ); + expect(updateResponse).toEqual({ + data: { + fileManager: { + updateFile: { + data: { + ...fileAData, + createdOn: "2005-01-01T00:00:00.000Z", + savedOn: "2005-01-01T00:00:00.000Z" + }, + error: null + } + } + } + }); + }); +}); diff --git a/packages/api-file-manager/__tests__/file.customIdentities.test.ts b/packages/api-file-manager/__tests__/file.customIdentities.test.ts new file mode 100644 index 00000000000..5776917fe4e --- /dev/null +++ b/packages/api-file-manager/__tests__/file.customIdentities.test.ts @@ -0,0 +1,107 @@ +import useGqlHandler from "./utils/useGqlHandler"; +import { fileAData } from "./mocks/files"; +import { SecurityIdentity } from "@webiny/api-security/types"; + +const extraFields = ["createdBy {id displayName type}", "modifiedBy {id displayName type}"]; +describe("file custom identities", () => { + const { createFile, updateFile, identity: defaultIdentity } = useGqlHandler(); + + const mockIdentityOne: SecurityIdentity = { + id: "mock-identity-one", + displayName: "Mock Identity One", + type: "mockOne" + }; + const mockIdentityTwo: SecurityIdentity = { + id: "mock-identity-two", + displayName: "Mock Identity Two", + type: "mockTwo" + }; + + it("should create a file with custom identity", async () => { + const [createRegularResponse] = await createFile( + { + data: { + ...fileAData + } + }, + extraFields + ); + expect(createRegularResponse).toEqual({ + data: { + fileManager: { + createFile: { + data: { + ...fileAData, + createdBy: defaultIdentity, + modifiedBy: null + }, + error: null + } + } + } + }); + + const [createCustomIdentityResponse] = await createFile( + { + data: { + ...fileAData, + createdBy: mockIdentityOne, + modifiedBy: mockIdentityTwo + } + }, + extraFields + ); + expect(createCustomIdentityResponse).toEqual({ + data: { + fileManager: { + createFile: { + data: { + ...fileAData, + createdBy: mockIdentityOne, + modifiedBy: mockIdentityTwo + }, + error: null + } + } + } + }); + }); + + it("should update a file with custom identity", async () => { + const [createResponse] = await createFile( + { + data: { + ...fileAData + } + }, + extraFields + ); + + const id = createResponse.data.fileManager.createFile.data.id; + + const [updateResponse] = await updateFile( + { + id, + data: { + createdBy: mockIdentityOne, + modifiedBy: mockIdentityTwo + } + }, + extraFields + ); + expect(updateResponse).toEqual({ + data: { + fileManager: { + updateFile: { + data: { + ...fileAData, + createdBy: mockIdentityOne, + modifiedBy: mockIdentityTwo + }, + error: null + } + } + } + }); + }); +}); diff --git a/packages/api-file-manager/__tests__/file.lifecycle.test.ts b/packages/api-file-manager/__tests__/file.lifecycle.test.ts index 4b63f419921..c2d265fa752 100644 --- a/packages/api-file-manager/__tests__/file.lifecycle.test.ts +++ b/packages/api-file-manager/__tests__/file.lifecycle.test.ts @@ -77,7 +77,7 @@ describe("File lifecycle events", () => { * Parameters that were received in the lifecycle hooks must be valid as well. */ const beforeCreate = tracker.getLast("file:beforeCreate"); - expect(beforeCreate && beforeCreate.params[0]).toEqual({ + expect(beforeCreate && beforeCreate.params[0]).toMatchObject({ file: { ...fileData, ...hookParamsExpected, @@ -88,7 +88,7 @@ describe("File lifecycle events", () => { } }); const afterCreate = tracker.getLast("file:beforeCreate"); - expect(afterCreate && afterCreate.params[0]).toEqual({ + expect(afterCreate && afterCreate.params[0]).toMatchObject({ file: { ...fileData, ...hookParamsExpected, @@ -138,7 +138,7 @@ describe("File lifecycle events", () => { * Parameters that were received in the lifecycle hooks must be valid as well. */ const beforeUpdate = tracker.getLast("file:beforeUpdate"); - expect(beforeUpdate && beforeUpdate.params[0]).toEqual({ + expect(beforeUpdate && beforeUpdate.params[0]).toMatchObject({ input: { tags: [...fileData.tags, TAG] }, original: { ...fileData, @@ -160,7 +160,7 @@ describe("File lifecycle events", () => { } }); const afterUpdate = tracker.getLast("file:afterUpdate"); - expect(afterUpdate && afterUpdate.params[0]).toEqual({ + expect(afterUpdate && afterUpdate.params[0]).toMatchObject({ input: { tags: [...fileData.tags, TAG] }, original: { ...fileData, @@ -219,6 +219,7 @@ describe("File lifecycle events", () => { file: { ...fileData, ...hookParamsExpected, + modifiedBy: null, location: { folderId: ROOT_FOLDER }, @@ -230,6 +231,7 @@ describe("File lifecycle events", () => { file: { ...fileData, ...hookParamsExpected, + modifiedBy: null, location: { folderId: ROOT_FOLDER }, diff --git a/packages/api-file-manager/__tests__/mocks/file.sdl.ts b/packages/api-file-manager/__tests__/mocks/file.sdl.ts index 7469ef7d170..2914a2427ae 100644 --- a/packages/api-file-manager/__tests__/mocks/file.sdl.ts +++ b/packages/api-file-manager/__tests__/mocks/file.sdl.ts @@ -98,6 +98,7 @@ export default /* GraphQL */ ` savedOn: DateTime! createdOn: DateTime! createdBy: FmCreatedBy! + modifiedBy: FmCreatedBy src: String location: FmFile_Location name: String @@ -127,8 +128,18 @@ export default /* GraphQL */ ` article: RefFieldInput } + input FmCreatedByInput { + id: ID! + displayName: String! + type: String! + } + input FmFileCreateInput { id: ID! + createdOn: DateTime + savedOn: DateTime + createdBy: FmCreatedByInput + modifiedBy: FmCreatedByInput location: FmFile_LocationInput name: String key: String @@ -141,6 +152,10 @@ export default /* GraphQL */ ` } input FmFileUpdateInput { + createdOn: DateTime + savedOn: DateTime + createdBy: FmCreatedByInput + modifiedBy: FmCreatedByInput location: FmFile_LocationInput name: String key: String @@ -176,6 +191,13 @@ export default /* GraphQL */ ` savedOn_lte: DateTime savedOn_between: [DateTime!] savedOn_not_between: [DateTime!] + publishedOn: DateTime + publishedOn_gt: DateTime + publishedOn_gte: DateTime + publishedOn_lt: DateTime + publishedOn_lte: DateTime + publishedOn_between: [DateTime!] + publishedOn_not_between: [DateTime!] createdBy: String createdBy_not: String createdBy_in: [String!] diff --git a/packages/api-file-manager/__tests__/utils/tenancySecurity.ts b/packages/api-file-manager/__tests__/utils/tenancySecurity.ts index 66639ef1764..50b1d430999 100644 --- a/packages/api-file-manager/__tests__/utils/tenancySecurity.ts +++ b/packages/api-file-manager/__tests__/utils/tenancySecurity.ts @@ -16,6 +16,12 @@ interface Config { identity?: SecurityIdentity | null; } +export const defaultIdentity: SecurityIdentity = { + id: "12345678", + type: "admin", + displayName: "John Doe" +}; + export const createTenancyAndSecurity = ({ permissions, identity }: Config) => { const securityStorage = getStorageOps("security"); const tenancyStorage = getStorageOps("tenancy"); @@ -41,13 +47,7 @@ export const createTenancyAndSecurity = ({ permissions, identity }: Config) => { }); context.security.addAuthenticator(async () => { - return ( - identity || { - id: "12345678", - type: "admin", - displayName: "John Doe" - } - ); + return identity || defaultIdentity; }); context.security.addAuthorizer(async () => { diff --git a/packages/api-file-manager/__tests__/utils/useGqlHandler.ts b/packages/api-file-manager/__tests__/utils/useGqlHandler.ts index d871a2de00e..ac855a33970 100644 --- a/packages/api-file-manager/__tests__/utils/useGqlHandler.ts +++ b/packages/api-file-manager/__tests__/utils/useGqlHandler.ts @@ -4,19 +4,20 @@ import { until } from "@webiny/project-utils/testing/helpers/until"; import { CREATE_FILE, CREATE_FILES, - UPDATE_FILE, DELETE_FILE, GET_FILE, LIST_FILES, - LIST_TAGS + LIST_TAGS, + UPDATE_FILE } from "~tests/graphql/file"; import { + GET_SETTINGS, INSTALL, IS_INSTALLED, - GET_SETTINGS, UPDATE_SETTINGS } from "~tests/graphql/fileManagerSettings"; import { HandlerParams, handlerPlugins } from "./plugins"; +import { defaultIdentity } from "~tests/utils/tenancySecurity"; interface InvokeParams { httpMethod?: "POST"; @@ -58,6 +59,7 @@ export default (params: HandlerParams = {}) => { until, handler, invoke, + identity: params.identity || defaultIdentity, // Files async createFile(variables: Record, fields: string[] = []) { return invoke({ body: { query: CREATE_FILE(fields), variables } }); diff --git a/packages/api-file-manager/src/cmsFileStorage/CmsFilesStorage.ts b/packages/api-file-manager/src/cmsFileStorage/CmsFilesStorage.ts index b014d10aa67..f78b03367f4 100644 --- a/packages/api-file-manager/src/cmsFileStorage/CmsFilesStorage.ts +++ b/packages/api-file-manager/src/cmsFileStorage/CmsFilesStorage.ts @@ -164,14 +164,7 @@ export class CmsFilesStorage implements FileManagerFilesStorageOperations { where: { entryId: file.id, latest: true } }); - const values = omit(file, [ - "id", - "createdOn", - "createdBy", - "tenant", - "locale", - "webinyVersion" - ]); + const values = omit(file, ["id", "tenant", "locale", "webinyVersion"]); const updatedEntry = await this.cms.updateEntry(model, entry.id, { ...values, @@ -188,6 +181,7 @@ export class CmsFilesStorage implements FileManagerFilesStorageOperations { return { id: entry.entryId, createdBy: entry.createdBy, + modifiedBy: entry.modifiedBy || null, createdOn: entry.createdOn, savedOn: entry.savedOn, locale: entry.locale, diff --git a/packages/api-file-manager/src/createFileManager/files.crud.ts b/packages/api-file-manager/src/createFileManager/files.crud.ts index 3147e097b02..20419c722ae 100644 --- a/packages/api-file-manager/src/createFileManager/files.crud.ts +++ b/packages/api-file-manager/src/createFileManager/files.crud.ts @@ -12,6 +12,7 @@ import { import { FileManagerConfig } from "~/createFileManager/index"; import { ROOT_FOLDER } from "~/contants"; import { NotAuthorizedError } from "@webiny/api-security"; +import { getDate } from "@webiny/api-headless-cms/utils/date"; export const createFilesCrud = (config: FileManagerConfig): FilesCRUD => { const { @@ -59,6 +60,7 @@ export const createFilesCrud = (config: FileManagerConfig): FilesCRUD => { // Extract ID from file key const [id] = input.key.split("/"); + const date = new Date(); const file: File = { ...input, tags: Array.isArray(input.tags) ? input.tags : [], @@ -72,13 +74,11 @@ export const createFilesCrud = (config: FileManagerConfig): FilesCRUD => { ...(input.meta || {}) }, tenant: getTenantId(), - createdOn: new Date().toISOString(), - savedOn: new Date().toISOString(), - createdBy: { - id: identity.id, - displayName: identity.displayName, - type: identity.type - }, + createdOn: getDate(input.createdOn, date), + savedOn: getDate(input.savedOn, date), + createdBy: input.createdBy || identity, + ownedBy: input.createdBy || identity, + modifiedBy: input.modifiedBy, locale: getLocaleCode(), webinyVersion: WEBINY_VERSION }; @@ -126,6 +126,10 @@ export const createFilesCrud = (config: FileManagerConfig): FilesCRUD => { const file: File = { ...original, ...input, + createdBy: input.createdBy || original.createdBy, + modifiedBy: input.modifiedBy, + createdOn: getDate(input.createdOn, original.createdOn), + savedOn: getDate(input.savedOn, new Date()), tags: Array.isArray(input.tags) ? input.tags : Array.isArray(original.tags) diff --git a/packages/api-file-manager/src/createFileManager/filevalidation.disabled.ts b/packages/api-file-manager/src/createFileManager/filevalidation.disabled.ts deleted file mode 100644 index 801f14ba823..00000000000 --- a/packages/api-file-manager/src/createFileManager/filevalidation.disabled.ts +++ /dev/null @@ -1,54 +0,0 @@ -// /** -// * Package @commodo/fields does not have types -// */ -// // @ts-ignore -// import { withFields, string, number, onSet } from "@commodo/fields"; -// /** -// * Package commodo-fields-object does not have types -// */ -// // @ts-ignore -// import { object } from "commodo-fields-object"; -// import { validation } from "@webiny/validation"; -// -// export default (create = true) => { -// return withFields({ -// key: string({ -// validation: validation.create(`${create ? "required," : ""}maxLength:1000`) -// }), -// name: string({ validation: validation.create("maxLength:1000") }), -// size: number(), -// type: string({ validation: validation.create("maxLength:255") }), -// meta: object({ value: { private: false } }), -// tags: onSet((value: string[]) => { -// if (!Array.isArray(value)) { -// return null; -// } -// -// return value.map(item => item.toLowerCase()); -// })( -// string({ -// list: true, -// validation: (tags: string[]) => { -// if (!Array.isArray(tags)) { -// return; -// } -// -// if (tags.length > 15) { -// throw Error("You cannot set more than 15 tags."); -// } -// -// for (let i = 0; i < tags.length; i++) { -// const tag = tags[i]; -// if (typeof tag !== "string") { -// throw Error("Tag must be typeof string."); -// } -// -// if (tag.length > 50) { -// throw Error(`Tag ${tag} is more than 50 characters long.`); -// } -// } -// } -// }) -// ) -// })(); -// }; diff --git a/packages/api-file-manager/src/createFileManager/settings.crud.ts b/packages/api-file-manager/src/createFileManager/settings.crud.ts index 662b934cc37..84d9802886e 100644 --- a/packages/api-file-manager/src/createFileManager/settings.crud.ts +++ b/packages/api-file-manager/src/createFileManager/settings.crud.ts @@ -2,7 +2,7 @@ import { createTopic } from "@webiny/pubsub"; /** * Package @commodo/fields does not have types. */ -// @ts-ignore +// @ts-expect-error import { withFields, string, number, onSet } from "@commodo/fields"; import { validation } from "@webiny/validation"; import { FileManagerSettings, SettingsCRUD } from "~/types"; diff --git a/packages/api-file-manager/src/graphql/baseSchema.ts b/packages/api-file-manager/src/graphql/baseSchema.ts index 8be6791e7e0..820bd9a66e2 100644 --- a/packages/api-file-manager/src/graphql/baseSchema.ts +++ b/packages/api-file-manager/src/graphql/baseSchema.ts @@ -15,6 +15,7 @@ export const createBaseSchema = () => { type FmCreatedBy { id: ID displayName: String + type: String } type FmListMeta { diff --git a/packages/api-file-manager/src/graphql/createFilesTypeDefs.ts b/packages/api-file-manager/src/graphql/createFilesTypeDefs.ts index b3733d9b6a4..cbe7ce1c79a 100644 --- a/packages/api-file-manager/src/graphql/createFilesTypeDefs.ts +++ b/packages/api-file-manager/src/graphql/createFilesTypeDefs.ts @@ -75,18 +75,33 @@ export const createFilesTypeDefs = (params: CreateFilesTypeDefsParams): string = savedOn: DateTime! createdOn: DateTime! createdBy: FmCreatedBy! + modifiedBy: FmCreatedBy src: String ${fieldTypes.map(f => f.fields).join("\n")} } ${inputCreateFields.map(f => f.typeDefs).join("\n")} + + input FmCreatedByInput { + id: ID! + displayName: String! + type: String! + } input FmFileCreateInput { id: ID! + createdOn: DateTime + savedOn: DateTime + createdBy: FmCreatedByInput + modifiedBy: FmCreatedByInput ${inputCreateFields.map(f => f.fields).join("\n")} } input FmFileUpdateInput { + createdOn: DateTime + savedOn: DateTime + createdBy: FmCreatedByInput + modifiedBy: FmCreatedByInput ${inputUpdateFields.map(f => f.fields).join("\n")} } @@ -131,7 +146,7 @@ export const createFilesTypeDefs = (params: CreateFilesTypeDefsParams): string = data: [FmFile!] error: FmError } - + type FmFileModelResponse { data: JSON error: FmError diff --git a/packages/api-file-manager/src/graphql/filesSchema.ts b/packages/api-file-manager/src/graphql/filesSchema.ts index bbef5cf30e7..3d5dea75be9 100644 --- a/packages/api-file-manager/src/graphql/filesSchema.ts +++ b/packages/api-file-manager/src/graphql/filesSchema.ts @@ -62,15 +62,19 @@ export const createFilesSchema = (params: CreateFilesTypeDefsParams) => { }, FmMutation: { async createFile(_, args: any, context) { - return resolve(() => context.fileManager.createFile(args.data, args.meta)); + return resolve(() => { + return context.fileManager.createFile(args.data, args.meta); + }); }, async createFiles(_, args: any, context) { - return resolve(() => - context.fileManager.createFilesInBatch(args.data, args.meta) - ); + return resolve(() => { + return context.fileManager.createFilesInBatch(args.data, args.meta); + }); }, async updateFile(_, args: any, context) { - return resolve(() => context.fileManager.updateFile(args.id, args.data)); + return resolve(() => { + return context.fileManager.updateFile(args.id, args.data); + }); }, async deleteFile(_, args: any, context) { return resolve(async () => { diff --git a/packages/api-file-manager/src/modelModifier/CmsModelModifier.ts b/packages/api-file-manager/src/modelModifier/CmsModelModifier.ts index a53f7285aa2..eb6e427ee85 100644 --- a/packages/api-file-manager/src/modelModifier/CmsModelModifier.ts +++ b/packages/api-file-manager/src/modelModifier/CmsModelModifier.ts @@ -4,7 +4,7 @@ import { FILE_MODEL_ID } from "~/cmsFileStorage/file.model"; import { createModelField } from "~/cmsFileStorage/createModelField"; import { CmsPrivateModelFull } from "@webiny/api-headless-cms"; -type CmsModelField = Omit; +type CmsModelField = Omit & { bulkEdit?: boolean }; class CmsModelFieldsModifier { private fields: BaseModelField[]; @@ -14,8 +14,11 @@ class CmsModelFieldsModifier { } addField(field: CmsModelField) { + const { bulkEdit, tags, ...rest } = field; + this.fields.push({ - ...field, + ...rest, + tags: (tags ?? []).concat(bulkEdit ? ["$bulk-edit"] : []), storageId: `${field.type}@${field.id}` }); } diff --git a/packages/api-file-manager/src/types.ts b/packages/api-file-manager/src/types.ts index ead8897f774..28b574cc3dc 100644 --- a/packages/api-file-manager/src/types.ts +++ b/packages/api-file-manager/src/types.ts @@ -4,9 +4,10 @@ import { TenancyContext } from "@webiny/api-tenancy/types"; import { SecurityContext, SecurityPermission } from "@webiny/api-security/types"; import { Context } from "@webiny/api/types"; import { FileLifecycleEvents } from "./types/file.lifecycle"; -import { File } from "./types/file"; +import { CreatedBy, File } from "./types/file"; import { Topic } from "@webiny/pubsub/types"; import { CmsContext } from "@webiny/api-headless-cms/types"; + export * from "./types/file.lifecycle"; export * from "./types/file"; @@ -31,6 +32,10 @@ export interface FilePermission extends SecurityPermission { export interface FileInput { id: string; + createdOn?: string | Date | null; + savedOn?: string | Date | null; + createdBy?: CreatedBy | null; + modifiedBy?: CreatedBy | null; key: string; name: string; size: number; diff --git a/packages/api-file-manager/src/types/file.ts b/packages/api-file-manager/src/types/file.ts index 25195243f7a..963162f5448 100644 --- a/packages/api-file-manager/src/types/file.ts +++ b/packages/api-file-manager/src/types/file.ts @@ -13,6 +13,7 @@ export interface File { createdOn: string; savedOn: string; createdBy: CreatedBy; + modifiedBy?: CreatedBy | null; /** * Added with new storage operations refactoring. */ diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts index 37dc865aed6..65b8c4b1c56 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts @@ -118,7 +118,7 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { return await this.cms.createEntryRevisionFrom(model, form.id, { status: "draft", published: false, - publishedOn: null, + publishedOn: undefined, locked: false, stats: { submissions: 0, diff --git a/packages/api-form-builder/src/plugins/crud/forms.crud.ts b/packages/api-form-builder/src/plugins/crud/forms.crud.ts index 72f1f161fbe..76384326f78 100644 --- a/packages/api-form-builder/src/plugins/crud/forms.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/forms.crud.ts @@ -284,7 +284,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { version, locked: false, published: false, - publishedOn: null, + publishedOn: undefined, status: getStatus({ published: false, locked: false @@ -610,7 +610,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { }, locked: false, published: false, - publishedOn: null, + publishedOn: undefined, status: getStatus({ published: false, locked: false }), tenant: getTenant().id, webinyVersion: context.WEBINY_VERSION diff --git a/packages/api-form-builder/src/types.ts b/packages/api-form-builder/src/types.ts index 65f261401f2..71960abe8a0 100644 --- a/packages/api-form-builder/src/types.ts +++ b/packages/api-form-builder/src/types.ts @@ -108,7 +108,7 @@ export interface FbForm { version: number; locked: boolean; published: boolean; - publishedOn: string | null; + publishedOn: string | Date | undefined; status: string; fields: FbFormField[]; steps: FbFormStep[]; diff --git a/packages/api-headless-cms-ddb-es/__tests__/plugins/elasticsearchSortModifier.test.ts b/packages/api-headless-cms-ddb-es/__tests__/plugins/elasticsearchSortModifier.test.ts index efc22451af6..ebc86d212f8 100644 --- a/packages/api-headless-cms-ddb-es/__tests__/plugins/elasticsearchSortModifier.test.ts +++ b/packages/api-headless-cms-ddb-es/__tests__/plugins/elasticsearchSortModifier.test.ts @@ -11,7 +11,6 @@ describe("Elasticsearch sort modifier plugin", () => { if (typeof sort !== "object") { return; } - // @ts-ignore sort["newField"] = { order: "asc" }; @@ -47,10 +46,8 @@ describe("Elasticsearch sort modifier plugin", () => { } for (const key in sort) { - // @ts-ignore delete sort[key]; } - // @ts-ignore sort["_script"] = { type: "number", script: { diff --git a/packages/api-headless-cms-ddb-es/src/helpers/entryIndexHelpers.ts b/packages/api-headless-cms-ddb-es/src/helpers/entryIndexHelpers.ts index ebcd8b18bf9..10bd3c915f2 100644 --- a/packages/api-headless-cms-ddb-es/src/helpers/entryIndexHelpers.ts +++ b/packages/api-headless-cms-ddb-es/src/helpers/entryIndexHelpers.ts @@ -191,15 +191,15 @@ export const extractEntriesFromIndex = ({ /** * If we want to remove the rawValues, TYPE, latest, published and __type, we must make them optional or ignore them. */ - // @ts-ignore + // @ts-expect-error delete newEntry["rawValues"]; - // @ts-ignore + // @ts-expect-error delete newEntry["TYPE"]; - // @ts-ignore + // @ts-expect-error delete newEntry["__type"]; - // @ts-ignore + // @ts-expect-error delete newEntry["latest"]; - // @ts-ignore + // @ts-expect-error delete newEntry["published"]; list.push({ ...newEntry }); } diff --git a/packages/api-headless-cms-ddb-es/src/operations/entry/elasticsearch/filtering/populated.ts b/packages/api-headless-cms-ddb-es/src/operations/entry/elasticsearch/filtering/populated.ts index e312f0123af..6628effb437 100644 --- a/packages/api-headless-cms-ddb-es/src/operations/entry/elasticsearch/filtering/populated.ts +++ b/packages/api-headless-cms-ddb-es/src/operations/entry/elasticsearch/filtering/populated.ts @@ -13,7 +13,7 @@ export const getPopulated = ( /** * TODO figure out better types. */ - // @ts-ignore + // @ts-expect-error result[key] = value; } return result; diff --git a/packages/api-headless-cms-ddb/__tests__/operations/entry/filtering/filter.test.ts b/packages/api-headless-cms-ddb/__tests__/operations/entry/filtering/filter.test.ts index 6c53a005b1b..e279200003b 100644 --- a/packages/api-headless-cms-ddb/__tests__/operations/entry/filtering/filter.test.ts +++ b/packages/api-headless-cms-ddb/__tests__/operations/entry/filtering/filter.test.ts @@ -34,7 +34,7 @@ describe("filtering", () => { "should filter entries by createdOn - %s results", async (expectedResults, modifier) => { const records = createEntries(100).map(r => { - // @ts-ignore + // @ts-expect-error delete r.values; return r; diff --git a/packages/api-headless-cms/__tests__/contentAPI/aco/setup/plugins.ts b/packages/api-headless-cms/__tests__/contentAPI/aco/setup/plugins.ts index 66f62e479b0..7f06298be52 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/aco/setup/plugins.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/aco/setup/plugins.ts @@ -68,7 +68,6 @@ export const createHandlerCore = (params: CreateHandlerCoreParams) => { id: apiKey, name: apiKey, tenant: tenant.id, - // @ts-ignore permissions: identity?.permissions || [], token, createdBy: { diff --git a/packages/api-headless-cms/__tests__/contentAPI/contentEntry.withId.test.ts b/packages/api-headless-cms/__tests__/contentAPI/contentEntry.withId.test.ts index e92abeca344..848f3cf2bd8 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/contentEntry.withId.test.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/contentEntry.withId.test.ts @@ -4,8 +4,8 @@ */ import { setupContentModelGroup, setupContentModels } from "~tests/testHelpers/setup"; import { useCategoryManageHandler } from "~tests/testHelpers/useCategoryManageHandler"; -import { useCategoryReadHandler } from "~tests/testHelpers/useCategoryReadHandler"; import { useProductManageHandler } from "~tests/testHelpers/useProductManageHandler"; + interface Category { id: string; title: string; @@ -27,9 +27,6 @@ describe("Content entry with user defined ID", () => { const productManageHandler = useProductManageHandler({ path: "manage/en-US" }); - const categoryReadHandler = useCategoryReadHandler({ - path: "read/en-US" - }); beforeEach(async () => { const group = await setupContentModelGroup(categoryManageHandler); @@ -60,19 +57,6 @@ describe("Content entry with user defined ID", () => { } }); - await categoryManageHandler.until( - () => { - return categoryManageHandler.listCategories().then(([data]) => data); - }, - ({ data }) => { - const entry = data.listCategories.data[0]; - return entry.id === id; - }, - { - name: "list categories after create" - } - ); - const [getAfterCreateResponse] = await categoryManageHandler.getCategory({ revision: id }); @@ -122,22 +106,6 @@ describe("Content entry with user defined ID", () => { } }); - await categoryManageHandler.until( - () => { - return categoryManageHandler.listCategories().then(([data]) => data); - }, - ({ data }) => { - const entry = data.listCategories.data[0]; - if (entry.title !== updatedTitle) { - return false; - } - return entry.id === id; - }, - { - name: "list categories after update" - } - ); - const [getAfterUpdateResponse] = await categoryManageHandler.getCategory({ revision: id }); @@ -183,39 +151,6 @@ describe("Content entry with user defined ID", () => { } }); - await categoryManageHandler.until( - () => { - return categoryManageHandler.listCategories().then(([data]) => data); - }, - ({ data }) => { - const entry = data.listCategories.data[0]; - if (entry.title !== updatedTitle) { - return false; - } else if (entry.meta.status !== "published") { - return false; - } - return entry.id === id; - }, - { - name: "list categories after published" - } - ); - await categoryManageHandler.until( - () => { - return categoryReadHandler.listCategories().then(([data]) => data); - }, - ({ data }) => { - const entry = data.listCategories.data[0]; - if (entry.title !== updatedTitle) { - return false; - } - return entry.id === id; - }, - { - name: "[READ] list categories after published" - } - ); - const [getAfterPublishResponse] = await categoryManageHandler.getCategory({ revision: id }); @@ -283,35 +218,6 @@ describe("Content entry with user defined ID", () => { } }); - await categoryManageHandler.until( - () => { - return categoryManageHandler.listCategories().then(([data]) => data); - }, - ({ data }) => { - const entry = data.listCategories.data[0]; - if (entry.title !== updatedTitle) { - return false; - } else if (entry.meta.status !== "unpublished") { - return false; - } - return entry.id === id; - }, - { - name: "list categories after unpublished" - } - ); - await categoryManageHandler.until( - () => { - return categoryReadHandler.listCategories().then(([data]) => data); - }, - ({ data }) => { - return data.listCategories.data.length === 0; - }, - { - name: "[READ] list categories after unpublished" - } - ); - const [getAfterUnpublishResponse] = await categoryManageHandler.getCategory({ revision: id }); @@ -438,18 +344,7 @@ describe("Content entry with user defined ID", () => { } }); const id = `${category.id}#0001`; - await categoryManageHandler.until( - () => { - return categoryManageHandler.listCategories().then(([data]) => data); - }, - ({ data }) => { - const entry = data.listCategories.data[0]; - return entry.id === id; - }, - { - name: "list categories after create" - } - ); + const productCategory = { id, modelId: "category" diff --git a/packages/api-headless-cms/__tests__/contentAPI/contentEntryCustomDates.test.ts b/packages/api-headless-cms/__tests__/contentAPI/contentEntryCustomDates.test.ts new file mode 100644 index 00000000000..65f1b44a67b --- /dev/null +++ b/packages/api-headless-cms/__tests__/contentAPI/contentEntryCustomDates.test.ts @@ -0,0 +1,175 @@ +import { useCategoryManageHandler } from "../testHelpers/useCategoryManageHandler"; +import { setupGroupAndModels } from "~tests/testHelpers/setup"; + +describe("content entry custom dates", () => { + const manager = useCategoryManageHandler({ + path: "manage/en-US" + }); + + beforeEach(async () => { + await setupGroupAndModels({ + manager, + models: ["category"] + }); + }); + + it("should populate entry with custom dates", async () => { + const createValues = { + createdOn: "1997-01-01T00:00:00.000Z", + savedOn: "1998-01-01T00:00:00.000Z", + publishedOn: "1999-01-01T00:00:00.000Z" + }; + const [createResponse] = await manager.createCategory({ + data: { + title: "Fruits", + slug: "fruits", + ...createValues + } + }); + + expect(createResponse).toMatchObject({ + data: { + createCategory: { + data: { + savedOn: createValues.savedOn, + createdOn: createValues.createdOn, + meta: { + publishedOn: createValues.publishedOn + } + }, + error: null + } + } + }); + const entryId = createResponse.data.createCategory.data.entryId; + + const createFromValues = { + createdOn: "1997-02-01T00:00:00.000Z", + savedOn: "1998-02-01T00:00:00.000Z", + publishedOn: "1999-02-01T00:00:00.000Z" + }; + const [createFromResponse] = await manager.createCategoryFrom({ + revision: `${entryId}#0001`, + data: { + ...createFromValues + } + }); + expect(createFromResponse).toMatchObject({ + data: { + createCategoryFrom: { + data: { + savedOn: createFromValues.savedOn, + createdOn: createFromValues.createdOn, + meta: { + publishedOn: createFromValues.publishedOn + } + }, + error: null + } + } + }); + + const updateValues = { + createdOn: "1997-03-01T00:00:00.000Z", + savedOn: "1998-03-01T00:00:00.000Z", + publishedOn: "1999-03-01T00:00:00.000Z" + }; + const [updateResponse] = await manager.updateCategory({ + revision: `${entryId}#0002`, + data: { + ...updateValues + } + }); + expect(updateResponse).toMatchObject({ + data: { + updateCategory: { + data: { + savedOn: updateValues.savedOn, + createdOn: updateValues.createdOn, + meta: { + publishedOn: updateValues.publishedOn + } + }, + error: null + } + } + }); + }); + + it("should skip updating publishedOn and savedOn when user chooses to skip the update", async () => { + const createValues = { + createdOn: "1997-01-01T00:00:00.000Z", + savedOn: "1998-01-01T00:00:00.000Z", + publishedOn: "1999-01-01T00:00:00.000Z" + }; + const [createResponse] = await manager.createCategory({ + data: { + title: "Fruits", + slug: "fruits", + ...createValues + } + }); + const entryId = createResponse.data.createCategory.data.entryId; + + const [publishResponse] = await manager.publishCategory({ + revision: `${entryId}#0001` + }); + expect(publishResponse).toMatchObject({ + data: { + publishCategory: { + data: { + savedOn: expect.stringMatching(/^20/), + createdOn: createValues.createdOn, + meta: { + publishedOn: expect.stringMatching(/^20/) + } + }, + error: null + } + } + }); + + const [createFromResponse] = await manager.createCategoryFrom({ + revision: `${entryId}#0001`, + data: { + ...createValues + } + }); + expect(createFromResponse).toMatchObject({ + data: { + createCategoryFrom: { + data: { + savedOn: createValues.savedOn, + createdOn: createValues.createdOn, + meta: { + publishedOn: createValues.publishedOn + } + }, + error: null + } + } + }); + + const [publishCreatedFromResponse] = await manager.publishCategory({ + revision: `${entryId}#0002`, + options: { + updatePublishedOn: false, + updateSavedOn: false + } + }); + expect(publishCreatedFromResponse).toMatchObject({ + data: { + publishCategory: { + data: { + savedOn: createValues.savedOn, + createdOn: createValues.createdOn, + meta: { + publishedOn: createValues.publishedOn + } + }, + error: null + } + } + }); + }); +}); diff --git a/packages/api-headless-cms/__tests__/contentAPI/contentEntryCustomIdentities.test.s.ts b/packages/api-headless-cms/__tests__/contentAPI/contentEntryCustomIdentities.test.s.ts new file mode 100644 index 00000000000..63b58cfcf68 --- /dev/null +++ b/packages/api-headless-cms/__tests__/contentAPI/contentEntryCustomIdentities.test.s.ts @@ -0,0 +1,138 @@ +import { useCategoryManageHandler } from "~tests/testHelpers/useCategoryManageHandler"; +import { setupGroupAndModels } from "~tests/testHelpers/setup"; +import { SecurityIdentity } from "@webiny/api-security/types"; + +describe("content entry custom identities", () => { + const manager = useCategoryManageHandler({ + path: "manage/en-US" + }); + + const mockIdentityOne: SecurityIdentity = { + id: "mock-identity-one", + displayName: "Mock Identity One", + type: "mockOne" + }; + const mockIdentityTwo: SecurityIdentity = { + id: "mock-identity-two", + displayName: "Mock Identity Two", + type: "mockTwo" + }; + const mockIdentityThree: SecurityIdentity = { + id: "mock-identity-three", + displayName: "Mock Identity Three", + type: "mockThree" + }; + + beforeEach(async () => { + await setupGroupAndModels({ + manager, + models: ["category"] + }); + }); + + it("should be possible to create an entry with different identity than the current user", async () => { + const [createRegularResponse] = await manager.createCategory({ + data: { + title: "Category Regular Identity", + slug: "category-regular-identity" + } + }); + expect(createRegularResponse).toMatchObject({ + data: { + createCategory: { + data: { + createdBy: manager.identity, + modifiedBy: null, + ownedBy: manager.identity + }, + error: null + } + } + }); + + const [createCustomIdentityResponse] = await manager.createCategory({ + data: { + title: "Category Custom Identity", + slug: "category-custom-identity", + ownedBy: mockIdentityOne, + createdBy: mockIdentityTwo, + modifiedBy: mockIdentityThree + } + }); + + expect(createCustomIdentityResponse).toMatchObject({ + data: { + createCategory: { + data: { + createdBy: mockIdentityTwo, + modifiedBy: mockIdentityThree, + ownedBy: mockIdentityOne + }, + error: null + } + } + }); + }); + + it("should create a new entry revision with different identity than the current user", async () => { + const [createRegularResponse] = await manager.createCategory({ + data: { + title: "Category Regular Identity", + slug: "category-regular-identity" + } + }); + const id = createRegularResponse.data.createCategory.data.id; + + const [createRevisionCustomIdentityResponse] = await manager.createCategoryFrom({ + revision: id, + data: { + ownedBy: mockIdentityOne, + createdBy: mockIdentityTwo, + modifiedBy: mockIdentityThree + } + }); + expect(createRevisionCustomIdentityResponse).toMatchObject({ + data: { + createCategoryFrom: { + data: { + createdBy: mockIdentityTwo, + modifiedBy: mockIdentityThree, + ownedBy: mockIdentityOne + }, + error: null + } + } + }); + }); + + it("should update an entry with different identity than the current user", async () => { + const [createRegularResponse] = await manager.createCategory({ + data: { + title: "Category Regular Identity", + slug: "category-regular-identity" + } + }); + const id = createRegularResponse.data.createCategory.data.id; + + const [updateCustomIdentityResponse] = await manager.updateCategory({ + revision: id, + data: { + ownedBy: mockIdentityOne, + createdBy: mockIdentityTwo, + modifiedBy: mockIdentityThree + } + }); + expect(updateCustomIdentityResponse).toMatchObject({ + data: { + updateCategory: { + data: { + createdBy: mockIdentityTwo, + modifiedBy: mockIdentityThree, + ownedBy: mockIdentityOne + }, + error: null + } + } + }); + }); +}); diff --git a/packages/api-headless-cms/__tests__/contentAPI/contentEntryMetaField.test.ts b/packages/api-headless-cms/__tests__/contentAPI/contentEntryMetaField.test.ts index 8f0090f23fe..44e1fd0a3d6 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/contentEntryMetaField.test.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/contentEntryMetaField.test.ts @@ -1,6 +1,3 @@ -/** - * There must be the "until" in this file because we are using storage operations directly. - */ import models from "./mocks/contentModels"; import { CmsEntry, CmsGroup, CmsModel } from "~/types"; import { useCategoryManageHandler } from "../testHelpers/useCategoryManageHandler"; @@ -43,8 +40,7 @@ describe("Content Entry Meta Field", () => { createContentModelMutation, updateContentModelMutation, createContentModelGroupMutation, - storageOperations, - until + storageOperations } = useCategoryManageHandler(manageOpts); const setup = async () => { @@ -147,39 +143,6 @@ describe("Content Entry Meta Field", () => { ...publishedRecord }); - await until( - () => { - return storageOperations.entries.list(model, { - where: { - latest: true - }, - limit: 10000 - }); - }, - (response: any) => { - return response.items.length === 1; - }, - { - name: "list latest storage entries after create" - } - ); - - await until( - () => { - return storageOperations.entries.list(model, { - where: { - published: true - }, - limit: 10000 - }); - }, - (response: any) => { - return response.items.length === 1; - }, - { - name: "list published storage entries after create" - } - ); /** * Meta field data should be available when getting and listing directly from the storage. */ diff --git a/packages/api-headless-cms/__tests__/contentAPI/entryPagination.test.ts b/packages/api-headless-cms/__tests__/contentAPI/entryPagination.test.ts index 2974c5824b0..a6db0d60363 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/entryPagination.test.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/entryPagination.test.ts @@ -1,7 +1,4 @@ import { mdbid } from "@webiny/utils"; -/** - * We need the "until" because we are using storage operations directly. - */ import { useFruitManageHandler } from "../testHelpers/useFruitManageHandler"; import { CmsEntry, CmsModel } from "~/types"; import { setupContentModelGroup, setupContentModels } from "../testHelpers/setup"; @@ -59,7 +56,7 @@ describe("entry pagination", () => { const manageOpts = { path: "manage/en-US" }; const manager = useFruitManageHandler(manageOpts); - const { storageOperations, until } = manager; + const { storageOperations } = manager; /** * We need to create N fruit entries */ @@ -78,23 +75,6 @@ describe("entry pagination", () => { entry: fruit }); } - await until( - () => - manager - .listFruits({ - limit: 1 - }) - .then(([data]) => data), - ({ data }: any) => { - return data.listFruits.meta.totalCount === NUMBER_OF_FRUITS; - }, - { - name: "list all fruits", - tries: 20, - debounce: 2000, - wait: 2000 - } - ); }); it("should paginate through entries", async () => { diff --git a/packages/api-headless-cms/__tests__/contentAPI/filtering.test.ts b/packages/api-headless-cms/__tests__/contentAPI/filtering.test.ts index 282fe32ef56..b1e4a07ce29 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/filtering.test.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/filtering.test.ts @@ -88,7 +88,7 @@ describe("filtering", () => { const fruit: Fruit = publish.data.publishFruit.data; for (const field of filterOutFields) { - // @ts-ignore + // @ts-expect-error delete fruit[field]; } return fruit; diff --git a/packages/api-headless-cms/__tests__/contentAPI/resolvers.apiKey.manage.test.ts b/packages/api-headless-cms/__tests__/contentAPI/resolvers.apiKey.manage.test.ts index f5e2091a456..a7996f2ff25 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/resolvers.apiKey.manage.test.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/resolvers.apiKey.manage.test.ts @@ -276,7 +276,6 @@ describe("MANAGE - resolvers - api key", () => { const updatedCategory = updateResponse.data.updateCategory.data; - // If this `until` resolves successfully, we know entry is accessible via the "read" API const [listResponse] = await listCategories({}, headers); expect(listResponse).toMatchObject({ diff --git a/packages/api-headless-cms/__tests__/contentAPI/resolvers.manage.test.ts b/packages/api-headless-cms/__tests__/contentAPI/resolvers.manage.test.ts index 0296e2ed32d..5d754077e5d 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/resolvers.manage.test.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/resolvers.manage.test.ts @@ -1245,7 +1245,7 @@ describe("MANAGE - Resolvers", () => { ...webiny.meta, locked: false, status: "draft", - publishedOn: null, + publishedOn: expect.stringMatching(/^20/), version: i + 2, revisions: expect.any(Array) }, diff --git a/packages/api-headless-cms/__tests__/contentAPI/resolvers.read.test.ts b/packages/api-headless-cms/__tests__/contentAPI/resolvers.read.test.ts index de8f0386965..38489807cd4 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/resolvers.read.test.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/resolvers.read.test.ts @@ -38,7 +38,7 @@ const createPermissions = ({ groups, models }: { groups?: string[]; models?: str const categoryManagerHelper = async (manageOpts: GraphQLHandlerParams) => { // Use "manage" API to create and publish entries - const { createCategory, publishCategory, sleep } = useCategoryManageHandler(manageOpts); + const { createCategory, publishCategory } = useCategoryManageHandler(manageOpts); const [fruitsResponse] = await createCategory({ data: { @@ -68,7 +68,6 @@ const categoryManagerHelper = async (manageOpts: GraphQLHandlerParams) => { const [publishedAnimalsResponse] = await publishCategory({ revision: animals.id }); return { - sleep, fruits: publishedFruitsResponse.data.publishCategory.data, vegetables: publishedVegetablesResponse.data.publishCategory.data, animals: publishedAnimalsResponse.data.publishCategory.data, @@ -222,7 +221,7 @@ describe("READ - Resolvers", () => { test(`list entries`, async () => { // Use "manage" API to create and publish entries - const { sleep, createCategory, publishCategory } = useCategoryManageHandler(manageOpts); + const { createCategory, publishCategory } = useCategoryManageHandler(manageOpts); // Create an entry const [create] = await createCategory({ data: { title: "Title 1", slug: "slug-1" } }); @@ -237,7 +236,6 @@ describe("READ - Resolvers", () => { // See if entries are available via "read" API const { listCategories } = useCategoryReadHandler(readOpts); - await sleep(2000); const [response] = await listCategories(); expect(response).toEqual({ @@ -265,7 +263,7 @@ describe("READ - Resolvers", () => { test(`list entries with specific group and model permissions`, async () => { // Use "manage" API to create and publish entries - const { sleep, createCategory, publishCategory } = useCategoryManageHandler(manageOpts); + const { createCategory, publishCategory } = useCategoryManageHandler(manageOpts); // Create an entry const [create] = await createCategory({ data: { title: "Title 1", slug: "slug-1" } }); @@ -286,8 +284,6 @@ describe("READ - Resolvers", () => { }) }); - await sleep(2000); - const [response] = await listCategories(); expect(response).toEqual({ diff --git a/packages/api-headless-cms/__tests__/contentAPI/snapshots/category.manage.ts b/packages/api-headless-cms/__tests__/contentAPI/snapshots/category.manage.ts index f93b22f828b..431a8992f7a 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/snapshots/category.manage.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/snapshots/category.manage.ts @@ -36,6 +36,14 @@ export default /* GraphQL */ ` input CategoryApiNameWhichIsABitDifferentThanModelIdInput { id: ID + # User can override the entry dates + createdOn: DateTime + savedOn: DateTime + publishedOn: DateTime + # User can override the entry related user identities + createdBy: CmsIdentityInput + modifiedBy: CmsIdentityInput + ownedBy: CmsIdentityInput wbyAco_location: WbyAcoLocationInput title: String slug: String @@ -72,6 +80,13 @@ export default /* GraphQL */ ` savedOn_lte: DateTime savedOn_between: [DateTime!] savedOn_not_between: [DateTime!] + publishedOn: DateTime + publishedOn_gt: DateTime + publishedOn_gte: DateTime + publishedOn_lt: DateTime + publishedOn_lte: DateTime + publishedOn_between: [DateTime!] + publishedOn_not_between: [DateTime!] createdBy: String createdBy_not: String createdBy_in: [String!] @@ -175,7 +190,7 @@ export default /* GraphQL */ ` deleteMultipleCategoriesApiModel(entries: [ID!]!): CmsDeleteMultipleResponse! - publishCategoryApiNameWhichIsABitDifferentThanModelId(revision: ID!): CategoryApiNameWhichIsABitDifferentThanModelIdResponse + publishCategoryApiNameWhichIsABitDifferentThanModelId(revision: ID!, options: CmsPublishEntryOptionsInput): CategoryApiNameWhichIsABitDifferentThanModelIdResponse republishCategoryApiNameWhichIsABitDifferentThanModelId(revision: ID!): CategoryApiNameWhichIsABitDifferentThanModelIdResponse diff --git a/packages/api-headless-cms/__tests__/contentAPI/snapshots/category.read.ts b/packages/api-headless-cms/__tests__/contentAPI/snapshots/category.read.ts index aac6e2f020e..d4afa2bd37f 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/snapshots/category.read.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/snapshots/category.read.ts @@ -44,6 +44,13 @@ export default /* GraphQL */ ` savedOn_lte: DateTime savedOn_between: [DateTime!] savedOn_not_between: [DateTime!] + publishedOn: DateTime + publishedOn_gt: DateTime + publishedOn_gte: DateTime + publishedOn_lt: DateTime + publishedOn_lte: DateTime + publishedOn_between: [DateTime!] + publishedOn_not_between: [DateTime!] createdBy: String createdBy_not: String createdBy_in: [String!] diff --git a/packages/api-headless-cms/__tests__/contentAPI/snapshots/page.manage.ts b/packages/api-headless-cms/__tests__/contentAPI/snapshots/page.manage.ts index a746805e31e..896f8a62387 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/snapshots/page.manage.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/snapshots/page.manage.ts @@ -330,6 +330,14 @@ export default /* GraphQL */ ` input PageModelApiNameInput { id: ID + # User can override the entry dates + createdOn: DateTime + savedOn: DateTime + publishedOn: DateTime + # User can override the entry related user identities + createdBy: CmsIdentityInput + modifiedBy: CmsIdentityInput + ownedBy: CmsIdentityInput wbyAco_location: WbyAcoLocationInput content: [PageModelApiName_ContentInput] header: PageModelApiName_HeaderInput @@ -369,6 +377,13 @@ export default /* GraphQL */ ` savedOn_lte: DateTime savedOn_between: [DateTime!] savedOn_not_between: [DateTime!] + publishedOn: DateTime + publishedOn_gt: DateTime + publishedOn_gte: DateTime + publishedOn_lt: DateTime + publishedOn_lte: DateTime + publishedOn_between: [DateTime!] + publishedOn_not_between: [DateTime!] createdBy: String createdBy_not: String createdBy_in: [String!] @@ -462,7 +477,7 @@ export default /* GraphQL */ ` deleteMultiplePagesModelApiName(entries: [ID!]!): CmsDeleteMultipleResponse! - publishPageModelApiName(revision: ID!): PageModelApiNameResponse + publishPageModelApiName(revision: ID!, options: CmsPublishEntryOptionsInput): PageModelApiNameResponse republishPageModelApiName(revision: ID!): PageModelApiNameResponse diff --git a/packages/api-headless-cms/__tests__/contentAPI/snapshots/page.read.ts b/packages/api-headless-cms/__tests__/contentAPI/snapshots/page.read.ts index 386730bcdcc..5a7cef218b6 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/snapshots/page.read.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/snapshots/page.read.ts @@ -190,6 +190,13 @@ export default /* GraphQL */ ` savedOn_lte: DateTime savedOn_between: [DateTime!] savedOn_not_between: [DateTime!] + publishedOn: DateTime + publishedOn_gt: DateTime + publishedOn_gte: DateTime + publishedOn_lt: DateTime + publishedOn_lte: DateTime + publishedOn_between: [DateTime!] + publishedOn_not_between: [DateTime!] createdBy: String createdBy_not: String createdBy_in: [String!] diff --git a/packages/api-headless-cms/__tests__/contentAPI/snapshots/product.manage.ts b/packages/api-headless-cms/__tests__/contentAPI/snapshots/product.manage.ts index 1a9b7e88027..977db78e91f 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/snapshots/product.manage.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/snapshots/product.manage.ts @@ -160,6 +160,14 @@ export default /* GraphQL */ ` input ProductApiSingularInput { id: ID + # User can override the entry dates + createdOn: DateTime + savedOn: DateTime + publishedOn: DateTime + # User can override the entry related user identities + createdBy: CmsIdentityInput + modifiedBy: CmsIdentityInput + ownedBy: CmsIdentityInput wbyAco_location: WbyAcoLocationInput title: String category: RefFieldInput @@ -213,6 +221,13 @@ export default /* GraphQL */ ` savedOn_lte: DateTime savedOn_between: [DateTime!] savedOn_not_between: [DateTime!] + publishedOn: DateTime + publishedOn_gt: DateTime + publishedOn_gte: DateTime + publishedOn_lt: DateTime + publishedOn_lte: DateTime + publishedOn_between: [DateTime!] + publishedOn_not_between: [DateTime!] createdBy: String createdBy_not: String createdBy_in: [String!] @@ -377,7 +392,7 @@ export default /* GraphQL */ ` deleteMultipleProductPluralApiName(entries: [ID!]!): CmsDeleteMultipleResponse! - publishProductApiSingular(revision: ID!): ProductApiSingularResponse + publishProductApiSingular(revision: ID!, options: CmsPublishEntryOptionsInput): ProductApiSingularResponse republishProductApiSingular(revision: ID!): ProductApiSingularResponse diff --git a/packages/api-headless-cms/__tests__/contentAPI/snapshots/product.read.ts b/packages/api-headless-cms/__tests__/contentAPI/snapshots/product.read.ts index 1309ef9fc7f..f7cc9411a13 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/snapshots/product.read.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/snapshots/product.read.ts @@ -150,6 +150,13 @@ export default /* GraphQL */ ` savedOn_lte: DateTime savedOn_between: [DateTime!] savedOn_not_between: [DateTime!] + publishedOn: DateTime + publishedOn_gt: DateTime + publishedOn_gte: DateTime + publishedOn_lt: DateTime + publishedOn_lte: DateTime + publishedOn_between: [DateTime!] + publishedOn_not_between: [DateTime!] createdBy: String createdBy_not: String createdBy_in: [String!] diff --git a/packages/api-headless-cms/__tests__/contentAPI/snapshots/review.manage.ts b/packages/api-headless-cms/__tests__/contentAPI/snapshots/review.manage.ts index 1fd1679264b..a6361ad77a7 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/snapshots/review.manage.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/snapshots/review.manage.ts @@ -38,6 +38,14 @@ export default /* GraphQL */ ` input ReviewApiModelInput { id: ID + # User can override the entry dates + createdOn: DateTime + savedOn: DateTime + publishedOn: DateTime + # User can override the entry related user identities + createdBy: CmsIdentityInput + modifiedBy: CmsIdentityInput + ownedBy: CmsIdentityInput wbyAco_location: WbyAcoLocationInput text: String product: RefFieldInput @@ -76,6 +84,13 @@ export default /* GraphQL */ ` savedOn_lte: DateTime savedOn_between: [DateTime!] savedOn_not_between: [DateTime!] + publishedOn: DateTime + publishedOn_gt: DateTime + publishedOn_gte: DateTime + publishedOn_lt: DateTime + publishedOn_lte: DateTime + publishedOn_between: [DateTime!] + publishedOn_not_between: [DateTime!] createdBy: String createdBy_not: String createdBy_in: [String!] @@ -187,7 +202,7 @@ export default /* GraphQL */ ` deleteMultipleReviewsApiModel(entries: [ID!]!): CmsDeleteMultipleResponse! - publishReviewApiModel(revision: ID!): ReviewApiModelResponse + publishReviewApiModel(revision: ID!, options: CmsPublishEntryOptionsInput): ReviewApiModelResponse republishReviewApiModel(revision: ID!): ReviewApiModelResponse diff --git a/packages/api-headless-cms/__tests__/contentAPI/snapshots/review.read.ts b/packages/api-headless-cms/__tests__/contentAPI/snapshots/review.read.ts index e1c1ca4906f..c0b0e3249f2 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/snapshots/review.read.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/snapshots/review.read.ts @@ -46,6 +46,13 @@ export default /* GraphQL */ ` savedOn_lte: DateTime savedOn_between: [DateTime!] savedOn_not_between: [DateTime!] + publishedOn: DateTime + publishedOn_gt: DateTime + publishedOn_gte: DateTime + publishedOn_lt: DateTime + publishedOn_lte: DateTime + publishedOn_between: [DateTime!] + publishedOn_not_between: [DateTime!] createdBy: String createdBy_not: String createdBy_in: [String!] diff --git a/packages/api-headless-cms/__tests__/converters/mocks/fieldIdStorageConverter.ts b/packages/api-headless-cms/__tests__/converters/mocks/fieldIdStorageConverter.ts index 9e8043e6ff6..7402e3883f5 100644 --- a/packages/api-headless-cms/__tests__/converters/mocks/fieldIdStorageConverter.ts +++ b/packages/api-headless-cms/__tests__/converters/mocks/fieldIdStorageConverter.ts @@ -583,6 +583,8 @@ export const createModel = (base?: Partial>) const fields = createModelFields(); return { name: "Test model", + singularApiName: "TestModel", + pluralApiName: "TestModels", titleFieldId: fields[0].fieldId, group: { id: "group-id", diff --git a/packages/api-headless-cms/__tests__/storageOperations/entries.test.ts b/packages/api-headless-cms/__tests__/storageOperations/entries.test.ts index 67f8f3c4471..19869e681d2 100644 --- a/packages/api-headless-cms/__tests__/storageOperations/entries.test.ts +++ b/packages/api-headless-cms/__tests__/storageOperations/entries.test.ts @@ -1,16 +1,11 @@ -import { - createPersonEntries, - createPersonModel, - deletePersonModel, - waitPersonRecords -} from "./helpers"; +import { createPersonEntries, createPersonModel, deletePersonModel } from "./helpers"; import { useGraphQLHandler } from "../testHelpers/useGraphQLHandler"; import { CmsContext } from "~/types"; jest.setTimeout(60000); describe("Entries storage operations", () => { - const { storageOperations, until, plugins } = useGraphQLHandler({ + const { storageOperations, plugins } = useGraphQLHandler({ path: "manage/en-US" }); @@ -55,14 +50,6 @@ describe("Entries storage operations", () => { expect(result.last.version).toEqual(result.revisions.length); } - await waitPersonRecords({ - records: results, - storageOperations, - name: "list all person entries after create", - until, - model: personModel - }); - /** * There must be "amount" of results. */ @@ -103,19 +90,12 @@ describe("Entries storage operations", () => { it("should list all entries", async () => { const personModel = createPersonModel(); const amount = 10; - const results = await createPersonEntries({ + await createPersonEntries({ amount, storageOperations, maxRevisions: 1, plugins }); - await waitPersonRecords({ - records: results, - storageOperations, - name: "list all person entries after create", - until, - model: personModel - }); const result = await storageOperations.entries.list(personModel, { where: { @@ -143,14 +123,6 @@ describe("Entries storage operations", () => { plugins }); - await waitPersonRecords({ - records: results, - storageOperations, - name: "list all person entries after create", - until, - model: personModel - }); - const items = Object.values(results); const records = await storageOperations.entries.getByIds(personModel, { diff --git a/packages/api-headless-cms/__tests__/storageOperations/fieldUniqueValues.test.ts b/packages/api-headless-cms/__tests__/storageOperations/fieldUniqueValues.test.ts index 2c92126dc65..6e49c1b6206 100644 --- a/packages/api-headless-cms/__tests__/storageOperations/fieldUniqueValues.test.ts +++ b/packages/api-headless-cms/__tests__/storageOperations/fieldUniqueValues.test.ts @@ -2,13 +2,12 @@ import { CmsContext } from "~/types"; import { createPersonEntries, createPersonModel, - deletePersonModel, - waitPersonRecords + deletePersonModel } from "~tests/storageOperations/helpers"; import { useGraphQLHandler } from "~tests/testHelpers/useGraphQLHandler"; describe("field unique values listing", () => { - const { storageOperations, until, plugins } = useGraphQLHandler({ + const { storageOperations, plugins } = useGraphQLHandler({ path: "manage/en-US" }); @@ -63,14 +62,6 @@ describe("field unique values listing", () => { results[entryId] = evenMoreResults[entryId]; } - await waitPersonRecords({ - records: results, - storageOperations, - name: "list all person entries after create", - until, - model: personModel - }); - /** * There must be "amount" * 3 of results. */ diff --git a/packages/api-headless-cms/__tests__/storageOperations/helpers.ts b/packages/api-headless-cms/__tests__/storageOperations/helpers.ts index bbf5fb883e4..34750baef19 100644 --- a/packages/api-headless-cms/__tests__/storageOperations/helpers.ts +++ b/packages/api-headless-cms/__tests__/storageOperations/helpers.ts @@ -241,37 +241,3 @@ export const deletePersonModel = async (params: DeletePersonModelParams) => { console.log(JSON.stringify(ex)); } }; - -interface WaitPersonRecordsParams { - records: PersonEntriesResult; - storageOperations: HeadlessCmsStorageOperations; - name: string; - until: Function; - model: CmsModel; -} - -export const waitPersonRecords = async (params: WaitPersonRecordsParams): Promise => { - const { records, storageOperations, until, model, name } = params; - await until( - () => { - return storageOperations.entries.list(model, { - where: { - latest: true - }, - sort: ["version_ASC"], - limit: 10000 - }); - }, - ({ items }: any) => { - /** - * There must be item for each result last revision id. - */ - return Object.values(records).every(record => { - return items.some((item: any) => item.id === record.last.id); - }); - }, - { - name - } - ); -}; diff --git a/packages/api-headless-cms/__tests__/testHelpers/helpers.ts b/packages/api-headless-cms/__tests__/testHelpers/helpers.ts index 1f689a1c026..7cbae8e31cf 100644 --- a/packages/api-headless-cms/__tests__/testHelpers/helpers.ts +++ b/packages/api-headless-cms/__tests__/testHelpers/helpers.ts @@ -2,9 +2,6 @@ import { SecurityIdentity } from "@webiny/api-security/types"; import { ContextPlugin } from "@webiny/api"; import { CmsContext } from "~/types"; -export { until } from "@webiny/project-utils/testing/helpers/until"; -export { sleep } from "@webiny/project-utils/testing/helpers/sleep"; - export interface PermissionsArg { name: string; locales?: string[]; diff --git a/packages/api-headless-cms/__tests__/testHelpers/plugins.ts b/packages/api-headless-cms/__tests__/testHelpers/plugins.ts index b576c09370b..f2c2b8ae69b 100644 --- a/packages/api-headless-cms/__tests__/testHelpers/plugins.ts +++ b/packages/api-headless-cms/__tests__/testHelpers/plugins.ts @@ -70,7 +70,6 @@ export const createHandlerCore = (params: CreateHandlerCoreParams) => { id: apiKey, name: apiKey, tenant: tenant.id, - // @ts-ignore permissions: identity?.permissions || [], token, createdBy: { diff --git a/packages/api-headless-cms/__tests__/testHelpers/tenancySecurity.ts b/packages/api-headless-cms/__tests__/testHelpers/tenancySecurity.ts index 44ef32cf146..844ed58a5ff 100644 --- a/packages/api-headless-cms/__tests__/testHelpers/tenancySecurity.ts +++ b/packages/api-headless-cms/__tests__/testHelpers/tenancySecurity.ts @@ -18,6 +18,12 @@ interface Config { identity?: SecurityIdentity | null; } +export const defaultIdentity: SecurityIdentity = { + id: "id-12345678", + type: "admin", + displayName: "John Doe" +}; + export const createTenancyAndSecurity = ({ setupGraphQL, permissions, @@ -39,13 +45,7 @@ export const createTenancyAndSecurity = ({ } as unknown as Tenant); context.security.addAuthenticator(async () => { - return ( - identity || { - id: "id-12345678", - type: "admin", - displayName: "John Doe" - } - ); + return identity || defaultIdentity; }); context.security.addAuthorizer(async () => { diff --git a/packages/api-headless-cms/__tests__/testHelpers/useCategoryManageHandler.ts b/packages/api-headless-cms/__tests__/testHelpers/useCategoryManageHandler.ts index b66cd6c2cfc..5ce1ec9a039 100644 --- a/packages/api-headless-cms/__tests__/testHelpers/useCategoryManageHandler.ts +++ b/packages/api-headless-cms/__tests__/testHelpers/useCategoryManageHandler.ts @@ -11,6 +11,16 @@ const categoryFields = ` displayName type } + modifiedBy { + id + displayName + type + } + ownedBy { + id + displayName + type + } savedOn meta { title @@ -110,8 +120,8 @@ const createCategoryMutation = (model: CmsModel) => { const createCategoryFromMutation = (model: CmsModel) => { return /* GraphQL */ ` - mutation CreateCategoryFrom($revision: ID!) { - createCategoryFrom: create${model.singularApiName}From(revision: $revision) { + mutation CreateCategoryFrom($revision: ID!, $data: ${model.singularApiName}Input) { + createCategoryFrom: create${model.singularApiName}From(revision: $revision, data: $data) { data { ${categoryFields} } @@ -174,8 +184,8 @@ const deleteCategoriesMutation = (model: CmsModel) => { const publishCategoryMutation = (model: CmsModel) => { return /* GraphQL */ ` - mutation PublishCategory($revision: ID!) { - publishCategory: publish${model.singularApiName}(revision: $revision) { + mutation PublishCategory($revision: ID!, $options: CmsPublishEntryOptionsInput) { + publishCategory: publish${model.singularApiName}(revision: $revision, options: $options) { data { ${categoryFields} } diff --git a/packages/api-headless-cms/__tests__/testHelpers/useGraphQLHandler.ts b/packages/api-headless-cms/__tests__/testHelpers/useGraphQLHandler.ts index 47a2f9f07da..10dbc804d3f 100644 --- a/packages/api-headless-cms/__tests__/testHelpers/useGraphQLHandler.ts +++ b/packages/api-headless-cms/__tests__/testHelpers/useGraphQLHandler.ts @@ -1,6 +1,5 @@ import { getIntrospectionQuery } from "graphql"; import { createHandler } from "@webiny/handler-aws"; -import { sleep, until } from "./helpers"; import { INSTALL_MUTATION, IS_INSTALLED_QUERY } from "./graphql/settings"; import { ContentModelGroupsMutationVariables, @@ -41,13 +40,14 @@ import { createOutputBenchmarkLogs } from "~tests/testHelpers/outputBenchmarkLog import { APIGatewayEvent, LambdaContext } from "@webiny/handler-aws/types"; import { CMS_EXPORT_STRUCTURE_QUERY, - CmsExportStructureQueryVariables, CMS_IMPORT_STRUCTURE_MUTATION, - CmsImportStructureMutationVariables, CMS_VALIDATE_STRUCTURE_MUTATION, - CmsValidateStructureMutationVariables, - CmsValidateStructureMutationResponse + CmsExportStructureQueryVariables, + CmsImportStructureMutationVariables, + CmsValidateStructureMutationResponse, + CmsValidateStructureMutationVariables } from "~tests/testHelpers/graphql/structure"; +import { defaultIdentity } from "~tests/testHelpers/tenancySecurity"; export type GraphQLHandlerParams = CreateHandlerCoreParams; @@ -107,12 +107,10 @@ export const useGraphQLHandler = (params: GraphQLHandlerParams = {}) => { }; return { - until, - sleep, handler, invoke, tenant: core.tenant, - identity, + identity: identity || defaultIdentity, plugins, storageOperations: core.storageOperations, async introspect() { diff --git a/packages/api-headless-cms/__tests__/testHelpers/useHandler.ts b/packages/api-headless-cms/__tests__/testHelpers/useHandler.ts index 577a9a47dc3..e4f91db8652 100644 --- a/packages/api-headless-cms/__tests__/testHelpers/useHandler.ts +++ b/packages/api-headless-cms/__tests__/testHelpers/useHandler.ts @@ -1,6 +1,7 @@ import { createHandlerCore, CreateHandlerCoreParams } from "~tests/testHelpers/plugins"; import { createRawEventHandler, createRawHandler } from "@webiny/handler-aws"; import { CmsContext } from "~/types"; +import { defaultIdentity } from "~tests/testHelpers/tenancySecurity"; import { LambdaContext } from "@webiny/handler-aws/types"; interface CmsHandlerEvent { @@ -27,6 +28,7 @@ export const useHandler = (params: Params) => { }); return { plugins, + identity: params.identity || defaultIdentity, tenant: core.tenant, handler: (payload: CmsHandlerEvent) => { return handler(payload, {} as LambdaContext); diff --git a/packages/api-headless-cms/src/crud/contentEntry.crud.ts b/packages/api-headless-cms/src/crud/contentEntry.crud.ts index 5fd73b022ac..71b2417a0c4 100644 --- a/packages/api-headless-cms/src/crud/contentEntry.crud.ts +++ b/packages/api-headless-cms/src/crud/contentEntry.crud.ts @@ -56,8 +56,7 @@ import { OnEntryRevisionBeforeDeleteTopicParams, OnEntryRevisionDeleteErrorTopicParams, OnEntryUnpublishErrorTopicParams, - OnEntryUpdateErrorTopicParams, - UpdateCmsEntryInput + OnEntryUpdateErrorTopicParams } from "~/types"; import { validateModelEntryData, @@ -78,6 +77,7 @@ import { EntriesPermissions } from "~/utils/permissions/EntriesPermissions"; import { ModelsPermissions } from "~/utils/permissions/ModelsPermissions"; import { NotAuthorizedError } from "@webiny/api-security"; import { ROOT_FOLDER } from "~/constants"; +import { getDate } from "~/utils/date"; export const STATUS_DRAFT = CONTENT_ENTRY_STATUS.DRAFT; export const STATUS_PUBLISHED = CONTENT_ENTRY_STATUS.PUBLISHED; @@ -150,8 +150,8 @@ const mapAndCleanCreateInputData = (model: CmsModel, input: CreateCmsEntryInput) /** * Cleans the update input entry data. */ -const mapAndCleanUpdatedInputData = (model: CmsModel, input: UpdateCmsEntryInput) => { - return model.fields.reduce((acc, field) => { +const mapAndCleanUpdatedInputData = (model: CmsModel, input: Record) => { + return model.fields.reduce>((acc, field) => { /** * This should never happen, but let's make it sure. * The fix would be for the user to add the fieldId on the field definition. @@ -247,6 +247,21 @@ const createSort = (sort?: CmsEntryListSort): CmsEntryListSort => { return sort; }; +const getIdentity = ( + input: SecurityIdentity | null | undefined, + defaultValue: T +): T => { + const identity = input?.id && input?.displayName && input?.type ? input : defaultValue; + if (!identity) { + return null as T; + } + return { + id: identity.id, + displayName: identity.displayName, + type: identity.type + } as T; +}; + interface CreateContentEntryCrudParams { storageOperations: HeadlessCmsStorageOperations; entriesPermissions: EntriesPermissions; @@ -263,20 +278,11 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm entriesPermissions, modelsPermissions, context, - getIdentity, + getIdentity: getSecurityIdentity, getTenant, getLocale } = params; - const getCreatedBy = () => { - const identity = getIdentity(); - return { - id: identity.id, - displayName: identity.displayName, - type: identity.type - }; - }; - /** * Create */ @@ -573,7 +579,7 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm * Or if searching for the owner set that value - in the case that user can see other entries than their own. */ if (await entriesPermissions.canAccessOnlyOwnRecords()) { - where.ownedBy = getIdentity().id; + where.ownedBy = getSecurityIdentity().id; } /** @@ -647,7 +653,7 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm ); } }; - const createEntry: CmsEntryContext["createEntry"] = async (model, inputData, options) => { + const createEntry: CmsEntryContext["createEntry"] = async (model, rawInput, options) => { await entriesPermissions.ensure({ rwd: "w" }); await modelsPermissions.ensureCanAccessModel({ model @@ -656,7 +662,7 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm /** * Make sure we only work with fields that are defined in the model. */ - const initialInput = mapAndCleanCreateInputData(model, inputData); + const initialInput = mapAndCleanCreateInputData(model, rawInput); await validateModelEntryDataOrThrow({ context, @@ -674,13 +680,14 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm const locale = getLocale(); - const owner = getCreatedBy(); + const identity = getSecurityIdentity(); - const { id, entryId, version } = createEntryId(inputData); + const { id, entryId, version } = createEntryId(rawInput); /** * There is a possibility that user sends an ID in the input, so we will use that one. * There is no check if the ID is unique or not, that is up to the user. */ + const currentDate = new Date(); const entry: CmsEntry = { webinyVersion: context.WEBINY_VERSION, tenant: getTenant().id, @@ -688,17 +695,18 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm id, modelId: model.modelId, locale: locale.code, - createdOn: new Date().toISOString(), - savedOn: new Date().toISOString(), - createdBy: owner, - ownedBy: owner, - modifiedBy: null, + createdOn: getDate(rawInput.createdOn, currentDate), + savedOn: getDate(rawInput.savedOn, currentDate), + publishedOn: getDate(rawInput.publishedOn), + createdBy: getIdentity(rawInput.createdBy, identity), + ownedBy: getIdentity(rawInput.ownedBy, identity), + modifiedBy: getIdentity(rawInput.modifiedBy, null), version, locked: false, status: STATUS_DRAFT, values: input, location: { - folderId: inputData.wbyAco_location?.folderId || ROOT_FOLDER + folderId: rawInput.wbyAco_location?.folderId || ROOT_FOLDER } }; @@ -747,7 +755,7 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm const createEntryRevisionFrom: CmsEntryContext["createEntryRevisionFrom"] = async ( model, sourceId, - inputData, + rawInput, options ) => { await entriesPermissions.ensure({ rwd: "w" }); @@ -758,7 +766,7 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm /** * Make sure we only work with fields that are defined in the model. */ - const input = mapAndCleanUpdatedInputData(model, inputData); + const input = mapAndCleanUpdatedInputData(model, rawInput); /** * Entries are identified by a common parent ID + Revision number. @@ -808,25 +816,21 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm await entriesPermissions.ensure({ owns: originalEntry.createdBy }); - const identity = getIdentity(); - const latestId = latestStorageEntry ? latestStorageEntry.id : sourceId; const { id, version: nextVersion } = increaseEntryIdVersion(latestId); + const currentDate = new Date(); const entry: CmsEntry = { ...originalEntry, id, version: nextVersion, - savedOn: new Date().toISOString(), - createdOn: new Date().toISOString(), - createdBy: { - id: identity.id, - displayName: identity.displayName, - type: identity.type - }, - modifiedBy: null, + savedOn: getDate(rawInput.savedOn, currentDate), + createdOn: getDate(rawInput.createdOn, currentDate), + publishedOn: getDate(rawInput.publishedOn, originalEntry.publishedOn), + createdBy: getIdentity(rawInput.createdBy, originalEntry.createdBy), + modifiedBy: getIdentity(rawInput.modifiedBy, null), + ownedBy: getIdentity(rawInput.ownedBy, originalEntry.ownedBy), locked: false, - publishedOn: undefined, status: STATUS_DRAFT, values }; @@ -880,7 +884,7 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm const updateEntry: CmsEntryContext["updateEntry"] = async ( model, id, - inputData, + rawInput, metaInput, options ) => { @@ -892,7 +896,7 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm /** * Make sure we only work with fields that are defined in the model. */ - const input = mapAndCleanUpdatedInputData(model, inputData); + const input = mapAndCleanUpdatedInputData(model, rawInput); /** * The entry we are going to update. @@ -941,6 +945,7 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm input: initialValues, validateEntries: false }); + /** * If users wants to remove a key from meta values, they need to send meta key with the null value. */ @@ -950,13 +955,17 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm */ const entry: CmsEntry = { ...originalEntry, - savedOn: new Date().toISOString(), - modifiedBy: getCreatedBy(), + savedOn: getDate(rawInput.savedOn, new Date()), + createdOn: getDate(rawInput.createdOn, originalEntry.createdOn), + publishedOn: getDate(rawInput.publishedOn, originalEntry.publishedOn), + createdBy: getIdentity(rawInput.createdBy, originalEntry.createdBy), + modifiedBy: getIdentity(rawInput.modifiedBy, getSecurityIdentity()), + ownedBy: getIdentity(rawInput.ownedBy, originalEntry.ownedBy), values, meta, status: transformEntryStatus(originalEntry.status) }; - const folderId = inputData.wbyAco_location?.folderId; + const folderId = rawInput.wbyAco_location?.folderId; if (folderId) { entry.location = { folderId @@ -1092,6 +1101,7 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm const republishEntry: CmsEntryContext["republishEntry"] = async (model, id) => { await entriesPermissions.ensure({ rwd: "w" }); + await modelsPermissions.ensureCanAccessModel({ model }); @@ -1118,8 +1128,8 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm const entry: CmsEntry = { ...originalEntry, status: STATUS_PUBLISHED, - publishedOn: originalEntry.publishedOn || new Date().toISOString(), - savedOn: new Date().toISOString(), + publishedOn: getDate(originalEntry.publishedOn, new Date()), + savedOn: getDate(originalEntry.savedOn, new Date()), webinyVersion: context.WEBINY_VERSION, values }; @@ -1389,7 +1399,7 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm entry }); }; - const publishEntry: CmsEntryContext["publishEntry"] = async (model, id) => { + const publishEntry: CmsEntryContext["publishEntry"] = async (model, id, options) => { await entriesPermissions.ensure({ pw: "p" }); await modelsPermissions.ensureCanAccessModel({ model @@ -1415,18 +1425,36 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm }); const currentDate = new Date().toISOString(); + /** + * The existing functionality is to set the publishedOn date to the current date. + * Users can now choose to skip updating the publishedOn date - unless it is not set. + * + * Same logic goes for the savedOn date. + */ + const { updatePublishedOn = true, updateSavedOn = true } = options || {}; + let publishedOn = originalEntry.publishedOn; + if (updatePublishedOn || !publishedOn) { + publishedOn = currentDate; + } + + let savedOn = originalEntry.savedOn; + if (updateSavedOn || !savedOn) { + savedOn = currentDate; + } + const entry: CmsEntry = { ...originalEntry, status: STATUS_PUBLISHED, locked: true, - savedOn: currentDate, - publishedOn: currentDate + savedOn, + publishedOn }; let storageEntry: CmsStorageEntry | null = null; try { await onEntryBeforePublish.publish({ + original: originalEntry, entry, model }); @@ -1438,6 +1466,7 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm }); await onEntryAfterPublish.publish({ + original: originalEntry, entry, storageEntry: result, model @@ -1445,6 +1474,7 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm return entry; } catch (ex) { await onEntryPublishError.publish({ + original: originalEntry, entry, model, error: ex @@ -1550,7 +1580,7 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm * Or if searching for the owner set that value - in the case that user can see other entries than their own. */ if (await entriesPermissions.canAccessOnlyOwnRecords()) { - where.ownedBy = getIdentity().id; + where.ownedBy = getSecurityIdentity().id; } /** @@ -1812,9 +1842,9 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm } ); }, - async publishEntry(model, id) { + async publishEntry(model, id, options) { return context.benchmark.measure("headlessCms.crud.entries.publishEntry", async () => { - return publishEntry(model, id); + return publishEntry(model, id, options); }); }, async unpublishEntry(model, id) { diff --git a/packages/api-headless-cms/src/graphql/schema/baseSchema.ts b/packages/api-headless-cms/src/graphql/schema/baseSchema.ts index 91bafe6e86e..862b1d137d5 100644 --- a/packages/api-headless-cms/src/graphql/schema/baseSchema.ts +++ b/packages/api-headless-cms/src/graphql/schema/baseSchema.ts @@ -105,6 +105,19 @@ const createSchema = (plugins: PluginsContainer): GraphQLSchemaPlugin { /** * This is required because due to ref field can be requested without the populated data. diff --git a/packages/api-headless-cms/src/graphql/schema/createManageSDL.ts b/packages/api-headless-cms/src/graphql/schema/createManageSDL.ts index b00dff81bce..531902b8751 100644 --- a/packages/api-headless-cms/src/graphql/schema/createManageSDL.ts +++ b/packages/api-headless-cms/src/graphql/schema/createManageSDL.ts @@ -106,6 +106,14 @@ export const createManageSDL: CreateManageSDL = ({ input ${singularName}Input { id: ID + # User can override the entry dates + createdOn: DateTime + savedOn: DateTime + publishedOn: DateTime + # User can override the entry related user identities + createdBy: CmsIdentityInput + modifiedBy: CmsIdentityInput + ownedBy: CmsIdentityInput wbyAco_location: WbyAcoLocationInput ${inputGraphQLFields} } @@ -179,7 +187,7 @@ export const createManageSDL: CreateManageSDL = ({ deleteMultiple${pluralName}(entries: [ID!]!): CmsDeleteMultipleResponse! - publish${singularName}(revision: ID!): ${singularName}Response + publish${singularName}(revision: ID!, options: CmsPublishEntryOptionsInput): ${singularName}Response republish${singularName}(revision: ID!): ${singularName}Response diff --git a/packages/api-headless-cms/src/graphql/schema/resolvers/manage/resolvePublish.ts b/packages/api-headless-cms/src/graphql/schema/resolvers/manage/resolvePublish.ts index 0edeb19a616..a61f425c5f9 100644 --- a/packages/api-headless-cms/src/graphql/schema/resolvers/manage/resolvePublish.ts +++ b/packages/api-headless-cms/src/graphql/schema/resolvers/manage/resolvePublish.ts @@ -1,8 +1,9 @@ -import { Response, ErrorResponse } from "@webiny/handler-graphql/responses"; -import { CmsEntryResolverFactory as ResolverFactory } from "~/types"; +import { ErrorResponse, Response } from "@webiny/handler-graphql/responses"; +import { CmsEntryResolverFactory as ResolverFactory, CmsPublishEntryOptions } from "~/types"; interface ResolvePublishArgs { revision: string; + options?: CmsPublishEntryOptions; } type ResolvePublish = ResolverFactory; @@ -11,7 +12,7 @@ export const resolvePublish: ResolvePublish = ({ model }) => async (_, args: any, context) => { try { - const entry = await context.cms.publishEntry(model, args.revision); + const entry = await context.cms.publishEntry(model, args.revision, args.options); return new Response(entry); } catch (e) { return new ErrorResponse(e); diff --git a/packages/api-headless-cms/src/graphqlFields/ref.ts b/packages/api-headless-cms/src/graphqlFields/ref.ts index 75f33b0ede3..b5f00ddf520 100644 --- a/packages/api-headless-cms/src/graphqlFields/ref.ts +++ b/packages/api-headless-cms/src/graphqlFields/ref.ts @@ -135,7 +135,7 @@ export const createRefField = (): CmsModelFieldToGraphQLPlugin => { * TS is complaining about mixed types for createResolver. * TODO @ts-refactor @pavel Maybe we should have a single createResolver method? */ - // @ts-ignore + // @ts-expect-error createResolver({ field, models }) { // Create a map of model types and corresponding modelIds so resolvers don't need to perform the lookup. const fieldModels = field.settings?.models || []; diff --git a/packages/api-headless-cms/src/plugins/CmsModelPlugin.ts b/packages/api-headless-cms/src/plugins/CmsModelPlugin.ts index f3652f5d29c..f4701851d96 100644 --- a/packages/api-headless-cms/src/plugins/CmsModelPlugin.ts +++ b/packages/api-headless-cms/src/plugins/CmsModelPlugin.ts @@ -243,7 +243,7 @@ export class CmsModelPlugin extends Plugin { /** * We can safely ignore error because we are going through the fields and making sure each has storageId. */ - // @ts-ignore + // @ts-expect-error let settings: BaseCmsModelFieldSettings = input.settings; const childFields = settings?.fields || []; diff --git a/packages/api-headless-cms/src/types.ts b/packages/api-headless-cms/src/types.ts index 61f8d3cebb7..fb5eac57fe9 100644 --- a/packages/api-headless-cms/src/types.ts +++ b/packages/api-headless-cms/src/types.ts @@ -1493,7 +1493,7 @@ export interface CmsEntry { * A string of Date.toISOString() type - if published. * Populated when entry is published. */ - publishedOn?: string; + publishedOn?: string | null; /** * A revision version of the entry. */ @@ -2026,11 +2026,13 @@ export interface OnEntryMoveErrorTopicParams { */ export interface OnEntryBeforePublishTopicParams { + original: CmsEntry; entry: CmsEntry; model: CmsModel; } export interface OnEntryAfterPublishTopicParams { + original: CmsEntry; entry: CmsEntry; model: CmsModel; storageEntry: CmsEntry; @@ -2038,6 +2040,7 @@ export interface OnEntryAfterPublishTopicParams { export interface OnEntryPublishErrorTopicParams { error: Error; + original: CmsEntry; entry: CmsEntry; model: CmsModel; } @@ -2150,6 +2153,12 @@ export interface EntryBeforeListTopicParams { */ export interface CreateCmsEntryInput { id?: string; + createdOn?: Date | string; + savedOn?: Date | string; + publishedOn?: Date | string; + createdBy?: CmsIdentity | null; + modifiedBy?: CmsIdentity | null; + ownedBy?: CmsIdentity | null; wbyAco_location?: { folderId?: string | null; }; @@ -2165,6 +2174,12 @@ export interface CreateCmsEntryOptionsInput { * @category CmsEntry */ export interface CreateFromCmsEntryInput { + createdOn?: Date; + savedOn?: Date; + publishedOn?: Date; + createdBy?: CmsIdentity; + modifiedBy?: CmsIdentity; + ownedBy?: CmsIdentity; [key: string]: any; } @@ -2177,6 +2192,12 @@ export interface CreateRevisionCmsEntryOptionsInput { * @category CmsEntry */ export interface UpdateCmsEntryInput { + createdOn?: Date | string | null; + savedOn?: Date | string | null; + publishedOn?: Date | string | null; + createdBy?: CmsIdentity | null; + modifiedBy?: CmsIdentity | null; + ownedBy?: CmsIdentity; wbyAco_location?: { folderId?: string | null; }; @@ -2207,6 +2228,20 @@ export interface CmsDeleteEntryOptions { force?: boolean; } +/** + * @category CmsEntry + */ +export interface CmsPublishEntryOptions { + /** + * By default, updatePublishedOn is "true". User can set it to "false" to skip the publishedOn field update. + */ + updatePublishedOn?: boolean; + /** + * By default, updateSavedOn is "true". User can set it to "false" to skip the publishedOn field update. + */ + updateSavedOn?: boolean; +} + /** * @category Context * @category CmsEntry @@ -2330,7 +2365,11 @@ export interface CmsEntryContext { /** * Publish entry. */ - publishEntry: (model: CmsModel, id: string) => Promise; + publishEntry: ( + model: CmsModel, + id: string, + options?: CmsPublishEntryOptions + ) => Promise; /** * Unpublish entry. */ diff --git a/packages/api-headless-cms/src/utils/date.ts b/packages/api-headless-cms/src/utils/date.ts new file mode 100644 index 00000000000..4d6d2ac8cab --- /dev/null +++ b/packages/api-headless-cms/src/utils/date.ts @@ -0,0 +1,29 @@ +/** + * Should not be used by users as method is prone to breaking changes. + * @internal + */ +export const formatDate = (date?: Date | string | null): string | undefined => { + if (!date) { + return undefined; + } else if (date instanceof Date) { + return date.toISOString(); + } + return new Date(date).toISOString(); +}; + +/** + * Should not be used by users as method is prone to breaking changes. + * @internal + */ +export const getDate = ( + input?: Date | string | null, + defaultValue?: Date | string | null +): T => { + if (!input) { + return formatDate(defaultValue) as T; + } + if (input instanceof Date) { + return formatDate(input) as T; + } + return formatDate(new Date(input)) as T; +}; diff --git a/packages/api-headless-cms/src/utils/renderListFilterFields.ts b/packages/api-headless-cms/src/utils/renderListFilterFields.ts index 44a206fb21c..dcaae2b37f9 100644 --- a/packages/api-headless-cms/src/utils/renderListFilterFields.ts +++ b/packages/api-headless-cms/src/utils/renderListFilterFields.ts @@ -47,6 +47,13 @@ export const renderListFilterFields: RenderListFilterFields = (params): string = "savedOn_lte: DateTime", "savedOn_between: [DateTime!]", "savedOn_not_between: [DateTime!]", + "publishedOn: DateTime", + "publishedOn_gt: DateTime", + "publishedOn_gte: DateTime", + "publishedOn_lt: DateTime", + "publishedOn_lte: DateTime", + "publishedOn_between: [DateTime!]", + "publishedOn_not_between: [DateTime!]", "createdBy: String", "createdBy_not: String", "createdBy_in: [String!]", diff --git a/packages/api-i18n-ddb/src/operations/locales/LocalesStorageOperations.ts b/packages/api-i18n-ddb/src/operations/locales/LocalesStorageOperations.ts index 82895c1ac6f..96abf44272f 100644 --- a/packages/api-i18n-ddb/src/operations/locales/LocalesStorageOperations.ts +++ b/packages/api-i18n-ddb/src/operations/locales/LocalesStorageOperations.ts @@ -286,7 +286,7 @@ export class LocalesStorageOperations implements I18NLocalesStorageOperations { const { where } = params; const tenant = where.tenant; - // @ts-ignore + // @ts-expect-error delete where.tenant; let partitionKey = this.createPartitionKey({ tenant }); diff --git a/packages/api-i18n/__tests__/useGqlHandler.ts b/packages/api-i18n/__tests__/useGqlHandler.ts index c04e40bb2f9..468dfe5e58d 100644 --- a/packages/api-i18n/__tests__/useGqlHandler.ts +++ b/packages/api-i18n/__tests__/useGqlHandler.ts @@ -1,6 +1,3 @@ -/** - * We use @ts-ignore because __getStorageOperationsPlugins and __getStorageOperationsPlugins are attached from other projects directly to JEST context. - */ import { createWcpContext, createWcpGraphQL } from "@webiny/api-wcp"; import { createHandler } from "@webiny/handler-aws"; import graphqlHandler from "@webiny/handler-graphql"; diff --git a/packages/api-i18n/src/graphql/graphql/resolvers/searchLocaleCodes.ts b/packages/api-i18n/src/graphql/graphql/resolvers/searchLocaleCodes.ts index 11df3de3931..d8a5c7984f5 100644 --- a/packages/api-i18n/src/graphql/graphql/resolvers/searchLocaleCodes.ts +++ b/packages/api-i18n/src/graphql/graphql/resolvers/searchLocaleCodes.ts @@ -1,7 +1,7 @@ /** * Package i18n-locales does not have types. */ -// @ts-ignore +// @ts-expect-error import localesList from "i18n-locales"; interface SearchLocaleCodesArgs { diff --git a/packages/api-mailer/src/graphql/settings.ts b/packages/api-mailer/src/graphql/settings.ts index b3bd677da86..1f229558b81 100644 --- a/packages/api-mailer/src/graphql/settings.ts +++ b/packages/api-mailer/src/graphql/settings.ts @@ -61,7 +61,7 @@ export const createSettingsGraphQL = () => { * We want to remove the password from the response, if it exists. */ if (settings?.password) { - // @ts-ignore + // @ts-expect-error delete settings.password; } return new Response(settings); @@ -83,7 +83,7 @@ export const createSettingsGraphQL = () => { * We want to remove the password from the response, if it exists. */ if (settings?.password) { - // @ts-ignore + // @ts-expect-error delete settings.password; } return new Response(settings); diff --git a/packages/api-mailer/src/transports/createSmtpTransport.ts b/packages/api-mailer/src/transports/createSmtpTransport.ts index 0839e98b94a..279a3cd00db 100644 --- a/packages/api-mailer/src/transports/createSmtpTransport.ts +++ b/packages/api-mailer/src/transports/createSmtpTransport.ts @@ -26,7 +26,7 @@ const applyDefaults = ( (config, key) => { const configKey = key as unknown as keyof SmtpTransportConfig; if (config[configKey] === undefined || config[configKey] === null) { - // @ts-ignore + // @ts-expect-error config[configKey] = configDefaults[configKey]; } diff --git a/packages/api-page-builder-aco/src/utils/PageBuilderCrudDecorators.ts b/packages/api-page-builder-aco/src/utils/PageBuilderCrudDecorators.ts index 693b8c30571..5500d5eb1ec 100644 --- a/packages/api-page-builder-aco/src/utils/PageBuilderCrudDecorators.ts +++ b/packages/api-page-builder-aco/src/utils/PageBuilderCrudDecorators.ts @@ -35,7 +35,8 @@ export class PageBuilderCrudDecorators { const originalPbGetPage = context.pageBuilder.getPage.bind(context.pageBuilder); - // @ts-ignore TODO: Couldn't figure out how to resolve the issue. + // TODO: Couldn't figure out how to resolve the issue. + // @ts-expect-error context.pageBuilder.getPage = async (pageId, options) => { const page = await originalPbGetPage(pageId, options); const pageSearchRecord = await context.pageBuilderAco.app.search.get(page.pid); diff --git a/packages/api-page-builder-import-export/src/client.ts b/packages/api-page-builder-import-export/src/client.ts index 2e6f9bc466b..d58017ed144 100644 --- a/packages/api-page-builder-import-export/src/client.ts +++ b/packages/api-page-builder-import-export/src/client.ts @@ -32,8 +32,9 @@ export async function invokeHandlerClient({ headers, /** * Required until type augmentation works correctly. + * Keep @ts-ignore because it will not build if using @ts-expect-error. */ - // @ts-ignore + // @ts-ignore read above cookies: request.cookies }; // Invoke handler diff --git a/packages/api-page-builder-import-export/src/graphql/crud/importExportTasks.crud.ts b/packages/api-page-builder-import-export/src/graphql/crud/importExportTasks.crud.ts index 1c3493d8f81..0a4c3f7c6ad 100644 --- a/packages/api-page-builder-import-export/src/graphql/crud/importExportTasks.crud.ts +++ b/packages/api-page-builder-import-export/src/graphql/crud/importExportTasks.crud.ts @@ -2,12 +2,12 @@ import { mdbid } from "@webiny/utils"; /** * Package @commodo/fields does not have types. */ -// @ts-ignore +// @ts-expect-error import { string, withFields } from "@commodo/fields"; /** * Package commodo-fields-object does not have types. */ -// @ts-ignore +// @ts-expect-error import { object } from "commodo-fields-object"; import { validation } from "@webiny/validation"; import { ContextPlugin } from "@webiny/api"; diff --git a/packages/api-page-builder-import-export/src/import/utils/extractAndUploadZipFileContents.ts b/packages/api-page-builder-import-export/src/import/utils/extractAndUploadZipFileContents.ts index c3b1c1e5233..e17b18b70db 100644 --- a/packages/api-page-builder-import-export/src/import/utils/extractAndUploadZipFileContents.ts +++ b/packages/api-page-builder-import-export/src/import/utils/extractAndUploadZipFileContents.ts @@ -37,7 +37,7 @@ export async function extractAndUploadZipFileContents(zipFileUrl: string): Promi const ZIP_FILE_PATH = path.join(INSTALL_DIR, zipFileName); const writeStream = createWriteStream(ZIP_FILE_PATH); - // @ts-ignore + await streamPipeline(readStream, writeStream); log(`Downloaded file "${zipFileName}" at ${ZIP_FILE_PATH}`); diff --git a/packages/api-page-builder-so-ddb-es/__tests__/useHandler.ts b/packages/api-page-builder-so-ddb-es/__tests__/useHandler.ts index 7a26a4d565b..e42cbe60614 100644 --- a/packages/api-page-builder-so-ddb-es/__tests__/useHandler.ts +++ b/packages/api-page-builder-so-ddb-es/__tests__/useHandler.ts @@ -31,10 +31,6 @@ import elasticsearchClientContextPlugin, { createGzipCompression, getElasticsearchOperators } from "@webiny/api-elasticsearch"; -/** - * File does not have types. - */ -// @ts-ignore import { simulateStream } from "@webiny/project-utils/testing/dynamodb"; import { configurations } from "~/configurations"; import { createAco } from "@webiny/api-aco"; diff --git a/packages/api-page-builder-so-ddb-es/src/operations/pageTemplate/index.ts b/packages/api-page-builder-so-ddb-es/src/operations/pageTemplate/index.ts index 91a6073e980..5a704073530 100644 --- a/packages/api-page-builder-so-ddb-es/src/operations/pageTemplate/index.ts +++ b/packages/api-page-builder-so-ddb-es/src/operations/pageTemplate/index.ts @@ -27,6 +27,7 @@ export interface CreatePageTemplateStorageOperationsParams { entity: Entity; plugins: PluginsContainer; } + export const createPageTemplateStorageOperations = ({ entity, plugins @@ -96,27 +97,29 @@ export const createPageTemplateStorageOperations = ({ ); } + const itemsData = items.map(item => item?.data).filter(Boolean); + const fields = plugins.byType( PageTemplateDynamoDbElasticFieldPlugin.type ); - const filteredItems = filterItems>({ + const filteredItems = filterItems({ plugins, where: restWhere, - items, + items: itemsData, fields }); - const sortedItems = sortItems>({ + const sortedItems = sortItems({ items: filteredItems, sort, fields }); return createListResponse({ - items: sortedItems.map(item => item?.data).filter(Boolean), + items: sortedItems, limit: limit || 100000, - totalCount: filteredItems.length, + totalCount: sortedItems.length, after: null }); }; diff --git a/packages/api-page-builder-so-ddb/src/operations/pageTemplate/index.ts b/packages/api-page-builder-so-ddb/src/operations/pageTemplate/index.ts index 056bbc66b47..dc48e74c39d 100644 --- a/packages/api-page-builder-so-ddb/src/operations/pageTemplate/index.ts +++ b/packages/api-page-builder-so-ddb/src/operations/pageTemplate/index.ts @@ -27,6 +27,7 @@ export interface CreatePageTemplateStorageOperationsParams { entity: Entity; plugins: PluginsContainer; } + export const createPageTemplateStorageOperations = ({ entity, plugins @@ -96,27 +97,29 @@ export const createPageTemplateStorageOperations = ({ ); } + const itemsData = items.map(item => item?.data).filter(Boolean); + const fields = plugins.byType( PageTemplateDynamoDbFieldPlugin.type ); - const filteredItems = filterItems>({ + const filteredItems = filterItems({ plugins, where: restWhere, - items, + items: itemsData, fields }); - const sortedItems = sortItems>({ + const sortedItems = sortItems({ items: filteredItems, sort, fields }); return createListResponse({ - items: sortedItems.map(item => item?.data).filter(Boolean), + items: sortedItems, limit: limit || 100000, - totalCount: filteredItems.length, + totalCount: sortedItems.length, after: null }); }; diff --git a/packages/api-page-builder/__tests__/graphql/pageFullUrl.test.ts b/packages/api-page-builder/__tests__/graphql/pageFullUrl.test.ts index 31dd4b98d58..c15554a0a9d 100644 --- a/packages/api-page-builder/__tests__/graphql/pageFullUrl.test.ts +++ b/packages/api-page-builder/__tests__/graphql/pageFullUrl.test.ts @@ -1,6 +1,5 @@ import useGqlHandler from "./useGqlHandler"; import { Page } from "~/types"; -import { waitPage } from "./utils/waitPage"; jest.setTimeout(100000); @@ -34,7 +33,7 @@ describe("page full URL test", () => { for (let i = 0; i < 3; i++) { const [response] = await createPage({ category: "category" }); const page = response.data.pageBuilder.createPage.data; - await waitPage(handler, page); + const [updateResponse] = await updatePage({ id: page.id, data: { @@ -42,7 +41,7 @@ describe("page full URL test", () => { } }); const updatedPage = updateResponse.data.pageBuilder.updatePage.data; - await waitPage(handler, updatedPage); + pages.push(updatedPage); } diff --git a/packages/api-page-builder/__tests__/graphql/pages.deletion.test.ts b/packages/api-page-builder/__tests__/graphql/pages.deletion.test.ts index ceadf676ada..01e4721e348 100644 --- a/packages/api-page-builder/__tests__/graphql/pages.deletion.test.ts +++ b/packages/api-page-builder/__tests__/graphql/pages.deletion.test.ts @@ -1,5 +1,4 @@ import useGqlHandler from "./useGqlHandler"; -import { waitPage } from "./utils/waitPage"; jest.setTimeout(100000); @@ -9,7 +8,7 @@ describe("deleting pages", () => { const { getPage, createPage, deletePage, listPages, listPublishedPages, publishPage, until } = handler; - let p1v1, p1v2, p1v3, category; + let p1v1: any, p1v2: any, p1v3: any, category; beforeEach(async () => { const { createCategory } = useGqlHandler(); @@ -25,17 +24,14 @@ describe("deleting pages", () => { p1v1 = await createPage({ category: category.slug }).then( ([res]) => res.data.pageBuilder.createPage.data ); - await waitPage(handler, p1v1); p1v2 = await createPage({ from: p1v1.id }).then(([res]) => { return res.data.pageBuilder.createPage.data; }); - await waitPage(handler, p1v2); p1v3 = await createPage({ from: p1v2.id }).then( ([res]) => res.data.pageBuilder.createPage.data ); - await waitPage(handler, p1v3); }); test("deleting page via `pid` should delete all related DB / index entries", async () => { diff --git a/packages/api-page-builder/__tests__/graphql/pages.test.ts b/packages/api-page-builder/__tests__/graphql/pages.test.ts index 6506a9cc7bd..88419a588aa 100644 --- a/packages/api-page-builder/__tests__/graphql/pages.test.ts +++ b/packages/api-page-builder/__tests__/graphql/pages.test.ts @@ -1,5 +1,5 @@ import useGqlHandler from "./useGqlHandler"; -import { waitPage } from "./utils/waitPage"; + import { defaultIdentity } from "../tenancySecurity"; import { expectCompressed } from "~tests/graphql/utils/expectCompressed"; import { decompress } from "./utils/compression"; @@ -119,14 +119,10 @@ describe("CRUD Test", () => { } }; - const [updatePageResponse] = await updatePage({ + await updatePage({ id, data }); - - const updatedPage = updatePageResponse.data.pageBuilder.updatePage.data; - - await waitPage(handler, updatedPage); } const [listAfterUpdateResponse] = await until( @@ -449,7 +445,7 @@ describe("CRUD Test", () => { updatePageBlock: { data: { id: blockData.id, - content: expectCompressed(updatedContent) + content: expectCompressed() }, error: null } diff --git a/packages/api-page-builder/__tests__/graphql/pagesGetPublished.test.ts b/packages/api-page-builder/__tests__/graphql/pagesGetPublished.test.ts index 3d359825c3a..b8cfdb811a6 100644 --- a/packages/api-page-builder/__tests__/graphql/pagesGetPublished.test.ts +++ b/packages/api-page-builder/__tests__/graphql/pagesGetPublished.test.ts @@ -1,5 +1,5 @@ import useGqlHandler from "./useGqlHandler"; -import { waitPage } from "./utils/waitPage"; + import { Page } from "~/types"; jest.setTimeout(100000); @@ -36,8 +36,6 @@ describe("getting published pages", () => { throw new Error(`Missing page data: ${letter}`); } - await waitPage(handler, page); - const title = `page-${letter}`; const path = `/path-${letter}`; @@ -52,7 +50,6 @@ describe("getting published pages", () => { if (!updatedPage) { throw new Error(`Missing updated page data: ${letter}`); } - await waitPage(handler, updatedPage); pages.push(updatedPage); @@ -67,7 +64,7 @@ describe("getting published pages", () => { await until( () => listPublishedPages({ sort: ["createdOn_DESC"] }), ([res]) => { - const data = res.data.pageBuilder.listPublishedPages.data; + const data: any[] = res.data.pageBuilder.listPublishedPages.data; const published = data.every(p => p.status === "published"); return published && data[0].title === "page-c"; }, diff --git a/packages/api-page-builder/__tests__/graphql/pagesListingLatest.test.ts b/packages/api-page-builder/__tests__/graphql/pagesListingLatest.test.ts index 6d1d775fa9c..fcf8503fb54 100644 --- a/packages/api-page-builder/__tests__/graphql/pagesListingLatest.test.ts +++ b/packages/api-page-builder/__tests__/graphql/pagesListingLatest.test.ts @@ -1,6 +1,5 @@ import useGqlHandler from "./useGqlHandler"; import { Page } from "~/types"; -import { waitPage } from "./utils/waitPage"; jest.setTimeout(100000); @@ -40,7 +39,6 @@ describe("listing latest pages", () => { throw new Error(response.data.pageBuilder.createPage.error.message); } - await waitPage(handler, page); const title = `page-${letter}`; const [updateResponse] = await updatePage({ id: page.id, @@ -158,7 +156,6 @@ describe("listing latest pages", () => { throw new Error(res.data.pageBuilder.createPage.error.message); } const page = res.data.pageBuilder.createPage.data; - await waitPage(handler, page); await updatePage({ id: page.id, data: { @@ -223,7 +220,6 @@ describe("listing latest pages", () => { const page = response.data.pageBuilder.createPage.data; - await waitPage(handler, page); const title = `page-${letter}`; await updatePage({ id: page.id, @@ -231,10 +227,6 @@ describe("listing latest pages", () => { title } }); - await waitPage(handler, { - ...page, - title - }); } // Just in case, ensure all ten pages are present. @@ -821,15 +813,11 @@ describe("listing latest pages", () => { }); const page = createPageResponse.data.pageBuilder.createPage.data; - await waitPage(handler, page); + await updatePage({ id: page.id, data }); - await waitPage(handler, { - ...page, - title: data.title - }); } await until( diff --git a/packages/api-page-builder/__tests__/graphql/simple.pages.test.ts b/packages/api-page-builder/__tests__/graphql/simple.pages.test.ts index be6e2176015..a2e9bb531ac 100644 --- a/packages/api-page-builder/__tests__/graphql/simple.pages.test.ts +++ b/packages/api-page-builder/__tests__/graphql/simple.pages.test.ts @@ -1,6 +1,5 @@ import useGqlHandler from "./useGqlHandler"; import { Page } from "~/types"; -import { waitPage } from "./utils/waitPage"; const sort: string[] = ["createdOn_DESC"]; @@ -282,7 +281,6 @@ describe("pages simple actions", () => { category: category.slug }); const page = createResponse.data.pageBuilder.createPage.data; - await waitPage(handler, page); const title = "Page updated title"; @@ -292,10 +290,6 @@ describe("pages simple actions", () => { title } }); - await waitPage(handler, { - ...page, - title - }); await handler.publishPage({ id: page.id @@ -377,7 +371,6 @@ describe("pages simple actions", () => { category: category.slug }); const page = createResponse.data.pageBuilder.createPage.data; - await waitPage(handler, page); const title = "Page updated title"; @@ -387,19 +380,10 @@ describe("pages simple actions", () => { title } }); - await waitPage(handler, { - ...page, - title - }); await handler.publishPage({ id: page.id }); - await waitPage(handler, { - ...page, - title, - status: "published" - }); const [response] = await handler.unpublishPage({ id: page.id @@ -446,7 +430,6 @@ describe("pages simple actions", () => { category: category.slug }); const page = createResponse.data.pageBuilder.createPage.data; - await waitPage(handler, page); const title = "Page updated title"; @@ -456,19 +439,10 @@ describe("pages simple actions", () => { title } }); - await waitPage(handler, { - ...page, - title - }); await handler.publishPage({ id: page.id }); - await waitPage(handler, { - ...page, - title, - status: "published" - }); await handler.unpublishPage({ id: page.id diff --git a/packages/api-page-builder/__tests__/graphql/utils/waitPage.ts b/packages/api-page-builder/__tests__/graphql/utils/waitPage.ts deleted file mode 100644 index e4cc62b5099..00000000000 --- a/packages/api-page-builder/__tests__/graphql/utils/waitPage.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Page } from "~/types"; - -interface Handler { - until: Function; - listPages: Function; -} - -export const waitPage = async (handler: Handler, page: Page) => { - const pageIdentifier = page.title.match("Untitled") === null ? page.title : page.id; - await handler.until( - () => - handler.listPages({ - sort: ["createdOn_DESC"] - }), - ([response]: any) => { - return response.data.pageBuilder.listPages.data.some((item: any) => { - return item.id === page.id && item.title === page.title; - }); - }, - { - name: `waiting for page ${pageIdentifier}` - } - ); -}; diff --git a/packages/api-page-builder/src/graphql/crud/menus/prepareMenuItems.ts b/packages/api-page-builder/src/graphql/crud/menus/prepareMenuItems.ts index 98a076ea1ac..19b8a6ec133 100644 --- a/packages/api-page-builder/src/graphql/crud/menus/prepareMenuItems.ts +++ b/packages/api-page-builder/src/graphql/crud/menus/prepareMenuItems.ts @@ -1,13 +1,8 @@ -/** - * Figure out correct types. - */ -// TODO @ts-refactor -// @ts-nocheck import cloneDeep from "lodash/cloneDeep"; -import { PbContext } from "../../types"; +import { ListPagesParamsWhere, PbContext } from "../../types"; import { Menu } from "~/types"; -const applyCleanup = async items => { +const applyCleanup = async (items: Menu["items"]) => { if (!Array.isArray(items)) { return; } @@ -26,7 +21,13 @@ const applyCleanup = async items => { } }; -const applyModifier = async ({ items, modifier, context }) => { +interface ApplyModifierParams { + items: Menu["items"]; + modifier: (args: { item: Record; context: PbContext }) => void; + context: PbContext; +} + +const applyModifier = async ({ items, modifier, context }: ApplyModifierParams) => { if (!Array.isArray(items)) { return; } @@ -44,7 +45,7 @@ const prepareItems = async ({ modifiers, context }: { - items?: Record[]; + items: Menu["items"]; modifiers: Array<(args: { item: Record; context: PbContext }) => void>; context: PbContext; }) => { @@ -58,8 +59,7 @@ const prepareItems = async ({ }; export default async ({ menu, context }: { menu: Menu; context: PbContext }) => { - // TODO determine real type - const items: any = cloneDeep(menu.items); + const items = cloneDeep(menu.items) as Menu["items"]; // Each modifier is recursively applied to all items. await prepareItems({ items, @@ -87,7 +87,10 @@ export default async ({ menu, context }: { menu: Menu; context: PbContext }) => if (item.type === "pages-list") { const { category, sortBy, sortDir, tags, tagsRule } = item; - const where = { category, tags: null }; + const where: ListPagesParamsWhere = { + category, + tags: undefined + }; if (tags) { where.tags = { query: tags, rule: tagsRule || "all" }; } diff --git a/packages/api-page-builder/src/graphql/crud/pageTemplates.crud.ts b/packages/api-page-builder/src/graphql/crud/pageTemplates.crud.ts index a4f16f9d635..46897db0c7f 100644 --- a/packages/api-page-builder/src/graphql/crud/pageTemplates.crud.ts +++ b/packages/api-page-builder/src/graphql/crud/pageTemplates.crud.ts @@ -141,7 +141,7 @@ export const createPageTemplatesCrud = ( tenant: getTenantId(), locale: getLocaleCode() }, - sort: Array.isArray(sort) && sort.length > 0 ? sort : ["createdOn_ASC"] + sort: Array.isArray(sort) && sort.length > 0 ? sort : ["createdOn_DESC"] }; // If user can only manage own records, let's add that to the listing. diff --git a/packages/api-page-builder/src/graphql/crud/system.crud.ts b/packages/api-page-builder/src/graphql/crud/system.crud.ts index 664f63c167e..ffdaf9f08f0 100644 --- a/packages/api-page-builder/src/graphql/crud/system.crud.ts +++ b/packages/api-page-builder/src/graphql/crud/system.crud.ts @@ -167,7 +167,7 @@ export const createSystemCrud = (params: CreateSystemCrudParams): SystemCrud => /** * Category is missing, but we cannot set it because it will override the created one. */ - // @ts-ignore + // @ts-expect-error { title: "Not Found", path: "/not-found", @@ -177,7 +177,7 @@ export const createSystemCrud = (params: CreateSystemCrudParams): SystemCrud => /** * Category is missing, but we cannot set it because it will override the created one. */ - // @ts-ignore + // @ts-expect-error { title: "Welcome to Webiny", path: "/welcome-to-webiny", diff --git a/packages/api-page-builder/src/graphql/types.ts b/packages/api-page-builder/src/graphql/types.ts index fcd2ae3d785..8a88285d8c1 100644 --- a/packages/api-page-builder/src/graphql/types.ts +++ b/packages/api-page-builder/src/graphql/types.ts @@ -3,37 +3,38 @@ import { SecurityContext, SecurityPermission } from "@webiny/api-security/types" import { TenancyContext } from "@webiny/api-tenancy/types"; import { I18NContext } from "@webiny/api-i18n/types"; import { Topic } from "@webiny/pubsub/types"; -import { RenderEvent, FlushEvent, QueueAddJob } from "@webiny/api-prerendering-service/types"; +import { FlushEvent, QueueAddJob, RenderEvent } from "@webiny/api-prerendering-service/types"; import { Context as BaseContext } from "@webiny/handler/types"; import { - PageBlock, - PageTemplate, BlockCategory, Category, DefaultSettings, Menu, Page, + PageBlock, PageElement, PageSettings, PageSpecialType, + PageTemplate, + PageTemplateInput, Settings, - System, - PageTemplateInput + System } from "~/types"; import { PrerenderingServiceClientContext } from "@webiny/api-prerendering-service/client/types"; import { FileManagerContext } from "@webiny/api-file-manager/types"; // CRUD types. +export interface ListPagesParamsWhere { + category?: string; + status?: string; + tags?: { query: string[]; rule?: "any" | "all" }; + [key: string]: any; +} export interface ListPagesParams { limit?: number; after?: string | null; - where?: { - category?: string; - status?: string; - tags?: { query: string[]; rule?: "any" | "all" }; - [key: string]: any; - }; + where?: ListPagesParamsWhere; exclude?: string[]; search?: { query?: string }; sort?: string[]; diff --git a/packages/api-page-builder/src/installation/createInstallationZip.ts b/packages/api-page-builder/src/installation/createInstallationZip.ts index 0e38319be44..8c8bb20ce03 100644 --- a/packages/api-page-builder/src/installation/createInstallationZip.ts +++ b/packages/api-page-builder/src/installation/createInstallationZip.ts @@ -2,7 +2,7 @@ import path from "path"; /** * Package zip-local does not have types. */ -// @ts-ignore +// @ts-expect-error import zipper from "zip-local"; import fs from "fs"; diff --git a/packages/api-prerendering-service/__tests__/render/handlers/render/linkPreloading.test.ts b/packages/api-prerendering-service/__tests__/render/handlers/render/linkPreloading.test.ts index 576a61cbfb1..9ce663a69fe 100644 --- a/packages/api-prerendering-service/__tests__/render/handlers/render/linkPreloading.test.ts +++ b/packages/api-prerendering-service/__tests__/render/handlers/render/linkPreloading.test.ts @@ -8,10 +8,10 @@ describe(`"renderUrl" Function Test`, () => { it("should insert basic meta data into the received HTML", async () => { const [[html], meta] = await render("https://some-url.com", { context: {} as Context, - // @ts-ignore + // @ts-expect-error args: {}, configuration: {}, - // @ts-ignore + // @ts-expect-error renderUrlFunction: async () => { return { content: BASE_HTML, @@ -54,7 +54,7 @@ describe(`"renderUrl" Function Test`, () => { tenant: "root", locale: "en-US" }, - // @ts-ignore + // @ts-expect-error renderUrlFunction: async () => { return { content: BASE_HTML, diff --git a/packages/api-prerendering-service/__tests__/render/handlers/render/renderUrl.test.ts b/packages/api-prerendering-service/__tests__/render/handlers/render/renderUrl.test.ts index 576a61cbfb1..9ce663a69fe 100644 --- a/packages/api-prerendering-service/__tests__/render/handlers/render/renderUrl.test.ts +++ b/packages/api-prerendering-service/__tests__/render/handlers/render/renderUrl.test.ts @@ -8,10 +8,10 @@ describe(`"renderUrl" Function Test`, () => { it("should insert basic meta data into the received HTML", async () => { const [[html], meta] = await render("https://some-url.com", { context: {} as Context, - // @ts-ignore + // @ts-expect-error args: {}, configuration: {}, - // @ts-ignore + // @ts-expect-error renderUrlFunction: async () => { return { content: BASE_HTML, @@ -54,7 +54,7 @@ describe(`"renderUrl" Function Test`, () => { tenant: "root", locale: "en-US" }, - // @ts-ignore + // @ts-expect-error renderUrlFunction: async () => { return { content: BASE_HTML, diff --git a/packages/api-prerendering-service/src/render/renderUrl.ts b/packages/api-prerendering-service/src/render/renderUrl.ts index d5d82c3fe9f..207137e5b37 100644 --- a/packages/api-prerendering-service/src/render/renderUrl.ts +++ b/packages/api-prerendering-service/src/render/renderUrl.ts @@ -5,7 +5,7 @@ import { noopener } from "posthtml-noopener"; /** * Package posthtml-plugin-link-preload has no types. */ -// @ts-ignore +// @ts-expect-error import posthtmlPluginLinkPreload from "posthtml-plugin-link-preload"; import absoluteAssetUrls from "./absoluteAssetUrls"; import injectApolloState from "./injectApolloState"; @@ -63,7 +63,7 @@ export default async (url: string, args: RenderUrlParams): Promise<[File[], Meta const render = await renderUrl(url, args); // Process HTML. - // TODO: should be plugins (will also eliminate lower @ts-ignore instructions). + // TODO: should be plugins (will also eliminate lower ts-ignore instructions). console.log("Processing HTML..."); // TODO: regular text processing plugins... @@ -207,7 +207,7 @@ export const defaultRenderUrlFunction = async ( await browserPage.goto(url, { waitUntil: "networkidle0" }); const apolloState = await browserPage.evaluate(() => { - // @ts-ignore + // @ts-expect-error return window.getApolloState(); }); diff --git a/packages/api-security-cognito/src/createAdminUsersHooks.ts b/packages/api-security-cognito/src/createAdminUsersHooks.ts index f23494a7b3c..376f64ad885 100644 --- a/packages/api-security-cognito/src/createAdminUsersHooks.ts +++ b/packages/api-security-cognito/src/createAdminUsersHooks.ts @@ -46,7 +46,7 @@ export const createAdminUsersHooks = () => { /** * Check few lines up. */ - // @ts-ignore + // @ts-expect-error tenant, // IMPORTANT! // Use the `id` that was assigned in the user creation process. @@ -115,7 +115,7 @@ export const createAdminUsersHooks = () => { * TODO @ts-refactor @pavel * Same as in afterCreate method */ - // @ts-ignore + // @ts-expect-error tenant, identity: updatedUser.id, @@ -142,7 +142,7 @@ export const createAdminUsersHooks = () => { * TODO @ts-refactor @pavel * Same as in afterCreate method */ - // @ts-ignore + // @ts-expect-error tenant, identity: user.id, diff --git a/packages/api-security-okta/src/createAuthenticator.ts b/packages/api-security-okta/src/createAuthenticator.ts index 39b39acbd28..46d293a3e2c 100644 --- a/packages/api-security-okta/src/createAuthenticator.ts +++ b/packages/api-security-okta/src/createAuthenticator.ts @@ -59,7 +59,7 @@ export const createAuthenticator = (config: AuthenticatorConfig) => { * Figure out the types. * TODO @ts-refactor */ - // @ts-ignore + // @ts-expect-error const token = (await verify(idToken, jwkToPem(jwk))) as VerifyResponse; if (!token.jti || !token.jti.startsWith("ID.")) { throw new WebinyError("idToken is invalid!", "SECURITY_OKTA_INVALID_TOKEN"); diff --git a/packages/api-security/__tests__/graphql/parallelQueries.ts b/packages/api-security/__tests__/graphql/parallelQueries.ts new file mode 100644 index 00000000000..20da55e6877 --- /dev/null +++ b/packages/api-security/__tests__/graphql/parallelQueries.ts @@ -0,0 +1,49 @@ +import { GraphQLSchemaPlugin } from "@webiny/handler-graphql"; +import { SecurityContext } from "~/types"; + +export const PARALLEL_QUERY = /* GraphQL */ ` + query ParallelQueries { + withoutAuthorization + withAuthorization + security { + listApiKeys { + data { + id + token + } + error { + code + } + } + } + } +`; + +export const withoutAuthorizationPlugin = new GraphQLSchemaPlugin({ + typeDefs: /* GraphQL*/ ` + extend type Query { + withoutAuthorization: String! + withAuthorization: String! + } + `, + resolvers: { + Query: { + withoutAuthorization(_, args, context) { + return context.security.withoutAuthorization(async () => { + const permissions = await context.security.getPermissions("security.apiKey"); + if (!permissions.length) { + return "NOT_AUTHORIZED"; + } + return "YOUR DATA!"; + }); + }, + async withAuthorization(_, args, context) { + const permissions = await context.security.getPermissions("security.apiKey"); + if (!permissions.length) { + return "NOT_AUTHORIZED"; + } + return "AUTHORIZED"; + } + } + } +}); diff --git a/packages/api-security/__tests__/identity.test.ts b/packages/api-security/__tests__/identity.test.ts index 8daf1624db4..83d743add07 100644 --- a/packages/api-security/__tests__/identity.test.ts +++ b/packages/api-security/__tests__/identity.test.ts @@ -4,7 +4,7 @@ import { getStorageOps } from "@webiny/project-utils/testing/environment"; describe("identity test", () => { const tenant = "root"; - // @ts-ignore + // @ts-expect-error const { storageOperations } = getStorageOps("security"); let security: Security; diff --git a/packages/api-security/__tests__/login.test.ts b/packages/api-security/__tests__/login.test.ts index 5b784c7ee3d..6d2d586ab45 100644 --- a/packages/api-security/__tests__/login.test.ts +++ b/packages/api-security/__tests__/login.test.ts @@ -11,7 +11,7 @@ describe(`"Login" test`, () => { if (response.data.security.install.error) { throw new Error(response.data.security.install.error.message); - // @ts-ignore + // @ts-expect-error process.exit(0); } }); diff --git a/packages/api-security/__tests__/mocks/customAuthenticator.ts b/packages/api-security/__tests__/mocks/customAuthenticator.ts index d9c3cc1b961..2907123a098 100644 --- a/packages/api-security/__tests__/mocks/customAuthenticator.ts +++ b/packages/api-security/__tests__/mocks/customAuthenticator.ts @@ -6,13 +6,9 @@ interface Context extends BaseContext, SecurityContext {} export const customAuthenticator = () => { return new ContextPlugin(context => { - /** - * TODO @pavel can we return null? - */ - // @ts-ignore context.security.addAuthenticator(async () => { if ("authorization" in context.request.headers) { - return; + return null; } return { diff --git a/packages/api-security/__tests__/parallelQueries.test.ts b/packages/api-security/__tests__/parallelQueries.test.ts new file mode 100644 index 00000000000..0047dcc19b5 --- /dev/null +++ b/packages/api-security/__tests__/parallelQueries.test.ts @@ -0,0 +1,31 @@ +import useGqlHandler from "./useGqlHandler"; +import { PARALLEL_QUERY, withoutAuthorizationPlugin } from "./graphql/parallelQueries"; + +describe("Security Parallel Queries", () => { + const { install, invoke } = useGqlHandler({ plugins: [withoutAuthorizationPlugin] }); + + beforeEach(async () => { + await install.install(); + }); + + test("should not disable authorization in parallel queries", async () => { + const [response] = await invoke({ + body: { query: PARALLEL_QUERY }, + // We want to simulate an anonymous user. + headers: { authorization: "anonymous" } + }); + + expect(response.data).toEqual({ + withoutAuthorization: "YOUR DATA!", + withAuthorization: "NOT_AUTHORIZED", + security: { + listApiKeys: { + data: null, + error: { + code: "SECURITY_NOT_AUTHORIZED" + } + } + } + }); + }); +}); diff --git a/packages/api-security/__tests__/wcp/aacl/customPermissionsFiltering.test.ts b/packages/api-security/__tests__/wcp/aacl/customPermissionsFiltering.test.ts index 03c6bd12a3b..3b17494048f 100644 --- a/packages/api-security/__tests__/wcp/aacl/customPermissionsFiltering.test.ts +++ b/packages/api-security/__tests__/wcp/aacl/customPermissionsFiltering.test.ts @@ -3,8 +3,9 @@ import { customPermissions } from "./mocks/customPermissions"; describe("Custom permissions filtering test", () => { it("should filter out all custom permission objects", async () => { - // @ts-ignore Even though this object doesn't contain the `name` property + // Even though this object doesn't contain the `name` property // and it's not valid according to TS, we still want to have it in our test. + // @ts-expect-error expect(filterOutCustomWbyAppsPermissions(customPermissions)).toEqual([ { something: "custom" }, { name: "custom" }, diff --git a/packages/api-security/__tests__/wcp/aacl/mocks/customAuthenticator.ts b/packages/api-security/__tests__/wcp/aacl/mocks/customAuthenticator.ts index 28182ca926f..d586e668203 100644 --- a/packages/api-security/__tests__/wcp/aacl/mocks/customAuthenticator.ts +++ b/packages/api-security/__tests__/wcp/aacl/mocks/customAuthenticator.ts @@ -1,9 +1,7 @@ -// @ts-nocheck import { SecurityContext } from "@webiny/api-security/types"; -// import { HttpContext } from "@webiny/handler-http/types"; import { ContextPlugin } from "@webiny/handler"; -interface Context extends HttpContext, SecurityContext {} +type Context = SecurityContext; export const customAuthenticator = () => { return new ContextPlugin(context => { diff --git a/packages/api-security/__tests__/wcp/aacl/mocks/customAuthorizer.ts b/packages/api-security/__tests__/wcp/aacl/mocks/customAuthorizer.ts index 4ca67ed80d5..3205e1de034 100644 --- a/packages/api-security/__tests__/wcp/aacl/mocks/customAuthorizer.ts +++ b/packages/api-security/__tests__/wcp/aacl/mocks/customAuthorizer.ts @@ -3,8 +3,9 @@ import { ContextPlugin } from "@webiny/handler"; export const customAuthorizer = () => { return new ContextPlugin(({ security }) => { - // @ts-ignore Even though this object doesn't contain the `name` property + // Even though this object doesn't contain the `name` property // and it's not valid according to TS, we still want to have it in our test. + // @ts-expect-error security.addAuthorizer(async () => { // Use customPermission object. return [ diff --git a/packages/api-security/__tests__/withoutAuthorization.test.ts b/packages/api-security/__tests__/withoutAuthorization.test.ts index f3ffadabb09..b7c0ca183e3 100644 --- a/packages/api-security/__tests__/withoutAuthorization.test.ts +++ b/packages/api-security/__tests__/withoutAuthorization.test.ts @@ -11,7 +11,9 @@ describe("without authorization", function () { advancedAccessControlLayer: { enabled: true, options: { - teams: false + teams: false, + folderLevelPermissions: false, + privateFiles: false } }, storageOperations: {} as SecurityStorageOperations, @@ -24,7 +26,7 @@ describe("without authorization", function () { security = await createSecurity(config); }); - it("should disable authorization inside the withoutAuthorization method", async () => { + it(`should disable authorization inside "withoutAuthorization" execution scope`, async () => { /** * Should not return permission as user does not have it (not defined in this case) */ @@ -47,74 +49,29 @@ describe("without authorization", function () { expect(noPermissionCheckAfterWithoutAuthorization).toEqual(null); }); - it("should not enable authorization if it was disabled before the withoutAuthorization method", async () => { - security.disableAuthorization(); - /** - * Should have full permission as we are disabling authorization. - */ - const noPermissionCheck = await security.getPermission("some-unknown-permission"); - expect(noPermissionCheck).toEqual(fullPermissions); - /** - * Should return full permission as we are disabling authorization. - */ - const result = await security.withoutAuthorization(async () => { - return security.getPermission("some-unknown-permission"); - }); - - expect(result).toEqual(fullPermissions); - /** - * Should have full permission as the authorization is not still enabled. - */ - const hasPermissionsCheckAfterWithoutAuthorization = await security.getPermission( - "some-unknown-permission" - ); - expect(hasPermissionsCheckAfterWithoutAuthorization).toEqual(fullPermissions); - security.enableAuthorization(); - /** - * Should not have permission again. - */ - const noPermissionCheckAfterEnabling = await security.getPermission( - "some-unknown-permission" - ); - expect(noPermissionCheckAfterEnabling).toEqual(null); - }); - - it("should enable authorization if there is an exception in the function - previously enabled authorization", async () => { + it("should re-enable authorization if callback throws an error", async () => { let error: Error | null = null; let result: any = null; + let authorizationWithinCallback = null; + const noPermissionCheck = await security.getPermission("some-unknown-permission"); expect(noPermissionCheck).toEqual(null); + try { result = await security.withoutAuthorization(async () => { + authorizationWithinCallback = security.isAuthorizationEnabled(); throw new Error("Some error"); }); } catch (ex) { error = ex; } + expect(result).toBeNull(); expect(error?.message).toEqual("Some error"); + expect(authorizationWithinCallback).toBe(false); + expect(security.isAuthorizationEnabled()).toBe(true); const stillNoPermissionCheck = await security.getPermission("some-unknown-permission"); expect(stillNoPermissionCheck).toEqual(null); }); - - it("should not enable authorization if there is an exception in the function - previously disabled authorization", async () => { - let error: Error | null = null; - let result: any = null; - security.disableAuthorization(); - const hasPermissionCheck = await security.getPermission("some-unknown-permission"); - expect(hasPermissionCheck).toEqual(fullPermissions); - try { - result = await security.withoutAuthorization(async () => { - throw new Error("Some error"); - }); - } catch (ex) { - error = ex; - } - expect(result).toBeNull(); - expect(error?.message).toEqual("Some error"); - - const stillHasPermissionCheck = await security.getPermission("some-unknown-permission"); - expect(stillHasPermissionCheck).toEqual(fullPermissions); - }); }); diff --git a/packages/api-security/src/createSecurity.ts b/packages/api-security/src/createSecurity.ts index 93f37be8aef..d16c85fea22 100644 --- a/packages/api-security/src/createSecurity.ts +++ b/packages/api-security/src/createSecurity.ts @@ -1,3 +1,4 @@ +import { AsyncLocalStorage } from "async_hooks"; import minimatch from "minimatch"; import { createAuthentication } from "@webiny/api-authentication/createAuthentication"; import { Authorizer, Security, SecurityPermission, SecurityConfig } from "./types"; @@ -14,15 +15,16 @@ export interface GetTenant { (): string | undefined; } +const asyncLocalStorage = new AsyncLocalStorage(); + export const createSecurity = async (config: SecurityConfig): Promise => { const authentication = createAuthentication(); const authorizers: Authorizer[] = []; - let performAuthorization = true; let permissions: SecurityPermission[]; let permissionsLoader: Promise; - const loadPermissions = async (security: Security): Promise => { + const loadPermissions = async (): Promise => { if (permissions) { return permissions; } @@ -31,13 +33,11 @@ export const createSecurity = async (config: SecurityConfig): Promise return permissionsLoader; } - const shouldEnableAuthorization = performAuthorization; permissionsLoader = new Promise(async resolve => { // Authorizers often need to query business-related data, and since the identity is not yet // authorized, these operations can easily trigger a NOT_AUTHORIZED error. // To avoid this, we disable permission checks (assume `full-access` permissions) for // the duration of the authorization process. - security.disableAuthorization(); for (const authorizer of authorizers) { const result = await authorizer(); if (Array.isArray(result)) { @@ -48,13 +48,6 @@ export const createSecurity = async (config: SecurityConfig): Promise // Set an empty array since no permissions were found. permissions = []; resolve(permissions); - }).then(permissions => { - // Re-enable authorization. - if (shouldEnableAuthorization) { - security.enableAuthorization(); - } - - return permissions; }); return permissionsLoader; @@ -70,12 +63,6 @@ export const createSecurity = async (config: SecurityConfig): Promise getStorageOperations() { return config.storageOperations; }, - enableAuthorization() { - performAuthorization = true; - }, - disableAuthorization() { - performAuthorization = false; - }, addAuthorizer(authorizer: Authorizer) { authorizers.push(authorizer); }, @@ -87,24 +74,16 @@ export const createSecurity = async (config: SecurityConfig): Promise this.onIdentity.publish({ identity }); }, isAuthorizationEnabled: () => { - return performAuthorization; + return asyncLocalStorage.getStore() ?? true; }, - async withoutAuthorization(cb: () => Promise): Promise { - const isAuthorizationEnabled = performAuthorization; - performAuthorization = false; - try { - return await cb(); - } finally { - if (isAuthorizationEnabled) { - performAuthorization = true; - } - } + async withoutAuthorization(this: Security, cb: () => Promise): Promise { + return await asyncLocalStorage.run(false, cb); }, async getPermission( this: Security, permission: string ): Promise { - if (!performAuthorization) { + if (!this.isAuthorizationEnabled()) { return { name: "*" } as TPermission; } @@ -129,7 +108,7 @@ export const createSecurity = async (config: SecurityConfig): Promise this: Security, permission: string ): Promise { - if (!performAuthorization) { + if (!this.isAuthorizationEnabled()) { return [{ name: "*" }] as TPermission[]; } @@ -146,7 +125,7 @@ export const createSecurity = async (config: SecurityConfig): Promise }, async listPermissions(this: Security): Promise { - const permissions = await loadPermissions(this); + const permissions = await this.withoutAuthorization(() => loadPermissions()); // Now we start checking whether we want to return all permissions, or we // need to omit the custom ones because of the one of the following reasons. diff --git a/packages/api-security/src/createSecurity/createApiKeysMethods.ts b/packages/api-security/src/createSecurity/createApiKeysMethods.ts index 260221370f2..50a56c5d75f 100644 --- a/packages/api-security/src/createSecurity/createApiKeysMethods.ts +++ b/packages/api-security/src/createSecurity/createApiKeysMethods.ts @@ -2,12 +2,12 @@ import crypto from "crypto"; /** * Package @commodo/fields does not have types. */ -// @ts-ignore +// @ts-expect-error import { withFields, string } from "@commodo/fields"; /** * Package commodo-fields-object does not have types. */ -// @ts-ignore +// @ts-expect-error import { object } from "commodo-fields-object"; import { validation } from "@webiny/validation"; import { createTopic } from "@webiny/pubsub"; diff --git a/packages/api-security/src/createSecurity/createGroupsMethods.ts b/packages/api-security/src/createSecurity/createGroupsMethods.ts index 7e9319e4bac..3a0dd8e31a3 100644 --- a/packages/api-security/src/createSecurity/createGroupsMethods.ts +++ b/packages/api-security/src/createSecurity/createGroupsMethods.ts @@ -1,17 +1,17 @@ /** * Package deep-equal does not have types. */ -// @ts-ignore +// @ts-expect-error import deepEqual from "deep-equal"; /** * Package commodo-fields-object does not have types. */ -// @ts-ignore +// @ts-expect-error import { object } from "commodo-fields-object"; /** * Package @commodo/fields does not have types. */ -// @ts-ignore +// @ts-expect-error import { withFields, string } from "@commodo/fields"; import { createTopic } from "@webiny/pubsub"; import { validation } from "@webiny/validation"; diff --git a/packages/api-security/src/createSecurity/createTeamsMethods.ts b/packages/api-security/src/createSecurity/createTeamsMethods.ts index b525ecd6858..b9e620462e0 100644 --- a/packages/api-security/src/createSecurity/createTeamsMethods.ts +++ b/packages/api-security/src/createSecurity/createTeamsMethods.ts @@ -3,12 +3,12 @@ import { mdbid } from "@webiny/utils"; /** * Package deep-equal does not have types. */ -// @ts-ignore +// @ts-expect-error import deepEqual from "deep-equal"; /** * Package @commodo/fields does not have types. */ -// @ts-ignore +// @ts-expect-error import { withFields, string } from "@commodo/fields"; import { createTopic } from "@webiny/pubsub"; import { validation } from "@webiny/validation"; diff --git a/packages/api-security/src/plugins/tenantLinkAuthorization.ts b/packages/api-security/src/plugins/tenantLinkAuthorization.ts index ad6cebdd2d3..33baaf9af77 100644 --- a/packages/api-security/src/plugins/tenantLinkAuthorization.ts +++ b/packages/api-security/src/plugins/tenantLinkAuthorization.ts @@ -15,12 +15,12 @@ export const createTenantLinkAuthorizer = (config: Config) => (context: Context) const identity = context.security.getIdentity(); const tenant = context.tenancy.getCurrentTenant(); - // @ts-ignore // I18N is not a dependency of this package. Yet, it always goes hand in hand with it. // Since, in the future, we'll most probably merge all of these base packages into one, // we'll just ignore the TS error for now and pretend I18N is always available. // This way we make the setup easier for the end user; no need to create an extra // NPM package just to get the I18N context which the user would need to set up manually. + // @ts-expect-error const locale = context.i18n?.getContentLocale() as { code: string }; if (!locale) { return null; diff --git a/packages/api-security/src/types.ts b/packages/api-security/src/types.ts index c6bb6703670..4992a713867 100644 --- a/packages/api-security/src/types.ts +++ b/packages/api-security/src/types.ts @@ -90,22 +90,6 @@ export interface Security extends Authentication(cb: () => Promise): Promise; - /** - * Replace in favor of withoutAuthorization. - * - * If really required, should be used carefully. - * @deprecated - */ - enableAuthorization(): void; - - /** - * Replace in favor of withoutAuthorization. - * - * If really required, should be used carefully. - * @deprecated - */ - disableAuthorization(): void; - addAuthorizer(authorizer: Authorizer): void; getAuthorizers(): Authorizer[]; diff --git a/packages/api/src/Context.ts b/packages/api/src/Context.ts index b6664e556df..46c7f5e42b4 100644 --- a/packages/api/src/Context.ts +++ b/packages/api/src/Context.ts @@ -134,7 +134,7 @@ export class Context implements ContextInterface { * TODO @ts-refactor * Problem with possible subtype initialization */ - // @ts-ignore + // @ts-expect-error cb }); } diff --git a/packages/app-aco/src/components/AdvancedSearch/QueryBuilderDrawer/QueryBuilderDrawer.tsx b/packages/app-aco/src/components/AdvancedSearch/QueryBuilderDrawer/QueryBuilderDrawer.tsx index 8a94b83acbe..5cb6ac49433 100644 --- a/packages/app-aco/src/components/AdvancedSearch/QueryBuilderDrawer/QueryBuilderDrawer.tsx +++ b/packages/app-aco/src/components/AdvancedSearch/QueryBuilderDrawer/QueryBuilderDrawer.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite"; import { FormAPI } from "@webiny/form"; import { DrawerContent } from "@webiny/ui/Drawer"; -// @ts-ignore +// @ts-expect-error import { useHotkeys } from "react-hotkeyz"; import { Footer } from "./Footer"; import { Header } from "./Header"; diff --git a/packages/app-admin-cognito/src/index.tsx b/packages/app-admin-cognito/src/index.tsx index fe9eb9bcfda..494547985b1 100644 --- a/packages/app-admin-cognito/src/index.tsx +++ b/packages/app-admin-cognito/src/index.tsx @@ -86,7 +86,7 @@ export const createAuthentication: AuthenticationFactory = ({ /** * TODO @ts-refactor */ - // @ts-ignore + // @ts-expect-error Object.keys(config).forEach(key => config[key] === undefined && delete config[key]); Auth.configure({ ...defaultOptions, ...config }); diff --git a/packages/app-admin-okta/src/OktaSignInWidget.tsx b/packages/app-admin-okta/src/OktaSignInWidget.tsx index 373bd6d7ac0..5d4419a107e 100644 --- a/packages/app-admin-okta/src/OktaSignInWidget.tsx +++ b/packages/app-admin-okta/src/OktaSignInWidget.tsx @@ -45,7 +45,7 @@ const OktaSignInWidget: React.FC = ({ oktaSignIn }) => { /** * TODO @ts-refactor figure out correct widgetRef type @pavel */ - // @ts-ignore + // @ts-expect-error el: widgetRef.current }, res => { diff --git a/packages/app-admin-rmwc/src/modules/Layout.tsx b/packages/app-admin-rmwc/src/modules/Layout.tsx index a073e64fcde..f7a225bf5af 100644 --- a/packages/app-admin-rmwc/src/modules/Layout.tsx +++ b/packages/app-admin-rmwc/src/modules/Layout.tsx @@ -1,15 +1,15 @@ import React, { Fragment } from "react"; import Helmet from "react-helmet"; import { + Brand, Compose, - LayoutRenderer, LayoutProps, - Brand, - Search, + LayoutRenderer, LocaleSelector, - UserMenu, Navigation, - Tags + Search, + Tags, + UserMenu } from "@webiny/app-admin"; import { TopAppBarPrimary, TopAppBarSection } from "@webiny/ui/TopAppBar"; @@ -40,9 +40,5 @@ const RMWCLayout = (): React.FC => { }; export const Layout: React.FC = () => { - /** - * TODO @ts-refactor @pavel - */ - // @ts-ignore return ; }; diff --git a/packages/app-admin-rmwc/src/modules/Overlays/OmniSearch.tsx b/packages/app-admin-rmwc/src/modules/Overlays/OmniSearch.tsx index 1bca9fe9899..3386f1f80fa 100644 --- a/packages/app-admin-rmwc/src/modules/Overlays/OmniSearch.tsx +++ b/packages/app-admin-rmwc/src/modules/Overlays/OmniSearch.tsx @@ -4,8 +4,10 @@ import { useNavigate } from "@webiny/react-router"; import { useSnackbar } from "@webiny/app-admin"; import { getTenantId } from "@webiny/app/utils"; import { ReactComponent as SearchIcon } from "@material-design-icons/svg/outlined/search.svg"; - -// @ts-ignore Library doesn't have types. +/** + * Library does not have types. + */ +// @ts-expect-error import { useHotkeys } from "react-hotkeyz"; import { Input } from "@webiny/ui/Input"; import { Typography } from "@webiny/ui/Typography"; diff --git a/packages/app-admin-rmwc/src/modules/Overlays/index.tsx b/packages/app-admin-rmwc/src/modules/Overlays/index.tsx index 7d0b2828662..244dd9af3e7 100644 --- a/packages/app-admin-rmwc/src/modules/Overlays/index.tsx +++ b/packages/app-admin-rmwc/src/modules/Overlays/index.tsx @@ -28,9 +28,5 @@ const OverlaysHOC = (Component: React.FC): React.FC => { }; export const Overlays: React.FC = () => { - /** - * TODO @ts-refactor @pavel - */ - // @ts-ignore return ; }; diff --git a/packages/app-admin-users-cognito/src/createAuthentication/createAuthentication.tsx b/packages/app-admin-users-cognito/src/createAuthentication/createAuthentication.tsx index d34406c561a..1be86d47976 100644 --- a/packages/app-admin-users-cognito/src/createAuthentication/createAuthentication.tsx +++ b/packages/app-admin-users-cognito/src/createAuthentication/createAuthentication.tsx @@ -35,7 +35,7 @@ export const createAuthentication = (config: CreateAuthenticationConfig = {}) => * createGetIdentityData return function does not have payload param so TS is complaining. * createGetIdentityData does not need the payload param */ - // @ts-ignore + // @ts-expect-error return {children}; }; diff --git a/packages/app-admin/src/base/providers/TelemetryProvider.tsx b/packages/app-admin/src/base/providers/TelemetryProvider.tsx index 1c73b441001..e8c227aeff5 100644 --- a/packages/app-admin/src/base/providers/TelemetryProvider.tsx +++ b/packages/app-admin/src/base/providers/TelemetryProvider.tsx @@ -1,8 +1,4 @@ import React, { useEffect } from "react"; -/** - * Package @webiny/telemetry is not a typescript project. - */ -// @ts-ignore import { sendEvent } from "@webiny/telemetry/react"; let eventSent = false; diff --git a/packages/app-admin/src/components/MultiImageUpload.tsx b/packages/app-admin/src/components/MultiImageUpload.tsx index d2a62d3af19..778aaa8d821 100644 --- a/packages/app-admin/src/components/MultiImageUpload.tsx +++ b/packages/app-admin/src/components/MultiImageUpload.tsx @@ -20,9 +20,9 @@ const MultiImageUpload: React.FC = ({ ) => { return ( { + const isMountedRef = useRef(false); + + useEffect(() => { + isMountedRef.current = true; + + return () => { + isMountedRef.current = false; + }; + }, []); + + return { + isMounted: () => isMountedRef.current + }; +}; diff --git a/packages/app-admin/src/hooks/useShiftKey.ts b/packages/app-admin/src/hooks/useShiftKey.ts index 8645d9e8814..1e053cde181 100644 --- a/packages/app-admin/src/hooks/useShiftKey.ts +++ b/packages/app-admin/src/hooks/useShiftKey.ts @@ -18,7 +18,6 @@ export function useShiftKey() { }, []); useEffect(() => { - // @ts-ignore document.onselectstart = () => !pressed; return () => { diff --git a/packages/app-admin/src/plugins/globalSearch/SearchBar.tsx b/packages/app-admin/src/plugins/globalSearch/SearchBar.tsx index a9519b24f5e..bd80783d5fb 100644 --- a/packages/app-admin/src/plugins/globalSearch/SearchBar.tsx +++ b/packages/app-admin/src/plugins/globalSearch/SearchBar.tsx @@ -8,7 +8,7 @@ import classnames from "classnames"; /** * Package react-hotkeyz does not have types. */ -// @ts-ignore +// @ts-expect-error import { Hotkeys } from "react-hotkeyz"; // UI components @@ -169,7 +169,7 @@ class SearchBar extends React.Component { document.activeElement.blur(), "/": this.handleOpenHotkey }} @@ -212,7 +212,6 @@ class SearchBar extends React.Component { ), ref: this.input, value: this.state.searchTerm.current, - // @ts-ignore onClick: openMenu, onBlur: () => { this.cancelSearchTerm(); diff --git a/packages/app-admin/src/plugins/globalSearch/styled.ts b/packages/app-admin/src/plugins/globalSearch/styled.ts index 67ebe9bf8b9..d58e3532d37 100644 --- a/packages/app-admin/src/plugins/globalSearch/styled.ts +++ b/packages/app-admin/src/plugins/globalSearch/styled.ts @@ -1,5 +1,3 @@ -// TODO remove -// @ts-nocheck import { css } from "emotion"; import styled from "@emotion/styled"; @@ -104,6 +102,7 @@ export const searchWrapper = css({ input: { color: "var(--mdc-theme-on-surface)" }, + // @ts-expect-error [SearchShortcut]: { display: "none" } diff --git a/packages/app-admin/src/ui/elements/AccordionElement.tsx b/packages/app-admin/src/ui/elements/AccordionElement.tsx index f72bfc58a1b..7872fe8c349 100644 --- a/packages/app-admin/src/ui/elements/AccordionElement.tsx +++ b/packages/app-admin/src/ui/elements/AccordionElement.tsx @@ -62,7 +62,7 @@ export class AccordionElement extends UIElement { * Figure out correct way to have props.children typed. * TODO @ts-refactor */ - // @ts-ignore + // @ts-expect-error return {super.render(props)}; } } diff --git a/packages/app-admin/src/ui/elements/ButtonElement.tsx b/packages/app-admin/src/ui/elements/ButtonElement.tsx index 9b864ce91c7..71ccf48ba5d 100644 --- a/packages/app-admin/src/ui/elements/ButtonElement.tsx +++ b/packages/app-admin/src/ui/elements/ButtonElement.tsx @@ -35,7 +35,7 @@ export class ButtonElement extends UIElement< * TODO @ts-refactor * 'TProps' could be instantiated with an arbitrary type which could be unrelated to 'TRenderProps' */ - // @ts-ignore + // @ts-expect-error this.config.label = label; } diff --git a/packages/app-admin/src/ui/elements/form/FileManagerElement/styled.ts b/packages/app-admin/src/ui/elements/form/FileManagerElement/styled.ts index 7d133689457..edd9edfef91 100644 --- a/packages/app-admin/src/ui/elements/form/FileManagerElement/styled.ts +++ b/packages/app-admin/src/ui/elements/form/FileManagerElement/styled.ts @@ -1,4 +1,3 @@ -// @ts-nocheck import styled from "@emotion/styled"; export const AddImageIconWrapper = styled("div")({ @@ -71,6 +70,7 @@ export const FilePreviewWrapper = styled("div")({ backgroundColor: "var(--mdc-theme-on-background)", borderRadius: 0, borderBottom: "1px solid var(--mdc-theme-text-hint-on-background)", + // @ts-expect-error [AddImageWrapper]: { position: "absolute", display: "none", @@ -78,6 +78,7 @@ export const FilePreviewWrapper = styled("div")({ height: "100%", zIndex: 1, backgroundColor: "rgba(0,0,0, 0.75)", + // @ts-expect-error [AddImageIconWrapper]: { top: "50%", left: "50%", @@ -87,9 +88,11 @@ export const FilePreviewWrapper = styled("div")({ } }, "&:hover": { + // @ts-expect-error [AddImageWrapper]: { display: "block" }, + // @ts-expect-error [RemoveImage]: { display: "block", zIndex: 2 diff --git a/packages/app-admin/src/ui/elements/form/FormFieldElement.tsx b/packages/app-admin/src/ui/elements/form/FormFieldElement.tsx index d29a121f2a6..9b5dbf3e43c 100644 --- a/packages/app-admin/src/ui/elements/form/FormFieldElement.tsx +++ b/packages/app-admin/src/ui/elements/form/FormFieldElement.tsx @@ -171,7 +171,7 @@ export class FormFieldElement< /** * TODO @ts-refactor possibly different subtype. Or so TS complains. */ - // @ts-ignore + // @ts-expect-error this._afterChange.push(cb); } diff --git a/packages/app-apw/src/components/ContentReviewEditor/ChangeRequest/ApwFile.tsx b/packages/app-apw/src/components/ContentReviewEditor/ChangeRequest/ApwFile.tsx index a4d5561e7e5..437abd37347 100644 --- a/packages/app-apw/src/components/ContentReviewEditor/ChangeRequest/ApwFile.tsx +++ b/packages/app-apw/src/components/ContentReviewEditor/ChangeRequest/ApwFile.tsx @@ -88,6 +88,6 @@ export const CommentFile: React.FC = props => { return ; } - // @ts-ignore + // @ts-expect-error return ; }; diff --git a/packages/app-apw/src/views/publishingWorkflows/components/WorkflowTitle.tsx b/packages/app-apw/src/views/publishingWorkflows/components/WorkflowTitle.tsx index ddab67d42f4..a9ec6812299 100644 --- a/packages/app-apw/src/views/publishingWorkflows/components/WorkflowTitle.tsx +++ b/packages/app-apw/src/views/publishingWorkflows/components/WorkflowTitle.tsx @@ -68,7 +68,7 @@ const Title: React.FunctionComponent<{ value: string; onChange: Function }> = ({ const onKeyDown = useCallback( (e: SyntheticEvent) => { - // @ts-ignore + // @ts-expect-error switch (e.key) { case "Escape": e.preventDefault(); diff --git a/packages/app-file-manager-s3/src/MultiPartUploadStrategy.ts b/packages/app-file-manager-s3/src/MultiPartUploadStrategy.ts index 480c82509e9..5af7738615c 100644 --- a/packages/app-file-manager-s3/src/MultiPartUploadStrategy.ts +++ b/packages/app-file-manager-s3/src/MultiPartUploadStrategy.ts @@ -23,7 +23,7 @@ export class MultiPartUploadStrategy implements FileUploadStrategy { // For dev purposes, we take this global var into consideration. if (process.env.NODE_ENV === "development") { - // @ts-ignore + // @ts-expect-error const windowSize = window["fmUploadChunkSize"]; if (windowSize) { return windowSize; @@ -39,7 +39,7 @@ export class MultiPartUploadStrategy implements FileUploadStrategy { // For dev purposes, we take this global var into consideration. if (process.env.NODE_ENV === "development") { - // @ts-ignore + // @ts-expect-error const windowChunks = window["fmUploadParallelChunks"]; if (windowChunks) { return windowChunks; diff --git a/packages/app-file-manager-s3/src/index.ts b/packages/app-file-manager-s3/src/index.ts index 47bb1c1869c..36bf290c8ac 100644 --- a/packages/app-file-manager-s3/src/index.ts +++ b/packages/app-file-manager-s3/src/index.ts @@ -13,7 +13,7 @@ export default (): FileUploaderPlugin => { upload(file: File, options: UploadOptions) { // Use "simple" strategy for files smaller than ~100MB - // @ts-ignore + // @ts-expect-error const multiPartThreshold = window["fmUploadMultiPartThreshold"] ?? 100; const simple = file.size < multiPartThreshold * 1024 * 1024; diff --git a/packages/app-file-manager/jest.config.js b/packages/app-file-manager/jest.config.js new file mode 100644 index 00000000000..cc5ac2bb64f --- /dev/null +++ b/packages/app-file-manager/jest.config.js @@ -0,0 +1,5 @@ +const base = require("../../jest.config.base"); + +module.exports = { + ...base({ path: __dirname }) +}; diff --git a/packages/app-file-manager/package.json b/packages/app-file-manager/package.json index c732c89968d..f9ff0192f55 100644 --- a/packages/app-file-manager/package.json +++ b/packages/app-file-manager/package.json @@ -52,7 +52,8 @@ "react-custom-scrollbars": "^4.2.1", "react-dom": "17.0.2", "react-hotkeyz": "^1.0.4", - "react-lazy-load": "^3.1.14" + "react-lazy-load": "^3.1.14", + "zod": "^3.22.4" }, "devDependencies": { "@babel/cli": "^7.22.6", diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEdit.styled.tsx b/packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEdit.styled.tsx new file mode 100644 index 00000000000..6ec26dcaf80 --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEdit.styled.tsx @@ -0,0 +1,36 @@ +import styled from "@emotion/styled"; +import { ReactComponent as AddIcon } from "@material-design-icons/svg/round/add.svg"; +import { Dialog } from "@webiny/ui/Dialog"; + +export const ActionEditFormContainer = styled.div` + margin: -24px !important; +`; + +export const DialogContainer = styled(Dialog)` + z-index: 22; + + .mdc-dialog__surface { + width: 800px; + min-width: 800px; + } +`; + +export const BatchEditorContainer = styled.div` + padding: 24px; +`; + +export const AddOperationInner = styled.div` + padding: 24px 0 0; + text-align: center; +`; + +interface ButtonIconProps { + disabled?: boolean; +} + +export const ButtonIcon = styled(AddIcon)` + fill: ${props => + props.disabled ? "var(--mdc-theme-text-hint-on-light)" : "var(--mdc-theme-primary)"}; + width: 18px; + margin-right: 8px; +`; diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEdit.tsx b/packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEdit.tsx new file mode 100644 index 00000000000..9b43f2a58ce --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEdit.tsx @@ -0,0 +1,134 @@ +import React, { useCallback, useEffect, useMemo } from "react"; +import { ReactComponent as EditIcon } from "@material-design-icons/svg/outlined/edit.svg"; +import { prepareFormData } from "@webiny/app-headless-cms-common"; +import { observer } from "mobx-react-lite"; +import omit from "lodash/omit"; + +import { FileManagerViewConfig } from "~/modules/FileManagerRenderer/FileManagerView/FileManagerViewConfig"; +import { useFileManagerApi } from "~/modules/FileManagerApiProvider/FileManagerApiContext"; +import { useFileManagerView } from "~/modules/FileManagerRenderer/FileManagerViewProvider"; + +import { useFileModel } from "~/hooks/useFileModel"; +import { getFilesLabel } from "~/components/BulkActions"; +import { GraphQLInputMapper } from "~/components/BulkActions/ActionEdit/GraphQLInputMapper"; +import { BatchDTO } from "~/components/BulkActions/ActionEdit/domain"; + +import { BatchEditorDialog } from "./BatchEditorDialog"; +import { ActionEditPresenter } from "./ActionEditPresenter"; + +export const ActionEdit = observer(() => { + const { fields: defaultFields } = useFileModel(); + const { + useWorker, + useButtons, + useDialog: useBulkActionDialog + } = FileManagerViewConfig.Browser.BulkAction; + const worker = useWorker(); + const { updateFile } = useFileManagerView(); + const { canEdit } = useFileManagerApi(); + const { IconButton } = useButtons(); + const { showConfirmationDialog, showResultsDialog } = useBulkActionDialog(); + + const presenter = useMemo(() => { + return new ActionEditPresenter(); + }, []); + + useEffect(() => { + presenter.load(defaultFields); + }, [defaultFields]); + + const filesLabel = useMemo(() => { + return getFilesLabel(worker.items.length); + }, [worker.items.length]); + + const canEditAll = useMemo(() => { + return worker.items.every(item => canEdit(item)); + }, [worker.items]); + + const openWorkerDialog = (batch: BatchDTO) => { + showConfirmationDialog({ + title: "Edit files", + message: `You are about to edit ${filesLabel}. Are you sure you want to continue?`, + loadingLabel: `Processing ${filesLabel}`, + execute: async () => { + await worker.processInSeries(async ({ item, report }) => { + try { + const extensions = defaultFields.find( + field => field.fieldId === "extensions" + ); + + const extensionsData = GraphQLInputMapper.toGraphQLExtensions( + item.extensions, + batch + ); + + const output = omit(item, ["id", "createdBy", "createdOn", "src"]); + + const fileData = { + ...output, + extensions: prepareFormData( + extensionsData, + extensions?.settings?.fields || [] + ) + }; + + await updateFile(item.id, fileData); + + report.success({ + title: `${item.name}`, + message: "File successfully edited." + }); + } catch (e) { + report.error({ + title: `${item.name}`, + message: e.message + }); + } + }); + + worker.resetItems(); + + showResultsDialog({ + results: worker.results, + title: "Edit files", + message: "Finished editing files! See full report below:" + }); + } + }); + }; + + const onBatchEditorSubmit = useCallback( + (batch: BatchDTO) => { + presenter.closeEditor(); + openWorkerDialog(batch); + }, + [openWorkerDialog] + ); + + if (!presenter.vm.show) { + return null; + } + + if (!canEditAll) { + console.log("You don't have permissions to edit files."); + return null; + } + + return ( + <> + } + onAction={() => presenter.openEditor()} + label={`Edit ${filesLabel}`} + tooltipPlacement={"bottom"} + /> + presenter.closeEditor()} + fields={presenter.vm.fields} + batch={presenter.vm.currentBatch} + vm={presenter.vm.editorVm} + onApply={onBatchEditorSubmit} + /> + + ); +}); diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEdit.types.ts b/packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEdit.types.ts new file mode 100644 index 00000000000..ad9ca9009b2 --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEdit.types.ts @@ -0,0 +1,3 @@ +import { FileItem } from "@webiny/app-admin/types"; + +export type ActionFormData = Partial>; diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEditPresenter.test.ts b/packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEditPresenter.test.ts new file mode 100644 index 00000000000..0ef21f74925 --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEditPresenter.test.ts @@ -0,0 +1,187 @@ +import { ActionEditPresenter } from "./ActionEditPresenter"; +import { FieldRaw } from "~/components/BulkActions/ActionEdit/domain"; + +describe("ActionEditPresenter", () => { + const extensionFields = [ + { + id: "field1", + fieldId: "field1", + label: "Field 1", + type: "text", + renderer: { + name: "text-input" + }, + tags: ["$bulk-edit"], + storageId: "text@field1" + }, + { + id: "field2", + fieldId: "field2", + label: "Field 2", + type: "ref", + renderer: { + name: "ref-inputs" + }, + multipleValues: true, + settings: { + models: [ + { + modelId: "any-model" + } + ] + }, + tags: ["$bulk-edit"], + storageId: "ref@field2" + }, + { + id: "field3", + fieldId: "field3", + label: "Field 3", + type: "number", + renderer: { + name: "number-input" + }, + tags: [], + storageId: "number@field3" + } + ]; + + const createFields = (extensionFields: FieldRaw[]) => { + return [ + { + id: "name", + storageId: "text@name", + fieldId: "name", + label: "Name", + type: "text", + settings: {}, + listValidation: [], + validation: [ + { + name: "required", + message: "Value is required." + } + ], + multipleValues: false, + predefinedValues: { + values: [], + enabled: false + }, + renderer: { + name: "text-input" + } + }, + { + id: "extensions", + storageId: "object@extensions", + fieldId: "extensions", + label: "Extensions", + type: "object", + settings: { + layout: extensionFields.map(field => [field.id]), + fields: extensionFields + }, + listValidation: [], + validation: [], + multipleValues: false, + predefinedValues: { + values: [], + enabled: false + }, + renderer: { + name: "any" + } + } + ]; + }; + + let presenter: ActionEditPresenter; + + beforeEach(() => { + jest.clearAllMocks(); + presenter = new ActionEditPresenter(); + }); + + it("should create a presenter and load extensions fields", () => { + presenter.load(createFields(extensionFields)); + + // I should see the bulk edit action + expect(presenter.vm.show).toEqual(true); + + // I should receive a `currentBatch` + expect(presenter.vm.currentBatch).toEqual({ + operations: [ + { + field: "", + operator: "", + value: {} + } + ] + }); + + // I should receive the `fields` available for bulk edit + expect(presenter.vm.fields).toEqual([ + { + label: "Field 1", + value: "field1", + operators: [ + { + label: "Override existing values", + value: "OVERRIDE" + }, + { + label: "Clear all existing values", + value: "REMOVE" + } + ], + raw: extensionFields[0] + }, + { + label: "Field 2", + value: "field2", + operators: [ + { + label: "Override existing values", + value: "OVERRIDE" + }, + { + label: "Clear all existing values", + value: "REMOVE" + }, + { + label: "Append to existing values", + value: "APPEND" + } + ], + raw: extensionFields[1] + } + ]); + + // I should receive the editor.vm + expect(presenter.vm.editorVm).toEqual({ + isOpen: false + }); + }); + + it("should not show the bulk action if no `extensions` fields are defined", () => { + presenter.load(createFields([])); + + // The editor action should not be rendered + expect(presenter.vm.show).toBe(false); + }); + + it("should open / close the editor", () => { + presenter.load(createFields(extensionFields)); + + // The editor should be closed by default + expect(presenter.vm.editorVm.isOpen).toBe(false); + + // Let's open the editor + presenter.openEditor(); + expect(presenter.vm.editorVm.isOpen).toBe(true); + + // Let's open the editor + presenter.closeEditor(); + expect(presenter.vm.editorVm.isOpen).toBe(false); + }); +}); diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEditPresenter.ts b/packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEditPresenter.ts new file mode 100644 index 00000000000..0d485e4a282 --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/ActionEditPresenter.ts @@ -0,0 +1,79 @@ +import { makeAutoObservable } from "mobx"; + +import { + Batch, + BatchDTO, + BatchMapper, + Field, + FieldDTO, + FieldMapper, + FieldRaw +} from "~/components/BulkActions/ActionEdit/domain"; + +interface IActionEditPresenter { + load: (fields: FieldRaw[]) => void; + openEditor: () => void; + closeEditor: () => void; + get vm(): { + show: boolean; + currentBatch: BatchDTO; + fields: FieldDTO[]; + editorVm: { + isOpen: boolean; + }; + }; +} + +export class ActionEditPresenter implements IActionEditPresenter { + private showEditor = false; + private readonly currentBatch: BatchDTO; + private extensionFields: FieldDTO[]; + + constructor() { + this.extensionFields = []; + this.currentBatch = BatchMapper.toDTO(Batch.createEmpty()); + makeAutoObservable(this); + } + + load(fields: FieldRaw[]) { + this.extensionFields = this.getExtensionFields(fields); + } + + private getExtensionFields(fields: FieldRaw[]) { + const extensions = fields.find(field => field.fieldId === "extensions"); + + if (!extensions?.settings?.fields) { + return []; + } + + const extensionFields = + extensions.settings.fields.filter( + field => field.tags && field.tags.includes("$bulk-edit") + ) || []; + + return FieldMapper.toDTO(extensionFields.map(field => Field.createFromRaw(field))); + } + + private get editorVm() { + return { + isOpen: this.showEditor + }; + } + + get vm() { + return { + show: this.extensionFields.length > 0, + currentBatch: this.currentBatch, + fields: this.extensionFields, + editorVm: this.editorVm + }; + } + + openEditor() { + this.showEditor = true; + } + + closeEditor() { + this.showEditor = false; + } +} diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/AddOperation.tsx b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/AddOperation.tsx new file mode 100644 index 00000000000..c6c65ec42f3 --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/AddOperation.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +import { ButtonDefault } from "@webiny/ui/Button"; + +import { + AddOperationInner, + ButtonIcon +} from "~/components/BulkActions/ActionEdit/ActionEdit.styled"; + +interface AddOperationProps { + disabled: boolean; + onClick: () => void; +} + +export const AddOperation = ({ disabled, onClick }: AddOperationProps) => { + return ( + + + {"Add new operation"} + + + ); +}; diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditor.tsx b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditor.tsx new file mode 100644 index 00000000000..23a22f0bee3 --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditor.tsx @@ -0,0 +1,80 @@ +import React, { useEffect } from "react"; + +import { observer } from "mobx-react-lite"; +import { ReactComponent as DeleteIcon } from "@material-design-icons/svg/outlined/delete_outline.svg"; +import { Form, FormAPI, FormOnSubmit } from "@webiny/form"; +import { Accordion, AccordionItem } from "@webiny/ui/Accordion"; + +import { AddOperation } from "~/components/BulkActions/ActionEdit/BatchEditorDialog/AddOperation"; +import { Operation } from "~/components/BulkActions/ActionEdit/BatchEditorDialog/Operation"; +import { + BatchEditorDialogViewModel, + BatchEditorFormData +} from "~/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditorDialogPresenter"; +import { BatchEditorContainer } from "~/components/BulkActions/ActionEdit/ActionEdit.styled"; + +export interface BatchEditorProps { + onForm: (form: FormAPI) => void; + onAdd: () => void; + onDelete: (operationIndex: number) => void; + onChange: (data: BatchEditorFormData) => void; + onSetOperationFieldData: (operationIndex: number, data: string) => void; + onSubmit: FormOnSubmit; + vm: BatchEditorDialogViewModel; +} + +export const BatchEditor = observer((props: BatchEditorProps) => { + const formRef = React.createRef(); + + useEffect(() => { + if (formRef.current) { + props.onForm(formRef.current); + } + }, []); + + return ( +
+ {() => ( + + + {props.vm.data.operations.map((operation, operationIndex) => ( + + } + onClick={() => props.onDelete(operationIndex)} + disabled={!operation.canDelete} + /> + + } + > + props.onDelete(operationIndex)} + onSetOperationFieldData={data => + props.onSetOperationFieldData(operationIndex, data) + } + /> + + ))} + + props.onAdd()} + /> + + )} +
+ ); +}); diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditorDialog.tsx b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditorDialog.tsx new file mode 100644 index 00000000000..143b3722785 --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditorDialog.tsx @@ -0,0 +1,74 @@ +import React, { useMemo, useEffect, useRef } from "react"; + +import { observer } from "mobx-react-lite"; +import { FormAPI } from "@webiny/form"; +import { ButtonPrimary } from "@webiny/ui/Button"; +import { DialogActions, DialogCancel, DialogContent, DialogTitle } from "@webiny/ui/Dialog"; + +import { BatchEditorDialogPresenter, BatchEditorFormData } from "./BatchEditorDialogPresenter"; +import { BatchEditor } from "~/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditor"; +import { ActionEditFormContainer, DialogContainer } from "../ActionEdit.styled"; +import { BatchDTO, FieldDTO } from "~/components/BulkActions/ActionEdit/domain"; + +interface BatchEditorDialogProps { + fields: FieldDTO[]; + batch: BatchDTO; + vm: { + isOpen: boolean; + }; + onApply: (batch: BatchDTO) => void; + onClose: () => void; +} + +export const BatchEditorDialog = observer((props: BatchEditorDialogProps) => { + const presenter = useMemo(() => { + return new BatchEditorDialogPresenter(); + }, []); + + useEffect(() => { + presenter.load(props.batch, props.fields); + }, [props.batch, props.fields]); + + const onChange = (data: BatchEditorFormData) => { + presenter.setBatch(data); + }; + + const onApply = () => { + presenter.onApply(batch => { + props.onApply(batch); + }); + }; + + const ref = useRef(null); + + return ( + + {props.vm.isOpen ? ( + <> + {"Edit items"} + + + (ref.current = form)} + onChange={data => onChange(data)} + onSubmit={onApply} + onDelete={operationIndex => + presenter.deleteOperation(operationIndex) + } + onAdd={() => presenter.addOperation()} + onSetOperationFieldData={(operationIndex, data) => + presenter.setOperationFieldData(operationIndex, data) + } + vm={presenter.vm} + /> + + + + {"Cancel"} + {"Submit"} + + + ) : null} + + ); +}); diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditorDialogPresenter.test.ts b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditorDialogPresenter.test.ts new file mode 100644 index 00000000000..7abdf2ce430 --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditorDialogPresenter.test.ts @@ -0,0 +1,400 @@ +import { BatchEditorDialogPresenter } from "./BatchEditorDialogPresenter"; +import { + BatchDTO, + FieldDTO, + OperatorDTO, + OperatorType +} from "~/components/BulkActions/ActionEdit/domain"; + +describe("BatchEditorDialogPresenter", () => { + const batch: BatchDTO = { + operations: [ + { + field: "", + operator: "", + value: {} + } + ] + }; + + const fields: FieldDTO[] = [ + { + label: "Field 1", + value: "field1", + operators: [ + { + label: "Override existing values", + value: OperatorType.OVERRIDE + }, + { + label: "Clear all existing values", + value: OperatorType.REMOVE + } + ], + raw: { + id: "field1", + fieldId: "field1", + label: "Field 1", + type: "text", + renderer: { + name: "text-input" + }, + tags: ["$bulk-edit"], + storageId: "text@field1" + } + }, + { + label: "Field 2", + value: "field2", + operators: [ + { + label: "Override existing values", + value: OperatorType.OVERRIDE + }, + { + label: "Clear all existing values", + value: OperatorType.REMOVE + } + ], + raw: { + id: "field2", + fieldId: "field2", + label: "Field 1", + type: "text", + renderer: { + name: "text-input" + }, + tags: ["$bulk-edit"], + storageId: "text@field2" + } + } + ]; + + const operators: OperatorDTO[] = [ + { + value: OperatorType.OVERRIDE, + label: "Override existing values" + }, + { + value: OperatorType.REMOVE, + label: "Clear all existing values" + }, + { + value: OperatorType.APPEND, + label: "Append to existing values" + } + ]; + + let presenter: BatchEditorDialogPresenter; + + beforeEach(() => { + jest.clearAllMocks(); + presenter = new BatchEditorDialogPresenter(); + }); + + it("should load data", () => { + presenter.load(batch, fields); + + // `vm` should have the expected `data` definition + expect(presenter.vm.data).toEqual({ + operations: [ + { + title: "Operation #1", + open: true, + canDelete: false, + field: "", + operator: "", + value: {}, + fieldOptions: fields, + operatorOptions: [], + selectedField: undefined + } + ] + }); + + // `vm` should have the expected `invalidFields` definition + expect(presenter.vm.invalidFields).toEqual({}); + + // `vm` should have the expected `canAddOperation` definition + expect(presenter.vm.canAddOperation).toEqual(true); + }); + + it("should be able to add and delete operations", () => { + presenter.load(batch, fields); + + // should only have 1 operator, created by default + expect(presenter.vm.data.operations.length).toBe(1); + expect(presenter.vm.data.operations).toEqual([ + { + title: "Operation #1", + open: true, + field: "", + operator: "", + value: {}, + canDelete: false, + fieldOptions: fields, + operatorOptions: [], + selectedField: undefined + } + ]); + + presenter.addOperation(); + + // should only have 2 operators + expect(presenter.vm.data.operations.length).toBe(2); + expect(presenter.vm.data.operations).toEqual([ + { + title: "Operation #1", + open: true, + field: "", + operator: "", + value: {}, + canDelete: false, + fieldOptions: fields, + operatorOptions: [], + selectedField: undefined + }, + { + title: "Operation #2", + open: true, + field: "", + operator: "", + value: {}, + canDelete: true, + fieldOptions: fields, + operatorOptions: [], + selectedField: undefined + } + ]); + + // let's delete the first operation + presenter.deleteOperation(0); + + expect(presenter.vm.data.operations.length).toBe(1); + expect(presenter.vm.data.operations).toEqual([ + { + title: "Operation #1", + open: true, + field: "", + operator: "", + value: {}, + canDelete: false, + fieldOptions: fields, + operatorOptions: [], + selectedField: undefined + } + ]); + + // let's delete the remaining operation + presenter.deleteOperation(0); + + // should still have 1 default operation + expect(presenter.vm.data.operations.length).toBe(1); + expect(presenter.vm.data.operations).toEqual([ + { + title: "Operation #1", + open: true, + field: "", + operator: "", + value: {}, + canDelete: false, + fieldOptions: fields, + operatorOptions: [], + selectedField: undefined + } + ]); + }); + + it("should be able to handle the `fieldOptions` based operations set in the batch", () => { + presenter.load(batch, fields); + + // let's set some `data` back to the operation + presenter.setBatch({ + operations: [ + { + title: `Override existing values for field "${fields[0].label}"`, + open: true, + field: fields[0].value, + operator: OperatorType.OVERRIDE, + value: { + [fields[0].value]: "newValue" + }, + canDelete: false, + fieldOptions: fields, + operatorOptions: [operators[0], operators[1]], + selectedField: fields[0] + } + ] + }); + + // I should be able to add a new operation + expect(presenter.vm.canAddOperation).toBeTruthy(); + + presenter.addOperation(); + + // Only the not-set field should be available + expect(presenter.vm.data.operations[1]).toEqual({ + title: "Operation #2", + open: true, + field: "", + operator: "", + value: {}, + canDelete: true, + fieldOptions: [fields[1]], + operatorOptions: [], + selectedField: undefined + }); + + // let's set some `data` back to the operations + presenter.setBatch({ + operations: [ + { + title: `Override existing values for field "${fields[0].label}"`, + open: true, + field: fields[0].value, + operator: OperatorType.OVERRIDE, + value: { + [fields[0].value]: "newValue" + }, + canDelete: false, + fieldOptions: fields, + operatorOptions: [operators[0], operators[1]], + selectedField: fields[0] + }, + { + title: `Remove all values for field "${fields[1].label}"`, + open: false, + field: fields[1].value, + operator: OperatorType.REMOVE, + value: {}, + canDelete: true, + fieldOptions: [fields[1]], + operatorOptions: [operators[0], operators[1]], + selectedField: fields[1] + } + ] + }); + + // I should NOT be able to add a new operation + expect(presenter.vm.canAddOperation).toBeFalsy(); + }); + + it("should be able to set data back to the operation", () => { + presenter.load(batch, fields); + + // should be able to set the `data` operation + presenter.setBatch({ + operations: [ + { + title: `Remove all values for field "${fields[0].label}"`, + open: false, + field: fields[0].value, + operator: OperatorType.OVERRIDE, + value: { + [fields[0].value]: "newValue" + }, + canDelete: false, + fieldOptions: fields, + operatorOptions: [operators[0], operators[1]], + selectedField: fields[0] + } + ] + }); + + expect(presenter.vm.data.operations[0].field).toEqual(fields[0].value); + expect(presenter.vm.data.operations[0].operator).toEqual(OperatorType.OVERRIDE); + expect(presenter.vm.data.operations[0].value).toEqual({ + [fields[0].value]: "newValue" + }); + expect(presenter.vm.data.operations[0].fieldOptions).toEqual(fields); + }); + + it("should able to set the operation `field` data", () => { + presenter.load(batch, fields); + + presenter.setBatch({ + operations: [ + { + title: `Override existing values for field "${fields[0].label}"`, + open: true, + field: fields[0].value, + operator: OperatorType.OVERRIDE, + value: { + [fields[0].value]: "newValue" + }, + canDelete: false, + fieldOptions: fields, + operatorOptions: [operators[0], operators[1]], + selectedField: fields[0] + } + ] + }); + + // let's empty the operation + presenter.setOperationFieldData(0, "new-field"); + + // should have an operation with default definition and new field value + expect(presenter.vm.data.operations[0]).toEqual({ + title: "Operation #1", + open: true, + field: "new-field", + operator: "", + value: {}, + canDelete: false, + fieldOptions: fields, + operatorOptions: [], + selectedField: undefined + }); + }); + + it("should perform validation and call provided callbacks `onApply`", () => { + presenter.load(batch, fields); + + const onSuccess = jest.fn(); + const onError = jest.fn(); + + presenter.setBatch({ + operations: [ + { + title: `Override existing values for field "${fields[0].label}"`, + open: true, + field: fields[0].value, + operator: "", // empty value -> this should trigger the error + value: {}, + canDelete: false, + fieldOptions: fields, + operatorOptions: [operators[0], operators[1]], + selectedField: fields[0] + } + ] + }); + + presenter.onApply(onSuccess, onError); + + expect(onError).toBeCalledTimes(1); + expect(Object.keys(presenter.vm.invalidFields).length).toBe(1); + + presenter.setBatch({ + operations: [ + { + title: `Override existing values for field "${fields[0].label}"`, + open: true, + field: fields[0].value, + operator: OperatorType.OVERRIDE, + value: { + [fields[0].value]: "newValue" + }, + canDelete: false, + fieldOptions: fields, + operatorOptions: [operators[0], operators[1]], + selectedField: fields[0] + } + ] + }); + + presenter.onApply(onSuccess, onError); + + expect(onSuccess).toBeCalledTimes(1); + expect(presenter.vm.invalidFields).toEqual({}); + }); +}); diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditorDialogPresenter.tsx b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditorDialogPresenter.tsx new file mode 100644 index 00000000000..f1b2abb5765 --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditorDialogPresenter.tsx @@ -0,0 +1,224 @@ +import { makeAutoObservable } from "mobx"; + +import { + Batch, + BatchDTO, + FieldDTO, + OperationDTO, + OperatorDTO +} from "~/components/BulkActions/ActionEdit/domain"; + +export interface IBatchEditorDialogPresenter { + load(batch: BatchDTO, fields: FieldDTO[]): void; + addOperation(): void; + deleteOperation(operationIndex: number): void; + setOperationFieldData(operationIndex: number, data: string): void; + setBatch(data: any): void; + onApply(onSuccess?: (batch: BatchDTO) => void, onError?: (batch: BatchDTO) => void): void; + get vm(): BatchEditorDialogViewModel; +} + +export interface BatchEditorDialogViewModel { + invalidFields: Record; + canAddOperation: boolean; + data: BatchEditorFormData; +} + +export interface BatchEditorFormData { + operations: OperationFormData[]; +} + +export type OperationFormData = OperationDTO & { + canDelete: boolean; + title: string; + open: boolean; + fieldOptions: FieldDTO[]; + operatorOptions: OperatorDTO[]; + selectedField?: FieldDTO; +}; + +export class BatchEditorDialogPresenter implements IBatchEditorDialogPresenter { + private batch: BatchDTO | undefined; + private fields: FieldDTO[]; + private invalidFields: BatchEditorDialogViewModel["invalidFields"] = {}; + private formWasSubmitted = false; + + constructor() { + this.batch = undefined; + this.fields = []; + makeAutoObservable(this); + } + + load(batch: BatchDTO, fields: FieldDTO[]) { + this.batch = batch; + this.fields = fields; + } + + get vm() { + const operations = this.getOperations(); + const canAddOperation = operations[operations.length - 1].fieldOptions.length > 1 ?? false; + + return { + invalidFields: this.invalidFields, + canAddOperation, + data: { + operations + } + }; + } + + private getOperations = () => { + return ( + this.batch?.operations.map((operation: OperationDTO, operationIndex) => { + const fieldOptions = this.getFieldOptions(operation.field); + const selectedField = fieldOptions.find(field => field.value === operation.field); + const operatorOptions = selectedField?.operators || []; + + return { + title: + this.getOperationTitle(operation.field, operation.operator) ?? + `Operation #${operationIndex + 1}`, + open: true, + field: operation.field, + operator: operation.operator, + value: operation.value, + canDelete: operationIndex !== 0, + fieldOptions, + selectedField, + operatorOptions + }; + }) || [] + ); + }; + + private getOperationTitle(inputField?: string, inputOperation?: string) { + if (!inputField || !inputOperation) { + return undefined; + } + + const field = this.fields.find(field => field.value === inputField); + + if (!field) { + return undefined; + } + + const operator = field.operators.find(operator => operator.value === inputOperation); + + if (!operator) { + return undefined; + } + + return `${operator.label} for field "${field.label}"`; + } + + private getFieldOptions(currentFieldId = "") { + if (!this.batch) { + return []; + } + + const existings = this.batch.operations + .filter(operation => operation.field !== currentFieldId) + .map(operation => operation.field); + + return this.fields.filter(field => !existings.includes(field.value)); + } + + addOperation(): void { + if (!this.batch) { + return; + } + + this.batch.operations.push({ + field: "", + operator: "", + value: {} + }); + } + + deleteOperation(operationIndex: number): void { + if (!this.batch) { + return; + } + + this.batch.operations = this.batch.operations.filter( + (_, index) => index !== operationIndex + ); + + // Make sure we always have at least 1 operation! + if (this.batch.operations.length === 0) { + this.addOperation(); + } + } + + setOperationFieldData(batchIndex: number, data: string) { + if (!this.batch) { + return; + } + + this.batch.operations = [ + ...this.batch.operations.slice(0, batchIndex), + { + field: data, + operator: "", + value: {} + }, + ...this.batch.operations.slice(batchIndex + 1) + ]; + } + + setBatch(data: BatchEditorFormData): void { + if (!this.batch) { + return; + } + + this.batch = { + ...this.batch, + operations: data.operations.map(operation => ({ + field: operation.field, + operator: operation.operator, + value: operation.value + })) + }; + + if (this.formWasSubmitted) { + this.validateBatch(this.batch); + } + } + + onApply( + onSuccess?: (batch: BatchDTO) => void, + onError?: ( + batch: BatchDTO, + invalidFields: BatchEditorDialogViewModel["invalidFields"] + ) => void + ) { + if (!this.batch) { + return; + } + + const result = this.validateBatch(this.batch); + if (result.success) { + onSuccess && onSuccess(this.batch); + } else { + onError && onError(this.batch, this.invalidFields); + } + } + + private validateBatch(data: BatchDTO) { + this.formWasSubmitted = true; + const validation = Batch.validate(data); + + if (!validation.success) { + this.invalidFields = validation.error.issues.reduce((acc, issue) => { + return { + ...acc, + [issue.path.join(".")]: issue.message + }; + }, {}); + } else { + this.invalidFields = {}; + } + + return validation; + } +} diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/FieldRenderer.tsx b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/FieldRenderer.tsx new file mode 100644 index 00000000000..473fb2c3341 --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/FieldRenderer.tsx @@ -0,0 +1,49 @@ +import React from "react"; + +import { RenderFieldElement, ModelProvider } from "@webiny/app-headless-cms"; +import { Bind, Form, useBind } from "@webiny/form"; + +import { FieldDTO, OperatorType } from "~/components/BulkActions/ActionEdit/domain"; + +import { useFileModel } from "~/hooks/useFileModel"; +import { Cell } from "@webiny/ui/Grid"; + +export interface FieldRendererProps { + name: string; + operator: string; + field?: FieldDTO; +} + +export const FieldRenderer = (props: FieldRendererProps) => { + const fileModel = useFileModel(); + + const { onChange } = useBind({ + name: props.name + }); + + if (!props.field) { + return null; + } + + if (!props.operator || props.operator === OperatorType.REMOVE) { + return null; + } + + return ( + + +
+ {() => { + return ( + + ); + }} + +
+
+ ); +}; diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/Operation.tsx b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/Operation.tsx new file mode 100644 index 00000000000..5eb64235829 --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/Operation.tsx @@ -0,0 +1,56 @@ +import React from "react"; + +import { observer } from "mobx-react-lite"; +import { Bind } from "@webiny/form"; +import { Cell, Grid } from "@webiny/ui/Grid"; +import { Select } from "@webiny/ui/Select"; + +import { FieldRenderer } from "~/components/BulkActions/ActionEdit/BatchEditorDialog/FieldRenderer"; +import { OperationFormData } from "~/components/BulkActions/ActionEdit/BatchEditorDialog/BatchEditorDialogPresenter"; + +export interface OperationProps { + operation: OperationFormData; + name: string; + onDelete: () => void; + onSetOperationFieldData: (data: string) => void; +} + +export const Operation = observer((props: OperationProps) => { + return ( + + + + {({ value, validation }) => ( + + )} + + )} + + + + ); +}); diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/RemoveOperation.tsx b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/RemoveOperation.tsx new file mode 100644 index 00000000000..6af71a20fdd --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/RemoveOperation.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +import { ReactComponent as DeleteIcon } from "@material-design-icons/svg/outlined/delete.svg"; +import { IconButton } from "@webiny/ui/Button"; +import { Tooltip } from "@webiny/ui/Tooltip"; + +interface RemoveOperationProps { + onClick: () => void; + disabled: boolean; +} + +export const RemoveOperation = ({ onClick, disabled }: RemoveOperationProps) => { + return ( + + } + onClick={onClick} + disabled={disabled} + /> + + ); +}; diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/index.tsx b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/index.tsx new file mode 100644 index 00000000000..bc92ccef35a --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/index.tsx @@ -0,0 +1 @@ +export * from "./BatchEditorDialog"; diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/GraphQLInputMapper.test.ts b/packages/app-file-manager/src/components/BulkActions/ActionEdit/GraphQLInputMapper.test.ts new file mode 100644 index 00000000000..8c355c9280f --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/GraphQLInputMapper.test.ts @@ -0,0 +1,70 @@ +import { GraphQLInputMapper } from "./GraphQLInputMapper"; +import { BatchDTO, OperatorType } from "~/components/BulkActions/ActionEdit/domain"; +import { FileItem } from "@webiny/app-admin/types"; + +describe("GraphQLInputMapper", () => { + it("should return a GraphQL formatted output based on the received BatchDTO and previous data", () => { + const data: FileItem["extensions"] = { + field1: "old-field1", + field2: "old-field2", + field3: ["old-field3"] + }; + + const batch: BatchDTO = { + operations: [ + { + field: "field1", + operator: OperatorType.OVERRIDE, + value: { + field1: "new-field1" + } + }, + { + field: "field2", + operator: OperatorType.REMOVE + }, + { + field: "field3", + operator: OperatorType.APPEND, + value: { + field3: ["new-field3-1", "new-field3-2"] + } + } + ] + }; + + const output = GraphQLInputMapper.toGraphQLExtensions(data, batch); + + expect(output).toEqual({ + field1: "new-field1", + field2: null, + field3: ["old-field3", "new-field3-1", "new-field3-2"] + }); + }); + + it("should not override data for fields not defined in the batch", () => { + const data: FileItem["extensions"] = { + field1: "old-field1", + field2: "old-field2" + }; + + const batch: BatchDTO = { + operations: [ + { + field: "field1", + operator: OperatorType.OVERRIDE, + value: { + field1: "new-field1" + } + } + ] + }; + + const output = GraphQLInputMapper.toGraphQLExtensions(data, batch); + + expect(output).toEqual({ + field1: "new-field1", + field2: "old-field2" + }); + }); +}); diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/GraphQLInputMapper.ts b/packages/app-file-manager/src/components/BulkActions/ActionEdit/GraphQLInputMapper.ts new file mode 100644 index 00000000000..c0066e6987b --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/GraphQLInputMapper.ts @@ -0,0 +1,42 @@ +import { FileItem } from "@webiny/app-admin/types"; +import { BatchDTO, OperatorType } from "~/components/BulkActions/ActionEdit/domain"; + +export class GraphQLInputMapper { + static toGraphQLExtensions(data: FileItem["extensions"], batch: BatchDTO) { + const update = { ...data }; + + batch.operations.forEach(operation => { + const { field, operator, value } = operation; + + switch (operator) { + case OperatorType.OVERRIDE: + if (!value || !value[field]) { + return; + } + + update[field] = value[field]; + break; + case OperatorType.REMOVE: + update[field] = null; + break; + case OperatorType.APPEND: + if (!value || !value[field] || !Array.isArray(value[field])) { + return; + } + + if (data && data[field]) { + update[field] = [...data[field], ...value[field]]; + } + + break; + default: + break; + } + }); + + return { + ...data, + ...update + }; + } +} diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/Batch.ts b/packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/Batch.ts new file mode 100644 index 00000000000..731e4f96271 --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/Batch.ts @@ -0,0 +1,48 @@ +import zod from "zod"; + +export interface BatchDTO { + operations: OperationDTO[]; +} + +const operationValidationSchema = zod.object({ + field: zod.string().trim().min(1, "Field is required."), + operator: zod.string().min(1, "Operator is required.") +}); + +export const batchValidationSchema = zod.object({ + operations: zod.array(operationValidationSchema).min(1) +}); + +export class Batch { + operations: Operation[]; + + static createEmpty() { + return new Batch([new Operation()]); + } + + static validate(data: BatchDTO) { + return batchValidationSchema.safeParse(data); + } + + protected constructor(operations: Operation[]) { + this.operations = operations; + } +} + +export interface OperationDTO { + field: string; + operator: string; + value?: Record; +} + +export class Operation { + public readonly field?: string; + public readonly operator?: string; + public readonly value?: Record = undefined; + + constructor(field?: string, operator?: string, value?: any) { + this.field = field; + this.operator = operator; + this.value = value; + } +} diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/BatchMapper.ts b/packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/BatchMapper.ts new file mode 100644 index 00000000000..f6626e36bf6 --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/BatchMapper.ts @@ -0,0 +1,13 @@ +import { Batch, BatchDTO } from "./Batch"; + +export class BatchMapper { + static toDTO(input: Batch): BatchDTO { + return { + operations: input.operations.map(operation => ({ + operator: operation.operator || "", + field: operation.field || "", + value: operation.value || {} + })) + }; + } +} diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/Field.ts b/packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/Field.ts new file mode 100644 index 00000000000..6ec2deeac87 --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/Field.ts @@ -0,0 +1,78 @@ +import { CmsModelField } from "@webiny/app-headless-cms-common/types/model"; + +export type FieldRaw = CmsModelField; + +export enum OperatorType { + OVERRIDE = "OVERRIDE", + APPEND = "APPEND", + REMOVE = "REMOVE" +} + +export interface FieldDTO { + label: string; + value: string; + operators: OperatorDTO[]; + raw: FieldRaw; +} + +export class Field { + public readonly label: string; + public readonly value: string; + public readonly operators: Operator[]; + public readonly raw: FieldRaw; + + static createFromRaw(field: FieldRaw) { + const label = field.label; + const value = field.id; + const operators = Operator.createFromField(field); + return new Field(label, value, operators, field); + } + + private constructor(label: string, value: string, operators: Operator[], renderer: FieldRaw) { + this.label = label; + this.value = value; + this.operators = operators; + this.raw = renderer; + } +} + +export interface OperatorDTO { + label: string; + value: string; +} + +export class Operator { + public readonly label: string; + public readonly value: string; + + static createFrom(rawData: OperatorDTO) { + return new Operator(rawData.label, rawData.value); + } + + static createFromField(field: CmsModelField) { + const operators = [ + { + label: "Override existing values", + value: OperatorType.OVERRIDE + }, + { + label: "Clear all existing values", + value: OperatorType.REMOVE + } + ]; + + if (field.multipleValues) { + operators.push({ + label: "Append to existing values", + value: OperatorType.APPEND + }); + } + + return operators.map(operator => this.createFrom(operator)); + } + + private constructor(label: string, value: string) { + this.label = label; + this.value = value; + } +} diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/FieldMapper.ts b/packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/FieldMapper.ts new file mode 100644 index 00000000000..d89764dd226 --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/FieldMapper.ts @@ -0,0 +1,23 @@ +import { Field, FieldDTO, Operator, OperatorDTO } from "./Field"; + +export class FieldMapper { + static toDTO(configuration: Field[]): FieldDTO[] { + return configuration.map(field => { + return { + label: field.label, + value: field.value, + operators: OperatorMapper.toDTO(field.operators), + raw: field.raw + }; + }); + } +} + +export class OperatorMapper { + static toDTO(operators: Operator[]): OperatorDTO[] { + return operators.map(operator => ({ + value: operator.value || "", + label: operator.label || "" + })); + } +} diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/index.ts b/packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/index.ts new file mode 100644 index 00000000000..a265956d97d --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/domain/index.ts @@ -0,0 +1,4 @@ +export * from "./Batch"; +export * from "./BatchMapper"; +export * from "./Field"; +export * from "./FieldMapper"; diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/index.tsx b/packages/app-file-manager/src/components/BulkActions/ActionEdit/index.tsx new file mode 100644 index 00000000000..be2387edd71 --- /dev/null +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/index.tsx @@ -0,0 +1 @@ +export * from "./ActionEdit"; diff --git a/packages/app-file-manager/src/components/BulkActions/index.tsx b/packages/app-file-manager/src/components/BulkActions/index.tsx index 2a288edd8a4..5a222c65179 100644 --- a/packages/app-file-manager/src/components/BulkActions/index.tsx +++ b/packages/app-file-manager/src/components/BulkActions/index.tsx @@ -1,3 +1,4 @@ +export { ActionEdit } from "./ActionEdit"; export { ActionDelete } from "./ActionDelete"; export { ActionMove } from "./ActionMove"; export * from "./BulkActions"; diff --git a/packages/app-file-manager/src/components/FileDetails/FileDetails.tsx b/packages/app-file-manager/src/components/FileDetails/FileDetails.tsx index fb800c17f0d..27ac79a97ad 100644 --- a/packages/app-file-manager/src/components/FileDetails/FileDetails.tsx +++ b/packages/app-file-manager/src/components/FileDetails/FileDetails.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useState } from "react"; -// @ts-ignore +// @ts-expect-error import { useHotkeys } from "react-hotkeyz"; import omit from "lodash/omit"; import styled from "@emotion/styled"; diff --git a/packages/app-file-manager/src/components/FileDetails/components/BaseFields.tsx b/packages/app-file-manager/src/components/FileDetails/components/BaseFields.tsx index 7a21cb4b047..601e6775bd9 100644 --- a/packages/app-file-manager/src/components/FileDetails/components/BaseFields.tsx +++ b/packages/app-file-manager/src/components/FileDetails/components/BaseFields.tsx @@ -22,7 +22,7 @@ export const BaseFields = ({ model }: BaseFieldsProps) => { /** * TODO: Figure out correct Bind type */ - // @ts-ignore + // @ts-expect-error Bind={Bind} fields={fields} layout={model.layout || []} diff --git a/packages/app-file-manager/src/components/FileDetails/components/Extensions.tsx b/packages/app-file-manager/src/components/FileDetails/components/Extensions.tsx index 3790818a376..57735f12094 100644 --- a/packages/app-file-manager/src/components/FileDetails/components/Extensions.tsx +++ b/packages/app-file-manager/src/components/FileDetails/components/Extensions.tsx @@ -47,7 +47,7 @@ export const Extensions = ({ model }: ExtensionsProps) => { /** * TODO: Figure out correct Bind type */ - // @ts-ignore + // @ts-expect-error Bind={Bind} fields={fields} layout={layout} diff --git a/packages/app-file-manager/src/components/Grid/File.tsx b/packages/app-file-manager/src/components/Grid/File.tsx index b127c78ac28..437e60f8542 100644 --- a/packages/app-file-manager/src/components/Grid/File.tsx +++ b/packages/app-file-manager/src/components/Grid/File.tsx @@ -2,7 +2,7 @@ import React, { useCallback } from "react"; /** * Package react-lazy-load has no types. */ -// @ts-ignore +// @ts-expect-error import LazyLoad from "react-lazy-load"; import { TimeAgo } from "@webiny/ui/TimeAgo"; import { IconButton } from "@webiny/ui/Button"; diff --git a/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/FileManagerView.tsx b/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/FileManagerView.tsx index 75d7dcd10b3..c4a6086524e 100644 --- a/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/FileManagerView.tsx +++ b/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/FileManagerView.tsx @@ -3,7 +3,7 @@ import Files, { FilesRenderChildren } from "react-butterfiles"; import styled from "@emotion/styled"; import debounce from "lodash/debounce"; import { positionValues } from "react-custom-scrollbars"; -// @ts-ignore +// @ts-expect-error import { useHotkeys } from "react-hotkeyz"; import { observer } from "mobx-react-lite"; import { ReactComponent as UploadIcon } from "@material-design-icons/svg/filled/cloud_upload.svg"; diff --git a/packages/app-file-manager/src/modules/FileManagerRenderer/index.tsx b/packages/app-file-manager/src/modules/FileManagerRenderer/index.tsx index 8996c301fc0..6815276b3b8 100644 --- a/packages/app-file-manager/src/modules/FileManagerRenderer/index.tsx +++ b/packages/app-file-manager/src/modules/FileManagerRenderer/index.tsx @@ -2,7 +2,7 @@ import React from "react"; import { FileManagerViewConfig as FileManagerConfig } from "~/index"; import { FileManagerRenderer } from "./FileManagerView"; import { FilterByType } from "./filters/FilterByType"; -import { ActionDelete, ActionMove } from "~/components/BulkActions"; +import { ActionDelete, ActionEdit, ActionMove } from "~/components/BulkActions"; import { Name } from "~/components/FileDetails/components/Name"; import { Tags } from "~/components/FileDetails/components/Tags"; import { Aliases } from "~/components/FileDetails/components/Aliases"; @@ -16,6 +16,7 @@ export const FileManagerRendererModule = () => { } /> + } /> } /> } /> } /> diff --git a/packages/app-file-manager/src/modules/FileTypes/fileImage/EditAction.tsx b/packages/app-file-manager/src/modules/FileTypes/fileImage/EditAction.tsx index e4e7bf39f54..5dbe279d751 100644 --- a/packages/app-file-manager/src/modules/FileTypes/fileImage/EditAction.tsx +++ b/packages/app-file-manager/src/modules/FileTypes/fileImage/EditAction.tsx @@ -1,7 +1,7 @@ import React from "react"; -// @ts-ignore +// @ts-expect-error import { Hotkeys } from "react-hotkeyz"; -// @ts-ignore +// @ts-expect-error import dataURLtoBlob from "dataurl-to-blob"; import { ImageEditorDialog } from "@webiny/ui/ImageUpload"; import { Tooltip } from "@webiny/ui/Tooltip"; diff --git a/packages/app-form-builder/src/admin/components/FormEditor/Context/useFormEditorFactory.tsx b/packages/app-form-builder/src/admin/components/FormEditor/Context/useFormEditorFactory.tsx index 0d93d380a47..5dbfd3dc88b 100644 --- a/packages/app-form-builder/src/admin/components/FormEditor/Context/useFormEditorFactory.tsx +++ b/packages/app-form-builder/src/admin/components/FormEditor/Context/useFormEditorFactory.tsx @@ -11,25 +11,25 @@ import { UpdateFormRevisionMutationVariables } from "./graphql"; import { + deleteField, getFieldPosition, moveField, moveFieldBetweenSteps, moveRow, - deleteField, - moveStep, - moveRowBetweenSteps + moveRowBetweenSteps, + moveStep } from "./functions"; import { plugins } from "@webiny/plugins"; import { - FbFormModelField, - FieldIdType, - FieldLayoutPositionType, FbBuilderFieldPlugin, - FbFormModel, - FbUpdateFormInput, FbErrorResponse, + FbFormModel, + FbFormModelField, FbFormStep, + FbUpdateFormInput, + FieldIdType, + FieldLayoutPositionType, MoveFieldParams } from "~/types"; import { ApolloClient } from "apollo-client"; @@ -279,20 +279,22 @@ export const useFormEditorFactory = ( * Returns complete layout with fields data in it (not just field IDs) */ getLayoutFields: targetStepId => { - const stepLayout = state.data.steps - .find(v => v.id === targetStepId) - ?.layout.filter(row => Boolean(row)); + const step = state.data.steps.find(v => v.id === targetStepId); + if (!step) { + return []; + } // Replace every field ID with actual field object. - // @ts-ignore - return stepLayout.map(row => { - return row - .map(id => { - return self.getField({ - _id: id - }); - }) - .filter(Boolean) as FbFormModelField[]; - }); + return step.layout + .filter(row => Boolean(row)) + .map(row => { + return row + .map(id => { + return self.getField({ + _id: id + }); + }) + .filter(Boolean) as FbFormModelField[]; + }); }, /** diff --git a/packages/app-form-builder/src/admin/components/FormEditor/DragPreview.tsx b/packages/app-form-builder/src/admin/components/FormEditor/DragPreview.tsx index f863154402c..912d4dc6936 100644 --- a/packages/app-form-builder/src/admin/components/FormEditor/DragPreview.tsx +++ b/packages/app-form-builder/src/admin/components/FormEditor/DragPreview.tsx @@ -21,7 +21,7 @@ const onOffsetChange = (monitor: DragSourceMonitor) => () => { /** * TS is complaining about webkit keyword */ - // @ts-ignore + // @ts-expect-error dragPreviewRef.style["-webkit-transform"] = transform; }; @@ -29,7 +29,7 @@ const DragPreview: React.FC = () => { const [dragHelperOpacity, setDragHelperOpacity] = useState(0); const { isDragging } = useDragLayer(monitor => { if (!subscribedToOffsetChange) { - // @ts-ignore + // @ts-expect-error monitor.subscribeToOffsetChange(onOffsetChange(monitor)); subscribedToOffsetChange = true; } diff --git a/packages/app-form-builder/src/admin/components/FormEditor/Draggable.tsx b/packages/app-form-builder/src/admin/components/FormEditor/Draggable.tsx index dc0b41bd4f2..298ec145f79 100644 --- a/packages/app-form-builder/src/admin/components/FormEditor/Draggable.tsx +++ b/packages/app-form-builder/src/admin/components/FormEditor/Draggable.tsx @@ -37,11 +37,6 @@ const Draggable: React.FC = props => { const [{ isDragging }, drag, preview] = useDrag({ item: { type: "element", - /** - * TODO @ts-refactor - * There is no target on item in types. - */ - // @ts-ignore target }, collect: monitor => ({ diff --git a/packages/app-form-builder/src/admin/components/FormEditor/DropZone/Horizontal.tsx b/packages/app-form-builder/src/admin/components/FormEditor/DropZone/Horizontal.tsx index 69b1b53bfa5..68963f67cb1 100644 --- a/packages/app-form-builder/src/admin/components/FormEditor/DropZone/Horizontal.tsx +++ b/packages/app-form-builder/src/admin/components/FormEditor/DropZone/Horizontal.tsx @@ -37,11 +37,11 @@ const OuterDiv = styled("div")( }, (props: OuterDivProps) => ({ [props.last ? "bottom" : "top"]: -15, - // @ts-ignore + // @ts-expect-error [InnerDiv]: { borderColor: props.isOver ? "var(--mdc-theme-primary)" : "var(--mdc-theme-secondary)", display: props.isDragging ? "block" : "none", - // @ts-ignore + // @ts-expect-error [BackgroundColorDiv]: { opacity: 0.5, backgroundColor: props.isOver diff --git a/packages/app-form-builder/src/admin/components/FormEditor/DropZone/Vertical.tsx b/packages/app-form-builder/src/admin/components/FormEditor/DropZone/Vertical.tsx index dd08f84328e..cae75a72850 100644 --- a/packages/app-form-builder/src/admin/components/FormEditor/DropZone/Vertical.tsx +++ b/packages/app-form-builder/src/admin/components/FormEditor/DropZone/Vertical.tsx @@ -38,12 +38,12 @@ const OuterDivVertical = styled("div")( (props: OuterDivVerticalProps) => ({ [props.last ? "right" : "left"]: -9, textAlign: props.last ? "right" : "left", - // @ts-ignore + // @ts-expect-error [InnerDivVertical]: { borderColor: props.isOver ? "var(--mdc-theme-primary)" : "var(--mdc-theme-secondary)", [props.last ? "right" : "left"]: -2, display: props.isDragging ? "block" : "none", - // @ts-ignore + // @ts-expect-error [BackgroundColorDiv]: { opacity: 0.5, backgroundColor: props.isOver diff --git a/packages/app-form-builder/src/admin/components/FormEditor/FormEditorApp.tsx b/packages/app-form-builder/src/admin/components/FormEditor/FormEditorApp.tsx index f71a8ad0fee..3df3e563837 100644 --- a/packages/app-form-builder/src/admin/components/FormEditor/FormEditorApp.tsx +++ b/packages/app-form-builder/src/admin/components/FormEditor/FormEditorApp.tsx @@ -14,8 +14,6 @@ const FormEditorApp: React.FC = () => { diff --git a/packages/app-form-builder/src/admin/components/FormEditor/Tabs/EditTab/EditFieldDialog/ValidatorsTab.tsx b/packages/app-form-builder/src/admin/components/FormEditor/Tabs/EditTab/EditFieldDialog/ValidatorsTab.tsx index 74013c2f752..3181f19af9a 100644 --- a/packages/app-form-builder/src/admin/components/FormEditor/Tabs/EditTab/EditFieldDialog/ValidatorsTab.tsx +++ b/packages/app-form-builder/src/admin/components/FormEditor/Tabs/EditTab/EditFieldDialog/ValidatorsTab.tsx @@ -1,5 +1,3 @@ -// TODO @ts-refactor figure out correct types. -// @ts-nocheck import React, { useMemo } from "react"; import { plugins } from "@webiny/plugins"; import { Switch } from "@webiny/ui/Switch"; @@ -9,20 +7,23 @@ import { SimpleFormHeader } from "@webiny/app-admin/components/SimpleForm"; import { useFormEditor } from "../../../Context"; -import { Form } from "@webiny/form"; +import { BindComponentRenderPropOnChange, Form, FormRenderPropParams } from "@webiny/form"; import cloneDeep from "lodash/cloneDeep"; import debounce from "lodash/debounce"; -import { FormRenderPropParams } from "@webiny/form"; -import { Grid, Cell } from "@webiny/ui/Grid"; +import { Cell, Grid } from "@webiny/ui/Grid"; import { Input } from "@webiny/ui/Input"; import { validation } from "@webiny/validation"; -import { FbBuilderFormFieldValidatorPlugin, FbFormModelField } from "~/types"; +import { + FbBuilderFormFieldValidatorPlugin, + FbBuilderFormFieldValidatorPluginValidator, + FbFormModelField +} from "~/types"; interface OnEnabledChangeParams { data: Record; - validationValue: string; - onChangeValidation: string; - validator: string; + validationValue: FbBuilderFormFieldValidatorPluginValidator[]; + onChangeValidation: BindComponentRenderPropOnChange; + validator: FbBuilderFormFieldValidatorPluginValidator; } const onEnabledChange = ({ data, @@ -67,6 +68,11 @@ const onFormChange = debounce( 200 ); +interface Validator { + optional: boolean; + validator: FbBuilderFormFieldValidatorPluginValidator; +} + interface ValidatorsTabProps { field: FbFormModelField; form: FormRenderPropParams; @@ -78,19 +84,22 @@ const ValidatorsTab: React.FC = props => { const fieldPlugin = getFieldPlugin({ name: field.name }); - const validators = useMemo(() => { + const validators = useMemo(() => { + const fieldPluginFieldValidators = fieldPlugin?.field?.validators; + if (!fieldPluginFieldValidators) { + return []; + } return plugins .byType("form-editor-field-validator") - .map(plugin => plugin.validator) - .map(validator => { - if (fieldPlugin.field.validators.includes(validator.name)) { - return { optional: true, validator }; - } else if (fieldPlugin.field.validators.includes(`!${validator.name}`)) { - return { optional: false, validator }; + .reduce((collection, plugin) => { + if (fieldPluginFieldValidators.includes(plugin.validator.name)) { + collection.push({ optional: true, validator: plugin.validator }); + } else if (fieldPluginFieldValidators.includes(`!${plugin.validator.name}`)) { + collection.push({ optional: false, validator: plugin.validator }); } - return null; - }) - .filter(Boolean) + + return collection; + }, []) .sort((a, b) => { if (!a.optional && b.optional) { return -1; @@ -102,85 +111,96 @@ const ValidatorsTab: React.FC = props => { return 0; }); - }, []); + }, [fieldPlugin]); return ( - {({ value: validationValue, onChange: onChangeValidation }) => - validators.map(({ optional, validator }) => { - const validatorIndex = validationValue.findIndex( - item => item.name === validator.name - ); - const data = validationValue[validatorIndex]; - - return ( - - {/*TODO: @ts-adrian nema descriptiona?*/} - - {optional && ( - = 0} - onChange={() => - onEnabledChange({ - data, - validationValue, - onChangeValidation, - validator - }) - } - /> - )} - - {data && ( -
- onFormChange({ - data, - validationValue, - onChangeValidation, - validatorIndex - }) - } - > - {({ Bind, setValue }) => ( - - - - {/*TODO: @ts-adrian kako ovo?*/} - - - - - + {({ value: validationValue, onChange: onChangeValidation }) => { + return ( + <> + {validators.map(({ optional, validator }) => { + const validatorIndex = validationValue.findIndex( + /** + * TODO remove expect error and fix the validationValue type + */ + // @ts-expect-error + item => item.name === validator.name + ); + const data = validationValue[validatorIndex]; - {typeof validator.renderSettings === "function" && - validator.renderSettings({ - setValue, - setMessage: message => { - setValue("message", message); - }, + return ( + + {/*TODO: @ts-adrian nema descriptiona?*/} + + {optional && ( + = 0} + onChange={() => + onEnabledChange({ + data, + validationValue, + onChangeValidation, + validator + }) + } + /> + )} + + {data && ( + + onFormChange({ data, - Bind, - formFieldData - })} - + validationValue, + onChangeValidation, + validatorIndex + }) + } + > + {({ Bind, setValue }) => ( + + + + {/*TODO: @ts-adrian kako ovo?*/} + + + + + + + {typeof validator.renderSettings === + "function" && + validator.renderSettings({ + setValue, + setMessage: message => { + setValue("message", message); + }, + data, + Bind, + formFieldData + })} + + )} +
)} - - )} -
- ); - }) - } + + ); + })} + + ); + }}
); }; diff --git a/packages/app-form-builder/src/admin/plugins/editor/defaultBar/Name/Name.tsx b/packages/app-form-builder/src/admin/plugins/editor/defaultBar/Name/Name.tsx index 5682c5cc49f..b94835a2dc4 100644 --- a/packages/app-form-builder/src/admin/plugins/editor/defaultBar/Name/Name.tsx +++ b/packages/app-form-builder/src/admin/plugins/editor/defaultBar/Name/Name.tsx @@ -6,7 +6,7 @@ import { useFormEditor } from "~/admin/components/FormEditor"; /** * Package react-hotkeyz does not have types. */ -// @ts-ignore +// @ts-expect-error import { useHotkeys } from "react-hotkeyz"; import { FormMeta, diff --git a/packages/app-form-builder/src/admin/plugins/editor/formFieldValidators/pattern.tsx b/packages/app-form-builder/src/admin/plugins/editor/formFieldValidators/pattern.tsx index ae77750d8ed..f06cff4bcc5 100644 --- a/packages/app-form-builder/src/admin/plugins/editor/formFieldValidators/pattern.tsx +++ b/packages/app-form-builder/src/admin/plugins/editor/formFieldValidators/pattern.tsx @@ -17,8 +17,6 @@ const plugin: FbBuilderFormFieldValidatorPlugin = { label: "Pattern", description: "Entered value must match a specific pattern.", defaultMessage: "Invalid value.", - // TODO @ts-refactor verify that settings is being used - there is no type written for it - // @ts-ignore defaultSettings: { preset: "custom" }, diff --git a/packages/app-form-builder/src/admin/plugins/editor/formFields/components/OptionsList.tsx b/packages/app-form-builder/src/admin/plugins/editor/formFields/components/OptionsList.tsx index ad03e9736ed..ef0a420457a 100644 --- a/packages/app-form-builder/src/admin/plugins/editor/formFields/components/OptionsList.tsx +++ b/packages/app-form-builder/src/admin/plugins/editor/formFields/components/OptionsList.tsx @@ -7,7 +7,7 @@ import { OptionsListItem, AddOptionInput, EditFieldOptionDialog } from "./Option /** * Package react-sortable-hoc is missing types. */ -// @ts-ignore +// @ts-expect-error import { sortableContainer, sortableElement, sortableHandle } from "react-sortable-hoc"; import { Icon } from "@webiny/ui/Icon"; import { Typography } from "@webiny/ui/Typography"; diff --git a/packages/app-form-builder/src/admin/plugins/editor/formFields/components/OptionsListComponents/EditFieldOptionDialog.tsx b/packages/app-form-builder/src/admin/plugins/editor/formFields/components/OptionsListComponents/EditFieldOptionDialog.tsx index 3013f8eb485..835213c5c43 100644 --- a/packages/app-form-builder/src/admin/plugins/editor/formFields/components/OptionsListComponents/EditFieldOptionDialog.tsx +++ b/packages/app-form-builder/src/admin/plugins/editor/formFields/components/OptionsListComponents/EditFieldOptionDialog.tsx @@ -7,7 +7,7 @@ import { i18n } from "@webiny/app/i18n"; /** * Package react-hotkeys does not have types. */ -// @ts-ignore +// @ts-expect-error import { Hotkeys } from "react-hotkeyz"; import { validation } from "@webiny/validation"; diff --git a/packages/app-form-builder/src/admin/plugins/editor/formSettings/components/GeneralSettings.tsx b/packages/app-form-builder/src/admin/plugins/editor/formSettings/components/GeneralSettings.tsx index 012db0f106a..cb901cd5a31 100644 --- a/packages/app-form-builder/src/admin/plugins/editor/formSettings/components/GeneralSettings.tsx +++ b/packages/app-form-builder/src/admin/plugins/editor/formSettings/components/GeneralSettings.tsx @@ -36,11 +36,6 @@ const GeneralSettings: React.FC = ({ Bind }) => { }, []); const rteProps = useMemo(() => { - /** - * TODO @ts-refactor - * Missing plugin type for fb-rte-config - */ - // @ts-ignore return createPropsFromConfig(plugins.byType("fb-rte-config").map(pl => pl.config)); }, []); diff --git a/packages/app-form-builder/src/admin/plugins/editor/formSettings/components/TermsOfServiceSettings.tsx b/packages/app-form-builder/src/admin/plugins/editor/formSettings/components/TermsOfServiceSettings.tsx index b5da3533820..c454594033b 100644 --- a/packages/app-form-builder/src/admin/plugins/editor/formSettings/components/TermsOfServiceSettings.tsx +++ b/packages/app-form-builder/src/admin/plugins/editor/formSettings/components/TermsOfServiceSettings.tsx @@ -1,22 +1,17 @@ import * as React from "react"; -import { Grid, Cell } from "@webiny/ui/Grid"; +import { useMemo } from "react"; +import { Cell, Grid } from "@webiny/ui/Grid"; import { Input } from "@webiny/ui/Input"; import { Switch } from "@webiny/ui/Switch"; import get from "lodash/get"; import { FormSettingsPluginRenderFunctionType } from "~/types"; -import { RichTextEditor, createPropsFromConfig } from "@webiny/app-admin/components/RichTextEditor"; -import { useMemo } from "react"; +import { createPropsFromConfig, RichTextEditor } from "@webiny/app-admin/components/RichTextEditor"; import { plugins } from "@webiny/plugins"; const TermsOfServiceSettings: FormSettingsPluginRenderFunctionType = ({ Bind, formData }) => { const enabled = get(formData, "termsOfServiceMessage.enabled"); const rteProps = useMemo(() => { - /** - * TODO @ts-refactor - * Missing plugin type for fb-rte-config - */ - // @ts-ignore return createPropsFromConfig(plugins.byType("fb-rte-config").map(pl => pl.config)); }, []); diff --git a/packages/app-form-builder/src/components/Form/components/createTermsOfServiceComponent.tsx b/packages/app-form-builder/src/components/Form/components/createTermsOfServiceComponent.tsx index 1fd8170d984..56d1af12071 100644 --- a/packages/app-form-builder/src/components/Form/components/createTermsOfServiceComponent.tsx +++ b/packages/app-form-builder/src/components/Form/components/createTermsOfServiceComponent.tsx @@ -29,7 +29,7 @@ const createTermsOfServiceComponent = ({ setTermsOfServiceAccepted }: CreateTermsOfServiceComponentArgs): TermsOfServiceComponent => // TODO @ts-refactor figure out how to type this - // @ts-ignore + // @ts-expect-error function TermsOfService(props: TermsOfServiceProps) { if (!termsOfServiceEnabled(formData)) { return null; diff --git a/packages/app-form-builder/src/types.ts b/packages/app-form-builder/src/types.ts index 38fe728d47f..cce7759e9b4 100644 --- a/packages/app-form-builder/src/types.ts +++ b/packages/app-form-builder/src/types.ts @@ -28,25 +28,26 @@ export interface FbBuilderFieldValidator { message: string; settings: any; } - +export interface FbBuilderFormFieldValidatorPluginValidator { + name: string; + label: string; + description: string; + defaultMessage: string; + defaultSettings?: Record; + renderSettings?: (props: { + Bind: BindComponent; + setValue: (name: string, value: any) => void; + setMessage: (message: string) => void; + data: FbBuilderFieldValidator; + // We need to return this optional "field" property in the case where we want to render different fields based on it's type or format + formFieldData?: { + [key: string]: any; + }; + }) => React.ReactElement; +} export type FbBuilderFormFieldValidatorPlugin = Plugin & { type: "form-editor-field-validator"; - validator: { - name: string; - label: string; - description: string; - defaultMessage: string; - renderSettings?: (props: { - Bind: BindComponent; - setValue: (name: string, value: any) => void; - setMessage: (message: string) => void; - data: FbBuilderFieldValidator; - // We need to return this optional "field" property in the case where we want to render different fields based on it's type or format - formFieldData?: { - [key: string]: any; - }; - }) => React.ReactElement; - }; + validator: FbBuilderFormFieldValidatorPluginValidator; }; export type FbBuilderFormFieldPatternValidatorPlugin = Plugin & { diff --git a/packages/app-graphql-playground/src/plugins/Playground.tsx b/packages/app-graphql-playground/src/plugins/Playground.tsx index e8e20b162c9..f76756a024b 100644 --- a/packages/app-graphql-playground/src/plugins/Playground.tsx +++ b/packages/app-graphql-playground/src/plugins/Playground.tsx @@ -4,7 +4,7 @@ import { setContext } from "apollo-link-context"; /** * Package load-script does not have types. */ -// @ts-ignore +// @ts-expect-error import loadScript from "load-script"; import { Global } from "@emotion/react"; import { plugins } from "@webiny/plugins"; @@ -34,7 +34,7 @@ const withHeaders = (link: ApolloLink, headers: Record): ApolloL const initScripts = () => { return new Promise((resolve: any) => { - // @ts-ignore + // @ts-expect-error if (window.GraphQLPlayground) { return resolve(); } @@ -95,7 +95,7 @@ const Playground: React.FC = ({ createApolloClient }) => { useEffect(() => { if (!loading) { - // @ts-ignore + // @ts-expect-error window.GraphQLPlayground.init(document.getElementById("graphql-playground"), { tabs, createApolloLink, diff --git a/packages/app-graphql-playground/src/plugins/index.tsx b/packages/app-graphql-playground/src/plugins/index.tsx index da42d93f6b2..83df8773c79 100644 --- a/packages/app-graphql-playground/src/plugins/index.tsx +++ b/packages/app-graphql-playground/src/plugins/index.tsx @@ -1,5 +1,5 @@ import { GraphQLPlaygroundTabPlugin } from "~/types"; -// @ts-ignore +// @ts-expect-error import placeholder from "!!raw-loader!./placeholder.graphql"; import { config as appConfig } from "@webiny/app/config"; diff --git a/packages/app-headless-cms/src/admin/components/DropZone/Horizontal.tsx b/packages/app-headless-cms/src/admin/components/DropZone/Horizontal.tsx index 54e908e247c..c81ecbe5f7b 100644 --- a/packages/app-headless-cms/src/admin/components/DropZone/Horizontal.tsx +++ b/packages/app-headless-cms/src/admin/components/DropZone/Horizontal.tsx @@ -38,11 +38,11 @@ const OuterDiv = styled("div")( }, (props: OuterDivProps) => ({ [props.last ? "bottom" : "top"]: -15, - // @ts-ignore + // @ts-expect-error [InnerDiv]: { borderColor: props.isOver ? "var(--mdc-theme-primary)" : "var(--mdc-theme-secondary)", display: props.isDragging ? "block" : "none", - // @ts-ignore + // @ts-expect-error [BackgroundColorDiv]: { opacity: 0.5, backgroundColor: props.isOver diff --git a/packages/app-headless-cms/src/admin/components/DropZone/Vertical.tsx b/packages/app-headless-cms/src/admin/components/DropZone/Vertical.tsx index 1da5429e070..f08a37b8a53 100644 --- a/packages/app-headless-cms/src/admin/components/DropZone/Vertical.tsx +++ b/packages/app-headless-cms/src/admin/components/DropZone/Vertical.tsx @@ -39,12 +39,12 @@ const OuterDivVertical = styled("div")( (props: OuterDivVerticalProps) => ({ [props.last ? "right" : "left"]: -9, textAlign: props.last ? "right" : "left", - // @ts-ignore + // @ts-expect-error [InnerDivVertical]: { borderColor: props.isOver ? "var(--mdc-theme-primary)" : "var(--mdc-theme-secondary)", [props.last ? "right" : "left"]: -2, display: props.isDragging ? "block" : "none", - // @ts-ignore + // @ts-expect-error [BackgroundColorDiv]: { opacity: 0.5, backgroundColor: props.isOver diff --git a/packages/app-headless-cms/src/admin/components/FieldEditor/FieldEditorContext.tsx b/packages/app-headless-cms/src/admin/components/FieldEditor/FieldEditorContext.tsx index 245ba1abc15..5290051a028 100644 --- a/packages/app-headless-cms/src/admin/components/FieldEditor/FieldEditorContext.tsx +++ b/packages/app-headless-cms/src/admin/components/FieldEditor/FieldEditorContext.tsx @@ -287,9 +287,8 @@ export const FieldEditorProvider: React.FC = ({ if (!(key in field)) { return false; } - // TODO @ts-refactor figure if there is a way to fix this. - // @ts-ignore - if (field[key] !== query[key]) { + + if (field[key as keyof typeof field] !== query[key as keyof typeof query]) { return false; } } diff --git a/packages/app-headless-cms/src/admin/plugins/editor/defaultBar/Name/Name.tsx b/packages/app-headless-cms/src/admin/plugins/editor/defaultBar/Name/Name.tsx index 2704b522725..d4ec33e5877 100644 --- a/packages/app-headless-cms/src/admin/plugins/editor/defaultBar/Name/Name.tsx +++ b/packages/app-headless-cms/src/admin/plugins/editor/defaultBar/Name/Name.tsx @@ -4,7 +4,7 @@ import { Tooltip } from "@webiny/ui/Tooltip"; /** * Package react-hotkeyz does not have types. */ -// @ts-ignore +// @ts-expect-error import { useHotkeys } from "react-hotkeyz"; import { FormName, formNameWrapper, NameInputWrapper, NameWrapper } from "./NameStyled"; import { i18n } from "@webiny/app/i18n"; diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dateTime/DateTimeWithTimezone.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dateTime/DateTimeWithTimezone.tsx index 14880383ebf..8c115b08cca 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dateTime/DateTimeWithTimezone.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dateTime/DateTimeWithTimezone.tsx @@ -55,8 +55,6 @@ const parseTime = (value?: string): Pick => { export interface DateTimeWithTimezoneProps { bind: BindComponentRenderProp; - // TODO @ts-refactor figure out correct trailing icon type - // @ts-ignore trailingIcon?: any; field: CmsModelField; } diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dateTime/utils.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dateTime/utils.tsx index e33237c6553..e79a725ef71 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dateTime/utils.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dateTime/utils.tsx @@ -65,8 +65,6 @@ const deleteIconStyles = css({ }); interface RemoveFieldButtonProps { - // TODO @ts-refactor figure out correct trailing icon type - // @ts-ignore trailingIcon: any; } export const RemoveFieldButton: React.FC = ({ trailingIcon }) => { diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/richText/richTextInputs.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/richText/richTextInputs.tsx index 9f5ae4a7481..1411f8aefdd 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/richText/richTextInputs.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/richText/richTextInputs.tsx @@ -80,11 +80,6 @@ const plugin: CmsEditorFieldRendererPlugin = { const { field } = props; const rteProps = useMemo(() => { - /** - * TODO @ts-refactor - * Missing cms-rte-config plugin. - */ - // @ts-ignore return createPropsFromConfig(plugins.byType("cms-rte-config").map(pl => pl.config)); }, []); diff --git a/packages/app-headless-cms/src/admin/plugins/icons.tsx b/packages/app-headless-cms/src/admin/plugins/icons.tsx index c8f1f0b1fc2..bde8b9d159c 100644 --- a/packages/app-headless-cms/src/admin/plugins/icons.tsx +++ b/packages/app-headless-cms/src/admin/plugins/icons.tsx @@ -38,7 +38,6 @@ const plugin: CmsIconsPlugin = { icons.push({ id: [pack, icon], name: icon, - // @ts-ignore svg: createSvg(defs[icon]) }); }); diff --git a/packages/app-headless-cms/src/admin/views/contentModels/cache.ts b/packages/app-headless-cms/src/admin/views/contentModels/cache.ts index c6244d1d14d..4eb450a4bd3 100644 --- a/packages/app-headless-cms/src/admin/views/contentModels/cache.ts +++ b/packages/app-headless-cms/src/admin/views/contentModels/cache.ts @@ -1,9 +1,13 @@ import dotProp from "dot-prop-immutable"; import { DataProxy } from "apollo-cache"; import ApolloClient from "apollo-client"; -import { LIST_CONTENT_MODELS, LIST_MENU_CONTENT_GROUPS_MODELS } from "../../viewsGraphql"; +import { + LIST_CONTENT_MODELS, + LIST_MENU_CONTENT_GROUPS_MODELS, + ListCmsModelsQueryResponse, + ListMenuCmsGroupsQueryResponse +} from "../../viewsGraphql"; import { CmsEditorContentModel } from "~/types"; -import { ListCmsModelsQueryResponse, ListMenuCmsGroupsQueryResponse } from "../../viewsGraphql"; export const addModelToListCache = (cache: DataProxy, model: CmsEditorContentModel): void => { const response = cache.readQuery({ @@ -59,13 +63,13 @@ export const removeModelFromCache = ( ): void => { const id = `CmsContentModel:${model.modelId}`; - // @ts-ignore + // @ts-expect-error client.cache.data.delete(id); - // @ts-ignore + // @ts-expect-error Object.keys(client.cache.data.data).forEach(key => { if (key.startsWith(`${id}.`) || key.startsWith(`$${id}.`)) { - // @ts-ignore + // @ts-expect-error client.cache.data.delete(key); } }); diff --git a/packages/app-mailer/src/views/settings/Settings.tsx b/packages/app-mailer/src/views/settings/Settings.tsx index 77da1c65e0b..c5499e35659 100644 --- a/packages/app-mailer/src/views/settings/Settings.tsx +++ b/packages/app-mailer/src/views/settings/Settings.tsx @@ -193,7 +193,7 @@ export const Settings: React.FC = () => { type="password" autoComplete="new-password" value={""} - // @ts-ignore + // @ts-expect-error inputRef={password} /> diff --git a/packages/app-page-builder-editor/src/components/Editor/Editor.tsx b/packages/app-page-builder-editor/src/components/Editor/Editor.tsx index 871dda58529..e42fa17dfca 100644 --- a/packages/app-page-builder-editor/src/components/Editor/Editor.tsx +++ b/packages/app-page-builder-editor/src/components/Editor/Editor.tsx @@ -35,7 +35,7 @@ export const Editor: React.FunctionComponent = (/*{ revisions } const firstRender = React.useRef(true); useEffect(() => { - // @ts-ignore + // @ts-expect-error window["editor"] = editor; // addKeyHandler("mod+z", e => { // e.preventDefault(); diff --git a/packages/app-page-builder-editor/src/components/RecoilExternal.ts b/packages/app-page-builder-editor/src/components/RecoilExternal.ts index dc2ad5983fb..92064b455be 100644 --- a/packages/app-page-builder-editor/src/components/RecoilExternal.ts +++ b/packages/app-page-builder-editor/src/components/RecoilExternal.ts @@ -18,7 +18,7 @@ interface Portal { const portal: Portal = {}; export default function RecoilExternal() { - // @ts-ignore + // @ts-expect-error portal.getState = useRecoilCallback<[atom: RecoilState], any>( ({ snapshot }) => function (atom: RecoilState) { @@ -27,7 +27,7 @@ export default function RecoilExternal() { [] ); - // @ts-ignore + // @ts-expect-error portal.setState = useRecoilCallback(({ set }) => set, []); return null; diff --git a/packages/app-page-builder-editor/src/contexts/EditorState.tsx b/packages/app-page-builder-editor/src/contexts/EditorState.tsx index 5cefbb60eed..344a373c079 100644 --- a/packages/app-page-builder-editor/src/contexts/EditorState.tsx +++ b/packages/app-page-builder-editor/src/contexts/EditorState.tsx @@ -1,4 +1,8 @@ -// @ts-nocheck +/** + * TODO @pavel + * + * eslint is disabled due to too much variables arent used, not sure why. + */ /* eslint-disable */ import React, { useEffect, useRef } from "react"; import { PbEditorElement } from "~/types"; @@ -37,7 +41,7 @@ const trackedAtoms = ["elements"]; const isTrackedAtomChanged = (state: Partial): boolean => { for (const atom of trackedAtoms) { - if (!state[atom]) { + if (!state[atom as keyof PbState]) { continue; } return true; @@ -80,6 +84,10 @@ export const EditorState: React.FunctionComponent = () => { // when saving new state history we must remove everything after the current one // since this is the new starting point of the state history snapshotsHistory.current.future = []; + /** + * TODO @pavel check this + */ + // @ts-expect-error snapshotsHistory.current.past.push(takeSnapshot()); snapshotsHistory.current.present = currentSnapshot; snapshotsHistory.current.busy = false; @@ -91,7 +99,7 @@ export const EditorState: React.FunctionComponent = () => { return { ...prevValue, ...item, - parent: item.parent !== undefined ? item.parent : prevValue.parent + parent: item.parent !== undefined ? item.parent : prevValue?.parent }; }); return item.id; diff --git a/packages/app-page-builder-editor/src/index.tsx b/packages/app-page-builder-editor/src/index.tsx index fe09cf8a3a6..52f02dd3272 100644 --- a/packages/app-page-builder-editor/src/index.tsx +++ b/packages/app-page-builder-editor/src/index.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck import React from "react"; import { RecoilRoot } from "recoil"; import { Compose, HigherOrderComponent } from "@webiny/app-admin"; @@ -13,6 +12,10 @@ import { PbEditorElement } from "~/types"; import omit from "lodash/omit"; const EditorHOC: HigherOrderComponent = () => { + /** + * TODO @pavel fix types + */ + // @ts-expect-error return function Editor({ page, revisions }) { return ( @@ -29,6 +32,7 @@ const EditorHOC: HigherOrderComponent = () => { // Unsetting the `content` object because content is reconstructed from a flat structure on each // save; thus, we don't need to store this massive object into our state. + // @ts-expect-error const pageData: PageAtomType = omit(page, ["content"]); set(pageAtom, pageData); }} diff --git a/packages/app-page-builder-elements/src/components/Elements.tsx b/packages/app-page-builder-elements/src/components/Elements.tsx index d179e766b7e..60ce9b94eb8 100644 --- a/packages/app-page-builder-elements/src/components/Elements.tsx +++ b/packages/app-page-builder-elements/src/components/Elements.tsx @@ -61,7 +61,6 @@ export const Elements: React.FC = props => { key={key} element={element} meta={{ - // @ts-ignore depth: (currentRendererMeta.depth || 0) + 1, parentElement: props.element, parentBlockElement, diff --git a/packages/app-page-builder-elements/src/contexts/Renderer.tsx b/packages/app-page-builder-elements/src/contexts/Renderer.tsx index 7b109b9226e..7e8a4971975 100644 --- a/packages/app-page-builder-elements/src/contexts/Renderer.tsx +++ b/packages/app-page-builder-elements/src/contexts/Renderer.tsx @@ -17,7 +17,7 @@ export const RendererProvider: React.FC = ({ const pageElements = usePageElements(); - // @ts-ignore Resolve the `getElement` issue. + // @ts-expect-error Resolve the `getElement` issue. const value: RendererContextValue = { ...pageElements, getElement, getAttributes, meta }; return {children}; diff --git a/packages/app-page-builder-elements/src/modifiers/attributes/animation/initializeAos.ts b/packages/app-page-builder-elements/src/modifiers/attributes/animation/initializeAos.ts index 6d8b38756d6..ecc7156e95e 100644 --- a/packages/app-page-builder-elements/src/modifiers/attributes/animation/initializeAos.ts +++ b/packages/app-page-builder-elements/src/modifiers/attributes/animation/initializeAos.ts @@ -21,7 +21,7 @@ export const initializeAos = async () => { await pbDocumentCheck; - // @ts-ignore Complains about the `.css` format, but all works correctly. + // @ts-expect-error Complains about the `.css` format, but all works correctly. await import("aos/dist/aos.css"); const aos = await import("aos"); diff --git a/packages/app-page-builder-elements/src/renderers/embeds/components/OEmbed.tsx b/packages/app-page-builder-elements/src/renderers/embeds/components/OEmbed.tsx index 1363772c6af..aceaa150083 100644 --- a/packages/app-page-builder-elements/src/renderers/embeds/components/OEmbed.tsx +++ b/packages/app-page-builder-elements/src/renderers/embeds/components/OEmbed.tsx @@ -18,7 +18,7 @@ export interface OEmbedProps { function appendSDK(props: OEmbedProps): Promise { const { sdk, global, element } = props; const { url } = element?.data?.source || {}; - // @ts-ignore Figure out better type for global. + // @ts-expect-error Figure out better type for global. if (!sdk || !url || window[global]) { return Promise.resolve(); } diff --git a/packages/app-page-builder-elements/src/renderers/embeds/pinterest.tsx b/packages/app-page-builder-elements/src/renderers/embeds/pinterest.tsx index d613e41fa86..57ae4b99775 100644 --- a/packages/app-page-builder-elements/src/renderers/embeds/pinterest.tsx +++ b/packages/app-page-builder-elements/src/renderers/embeds/pinterest.tsx @@ -31,9 +31,7 @@ function appendSDK(element: Element): Promise { function initEmbed(element: Element): void { const node = document.getElementById(element.id); - // @ts-ignore if (node && window.PinUtils) { - // @ts-ignore window.PinUtils.build(); } } diff --git a/packages/app-page-builder-elements/src/renderers/embeds/twitter.tsx b/packages/app-page-builder-elements/src/renderers/embeds/twitter.tsx index 1d111bf5e6a..d115da79a39 100644 --- a/packages/app-page-builder-elements/src/renderers/embeds/twitter.tsx +++ b/packages/app-page-builder-elements/src/renderers/embeds/twitter.tsx @@ -8,7 +8,7 @@ const oembed: Partial = { global: "twttr", sdk: "https://platform.twitter.com/widgets.js", init({ node }) { - // @ts-ignore + // @ts-expect-error window.twttr.widgets.load(node); } }; diff --git a/packages/app-page-builder-elements/src/renderers/form/FormRender/components/createTermsOfServiceComponent.tsx b/packages/app-page-builder-elements/src/renderers/form/FormRender/components/createTermsOfServiceComponent.tsx index eeae29913fe..56520f857de 100644 --- a/packages/app-page-builder-elements/src/renderers/form/FormRender/components/createTermsOfServiceComponent.tsx +++ b/packages/app-page-builder-elements/src/renderers/form/FormRender/components/createTermsOfServiceComponent.tsx @@ -29,7 +29,7 @@ const createTermsOfServiceComponent = ({ setTermsOfServiceAccepted }: CreateTermsOfServiceComponentArgs): TermsOfServiceComponent => // TODO @ts-refactor figure out how to type this - // @ts-ignore + // @ts-expect-error function TermsOfService(props: TermsOfServiceProps) { if (!termsOfServiceEnabled(formData)) { return null; diff --git a/packages/app-page-builder-elements/src/renderers/imagesList/imagesComponents/defaultImagesListComponent.tsx b/packages/app-page-builder-elements/src/renderers/imagesList/imagesComponents/defaultImagesListComponent.tsx index 2691ca37310..5259a3a9be5 100644 --- a/packages/app-page-builder-elements/src/renderers/imagesList/imagesComponents/defaultImagesListComponent.tsx +++ b/packages/app-page-builder-elements/src/renderers/imagesList/imagesComponents/defaultImagesListComponent.tsx @@ -4,7 +4,7 @@ import { ImagesListComponent } from "../types"; /** * Package react-columned does not have types. */ -// @ts-ignore +// @ts-expect-error import Columned from "react-columned"; import Lightbox from "react-images"; import styled from "@emotion/styled"; diff --git a/packages/app-page-builder-elements/src/renderers/paragraph.tsx b/packages/app-page-builder-elements/src/renderers/paragraph.tsx index 70079702660..f562b16c946 100644 --- a/packages/app-page-builder-elements/src/renderers/paragraph.tsx +++ b/packages/app-page-builder-elements/src/renderers/paragraph.tsx @@ -34,7 +34,7 @@ export const createParagraph = () => { // were cases where the received text was not just one `p` tag, but an array of `p` tags. // In that case, we still need a separate wrapper element. So, we're leaving this solution. if (__html.startsWith("; } diff --git a/packages/app-page-builder/src/admin/contexts/AdminPageBuilder/PageBlocks/BlocksGateway.ts b/packages/app-page-builder/src/admin/contexts/AdminPageBuilder/PageBlocks/BlocksGateway.ts index 55fa644dd3f..31b2851dcdb 100644 --- a/packages/app-page-builder/src/admin/contexts/AdminPageBuilder/PageBlocks/BlocksGateway.ts +++ b/packages/app-page-builder/src/admin/contexts/AdminPageBuilder/PageBlocks/BlocksGateway.ts @@ -1,5 +1,4 @@ import { ApolloClient } from "apollo-client"; - import { CREATE_PAGE_BLOCK, CreatePageBlockMutationResponse, @@ -23,6 +22,7 @@ import { CreatePageBlockInput, UpdatePageBlockInput } from "./BlockGatewayInterface"; +import { decompress } from "~/admin/components/useDecompress"; export class BlocksGateway implements BlockGatewayInterface { private client: ApolloClient; @@ -50,7 +50,7 @@ export class BlocksGateway implements BlockGatewayInterface { throw new Error(error?.message || "Could not fetch filters."); } - return data; + return data.map(block => this.decompressContent(block)); } async create(pageBlock: CreatePageBlockInput): Promise { @@ -74,7 +74,7 @@ export class BlocksGateway implements BlockGatewayInterface { throw new Error(error?.message || "Could not create filter."); } - return data; + return this.decompressContent(data); } async delete(id: string): Promise { @@ -105,7 +105,8 @@ export class BlocksGateway implements BlockGatewayInterface { GetPageBlockQueryVariables >({ query: GET_PAGE_BLOCK, - variables: { id } + variables: { id }, + fetchPolicy: "network-only" }); if (!response) { @@ -118,7 +119,7 @@ export class BlocksGateway implements BlockGatewayInterface { throw new Error(error?.message || `Could not fetch page block with id: ${id}`); } - return data; + return this.decompressContent(data); } async update({ id, ...pageBlock }: UpdatePageBlockInput) { @@ -147,4 +148,11 @@ export class BlocksGateway implements BlockGatewayInterface { throw new Error(error?.message || "Could not update filter."); } } + + private decompressContent(pageBlock: PbPageBlock) { + return { + ...pageBlock, + content: decompress(pageBlock.content) + }; + } } diff --git a/packages/app-page-builder/src/admin/contexts/AdminPageBuilder/PageBlocks/BlocksRepository.ts b/packages/app-page-builder/src/admin/contexts/AdminPageBuilder/PageBlocks/BlocksRepository.ts index 74d79b3fa8b..2e9f021edb9 100644 --- a/packages/app-page-builder/src/admin/contexts/AdminPageBuilder/PageBlocks/BlocksRepository.ts +++ b/packages/app-page-builder/src/admin/contexts/AdminPageBuilder/PageBlocks/BlocksRepository.ts @@ -3,7 +3,6 @@ import { plugins } from "@webiny/plugins"; import { Loading } from "./Loading"; import { BlockGatewayInterface } from "./BlockGatewayInterface"; import { PbEditorBlockPlugin, PbPageBlock } from "~/types"; -import { decompress } from "~/admin/components/useDecompress"; import { getDefaultBlockContent } from "./defaultBlockContent"; import { addElementId } from "~/editor/helpers"; import { createBlockPlugin } from "./createBlockPlugin"; @@ -34,7 +33,7 @@ export class BlocksRepository { } private async runWithLoading( - action: Promise, + action: () => Promise, loadingLabel?: string, successMessage?: string, failureMessage?: string @@ -58,7 +57,7 @@ export class BlocksRepository { return (this.listOperation = (async () => { const pageBlocks = await this.runWithLoading( - this.gateway.list(), + () => this.gateway.list(), "Loading page blocks" ); @@ -66,16 +65,14 @@ export class BlocksRepository { return []; } - const decompressedPageBlocks = await Promise.all( - pageBlocks.map(pageBlock => this.decompressPageBlock(pageBlock)) + const processedBlocks = await Promise.all( + pageBlocks.map(pageBlock => this.processBlockFromApi(pageBlock)) ); runInAction(() => { - this.pageBlocks = decompressedPageBlocks; + this.pageBlocks = processedBlocks; }); - this.pageBlocks.map(pageBlock => this.createBlockPlugin(pageBlock)); - return this.pageBlocks; })()); } @@ -87,31 +84,56 @@ export class BlocksRepository { return structuredClone(blockInCache); } - const pageBlock = await this.runWithLoading(this.gateway.getById(id)); + const pageBlock = await this.runWithLoading(async () => { + const block = await this.gateway.getById(id); + return this.processBlockFromApi(block); + }); + + runInAction(() => { + this.pageBlocks = [...this.pageBlocks, pageBlock]; + }); + + return structuredClone(pageBlock); + } + + async refetchById(id: string): Promise { + const pageBlock = await this.runWithLoading(async () => { + const block = await this.gateway.getById(id); + return this.processBlockFromApi(block); + }); + + runInAction(() => { + const blockIndex = this.pageBlocks.findIndex(pb => pb.id === id); + if (blockIndex > -1) { + this.pageBlocks = this.pageBlocks.splice(blockIndex, 1, pageBlock); + } else { + this.pageBlocks = [...this.pageBlocks, pageBlock]; + } + }); return structuredClone(pageBlock); } async createPageBlock(input: { name: string; category: string; content?: unknown }) { const pageBlock = await this.runWithLoading( - this.gateway.create({ - name: input.name, - blockCategory: input.category, - content: input.content ?? getDefaultBlockContent() - }), + () => { + return this.gateway.create({ + name: input.name, + blockCategory: input.category, + content: input.content ?? getDefaultBlockContent() + }); + }, "Creating page block", `Page block "${input.name}" was created successfully.` ); - const decompressed = this.decompressPageBlock(pageBlock); + const processedBlock = this.processBlockFromApi(pageBlock); runInAction(() => { - this.pageBlocks = [...this.pageBlocks, decompressed]; + this.pageBlocks = [...this.pageBlocks, processedBlock]; }); - this.createBlockPlugin(decompressed); - - return decompressed; + return processedBlock; } async updatePageBlock(pageBlock: { @@ -130,16 +152,18 @@ export class BlocksRepository { ...block, name: pageBlock.name ?? block.name, blockCategory: pageBlock.category ?? block.blockCategory, - content: pageBlock.content ?? block.content + content: addElementId(pageBlock.content ?? block.content) }; await this.runWithLoading( - this.gateway.update({ - id: updatePageBlock.id, - name: updatePageBlock.name, - content: updatePageBlock.content, - blockCategory: updatePageBlock.blockCategory - }), + () => { + return this.gateway.update({ + id: updatePageBlock.id, + name: updatePageBlock.name, + content: updatePageBlock.content, + blockCategory: updatePageBlock.blockCategory + }); + }, "Updating page block", `Filter "${updatePageBlock.name}" was updated successfully.` ); @@ -167,7 +191,7 @@ export class BlocksRepository { } await this.runWithLoading( - this.gateway.delete(id), + () => this.gateway.delete(id), "Deleting page block", `Filter "${block.name}" was deleted successfully.` ); @@ -179,11 +203,10 @@ export class BlocksRepository { this.removeBlockPlugin(block); } - private decompressPageBlock(pageBlock: PbPageBlock) { - return { - ...pageBlock, - content: addElementId(decompress(pageBlock.content)) - }; + private processBlockFromApi(pageBlock: PbPageBlock) { + const withElementIds = { ...pageBlock, content: addElementId(pageBlock.content) }; + this.createBlockPlugin(withElementIds); + return withElementIds; } /** diff --git a/packages/app-page-builder/src/admin/contexts/AdminPageBuilder/PageBlocks/Loading.ts b/packages/app-page-builder/src/admin/contexts/AdminPageBuilder/PageBlocks/Loading.ts index 250aea9cba5..06c33cc0804 100644 --- a/packages/app-page-builder/src/admin/contexts/AdminPageBuilder/PageBlocks/Loading.ts +++ b/packages/app-page-builder/src/admin/contexts/AdminPageBuilder/PageBlocks/Loading.ts @@ -48,14 +48,14 @@ export class Loading { } async runCallbackWithLoading( - callback: Promise, + callback: () => Promise, loadingLabel?: string, successMessage?: string, failureMessage?: string ): Promise { try { this.startLoading(loadingLabel); - const result = await callback; + const result = await callback(); this.stopLoadingWithSuccess(successMessage); return result; } catch (e) { diff --git a/packages/app-page-builder/src/admin/contexts/AdminPageBuilder/PageBlocks/usePageBlocks.ts b/packages/app-page-builder/src/admin/contexts/AdminPageBuilder/PageBlocks/usePageBlocks.ts index ac2a574c694..708de868b69 100644 --- a/packages/app-page-builder/src/admin/contexts/AdminPageBuilder/PageBlocks/usePageBlocks.ts +++ b/packages/app-page-builder/src/admin/contexts/AdminPageBuilder/PageBlocks/usePageBlocks.ts @@ -79,5 +79,10 @@ export function usePageBlocks() { [blocksRepository] ); - return { ...vm, listBlocks, getBlockById, createBlock, updateBlock, deleteBlock }; + const refetchBlock = useCallback( + (id: string) => blocksRepository.refetchById(id), + [blocksRepository] + ); + + return { ...vm, listBlocks, getBlockById, createBlock, updateBlock, deleteBlock, refetchBlock }; } diff --git a/packages/app-page-builder/src/admin/plugins/icons/index.tsx b/packages/app-page-builder/src/admin/plugins/icons/index.tsx index c9a963cfcf1..aed1e025933 100644 --- a/packages/app-page-builder/src/admin/plugins/icons/index.tsx +++ b/packages/app-page-builder/src/admin/plugins/icons/index.tsx @@ -34,15 +34,14 @@ const plugin: PbIconsPlugin = { /** * Ignoring TS errors. We know what we coded is good, but cannot get it to work with typescript. */ - // @ts-ignore + // @ts-expect-error Object.keys(definitions).forEach((pack: IconPrefix) => { const defs = definitions[pack]; - // @ts-ignore + // @ts-expect-error Object.keys(defs).forEach((icon: IconName) => { icons.push({ id: [pack, icon], name: icon, - // @ts-ignore svg: createSvg(defs[icon]) }); }); diff --git a/packages/app-page-builder/src/admin/utils/createElementPlugin.tsx b/packages/app-page-builder/src/admin/utils/createElementPlugin.tsx index 8b80062ef38..8b694b6d8c3 100644 --- a/packages/app-page-builder/src/admin/utils/createElementPlugin.tsx +++ b/packages/app-page-builder/src/admin/utils/createElementPlugin.tsx @@ -23,14 +23,12 @@ export default (el: PbEditorElement): void => { const plugin: PbEditorPageElementPlugin = { name, - // @ts-ignore title: el.name, type: "pb-editor-page-element", elementType: name, target: rootPlugin.target, toolbar: { title({ refresh }) { - // @ts-ignore return ; }, group: "pb-editor-element-group-saved", @@ -41,7 +39,7 @@ export default (el: PbEditorElement): void => { onCreate: OnCreateActions.SKIP, settings: rootPlugin ? rootPlugin.settings : [], - // @ts-ignore + // @ts-expect-error create() { return cloneDeep(el.content); }, diff --git a/packages/app-page-builder/src/admin/views/Categories/CategoriesDialog.tsx b/packages/app-page-builder/src/admin/views/Categories/CategoriesDialog.tsx index e5bf00ddfc9..7f4ccbe6686 100644 --- a/packages/app-page-builder/src/admin/views/Categories/CategoriesDialog.tsx +++ b/packages/app-page-builder/src/admin/views/Categories/CategoriesDialog.tsx @@ -72,7 +72,7 @@ const CategoriesDialog: React.FC<CategoriesDialogProps> = ({ key={item.slug} onClick={() => { onSelect(item); - // @ts-ignore + // @ts-expect-error onClose(); }} > diff --git a/packages/app-page-builder/src/admin/views/Menus/MenusForm/MenuItems/MenuItemRenderer.tsx b/packages/app-page-builder/src/admin/views/Menus/MenusForm/MenuItems/MenuItemRenderer.tsx index ee300768f8a..35608ec42a3 100644 --- a/packages/app-page-builder/src/admin/views/Menus/MenusForm/MenuItems/MenuItemRenderer.tsx +++ b/packages/app-page-builder/src/admin/views/Menus/MenusForm/MenuItems/MenuItemRenderer.tsx @@ -7,7 +7,7 @@ import React from "react"; * * Package react-sortable-tree does not have types */ -// @ts-ignore +// @ts-expect-error import { isDescendant } from "react-sortable-tree"; import classnames from "classnames"; import { plugins } from "@webiny/plugins"; diff --git a/packages/app-page-builder/src/admin/views/Menus/MenusForm/MenuItems/MenuItemsList.tsx b/packages/app-page-builder/src/admin/views/Menus/MenusForm/MenuItems/MenuItemsList.tsx index 4cce2e5faea..78776241e5c 100644 --- a/packages/app-page-builder/src/admin/views/Menus/MenusForm/MenuItems/MenuItemsList.tsx +++ b/packages/app-page-builder/src/admin/views/Menus/MenusForm/MenuItems/MenuItemsList.tsx @@ -3,7 +3,7 @@ import React from "react"; * * Package react-sortable-tree does not have types */ -// @ts-ignore +// @ts-expect-error import SortableTree from "react-sortable-tree"; import { plugins } from "@webiny/plugins"; import MenuItemRenderer from "./MenuItemRenderer"; diff --git a/packages/app-page-builder/src/admin/views/Pages/PageTemplatesDialog.tsx b/packages/app-page-builder/src/admin/views/Pages/PageTemplatesDialog.tsx index ce31658f51d..e01a20fa176 100644 --- a/packages/app-page-builder/src/admin/views/Pages/PageTemplatesDialog.tsx +++ b/packages/app-page-builder/src/admin/views/Pages/PageTemplatesDialog.tsx @@ -174,7 +174,7 @@ const PageTemplatesDialog = ({ onClose, onSelect, isLoading }: PageTemplatesDial </SearchInputWrapper> <ScrollList className={listStyle} - data-list={"pb-new-page-dialog-templates-list"} + data-testid={"pb-new-page-dialog-templates-list"} > {filteredPageTemplates.map(template => ( <ListItem diff --git a/packages/app-page-builder/src/admin/views/Pages/cache.ts b/packages/app-page-builder/src/admin/views/Pages/cache.ts index d31a2d0b3ad..887d3751d57 100644 --- a/packages/app-page-builder/src/admin/views/Pages/cache.ts +++ b/packages/app-page-builder/src/admin/views/Pages/cache.ts @@ -46,7 +46,7 @@ const modifyCacheForAllListPagesQuery = ( /** * Figure out correct type for cache object because DataProxy does not have data type on it. */ - // @ts-ignore + // @ts-expect-error const existingQueriesInCache = Object.keys(cache.data.data).filter( key => key.includes(".listPages") && !key.endsWith(".meta") ); diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/Title/Title.tsx b/packages/app-page-builder/src/blockEditor/config/editorBar/Title/Title.tsx index ef2238dbd07..e4500ecee40 100644 --- a/packages/app-page-builder/src/blockEditor/config/editorBar/Title/Title.tsx +++ b/packages/app-page-builder/src/blockEditor/config/editorBar/Title/Title.tsx @@ -54,7 +54,7 @@ const Title: React.FC = () => { const onKeyDown = useCallback( (e: SyntheticEvent<HTMLInputElement>) => { - // @ts-ignore + // @ts-expect-error switch (e.key) { case "Escape": e.preventDefault(); diff --git a/packages/app-page-builder/src/editor/components/Editor/DragPreview.tsx b/packages/app-page-builder/src/editor/components/Editor/DragPreview.tsx index 2b8e6aa7168..2b348195089 100644 --- a/packages/app-page-builder/src/editor/components/Editor/DragPreview.tsx +++ b/packages/app-page-builder/src/editor/components/Editor/DragPreview.tsx @@ -38,7 +38,7 @@ const DragPreview: React.FC = () => { const { isDragging, item } = useDragLayer((monitor: DragLayerMonitor) => { if (!subscribedToOffsetChange) { - // @ts-ignore + // @ts-expect-error monitor.subscribeToOffsetChange(onOffsetChange(monitor)); subscribedToOffsetChange = true; } diff --git a/packages/app-page-builder/src/editor/components/MediumEditor/index.ts b/packages/app-page-builder/src/editor/components/MediumEditor/index.ts index 3bf226cb6a3..34f289bea34 100644 --- a/packages/app-page-builder/src/editor/components/MediumEditor/index.ts +++ b/packages/app-page-builder/src/editor/components/MediumEditor/index.ts @@ -115,7 +115,7 @@ const ReactMediumEditor: React.FC<ReactMediumEditorProps> = ({ // Approach was taken from: https://github.com/yabwe/medium-editor/issues/850 editorRef.current?.selectElement(elementRef.current as HTMLElement); elementRef.current?.click(); - // @ts-ignore + // @ts-expect-error MediumEditor.selection.moveCursor(document, elementRef.current); }, 200); } diff --git a/packages/app-page-builder/src/editor/contexts/EditorPageElementsProvider/ElementControlsOverlay.tsx b/packages/app-page-builder/src/editor/contexts/EditorPageElementsProvider/ElementControlsOverlay.tsx index 7add53043c6..59ebcb76b99 100644 --- a/packages/app-page-builder/src/editor/contexts/EditorPageElementsProvider/ElementControlsOverlay.tsx +++ b/packages/app-page-builder/src/editor/contexts/EditorPageElementsProvider/ElementControlsOverlay.tsx @@ -59,7 +59,7 @@ const PbElementControlsOverlay = ({ <> {isActive && <ElementControlsOverlayBorders zIndex={zIndex} color={ACTIVE_COLOR} />} <pb-eco - // @ts-ignore Not supported by `React.HTMLProps<HTMLDivElement>`. + // @ts-expect-error Not supported by `React.HTMLProps<HTMLDivElement>`. class={className} onClick={onClick} onMouseEnter={onMouseEnter} diff --git a/packages/app-page-builder/src/editor/contexts/EditorPageElementsProvider/ElementControlsOverlay/ElementControlsOverlayBorders.tsx b/packages/app-page-builder/src/editor/contexts/EditorPageElementsProvider/ElementControlsOverlay/ElementControlsOverlayBorders.tsx index d88f78cc82b..8eda1b9d00e 100644 --- a/packages/app-page-builder/src/editor/contexts/EditorPageElementsProvider/ElementControlsOverlay/ElementControlsOverlayBorders.tsx +++ b/packages/app-page-builder/src/editor/contexts/EditorPageElementsProvider/ElementControlsOverlay/ElementControlsOverlayBorders.tsx @@ -19,7 +19,7 @@ interface PbElementControlsOverlayBorderProps { } const ElementControlsOverlayBorder = styled((props: PbElementControlsOverlayBorderProps) => { - // @ts-ignore Not supported by `React.HTMLProps<HTMLDivElement>`. + // @ts-expect-error Not supported by `React.HTMLProps<HTMLDivElement>`. return <pb-eco-border data-type={props.type} class={props.className} />; })((props: PbElementControlsOverlayBorderProps) => { const { placement, zIndex, color } = props; diff --git a/packages/app-page-builder/src/editor/contexts/EventActionHandlerProvider.tsx b/packages/app-page-builder/src/editor/contexts/EventActionHandlerProvider.tsx index 3cf44689f2d..17b4f15c049 100644 --- a/packages/app-page-builder/src/editor/contexts/EventActionHandlerProvider.tsx +++ b/packages/app-page-builder/src/editor/contexts/EventActionHandlerProvider.tsx @@ -457,7 +457,7 @@ export const EventActionHandlerProvider = makeComposable< for (const cb of callables) { const r = (await cb( - // @ts-ignore TODO: figure this out! + // @ts-expect-error TODO: figure this out! getCallableState({ ...initialState, ...results.state }), { client: apolloClient, @@ -478,7 +478,7 @@ export const EventActionHandlerProvider = makeComposable< for (const action of results.actions) { const r = await triggerEventAction( action, - // @ts-ignore TODO: figure this out! + // @ts-expect-error TODO: figure this out! getCallableState({ ...initialState, ...results.state }), initiator.concat([name]) ); diff --git a/packages/app-page-builder/src/editor/helpers.ts b/packages/app-page-builder/src/editor/helpers.ts index ea8bbbfd6c3..75134295f18 100644 --- a/packages/app-page-builder/src/editor/helpers.ts +++ b/packages/app-page-builder/src/editor/helpers.ts @@ -83,16 +83,16 @@ export const createElement: CreateElementCallable = ( * Used ts-ignore because TS is complaining about always overriding some properties */ return { - // @ts-ignore + // @ts-expect-error id: getNanoid(), - // @ts-ignore + // @ts-expect-error data: { settings: {} }, - // @ts-ignore + // @ts-expect-error elements: [], parent: parent ? parent.id : undefined, - // @ts-ignore + // @ts-expect-error type, ...addElementId(plugin.create(options, parent)) }; @@ -178,11 +178,11 @@ export const addElementId = (target: Omit<PbEditorElement, "id">): PbEditorEleme * Remove id from elements recursively */ export const removeElementId = (el: PbElement): PbElement => { - // @ts-ignore + // @ts-expect-error delete el.id; el.elements = el.elements.map(el => { - // @ts-ignore + // @ts-expect-error delete el.id; if (el.elements && el.elements.length) { el = removeElementId(el); @@ -202,11 +202,11 @@ export const createBlockElements = (name: string): PbEditorElement => { * Used ts-ignore because TS is complaining about always overriding some properties */ return { - // @ts-ignore + // @ts-expect-error id: getNanoid(), - // @ts-ignore + // @ts-expect-error data: {}, - // @ts-ignore + // @ts-expect-error elements: [], ...addElementId(plugin.create()) }; diff --git a/packages/app-page-builder/src/editor/hooks/useRefreshBlock.ts b/packages/app-page-builder/src/editor/hooks/useRefreshBlock.ts index dcf8764821d..cdcde66dd25 100644 --- a/packages/app-page-builder/src/editor/hooks/useRefreshBlock.ts +++ b/packages/app-page-builder/src/editor/hooks/useRefreshBlock.ts @@ -1,55 +1,45 @@ -import get from "lodash/get"; -import { useApolloClient } from "@apollo/react-hooks"; +import { useCallback, useState } from "react"; import { useUpdateElement } from "~/editor/hooks/useUpdateElement"; -import { GET_PAGE_BLOCK } from "~/admin/views/PageBlocks/graphql"; -import { ListPageBlocksQueryResponse } from "~/admin/views/PageBlocks/graphql"; +import { PbEditorElement } from "~/types"; import { addElementId } from "~/editor/helpers"; -import { PbPageBlock, PbEditorElement } from "~/types"; -import { useCallback } from "react"; +import { usePageBlocks } from "~/admin/contexts/AdminPageBuilder/PageBlocks/usePageBlocks"; export const useRefreshBlock = (block: PbEditorElement) => { const updateElement = useUpdateElement(); - const client = useApolloClient(); + const { refetchBlock } = usePageBlocks(); + const [loading, setLoading] = useState(false); - return useCallback(async () => { - if (!block?.id) { + const refreshBlock = useCallback(async () => { + if (!block?.id || !block.data.blockId) { return; } - await client - .query<ListPageBlocksQueryResponse>({ - query: GET_PAGE_BLOCK, - variables: { id: block.data.blockId }, - fetchPolicy: "network-only" - }) - .then(({ data }) => { - const pageBlockData = get( - data, - "pageBuilder.getPageBlock.data" - ) as unknown as PbPageBlock; + setLoading(true); + const pageBlock = await refetchBlock(block.data.blockId); - const blockDataVariables = pageBlockData?.content?.data?.variables || []; - const variables = blockDataVariables.map((blockDataVariable: any) => { - const value = - block.data?.variables?.find( - (variable: any) => variable.id === blockDataVariable.id - )?.value || blockDataVariable.value; + const blockDataVariables = pageBlock.content.data.variables || []; + const variables = blockDataVariables.map((blockDataVariable: any) => { + const value = + block.data?.variables?.find((variable: any) => variable.id === blockDataVariable.id) + ?.value || blockDataVariable.value; - return { - ...blockDataVariable, - value - }; - }); + return { + ...blockDataVariable, + value + }; + }); - updateElement({ - ...addElementId(pageBlockData.content), - id: block.id, - data: { - ...block?.data, - ...pageBlockData?.content?.data, - variables - } - }); - }); - }, [block, client, updateElement]); + updateElement({ + ...addElementId(pageBlock.content), + id: block.id, + data: { + ...block?.data, + ...pageBlock?.content?.data, + variables + } + }); + setLoading(false); + }, [block, updateElement]); + + return { loading, refreshBlock }; }; diff --git a/packages/app-page-builder/src/editor/plugins/elementSettings/components/Action.tsx b/packages/app-page-builder/src/editor/plugins/elementSettings/components/Action.tsx index 1a4dd65a4b9..4a27953520e 100644 --- a/packages/app-page-builder/src/editor/plugins/elementSettings/components/Action.tsx +++ b/packages/app-page-builder/src/editor/plugins/elementSettings/components/Action.tsx @@ -17,6 +17,7 @@ const activeStyle = css({ }); interface ActionProps { + disabled?: boolean; plugin?: string; icon?: ReactElement; tooltip?: string; @@ -32,6 +33,7 @@ const Action: React.FC<ActionProps> = ({ tooltip, onClick, shortcut = [], + disabled = false, ...props }) => { const eventActionHandler = useEventActionHandler(); @@ -82,6 +84,7 @@ const Action: React.FC<ActionProps> = ({ {...(isPluginActive ? { visible: false } : {})} > <IconButton + disabled={disabled} icon={icon} onClick={clickHandler} className={isPluginActive ? activeStyle : ""} diff --git a/packages/app-page-builder/src/editor/plugins/elementSettings/variable/MultipleImageVariableInput.tsx b/packages/app-page-builder/src/editor/plugins/elementSettings/variable/MultipleImageVariableInput.tsx index 61b9352a120..80decb7692a 100644 --- a/packages/app-page-builder/src/editor/plugins/elementSettings/variable/MultipleImageVariableInput.tsx +++ b/packages/app-page-builder/src/editor/plugins/elementSettings/variable/MultipleImageVariableInput.tsx @@ -3,7 +3,7 @@ import { css } from "emotion"; /** * Package react-sortable does not have types. */ -// @ts-ignore +// @ts-expect-error import { sortable } from "react-sortable"; import cloneDeep from "lodash/cloneDeep"; import { FileManager } from "@webiny/app-admin/components"; diff --git a/packages/app-page-builder/src/editor/plugins/elements/accordion/index.tsx b/packages/app-page-builder/src/editor/plugins/elements/accordion/index.tsx index 627c586f0e1..260184e0839 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/accordion/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/accordion/index.tsx @@ -49,7 +49,7 @@ export default (args: PbEditorElementPluginArgs = {}): PbEditorPageElementPlugin type: "pb-editor-page-element", name: `pb-editor-page-element-${elementType}`, elementType: elementType, - // @ts-ignore + // @ts-expect-error toolbar: typeof args.toolbar === "function" ? args.toolbar(defaultToolbar) : defaultToolbar, settings: typeof args.settings === "function" ? args.settings(defaultSettings) : defaultSettings, diff --git a/packages/app-page-builder/src/editor/plugins/elements/carousel/index.tsx b/packages/app-page-builder/src/editor/plugins/elements/carousel/index.tsx index 302c2199218..5c321138106 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/carousel/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/carousel/index.tsx @@ -50,7 +50,7 @@ export default (args: PbEditorElementPluginArgs = {}): PbEditorPageElementPlugin type: "pb-editor-page-element", name: `pb-editor-page-element-${elementType}`, elementType: elementType, - // @ts-ignore + // @ts-expect-error toolbar: typeof args.toolbar === "function" ? args.toolbar(defaultToolbar) : defaultToolbar, settings: typeof args.settings === "function" ? args.settings(defaultSettings) : defaultSettings, diff --git a/packages/app-page-builder/src/editor/plugins/elements/code/codesandbox/index.tsx b/packages/app-page-builder/src/editor/plugins/elements/code/codesandbox/index.tsx index 7ab3905e51a..02b706b199d 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/code/codesandbox/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/code/codesandbox/index.tsx @@ -46,7 +46,7 @@ export default () => [ } }, render: params => { - // @ts-ignore No need to worry about different element.elements type. + // @ts-expect-error No need to worry about different element.elements type. return <PeCodesandbox {...params} />; } }), diff --git a/packages/app-page-builder/src/editor/plugins/elements/embeds/soundcloud/index.tsx b/packages/app-page-builder/src/editor/plugins/elements/embeds/soundcloud/index.tsx index a8be60dae90..d4316b6e614 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/embeds/soundcloud/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/embeds/soundcloud/index.tsx @@ -26,7 +26,7 @@ const PreviewBox = styled("div")({ }); const render: EmbedPluginConfigRenderCallable = props => ( - // @ts-ignore Sync `elements` property type. + // @ts-expect-error Sync `elements` property type. <PeSoundcloud {...props} /> ); @@ -50,7 +50,7 @@ export default (args: PbEditorElementPluginArgs = {}) => { /** * TODO @ts-refactor @ashutosh */ - // @ts-ignore + // @ts-expect-error toolbar: typeof args.toolbar === "function" ? args.toolbar(defaultToolbar) : defaultToolbar, settings: args.settings, diff --git a/packages/app-page-builder/src/editor/plugins/elements/embeds/vimeo/index.tsx b/packages/app-page-builder/src/editor/plugins/elements/embeds/vimeo/index.tsx index 765380c6cf2..82d0e46b61e 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/embeds/vimeo/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/embeds/vimeo/index.tsx @@ -27,7 +27,7 @@ const PreviewBox = styled("div")({ }); const render: EmbedPluginConfigRenderCallable = props => ( - // @ts-ignore Sync `elements` property type. + // @ts-expect-error Sync `elements` property type. <PeVimeo {...props} /> ); @@ -51,7 +51,7 @@ export default (args: PbEditorElementPluginArgs = {}) => { /** * TODO @ts-refactor @ashutosh */ - // @ts-ignore + // @ts-expect-error toolbar: typeof args.toolbar === "function" ? args.toolbar(defaultToolbar) : defaultToolbar, create: args.create, diff --git a/packages/app-page-builder/src/editor/plugins/elements/embeds/youtube/index.tsx b/packages/app-page-builder/src/editor/plugins/elements/embeds/youtube/index.tsx index cd1f48f2aec..7ade39204b5 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/embeds/youtube/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/embeds/youtube/index.tsx @@ -29,7 +29,7 @@ const ButtonContainer = styled("div")({ }); const render: EmbedPluginConfigRenderCallable = props => ( - // @ts-ignore Sync `elements` property type. + // @ts-expect-error Sync `elements` property type. <PElementsYouTube {...props} /> ); @@ -53,7 +53,7 @@ export default (args: PbEditorElementPluginArgs = {}) => { /** * TODO @ts-refactor @ashutosh */ - // @ts-ignore + // @ts-expect-error toolbar: typeof args.toolbar === "function" ? args.toolbar(defaultToolbar) : defaultToolbar, create: args.create, diff --git a/packages/app-page-builder/src/editor/plugins/elements/grid/PeGrid.tsx b/packages/app-page-builder/src/editor/plugins/elements/grid/PeGrid.tsx index 611a820170d..8ad66160d78 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/grid/PeGrid.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/grid/PeGrid.tsx @@ -13,6 +13,10 @@ const PeGrid = createRenderer( elementWithChildrenByIdSelector(element.id) ) as Element; + if (!elementWithChildren) { + return null; + } + return <Elements element={elementWithChildren} />; }, { diff --git a/packages/app-page-builder/src/editor/plugins/elements/grid/index.tsx b/packages/app-page-builder/src/editor/plugins/elements/grid/index.tsx index a9e380e3f3c..1ff891c4c70 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/grid/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/grid/index.tsx @@ -70,7 +70,7 @@ export default (args: PbEditorElementPluginArgs = {}): PbEditorPageElementPlugin /** * TODO @ts-refactor @ashutosh */ - // @ts-ignore + // @ts-expect-error toolbar: typeof args.toolbar === "function" ? args.toolbar(defaultToolbar) : defaultToolbar, settings: typeof args.settings === "function" ? args.settings(defaultSettings) : defaultSettings, diff --git a/packages/app-page-builder/src/editor/plugins/elements/heading/index.tsx b/packages/app-page-builder/src/editor/plugins/elements/heading/index.tsx index 586af43dece..f0a2ca247c4 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/heading/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/heading/index.tsx @@ -44,7 +44,7 @@ export default (args: PbEditorTextElementPluginsArgs = {}): PbEditorPageElementP * TODO @ts-refactor @ashutosh * Please check this. args.toolbar() and defaultToolbar are totally different types */ - // @ts-ignore + // @ts-expect-error toolbar: typeof args.toolbar === "function" ? args.toolbar(defaultToolbar) : defaultToolbar, settings: typeof args.settings === "function" ? args.settings(defaultSettings) : defaultSettings, diff --git a/packages/app-page-builder/src/editor/plugins/elements/imagesList/ImagesListImagesSettings.tsx b/packages/app-page-builder/src/editor/plugins/elements/imagesList/ImagesListImagesSettings.tsx index ddc82437981..cd5ee3bf5be 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/imagesList/ImagesListImagesSettings.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/imagesList/ImagesListImagesSettings.tsx @@ -3,7 +3,7 @@ import { css } from "emotion"; /** * Package react-sortable does not have types. */ -// @ts-ignore +// @ts-expect-error import { sortable } from "react-sortable"; import { FileManager } from "@webiny/app-admin/components"; import { Grid, Cell } from "@webiny/ui/Grid"; diff --git a/packages/app-page-builder/src/editor/plugins/elements/paragraph/PeParagraph.tsx b/packages/app-page-builder/src/editor/plugins/elements/paragraph/PeParagraph.tsx index b965252b359..b39fb2a700a 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/paragraph/PeParagraph.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/paragraph/PeParagraph.tsx @@ -38,7 +38,7 @@ const PeParagraph = createRenderer(() => { // If the text already contains `p` tags (happens when c/p-ing text into the editor), // we don't want to wrap it with another pair of `p` tag. if (__html.startsWith("<p")) { - // @ts-ignore We don't need type-checking here. + // @ts-expect-error We don't need type-checking here. return <p-wrap dangerouslySetInnerHTML={{ __html }} />; } diff --git a/packages/app-page-builder/src/editor/plugins/elements/paragraph/index.tsx b/packages/app-page-builder/src/editor/plugins/elements/paragraph/index.tsx index 391e06fffc8..479aa4f2cad 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/paragraph/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/paragraph/index.tsx @@ -41,7 +41,7 @@ export default (args: PbEditorTextElementPluginsArgs = {}): PbEditorPageElementP * TODO @ts-refactor @ashutosh * Completely different types between method result and variable */ - // @ts-ignore + // @ts-expect-error toolbar: typeof args.toolbar === "function" ? args.toolbar(defaultToolbar) : defaultToolbar, settings: typeof args.settings === "function" ? args.settings(defaultSettings) : defaultSettings, diff --git a/packages/app-page-builder/src/editor/plugins/elements/social/instagram/index.tsx b/packages/app-page-builder/src/editor/plugins/elements/social/instagram/index.tsx index 9a78df9b769..c4bebf7dd50 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/social/instagram/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/social/instagram/index.tsx @@ -42,7 +42,7 @@ export default () => [ global: "instgrm" as keyof Window, sdk: "https://www.instagram.com/embed.js", init({ node }) { - // @ts-ignore + // @ts-expect-error window.instgrm.Embeds.process(node.firstChild); } }, diff --git a/packages/app-page-builder/src/editor/plugins/elements/social/pinterest/index.tsx b/packages/app-page-builder/src/editor/plugins/elements/social/pinterest/index.tsx index c259a4ca510..1c195ff8d44 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/social/pinterest/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/social/pinterest/index.tsx @@ -48,13 +48,13 @@ export default (args: PbEditorElementPluginArgs = {}) => { * TODO @ts-refactor @ashutosh * Completely different types between method result and variable */ - // @ts-ignore + // @ts-expect-error toolbar: typeof args.toolbar === "function" ? args.toolbar(defaultToolbar) : defaultToolbar, create: args.create, settings: args.settings, render(props) { - // @ts-ignore No need to worry about different `element.elements` type. + // @ts-expect-error No need to worry about different `element.elements` type. return <PePinterest {...props} />; } }), diff --git a/packages/app-page-builder/src/editor/plugins/elements/social/twitter/index.tsx b/packages/app-page-builder/src/editor/plugins/elements/social/twitter/index.tsx index e1c311c53b1..564b67778a4 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/social/twitter/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/social/twitter/index.tsx @@ -51,7 +51,7 @@ export default (args: PbEditorElementPluginArgs = {}) => { * TODO @ts-refactor @ashutosh * Completely different types between method result and variable */ - // @ts-ignore + // @ts-expect-error toolbar: typeof args.toolbar === "function" ? args.toolbar(defaultToolbar) : defaultToolbar, create: args.create, @@ -67,7 +67,7 @@ export default (args: PbEditorElementPluginArgs = {}) => { return <img style={{ width, height }} src={placeholder} alt={"Tweet"} />; }, render(props) { - // @ts-ignore No need to worry about different element.elements type. + // @ts-expect-error No need to worry about different element.elements type. return <PeTwitter {...props} />; } }), diff --git a/packages/app-page-builder/src/editor/plugins/elements/tabs/index.tsx b/packages/app-page-builder/src/editor/plugins/elements/tabs/index.tsx index 4e7c3ec4e0e..7a22ead62b3 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/tabs/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/tabs/index.tsx @@ -49,7 +49,7 @@ export default (args: PbEditorElementPluginArgs = {}): PbEditorPageElementPlugin type: "pb-editor-page-element", name: `pb-editor-page-element-${elementType}`, elementType: elementType, - // @ts-ignore + // @ts-expect-error toolbar: typeof args.toolbar === "function" ? args.toolbar(defaultToolbar) : defaultToolbar, settings: typeof args.settings === "function" ? args.settings(defaultSettings) : defaultSettings, diff --git a/packages/app-page-builder/src/modules/WebsiteSettings/usePbWebsiteSettings.ts b/packages/app-page-builder/src/modules/WebsiteSettings/usePbWebsiteSettings.ts index cf847ffca04..057f54e4ee8 100644 --- a/packages/app-page-builder/src/modules/WebsiteSettings/usePbWebsiteSettings.ts +++ b/packages/app-page-builder/src/modules/WebsiteSettings/usePbWebsiteSettings.ts @@ -3,10 +3,6 @@ import get from "lodash/get"; import set from "lodash/set"; import { useMutation, useQuery } from "@apollo/react-hooks"; import { useSnackbar } from "@webiny/app-admin"; -/** - * Package @webiny/telemetry is missing types. - */ -// @ts-ignore import { sendEvent, setProperties } from "@webiny/telemetry/react"; import { GET_SETTINGS, diff --git a/packages/app-page-builder/src/pageEditor/config/ElementSettingsTabContentPlugin.tsx b/packages/app-page-builder/src/pageEditor/config/ElementSettingsTabContentPlugin.tsx index c27766f21aa..e439d0782c0 100644 --- a/packages/app-page-builder/src/pageEditor/config/ElementSettingsTabContentPlugin.tsx +++ b/packages/app-page-builder/src/pageEditor/config/ElementSettingsTabContentPlugin.tsx @@ -18,7 +18,7 @@ export const ElementSettingsTabContentPlugin = createComponentPlugin( const [element] = useActiveElement(); const elementSettings = useElementSettings(); const [isTemplateMode] = useTemplateMode(); - const refreshBlock = useRefreshBlock(element as PbEditorElement); + const { refreshBlock, loading } = useRefreshBlock(element as PbEditorElement); if (isTemplateMode) { return <VariableSettings />; @@ -53,7 +53,8 @@ export const ElementSettingsTabContentPlugin = createComponentPlugin( } /> <Action - tooltip={"Refresh block"} + disabled={loading} + tooltip={loading ? "Refreshing..." : "Refresh block"} onClick={refreshBlock} icon={<RefreshIcon />} /> diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/Title/Title.tsx b/packages/app-page-builder/src/pageEditor/config/editorBar/Title/Title.tsx index b0afe49725a..d5ba188ad69 100644 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/Title/Title.tsx +++ b/packages/app-page-builder/src/pageEditor/config/editorBar/Title/Title.tsx @@ -77,7 +77,7 @@ const Title: React.FC = () => { const onKeyDown = useCallback( (e: SyntheticEvent<HTMLInputElement>) => { - // @ts-ignore + // @ts-expect-error switch (e.key) { case "Escape": e.preventDefault(); diff --git a/packages/app-page-builder/src/pageEditor/helpers.ts b/packages/app-page-builder/src/pageEditor/helpers.ts index 63f8013df64..93ff027a8ad 100644 --- a/packages/app-page-builder/src/pageEditor/helpers.ts +++ b/packages/app-page-builder/src/pageEditor/helpers.ts @@ -19,12 +19,11 @@ export const createBlockReference = (name: string): PbEditorElement => { const blockElement = addElementId(plugin.create()); return { - // @ts-ignore + // @ts-expect-error id: getNanoid(), - // @ts-ignore + // @ts-expect-error elements: [], ...blockElement, - // @ts-ignore data: { ...blockElement.data, blockId: plugin.id } }; }; @@ -53,7 +52,6 @@ export const removeElementVariableIds = ( // we need to replace element value with the one from variables before removing variableId el = elementVariablePlugin?.setElementValue(el, elementVariables) || el; - // @ts-ignore delete el.data?.variableId; } if (el.elements && el.elements.length) { diff --git a/packages/app-page-builder/src/render/components/OEmbed.tsx b/packages/app-page-builder/src/render/components/OEmbed.tsx index cb7ed0db08b..311171d703e 100644 --- a/packages/app-page-builder/src/render/components/OEmbed.tsx +++ b/packages/app-page-builder/src/render/components/OEmbed.tsx @@ -19,7 +19,7 @@ function appendSDK(props: OEmbedProps): Promise<void> { const { sdk, global, element } = props; const { url } = get(element, "data.source") || {}; // TODO @ts-refactor figure out better type for global - // @ts-ignore + // @ts-expect-error if (!sdk || !url || window[global]) { return Promise.resolve(); } diff --git a/packages/app-page-builder/src/render/plugins/elements/embeds/instagram/index.tsx b/packages/app-page-builder/src/render/plugins/elements/embeds/instagram/index.tsx index e95127d582e..ce3e56a0c5e 100644 --- a/packages/app-page-builder/src/render/plugins/elements/embeds/instagram/index.tsx +++ b/packages/app-page-builder/src/render/plugins/elements/embeds/instagram/index.tsx @@ -7,7 +7,7 @@ const oembed: Partial<OEmbedProps> = { sdk: "https://www.instagram.com/embed.js", init({ node }) { // TODO @ts-refactor any way to use key on window? - // @ts-ignore + // @ts-expect-error window.instgrm.Embeds.process(node.firstChild); } }; diff --git a/packages/app-page-builder/src/render/plugins/elements/imagesList/components/Slider.tsx b/packages/app-page-builder/src/render/plugins/elements/imagesList/components/Slider.tsx deleted file mode 100644 index 91b6b164760..00000000000 --- a/packages/app-page-builder/src/render/plugins/elements/imagesList/components/Slider.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * There is no slick slider imported anywhere. How? - */ -// TODO @ts-refactor -// TODO remove -// @ts-nocheck -/* eslint-disable */ - -import * as React from "react"; -import { Image } from "react-images"; - -interface SliderProps { - data: Image[]; -} -const Slider: React.FC<SliderProps> = ({ data }) => { - if (Array.isArray(data)) { - const settings = { - dots: true, - infinite: true, - speed: 500, - slidesToShow: 1, - slidesToScroll: 1 - }; - - return ( - <div> - {Array.isArray(data) && ( - <SlickSlider {...settings}> - {data.map((item, index) => ( - <div key={item.src + index}> - <img style={{ width: "100%" }} src={item.src} /> - </div> - ))} - </SlickSlider> - )} - </div> - ); - } - return null; -}; - -export default Slider; diff --git a/packages/app-page-builder/src/templateEditor/config/ElementSettingsTabContentPlugin.tsx b/packages/app-page-builder/src/templateEditor/config/ElementSettingsTabContentPlugin.tsx index 188e53bcb27..cb394e38054 100644 --- a/packages/app-page-builder/src/templateEditor/config/ElementSettingsTabContentPlugin.tsx +++ b/packages/app-page-builder/src/templateEditor/config/ElementSettingsTabContentPlugin.tsx @@ -23,7 +23,7 @@ export const ElementSettingsTabContentPlugin = createComponentPlugin( return function SettingsTabContent({ children, ...props }) { const [element] = useActiveElement(); const elementSettings = useElementSettings(); - const refreshBlock = useRefreshBlock(element as PbEditorElement); + const { refreshBlock, loading } = useRefreshBlock(element as PbEditorElement); const canHaveVariable = element && variablePlugins.some(variablePlugin => variablePlugin.elementType === element.type); @@ -58,7 +58,8 @@ export const ElementSettingsTabContentPlugin = createComponentPlugin( } /> <Action - tooltip={"Refresh block"} + disabled={loading} + tooltip={loading ? "Refreshing..." : "Refresh block"} onClick={refreshBlock} icon={<RefreshIcon />} /> diff --git a/packages/app-page-builder/src/templateEditor/config/editorBar/Title/Title.tsx b/packages/app-page-builder/src/templateEditor/config/editorBar/Title/Title.tsx index 5f3b7471e40..4218ae20cac 100644 --- a/packages/app-page-builder/src/templateEditor/config/editorBar/Title/Title.tsx +++ b/packages/app-page-builder/src/templateEditor/config/editorBar/Title/Title.tsx @@ -55,7 +55,7 @@ const Title: React.FC = () => { const onKeyDown = useCallback( (e: SyntheticEvent) => { - // @ts-ignore + // @ts-expect-error switch (e.key) { case "Escape": e.preventDefault(); diff --git a/packages/app-security-access-management/src/index.tsx b/packages/app-security-access-management/src/index.tsx index b8789fe18c6..7ada0c3e9c9 100644 --- a/packages/app-security-access-management/src/index.tsx +++ b/packages/app-security-access-management/src/index.tsx @@ -1,6 +1,6 @@ import React, { memo } from "react"; import { plugins } from "@webiny/plugins"; -import { Layout, Plugins, AddMenu, AddRoute, useWcp } from "@webiny/app-admin"; +import { AddMenu, AddRoute, Layout, Plugins, useWcp } from "@webiny/app-admin"; import { HasPermission } from "@webiny/app-security"; import { Permission } from "~/plugins/constants"; import { Groups } from "~/ui/views/Groups"; @@ -12,7 +12,6 @@ import accessManagementPlugins from "./plugins"; * TODO @ts-refactor * Find out why is there empty default export */ -// @ts-ignore export default () => []; export const AccessManagementExtension = () => { diff --git a/packages/app-website/src/utils/createApolloClient.ts b/packages/app-website/src/utils/createApolloClient.ts index 2f644152877..0ff2b10d94d 100644 --- a/packages/app-website/src/utils/createApolloClient.ts +++ b/packages/app-website/src/utils/createApolloClient.ts @@ -38,14 +38,14 @@ export const createApolloClient = () => { } }); - // @ts-ignore + // @ts-expect-error cache.restore("__APOLLO_STATE__" in window ? window.__APOLLO_STATE__ : {}); const uri = process.env.REACT_APP_GRAPHQL_API_URL; const link = ApolloLink.from([new ApolloDynamicLink(), new BatchHttpLink({ uri })]); window.getApolloState = () => { - // @ts-ignore `cache.data` is marked as private in the `apollo-cache-inmemory` package. + // @ts-expect-error `cache.data` is marked as private in the `apollo-cache-inmemory` package. return cache?.data?.data; }; diff --git a/packages/app/src/apollo-client/InMemoryCache.ts b/packages/app/src/apollo-client/InMemoryCache.ts index 9b2f0b33a22..7d8f444fbee 100644 --- a/packages/app/src/apollo-client/InMemoryCache.ts +++ b/packages/app/src/apollo-client/InMemoryCache.ts @@ -16,7 +16,7 @@ export class InMemoryCache extends BaseInMemoryCache { } public override transformDocument(document: DocumentNode): DocumentNode { - // @ts-ignore + // @ts-expect-error const operationName = document.definitions[0].name.value; for (const pl of this.transformPlugins) { diff --git a/packages/app/src/plugins/index.tsx b/packages/app/src/plugins/index.tsx index 7c081de0a88..1e42001ddf8 100644 --- a/packages/app/src/plugins/index.tsx +++ b/packages/app/src/plugins/index.tsx @@ -74,7 +74,7 @@ export const renderPlugins: RenderPlugins = (type, params = {}, options = {}) => /** * TODO @ts-refactor Problem with possibility of a different subtype. */ - // @ts-ignore + // @ts-expect-error return filter(pl); }) /** diff --git a/packages/cli-plugin-scaffold-ci/src/githubActions/index.ts b/packages/cli-plugin-scaffold-ci/src/githubActions/index.ts index 501f1360b31..f7a763d26d6 100644 --- a/packages/cli-plugin-scaffold-ci/src/githubActions/index.ts +++ b/packages/cli-plugin-scaffold-ci/src/githubActions/index.ts @@ -258,7 +258,7 @@ const plugin: CliPluginsScaffoldCi<GithubActionsInput> = { /** * TODO @ts-refactor try to get the heads and tails of this. */ - // @ts-ignore + // @ts-expect-error repo = await octokit.rest.repos .get({ repo: existingRepo.name, diff --git a/packages/cli-plugin-scaffold-full-stack-app/templates/essentials/code/webiny.config.ts b/packages/cli-plugin-scaffold-full-stack-app/templates/essentials/code/webiny.config.ts index 862484e3980..9c467176b9c 100644 --- a/packages/cli-plugin-scaffold-full-stack-app/templates/essentials/code/webiny.config.ts +++ b/packages/cli-plugin-scaffold-full-stack-app/templates/essentials/code/webiny.config.ts @@ -22,7 +22,7 @@ const NO_ENV_MESSAGE = `Please specify the environment via the "--env" argument, */ export default { commands: { - // @ts-ignore + // @ts-expect-error async watch(options) { invariant(options.env, NO_ENV_MESSAGE); Object.assign( @@ -41,7 +41,7 @@ export default { const watch = createWatchApp({ cwd: __dirname }); await watch(options); }, - // @ts-ignore + // @ts-expect-error async build(options) { invariant(options.env, NO_ENV_MESSAGE); Object.assign( diff --git a/packages/cli-plugin-scaffold-graphql-api/src/index.ts b/packages/cli-plugin-scaffold-graphql-api/src/index.ts index cf284b5c405..473d56b951a 100644 --- a/packages/cli-plugin-scaffold-graphql-api/src/index.ts +++ b/packages/cli-plugin-scaffold-graphql-api/src/index.ts @@ -42,7 +42,7 @@ export const deployGraphQLAPI = (stack: string, env: string, inputs: unknown) => * * packages/cli-plugin-scaffold-full-stack-app/src/index.ts:239 * * packages/cli-plugin-scaffold-graphql-api/src/index.ts:345 */ - // @ts-ignore + // @ts-expect-error Boolean(inputs.debug) ? "true" : "false" ], { diff --git a/packages/cli-plugin-scaffold-graphql-api/template/code/graphql/src/plugins/scaffolds/index.ts b/packages/cli-plugin-scaffold-graphql-api/template/code/graphql/src/plugins/scaffolds/index.ts index d199ea25576..c0ad1bab587 100644 --- a/packages/cli-plugin-scaffold-graphql-api/template/code/graphql/src/plugins/scaffolds/index.ts +++ b/packages/cli-plugin-scaffold-graphql-api/template/code/graphql/src/plugins/scaffolds/index.ts @@ -1,4 +1,2 @@ // This file is automatically updated via various scaffolding utilities. - -// @ts-ignore export default () => []; diff --git a/packages/cli-plugin-scaffold-react-component/src/index.ts b/packages/cli-plugin-scaffold-react-component/src/index.ts index 2553c0a1c83..93ac54b3864 100644 --- a/packages/cli-plugin-scaffold-react-component/src/index.ts +++ b/packages/cli-plugin-scaffold-react-component/src/index.ts @@ -20,7 +20,7 @@ import validateNpmPackageName from "validate-npm-package-name"; /** * TODO: rewrite cli into typescript */ -// @ts-ignore +// @ts-expect-error import { getProject } from "@webiny/cli/utils"; const ncp = util.promisify(ncpBase.ncp); diff --git a/packages/commodo/src/compose.ts b/packages/commodo/src/compose.ts index 1dc335b16f0..eac8f05d25e 100644 --- a/packages/commodo/src/compose.ts +++ b/packages/commodo/src/compose.ts @@ -1,5 +1,5 @@ /** * We need to ignore here because ramda TS produces errors. */ -// @ts-ignore +// @ts-expect-error export { default as compose } from "ramda/src/compose"; diff --git a/packages/commodo/src/fields-date.ts b/packages/commodo/src/fields-date.ts index ceb72a65103..02ba98a147b 100644 --- a/packages/commodo/src/fields-date.ts +++ b/packages/commodo/src/fields-date.ts @@ -1,5 +1,5 @@ /** * We need to ignore here because commodo-fields-date do not have types. */ -// @ts-ignore +// @ts-expect-error export * from "commodo-fields-date"; diff --git a/packages/commodo/src/fields-float.ts b/packages/commodo/src/fields-float.ts index 2f8675a077f..149281fa412 100644 --- a/packages/commodo/src/fields-float.ts +++ b/packages/commodo/src/fields-float.ts @@ -1,5 +1,5 @@ /** * We need to ignore here because commodo-fields-float do not have types. */ -// @ts-ignore +// @ts-expect-error export * from "commodo-fields-float"; diff --git a/packages/commodo/src/fields-int.ts b/packages/commodo/src/fields-int.ts index b21c0c19d11..00c83ff72bc 100644 --- a/packages/commodo/src/fields-int.ts +++ b/packages/commodo/src/fields-int.ts @@ -1,5 +1,5 @@ /** * We need to ignore here because commodo-fields-int do not have types. */ -// @ts-ignore +// @ts-expect-error export * from "commodo-fields-int"; diff --git a/packages/commodo/src/fields-object.ts b/packages/commodo/src/fields-object.ts index efba7383ff7..174f6046920 100644 --- a/packages/commodo/src/fields-object.ts +++ b/packages/commodo/src/fields-object.ts @@ -1,5 +1,5 @@ /** * We need to ignore here because commodo-fields-object do not have types. */ -// @ts-ignore +// @ts-expect-error export * from "commodo-fields-object"; diff --git a/packages/commodo/src/fields.ts b/packages/commodo/src/fields.ts index 057c4648ad2..9121837b129 100644 --- a/packages/commodo/src/fields.ts +++ b/packages/commodo/src/fields.ts @@ -1,5 +1,5 @@ /** * We need to ignore here because @commodo/fields do not have types. */ -// @ts-ignore +// @ts-expect-error export * from "@commodo/fields"; diff --git a/packages/commodo/src/hooks.ts b/packages/commodo/src/hooks.ts index bb23c1f8e51..dba464d735e 100644 --- a/packages/commodo/src/hooks.ts +++ b/packages/commodo/src/hooks.ts @@ -1,5 +1,5 @@ /** * We need to ignore here because @commodo/hooks do not have types. */ -// @ts-ignore +// @ts-expect-error export * from "@commodo/hooks"; diff --git a/packages/commodo/src/name.ts b/packages/commodo/src/name.ts index 365d6f5ae57..901ebbda61e 100644 --- a/packages/commodo/src/name.ts +++ b/packages/commodo/src/name.ts @@ -1,5 +1,5 @@ /** * We need to ignore here because ramda TS produces errors. */ -// @ts-ignore +// @ts-expect-error export * from "@commodo/name"; diff --git a/packages/commodo/src/pipe.ts b/packages/commodo/src/pipe.ts index 222656e34b4..e198306a94c 100644 --- a/packages/commodo/src/pipe.ts +++ b/packages/commodo/src/pipe.ts @@ -1,5 +1,5 @@ /** * We need to ignore here because ramda TS produces errors. */ -// @ts-ignore +// @ts-expect-error export { default as pipe } from "ramda/src/pipe"; diff --git a/packages/commodo/src/repropose.ts b/packages/commodo/src/repropose.ts index 960193269fc..8fd8cf4e2ad 100644 --- a/packages/commodo/src/repropose.ts +++ b/packages/commodo/src/repropose.ts @@ -1,5 +1,5 @@ /** * We need to ignore here because repropose do not have types. */ -// @ts-ignore +// @ts-expect-error export * from "repropose"; diff --git a/packages/cwp-template-aws/template/common/apps/admin/src/plugins/formBuilder/richTextEditor.ts b/packages/cwp-template-aws/template/common/apps/admin/src/plugins/formBuilder/richTextEditor.ts index 96de4f24eca..77eb207d3ec 100644 --- a/packages/cwp-template-aws/template/common/apps/admin/src/plugins/formBuilder/richTextEditor.ts +++ b/packages/cwp-template-aws/template/common/apps/admin/src/plugins/formBuilder/richTextEditor.ts @@ -1,13 +1,13 @@ /** * Package @editorjs/* is missing types. */ -// @ts-ignore +// @ts-expect-error import Delimiter from "@editorjs/delimiter"; -// @ts-ignore +// @ts-expect-error import Quote from "@editorjs/quote"; -// @ts-ignore +// @ts-expect-error import List from "@editorjs/list"; -// @ts-ignore +// @ts-expect-error import Underline from "@editorjs/underline"; import Image from "@webiny/app-admin/components/RichTextEditor/tools/image"; import TextColor from "@webiny/app-admin/components/RichTextEditor/tools/textColor"; diff --git a/packages/cwp-template-aws/template/common/apps/admin/src/plugins/headlessCMS/richTextEditor.ts b/packages/cwp-template-aws/template/common/apps/admin/src/plugins/headlessCMS/richTextEditor.ts index 24bdaa40760..4c5066bf443 100644 --- a/packages/cwp-template-aws/template/common/apps/admin/src/plugins/headlessCMS/richTextEditor.ts +++ b/packages/cwp-template-aws/template/common/apps/admin/src/plugins/headlessCMS/richTextEditor.ts @@ -1,13 +1,13 @@ /** * Package @editorjs/* is missing types. */ -// @ts-ignore +// @ts-expect-error import Delimiter from "@editorjs/delimiter"; -// @ts-ignore +// @ts-expect-error import Quote from "@editorjs/quote"; -// @ts-ignore +// @ts-expect-error import List from "@editorjs/list"; -// @ts-ignore +// @ts-expect-error import Underline from "@editorjs/underline"; import Image from "@webiny/app-admin/components/RichTextEditor/tools/image"; import TextColor from "@webiny/app-admin/components/RichTextEditor/tools/textColor"; diff --git a/packages/cwp-template-aws/template/common/apps/admin/src/plugins/pageBuilder/richTextEditor.ts b/packages/cwp-template-aws/template/common/apps/admin/src/plugins/pageBuilder/richTextEditor.ts index 684b3227ef1..b71d0269e6d 100644 --- a/packages/cwp-template-aws/template/common/apps/admin/src/plugins/pageBuilder/richTextEditor.ts +++ b/packages/cwp-template-aws/template/common/apps/admin/src/plugins/pageBuilder/richTextEditor.ts @@ -1,13 +1,13 @@ /** * Package @editorjs/* is missing types. */ -// @ts-ignore +// @ts-expect-error import Delimiter from "@editorjs/delimiter"; -// @ts-ignore +// @ts-expect-error import Quote from "@editorjs/quote"; -// @ts-ignore +// @ts-expect-error import List from "@editorjs/list"; -// @ts-ignore +// @ts-expect-error import Underline from "@editorjs/underline"; import Image from "@webiny/app-admin/components/RichTextEditor/tools/image"; import TextColor from "@webiny/app-admin/components/RichTextEditor/tools/textColor"; diff --git a/packages/cwp-template-aws/template/common/apps/admin/src/plugins/scaffolds/index.ts b/packages/cwp-template-aws/template/common/apps/admin/src/plugins/scaffolds/index.ts index d199ea25576..be7d9a84514 100644 --- a/packages/cwp-template-aws/template/common/apps/admin/src/plugins/scaffolds/index.ts +++ b/packages/cwp-template-aws/template/common/apps/admin/src/plugins/scaffolds/index.ts @@ -1,4 +1,3 @@ // This file is automatically updated via various scaffolding utilities. -// @ts-ignore export default () => []; diff --git a/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/plugins/scaffolds/index.ts b/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/plugins/scaffolds/index.ts index d199ea25576..be7d9a84514 100644 --- a/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/plugins/scaffolds/index.ts +++ b/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/plugins/scaffolds/index.ts @@ -1,4 +1,3 @@ // This file is automatically updated via various scaffolding utilities. -// @ts-ignore export default () => []; diff --git a/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/plugins/scaffolds/index.ts b/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/plugins/scaffolds/index.ts index d199ea25576..be7d9a84514 100644 --- a/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/plugins/scaffolds/index.ts +++ b/packages/cwp-template-aws/template/ddb/apps/api/graphql/src/plugins/scaffolds/index.ts @@ -1,4 +1,3 @@ // This file is automatically updated via various scaffolding utilities. -// @ts-ignore export default () => []; diff --git a/packages/db-dynamodb/src/BatchProcess.ts b/packages/db-dynamodb/src/BatchProcess.ts deleted file mode 100644 index fddb21e6ef5..00000000000 --- a/packages/db-dynamodb/src/BatchProcess.ts +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Remove this when no apps are using our internal db drivers anymore - */ -// @ts-nocheck -import { DynamoDBClient } from "@webiny/aws-sdk/client-dynamodb"; -import { Batch } from "@webiny/db"; - -type BatchType = "batchWrite" | "batchGet"; - -export type AddBatchOperationResponse = () => any | null; - -interface RejectBuildCallable { - ({ message }: { message: string }): void; -} - -interface RejectExecutionCallable { - ({ message }: { message: string }): void; -} - -interface AddBatchOperationArgs { - /** - * TODO: determine correct type. - */ - [key: string]: any; -} - -interface Response { - /** - * TODO: determine correct type. - */ - [key: string]: any; -} - -interface DocumentClientArgs { - ReturnConsumedCapacity: string; - RequestItems: Record<string, any>; -} - -class BatchProcess { - documentClient: DynamoDBClient; - batch: Batch; - resolveBuild: () => void; - rejectBuild: RejectBuildCallable; - queryBuild: Promise<void>; - resolveExecution: () => void; - rejectExecution: RejectExecutionCallable; - queryExecution: Promise<void>; - operations: [Record<string, any>, Record<string, any>][]; - batchType: BatchType; - results: Record<string, any>[]; - response: Record<string, any>; - constructor(batch: Batch, documentClient: DynamoDBClient) { - this.documentClient = documentClient; - this.batch = batch; - - this.resolveBuild = null; - this.rejectBuild = null; - this.queryBuild = new Promise((resolve, reject) => { - this.resolveBuild = resolve; - this.rejectBuild = reject; - }); - - this.resolveExecution = null; - this.rejectExecution = null; - this.queryExecution = new Promise((resolve, reject) => { - this.resolveExecution = resolve; - this.rejectExecution = reject; - }); - - this.operations = []; - this.results = []; - this.response = []; - - this.batchType; - } - - waitStartExecution(): Promise<void> { - return this.queryBuild; - } - - waitExecution(): Promise<void> { - return this.queryExecution; - } - - addBatchOperation( - type: BatchType, - args: AddBatchOperationArgs, - meta = {} - ): AddBatchOperationResponse { - if (!this.batchType) { - this.batchType = type; - } else if (this.batchType !== type) { - const initial = this.batchType; - const index = this.operations.length; - this.rejectBuild({ - message: `Cannot batch operations - all operations must be of the same type (the initial operation type was "${initial}", and operation type on index "${index}" is "${type}").` - }); - return null; - } - - this.operations.push([args, meta]); - const index = this.operations.length - 1; - return () => this.results[index]; - } - - addBatchWrite(args: AddBatchOperationArgs): AddBatchOperationResponse { - return this.addBatchOperation("batchWrite", args); - } - - addBatchDelete(args: AddBatchOperationArgs): AddBatchOperationResponse { - return this.addBatchOperation("batchWrite", { ...args }, { delete: true }); - } - - addBatchGet(args: AddBatchOperationArgs): AddBatchOperationResponse { - return this.addBatchOperation("batchGet", args); - } - - allOperationsAdded(): boolean { - return this.operations.length === this.batch.operations.length; - } - - startExecution() { - this.resolveBuild(); - - const documentClientArgs: DocumentClientArgs = { - ReturnConsumedCapacity: "TOTAL", - RequestItems: {} - }; - - const reject = (e: Error) => { - e.message = `An error occurred while executing "${this.batchType}" batch operation: ${e.message}`; - return this.rejectExecution(e); - }; - - let resolve = (response: Response) => { - this.response = response; - this.resolveExecution(); - }; - - switch (this.batchType) { - case "batchWrite": - documentClientArgs.RequestItems = {}; - for (let i = 0; i < this.operations.length; i++) { - const [args, meta] = this.operations[i]; - - if (!documentClientArgs.RequestItems[args.table]) { - documentClientArgs.RequestItems[args.table] = []; - } - - const push: { - DeleteRequest?: Record<string, any>; - PutRequest?: Record<string, any>; - } = {}; - - if (meta.delete) { - push.DeleteRequest = { - Key: args.query - }; - } else { - push.PutRequest = { - Item: args.data - }; - } - - documentClientArgs.RequestItems[args.table].push(push); - } - break; - case "batchGet": - documentClientArgs.RequestItems = {}; - for (let i = 0; i < this.operations.length; i++) { - const [args] = this.operations[i]; - - if (!documentClientArgs.RequestItems[args.table]) { - documentClientArgs.RequestItems[args.table] = { Keys: [] }; - } - - documentClientArgs.RequestItems[args.table].Keys.push(args.query); - } - - resolve = response => { - this.response = response; - const results = []; - - // The results of batchGet aren't ordered so we have to figure out the order of results ourselves. - for (let i = 0; i < this.operations.length; i++) { - const [args] = this.operations[i]; - const responseItems = response.Responses[args.table]; - - let foundResult = null; - outer: for (let j = 0; j < responseItems.length; j++) { - const responseItem = responseItems[j]; - for (const queryKey in args.query) { - if ( - typeof responseItem[queryKey] === "undefined" || - args.query[queryKey] !== responseItem[queryKey] - ) { - continue outer; - } - } - foundResult = responseItem; - } - - results.push(foundResult); - } - - this.results = results; - this.resolveExecution(); - }; - break; - } - - return this.documentClient[this.batchType]( - documentClientArgs, - (error: Error, result: Record<string, any>) => { - if (error) { - reject(error); - } else { - resolve(result); - } - } - ); - } -} - -export default BatchProcess; diff --git a/packages/db-dynamodb/src/DynamoDbDriver.ts b/packages/db-dynamodb/src/DynamoDbDriver.ts index dcbe13d1ff8..ec6a03fec8b 100644 --- a/packages/db-dynamodb/src/DynamoDbDriver.ts +++ b/packages/db-dynamodb/src/DynamoDbDriver.ts @@ -1,13 +1,12 @@ import { DynamoDBClient } from "@webiny/aws-sdk/client-dynamodb"; import { DbDriver, Result } from "@webiny/db"; -import BatchProcess from "./BatchProcess"; interface ConstructorArgs { documentClient: DynamoDBClient; } class DynamoDbDriver implements DbDriver { - batchProcesses: Record<string, BatchProcess>; + batchProcesses: Record<string, any>; documentClient: DynamoDBClient; constructor({ documentClient }: ConstructorArgs) { this.batchProcesses = {}; diff --git a/packages/db-dynamodb/src/QueryGenerator.ts b/packages/db-dynamodb/src/QueryGenerator.ts deleted file mode 100644 index f6930d59179..00000000000 --- a/packages/db-dynamodb/src/QueryGenerator.ts +++ /dev/null @@ -1,61 +0,0 @@ -import createKeyConditionExpressionArgs from "./statements/createKeyConditionExpressionArgs"; -import { Query, QueryKey, QueryKeyField, QueryKeys, QuerySort } from "~/types"; - -interface GenerateParams { - query: Query; - keys: QueryKeys; - sort: QuerySort; - limit: number; - tableName: string; -} -class QueryGenerator { - generate(params: GenerateParams) { - const { query, keys, sort, limit, tableName } = params; - // 1. Which key can we use in this query operation? - const key = this.findQueryKey(query, keys); - - if (!key) { - throw new Error("Cannot perform query - key not found."); - } - - // 2. Now that we know the key, let's separate the key attributes from the rest. - const keyAttributesValues: Record<string, string> = {}; - const nonKeyAttributesValues: Record<string, string> = {}; - for (const queryKey in query) { - if (key.fields.find((item: QueryKeyField) => item.name === queryKey)) { - keyAttributesValues[queryKey] = query[queryKey]; - } else { - nonKeyAttributesValues[queryKey] = query[queryKey]; - } - } - - const keyConditionExpression = createKeyConditionExpressionArgs({ - query: keyAttributesValues, - sort, - key - }); - - return { ...keyConditionExpression, TableName: tableName, Limit: limit }; - } - - findQueryKey(query: Query = {}, keys: QueryKeys = []): QueryKey | null { - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - let hasAllFields = true; - for (let j = 0; j < key.fields.length; j++) { - const field = key.fields[j]; - if (!query[field.name]) { - hasAllFields = false; - break; - } - } - - if (hasAllFields) { - return key; - } - } - return null; - } -} - -export default QueryGenerator; diff --git a/packages/db-dynamodb/src/operators/comparison/beginsWith.ts b/packages/db-dynamodb/src/operators/comparison/beginsWith.ts deleted file mode 100644 index f521f4083a8..00000000000 --- a/packages/db-dynamodb/src/operators/comparison/beginsWith.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Operator } from "~/types"; - -const beginsWith: Operator = { - canProcess: ({ value }) => { - return value && typeof value["$beginsWith"] !== "undefined"; - }, - process: ({ key, value, args }) => { - args.expression += `begins_with (#${key}, :${key})`; - args.attributeNames[`#${key}`] = key; - args.attributeValues[`:${key}`] = value["$beginsWith"]; - } -}; - -export default beginsWith; diff --git a/packages/db-dynamodb/src/operators/comparison/between.ts b/packages/db-dynamodb/src/operators/comparison/between.ts deleted file mode 100644 index 7d8c03bb531..00000000000 --- a/packages/db-dynamodb/src/operators/comparison/between.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Operator } from "~/types"; - -const between: Operator = { - canProcess: ({ value }) => { - return value && typeof value["$between"] !== "undefined"; - }, - process: ({ key, value }) => { - return { - statement: `#${key} BETWEEN :${key}Gte AND :${key}Lte`, - attributeNames: { - [`#${key}`]: key - }, - attributeValues: { - [`:${key}Gte`]: value[0], - [`:${key}Lte`]: value[1] - } - }; - } -}; - -export default between; diff --git a/packages/db-dynamodb/src/operators/comparison/eq.ts b/packages/db-dynamodb/src/operators/comparison/eq.ts deleted file mode 100644 index 2f56aaa258e..00000000000 --- a/packages/db-dynamodb/src/operators/comparison/eq.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Operator } from "~/types"; - -const validTypes = ["string", "boolean", "number"]; - -const eq: Operator = { - canProcess: ({ key, value }) => { - if (key && key.charAt(0) === "$") { - return false; - } - - if (value && typeof value["$eq"] !== "undefined") { - return true; - } - - return validTypes.includes(typeof value); - }, - process: ({ key, value, args }) => { - args.expression += `#${key} = :${key}`; - args.attributeNames[`#${key}`] = key; - args.attributeValues[`:${key}`] = value; - } -}; - -export default eq; diff --git a/packages/db-dynamodb/src/operators/comparison/gt.ts b/packages/db-dynamodb/src/operators/comparison/gt.ts deleted file mode 100644 index 8c7181f3b44..00000000000 --- a/packages/db-dynamodb/src/operators/comparison/gt.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Operator } from "~/types"; - -const gt: Operator = { - canProcess: ({ value }) => { - return value && typeof value["$gt"] !== "undefined"; - }, - process: ({ key, value, args }) => { - args.expression += `#${key} > :${key}`; - args.attributeNames[`#${key}`] = key; - args.attributeValues[`:${key}`] = value["$gt"]; - } -}; - -export default gt; diff --git a/packages/db-dynamodb/src/operators/comparison/gte.ts b/packages/db-dynamodb/src/operators/comparison/gte.ts deleted file mode 100644 index d9bcaf0c188..00000000000 --- a/packages/db-dynamodb/src/operators/comparison/gte.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Operator } from "~/types"; - -const gte: Operator = { - canProcess: ({ value }) => { - return value && typeof value["$gte"] !== "undefined"; - }, - process: ({ key, value, args }) => { - args.expression += `#${key} >= :${key}`; - args.attributeNames[`#${key}`] = key; - args.attributeValues[`:${key}`] = value["$gte"]; - } -}; - -export default gte; diff --git a/packages/db-dynamodb/src/operators/comparison/lt.ts b/packages/db-dynamodb/src/operators/comparison/lt.ts deleted file mode 100644 index 20d587388a2..00000000000 --- a/packages/db-dynamodb/src/operators/comparison/lt.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Operator } from "~/types"; - -const lt: Operator = { - canProcess: ({ value }) => { - return value && typeof value["$lt"] !== "undefined"; - }, - process: ({ key, value, args }) => { - args.expression += `#${key} < :${key}`; - args.attributeNames[`#${key}`] = key; - args.attributeValues[`:${key}`] = value["$lt"]; - } -}; - -export default lt; diff --git a/packages/db-dynamodb/src/operators/comparison/lte.ts b/packages/db-dynamodb/src/operators/comparison/lte.ts deleted file mode 100644 index 5093f993437..00000000000 --- a/packages/db-dynamodb/src/operators/comparison/lte.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Operator } from "~/types"; - -const lte: Operator = { - canProcess: ({ value }) => { - return value && typeof value["$lte"] !== "undefined"; - }, - process: ({ key, value, args }) => { - args.expression += `#${key} <= :${key}`; - args.attributeNames[`#${key}`] = key; - args.attributeValues[`:${key}`] = value["$lte"]; - } -}; - -export default lte; diff --git a/packages/db-dynamodb/src/operators/index.ts b/packages/db-dynamodb/src/operators/index.ts deleted file mode 100644 index 7aec779c50a..00000000000 --- a/packages/db-dynamodb/src/operators/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import $and from "./logical/and"; -import $or from "./logical/or"; -import $beginsWith from "./comparison/beginsWith"; -import $between from "./comparison/between"; -import $gt from "./comparison/gt"; -import $gte from "./comparison/gte"; -import $lt from "./comparison/lt"; -import $lte from "./comparison/lte"; -import $eq from "./comparison/eq"; - -export default { - $and, - $or, - $beginsWith, - $between, - $eq, - $gt, - $gte, - $lt, - $lte -}; diff --git a/packages/db-dynamodb/src/operators/logical/and.ts b/packages/db-dynamodb/src/operators/logical/and.ts deleted file mode 100644 index d99b7a6b28a..00000000000 --- a/packages/db-dynamodb/src/operators/logical/and.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - Operator, - ProcessStatementArgsParam, - ProcessStatementCallable, - ProcessStatementQueryParam -} from "~/types"; - -const processQuery = ( - query: ProcessStatementQueryParam, - andArgs: ProcessStatementArgsParam, - processStatement: ProcessStatementCallable -) => { - const args: ProcessStatementArgsParam = { - expression: "", - attributeNames: {}, - attributeValues: {} - }; - - processStatement({ args, query }); - - Object.assign(andArgs.attributeNames, args.attributeNames); - Object.assign(andArgs.attributeValues, args.attributeValues); - - if (andArgs.expression === "") { - andArgs.expression = args.expression; - } else { - andArgs.expression += " and " + args.expression; - } -}; - -const and: Operator = { - canProcess: ({ key }) => { - return key === "$and"; - }, - process: ({ value, args, processStatement }) => { - const andArgs = { - expression: "", - attributeNames: {}, - attributeValues: {} - }; - - if (Array.isArray(value)) { - for (let i = 0; i < value.length; i++) { - processQuery(value[i], andArgs, processStatement); - } - } else { - for (const [andKey, andValue] of Object.entries(value)) { - processQuery({ [andKey]: andValue }, andArgs, processStatement); - } - } - - args.expression += "(" + andArgs.expression + ")"; - Object.assign(args.attributeNames, andArgs.attributeNames); - Object.assign(args.attributeValues, andArgs.attributeValues); - } -}; - -export default and; diff --git a/packages/db-dynamodb/src/operators/logical/or.ts b/packages/db-dynamodb/src/operators/logical/or.ts deleted file mode 100644 index 21a6f760481..00000000000 --- a/packages/db-dynamodb/src/operators/logical/or.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - Operator, - ProcessStatementArgsParam, - ProcessStatementCallable, - ProcessStatementQueryParam -} from "~/types"; - -const processQuery = ( - query: ProcessStatementQueryParam, - orArgs: ProcessStatementArgsParam, - processStatement: ProcessStatementCallable -) => { - const args: ProcessStatementArgsParam = { - expression: "", - attributeNames: {}, - attributeValues: {} - }; - - processStatement({ args, query }); - - Object.assign(orArgs.attributeNames, args.attributeNames); - Object.assign(orArgs.attributeValues, args.attributeValues); - - if (orArgs.expression === "") { - orArgs.expression = args.expression; - } else { - orArgs.expression += " or " + args.expression; - } -}; - -const or: Operator = { - canProcess: ({ key }) => { - return key === "$or"; - }, - process: ({ value, args, processStatement }) => { - const orArgs: ProcessStatementArgsParam = { - expression: "", - attributeNames: {}, - attributeValues: {} - }; - - if (Array.isArray(value)) { - for (let i = 0; i < value.length; i++) { - processQuery(value[i], orArgs, processStatement); - } - } else { - for (const [orKey, orValue] of Object.entries(value)) { - processQuery({ [orKey]: orValue }, orArgs, processStatement); - } - } - - args.expression += "(" + orArgs.expression + ")"; - Object.assign(args.attributeNames, orArgs.attributeNames); - Object.assign(args.attributeValues, orArgs.attributeValues); - } -}; - -export default or; diff --git a/packages/db-dynamodb/src/statements/createKeyConditionExpressionArgs.ts b/packages/db-dynamodb/src/statements/createKeyConditionExpressionArgs.ts deleted file mode 100644 index 9fdd608be08..00000000000 --- a/packages/db-dynamodb/src/statements/createKeyConditionExpressionArgs.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Remove this when no apps are using our internal db drivers anymore - */ -// @ts-nocheck -import processStatement from "./processStatement"; -import { ProcessStatementArgsParam, Query, QueryKey, QuerySort } from "~/types"; - -interface Output { - KeyConditionExpression: string; - ExpressionAttributeNames: Record<string, any>; - ExpressionAttributeValues: Record<string, any>; - ScanIndexForward: boolean; - IndexName: string; -} -interface Params { - query: Query; - sort: QuerySort; - key: QueryKey; -} -export default ({ query, sort, key }: Params): Output => { - const args: ProcessStatementArgsParam = { - expression: "", - attributeNames: {}, - attributeValues: {} - }; - - processStatement({ args, query: { $and: query } }); - - const output: Output = { - KeyConditionExpression: args.expression, - ExpressionAttributeNames: args.attributeNames, - ExpressionAttributeValues: args.attributeValues, - ScanIndexForward: true, - IndexName: null - }; - - const sortKey = key.fields && key.fields[1]; - if (sort && sort[sortKey.name] === -1) { - output.ScanIndexForward = false; - } - - if (!key.primary) { - output.IndexName = key.name; - } - - return output; -}; diff --git a/packages/db-dynamodb/src/statements/processStatement.ts b/packages/db-dynamodb/src/statements/processStatement.ts deleted file mode 100644 index 3e55ed2d23b..00000000000 --- a/packages/db-dynamodb/src/statements/processStatement.ts +++ /dev/null @@ -1,17 +0,0 @@ -import allOperators from "./../operators"; -import { ProcessStatementCallable } from "~/types"; - -const processStatement: ProcessStatementCallable = ({ args, query }) => { - outerLoop: for (const [key, value] of Object.entries(query)) { - const operators = Object.values(allOperators); - for (let i = 0; i < operators.length; i++) { - const operator = operators[i]; - if (operator.canProcess({ key, value, args })) { - operator.process({ key, value, args, processStatement }); - continue outerLoop; - } - } - throw new Error(`Invalid operator {${key} : ${value}}.`); - } -}; -export default processStatement; diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 25da400db64..e06d9da2d2b 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -99,7 +99,7 @@ class Db { constructor({ driver, table, logTable }: ConstructorArgs) { this.driver = driver; - // @ts-ignore + // @ts-expect-error this.table = table; this.logTable = logTable; } diff --git a/packages/form/src/Form.tsx b/packages/form/src/Form.tsx index 5499f8b7cce..facddc272ee 100644 --- a/packages/form/src/Form.tsx +++ b/packages/form/src/Form.tsx @@ -437,7 +437,7 @@ function FormInner<T extends GenericFormData = GenericFormData>( !e.isDefaultPrevented() ) { // Need to blur current target in case of input fields to trigger validation - // @ts-ignore + // @ts-expect-error e.target && e.target.blur(); e.preventDefault(); e.stopPropagation(); diff --git a/packages/handler-graphql/src/builtInTypes/RefInputScalar.ts b/packages/handler-graphql/src/builtInTypes/RefInputScalar.ts index a9aeb133ce9..b8ea9f61f80 100644 --- a/packages/handler-graphql/src/builtInTypes/RefInputScalar.ts +++ b/packages/handler-graphql/src/builtInTypes/RefInputScalar.ts @@ -70,7 +70,7 @@ export const RefInputScalar = new GraphQLScalarType({ for (let i = 0; i < ast.fields.length; i++) { const { name, value } = ast.fields[i]; if (name.value === "id") { - // @ts-ignore + // @ts-expect-error return isValidId(value.value); } } diff --git a/packages/handler-graphql/src/createGraphQLHandler.ts b/packages/handler-graphql/src/createGraphQLHandler.ts index e29d99e6d14..fa8886ebe68 100644 --- a/packages/handler-graphql/src/createGraphQLHandler.ts +++ b/packages/handler-graphql/src/createGraphQLHandler.ts @@ -16,12 +16,12 @@ const createCacheKey = (context: Context) => { // TODO: in the near future, we have to assign a fixed name to every // TODO: GraphQLSchema plugin, to be able to create a reliable cache key. - // @ts-ignore TODO: `getCurrentTenant` should be injected as a parameter. - // @ts-ignore TODO: We should not be accessing `context` like this here. + // TODO: `getCurrentTenant` should be injected as a parameter. + // @ts-expect-error TODO: We should not be accessing `context` like this here. const tenant = context.tenancy?.getCurrentTenant(); - // @ts-ignore TODO: `getContentLocale` should be injected as a parameter. - // @ts-ignore TODO: We should not be accessing `context` like this here. + // TODO: `getContentLocale` should be injected as a parameter. + // @ts-expect-error TODO: We should not be accessing `context` like this here. const contentLocale = context.i18n?.getContentLocale(); return [ diff --git a/packages/handler-graphql/src/createGraphQLSchema.ts b/packages/handler-graphql/src/createGraphQLSchema.ts index c56ce00bdff..5b4bdb51302 100644 --- a/packages/handler-graphql/src/createGraphQLSchema.ts +++ b/packages/handler-graphql/src/createGraphQLSchema.ts @@ -69,9 +69,9 @@ export const createGraphQLSchema = (context: Context) => { * TODO @ts-refactor * Figure out correct types on typeDefs and resolvers */ - // @ts-ignore + // @ts-expect-error typeDefs.push(plugin.schema.typeDefs); - // @ts-ignore + // @ts-expect-error resolvers.push(plugin.schema.resolvers); } diff --git a/packages/handler-graphql/src/interceptConsole.ts b/packages/handler-graphql/src/interceptConsole.ts index 5046192e190..74e41f41ace 100644 --- a/packages/handler-graphql/src/interceptConsole.ts +++ b/packages/handler-graphql/src/interceptConsole.ts @@ -17,7 +17,7 @@ const skipOriginal: string[] = ["table"]; const restoreOriginalMethods = () => { for (const method of consoleMethods) { - // @ts-ignore + // @ts-expect-error console[method] = originalMethods[method]; } }; @@ -27,18 +27,18 @@ interface InterceptConsoleCallable { } export const interceptConsole = (callback: InterceptConsoleCallable) => { - // @ts-ignore + // @ts-expect-error if (console["__WEBINY__"] === true) { restoreOriginalMethods(); } - // @ts-ignore + // @ts-expect-error console["__WEBINY__"] = true; for (const method of consoleMethods) { - // @ts-ignore + // @ts-expect-error originalMethods[method] = console[method]; - // @ts-ignore + // @ts-expect-error console[method] = (...args) => { callback(method, args); if (skipOriginal.includes(method)) { diff --git a/packages/handler/src/Context.ts b/packages/handler/src/Context.ts index 8da5f625959..77bc082e6aa 100644 --- a/packages/handler/src/Context.ts +++ b/packages/handler/src/Context.ts @@ -15,11 +15,11 @@ export interface ContextParams extends BaseContextParams { export class Context extends BaseContext implements ContextInterface { public readonly server: ContextInterface["server"]; public readonly routes: ContextInterface["routes"]; - // @ts-ignore + // @ts-expect-error public handlerClient: ContextInterface["handlerClient"]; - // @ts-ignore + // @ts-expect-error public request: ContextInterface["request"]; - // @ts-ignore + // @ts-expect-error public reply: ContextInterface["reply"]; public constructor(params: ContextParams) { diff --git a/packages/i18n/src/I18n.ts b/packages/i18n/src/I18n.ts index 23364efd5a3..16157c2a561 100644 --- a/packages/i18n/src/I18n.ts +++ b/packages/i18n/src/I18n.ts @@ -3,7 +3,7 @@ import * as fecha from "fecha"; /** * Package short-hash has no types. */ -// @ts-ignore +// @ts-expect-error import hash from "short-hash"; import lodashAssign from "lodash/assign"; import lodashGet from "lodash/get"; diff --git a/packages/i18n/src/extractor/extract.ts b/packages/i18n/src/extractor/extract.ts index dae27939e0a..670b97914da 100644 --- a/packages/i18n/src/extractor/extract.ts +++ b/packages/i18n/src/extractor/extract.ts @@ -1,7 +1,7 @@ /** * Package short-hash has no types. */ -// @ts-ignore +// @ts-expect-error import hash from "short-hash"; /** diff --git a/packages/ioc/__tests__/useCases/models.test.ts b/packages/ioc/__tests__/useCases/models.test.ts index 5acee52220b..33c5e2ddc65 100644 --- a/packages/ioc/__tests__/useCases/models.test.ts +++ b/packages/ioc/__tests__/useCases/models.test.ts @@ -64,7 +64,7 @@ describe("Extend model via DI container activation", () => { }); it("should apply class extensions correctly", async () => { - // @ts-ignore for now + // @ts-expect-error for now createPageModelPlugin(Page => { return class MyPage extends Page { price = 0; @@ -90,7 +90,7 @@ describe("Extend model via DI container activation", () => { }; }); - // @ts-ignore for now + // @ts-expect-error for now createPageModelPlugin(Page => { return class MyPage extends Page { category = ""; @@ -136,7 +136,7 @@ describe("Extend model via DI container activation", () => { const secondRun = page.validate(); expect(secondRun.success).toBe(true); - // @ts-ignore + // @ts-expect-error expect(page["bogus"]).toBeUndefined(); expect(page.title).toEqual("My title"); expect(page.price).toEqual(200); diff --git a/packages/lexical-converter/__tests__/setup/setupEnv.ts b/packages/lexical-converter/__tests__/setup/setupEnv.ts index 3e7eb83fb5c..763f0b7ed83 100644 --- a/packages/lexical-converter/__tests__/setup/setupEnv.ts +++ b/packages/lexical-converter/__tests__/setup/setupEnv.ts @@ -1,5 +1,5 @@ -// @ts-nocheck // noinspection JSConstantReassignment +// @ts-expect-error const { TextEncoder, TextDecoder } = require("util"); global.TextEncoder = TextEncoder; diff --git a/packages/lexical-converter/__tests__/utils/toDom.ts b/packages/lexical-converter/__tests__/utils/toDom.ts index 2e278a93f53..e530e81825b 100644 --- a/packages/lexical-converter/__tests__/utils/toDom.ts +++ b/packages/lexical-converter/__tests__/utils/toDom.ts @@ -1,4 +1,4 @@ -// @ts-ignore jsdom types are messing up with the repo, so they're disabled in the root package.json. +// @ts-expect-error jsdom types are messing up with the repo, so they're disabled in the root package.json. import jsdom from "jsdom"; interface HtmlToDom { diff --git a/packages/lexical-nodes/src/ImageNode.tsx b/packages/lexical-nodes/src/ImageNode.tsx index aa01bab0316..c7ea54c8ecf 100644 --- a/packages/lexical-nodes/src/ImageNode.tsx +++ b/packages/lexical-nodes/src/ImageNode.tsx @@ -19,10 +19,7 @@ import type { } from "lexical"; import { $applyNodeReplacement, createEditor, DecoratorNode } from "lexical"; -const ImageComponent = React.lazy( - // @ts-ignore - () => import("./components/ImageNode/ImageComponent") -); +const ImageComponent = React.lazy(() => import("./components/ImageNode/ImageComponent")); export type SerializedImageNode = Spread< { diff --git a/packages/lexical-nodes/src/ListNode.ts b/packages/lexical-nodes/src/ListNode.ts index c630b16fc82..59275703135 100644 --- a/packages/lexical-nodes/src/ListNode.ts +++ b/packages/lexical-nodes/src/ListNode.ts @@ -111,7 +111,6 @@ export class ListNode extends ElementNode { return node; } - // @ts-ignore override exportJSON(): SerializedWebinyListNode { return { ...super.exportJSON(), @@ -282,7 +281,7 @@ function convertListNode(domNode: Node): DOMConversionOutput { } return { - // @ts-ignore + // @ts-expect-error after: normalizeChildren, node }; diff --git a/packages/lexical-theme/src/utils/styleObjectToString.ts b/packages/lexical-theme/src/utils/styleObjectToString.ts index 44dc876e41d..1cc5d6d9cf8 100644 --- a/packages/lexical-theme/src/utils/styleObjectToString.ts +++ b/packages/lexical-theme/src/utils/styleObjectToString.ts @@ -1,4 +1,4 @@ -// @ts-ignore - There are no types "@types/react-style-object-to-css" for this lib. +// @ts-expect-error - There are no types "@types/react-style-object-to-css" for this lib. import reactToCSS from "react-style-object-to-css"; import type { CSSObject } from "@emotion/react"; diff --git a/packages/migrations/__tests__/migrations/5.36.0/001/ddb/001.test.ts b/packages/migrations/__tests__/migrations/5.36.0/001/ddb/001.test.ts index acbe49f2291..45c54a568ac 100644 --- a/packages/migrations/__tests__/migrations/5.36.0/001/ddb/001.test.ts +++ b/packages/migrations/__tests__/migrations/5.36.0/001/ddb/001.test.ts @@ -332,7 +332,7 @@ describe("5.36.0-001", () => { // Should force-run the migration { - // @ts-ignore + // @ts-expect-error process.env["WEBINY_MIGRATION_FORCE_EXECUTE_5_36_0_001"] = "true"; process.stdout.write("[Second run]\n"); const { data, error } = await handler(); diff --git a/packages/project-utils/testing/elasticsearch/client.ts b/packages/project-utils/testing/elasticsearch/client.ts index ced44603be5..f1feaee0bfe 100644 --- a/packages/project-utils/testing/elasticsearch/client.ts +++ b/packages/project-utils/testing/elasticsearch/client.ts @@ -119,23 +119,23 @@ const attachCustomEvents = (client: Client): ElasticsearchClient => { } }; - // @ts-ignore + // @ts-expect-error client.indices.exists = async ( params: RequestParams.IndicesExists, options: TransportRequestOptions = {} ) => { registerIndex(params.index); - // @ts-ignore + // @ts-expect-error return originalExists.apply(client.indices, [params, options]); }; - // @ts-ignore + // @ts-expect-error client.indices.create = async ( params: RequestParams.IndicesCreate<any>, options: TransportRequestOptions = {} ) => { await deleteIndexCallable(params.index); - // @ts-ignore + // @ts-expect-error const response = await originalCreate.apply(client.indices, [params, options]); registerIndex(params.index); diff --git a/packages/project-utils/testing/elasticsearch/getElasticsearchClient.ts b/packages/project-utils/testing/elasticsearch/getElasticsearchClient.ts index ca48e1f658b..256abce4f55 100644 --- a/packages/project-utils/testing/elasticsearch/getElasticsearchClient.ts +++ b/packages/project-utils/testing/elasticsearch/getElasticsearchClient.ts @@ -1,7 +1,7 @@ /** * We can safely ignore the error being thrown for the path import. */ -// @ts-ignore +// @ts-expect-error import path from "path"; import { ContextPlugin } from "@webiny/api"; import elasticsearchClientContextPlugin, { diff --git a/packages/project-utils/testing/presets/index.js b/packages/project-utils/testing/presets/index.js index 966ff5894c2..875398387cd 100644 --- a/packages/project-utils/testing/presets/index.js +++ b/packages/project-utils/testing/presets/index.js @@ -5,7 +5,7 @@ const loadJsonFile = require("load-json-file"); const getAllPackages = targetKeywords => { const yargs = require("yargs"); - const { storage } = yargs.argv; + const { storage = "ddb" } = yargs.argv; if (!storage) { throw Error(`Missing required --storage parameter!`); diff --git a/packages/pubsub/src/index.ts b/packages/pubsub/src/index.ts index 11daf5c7be3..e1a541361ef 100644 --- a/packages/pubsub/src/index.ts +++ b/packages/pubsub/src/index.ts @@ -23,14 +23,14 @@ export const createTopic = <TEvent extends Event = Event>(topicName?: string): T /** * TODO @ts-refactor figure out types for callback */ - // @ts-ignore + // @ts-expect-error subscribers.push(cb); }, subscribeOnce(cb) { /** * TODO @ts-refactor figure out types for callback */ - // @ts-ignore + // @ts-expect-error subscribers.push(withUnsubscribe(cb)); }, getSubscribers(): Subscriber<TEvent>[] { diff --git a/packages/pulumi-aws/src/apps/api/ApiFileManager.ts b/packages/pulumi-aws/src/apps/api/ApiFileManager.ts index 08f894cae91..bb4aaaaade2 100644 --- a/packages/pulumi-aws/src/apps/api/ApiFileManager.ts +++ b/packages/pulumi-aws/src/apps/api/ApiFileManager.ts @@ -2,7 +2,7 @@ import path from "path"; import * as pulumi from "@pulumi/pulumi"; import * as aws from "@pulumi/aws"; -// @ts-ignore +// @ts-expect-error import { getLayerArn } from "@webiny/aws-layers"; import { createAppModule, PulumiApp, PulumiAppModule } from "@webiny/pulumi"; diff --git a/packages/pulumi-aws/src/apps/api/ApiPageBuilder.ts b/packages/pulumi-aws/src/apps/api/ApiPageBuilder.ts index 3995b879792..d5cf22166e5 100644 --- a/packages/pulumi-aws/src/apps/api/ApiPageBuilder.ts +++ b/packages/pulumi-aws/src/apps/api/ApiPageBuilder.ts @@ -1,8 +1,6 @@ import * as path from "path"; import * as pulumi from "@pulumi/pulumi"; import * as aws from "@pulumi/aws"; - -//@ts-ignore import { createInstallationZip } from "@webiny/api-page-builder/installation"; import { createAppModule, PulumiApp, PulumiAppModule } from "@webiny/pulumi"; import { CoreOutput } from "../common"; diff --git a/packages/pulumi-aws/src/apps/website/WebsitePrerendering.ts b/packages/pulumi-aws/src/apps/website/WebsitePrerendering.ts index c571dd8ab34..7b44018ed8a 100644 --- a/packages/pulumi-aws/src/apps/website/WebsitePrerendering.ts +++ b/packages/pulumi-aws/src/apps/website/WebsitePrerendering.ts @@ -4,7 +4,7 @@ import * as aws from "@pulumi/aws"; import { marshall } from "@webiny/aws-sdk/client-dynamodb"; import { PulumiApp } from "@webiny/pulumi"; -// @ts-ignore +// @ts-expect-error import { getLayerArn } from "@webiny/aws-layers"; import { createLambdaRole, getCommonLambdaEnvVariables } from "../lambdaUtils"; diff --git a/packages/pulumi-aws/src/utils/lambdaEnvVariables.ts b/packages/pulumi-aws/src/utils/lambdaEnvVariables.ts index cfd4fe0a417..56692bb1e83 100644 --- a/packages/pulumi-aws/src/utils/lambdaEnvVariables.ts +++ b/packages/pulumi-aws/src/utils/lambdaEnvVariables.ts @@ -69,7 +69,7 @@ export function withCommonLambdaEnvVariables<T extends PulumiApp>( // We must first execute the original program, and pass in the augmented app. const resources = await originalProgram({ ...app, - // @ts-ignore because currently, we don't have a way of passing in a custom app type. + // @ts-expect-error because currently, we don't have a way of passing in a custom app type. setCommonLambdaEnvVariables }); diff --git a/packages/react-properties/__tests__/cases/app-config/Module.tsx b/packages/react-properties/__tests__/cases/app-config/Module.tsx index f35c174c72f..bbb11041f57 100644 --- a/packages/react-properties/__tests__/cases/app-config/Module.tsx +++ b/packages/react-properties/__tests__/cases/app-config/Module.tsx @@ -1,4 +1,7 @@ -// @ts-nocheck +/** + * Not meant to work, just to show how the config looks like. + */ +// @ts-nocheck keep it /* eslint-disable */ import React from "react"; export const FilesRepositorySymbol = Symbol.for("FilesRepositorySymbol"); diff --git a/packages/react-properties/__tests__/setupEnv.ts b/packages/react-properties/__tests__/setupEnv.ts index f80108f392e..3c6909a2b3f 100644 --- a/packages/react-properties/__tests__/setupEnv.ts +++ b/packages/react-properties/__tests__/setupEnv.ts @@ -1,13 +1,14 @@ -// @ts-nocheck // noinspection JSConstantReassignment // This is why this file is necessary: https://github.com/ai/nanoid/issues/363 const { randomFillSync } = require("crypto"); +// @ts-expect-error const { TextEncoder, TextDecoder } = require("util"); global.TextEncoder = TextEncoder; global.TextDecoder = TextDecoder; +// @ts-expect-error window.crypto = { getRandomValues(buffer) { return randomFillSync(buffer); diff --git a/packages/react-router/src/context/RouterContext.tsx b/packages/react-router/src/context/RouterContext.tsx index b7ed20acddb..c7a167226b7 100644 --- a/packages/react-router/src/context/RouterContext.tsx +++ b/packages/react-router/src/context/RouterContext.tsx @@ -50,7 +50,7 @@ export const RouterConsumer: React.FC = ({ children }) => ( /** * TODO: Figure out correct type for children. */ - // @ts-ignore + // @ts-expect-error return React.cloneElement(children, props); }} </RouterContext.Consumer> diff --git a/packages/storybook-utils/src/CodeBlock/index.tsx b/packages/storybook-utils/src/CodeBlock/index.tsx index f4a23b8ef33..d84fa8c6d3a 100644 --- a/packages/storybook-utils/src/CodeBlock/index.tsx +++ b/packages/storybook-utils/src/CodeBlock/index.tsx @@ -2,7 +2,7 @@ import * as React from "react"; /** * No types for react-highlight.js */ -// @ts-ignore +// @ts-expect-error import Highlight from "react-highlight.js"; import copy from "copy-to-clipboard"; import elementToString from "react-element-to-jsx-string"; diff --git a/packages/storybook-utils/src/Markdown/index.tsx b/packages/storybook-utils/src/Markdown/index.tsx index e54bf889870..5a94ea1c799 100644 --- a/packages/storybook-utils/src/Markdown/index.tsx +++ b/packages/storybook-utils/src/Markdown/index.tsx @@ -2,7 +2,7 @@ import * as React from "react"; /** * Package react-remarkable does not have types. */ -// @ts-ignore +// @ts-expect-error import Remarkable from "react-remarkable"; import hljs from "highlight.js"; diff --git a/packages/telemetry/react.d.ts b/packages/telemetry/react.d.ts new file mode 100644 index 00000000000..aa337619f10 --- /dev/null +++ b/packages/telemetry/react.d.ts @@ -0,0 +1,2 @@ +export declare function setProperties(data: Record<string, any>): void; +export declare function sendEvent(ev: string, data?: Record<string, any>): Promise<any>; diff --git a/packages/ui/src/AutoComplete/AutoComplete.tsx b/packages/ui/src/AutoComplete/AutoComplete.tsx index 7cab88f17f2..451cfcc2f59 100644 --- a/packages/ui/src/AutoComplete/AutoComplete.tsx +++ b/packages/ui/src/AutoComplete/AutoComplete.tsx @@ -284,9 +284,8 @@ class AutoComplete extends React.Component<AutoCompleteProps, State> { // This prop is above `otherInputProps` since it can be overridden by the user. trailingIcon: this.props.loading && <Spinner />, ...otherInputProps, - // @ts-ignore + // @ts-expect-error size: this.props.size, - // @ts-ignore validation, rawOnChange: true, onChange: ev => ev, diff --git a/packages/ui/src/AutoComplete/MultiAutoComplete.tsx b/packages/ui/src/AutoComplete/MultiAutoComplete.tsx index 926547f5e76..ac8d7b0a28d 100644 --- a/packages/ui/src/AutoComplete/MultiAutoComplete.tsx +++ b/packages/ui/src/AutoComplete/MultiAutoComplete.tsx @@ -636,7 +636,7 @@ export class MultiAutoComplete extends React.Component< <div className={classNames(autoCompleteStyle, props.className)}> <Downshift defaultSelectedItem={null} - // @ts-ignore there is no className on Downshift + // @ts-expect-error there is no className on Downshift className={autoCompleteStyle} itemToString={item => item && getOptionText(item, props)} ref={this.downshift} @@ -669,7 +669,7 @@ export class MultiAutoComplete extends React.Component< <Input {...getInputProps({ ...otherInputProps, - // @ts-ignore + // @ts-expect-error validation, // Only pass description if not using "useMultipleSelectionList". diff --git a/packages/ui/src/Checkbox/Checkbox.tsx b/packages/ui/src/Checkbox/Checkbox.tsx index fe2e7504137..f68ddcf99e9 100644 --- a/packages/ui/src/Checkbox/Checkbox.tsx +++ b/packages/ui/src/Checkbox/Checkbox.tsx @@ -48,7 +48,6 @@ class Checkbox extends React.Component<Props> { checked={Boolean(value)} onChange={this.onChange} onClick={() => typeof onClick === "function" && onClick(Boolean(value))} - // @ts-ignore Although the label is React.ReactNode internally, an error is still thrown. label={label} data-testid={this.props["data-testid"]} /> diff --git a/packages/ui/src/ColorPicker/ColorPicker.tsx b/packages/ui/src/ColorPicker/ColorPicker.tsx index 25235b02250..b1c88e779ec 100644 --- a/packages/ui/src/ColorPicker/ColorPicker.tsx +++ b/packages/ui/src/ColorPicker/ColorPicker.tsx @@ -22,7 +22,7 @@ const classes = { display: "inline-block", cursor: "pointer" }), - // @ts-ignore + // @ts-expect-error popover: css({ position: "absolute", zIndex: "2" diff --git a/packages/ui/src/ImageEditor/ImageEditor.tsx b/packages/ui/src/ImageEditor/ImageEditor.tsx index d75e1c06d75..86b626da704 100644 --- a/packages/ui/src/ImageEditor/ImageEditor.tsx +++ b/packages/ui/src/ImageEditor/ImageEditor.tsx @@ -7,7 +7,7 @@ import { ButtonSecondary, ButtonPrimary } from "../Button"; /** * Package load-script does not have types. */ -// @ts-ignore +// @ts-expect-error import loadScript from "load-script"; const toolbar = { @@ -48,7 +48,7 @@ const ApplyCancelActions = styled("div")({ const initScripts = (): Promise<string> => { return new Promise((resolve: any) => { - // @ts-ignore + // @ts-expect-error if (window.Caman) { return resolve(); } diff --git a/packages/ui/src/ImageEditor/toolbar/filter.tsx b/packages/ui/src/ImageEditor/toolbar/filter.tsx index 5d6961d669e..2ec371fa2ba 100644 --- a/packages/ui/src/ImageEditor/toolbar/filter.tsx +++ b/packages/ui/src/ImageEditor/toolbar/filter.tsx @@ -1,5 +1,5 @@ /** - * When using Caman, we added @ts-ignore because it does not exist in packages, but it is loaded in packages/ui/src/ImageEditor/ImageEditor.tsx:38. + * When using Caman, we added @ts-expect-error because it does not exist in packages, but it is loaded in packages/ui/src/ImageEditor/ImageEditor.tsx:38. * TODO: use some other library to edit images */ import React from "react"; @@ -108,15 +108,15 @@ class RenderForm extends React.Component<RenderFormProps, RenderFormState> { // eslint-disable-next-line @typescript-eslint/no-this-alias const component = this; - // @ts-ignore + // @ts-expect-error Caman(canvas.current, function () { - // @ts-ignore + // @ts-expect-error this.revert(false); Object.keys(values).forEach( - // @ts-ignore + // @ts-expect-error key => values[key] !== 0 && this[key] && this[key](values[key]) ); - // @ts-ignore + // @ts-expect-error this.render(); component.setState({ processing: false }); }); @@ -193,11 +193,11 @@ const tool: ImageEditorTool = { return <RenderForm {...props} />; }, cancel: ({ canvas }) => { - // @ts-ignore + // @ts-expect-error Caman(canvas.current, function () { - // @ts-ignore + // @ts-expect-error this.revert(false); - // @ts-ignore + // @ts-expect-error this.render(); }); } diff --git a/packages/ui/src/ImageUpload/MultiImageUpload.tsx b/packages/ui/src/ImageUpload/MultiImageUpload.tsx index 0bc32026cbe..32c27b819ea 100644 --- a/packages/ui/src/ImageUpload/MultiImageUpload.tsx +++ b/packages/ui/src/ImageUpload/MultiImageUpload.tsx @@ -174,7 +174,7 @@ class MultiImageUpload extends React.Component<MultiImageUploadProps, State> { */ let imageEditorImageSrc = ""; if (this.state.imageEditor.image) { - // @ts-ignore + // @ts-expect-error imageEditorImageSrc = this.state.imageEditor.image.src; console.warn("Figure out correct type if this.state.imageEditor.image.src"); console.log(this.state.imageEditor.image.src); diff --git a/packages/ui/src/ImageUpload/styled.ts b/packages/ui/src/ImageUpload/styled.ts index 29751ffd2fa..5130ba66e2e 100644 --- a/packages/ui/src/ImageUpload/styled.ts +++ b/packages/ui/src/ImageUpload/styled.ts @@ -1,5 +1,3 @@ -// TODO remove -// @ts-nocheck import styled from "@emotion/styled"; export const AddImageIconWrapper = styled("div")({ @@ -82,6 +80,7 @@ export const ImagePreviewWrapper = styled("div")({ flexDirection: "column", boxSizing: "border-box", position: "relative", + // @ts-expect-error [AddImageWrapper]: { position: "absolute", display: "none", @@ -89,6 +88,7 @@ export const ImagePreviewWrapper = styled("div")({ height: "100%", zIndex: 1, backgroundColor: "rgba(0,0,0, 0.75)", + // @ts-expect-error [AddImageIconWrapper]: { top: "50%", left: "50%", @@ -98,13 +98,16 @@ export const ImagePreviewWrapper = styled("div")({ } }, "&:hover": { + // @ts-expect-error [AddImageWrapper]: { display: "block" }, + // @ts-expect-error [RemoveImage]: { display: "block", zIndex: 2 }, + // @ts-expect-error [EditImage]: { display: "block", zIndex: 2 diff --git a/packages/ui/src/Input/Input.tsx b/packages/ui/src/Input/Input.tsx index d867d7d63d6..9722cc3ef03 100644 --- a/packages/ui/src/Input/Input.tsx +++ b/packages/ui/src/Input/Input.tsx @@ -81,7 +81,7 @@ export const Input: React.FC<InputProps> = props => { return; } - // @ts-ignore + // @ts-expect-error onChange(rawOnChange ? e : e.target.value); }, [props.onChange, props.rawOnChange] diff --git a/packages/ui/src/Input/__tests__/Input.test.tsx b/packages/ui/src/Input/__tests__/Input.test.tsx index 79ee295e8d7..971e98f3a93 100644 --- a/packages/ui/src/Input/__tests__/Input.test.tsx +++ b/packages/ui/src/Input/__tests__/Input.test.tsx @@ -69,7 +69,7 @@ describe("Input tests", () => { test("passes expected props to render prop", () => { const { renderArg } = setup(); - // @ts-ignore + // @ts-expect-error expect(renderArg).toContainKeys(["value", "validation", "onChange", "onBlur"]); }); diff --git a/packages/ui/src/List/DataList/DataList.stories.tsx b/packages/ui/src/List/DataList/DataList.stories.tsx index b3073f78f9f..fab029be964 100644 --- a/packages/ui/src/List/DataList/DataList.stories.tsx +++ b/packages/ui/src/List/DataList/DataList.stories.tsx @@ -129,7 +129,7 @@ story.add( {...generalOptionsAndCallbacks} data={dataProp} meta={metaProp} - // @ts-ignore + // @ts-expect-error sorters={sortersProp.list} > {({ data }: { data: any[] }) => ( diff --git a/packages/ui/src/Mosaic/Mosaic.tsx b/packages/ui/src/Mosaic/Mosaic.tsx index 841ec4b105f..e627898a82f 100644 --- a/packages/ui/src/Mosaic/Mosaic.tsx +++ b/packages/ui/src/Mosaic/Mosaic.tsx @@ -2,7 +2,7 @@ import React from "react"; /** * Package react-columned does not have types. */ -// @ts-ignore +// @ts-expect-error import Columned from "react-columned"; export interface MosaicProps { diff --git a/packages/ui/src/Radio/Radio.tsx b/packages/ui/src/Radio/Radio.tsx index 8651649b3a0..6680676569c 100644 --- a/packages/ui/src/Radio/Radio.tsx +++ b/packages/ui/src/Radio/Radio.tsx @@ -34,7 +34,6 @@ class Radio extends React.Component<Props> { disabled={disabled} checked={Boolean(value)} onChange={this.onChange} - // @ts-ignore Although the label is React.ReactNode internally, an error is still thrown. label={label} /> {validationIsValid === false && ( diff --git a/packages/ui/src/Select/Select.tsx b/packages/ui/src/Select/Select.tsx index 4ba54969cc5..db8e9eeea9a 100644 --- a/packages/ui/src/Select/Select.tsx +++ b/packages/ui/src/Select/Select.tsx @@ -80,7 +80,7 @@ const getRmwcProps = (props: SelectProps): FormComponentProps & RmwcSelectProps const newProps: FormComponentProps & RmwcSelectProps = {}; Object.keys(props) .filter(name => !skipProps.includes(name)) - // @ts-ignore + // @ts-expect-error .forEach((name: any) => (newProps[name] = props[name])); return newProps; diff --git a/packages/ui/src/Tooltip/Tooltip.tsx b/packages/ui/src/Tooltip/Tooltip.tsx index ee64b98db67..504c78b2c35 100644 --- a/packages/ui/src/Tooltip/Tooltip.tsx +++ b/packages/ui/src/Tooltip/Tooltip.tsx @@ -58,7 +58,7 @@ class Tooltip extends React.Component<TooltipProps, State> { /** * rc-tooltip types do not have animation as prop, but the rc-tooltip lib has. */ - // @ts-ignore + // @ts-expect-error animation={"fade"} onVisibleChange={this.onVisibleChange} overlay={this.props.content} diff --git a/packages/utils/src/generateId.ts b/packages/utils/src/generateId.ts index 197b082c7f7..1dd296ff83f 100644 --- a/packages/utils/src/generateId.ts +++ b/packages/utils/src/generateId.ts @@ -2,7 +2,7 @@ import { nanoid, customAlphabet } from "nanoid"; /** * Package nanoid-dictionary is missing types */ -// @ts-ignore +// @ts-expect-error import { lowercase, uppercase, alphanumeric, numbers } from "nanoid-dictionary"; const DEFAULT_SIZE = 21; diff --git a/packages/utils/src/mdbid.ts b/packages/utils/src/mdbid.ts index c37ee7bd13f..f3200f98669 100644 --- a/packages/utils/src/mdbid.ts +++ b/packages/utils/src/mdbid.ts @@ -1,4 +1,4 @@ -// @ts-ignore `mdbid` package has no types +// @ts-expect-error `mdbid` package has no types import generateId from "mdbid"; export const mdbid = (): string => generateId(); diff --git a/packages/validation/src/validators/numeric.ts b/packages/validation/src/validators/numeric.ts index 809ba24f250..53302147f1e 100644 --- a/packages/validation/src/validators/numeric.ts +++ b/packages/validation/src/validators/numeric.ts @@ -2,7 +2,7 @@ import ValidationError from "~/validationError"; /** * Package isnumeric does not have types so we ignore it. */ -// @ts-ignore +// @ts-expect-error import isNumeric from "isnumeric"; /** diff --git a/scripts/listPackagesWithTests.js b/scripts/listPackagesWithTests.js index e919d0e4d34..73fb442f020 100644 --- a/scripts/listPackagesWithTests.js +++ b/scripts/listPackagesWithTests.js @@ -111,6 +111,9 @@ const CUSTOM_HANDLERS = { }, "app-aco": () => { return ["packages/app-aco"]; + }, + "app-file-manager": () => { + return ["packages/app-file-manager"]; } }; diff --git a/scripts/prepublishOnly/src/prepublishOnly.ts b/scripts/prepublishOnly/src/prepublishOnly.ts index e1aa9a0ee38..41dce5be03e 100644 --- a/scripts/prepublishOnly/src/prepublishOnly.ts +++ b/scripts/prepublishOnly/src/prepublishOnly.ts @@ -1,4 +1,4 @@ -// @ts-ignore +// @ts-expect-error import getYarnWorkspaces from "get-yarn-workspaces"; import { blueBright, gray } from "chalk"; import fs from "fs-extra"; @@ -51,8 +51,9 @@ class FileLocker { }); if (resolvedVersion) { - // @ts-ignore - lockPackageJson[depKey][key] = resolvedVersion; + const newDepValue = lockPackageJson[depKey] || {}; + newDepValue[key] = resolvedVersion; + lockPackageJson[depKey] = newDepValue; } else { console.log(`Failed to resolve`, dependencies[key]); } diff --git a/scripts/prepublishOnly/src/resolvePackageVersion.ts b/scripts/prepublishOnly/src/resolvePackageVersion.ts index 29ac8408786..9712c7d2099 100644 --- a/scripts/prepublishOnly/src/resolvePackageVersion.ts +++ b/scripts/prepublishOnly/src/resolvePackageVersion.ts @@ -12,8 +12,7 @@ export const resolvePackageVersion = (packageName: string, { cwd }: Options) => const packageJson = findUp.sync(searchPath, { cwd }); if (packageJson) { const json = sync<PackageJson>(packageJson); - // @ts-ignore - return json ? json.version : undefined; + return json?.version; } return undefined; diff --git a/scripts/release/Release.js b/scripts/release/Release.js index c2b22cb6501..bd2cce71b21 100644 --- a/scripts/release/Release.js +++ b/scripts/release/Release.js @@ -53,7 +53,7 @@ class Release { this.resetAllChanges = reset; } - async execute() { + async versionPackages() { this.__validateConfig(); this.logger.info("Attempting to release tag %s", this.tag); @@ -98,6 +98,14 @@ class Release { await execa("yarn", lernaVersionArgs, { stdio: "inherit" }); this.logger.info("Packages versioning completed"); + // Read the new version + const lernaJSON = await loadJSON("lerna.json"); + return { version: lernaJSON.version, tag: this.tag }; + } + + async execute() { + await this.versionPackages(); + // Run `lerna` to publish packages const lernaPublishArgs = [ "lerna", diff --git a/scripts/release/index.js b/scripts/release/index.js index b7c7267468a..b43ebc76a97 100644 --- a/scripts/release/index.js +++ b/scripts/release/index.js @@ -6,7 +6,7 @@ const { getReleaseType } = require("./releaseTypes"); yargs.version(false); async function runRelease() { - const { type, tag, gitReset, version, createGithubRelease } = yargs.argv; + const { type, tag, gitReset, version, createGithubRelease, printVersion } = yargs.argv; console.log({ type, tag, gitReset, version }); if (!type) { @@ -34,7 +34,13 @@ async function runRelease() { release.setCreateGithubRelease(createGithubRelease); } - await release.execute(); + if (printVersion) { + const { version } = await release.versionPackages(); + + console.log(version); + } else { + await release.execute(); + } } (async () => { diff --git a/webiny.config.ts b/webiny.config.ts index 3aad8b0a888..3d124b4b601 100644 --- a/webiny.config.ts +++ b/webiny.config.ts @@ -1,4 +1,6 @@ +// @ts-expect-error import { defineProject } from "@webiny/core"; +// @ts-expect-error import { configurePreset } from "@webiny/preset-aws"; export default defineProject({ diff --git a/webiny.project.ts b/webiny.project.ts index fb67cbe483c..225bb630061 100644 --- a/webiny.project.ts +++ b/webiny.project.ts @@ -1,4 +1,3 @@ -// @ts-nocheck export default { name: "webiny-js", cli: { @@ -12,8 +11,11 @@ export default { */ try { const modules = await Promise.allSettled([ + // @ts-expect-error import("@webiny/cli-plugin-workspaces"), + // @ts-expect-error import("@webiny/cli-plugin-deploy-pulumi"), + // @ts-expect-error import("@webiny/cwp-template-aws/cli"), import("@webiny/cli-plugin-scaffold"), import("@webiny/cli-plugin-scaffold-graphql-service"), diff --git a/yarn.lock b/yarn.lock index a5e645933f1..d465b7c3b21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15550,6 +15550,7 @@ __metadata: rimraf: ^3.0.2 ttypescript: ^1.5.13 typescript: 4.7.4 + zod: ^3.22.4 languageName: unknown linkType: soft @@ -45894,3 +45895,10 @@ __metadata: checksum: f185ba87342ff16f7a06686767c2b2a7af41110c7edf7c1974095d8db7a73792696bcb4a00853de0d2edeb34a5b2ea6a55871bc864227dace682a0a28de33e1f languageName: node linkType: hard + +"zod@npm:^3.22.4": + version: 3.22.4 + resolution: "zod@npm:3.22.4" + checksum: 80bfd7f8039b24fddeb0718a2ec7c02aa9856e4838d6aa4864335a047b6b37a3273b191ef335bf0b2002e5c514ef261ffcda5a589fb084a48c336ffc4cdbab7f + languageName: node + linkType: hard From 989f4e5254186accd54b958201de8cd33f37649a Mon Sep 17 00:00:00 2001 From: Vitalii Nobis <vitalii.nobis@neatbyte.solutions> Date: Tue, 28 Nov 2023 14:23:22 +0000 Subject: [PATCH 19/37] fix: fixed issue with settings model validation --- .../FormBuilderContextSetup.ts | 22 ++++++++--- .../src/plugins/crud/settings.models.ts | 37 +++++++++++++------ packages/api-form-builder/src/types.ts | 6 +-- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts index 16caee4fbba..44c1b9ab611 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts @@ -5,6 +5,7 @@ import WebinyError from "@webiny/error"; import { createFormBuilderBasicContext } from "./createFormBuilderBasicContext"; import { createFormBuilderPlugins } from "./createFormBuilderPlugins"; import { CmsFormsStorage } from "./CmsFormsStorage"; +import { isInstallationPending } from "./isInstallationPending"; import { CmsSubmissionsStorage } from "./CmsSubmissionsStorage"; import { FormBuilderContext, FbFormPermission, FormBuilderStorageOperations } from "~/types"; @@ -32,15 +33,18 @@ export class FormBuilderContextSetup { const formsStorageOps = await this.context.security.withoutAuthorization(() => { return this.setupFormsCmsStorageOperations(); }); + + if (formsStorageOps) { + storageOperations.forms = formsStorageOps; + } + const submissionsStorageOps = await this.context.security.withoutAuthorization(() => { return this.setupSubmissionsCmsStorageOperations(); }); - storageOperations = { - ...storageOperations, - forms: formsStorageOps, - submissions: submissionsStorageOps - }; + if (submissionsStorageOps) { + storageOperations.submissions = submissionsStorageOps; + } const formsPermissions = new FormsPermissions({ getIdentity: this.getIdentity.bind(this), @@ -60,6 +64,10 @@ export class FormBuilderContextSetup { } private async setupFormsCmsStorageOperations() { + if (isInstallationPending({ tenancy: this.context.tenancy, i18n: this.context.i18n })) { + return; + } + const model = await this.getModel("fbForm"); return await CmsFormsStorage.create({ @@ -70,6 +78,10 @@ export class FormBuilderContextSetup { } private async setupSubmissionsCmsStorageOperations() { + if (isInstallationPending({ tenancy: this.context.tenancy, i18n: this.context.i18n })) { + return; + } + const model = await this.getModel("fbSubmission"); return await CmsSubmissionsStorage.create({ diff --git a/packages/api-form-builder/src/plugins/crud/settings.models.ts b/packages/api-form-builder/src/plugins/crud/settings.models.ts index 11268195342..767e8ce3648 100644 --- a/packages/api-form-builder/src/plugins/crud/settings.models.ts +++ b/packages/api-form-builder/src/plugins/crud/settings.models.ts @@ -2,22 +2,35 @@ import zod from "zod"; export const CreateDataModel = () => { return zod.object({ - domain: zod.string(), - reCaptcha: zod.object({ - enabled: zod.boolean(), - siteKey: zod.string().max(100), - secretKey: zod.string().max(100) - }) + domain: zod.string().default(""), + reCaptcha: zod + .object({ + enabled: zod.boolean().nullish().default(null), + siteKey: zod.string().max(100).nullish().default(null), + secretKey: zod.string().max(100).nullish().default(null) + }) + .default({ + enabled: null, + siteKey: null, + secretKey: null + }) }); }; export const UpdateDataModel = () => { return zod.object({ - domain: zod.string(), - reCaptcha: zod.object({ - enabled: zod.boolean(), - siteKey: zod.string().max(100), - secretKey: zod.string().max(100) - }) + domain: zod.string().default(""), + reCaptcha: zod + .object({ + enabled: zod.boolean().nullish().default(null), + siteKey: zod.string().max(100).nullish().default(null), + secretKey: zod.string().max(100).nullish().default(null) + }) + .nullish() + .default({ + enabled: null, + siteKey: null, + secretKey: null + }) }); }; diff --git a/packages/api-form-builder/src/types.ts b/packages/api-form-builder/src/types.ts index 71960abe8a0..fc170ee8069 100644 --- a/packages/api-form-builder/src/types.ts +++ b/packages/api-form-builder/src/types.ts @@ -373,9 +373,9 @@ export interface FbSubmission { export interface Settings { domain: string; reCaptcha: { - enabled: boolean; - siteKey: string; - secretKey: string; + enabled: boolean | null; + siteKey: string | null; + secretKey: string | null; }; tenant: string; locale: string; From a987ff7ccbf488b76736b29b66cd6654594317f1 Mon Sep 17 00:00:00 2001 From: Vitalii Nobis <vitalii.nobis@neatbyte.solutions> Date: Thu, 30 Nov 2023 13:15:53 +0000 Subject: [PATCH 20/37] fix: fix tests --- .../src/cmsFormBuilderStorage/CmsFormsStorage.ts | 2 +- .../src/cmsFormBuilderStorage/models/form.model.ts | 9 --------- packages/api-form-builder/src/plugins/crud/forms.crud.ts | 2 -- .../src/plugins/graphql/createFormsTypeDefs.ts | 1 + packages/api-form-builder/src/types.ts | 2 +- .../__tests__/contentAPI/resolvers.manage.test.ts | 2 +- packages/api-headless-cms/src/crud/contentEntry.crud.ts | 2 +- 7 files changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts index 65b8c4b1c56..8db71291e2b 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts @@ -118,7 +118,6 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { return await this.cms.createEntryRevisionFrom(model, form.id, { status: "draft", published: false, - publishedOn: undefined, locked: false, stats: { submissions: 0, @@ -222,6 +221,7 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { createdBy: entry.createdBy, createdOn: entry.createdOn, savedOn: entry.savedOn, + publishedOn: entry.publishedOn, locale: entry.locale, tenant: entry.tenant, webinyVersion: entry.webinyVersion, diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts index 08579656ac8..920f15438b2 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts @@ -402,14 +402,6 @@ const triggersField = () => { }); }; -const publishedOnField = () => { - return createModelField({ - label: "Published On", - fieldId: "publishedOn", - type: "datetime" - }); -}; - const slugField = () => { return createModelField({ label: "Slug", @@ -500,7 +492,6 @@ export const createFormDataModelDefinition = (group: CmsModelGroup): CmsPrivateM stepsField(STEP_FIELDS), settingsField(SETTINGS_FIELDS), triggersField(), - publishedOnField(), slugField() ], description: "Form Builder - Form builder create data model", diff --git a/packages/api-form-builder/src/plugins/crud/forms.crud.ts b/packages/api-form-builder/src/plugins/crud/forms.crud.ts index 76384326f78..9bd922d1028 100644 --- a/packages/api-form-builder/src/plugins/crud/forms.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/forms.crud.ts @@ -284,7 +284,6 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { version, locked: false, published: false, - publishedOn: undefined, status: getStatus({ published: false, locked: false @@ -610,7 +609,6 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { }, locked: false, published: false, - publishedOn: undefined, status: getStatus({ published: false, locked: false }), tenant: getTenant().id, webinyVersion: context.WEBINY_VERSION diff --git a/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts b/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts index 7d4c1c2f869..f4a81ab159b 100644 --- a/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts +++ b/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts @@ -103,6 +103,7 @@ export const createFormsTypeDefs = (params: CreateFormsTypeDefsParams): string = ownedBy: FbFormUser! createdOn: DateTime! savedOn: DateTime! + publishedOn: DateTime version: Number! ${fieldTypes.map(f => f.fields).join("\n")} } diff --git a/packages/api-form-builder/src/types.ts b/packages/api-form-builder/src/types.ts index fc170ee8069..00e79a770b9 100644 --- a/packages/api-form-builder/src/types.ts +++ b/packages/api-form-builder/src/types.ts @@ -108,7 +108,7 @@ export interface FbForm { version: number; locked: boolean; published: boolean; - publishedOn: string | Date | undefined; + publishedOn?: string | Date; status: string; fields: FbFormField[]; steps: FbFormStep[]; diff --git a/packages/api-headless-cms/__tests__/contentAPI/resolvers.manage.test.ts b/packages/api-headless-cms/__tests__/contentAPI/resolvers.manage.test.ts index 5d754077e5d..0296e2ed32d 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/resolvers.manage.test.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/resolvers.manage.test.ts @@ -1245,7 +1245,7 @@ describe("MANAGE - Resolvers", () => { ...webiny.meta, locked: false, status: "draft", - publishedOn: expect.stringMatching(/^20/), + publishedOn: null, version: i + 2, revisions: expect.any(Array) }, diff --git a/packages/api-headless-cms/src/crud/contentEntry.crud.ts b/packages/api-headless-cms/src/crud/contentEntry.crud.ts index 71b2417a0c4..a90538702dc 100644 --- a/packages/api-headless-cms/src/crud/contentEntry.crud.ts +++ b/packages/api-headless-cms/src/crud/contentEntry.crud.ts @@ -826,7 +826,7 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm version: nextVersion, savedOn: getDate(rawInput.savedOn, currentDate), createdOn: getDate(rawInput.createdOn, currentDate), - publishedOn: getDate(rawInput.publishedOn, originalEntry.publishedOn), + publishedOn: getDate(rawInput.publishedOn, undefined), createdBy: getIdentity(rawInput.createdBy, originalEntry.createdBy), modifiedBy: getIdentity(rawInput.modifiedBy, null), ownedBy: getIdentity(rawInput.ownedBy, originalEntry.ownedBy), From bc629bcac449ac0543f873cc295675e38636b4ce Mon Sep 17 00:00:00 2001 From: Vitalii Nobis <vitalii.nobis@neatbyte.solutions> Date: Fri, 1 Dec 2023 14:19:04 +0000 Subject: [PATCH 21/37] fix: revert back cwp-template-aws changes --- .../template/ddb-es/apps/api/graphql/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/index.ts b/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/index.ts index cdd3152ce2b..781ae150326 100644 --- a/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/index.ts +++ b/packages/cwp-template-aws/template/ddb-es/apps/api/graphql/src/index.ts @@ -25,7 +25,7 @@ import { createFileManagerStorageOperations } from "@webiny/api-file-manager-ddb import logsPlugins from "@webiny/handler-logs"; import fileManagerS3 from "@webiny/api-file-manager-s3"; import { createFormBuilder } from "@webiny/api-form-builder"; -import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb"; +import { createFormBuilderStorageOperations } from "@webiny/api-form-builder-so-ddb-es"; import { createHeadlessCmsContext, createHeadlessCmsGraphQL } from "@webiny/api-headless-cms"; import { createStorageOperations as createHeadlessCmsStorageOperations } from "@webiny/api-headless-cms-ddb-es"; import { createAco } from "@webiny/api-aco"; @@ -98,7 +98,8 @@ export const handler = createHandler({ }), createFormBuilder({ storageOperations: createFormBuilderStorageOperations({ - documentClient + documentClient, + elasticsearch: elasticsearchClient }) }), createGzipCompression(), From b4de07c6c85fb252242cfa6163efb87f8c007222 Mon Sep 17 00:00:00 2001 From: Vitalii Nobis <vitalii.nobis@neatbyte.solutions> Date: Wed, 6 Dec 2023 11:55:51 +0000 Subject: [PATCH 22/37] fix: revert back api-headless-cms publishedOn changes --- packages/api-form-builder/__tests__/formsSecurity.test.ts | 1 + .../__tests__/contentAPI/resolvers.manage.test.ts | 2 +- packages/api-headless-cms/src/crud/contentEntry.crud.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/api-form-builder/__tests__/formsSecurity.test.ts b/packages/api-form-builder/__tests__/formsSecurity.test.ts index a1e87a30c9b..780a5577ca4 100644 --- a/packages/api-form-builder/__tests__/formsSecurity.test.ts +++ b/packages/api-form-builder/__tests__/formsSecurity.test.ts @@ -518,6 +518,7 @@ describe("Forms Security Test", () => { prefix: "create-revision-form-", id }), + publishedOn: /^20/, status: "draft", version: i + 2 }, diff --git a/packages/api-headless-cms/__tests__/contentAPI/resolvers.manage.test.ts b/packages/api-headless-cms/__tests__/contentAPI/resolvers.manage.test.ts index 0296e2ed32d..5d754077e5d 100644 --- a/packages/api-headless-cms/__tests__/contentAPI/resolvers.manage.test.ts +++ b/packages/api-headless-cms/__tests__/contentAPI/resolvers.manage.test.ts @@ -1245,7 +1245,7 @@ describe("MANAGE - Resolvers", () => { ...webiny.meta, locked: false, status: "draft", - publishedOn: null, + publishedOn: expect.stringMatching(/^20/), version: i + 2, revisions: expect.any(Array) }, diff --git a/packages/api-headless-cms/src/crud/contentEntry.crud.ts b/packages/api-headless-cms/src/crud/contentEntry.crud.ts index a90538702dc..71b2417a0c4 100644 --- a/packages/api-headless-cms/src/crud/contentEntry.crud.ts +++ b/packages/api-headless-cms/src/crud/contentEntry.crud.ts @@ -826,7 +826,7 @@ export const createContentEntryCrud = (params: CreateContentEntryCrudParams): Cm version: nextVersion, savedOn: getDate(rawInput.savedOn, currentDate), createdOn: getDate(rawInput.createdOn, currentDate), - publishedOn: getDate(rawInput.publishedOn, undefined), + publishedOn: getDate(rawInput.publishedOn, originalEntry.publishedOn), createdBy: getIdentity(rawInput.createdBy, originalEntry.createdBy), modifiedBy: getIdentity(rawInput.modifiedBy, null), ownedBy: getIdentity(rawInput.ownedBy, originalEntry.ownedBy), From 0033d87ba25fb5b49e108571da5ed3b44e2c598f Mon Sep 17 00:00:00 2001 From: Vitalii Nobis <vitalii.nobis@neatbyte.solutions> Date: Mon, 11 Dec 2023 13:55:03 +0000 Subject: [PATCH 23/37] fix: use entry publish status --- .../__tests__/formSubmissionSecurity.test.ts | 6 +- .../api-form-builder/__tests__/forms.test.ts | 14 +- .../__tests__/formsSecurity.test.ts | 14 +- .../__tests__/graphql/forms.ts | 8 +- .../cmsFormBuilderStorage/CmsFormsStorage.ts | 23 +-- .../models/form.model.ts | 32 ---- .../src/plugins/crud/forms.crud.ts | 51 ++---- .../src/plugins/crud/utils/getStatus.ts | 7 - .../src/plugins/crud/utils/index.ts | 1 - .../plugins/graphql/createFormsTypeDefs.ts | 23 +-- .../src/plugins/graphql/formsSchema.ts | 18 +- .../src/plugins/graphql/submissionsSchema.ts | 2 +- packages/api-form-builder/src/types.ts | 2 - .../components/FormEditor/Context/graphql.ts | 2 - .../app-form-builder/src/admin/graphql.ts | 1 - .../plugins/editor/defaultBar/Name/Name.tsx | 2 +- .../formDetails/formRevisions/Revision.tsx | 2 +- .../formDetails/formRevisions/useRevision.tsx | 6 +- .../src/admin/views/Forms/FormsDataList.tsx | 2 +- .../FormElementAdvancedSettings.tsx | 156 +++--------------- .../admin/plugins/components/graphql.ts | 5 +- packages/app-form-builder/src/types.ts | 2 - .../form/dataLoaders/getFormDataLoader.ts | 3 +- .../src/renderers/form/dataLoaders/graphql.ts | 4 +- .../src/renderers/form/index.tsx | 23 +-- .../src/renderers/form/types.ts | 2 - 26 files changed, 75 insertions(+), 336 deletions(-) delete mode 100644 packages/api-form-builder/src/plugins/crud/utils/getStatus.ts diff --git a/packages/api-form-builder/__tests__/formSubmissionSecurity.test.ts b/packages/api-form-builder/__tests__/formSubmissionSecurity.test.ts index 246f5d029cd..b4bf3812a30 100644 --- a/packages/api-form-builder/__tests__/formSubmissionSecurity.test.ts +++ b/packages/api-form-builder/__tests__/formSubmissionSecurity.test.ts @@ -93,8 +93,6 @@ describe("Forms Submission Security Test", () => { createdOn: expect.stringMatching(/^20/), savedOn: expect.stringMatching(/^20/), fields: [], - locked: false, - published: false, publishedOn: null, name: "A1-name", overallStats: { @@ -194,9 +192,7 @@ describe("Forms Submission Security Test", () => { } ], status: "published", - published: true, - publishedOn: expect.stringMatching(/^20/), - locked: true + publishedOn: expect.stringMatching(/^20/) }, error: null } diff --git a/packages/api-form-builder/__tests__/forms.test.ts b/packages/api-form-builder/__tests__/forms.test.ts index 0b29a520ec1..3c2d5be636f 100644 --- a/packages/api-form-builder/__tests__/forms.test.ts +++ b/packages/api-form-builder/__tests__/forms.test.ts @@ -254,7 +254,7 @@ describe('Form Builder "Form" Test', () => { await publishRevision({ revision: id }); // Get the published form - const [{ data: get }] = await getPublishedForm({ revision: id }); + const [{ data: get }] = await getPublishedForm({ formId: id }); expect(get.formBuilder.getPublishedForm.data.id).toEqual(id); // Create a new revision @@ -262,7 +262,7 @@ describe('Form Builder "Form" Test', () => { const { id: id2 } = create2.data.formBuilder.createRevisionFrom.data; // Latest published form should still be #1 - const [latestPublished] = await getPublishedForm({ parent: id.split("#")[0] }); + const [latestPublished] = await getPublishedForm({ formId: id.split("#")[0] }); expect(latestPublished.data.formBuilder.getPublishedForm.data.id).toEqual(id); // Latest revision should be #2 @@ -284,7 +284,7 @@ describe('Form Builder "Form" Test', () => { await publishRevision({ revision: id2 }); // Latest published form should now be #2 - const [latestPublished2] = await getPublishedForm({ parent: id.split("#")[0] }); + const [latestPublished2] = await getPublishedForm({ formId: id.split("#")[0] }); expect(latestPublished2.data.formBuilder.getPublishedForm.data.id).toEqual(id2); // Increment views for #2 @@ -302,7 +302,7 @@ describe('Form Builder "Form" Test', () => { await unpublishRevision({ revision: id2 }); // Latest published form should now again be #1 - const [latestPublished3] = await getPublishedForm({ parent: id.split("#")[0] }); + const [latestPublished3] = await getPublishedForm({ formId: id.split("#")[0] }); expect(latestPublished3.data.formBuilder.getPublishedForm.data.id).toEqual(id); }); @@ -470,7 +470,6 @@ describe('Form Builder "Form" Test', () => { publishRevision: { data: { name: "form 2", - published: true, stats: { submissions: 0, views: 0 @@ -496,7 +495,6 @@ describe('Form Builder "Form" Test', () => { data: { id: `${form2.formId}#0002`, version: 2, - published: false, status: "draft" }, error: null @@ -530,7 +528,6 @@ describe('Form Builder "Form" Test', () => { publishRevision: { data: { name: "form 1", - published: true, stats: { submissions: 0, views: 0 @@ -556,7 +553,6 @@ describe('Form Builder "Form" Test', () => { data: { id: `${form1.formId}#0002`, version: 2, - published: false, status: "draft" }, error: null @@ -576,7 +572,6 @@ describe('Form Builder "Form" Test', () => { publishRevision: { data: { name: "form 1", - published: true, stats: { submissions: 0, views: 0 @@ -599,7 +594,6 @@ describe('Form Builder "Form" Test', () => { data: { id: `${form1.formId}#0003`, version: 3, - published: false, status: "draft" }, error: null diff --git a/packages/api-form-builder/__tests__/formsSecurity.test.ts b/packages/api-form-builder/__tests__/formsSecurity.test.ts index 780a5577ca4..725dbc4be3e 100644 --- a/packages/api-form-builder/__tests__/formsSecurity.test.ts +++ b/packages/api-form-builder/__tests__/formsSecurity.test.ts @@ -15,8 +15,6 @@ class MockResponse { public readonly createdOn: RegExp; public readonly savedOn: RegExp; public readonly publishedOn: RegExp | null; - public readonly locked: boolean; - public readonly published: boolean; public readonly status: "published" | "draft"; public readonly version: number; @@ -26,8 +24,6 @@ class MockResponse { this.createdOn = /^20/; this.savedOn = /^20/; this.publishedOn = null; - this.locked = false; - this.published = false; this.status = "draft"; this.version = 1; } @@ -438,9 +434,7 @@ describe("Forms Security Test", () => { id: formId }), publishedOn: /^20/, - published: true, - status: "published", - locked: true + status: "published" }, error: null } @@ -470,9 +464,7 @@ describe("Forms Security Test", () => { id: formId }), publishedOn: /^20/, - published: true, - status: "published", - locked: true + status: "published" }, error: null } @@ -540,9 +532,7 @@ describe("Forms Security Test", () => { id }), publishedOn: /^20/, - published: true, status: "published", - locked: true, version: i + 2 }, error: null diff --git a/packages/api-form-builder/__tests__/graphql/forms.ts b/packages/api-form-builder/__tests__/graphql/forms.ts index d0c8dec6121..9036185af99 100644 --- a/packages/api-form-builder/__tests__/graphql/forms.ts +++ b/packages/api-form-builder/__tests__/graphql/forms.ts @@ -26,8 +26,6 @@ export const FORM_DATA_FIELD = /* GraphQL */ ` } } triggers - published - locked status stats { views @@ -58,10 +56,8 @@ export const FORMS_DATA_FIELD = /* GraphQL */ ` savedOn name slug - published publishedOn version - locked status createdBy { id @@ -190,9 +186,9 @@ export const GET_FORM_REVISIONS = /* GraphQL */ ` `; export const GET_PUBLISHED_FORM = /* GraphQL */ ` - query GetPublishedForm($revision: ID, $parent: ID) { + query GetPublishedForm($formId: ID) { formBuilder { - getPublishedForm(revision: $revision, parent: $parent) { + getPublishedForm(formId: $formId) { data ${FORM_DATA_FIELD} error ${ERROR_FIELD} } diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts index 8db71291e2b..08b776b69a7 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts @@ -1,4 +1,4 @@ -import { CmsEntry, CmsEntryValues, CmsModel, HeadlessCms } from "@webiny/api-headless-cms/types"; +import { CmsEntry, CmsModel, HeadlessCms } from "@webiny/api-headless-cms/types"; import WebinyError from "@webiny/error"; import { Security } from "@webiny/api-security/types"; import { createIdentifier, parseIdentifier } from "@webiny/utils"; @@ -43,17 +43,6 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { return { ...this.model, tenant, locale }; } - private async getSortedFormRevisions( - model: CmsModel, - formId: string - ): Promise<CmsEntry<CmsEntryValues>> { - const entries = (await this.cms.getEntryRevisions(model, formId)) - .filter(entryItem => entryItem.values.published) - .sort((a, b) => b.version - a.version); - - return entries[0]; - } - async getForm(params: FormBuilderStorageOperationsGetFormParams): Promise<FbForm | null> { const { id, @@ -75,7 +64,7 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { return entry; } else if (published && !version) { - const entry = await this.getSortedFormRevisions(model, formId); + const [entry] = await this.cms.getPublishedEntriesByIds(model, [formId]); return entry; } else if (id || version) { @@ -116,9 +105,6 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { const entry = await this.security.withoutAuthorization(async () => { return await this.cms.createEntryRevisionFrom(model, form.id, { - status: "draft", - published: false, - locked: false, stats: { submissions: 0, views: 0 @@ -198,7 +184,7 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { const model = this.modelWithContext(form); const entry = await this.security.withoutAuthorization(async () => { - return await this.cms.updateEntry(model, form.id, form); + return await this.cms.publishEntry(model, form.id); }); return this.getFormFieldValues(entry); @@ -209,7 +195,7 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { const model = this.modelWithContext(form); const entry = await this.security.withoutAuthorization(async () => { - return await this.cms.updateEntry(model, form.id, form); + return await this.cms.unpublishEntry(model, form.id); }); return this.getFormFieldValues(entry); @@ -222,6 +208,7 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { createdOn: entry.createdOn, savedOn: entry.savedOn, publishedOn: entry.publishedOn, + status: entry.status, locale: entry.locale, tenant: entry.tenant, webinyVersion: entry.webinyVersion, diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts index 920f15438b2..6a702466b7d 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts @@ -27,22 +27,6 @@ const nameField = () => { }); }; -const publishedField = () => { - return createModelField({ - label: "Published", - type: "boolean", - validation: [required()] - }); -}; - -const statusField = () => { - return createModelField({ - label: "Status", - type: "text", - validation: [required()] - }); -}; - const statsViewsField = () => { return createModelField({ label: "Views", @@ -86,15 +70,6 @@ const overallStatsField = (fields: CmsModelField[]) => { }); }; -const lockedField = () => { - return createModelField({ - label: "Locked", - fieldId: "locked", - type: "boolean", - validation: [required()] - }); -}; - const field_IdField = () => { return createModelField({ label: "ID", @@ -413,16 +388,12 @@ const slugField = () => { const DEFAULT_FIELDS = [ "formId", "name", - "published", - "status", "stats", "overallStats", - "locked", "fields", "steps", "settings", "triggers", - "publishedOn", "slug" ]; @@ -475,8 +446,6 @@ export const createFormDataModelDefinition = (group: CmsModelGroup): CmsPrivateM fields: [ formIdField(), nameField(), - publishedField(), - statusField(), statsField([ statsViewsField(), statsSubmissionsField(), @@ -487,7 +456,6 @@ export const createFormDataModelDefinition = (group: CmsModelGroup): CmsPrivateM statsSubmissionsField(), conversionRateStatsSubmissionsField() ]), - lockedField(), fieldsField(FIELD_FIELDS), stepsField(STEP_FIELDS), settingsField(SETTINGS_FIELDS), diff --git a/packages/api-form-builder/src/plugins/crud/forms.crud.ts b/packages/api-form-builder/src/plugins/crud/forms.crud.ts index 9bd922d1028..ee2ddffae1a 100644 --- a/packages/api-form-builder/src/plugins/crud/forms.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/forms.crud.ts @@ -27,7 +27,7 @@ import { Tenant } from "@webiny/api-tenancy/types"; import { I18NLocale } from "@webiny/api-i18n/types"; import { createIdentifier, mdbid, parseIdentifier } from "@webiny/utils"; import { createTopic } from "@webiny/pubsub"; -import { getStatus, createFormSettings } from "./utils"; +import { createFormSettings } from "./utils"; import { FormsPermissions } from "~/plugins/crud/permissions/FormsPermissions"; export interface CreateFormsCrudParams { @@ -225,12 +225,12 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { ); } }, - async getLatestPublishedFormRevision(this: FormBuilder, id) { + async getLatestPublishedFormRevision(this: FormBuilder, formId) { let form: FbForm | null = null; try { form = await this.storageOperations.forms.getForm({ where: { - id, + formId, published: true, tenant: getTenant().id, locale: getLocale().code @@ -241,12 +241,12 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { ex.message || "Could not load published form revision by ID.", ex.code || "GET_PUBLISHED_FORM_BY_ID_ERROR", { - id + formId } ); } if (!form) { - throw new NotFoundError(`Form "${id}" was not found!`); + throw new NotFoundError(`Form "${formId}" was not found!`); } return form; }, @@ -282,12 +282,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { name: input.name, slug, version, - locked: false, - published: false, - status: getStatus({ - published: false, - locked: false - }), + status: "draft", stats: { views: 0, submissions: 0 @@ -341,7 +336,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { if (!original) { throw new NotFoundError(`Form "${id}" was not found!`); - } else if (original.locked) { + } else if (original.status === "unpublished") { throw new WebinyError("Not allowed to modify locked form.", "FORM_LOCKED_ERROR", { form: original }); @@ -481,20 +476,11 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { /** * getForm checks for existence of the form. */ - const original = await this.getForm(formId, { + const form = await this.getForm(formId, { auth: false }); - await formsPermissions.ensure({ owns: original.ownedBy }); - - const form: FbForm = { - ...original, - published: true, - publishedOn: new Date().toISOString(), - status: getStatus({ published: true, locked: true }), - locked: true, - webinyVersion: context.WEBINY_VERSION - }; + await formsPermissions.ensure({ owns: form.ownedBy }); try { await onFormBeforePublish.publish({ @@ -504,7 +490,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { form }); await onFormAfterPublish.publish({ - form + form: result }); return result; } catch (ex) { @@ -513,7 +499,6 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { ex.code || "PUBLISH_FORM_ERROR", { ...(ex.data || {}), - original, form } ); @@ -522,19 +507,11 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { async unpublishForm(this: FormBuilder, id) { await formsPermissions.ensure({ rwd: "r", pw: "u" }); - const original = await this.getForm(id, { + const form = await this.getForm(id, { auth: false }); - await formsPermissions.ensure({ owns: original.ownedBy }); - - const form: FbForm = { - ...original, - published: false, - savedOn: new Date().toISOString(), - status: getStatus({ published: false, locked: true }), - webinyVersion: context.WEBINY_VERSION - }; + await formsPermissions.ensure({ owns: form.ownedBy }); try { await onFormBeforeUnpublish.publish({ @@ -553,7 +530,6 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { ex.code || "UNPUBLISH_FORM_ERROR", { ...(ex.data || {}), - original, form } ); @@ -607,9 +583,6 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { displayName: identity.displayName, type: identity.type }, - locked: false, - published: false, - status: getStatus({ published: false, locked: false }), tenant: getTenant().id, webinyVersion: context.WEBINY_VERSION }; diff --git a/packages/api-form-builder/src/plugins/crud/utils/getStatus.ts b/packages/api-form-builder/src/plugins/crud/utils/getStatus.ts deleted file mode 100644 index 04d68d21223..00000000000 --- a/packages/api-form-builder/src/plugins/crud/utils/getStatus.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const getStatus = (params: { published: boolean; locked: boolean }) => { - if (params.published) { - return "published"; - } - - return params.locked ? "locked" : "draft"; -}; diff --git a/packages/api-form-builder/src/plugins/crud/utils/index.ts b/packages/api-form-builder/src/plugins/crud/utils/index.ts index 6e682217f5f..39b965424eb 100644 --- a/packages/api-form-builder/src/plugins/crud/utils/index.ts +++ b/packages/api-form-builder/src/plugins/crud/utils/index.ts @@ -1,4 +1,3 @@ export * from "./flattenSubmissionMeta"; -export * from "./getStatus"; export * from "./sanitizeFormSubmissionData"; export * from "./createFormSettings"; diff --git a/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts b/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts index f4a81ab159b..7b0db0a4149 100644 --- a/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts +++ b/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts @@ -46,11 +46,8 @@ export const createFormsTypeDefs = (params: CreateFormsTypeDefsParams): string = const excludeFormFields = [ "formId", - "published", - "status", "stats", "overallStats", - "locked", "fields", "steps", "settings", @@ -64,14 +61,7 @@ export const createFormsTypeDefs = (params: CreateFormsTypeDefsParams): string = fieldTypePlugins }); - const excludeUpdateFormFields = [ - "formId", - "published", - "status", - "stats", - "overallStats", - "locked" - ]; + const excludeUpdateFormFields = ["formId", "stats", "overallStats"]; const inputUpdateFields = renderInputFields({ models, @@ -85,12 +75,6 @@ export const createFormsTypeDefs = (params: CreateFormsTypeDefsParams): string = return /* GraphQL */ ` ${fieldTypes.map(f => f.typeDefs).join("\n")} - enum FbFormStatusEnum { - published - draft - locked - } - type FbFormUser { id: String displayName: String @@ -104,6 +88,7 @@ export const createFormsTypeDefs = (params: CreateFormsTypeDefsParams): string = createdOn: DateTime! savedOn: DateTime! publishedOn: DateTime + status: String! version: Number! ${fieldTypes.map(f => f.fields).join("\n")} } @@ -147,8 +132,8 @@ export const createFormsTypeDefs = (params: CreateFormsTypeDefsParams): string = # Get form revisions getFormRevisions(id: ID!): FbFormRevisionsResponse - # Get published form by exact revision ID, or parent form ID (public access) - getPublishedForm(revision: ID, parent: ID): FbFormResponse + # Get published form form ID (public access) + getPublishedForm(formId: ID): FbFormResponse } extend type FbMutation { diff --git a/packages/api-form-builder/src/plugins/graphql/formsSchema.ts b/packages/api-form-builder/src/plugins/graphql/formsSchema.ts index bc7e5e87416..6b252ade26e 100644 --- a/packages/api-form-builder/src/plugins/graphql/formsSchema.ts +++ b/packages/api-form-builder/src/plugins/graphql/formsSchema.ts @@ -71,23 +71,11 @@ export const createFormsSchema = (params: CreateFormsTypeDefsParams) => { } }, getPublishedForm: async (_, args: any, { formBuilder }) => { - if (!args.revision && !args.parent) { - return new NotFoundResponse("Revision ID or Form ID missing."); + if (!args.formId) { + return new NotFoundResponse("Form ID missing."); } - let form; - - if (args.revision) { - /** - * This fetches the exact revision specified by revision ID - */ - form = await formBuilder.getForm(args.revision, { auth: false }); - } else if (args.parent) { - /** - * This fetches the latest published revision for given parent form - */ - form = await formBuilder.getLatestPublishedFormRevision(args.parent); - } + const form = await formBuilder.getLatestPublishedFormRevision(args.formId); if (!form) { return new NotFoundResponse("The requested form was not found."); diff --git a/packages/api-form-builder/src/plugins/graphql/submissionsSchema.ts b/packages/api-form-builder/src/plugins/graphql/submissionsSchema.ts index 697a078140f..8ae0279cd84 100644 --- a/packages/api-form-builder/src/plugins/graphql/submissionsSchema.ts +++ b/packages/api-form-builder/src/plugins/graphql/submissionsSchema.ts @@ -69,7 +69,7 @@ export const createSubmissionsSchema = (params: CreateSubmissionsTypeDefsParams) * Get all revisions of the form. */ const revisions = await formBuilder.getFormRevisions(form); - const publishedRevisions = revisions.filter(r => r.published); + const publishedRevisions = revisions.filter(r => r.status === "published"); const rows: Record<string, string>[] = []; const fields: Record<string, string> = {}; diff --git a/packages/api-form-builder/src/types.ts b/packages/api-form-builder/src/types.ts index 00e79a770b9..799969eca87 100644 --- a/packages/api-form-builder/src/types.ts +++ b/packages/api-form-builder/src/types.ts @@ -106,8 +106,6 @@ export interface FbForm { name: string; slug: string; version: number; - locked: boolean; - published: boolean; publishedOn?: string | Date; status: string; fields: FbFormField[]; diff --git a/packages/app-form-builder/src/admin/components/FormEditor/Context/graphql.ts b/packages/app-form-builder/src/admin/components/FormEditor/Context/graphql.ts index a2729d11c73..e9e0fd0b9f3 100644 --- a/packages/app-form-builder/src/admin/components/FormEditor/Context/graphql.ts +++ b/packages/app-form-builder/src/admin/components/FormEditor/Context/graphql.ts @@ -85,8 +85,6 @@ export const GET_FORM = gql` } settings ${SETTINGS_FIELDS} triggers - published - locked status } error ${ERROR_FIELDS} diff --git a/packages/app-form-builder/src/admin/graphql.ts b/packages/app-form-builder/src/admin/graphql.ts index c38b2678d62..45b91f7e907 100644 --- a/packages/app-form-builder/src/admin/graphql.ts +++ b/packages/app-form-builder/src/admin/graphql.ts @@ -18,7 +18,6 @@ const BASE_FORM_FIELDS = ` id name version - published status savedOn createdBy { diff --git a/packages/app-form-builder/src/admin/plugins/editor/defaultBar/Name/Name.tsx b/packages/app-form-builder/src/admin/plugins/editor/defaultBar/Name/Name.tsx index 5e49c5260c8..162857b7300 100644 --- a/packages/app-form-builder/src/admin/plugins/editor/defaultBar/Name/Name.tsx +++ b/packages/app-form-builder/src/admin/plugins/editor/defaultBar/Name/Name.tsx @@ -92,7 +92,7 @@ export const Name = () => { <NameWrapper> <FormMeta> <Typography use={"overline"}>{`status: ${ - state.data.published ? t`published` : t`draft` + state.data.status === "published" ? t`published` : t`draft` }`}</Typography> </FormMeta> <div style={{ width: "100%", display: "flex" }}> diff --git a/packages/app-form-builder/src/admin/plugins/formDetails/formRevisions/Revision.tsx b/packages/app-form-builder/src/admin/plugins/formDetails/formRevisions/Revision.tsx index bd1db10039a..494859a6f0c 100644 --- a/packages/app-form-builder/src/admin/plugins/formDetails/formRevisions/Revision.tsx +++ b/packages/app-form-builder/src/admin/plugins/formDetails/formRevisions/Revision.tsx @@ -37,7 +37,7 @@ const revisionsMenu = css({ const getIcon = (revision: Pick<FbFormModel, "status">) => { switch (revision.status) { - case "locked": + case "unpublished": return { icon: <Icon icon={<LockIcon />} />, text: "This revision is locked (it has already been published)" diff --git a/packages/app-form-builder/src/admin/plugins/formDetails/formRevisions/useRevision.tsx b/packages/app-form-builder/src/admin/plugins/formDetails/formRevisions/useRevision.tsx index b3ca4560479..3258c6b6b00 100644 --- a/packages/app-form-builder/src/admin/plugins/formDetails/formRevisions/useRevision.tsx +++ b/packages/app-form-builder/src/admin/plugins/formDetails/formRevisions/useRevision.tsx @@ -6,6 +6,7 @@ import { CREATE_REVISION_FROM, CreateRevisionFromMutationResponse, CreateRevisionFromMutationVariables, + GET_FORM_REVISIONS, DELETE_REVISION, PUBLISH_REVISION, UNPUBLISH_REVISION @@ -126,7 +127,10 @@ export const useRevision = ({ revision, form }: UseRevisionProps): UseRevisionRe mutation: PUBLISH_REVISION, variables: { revision: id || revision.id - } + }, + refetchQueries: [ + { query: GET_FORM_REVISIONS, variables: { id: id || revision.id } } + ] }); const { error } = res.formBuilder.publishRevision; diff --git a/packages/app-form-builder/src/admin/views/Forms/FormsDataList.tsx b/packages/app-form-builder/src/admin/views/Forms/FormsDataList.tsx index a4f0260fe43..440f53c6fa2 100644 --- a/packages/app-form-builder/src/admin/views/Forms/FormsDataList.tsx +++ b/packages/app-form-builder/src/admin/views/Forms/FormsDataList.tsx @@ -138,7 +138,7 @@ const FormsDataList = (props: FormsDataListProps) => { const handlerKey = form.id + form.status; if (!editHandlers.current[handlerKey]) { editHandlers.current[handlerKey] = async () => { - if (!form.published) { + if (form.status !== "published") { history.push(`/form-builder/forms/${encodeURIComponent(form.id)}`); } diff --git a/packages/app-form-builder/src/page-builder/admin/plugins/components/FormElementAdvancedSettings.tsx b/packages/app-form-builder/src/page-builder/admin/plugins/components/FormElementAdvancedSettings.tsx index 4cfb76b6b9a..cdf40636c65 100644 --- a/packages/app-form-builder/src/page-builder/admin/plugins/components/FormElementAdvancedSettings.tsx +++ b/packages/app-form-builder/src/page-builder/admin/plugins/components/FormElementAdvancedSettings.tsx @@ -1,8 +1,6 @@ import React, { useMemo } from "react"; -import { useLazyQuery, useQuery } from "@apollo/react-hooks"; -import get from "lodash/get"; +import { useQuery } from "@apollo/react-hooks"; import { Grid, Cell } from "@webiny/ui/Grid"; -import { Alert } from "@webiny/ui/Alert"; import { AutoComplete } from "@webiny/ui/AutoComplete"; import styled from "@emotion/styled"; import { validation } from "@webiny/validation"; @@ -12,106 +10,35 @@ import { SimpleButton, classes } from "@webiny/app-page-builder/editor/plugins/elementSettings/components/StyledComponents"; -import { - LIST_FORMS, - GET_FORM_REVISIONS, - GetFormRevisionsQueryResponse, - GetFormRevisionsQueryVariables, - ListFormsQueryResponse -} from "./graphql"; +import { LIST_FORMS, ListFormsQueryResponse } from "./graphql"; import { BindComponent, FormOnSubmit } from "@webiny/form"; -import { FbRevisionModel } from "~/types"; const FormOptionsWrapper = styled("div")({ - minHeight: 250 + minHeight: 150 }); interface FormElementAdvancedSettingsProps { Bind: BindComponent; submit: FormOnSubmit; - data: Record<string, string>; -} -interface RevisionsOutputOption { - name: string; - id: string; + data: Record<string, any>; } -interface RevisionsOutput { - options: RevisionsOutputOption[]; - value: RevisionsOutputOption | null; -} -const FormElementAdvancedSettings = ({ Bind, submit, data }: FormElementAdvancedSettingsProps) => { - const listQuery = useQuery<ListFormsQueryResponse>(LIST_FORMS, { fetchPolicy: "network-only" }); - const selectedForm = useMemo(() => { - return { - parent: get(data, "settings.form.parent"), - revision: get(data, "settings.form.revision") - }; - }, [data]); - - const [getFormRevisions, getQuery] = useLazyQuery< - GetFormRevisionsQueryResponse, - GetFormRevisionsQueryVariables - >(GET_FORM_REVISIONS, { - variables: { - id: selectedForm.parent - } +const FormElementAdvancedSettings = ({ Bind, submit, data }: FormElementAdvancedSettingsProps) => { + const listQuery = useQuery<ListFormsQueryResponse>(LIST_FORMS, { + fetchPolicy: "network-only" }); - const latestRevisions = useMemo(() => { - const output: RevisionsOutput = { - options: [], - value: null - }; - if (listQuery.data) { - const latestFormRevisionsList = - (get( - listQuery, - "data.formBuilder.listForms.data" - ) as unknown as FbRevisionModel[]) || []; - - output.options = latestFormRevisionsList.map(({ id, name }) => ({ id, name })); - output.value = - output.options.find(item => { - if (typeof item.id !== "string" || typeof selectedForm.parent !== "string") { - return false; - } - // Get selected form's "baseId", i.e without the revision number suffix. - const [baseId] = selectedForm.parent.split("#"); - return item.id.includes(baseId); - }) || null; - } - - return output; - }, [listQuery, selectedForm]); + const publishedForms = listQuery?.data?.formBuilder.listForms.data || []; - const publishedRevisions = useMemo(() => { - const output: RevisionsOutput = { - options: [], - value: null - }; - - if (getQuery.data) { - const publishedRevisions = ( - get(getQuery, "data.formBuilder.getFormRevisions.data") as FbRevisionModel[] - ).filter(revision => revision.published); - output.options = publishedRevisions.map(item => ({ - id: item.id, - name: `${item.name} (version ${item.version})` - })); - - if (output.options.length > 0) { - output.options.unshift({ - id: "latest", - name: "Latest published revision" - }); - } - - output.value = output.options.find(item => item.id === selectedForm.revision) || null; - } + const publishedFormsOptions = useMemo( + () => + publishedForms.map(publishedForm => ({ + id: publishedForm.formId, + name: publishedForm.name + })), + [publishedForms] + ); - return output; - }, [getQuery, selectedForm]); // required so ts build does not break const buttonProps: any = {}; @@ -120,58 +47,19 @@ const FormElementAdvancedSettings = ({ Bind, submit, data }: FormElementAdvanced <FormOptionsWrapper> <Grid className={classes.simpleGrid}> <Cell span={12}> - <Bind - name={"settings.form.parent"} - validators={validation.create("required")} - > + <Bind name={"settings.formId"} validators={validation.create("required")}> {({ onChange }) => ( <AutoComplete - options={latestRevisions.options} - value={latestRevisions.value || undefined} - onChange={value => { - onChange(value); - getFormRevisions(); - }} + options={publishedFormsOptions} label={"Form"} + value={publishedFormsOptions.find( + option => option.id === data.settings.formId + )} + onChange={onChange} /> )} </Bind> </Cell> - <Cell span={12}> - <Bind - name={"settings.form.revision"} - validators={validation.create("required")} - > - {({ onChange }) => { - const parentSelected = !!latestRevisions.value; - const noPublished = publishedRevisions.options.length === 0; - if (getQuery.loading) { - return <span>Loading revisions...</span>; - } - - const description = "Choose a published revision."; - if (parentSelected && noPublished) { - return ( - <Alert type="danger" title="Form not published"> - Please publish the form and then you can insert it into - your page. - </Alert> - ); - } else { - return ( - <AutoComplete - label={"Revision"} - description={description} - disabled={!parentSelected || noPublished} - options={publishedRevisions.options} - value={publishedRevisions.value || undefined} - onChange={onChange} - /> - ); - } - }} - </Bind> - </Cell> <Cell span={12}> <ButtonContainer {...buttonProps}> <SimpleButton onClick={submit} {...buttonProps}> diff --git a/packages/app-form-builder/src/page-builder/admin/plugins/components/graphql.ts b/packages/app-form-builder/src/page-builder/admin/plugins/components/graphql.ts index a38470b80db..08b665bb30d 100644 --- a/packages/app-form-builder/src/page-builder/admin/plugins/components/graphql.ts +++ b/packages/app-form-builder/src/page-builder/admin/plugins/components/graphql.ts @@ -8,7 +8,7 @@ import { FbErrorResponse, FbFormModel, FbRevisionModel } from "~/types"; export interface ListFormsQueryResponse { formBuilder: { listForms: { - data: Pick<FbFormModel, "id" | "name">[]; + data: Pick<FbFormModel, "formId" | "name">[]; error: FbErrorResponse | null; }; }; @@ -18,7 +18,7 @@ export const LIST_FORMS = gql` formBuilder { listForms { data { - id + formId name } error { @@ -52,7 +52,6 @@ export const GET_FORM_REVISIONS = gql` data { id name - published version } error { diff --git a/packages/app-form-builder/src/types.ts b/packages/app-form-builder/src/types.ts index 30015bba7dd..64dd8604de5 100644 --- a/packages/app-form-builder/src/types.ts +++ b/packages/app-form-builder/src/types.ts @@ -142,7 +142,6 @@ export interface FbRevisionModel { id: string; name: string; version: number; - published: boolean; status: string; savedOn: string; createdBy: FbCreatedBy; @@ -173,7 +172,6 @@ export interface FbFormModel { version: number; fields: FbFormModelField[]; steps: FbFormStep[]; - published: boolean; name: string; settings: any; status: string; diff --git a/packages/app-page-builder-elements/src/renderers/form/dataLoaders/getFormDataLoader.ts b/packages/app-page-builder-elements/src/renderers/form/dataLoaders/getFormDataLoader.ts index 5ff89ae74bd..3b1a9068842 100644 --- a/packages/app-page-builder-elements/src/renderers/form/dataLoaders/getFormDataLoader.ts +++ b/packages/app-page-builder-elements/src/renderers/form/dataLoaders/getFormDataLoader.ts @@ -9,8 +9,7 @@ export interface CreateGetFormDataLoaderParams { } export interface GetFormDataLoaderVariables { - revision?: string; - parent?: string; + formId?: string; } export type GetFormDataLoaderResult = FormData; diff --git a/packages/app-page-builder-elements/src/renderers/form/dataLoaders/graphql.ts b/packages/app-page-builder-elements/src/renderers/form/dataLoaders/graphql.ts index 384e2580c9f..e20a45090ef 100644 --- a/packages/app-page-builder-elements/src/renderers/form/dataLoaders/graphql.ts +++ b/packages/app-page-builder-elements/src/renderers/form/dataLoaders/graphql.ts @@ -1,7 +1,7 @@ export const GET_PUBLISHED_FORM = /* GraphQL */ ` - query FbGetPublishedForm($revision: ID, $parent: ID) @ps(cache: true) { + query FbGetPublishedForm($formId: ID) @ps(cache: true) { formBuilder { - getPublishedForm(revision: $revision, parent: $parent) { + getPublishedForm(formId: $formId) { data { id formId diff --git a/packages/app-page-builder-elements/src/renderers/form/index.tsx b/packages/app-page-builder-elements/src/renderers/form/index.tsx index 51075372697..c3d308830bb 100644 --- a/packages/app-page-builder-elements/src/renderers/form/index.tsx +++ b/packages/app-page-builder-elements/src/renderers/form/index.tsx @@ -9,10 +9,7 @@ export type FormRenderer = ReturnType<typeof createForm>; export interface FormElementData { settings: { - form?: { - parent?: string; - revision?: string; - }; + formId?: string; }; } @@ -24,15 +21,8 @@ export const createForm = (params: CreateFormParams) => { const element = getElement<FormElementData>(); - const form = element.data.settings?.form; - const variables: GetFormDataLoaderVariables = {}; - if (form) { - if (form.revision === "latest") { - variables.parent = form.parent; - } else { - variables.revision = form.revision; - } - } + const formId = element.data.settings?.formId; + const variables: GetFormDataLoaderVariables = { formId }; const variablesHash = JSON.stringify({ variables, headers }); @@ -56,11 +46,10 @@ export const createForm = (params: CreateFormParams) => { useEffect(() => { if (preloadedFormData) { - return; + setFormData(preloadedFormData); } - const hasRequiredVariables = variables.parent || variables.revision; - if (!hasRequiredVariables) { + if (!variables.formId) { return; } @@ -79,7 +68,7 @@ export const createForm = (params: CreateFormParams) => { } }, [variablesHash]); - if (!(variables.parent || variables.revision)) { + if (!variables.formId) { if (params.renderFormNotSelected) { return params.renderFormNotSelected({}); } diff --git a/packages/app-page-builder-elements/src/renderers/form/types.ts b/packages/app-page-builder-elements/src/renderers/form/types.ts index d7b5fc03ad5..6dcabbdde97 100644 --- a/packages/app-page-builder-elements/src/renderers/form/types.ts +++ b/packages/app-page-builder-elements/src/renderers/form/types.ts @@ -37,7 +37,6 @@ export interface FormDataRevision { id: string; name: string; version: number; - published: boolean; status: string; savedOn: string; createdBy: FormDataCreatedBy; @@ -55,7 +54,6 @@ export interface FormData { version: number; fields: FormDataField[]; steps: FormDataStep[]; - published: boolean; name: string; settings: any; status: string; From f3da9b1a839697e9548cff8e75b678b8578be00f Mon Sep 17 00:00:00 2001 From: Vitalii Nobis <vitalii.nobis@neatbyte.solutions> Date: Thu, 14 Dec 2023 15:27:15 +0000 Subject: [PATCH 24/37] fix: requested changes and add test --- .../api-form-builder/__tests__/forms.test.ts | 41 +++++++++++++ .../src/plugins/crud/forms.crud.ts | 61 ++----------------- .../src/plugins/graphql/submissionsSchema.ts | 6 +- packages/api-form-builder/src/types.ts | 6 +- .../plugins/editor/defaultBar/Name/Name.tsx | 4 +- .../formDetails/formRevisions/Revision.tsx | 12 ++-- .../src/admin/views/Forms/FormsDataList.tsx | 4 +- .../FormElementAdvancedSettings.tsx | 11 +++- packages/app-form-builder/src/types.ts | 6 ++ 9 files changed, 78 insertions(+), 73 deletions(-) diff --git a/packages/api-form-builder/__tests__/forms.test.ts b/packages/api-form-builder/__tests__/forms.test.ts index 3c2d5be636f..d766c779147 100644 --- a/packages/api-form-builder/__tests__/forms.test.ts +++ b/packages/api-form-builder/__tests__/forms.test.ts @@ -246,6 +246,47 @@ describe('Form Builder "Form" Test', () => { expect(list.data.formBuilder.listForms.data.length).toBe(0); }); + test("should create revision from specified revision", async () => { + // Create revision #1 + const [create1] = await createForm({ data: { name: "first" } }); + const data1 = create1.data.formBuilder.createForm.data; + + // Create revision #2 from revision #1 + const [create2] = await createRevisionFrom({ revision: data1.id }); + const data2 = create2.data.formBuilder.createRevisionFrom.data; + + // Update revision #2 name + const [update2] = await updateRevision({ + revision: data2.id, + data: { name: "second" } + }); + const data2updated = update2.data.formBuilder.updateRevision.data; + + // Create revision #3 from revision #1 + const [create3] = await createRevisionFrom({ revision: data1.id }); + + // Revision #3 data should match revision #1 + expect(create3.data.formBuilder.createRevisionFrom.data).toMatchObject({ + ...data1, + id: expect.any(String), + createdOn: /^20/, + savedOn: /^20/, + version: 3 + }); + + // Create revision #4 from revision #2 + const [create4] = await createRevisionFrom({ revision: data2.id }); + + // Revision #4 data should match revision #2 + expect(create4.data.formBuilder.createRevisionFrom.data).toMatchObject({ + ...data2updated, + id: expect.any(String), + createdOn: /^20/, + savedOn: /^20/, + version: 4 + }); + }); + test("should publish, add views and unpublish", async () => { const [create] = await createForm({ data: { name: "contact-us" } }); const { id } = create.data.formBuilder.createForm.data; diff --git a/packages/api-form-builder/src/plugins/crud/forms.crud.ts b/packages/api-form-builder/src/plugins/crud/forms.crud.ts index ee2ddffae1a..5a605993a91 100644 --- a/packages/api-form-builder/src/plugins/crud/forms.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/forms.crud.ts @@ -20,7 +20,8 @@ import { OnFormRevisionBeforeCreateTopicParams, OnFormRevisionBeforeDeleteTopicParams, OnFormBeforeUnpublishTopicParams, - OnFormBeforeUpdateTopicParams + OnFormBeforeUpdateTopicParams, + FORM_STATUS } from "~/types"; import WebinyError from "@webiny/error"; import { Tenant } from "@webiny/api-tenancy/types"; @@ -282,7 +283,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { name: input.name, slug, version, - status: "draft", + status: FORM_STATUS.DRAFT, stats: { views: 0, submissions: 0 @@ -336,7 +337,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { if (!original) { throw new NotFoundError(`Form "${id}" was not found!`); - } else if (original.status === "unpublished") { + } else if (original.status === FORM_STATUS.UNPUBLISHED) { throw new WebinyError("Not allowed to modify locked form.", "FORM_LOCKED_ERROR", { form: original }); @@ -538,67 +539,18 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { async createFormRevision(this: FormBuilder, id) { await formsPermissions.ensure({ rwd: "w" }); - const original = await this.getForm(id, { + const form = await this.getForm(id, { auth: false }); - const { id: originalFormFormId } = parseIdentifier(original.id); - const latest = await this.storageOperations.forms.getForm({ - where: { - id, - latest: true, - tenant: original.tenant, - locale: original.locale - } - }); - if (!latest) { - throw new WebinyError( - "Could not fetch latest form revision.", - "LATEST_FORM_REVISION_ERROR", - { - formId: originalFormFormId, - tenant: original.tenant, - locale: original.locale - } - ); - } - - const identity = context.security.getIdentity(); - const version = (latest ? latest.version : original.version) + 1; - - const form: FbForm = { - ...original, - id: createIdentifier({ - id: originalFormFormId, - version - }), - version, - stats: { - submissions: 0, - views: 0 - }, - savedOn: new Date().toISOString(), - createdOn: new Date().toISOString(), - createdBy: { - id: identity.id, - displayName: identity.displayName, - type: identity.type - }, - tenant: getTenant().id, - webinyVersion: context.WEBINY_VERSION - }; try { await onFormRevisionBeforeCreate.publish({ - original, - latest, form }); const result = await this.storageOperations.forms.createFormFrom({ - form: latest + form }); await onFormRevisionAfterCreate.publish({ - original, - latest, form: result }); return result; @@ -608,7 +560,6 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { ex.code || "CREATE_FORM_FROM_ERROR", { ...(ex.data || {}), - original, form } ); diff --git a/packages/api-form-builder/src/plugins/graphql/submissionsSchema.ts b/packages/api-form-builder/src/plugins/graphql/submissionsSchema.ts index 8ae0279cd84..a68f2dcaf74 100644 --- a/packages/api-form-builder/src/plugins/graphql/submissionsSchema.ts +++ b/packages/api-form-builder/src/plugins/graphql/submissionsSchema.ts @@ -15,7 +15,7 @@ import { createSubmissionsTypeDefs, CreateSubmissionsTypeDefsParams } from "~/plugins/graphql/createSubmissionsTypeDefs"; -import { FormBuilderContext, FbFormField } from "~/types"; +import { FormBuilderContext, FbFormField, FORM_STATUS } from "~/types"; export const createSubmissionsSchema = (params: CreateSubmissionsTypeDefsParams) => { const submissionsGraphQL = new GraphQLSchemaPlugin<FormBuilderContext>({ @@ -69,7 +69,9 @@ export const createSubmissionsSchema = (params: CreateSubmissionsTypeDefsParams) * Get all revisions of the form. */ const revisions = await formBuilder.getFormRevisions(form); - const publishedRevisions = revisions.filter(r => r.status === "published"); + const publishedRevisions = revisions.filter( + r => r.status === FORM_STATUS.PUBLISHED + ); const rows: Record<string, string>[] = []; const fields: Record<string, string> = {}; diff --git a/packages/api-form-builder/src/types.ts b/packages/api-form-builder/src/types.ts index 799969eca87..2bb92ab8c95 100644 --- a/packages/api-form-builder/src/types.ts +++ b/packages/api-form-builder/src/types.ts @@ -6,6 +6,8 @@ import { CmsEntryListWhere } from "@webiny/api-headless-cms/types"; import { I18NContext } from "@webiny/api-i18n/types"; import { Topic } from "@webiny/pubsub/types"; +export { CONTENT_ENTRY_STATUS as FORM_STATUS } from "@webiny/api-headless-cms/types"; + interface FbFormTriggerData { urls?: string[]; [key: string]: any; @@ -174,13 +176,9 @@ export interface OnFormAfterCreateTopicParams { } export interface OnFormRevisionBeforeCreateTopicParams { form: FbForm; - original: FbForm; - latest: FbForm; } export interface OnFormRevisionAfterCreateTopicParams { form: FbForm; - original: FbForm; - latest: FbForm; } export interface OnFormBeforeUpdateTopicParams { form: FbForm; diff --git a/packages/app-form-builder/src/admin/plugins/editor/defaultBar/Name/Name.tsx b/packages/app-form-builder/src/admin/plugins/editor/defaultBar/Name/Name.tsx index 162857b7300..4bd4309139b 100644 --- a/packages/app-form-builder/src/admin/plugins/editor/defaultBar/Name/Name.tsx +++ b/packages/app-form-builder/src/admin/plugins/editor/defaultBar/Name/Name.tsx @@ -17,6 +17,8 @@ import { NameWrapper } from "./NameStyled"; import { i18n } from "@webiny/app/i18n"; +import { FORM_STATUS } from "~/types"; + const t = i18n.namespace("FormEditor.Name"); declare global { @@ -92,7 +94,7 @@ export const Name = () => { <NameWrapper> <FormMeta> <Typography use={"overline"}>{`status: ${ - state.data.status === "published" ? t`published` : t`draft` + state.data.status === FORM_STATUS.PUBLISHED ? t`published` : t`draft` }`}</Typography> </FormMeta> <div style={{ width: "100%", display: "flex" }}> diff --git a/packages/app-form-builder/src/admin/plugins/formDetails/formRevisions/Revision.tsx b/packages/app-form-builder/src/admin/plugins/formDetails/formRevisions/Revision.tsx index 494859a6f0c..ade6ddb0392 100644 --- a/packages/app-form-builder/src/admin/plugins/formDetails/formRevisions/Revision.tsx +++ b/packages/app-form-builder/src/admin/plugins/formDetails/formRevisions/Revision.tsx @@ -24,7 +24,7 @@ import { ReactComponent as MoreVerticalIcon } from "../../../icons/more_vert.svg import { ReactComponent as PublishIcon } from "../../../icons/publish.svg"; import { ReactComponent as UnpublishIcon } from "../../../icons/unpublish.svg"; import { useRevision } from "./useRevision"; -import { FbFormModel, FbRevisionModel } from "~/types"; +import { FORM_STATUS, FbFormModel, FbRevisionModel } from "~/types"; import { usePermission } from "~/hooks/usePermission"; const primaryColor = css({ color: "var(--mdc-theme-primary)" }); @@ -37,12 +37,12 @@ const revisionsMenu = css({ const getIcon = (revision: Pick<FbFormModel, "status">) => { switch (revision.status) { - case "unpublished": + case FORM_STATUS.UNPUBLISHED: return { icon: <Icon icon={<LockIcon />} />, text: "This revision is locked (it has already been published)" }; - case "published": + case FORM_STATUS.PUBLISHED: return { icon: <Icon icon={<BeenHereIcon />} className={primaryColor} />, text: "This revision is currently published!" @@ -104,7 +104,7 @@ const Revision = (props: RevisionProps) => { New from current </MenuItem> )} - {revision.status === "draft" && canUpdate(form) && ( + {revision.status === FORM_STATUS.DRAFT && canUpdate(form) && ( <MenuItem onClick={() => editRevision(revision.id)} data-testid={"fb.form-revisions.action-menu.edit"} @@ -116,7 +116,7 @@ const Revision = (props: RevisionProps) => { </MenuItem> )} - {revision.status !== "published" && canPublish() && ( + {revision.status !== FORM_STATUS.PUBLISHED && canPublish() && ( <MenuItem onClick={() => publishRevision(revision.id)} data-testid={"fb.form-revisions.action-menu.publish"} @@ -128,7 +128,7 @@ const Revision = (props: RevisionProps) => { </MenuItem> )} - {revision.status === "published" && canUnpublish() && ( + {revision.status === FORM_STATUS.PUBLISHED && canUnpublish() && ( <ConfirmationDialog title="Confirmation required!" message={ diff --git a/packages/app-form-builder/src/admin/views/Forms/FormsDataList.tsx b/packages/app-form-builder/src/admin/views/Forms/FormsDataList.tsx index 440f53c6fa2..81c21dd3ca4 100644 --- a/packages/app-form-builder/src/admin/views/Forms/FormsDataList.tsx +++ b/packages/app-form-builder/src/admin/views/Forms/FormsDataList.tsx @@ -45,7 +45,7 @@ import { ExportFormsButton } from "~/admin/plugins/formsDataList/ExportButton"; import { OptionsMenu } from "~/admin/components/OptionsMenu"; import { useForms } from "./useForms"; import { deserializeSorters } from "../utils"; -import { FbFormModel, FbRevisionModel } from "~/types"; +import { FORM_STATUS, FbFormModel, FbRevisionModel } from "~/types"; const t = i18n.namespace("FormsApp.FormsDataList"); const rightAlign = css({ @@ -138,7 +138,7 @@ const FormsDataList = (props: FormsDataListProps) => { const handlerKey = form.id + form.status; if (!editHandlers.current[handlerKey]) { editHandlers.current[handlerKey] = async () => { - if (form.status !== "published") { + if (form.status !== FORM_STATUS.PUBLISHED) { history.push(`/form-builder/forms/${encodeURIComponent(form.id)}`); } diff --git a/packages/app-form-builder/src/page-builder/admin/plugins/components/FormElementAdvancedSettings.tsx b/packages/app-form-builder/src/page-builder/admin/plugins/components/FormElementAdvancedSettings.tsx index cdf40636c65..4556a9a7362 100644 --- a/packages/app-form-builder/src/page-builder/admin/plugins/components/FormElementAdvancedSettings.tsx +++ b/packages/app-form-builder/src/page-builder/admin/plugins/components/FormElementAdvancedSettings.tsx @@ -1,5 +1,6 @@ import React, { useMemo } from "react"; import { useQuery } from "@apollo/react-hooks"; +import get from "lodash/get"; import { Grid, Cell } from "@webiny/ui/Grid"; import { AutoComplete } from "@webiny/ui/AutoComplete"; import styled from "@emotion/styled"; @@ -39,6 +40,12 @@ const FormElementAdvancedSettings = ({ Bind, submit, data }: FormElementAdvanced [publishedForms] ); + const selectedOption = useMemo(() => { + const formId = get(data, "settings.formId"); + + return publishedFormsOptions.find(option => option.id === formId); + }, [data, publishedFormsOptions]); + // required so ts build does not break const buttonProps: any = {}; @@ -52,9 +59,7 @@ const FormElementAdvancedSettings = ({ Bind, submit, data }: FormElementAdvanced <AutoComplete options={publishedFormsOptions} label={"Form"} - value={publishedFormsOptions.find( - option => option.id === data.settings.formId - )} + value={selectedOption} onChange={onChange} /> )} diff --git a/packages/app-form-builder/src/types.ts b/packages/app-form-builder/src/types.ts index 64dd8604de5..15e5d9b5efa 100644 --- a/packages/app-form-builder/src/types.ts +++ b/packages/app-form-builder/src/types.ts @@ -473,3 +473,9 @@ export interface FormBuilderImportExportSubTask { }; error: Record<string, string>; } + +export enum FORM_STATUS { + DRAFT = "draft", + PUBLISHED = "published", + UNPUBLISHED = "unpublished" +} From 6c5da6f09e7e8d6ec6a0406ec097bf6b0ffda701 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone <leonardo.giacone@gmail.com> Date: Tue, 9 Jan 2024 20:22:07 +0100 Subject: [PATCH 25/37] fix: remove reCaptcha settings fields from form entry --- .../models/form.model.ts | 43 +------------------ .../plugins/graphql/createFormsTypeDefs.ts | 15 +++++++ 2 files changed, 16 insertions(+), 42 deletions(-) diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts index 6a702466b7d..48a10c6a12e 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts @@ -238,39 +238,6 @@ const settingsReCaptchaEnabledField = () => { }); }; -const settingsReCaptchaSettingsEnabledField = () => { - return createModelField({ - label: "Enabled", - type: "boolean" - }); -}; - -const settingsReCaptchaSettingsSecretKeyField = () => { - return createModelField({ - label: "Secret Key", - fieldId: "secretKey", - type: "text" - }); -}; - -const settingsReCaptchaSettingsSiteKeyField = () => { - return createModelField({ - label: "Site Key", - fieldId: "siteKey", - type: "text" - }); -}; - -const settingsReCaptchaSettingsField = (fields: CmsModelField[]) => { - return createModelField({ - label: "Settings", - type: "object", - settings: { - fields - } - }); -}; - const settingsReCaptchaErrorMessageField = () => { return createModelField({ label: "ErrorMessage", @@ -407,15 +374,7 @@ const SETTINGS_FIELDS: CmsModelField[] = [ settingsTermsOfServiceMessageMessageField(), settingsTermsOfServiceMessageErrorMessageField() ]), - settingsReCaptchaField([ - settingsReCaptchaEnabledField(), - settingsReCaptchaSettingsField([ - settingsReCaptchaSettingsEnabledField(), - settingsReCaptchaSettingsSecretKeyField(), - settingsReCaptchaSettingsSiteKeyField() - ]), - settingsReCaptchaErrorMessageField() - ]) + settingsReCaptchaField([settingsReCaptchaEnabledField(), settingsReCaptchaErrorMessageField()]) ]; export const FIELD_FIELDS = [ diff --git a/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts b/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts index 7b0db0a4149..d498a6f0978 100644 --- a/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts +++ b/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts @@ -121,6 +121,21 @@ export const createFormsTypeDefs = (params: CreateFormsTypeDefsParams): string = type FbSaveFormViewResponse { error: FbError } + + + type FbForm_Settings_ReCaptcha_Settings { + enabled: Boolean + secretKey: String + siteKey: String + } + + extend type FbForm_Settings_ReCaptcha { + settings: FbForm_Settings_ReCaptcha_Settings + } + + extend input FbForm_Settings_ReCaptchaInput { + settings: JSON + } extend type FbQuery { # Get form (can be published or not, requires authorization ) From cca3b82e5b59b1e0ba870dc3a1b413f83f206c22 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone <leonardo.giacone@gmail.com> Date: Wed, 10 Jan 2024 17:39:56 +0100 Subject: [PATCH 26/37] fix: change Recaptcha error message field type --- .../src/cmsFormBuilderStorage/models/form.model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts index 48a10c6a12e..3a268003a57 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts @@ -241,7 +241,7 @@ const settingsReCaptchaEnabledField = () => { const settingsReCaptchaErrorMessageField = () => { return createModelField({ label: "ErrorMessage", - type: "json" + type: "text" }); }; From 2d51809243b74b2edacfc28e17c74b5e2945b2c2 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone <leonardo.giacone@gmail.com> Date: Wed, 24 Jan 2024 12:21:58 +0100 Subject: [PATCH 27/37] chore: merge next --- .../api-form-builder/__tests__/forms.test.ts | 3 +-- .../__tests__/graphql/forms.ts | 5 ---- .../cmsFormBuilderStorage/CmsFormsStorage.ts | 3 +-- .../CmsSubmissionsStorage.ts | 2 +- .../models/form.model.ts | 14 ---------- .../models/submission.model.ts | 4 --- .../src/plugins/crud/forms.crud.ts | 17 +++++------- .../src/plugins/crud/submissions.crud.ts | 2 +- .../plugins/graphql/createFormsTypeDefs.ts | 1 - packages/api-form-builder/src/types.ts | 27 ++++++++----------- .../src/graphqlFields/index.ts | 4 +-- 11 files changed, 22 insertions(+), 60 deletions(-) diff --git a/packages/api-form-builder/__tests__/forms.test.ts b/packages/api-form-builder/__tests__/forms.test.ts index d766c779147..8a0068bda6f 100644 --- a/packages/api-form-builder/__tests__/forms.test.ts +++ b/packages/api-form-builder/__tests__/forms.test.ts @@ -55,8 +55,7 @@ describe('Form Builder "Form" Test', () => { createdOn: /^20/, savedOn: /^20/, status: "draft", - createdBy: defaultIdentity, - ownedBy: defaultIdentity + createdBy: defaultIdentity }, error: null } diff --git a/packages/api-form-builder/__tests__/graphql/forms.ts b/packages/api-form-builder/__tests__/graphql/forms.ts index 9036185af99..227361f7980 100644 --- a/packages/api-form-builder/__tests__/graphql/forms.ts +++ b/packages/api-form-builder/__tests__/graphql/forms.ts @@ -41,11 +41,6 @@ export const FORM_DATA_FIELD = /* GraphQL */ ` displayName type } - ownedBy { - id - displayName - type - } } `; diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts index 08b776b69a7..b9b6c166f71 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts @@ -207,13 +207,12 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { createdBy: entry.createdBy, createdOn: entry.createdOn, savedOn: entry.savedOn, - publishedOn: entry.publishedOn, + publishedOn: entry.lastPublishedOn, status: entry.status, locale: entry.locale, tenant: entry.tenant, webinyVersion: entry.webinyVersion, version: entry.version, - ownedBy: entry.ownedBy, ...entry.values } as FbForm; } diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts index 6f83cd7e847..c0aa471a5d0 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts @@ -102,7 +102,7 @@ export class CmsSubmissionsStorage implements FormBuilderSubmissionStorageOperat private getSubmissionValues(entry: CmsEntry) { return { id: entry.entryId, - ownedBy: entry.createdBy, + createdBy: entry.createdBy, createdOn: entry.createdOn, savedOn: entry.savedOn, locale: entry.locale, diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts index 3a268003a57..a3aac7f7e94 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts @@ -352,18 +352,6 @@ const slugField = () => { }); }; -const DEFAULT_FIELDS = [ - "formId", - "name", - "stats", - "overallStats", - "fields", - "steps", - "settings", - "triggers", - "slug" -]; - const SETTINGS_FIELDS: CmsModelField[] = [ settingsLayoutField([settingsLayoutRendererField()]), settingsSubmitButtonLabelField(), @@ -401,7 +389,6 @@ export const createFormDataModelDefinition = (group: CmsModelGroup): CmsPrivateM name: "FbForm", modelId: "fbForm", titleFieldId: "name", - layout: DEFAULT_FIELDS.map(field => [field]), fields: [ formIdField(), nameField(), @@ -421,7 +408,6 @@ export const createFormDataModelDefinition = (group: CmsModelGroup): CmsPrivateM triggersField(), slugField() ], - description: "Form Builder - Form builder create data model", isPrivate: true, group, noValidate: true diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/submission.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/submission.model.ts index ff3c1040066..2fcd5adce5c 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/models/submission.model.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/submission.model.ts @@ -134,15 +134,12 @@ const logsField = () => { }); }; -const DEFAULT_FIELDS = ["data", "meta", "form"]; - export const createSubmissionDataModelDefinition = (group: CmsModelGroup): CmsPrivateModelFull => { return { name: "FbSubmission", modelId: "fbSubmission", titleFieldId: "", group, - layout: DEFAULT_FIELDS.map(field => [field]), fields: [ dataField(), metaField([ @@ -160,7 +157,6 @@ export const createSubmissionDataModelDefinition = (group: CmsModelGroup): CmsPr ]), logsField() ], - description: "Form Builder - Submission content model", isPrivate: true, noValidate: true }; diff --git a/packages/api-form-builder/src/plugins/crud/forms.crud.ts b/packages/api-form-builder/src/plugins/crud/forms.crud.ts index 5a605993a91..0c8ad1bfc90 100644 --- a/packages/api-form-builder/src/plugins/crud/forms.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/forms.crud.ts @@ -136,7 +136,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { } if (options?.auth !== false) { - await formsPermissions.ensure({ owns: form.ownedBy }); + await formsPermissions.ensure({ owns: form.createdBy }); } return form; @@ -275,11 +275,6 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { displayName: identity.displayName, type: identity.type }, - ownedBy: { - id: identity.id, - displayName: identity.displayName, - type: identity.type - }, name: input.name, slug, version, @@ -343,7 +338,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { }); } - await formsPermissions.ensure({ owns: original.ownedBy }); + await formsPermissions.ensure({ owns: original.createdBy }); const form: FbForm = { ...original, @@ -390,7 +385,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { throw new NotFoundError(`Form ${id} was not found!`); } - await formsPermissions.ensure({ owns: form.ownedBy }); + await formsPermissions.ensure({ owns: form.createdBy }); try { await onFormBeforeDelete.publish({ @@ -420,7 +415,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { auth: false }); - await formsPermissions.ensure({ owns: form.ownedBy }); + await formsPermissions.ensure({ owns: form.createdBy }); const { id: formId } = parseIdentifier(form.id); @@ -481,7 +476,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { auth: false }); - await formsPermissions.ensure({ owns: form.ownedBy }); + await formsPermissions.ensure({ owns: form.createdBy }); try { await onFormBeforePublish.publish({ @@ -512,7 +507,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { auth: false }); - await formsPermissions.ensure({ owns: form.ownedBy }); + await formsPermissions.ensure({ owns: form.createdBy }); try { await onFormBeforeUnpublish.publish({ diff --git a/packages/api-form-builder/src/plugins/crud/submissions.crud.ts b/packages/api-form-builder/src/plugins/crud/submissions.crud.ts index 984ccc9e69b..74767417c0f 100644 --- a/packages/api-form-builder/src/plugins/crud/submissions.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/submissions.crud.ts @@ -293,7 +293,7 @@ export const createSubmissionsCrud = (params: CreateSubmissionsCrudParams): Subm savedOn: new Date().toISOString(), id: mdbid(), locale: form.locale, - ownedBy: form.ownedBy, + createdBy: form.createdBy, tenant: form.tenant, logs: [], webinyVersion: context.WEBINY_VERSION diff --git a/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts b/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts index d498a6f0978..1afe691166a 100644 --- a/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts +++ b/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts @@ -84,7 +84,6 @@ export const createFormsTypeDefs = (params: CreateFormsTypeDefsParams): string = type FbForm { id: ID! createdBy: FbFormUser! - ownedBy: FbFormUser! createdOn: DateTime! savedOn: DateTime! publishedOn: DateTime diff --git a/packages/api-form-builder/src/types.ts b/packages/api-form-builder/src/types.ts index 2bb92ab8c95..339263d6f61 100644 --- a/packages/api-form-builder/src/types.ts +++ b/packages/api-form-builder/src/types.ts @@ -2,7 +2,11 @@ import { Plugin } from "@webiny/plugins/types"; import { TenancyContext } from "@webiny/api-tenancy/types"; import { SecurityPermission } from "@webiny/api-security/types"; import { FileManagerContext } from "@webiny/api-file-manager/types"; -import { CmsEntryListWhere } from "@webiny/api-headless-cms/types"; +import { + CmsEntryListWhere, + CmsEntryStatus as FormStatus, + CmsIdentity as FormIdentity +} from "@webiny/api-headless-cms/types"; import { I18NContext } from "@webiny/api-i18n/types"; import { Topic } from "@webiny/pubsub/types"; @@ -101,15 +105,14 @@ export interface FbForm { id: string; tenant: string; locale: string; - createdBy: CreatedBy; - ownedBy: OwnedBy; - savedOn: string; - createdOn: string; + createdBy: FormIdentity; + createdOn: Date | string; + savedOn: Date | string; + publishedOn?: Date | string; name: string; slug: string; version: number; - publishedOn?: string | Date; - status: string; + status: FormStatus; fields: FbFormField[]; steps: FbFormStep[]; stats: Omit<FbFormStats, "conversionRate">; @@ -119,14 +122,6 @@ export interface FbForm { webinyVersion: string; } -export interface CreatedBy { - id: string; - displayName: string | null; - type: string; -} - -export type OwnedBy = CreatedBy; - interface FormCreateInput { name: string; } @@ -344,7 +339,6 @@ export interface SystemCRUD { export interface FbSubmission { id: string; locale: string; - ownedBy: OwnedBy; data: Record<string, any>; meta: Record<string, any>; form: { @@ -357,6 +351,7 @@ export interface FbSubmission { }; logs: Record<string, any>[]; createdOn: string; + createdBy: FormIdentity; savedOn: string; webinyVersion: string; tenant: string; diff --git a/packages/api-headless-cms/src/graphqlFields/index.ts b/packages/api-headless-cms/src/graphqlFields/index.ts index a43c70a9a88..a558071477a 100644 --- a/packages/api-headless-cms/src/graphqlFields/index.ts +++ b/packages/api-headless-cms/src/graphqlFields/index.ts @@ -8,7 +8,6 @@ import { createRichTextField } from "./richText"; import { createFileField } from "./file"; import { createObjectField } from "./object"; import { createDynamicZoneField } from "~/graphqlFields/dynamicZone"; -import { createJSONField } from "./json"; import { CmsModelFieldToGraphQLPlugin } from "~/types"; import { createJsonField } from "~/graphqlFields/json"; @@ -23,6 +22,5 @@ export const createGraphQLFields = (): CmsModelFieldToGraphQLPlugin<any>[] => [ createJsonField(), createFileField(), createObjectField(), - createDynamicZoneField(), - createJSONField() + createDynamicZoneField() ]; From 5f4554406c1a8bd5daf99556355f25b80e14297e Mon Sep 17 00:00:00 2001 From: Leonardo Giacone <leonardo.giacone@gmail.com> Date: Wed, 24 Jan 2024 14:19:16 +0100 Subject: [PATCH 28/37] test: fix ownedBy --- .../__tests__/formSubmissionSecurity.test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/api-form-builder/__tests__/formSubmissionSecurity.test.ts b/packages/api-form-builder/__tests__/formSubmissionSecurity.test.ts index b4bf3812a30..8609a158334 100644 --- a/packages/api-form-builder/__tests__/formSubmissionSecurity.test.ts +++ b/packages/api-form-builder/__tests__/formSubmissionSecurity.test.ts @@ -121,11 +121,6 @@ describe("Forms Submission Security Test", () => { } } }, - ownedBy: { - id: identityA.id, - displayName: identityA.displayName, - type: identityA.type - }, createdBy: { id: identityA.id, displayName: identityA.displayName, From 32f071fc11143cbece5c8e18e85a7c25a6bfbf9c Mon Sep 17 00:00:00 2001 From: Leonardo Giacone <leonardo.giacone@gmail.com> Date: Mon, 5 Feb 2024 16:53:54 +0100 Subject: [PATCH 29/37] chore: update deps --- packages/api-form-builder/package.json | 2 -- yarn.lock | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/api-form-builder/package.json b/packages/api-form-builder/package.json index 43ec563151c..e1af3e901d0 100644 --- a/packages/api-form-builder/package.json +++ b/packages/api-form-builder/package.json @@ -18,7 +18,6 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@commodo/fields": "1.1.2-beta.20", "@webiny/api": "0.0.0", "@webiny/api-file-manager": "0.0.0", "@webiny/api-i18n": "0.0.0", @@ -34,7 +33,6 @@ "@webiny/pubsub": "0.0.0", "@webiny/utils": "0.0.0", "@webiny/validation": "0.0.0", - "commodo-fields-object": "^1.0.6", "date-fns": "^2.22.1", "got": "^9.6.0", "json2csv": "^4.5.2", diff --git a/yarn.lock b/yarn.lock index 59eafd4cc82..8065ff02dc5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15545,7 +15545,6 @@ __metadata: "@babel/preset-env": ^7.23.9 "@babel/preset-typescript": ^7.23.3 "@babel/runtime": ^7.23.9 - "@commodo/fields": 1.1.2-beta.20 "@types/got": ^9.6.12 "@types/json2csv": ^4.5.1 "@types/node-fetch": ^2.6.1 @@ -15568,7 +15567,6 @@ __metadata: "@webiny/pubsub": 0.0.0 "@webiny/utils": 0.0.0 "@webiny/validation": 0.0.0 - commodo-fields-object: ^1.0.6 csvtojson: ^2.0.10 date-fns: ^2.22.1 fs-extra: ^9.1.0 From 91e745c8a165fcc314bf7a950cedb25a6e9422d5 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone <leonardo.giacone@gmail.com> Date: Tue, 13 Feb 2024 08:54:56 +0100 Subject: [PATCH 30/37] fix: re-introduce getPublishedFormRevisionById crud operation --- .../src/plugins/crud/forms.crud.ts | 34 +++++++++++++++++++ .../plugins/graphql/createFormsTypeDefs.ts | 4 +-- .../src/plugins/graphql/formsSchema.ts | 18 ++++++++-- packages/api-form-builder/src/types.ts | 1 + 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/packages/api-form-builder/src/plugins/crud/forms.crud.ts b/packages/api-form-builder/src/plugins/crud/forms.crud.ts index 0c8ad1bfc90..f6c6da555b6 100644 --- a/packages/api-form-builder/src/plugins/crud/forms.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/forms.crud.ts @@ -226,6 +226,40 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { ); } }, + async getPublishedFormRevisionById(this: FormBuilder, revisionId) { + const { id: formId, version } = parseIdentifier(revisionId); + + if (!version) { + throw new WebinyError("There is no version in given ID value.", "VERSION_ERROR", { + revisionId + }); + } + + let form: FbForm | null = null; + try { + form = await this.storageOperations.forms.getForm({ + where: { + formId, + version: Number(version), + published: true, + tenant: getTenant().id, + locale: getLocale().code + } + }); + } catch (ex) { + throw new WebinyError( + ex.message || "Could not load published form revision by ID.", + ex.code || "GET_PUBLISHED_FORM_BY_ID_ERROR", + { + revisionId + } + ); + } + if (!form) { + throw new NotFoundError(`Form "${revisionId}" was not found!`); + } + return form; + }, async getLatestPublishedFormRevision(this: FormBuilder, formId) { let form: FbForm | null = null; try { diff --git a/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts b/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts index 1afe691166a..54622194085 100644 --- a/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts +++ b/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts @@ -146,8 +146,8 @@ export const createFormsTypeDefs = (params: CreateFormsTypeDefsParams): string = # Get form revisions getFormRevisions(id: ID!): FbFormRevisionsResponse - # Get published form form ID (public access) - getPublishedForm(formId: ID): FbFormResponse + # Get published form by revision ID, or parent form ID (public access) + getPublishedForm(revision: ID, parent: ID): FbFormResponse } extend type FbMutation { diff --git a/packages/api-form-builder/src/plugins/graphql/formsSchema.ts b/packages/api-form-builder/src/plugins/graphql/formsSchema.ts index 6b252ade26e..6409ac16d9d 100644 --- a/packages/api-form-builder/src/plugins/graphql/formsSchema.ts +++ b/packages/api-form-builder/src/plugins/graphql/formsSchema.ts @@ -71,11 +71,23 @@ export const createFormsSchema = (params: CreateFormsTypeDefsParams) => { } }, getPublishedForm: async (_, args: any, { formBuilder }) => { - if (!args.formId) { - return new NotFoundResponse("Form ID missing."); + if (!args.revision && !args.parent) { + return new NotFoundResponse("Revision ID or Form ID missing."); } - const form = await formBuilder.getLatestPublishedFormRevision(args.formId); + let form; + + if (args.revision) { + /** + * This fetches the latest published revision for given revision id + */ + form = await formBuilder.getPublishedFormRevisionById(args.revision); + } else if (args.parent) { + /** + * This fetches the latest published revision for given parent form + */ + form = await formBuilder.getLatestPublishedFormRevision(args.parent); + } if (!form) { return new NotFoundResponse("The requested form was not found."); diff --git a/packages/api-form-builder/src/types.ts b/packages/api-form-builder/src/types.ts index 339263d6f61..e56586a6038 100644 --- a/packages/api-form-builder/src/types.ts +++ b/packages/api-form-builder/src/types.ts @@ -225,6 +225,7 @@ export interface FormsCRUD { incrementFormViews(id: string): Promise<boolean>; incrementFormSubmissions(id: string): Promise<boolean>; getFormRevisions(id: string): Promise<FbForm[]>; + getPublishedFormRevisionById(revisionId: string): Promise<FbForm>; getLatestPublishedFormRevision(formId: string): Promise<FbForm>; deleteFormRevision(id: string): Promise<boolean>; /** From 1c2410205ae4e448539c8a1ffb336551b39e2d7a Mon Sep 17 00:00:00 2001 From: Leonardo Giacone <leonardo.giacone@gmail.com> Date: Tue, 13 Feb 2024 10:06:29 +0100 Subject: [PATCH 31/37] fix: re-introduce form parent and revision in form renderer --- .../FormElementAdvancedSettings.tsx | 12 +++++++--- .../form/dataLoaders/getFormDataLoader.ts | 3 ++- .../src/renderers/form/dataLoaders/graphql.ts | 4 ++-- .../src/renderers/form/index.tsx | 23 ++++++++++++++----- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/app-form-builder/src/page-builder/admin/plugins/components/FormElementAdvancedSettings.tsx b/packages/app-form-builder/src/page-builder/admin/plugins/components/FormElementAdvancedSettings.tsx index 4556a9a7362..ac32f5ba889 100644 --- a/packages/app-form-builder/src/page-builder/admin/plugins/components/FormElementAdvancedSettings.tsx +++ b/packages/app-form-builder/src/page-builder/admin/plugins/components/FormElementAdvancedSettings.tsx @@ -41,7 +41,7 @@ const FormElementAdvancedSettings = ({ Bind, submit, data }: FormElementAdvanced ); const selectedOption = useMemo(() => { - const formId = get(data, "settings.formId"); + const formId = get(data, "settings.form.parent"); return publishedFormsOptions.find(option => option.id === formId); }, [data, publishedFormsOptions]); @@ -54,13 +54,19 @@ const FormElementAdvancedSettings = ({ Bind, submit, data }: FormElementAdvanced <FormOptionsWrapper> <Grid className={classes.simpleGrid}> <Cell span={12}> - <Bind name={"settings.formId"} validators={validation.create("required")}> + <Bind name={"settings.form"} validators={validation.create("required")}> {({ onChange }) => ( <AutoComplete options={publishedFormsOptions} label={"Form"} value={selectedOption} - onChange={onChange} + onChange={value => { + // For backward compatibility we always set revision "latest" and parent the actual formId + onChange({ + parent: value, + revision: "latest" + }); + }} /> )} </Bind> diff --git a/packages/app-page-builder-elements/src/renderers/form/dataLoaders/getFormDataLoader.ts b/packages/app-page-builder-elements/src/renderers/form/dataLoaders/getFormDataLoader.ts index 3b1a9068842..5ff89ae74bd 100644 --- a/packages/app-page-builder-elements/src/renderers/form/dataLoaders/getFormDataLoader.ts +++ b/packages/app-page-builder-elements/src/renderers/form/dataLoaders/getFormDataLoader.ts @@ -9,7 +9,8 @@ export interface CreateGetFormDataLoaderParams { } export interface GetFormDataLoaderVariables { - formId?: string; + revision?: string; + parent?: string; } export type GetFormDataLoaderResult = FormData; diff --git a/packages/app-page-builder-elements/src/renderers/form/dataLoaders/graphql.ts b/packages/app-page-builder-elements/src/renderers/form/dataLoaders/graphql.ts index e20a45090ef..384e2580c9f 100644 --- a/packages/app-page-builder-elements/src/renderers/form/dataLoaders/graphql.ts +++ b/packages/app-page-builder-elements/src/renderers/form/dataLoaders/graphql.ts @@ -1,7 +1,7 @@ export const GET_PUBLISHED_FORM = /* GraphQL */ ` - query FbGetPublishedForm($formId: ID) @ps(cache: true) { + query FbGetPublishedForm($revision: ID, $parent: ID) @ps(cache: true) { formBuilder { - getPublishedForm(formId: $formId) { + getPublishedForm(revision: $revision, parent: $parent) { data { id formId diff --git a/packages/app-page-builder-elements/src/renderers/form/index.tsx b/packages/app-page-builder-elements/src/renderers/form/index.tsx index c3d308830bb..51075372697 100644 --- a/packages/app-page-builder-elements/src/renderers/form/index.tsx +++ b/packages/app-page-builder-elements/src/renderers/form/index.tsx @@ -9,7 +9,10 @@ export type FormRenderer = ReturnType<typeof createForm>; export interface FormElementData { settings: { - formId?: string; + form?: { + parent?: string; + revision?: string; + }; }; } @@ -21,8 +24,15 @@ export const createForm = (params: CreateFormParams) => { const element = getElement<FormElementData>(); - const formId = element.data.settings?.formId; - const variables: GetFormDataLoaderVariables = { formId }; + const form = element.data.settings?.form; + const variables: GetFormDataLoaderVariables = {}; + if (form) { + if (form.revision === "latest") { + variables.parent = form.parent; + } else { + variables.revision = form.revision; + } + } const variablesHash = JSON.stringify({ variables, headers }); @@ -46,10 +56,11 @@ export const createForm = (params: CreateFormParams) => { useEffect(() => { if (preloadedFormData) { - setFormData(preloadedFormData); + return; } - if (!variables.formId) { + const hasRequiredVariables = variables.parent || variables.revision; + if (!hasRequiredVariables) { return; } @@ -68,7 +79,7 @@ export const createForm = (params: CreateFormParams) => { } }, [variablesHash]); - if (!variables.formId) { + if (!(variables.parent || variables.revision)) { if (params.renderFormNotSelected) { return params.renderFormNotSelected({}); } From cd3557512eb31523514dff01fa7f253be72b597a Mon Sep 17 00:00:00 2001 From: Leonardo Giacone <leonardo.giacone@gmail.com> Date: Tue, 13 Feb 2024 12:49:07 +0100 Subject: [PATCH 32/37] fix: fetch id instead of formId --- .../admin/plugins/components/FormElementAdvancedSettings.tsx | 2 +- .../src/page-builder/admin/plugins/components/graphql.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/app-form-builder/src/page-builder/admin/plugins/components/FormElementAdvancedSettings.tsx b/packages/app-form-builder/src/page-builder/admin/plugins/components/FormElementAdvancedSettings.tsx index ac32f5ba889..638bb954b85 100644 --- a/packages/app-form-builder/src/page-builder/admin/plugins/components/FormElementAdvancedSettings.tsx +++ b/packages/app-form-builder/src/page-builder/admin/plugins/components/FormElementAdvancedSettings.tsx @@ -34,7 +34,7 @@ const FormElementAdvancedSettings = ({ Bind, submit, data }: FormElementAdvanced const publishedFormsOptions = useMemo( () => publishedForms.map(publishedForm => ({ - id: publishedForm.formId, + id: publishedForm.id, name: publishedForm.name })), [publishedForms] diff --git a/packages/app-form-builder/src/page-builder/admin/plugins/components/graphql.ts b/packages/app-form-builder/src/page-builder/admin/plugins/components/graphql.ts index 08b665bb30d..aa3eda68157 100644 --- a/packages/app-form-builder/src/page-builder/admin/plugins/components/graphql.ts +++ b/packages/app-form-builder/src/page-builder/admin/plugins/components/graphql.ts @@ -8,7 +8,7 @@ import { FbErrorResponse, FbFormModel, FbRevisionModel } from "~/types"; export interface ListFormsQueryResponse { formBuilder: { listForms: { - data: Pick<FbFormModel, "formId" | "name">[]; + data: Pick<FbFormModel, "id" | "name">[]; error: FbErrorResponse | null; }; }; @@ -18,7 +18,7 @@ export const LIST_FORMS = gql` formBuilder { listForms { data { - formId + id name } error { From 00645430d56269c8bdd762acbe844bdfe10b9652 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone <leonardo.giacone@gmail.com> Date: Tue, 13 Feb 2024 15:06:50 +0100 Subject: [PATCH 33/37] fix: remove version from getPublishedFormRevisionById --- packages/api-form-builder/src/plugins/crud/forms.crud.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/api-form-builder/src/plugins/crud/forms.crud.ts b/packages/api-form-builder/src/plugins/crud/forms.crud.ts index f6c6da555b6..dc10d417d3c 100644 --- a/packages/api-form-builder/src/plugins/crud/forms.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/forms.crud.ts @@ -240,7 +240,6 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { form = await this.storageOperations.forms.getForm({ where: { formId, - version: Number(version), published: true, tenant: getTenant().id, locale: getLocale().code From 929b9fecf79a94a31e514387d52eaa2e9d73b01b Mon Sep 17 00:00:00 2001 From: Vitalii Nobis <77202393+neatbyte-vnobis@users.noreply.github.com> Date: Wed, 14 Feb 2024 12:09:51 +0200 Subject: [PATCH 34/37] refactor(api-form-builder): form statistics (#3780) --- .../src/definitions/form.ts | 3 - .../api-form-builder-so-ddb-es/src/index.ts | 2 + .../src/operations/formStats/index.ts | 41 ++++ .../src/definitions/form.ts | 3 - packages/api-form-builder-so-ddb/src/index.ts | 2 + .../src/operations/formStats/index.ts | 41 ++++ .../__tests__/formSubmissionSecurity.test.ts | 9 - .../api-form-builder/__tests__/forms.test.ts | 60 ++--- .../__tests__/graphql/formStats.ts | 38 +++ .../__tests__/graphql/forms.ts | 9 - .../__tests__/useGqlHandler.ts | 8 + .../CmsFormStatsStorage.ts | 108 ++++++++ .../cmsFormBuilderStorage/CmsFormsStorage.ts | 11 +- .../CmsSubmissionsStorage.ts | 4 +- .../FormBuilderContextSetup.ts | 32 ++- .../createFormBuilderPlugins.ts | 2 + .../createGraphQLSchemaPlugin.ts | 9 + .../models/form.model.ts | 53 ---- .../models/formStat.model.ts | 59 +++++ .../src/plugins/crud/formStats.crud.ts | 230 ++++++++++++++++++ .../src/plugins/crud/forms.crud.ts | 124 ++++------ .../src/plugins/crud/index.ts | 5 + .../graphql/createFormStatsTypeDefs.ts | 59 +++++ .../plugins/graphql/createFormsTypeDefs.ts | 12 +- .../src/plugins/graphql/formStatsSchema.ts | 38 +++ .../src/plugins/graphql/formsSchema.ts | 13 - packages/api-form-builder/src/types.ts | 131 +++++++++- .../src/operations/entry/index.ts | 99 +++----- .../app-form-builder/src/admin/graphql.ts | 37 ++- .../FormSubmissionsOverview.tsx | 18 +- .../formDetails/formSubmissions/index.tsx | 10 +- .../src/admin/views/Forms/FormDetails.tsx | 28 ++- packages/app-form-builder/src/types.ts | 13 +- .../src/renderers/form/types.ts | 5 - 34 files changed, 999 insertions(+), 317 deletions(-) create mode 100644 packages/api-form-builder-so-ddb-es/src/operations/formStats/index.ts create mode 100644 packages/api-form-builder-so-ddb/src/operations/formStats/index.ts create mode 100644 packages/api-form-builder/__tests__/graphql/formStats.ts create mode 100644 packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormStatsStorage.ts create mode 100644 packages/api-form-builder/src/cmsFormBuilderStorage/models/formStat.model.ts create mode 100644 packages/api-form-builder/src/plugins/crud/formStats.crud.ts create mode 100644 packages/api-form-builder/src/plugins/graphql/createFormStatsTypeDefs.ts create mode 100644 packages/api-form-builder/src/plugins/graphql/formStatsSchema.ts diff --git a/packages/api-form-builder-so-ddb-es/src/definitions/form.ts b/packages/api-form-builder-so-ddb-es/src/definitions/form.ts index 194ef97fe25..ebf5ce3b39b 100644 --- a/packages/api-form-builder-so-ddb-es/src/definitions/form.ts +++ b/packages/api-form-builder-so-ddb-es/src/definitions/form.ts @@ -73,9 +73,6 @@ export const createFormEntity = (params: Params): Entity<any> => { steps: { type: "list" }, - stats: { - type: "map" - }, settings: { type: "map" }, diff --git a/packages/api-form-builder-so-ddb-es/src/index.ts b/packages/api-form-builder-so-ddb-es/src/index.ts index 7c69199b332..dc1182848da 100644 --- a/packages/api-form-builder-so-ddb-es/src/index.ts +++ b/packages/api-form-builder-so-ddb-es/src/index.ts @@ -7,6 +7,7 @@ import { createSystemStorageOperations } from "~/operations/system"; import { createSubmissionStorageOperations } from "~/operations/submission"; import { createSettingsStorageOperations } from "~/operations/settings"; import { createFormStorageOperations } from "~/operations/form"; +import { createFormStatsStorageOperations } from "~/operations/formStats"; import { createElasticsearchTable } from "~/definitions/tableElasticsearch"; const reservedFields = ["PK", "SK", "index", "data", "TYPE", "__type", "GSI1_PK", "GSI1_SK"]; @@ -70,6 +71,7 @@ export const createFormBuilderStorageOperations: FormBuilderStorageOperationsFac entity: entities.settings }), forms: createFormStorageOperations(), + formStats: createFormStatsStorageOperations(), submissions: createSubmissionStorageOperations() }; }; diff --git a/packages/api-form-builder-so-ddb-es/src/operations/formStats/index.ts b/packages/api-form-builder-so-ddb-es/src/operations/formStats/index.ts new file mode 100644 index 00000000000..86e543d5d18 --- /dev/null +++ b/packages/api-form-builder-so-ddb-es/src/operations/formStats/index.ts @@ -0,0 +1,41 @@ +import { FormBuilderFormStatsStorageOperations } from "@webiny/api-form-builder/types"; + +export const createFormStatsStorageOperations = (): FormBuilderFormStatsStorageOperations => { + const getFormStats = async () => { + throw new Error( + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." + ); + }; + + const listFormStats = async () => { + throw new Error( + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." + ); + }; + + const createFormStats = async () => { + throw new Error( + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." + ); + }; + + const updateFormStats = async () => { + throw new Error( + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." + ); + }; + + const deleteFormStats = async () => { + throw new Error( + "api-form-builder-so-ddb-es does not implement the Form Builder storage operations." + ); + }; + + return { + getFormStats, + listFormStats, + createFormStats, + updateFormStats, + deleteFormStats + }; +}; diff --git a/packages/api-form-builder-so-ddb/src/definitions/form.ts b/packages/api-form-builder-so-ddb/src/definitions/form.ts index 400e93a26b4..17d3b21f2b8 100644 --- a/packages/api-form-builder-so-ddb/src/definitions/form.ts +++ b/packages/api-form-builder-so-ddb/src/definitions/form.ts @@ -79,9 +79,6 @@ export const createFormEntity = (params: Params): Entity<any> => { steps: { type: "list" }, - stats: { - type: "map" - }, settings: { type: "map" }, diff --git a/packages/api-form-builder-so-ddb/src/index.ts b/packages/api-form-builder-so-ddb/src/index.ts index 220a0a0e59d..61fbf108ad3 100644 --- a/packages/api-form-builder-so-ddb/src/index.ts +++ b/packages/api-form-builder-so-ddb/src/index.ts @@ -7,6 +7,7 @@ import { createSystemStorageOperations } from "~/operations/system"; import { createSubmissionStorageOperations } from "~/operations/submission"; import { createSettingsStorageOperations } from "~/operations/settings"; import { createFormStorageOperations } from "~/operations/form"; +import { createFormStatsStorageOperations } from "~/operations/formStats"; const reservedFields = ["PK", "SK", "index", "data", "TYPE", "__type", "GSI1_PK", "GSI1_SK"]; @@ -63,6 +64,7 @@ export const createFormBuilderStorageOperations: FormBuilderStorageOperationsFac entity: entities.settings }), forms: createFormStorageOperations(), + formStats: createFormStatsStorageOperations(), submissions: createSubmissionStorageOperations() }; }; diff --git a/packages/api-form-builder-so-ddb/src/operations/formStats/index.ts b/packages/api-form-builder-so-ddb/src/operations/formStats/index.ts new file mode 100644 index 00000000000..d57b5383821 --- /dev/null +++ b/packages/api-form-builder-so-ddb/src/operations/formStats/index.ts @@ -0,0 +1,41 @@ +import { FormBuilderFormStatsStorageOperations } from "@webiny/api-form-builder/types"; + +export const createFormStatsStorageOperations = (): FormBuilderFormStatsStorageOperations => { + const getFormStats = async () => { + throw new Error( + "api-form-builder-so-ddb does not implement the Form Builder storage operations." + ); + }; + + const listFormStats = async () => { + throw new Error( + "api-form-builder-so-ddb does not implement the Form Builder storage operations." + ); + }; + + const createFormStats = async () => { + throw new Error( + "api-form-builder-so-ddb does not implement the Form Builder storage operations." + ); + }; + + const updateFormStats = async () => { + throw new Error( + "api-form-builder-so-ddb does not implement the Form Builder storage operations." + ); + }; + + const deleteFormStats = async () => { + throw new Error( + "api-form-builder-so-ddb does not implement the Form Builder storage operations." + ); + }; + + return { + getFormStats, + listFormStats, + createFormStats, + updateFormStats, + deleteFormStats + }; +}; diff --git a/packages/api-form-builder/__tests__/formSubmissionSecurity.test.ts b/packages/api-form-builder/__tests__/formSubmissionSecurity.test.ts index 8609a158334..dcb355f186a 100644 --- a/packages/api-form-builder/__tests__/formSubmissionSecurity.test.ts +++ b/packages/api-form-builder/__tests__/formSubmissionSecurity.test.ts @@ -95,15 +95,6 @@ describe("Forms Submission Security Test", () => { fields: [], publishedOn: null, name: "A1-name", - overallStats: { - conversionRate: 0, - submissions: 0, - views: 0 - }, - stats: { - submissions: 0, - views: 0 - }, status: "draft", steps: [ { diff --git a/packages/api-form-builder/__tests__/forms.test.ts b/packages/api-form-builder/__tests__/forms.test.ts index 8a0068bda6f..9f81ce40ecd 100644 --- a/packages/api-form-builder/__tests__/forms.test.ts +++ b/packages/api-form-builder/__tests__/forms.test.ts @@ -25,6 +25,8 @@ describe('Form Builder "Form" Test', () => { createFormSubmission, listFormSubmissions, exportFormSubmissions, + getFormStats, + getFormOverallStats, defaultIdentity } = useGqlHandler(); @@ -288,22 +290,22 @@ describe('Form Builder "Form" Test', () => { test("should publish, add views and unpublish", async () => { const [create] = await createForm({ data: { name: "contact-us" } }); - const { id } = create.data.formBuilder.createForm.data; + const { id, formId } = create.data.formBuilder.createForm.data; // Publish revision #1 await publishRevision({ revision: id }); // Get the published form - const [{ data: get }] = await getPublishedForm({ formId: id }); + const [{ data: get }] = await getPublishedForm({ formId }); expect(get.formBuilder.getPublishedForm.data.id).toEqual(id); // Create a new revision const [create2] = await createRevisionFrom({ revision: id }); const { id: id2 } = create2.data.formBuilder.createRevisionFrom.data; - // Latest published form should still be #1 - const [latestPublished] = await getPublishedForm({ formId: id.split("#")[0] }); - expect(latestPublished.data.formBuilder.getPublishedForm.data.id).toEqual(id); + // Published form should still be #1 + const [published] = await getPublishedForm({ formId }); + expect(published.data.formBuilder.getPublishedForm.data.id).toEqual(id); // Latest revision should be #2 const [list] = await listForms(); @@ -317,33 +319,47 @@ describe('Form Builder "Form" Test', () => { await saveFormView({ revision: id }); // Verify stats for #1 - const [{ data: get2 }] = await getForm({ revision: id }); - expect(get2.formBuilder.getForm.data.stats.views).toEqual(3); + const [{ data: get2 }] = await getFormStats({ id }); + expect(get2.formBuilder.getFormStats.data.views).toEqual(3); // Publish revision #2 await publishRevision({ revision: id2 }); - // Latest published form should now be #2 - const [latestPublished2] = await getPublishedForm({ formId: id.split("#")[0] }); - expect(latestPublished2.data.formBuilder.getPublishedForm.data.id).toEqual(id2); + // Published form should now be #2 + const [published2] = await getPublishedForm({ formId }); + expect(published2.data.formBuilder.getPublishedForm.data.id).toEqual(id2); // Increment views for #2 await saveFormView({ revision: id2 }); await saveFormView({ revision: id2 }); // Verify stats for #2 - const [{ data: get3 }] = await getForm({ revision: id2 }); - expect(get3.formBuilder.getForm.data.stats.views).toEqual(2); + const [{ data: get3 }] = await getFormStats({ id: id2 }); + expect(get3.formBuilder.getFormStats.data.views).toEqual(2); // Verify overall stats - expect(get3.formBuilder.getForm.data.overallStats.views).toEqual(5); + const [{ data: get3overall }] = await getFormOverallStats({ id: id2 }); + expect(get3overall.formBuilder.getFormOverallStats.data.views).toEqual(5); // Unpublish #2 await unpublishRevision({ revision: id2 }); - // Latest published form should now again be #1 - const [latestPublished3] = await getPublishedForm({ formId: id.split("#")[0] }); - expect(latestPublished3.data.formBuilder.getPublishedForm.data.id).toEqual(id); + // There should be no published forms + const [published3] = await getPublishedForm({ formId }); + expect(published3).toEqual({ + data: { + formBuilder: { + getPublishedForm: { + data: null, + error: { + message: `Form "${formId}" was not found!`, + code: "NOT_FOUND", + data: null + } + } + } + } + }); }); test("should create, list and export submissions to file", async () => { @@ -510,10 +526,6 @@ describe('Form Builder "Form" Test', () => { publishRevision: { data: { name: "form 2", - stats: { - submissions: 0, - views: 0 - }, status: "published", version: 1 }, @@ -568,10 +580,6 @@ describe('Form Builder "Form" Test', () => { publishRevision: { data: { name: "form 1", - stats: { - submissions: 0, - views: 0 - }, status: "published", version: 1 }, @@ -612,10 +620,6 @@ describe('Form Builder "Form" Test', () => { publishRevision: { data: { name: "form 1", - stats: { - submissions: 0, - views: 0 - }, status: "published", version: 2 }, diff --git a/packages/api-form-builder/__tests__/graphql/formStats.ts b/packages/api-form-builder/__tests__/graphql/formStats.ts new file mode 100644 index 00000000000..1ffcdef1aa8 --- /dev/null +++ b/packages/api-form-builder/__tests__/graphql/formStats.ts @@ -0,0 +1,38 @@ +export const ERROR_FIELD = /* GraphQL */ ` + { + code + data + message + } +`; + +export const GET_FORM_STATS = /* GraphQL */ ` + query FbGetFormStats($id: ID!) { + formBuilder { + getFormStats(formId: $id) { + data { + id + formId + formVersion + views + submissions + } + error ${ERROR_FIELD} + } + } + } +`; + +export const GET_FORM_OVERALL_STATS = /* GraphQL */ ` + query FbGetFormOverallStats($id: ID!) { + formBuilder { + getFormOverallStats(formId: $id) { + data { + views + submissions + } + error ${ERROR_FIELD} + } + } + } +`; diff --git a/packages/api-form-builder/__tests__/graphql/forms.ts b/packages/api-form-builder/__tests__/graphql/forms.ts index 227361f7980..7e4bf7402ad 100644 --- a/packages/api-form-builder/__tests__/graphql/forms.ts +++ b/packages/api-form-builder/__tests__/graphql/forms.ts @@ -27,15 +27,6 @@ export const FORM_DATA_FIELD = /* GraphQL */ ` } triggers status - stats { - views - submissions - } - overallStats { - views - submissions - conversionRate - } createdBy { id displayName diff --git a/packages/api-form-builder/__tests__/useGqlHandler.ts b/packages/api-form-builder/__tests__/useGqlHandler.ts index 3cf7e9aa79a..f6454dce3ce 100644 --- a/packages/api-form-builder/__tests__/useGqlHandler.ts +++ b/packages/api-form-builder/__tests__/useGqlHandler.ts @@ -30,6 +30,7 @@ import { UNPUBLISH_REVISION, UPDATE_REVISION } from "./graphql/forms"; +import { GET_FORM_STATS, GET_FORM_OVERALL_STATS } from "./graphql/formStats"; import { CREATE_FROM_SUBMISSION, EXPORT_FORM_SUBMISSIONS, @@ -214,6 +215,13 @@ export default (params: UseGqlHandlerParams = {}) => { async listForms(variables: Record<string, any> = {}) { return invoke({ body: { query: LIST_FORMS, variables } }); }, + // Form Stats + async getFormStats(variables: Record<string, any>) { + return invoke({ body: { query: GET_FORM_STATS, variables } }); + }, + async getFormOverallStats(variables: Record<string, any>) { + return invoke({ body: { query: GET_FORM_OVERALL_STATS, variables } }); + }, // Form Submission async createFormSubmission(variables: Record<string, any>) { return invoke({ body: { query: CREATE_FROM_SUBMISSION, variables } }); diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormStatsStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormStatsStorage.ts new file mode 100644 index 00000000000..c5527fc17f2 --- /dev/null +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormStatsStorage.ts @@ -0,0 +1,108 @@ +import { CmsEntry, CmsModel, HeadlessCms } from "@webiny/api-headless-cms/types"; +import { Security } from "@webiny/api-security/types"; +import { createIdentifier } from "@webiny/utils"; + +import { + FormBuilderStorageOperationsGetFormStatsParams, + FormBuilderStorageOperationsListFormStatsParams, + FormBuilderStorageOperationsCreateFormStatsParams, + FormBuilderStorageOperationsUpdateFormStatsParams, + FormBuilderStorageOperationsDeleteFormStatsParams, + FormBuilderFormStatsStorageOperations, + FbFormStats +} from "~/types"; + +interface ModelContext { + tenant: string; + locale: string; +} + +export class CmsFormStatsStorage implements FormBuilderFormStatsStorageOperations { + private readonly cms: HeadlessCms; + private readonly security: Security; + private readonly model: CmsModel; + + static async create(params: { model: CmsModel; cms: HeadlessCms; security: Security }) { + return new CmsFormStatsStorage(params.model, params.cms, params.security); + } + + private constructor(model: CmsModel, cms: HeadlessCms, security: Security) { + this.model = model; + this.cms = cms; + this.security = security; + } + + private modelWithContext({ tenant, locale }: ModelContext): CmsModel { + return { ...this.model, tenant, locale }; + } + + async getFormStats(params: FormBuilderStorageOperationsGetFormStatsParams) { + const { id, tenant, locale } = params.where; + const model = this.modelWithContext({ tenant, locale }); + + const entry = await this.security.withoutAuthorization(() => { + return this.cms.getEntry(model, { + where: { entryId: id, latest: true } + }); + }); + + return this.getFormStatsValues(entry); + } + + async listFormStats(params: FormBuilderStorageOperationsListFormStatsParams) { + const { tenant, locale, ...restWhere } = params.where; + const model = this.modelWithContext({ tenant, locale }); + + const [entries] = await this.security.withoutAuthorization(() => { + return this.cms.listEntries(model, { + where: { ...restWhere, latest: true } + }); + }); + + return entries.map(entry => this.getFormStatsValues(entry)); + } + + async createFormStats({ formStats }: FormBuilderStorageOperationsCreateFormStatsParams) { + const model = this.modelWithContext(formStats); + + const entry = await this.security.withoutAuthorization(() => { + return this.cms.createEntry(model, formStats); + }); + + return this.getFormStatsValues(entry); + } + + async updateFormStats({ formStats }: FormBuilderStorageOperationsUpdateFormStatsParams) { + const model = this.modelWithContext(formStats); + + // The version is set to 1, as `formStats` always has only one revision. + const formStatsRevisionId = createIdentifier({ + id: formStats.id, + version: 1 + }); + + const entry = await this.security.withoutAuthorization(() => { + return this.cms.updateEntry(model, formStatsRevisionId, formStats); + }); + + return this.getFormStatsValues(entry); + } + + async deleteFormStats(params: FormBuilderStorageOperationsDeleteFormStatsParams) { + const { ids, tenant, locale } = params; + const model = this.modelWithContext({ tenant, locale }); + + await this.security.withoutAuthorization(() => { + return this.cms.deleteMultipleEntries(model, { entries: ids }); + }); + } + + private getFormStatsValues(entry: CmsEntry) { + return { + id: entry.entryId, + locale: entry.locale, + tenant: entry.tenant, + ...entry.values + } as FbFormStats; + } +} diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts index b9b6c166f71..f18b505ff40 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsFormsStorage.ts @@ -104,12 +104,7 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { const model = this.modelWithContext(form); const entry = await this.security.withoutAuthorization(async () => { - return await this.cms.createEntryRevisionFrom(model, form.id, { - stats: { - submissions: 0, - views: 0 - } - }); + return await this.cms.createEntryRevisionFrom(model, form.id, {}); }); return this.getFormFieldValues(entry); @@ -131,9 +126,7 @@ export class CmsFormsStorage implements FormBuilderFormStorageOperations { const model = this.modelWithContext(form); await this.security.withoutAuthorization(async () => { - return await this.cms.deleteEntry(model, form.id, { - force: true - }); + return await this.cms.deleteEntry(model, form.id); }); } diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts index c0aa471a5d0..0655339f6a2 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/CmsSubmissionsStorage.ts @@ -93,9 +93,7 @@ export class CmsSubmissionsStorage implements FormBuilderSubmissionStorageOperat const model = this.modelWithContext(submission); return await this.security.withoutAuthorization(async () => { - return this.cms.deleteEntry(model, submission.id, { - force: true - }); + return this.cms.deleteEntry(model, submission.id); }); } diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts index 44c1b9ab611..b5ea53ceeff 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/FormBuilderContextSetup.ts @@ -5,6 +5,7 @@ import WebinyError from "@webiny/error"; import { createFormBuilderBasicContext } from "./createFormBuilderBasicContext"; import { createFormBuilderPlugins } from "./createFormBuilderPlugins"; import { CmsFormsStorage } from "./CmsFormsStorage"; +import { CmsFormStatsStorage } from "./CmsFormStatsStorage"; import { isInstallationPending } from "./isInstallationPending"; import { CmsSubmissionsStorage } from "./CmsSubmissionsStorage"; import { FormBuilderContext, FbFormPermission, FormBuilderStorageOperations } from "~/types"; @@ -20,13 +21,18 @@ export class FormBuilderContextSetup { async setupContext(storageOperations: FormBuilderStorageOperations) { // This registers code plugins (model group, models) - const { groupPlugin, formModelDefinition, submissionModelDefinition } = - createFormBuilderPlugins(); + const { + groupPlugin, + formModelDefinition, + formStatModelDefinition, + submissionModelDefinition + } = createFormBuilderPlugins(); // Finally, register all plugins this.context.plugins.register([ groupPlugin, new CmsModelPlugin(formModelDefinition), + new CmsModelPlugin(formStatModelDefinition), new CmsModelPlugin(submissionModelDefinition) ]); @@ -38,6 +44,14 @@ export class FormBuilderContextSetup { storageOperations.forms = formsStorageOps; } + const formsStatsStorageOps = await this.context.security.withoutAuthorization(() => { + return this.setupFormStatsCmsStorageOperations(); + }); + + if (formsStatsStorageOps) { + storageOperations.formStats = formsStatsStorageOps; + } + const submissionsStorageOps = await this.context.security.withoutAuthorization(() => { return this.setupSubmissionsCmsStorageOperations(); }); @@ -77,6 +91,20 @@ export class FormBuilderContextSetup { }); } + private async setupFormStatsCmsStorageOperations() { + if (isInstallationPending({ tenancy: this.context.tenancy, i18n: this.context.i18n })) { + return; + } + + const model = await this.getModel("fbFormStat"); + + return await CmsFormStatsStorage.create({ + model, + cms: this.context.cms, + security: this.context.security + }); + } + private async setupSubmissionsCmsStorageOperations() { if (isInstallationPending({ tenancy: this.context.tenancy, i18n: this.context.i18n })) { return; diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderPlugins.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderPlugins.ts index 0d72e0df969..995f30cbe8c 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderPlugins.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/createFormBuilderPlugins.ts @@ -1,5 +1,6 @@ import { CmsGroupPlugin } from "@webiny/api-headless-cms"; import { createFormDataModelDefinition } from "./models/form.model"; +import { createFormStatDataModelDefinition } from "./models/formStat.model"; import { createSubmissionDataModelDefinition } from "./models/submission.model"; export const createFormBuilderPlugins = () => { @@ -17,6 +18,7 @@ export const createFormBuilderPlugins = () => { return { groupPlugin, formModelDefinition: createFormDataModelDefinition(groupPlugin.contentModelGroup), + formStatModelDefinition: createFormStatDataModelDefinition(groupPlugin.contentModelGroup), submissionModelDefinition: createSubmissionDataModelDefinition( groupPlugin.contentModelGroup ) diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/createGraphQLSchemaPlugin.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/createGraphQLSchemaPlugin.ts index 90b8684fa17..08d1fca0b02 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/createGraphQLSchemaPlugin.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/createGraphQLSchemaPlugin.ts @@ -9,6 +9,7 @@ import { createFormBuilderSettingsSchema } from "~/plugins/graphql/formSettings" import { createBaseSchema } from "~/plugins/graphql"; import { createSubmissionsSchema } from "~/plugins/graphql/submissionsSchema"; import { createFormsSchema } from "~/plugins/graphql/formsSchema"; +import { createFormStatsSchema } from "~/plugins/graphql/formStatsSchema"; import { FormBuilderContext } from "~/types"; export const createGraphQLSchemaPlugin = () => { @@ -25,6 +26,7 @@ export const createGraphQLSchemaPlugin = () => { await context.security.withoutAuthorization(async () => { const submissionModel = (await context.cms.getModel("fbSubmission")) as CmsModel; const formsModel = (await context.cms.getModel("fbForm")) as CmsModel; + const formStatsModel = (await context.cms.getModel("fbFormStat")) as CmsModel; const models = await context.cms.listModels(); const fieldPlugins = createFieldTypePluginRecords(context.plugins); /** @@ -47,6 +49,12 @@ export const createGraphQLSchemaPlugin = () => { plugins: fieldPlugins }); + const formStatsGraphQlPlugin = createFormStatsSchema({ + model: formStatsModel, + models, + plugins: fieldPlugins + }); + const submissionsGraphQlPlugin = createSubmissionsSchema({ model: submissionModel, models, @@ -56,6 +64,7 @@ export const createGraphQLSchemaPlugin = () => { context.plugins.register([ ...plugins, formsGraphQlPlugin, + formStatsGraphQlPlugin, submissionsGraphQlPlugin ]); }); diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts index a3aac7f7e94..fd148efd52d 100644 --- a/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/form.model.ts @@ -27,49 +27,6 @@ const nameField = () => { }); }; -const statsViewsField = () => { - return createModelField({ - label: "Views", - type: "number" - }); -}; - -const statsSubmissionsField = () => { - return createModelField({ - label: "Submissions", - type: "number" - }); -}; - -const conversionRateStatsSubmissionsField = () => { - return createModelField({ - label: "Conversion Rate", - fieldId: "conversionRate", - type: "number" - }); -}; - -const statsField = (fields: CmsModelField[]) => { - return createModelField({ - label: "Stats", - type: "object", - settings: { - fields - } - }); -}; - -const overallStatsField = (fields: CmsModelField[]) => { - return createModelField({ - label: "Overall Stats", - fieldId: "overallStats", - type: "object", - settings: { - fields - } - }); -}; - const field_IdField = () => { return createModelField({ label: "ID", @@ -392,16 +349,6 @@ export const createFormDataModelDefinition = (group: CmsModelGroup): CmsPrivateM fields: [ formIdField(), nameField(), - statsField([ - statsViewsField(), - statsSubmissionsField(), - conversionRateStatsSubmissionsField() - ]), - overallStatsField([ - statsViewsField(), - statsSubmissionsField(), - conversionRateStatsSubmissionsField() - ]), fieldsField(FIELD_FIELDS), stepsField(STEP_FIELDS), settingsField(SETTINGS_FIELDS), diff --git a/packages/api-form-builder/src/cmsFormBuilderStorage/models/formStat.model.ts b/packages/api-form-builder/src/cmsFormBuilderStorage/models/formStat.model.ts new file mode 100644 index 00000000000..73790ef7086 --- /dev/null +++ b/packages/api-form-builder/src/cmsFormBuilderStorage/models/formStat.model.ts @@ -0,0 +1,59 @@ +import { CmsModelGroup } from "@webiny/api-headless-cms/types"; +import { CmsPrivateModelFull } from "@webiny/api-headless-cms"; + +import { createModelField } from "../createModelField"; + +const required = () => { + return { + name: "required", + message: "Value is required." + }; +}; + +const formIdField = () => { + return createModelField({ + label: "Form ID", + fieldId: "formId", + type: "text", + validation: [required()] + }); +}; + +const formVersionField = () => { + return createModelField({ + label: "Form Version", + fieldId: "formVersion", + type: "number", + validation: [required()] + }); +}; + +const viewsField = () => { + return createModelField({ + label: "Views", + fieldId: "views", + type: "number", + validation: [required()] + }); +}; + +const submissionsField = () => { + return createModelField({ + label: "Submissions", + fieldId: "submissions", + type: "number", + validation: [required()] + }); +}; + +export const createFormStatDataModelDefinition = (group: CmsModelGroup): CmsPrivateModelFull => { + return { + name: "FbFormStat", + modelId: "fbFormStat", + titleFieldId: "", + group, + fields: [formIdField(), formVersionField(), viewsField(), submissionsField()], + isPrivate: true, + noValidate: true + }; +}; diff --git a/packages/api-form-builder/src/plugins/crud/formStats.crud.ts b/packages/api-form-builder/src/plugins/crud/formStats.crud.ts new file mode 100644 index 00000000000..194ae0e4fc8 --- /dev/null +++ b/packages/api-form-builder/src/plugins/crud/formStats.crud.ts @@ -0,0 +1,230 @@ +import { NotFoundError } from "@webiny/handler-graphql"; +import { createTopic } from "@webiny/pubsub"; +import WebinyError from "@webiny/error"; +import { Tenant } from "@webiny/api-tenancy/types"; +import { I18NLocale } from "@webiny/api-i18n/types"; +import { parseIdentifier, zeroPad } from "@webiny/utils"; + +import { + FbFormStats, + FormBuilder, + FormStatsCRUD, + OnFormStatsBeforeCreate, + OnFormStatsAfterCreate, + OnFormStatsBeforeUpdate, + OnFormStatsAfterUpdate, + OnFormStatsBeforeDelete, + OnFormStatsAfterDelete +} from "~/types"; + +const getFormStatsId = (formRevisionId: string) => { + const { id: formId, version } = parseIdentifier(formRevisionId); + + if (version === null) { + throw new WebinyError("Wrong form revision id value", "GET_FORM_STATS_ID_ERROR", { + id: formRevisionId + }); + } + + return `${formId}-${zeroPad(version)}-stats`; +}; + +interface CreateFormStatsCrudParams { + getTenant: () => Tenant; + getLocale: () => I18NLocale; +} + +export const createFormStatsCrud = (params: CreateFormStatsCrudParams): FormStatsCRUD => { + const { getTenant, getLocale } = params; + + // create + const onFormStatsBeforeCreate = createTopic<OnFormStatsBeforeCreate>( + "formBuilder.onFormStatsBeforeCreate" + ); + const onFormStatsAfterCreate = createTopic<OnFormStatsAfterCreate>( + "formBuilder.onFormStatsAfterCreate" + ); + // update + const onFormStatsBeforeUpdate = createTopic<OnFormStatsBeforeUpdate>( + "formBuilder.onFormStatsBeforeUpdate" + ); + const onFormStatsAfterUpdate = createTopic<OnFormStatsAfterUpdate>( + "formBuilder.onFormStatsAfterUpdate" + ); + // delete + const onFormStatsBeforeDelete = createTopic<OnFormStatsBeforeDelete>( + "formBuilder.onFormStatsBeforeDelete" + ); + const onFormStatsAfterDelete = createTopic<OnFormStatsAfterDelete>( + "formBuilder.onFormStatsAfterDelete" + ); + + return { + onFormStatsBeforeCreate, + onFormStatsAfterCreate, + onFormStatsBeforeUpdate, + onFormStatsAfterUpdate, + onFormStatsBeforeDelete, + onFormStatsAfterDelete, + async getFormStats(this: FormBuilder, formRevisionId) { + const id = getFormStatsId(formRevisionId); + + try { + const formStats = await this.storageOperations.formStats.getFormStats({ + where: { id, tenant: getTenant().id, locale: getLocale().code } + }); + + if (!formStats) { + throw new NotFoundError("Form stats not found."); + } + + return formStats; + } catch (ex) { + throw new WebinyError( + ex.message || "Could not load form stats.", + ex.code || "GET_FORM_STATS_ERROR", + { + id + } + ); + } + }, + async getFormOverallStats(this: FormBuilder, id) { + const { id: formId } = parseIdentifier(id); + + try { + const formStats = await this.storageOperations.formStats.listFormStats({ + where: { formId, tenant: getTenant().id, locale: getLocale().code } + }); + + if (!formStats?.length) { + throw new NotFoundError("Form overall stats not found."); + } + + const overallFormStats = { + formId, + views: 0, + submissions: 0, + tenant: getTenant().id, + locale: getLocale().code + }; + + formStats.forEach(stat => { + overallFormStats.views += stat.views; + overallFormStats.submissions += stat.submissions; + }); + + return overallFormStats; + } catch (ex) { + throw new WebinyError( + ex.message || "Could not load form overall stats.", + ex.code || "GET_FORM_OVERALL_STATS_ERROR", + { + id + } + ); + } + }, + async createFormStats(this: FormBuilder, form) { + const id = getFormStatsId(form.id); + + const formStats: FbFormStats = { + id, + formId: form.formId, + formVersion: form.version, + views: 0, + submissions: 0, + tenant: getTenant().id, + locale: getLocale().code + }; + + try { + await onFormStatsBeforeCreate.publish({ + formStats + }); + const result = await this.storageOperations.formStats.createFormStats({ + formStats + }); + await onFormStatsAfterCreate.publish({ + formStats: result + }); + + return result; + } catch (ex) { + throw new WebinyError( + ex.message || "Could not create form stats.", + ex.code || "CREATE_FORM_STATS_ERROR", + { + ...(ex.data || {}), + input: form, + formStats + } + ); + } + }, + async updateFormStats(this: FormBuilder, formRevisionId, input) { + const original = await this.getFormStats(formRevisionId); + + if (!original) { + throw new NotFoundError("Form stats not found."); + } + + const formStats = { ...original, ...input }; + + try { + await onFormStatsBeforeUpdate.publish({ + original, + formStats + }); + const result = await this.storageOperations.formStats.updateFormStats({ + formStats + }); + await onFormStatsAfterUpdate.publish({ + original, + formStats: result + }); + return result; + } catch (ex) { + throw new WebinyError( + ex.message || "Could not update form stats.", + ex.code || "UPDATE_FORM_STATS_ERROR", + { + input, + original, + formStats + } + ); + } + }, + async deleteFormStats(this: FormBuilder, formId) { + const formStats = + (await this.storageOperations.formStats.listFormStats({ + where: { formId, tenant: getTenant().id, locale: getLocale().code } + })) || []; + + const formStatsIds = formStats.map(formStat => formStat.id); + + try { + await onFormStatsBeforeDelete.publish({ + ids: formStatsIds + }); + await this.storageOperations.formStats.deleteFormStats({ + ids: formStatsIds, + tenant: getTenant().id, + locale: getLocale().code + }); + await onFormStatsAfterDelete.publish({ + ids: formStatsIds + }); + } catch (ex) { + throw new WebinyError( + ex.message || "Could not delete form stats.", + ex.code || "DELETE_FORM_STATS_ERROR", + { + ids: formStatsIds + } + ); + } + } + }; +}; diff --git a/packages/api-form-builder/src/plugins/crud/forms.crud.ts b/packages/api-form-builder/src/plugins/crud/forms.crud.ts index dc10d417d3c..e8e2d55e0eb 100644 --- a/packages/api-form-builder/src/plugins/crud/forms.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/forms.crud.ts @@ -2,7 +2,6 @@ import slugify from "slugify"; import { NotFoundError } from "@webiny/handler-graphql"; import { FbForm, - FbFormStats, FormBuilder, FormBuilderContext, FormBuilderStorageOperationsListFormsParams, @@ -141,38 +140,6 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { return form; }, - async getFormStats(this: FormBuilder, id) { - /** - * We don't need to check permissions here, as this method is only called - * as a resolver to an `FbForm` GraphQL type, and we already check permissions - * and ownership when resolving the form in `getForm`. - */ - const revisions = await this.getFormRevisions(id); - - /** - * Then calculate the stats - */ - const stats: FbFormStats = { - submissions: 0, - views: 0, - conversionRate: 0 - }; - - for (const form of revisions) { - stats.views += form.stats.views; - stats.submissions += form.stats.submissions; - } - - let conversionRate = 0; - if (stats.views > 0) { - conversionRate = parseFloat(((stats.submissions / stats.views) * 100).toFixed(2)); - } - - return { - ...stats, - conversionRate - }; - }, async listForms(this: FormBuilder) { await formsPermissions.ensure({ rwd: "r" }); @@ -312,10 +279,6 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { slug, version, status: FORM_STATUS.DRAFT, - stats: { - views: 0, - submissions: 0 - }, /** * Will be added via a "update" */ @@ -333,15 +296,16 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { webinyVersion: context.WEBINY_VERSION }; + let result: FbForm; + try { await onFormBeforeCreate.publish({ form }); - const result = await this.storageOperations.forms.createForm({ form }); + result = await this.storageOperations.forms.createForm({ form }); await onFormAfterCreate.publish({ form: result }); - return result; } catch (ex) { throw new WebinyError( ex.message || "Could not create form.", @@ -351,6 +315,18 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { } ); } + + try { + await this.createFormStats(result); + } catch (ex) { + // If `formStats` creation fails, delete the form and rethrow the error. + // TODO: Consider adding a unit test to cover this scenario. + await this.deleteForm(result.id); + + throw ex; + } + + return result; }, async updateForm(this: FormBuilder, id, input) { await formsPermissions.ensure({ rwd: "w" }); @@ -430,7 +406,6 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { await onFormAfterDelete.publish({ form }); - return true; } catch (ex) { throw new WebinyError( ex.message || "Could not delete form.", @@ -440,6 +415,10 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { } ); } + + await this.deleteFormStats(form.formId); + + return true; }, async deleteFormRevision(this: FormBuilder, id) { await formsPermissions.ensure({ rwd: "d" }); @@ -571,17 +550,18 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { auth: false }); + let result: FbForm; + try { await onFormRevisionBeforeCreate.publish({ form }); - const result = await this.storageOperations.forms.createFormFrom({ + result = await this.storageOperations.forms.createFormFrom({ form }); await onFormRevisionAfterCreate.publish({ form: result }); - return result; } catch (ex) { throw new WebinyError( ex.message || "Could not create form from given one.", @@ -592,25 +572,31 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { } ); } + + try { + await this.createFormStats(result); + } catch (ex) { + // If `formStats` creation fails, delete the form revision and rethrow the error. + // TODO: Consider adding a unit test to cover this scenario. + await this.deleteFormRevision(result.id); + + throw ex; + } + + return result; }, async incrementFormViews(this: FormBuilder, id) { - const original = await this.getForm(id, { - auth: false - }); + const original = await this.getFormStats(id); - const form: FbForm = { - ...original, - stats: { - ...original.stats, - views: original.stats.views + 1 - }, - tenant: getTenant().id, - webinyVersion: context.WEBINY_VERSION - }; + if (!original) { + throw new NotFoundError(`Form stats for form "${id}" were not found!`); + } + + const views = original.views + 1; try { - await this.storageOperations.forms.updateForm({ - form + await this.updateFormStats(id, { + views }); } catch (ex) { throw new WebinyError( @@ -618,7 +604,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { ex.code || "UPDATE_FORM_STATS_VIEWS_ERROR", { original, - form + views } ); } @@ -626,31 +612,23 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { return true; }, async incrementFormSubmissions(this: FormBuilder, id) { - const original = await this.getForm(id, { - auth: false - }); + const original = await this.getFormStats(id); - const form: FbForm = { - ...original, - stats: { - ...original.stats, - submissions: original.stats.submissions + 1 - }, - tenant: getTenant().id, - webinyVersion: context.WEBINY_VERSION - }; + if (!original) { + throw new NotFoundError(`Form stats for form "${id}" were not found!`); + } + + const submissions = original.submissions + 1; try { - await this.storageOperations.forms.updateForm({ - form - }); + await this.updateFormStats(id, { submissions }); } catch (ex) { throw new WebinyError( ex.message || "Could not update form stats submissions stats.", ex.code || "UPDATE_FORM_STATS_SUBMISSIONS_ERROR", { original, - form + submissions } ); } diff --git a/packages/api-form-builder/src/plugins/crud/index.ts b/packages/api-form-builder/src/plugins/crud/index.ts index 9ccbc7ab57c..0b5980a7fbe 100644 --- a/packages/api-form-builder/src/plugins/crud/index.ts +++ b/packages/api-form-builder/src/plugins/crud/index.ts @@ -2,6 +2,7 @@ import { FormBuilderStorageOperations, FormBuilderContext } from "~/types"; import { createSystemCrud } from "~/plugins/crud/system.crud"; import { createSettingsCrud } from "~/plugins/crud/settings.crud"; import { createFormsCrud } from "~/plugins/crud/forms.crud"; +import { createFormStatsCrud } from "~/plugins/crud/formStats.crud"; import { createSubmissionsCrud } from "~/plugins/crud/submissions.crud"; import WebinyError from "@webiny/error"; import { FormsPermissions } from "./permissions/FormsPermissions"; @@ -69,6 +70,10 @@ export const setupFormBuilderContext = async (params: CreateFormBuilderCrudParam formsPermissions, context }), + ...createFormStatsCrud({ + getTenant, + getLocale + }), ...createSubmissionsCrud({ context, formsPermissions diff --git a/packages/api-form-builder/src/plugins/graphql/createFormStatsTypeDefs.ts b/packages/api-form-builder/src/plugins/graphql/createFormStatsTypeDefs.ts new file mode 100644 index 00000000000..ceb80e33a59 --- /dev/null +++ b/packages/api-form-builder/src/plugins/graphql/createFormStatsTypeDefs.ts @@ -0,0 +1,59 @@ +import { CmsFieldTypePlugins, CmsModel } from "@webiny/api-headless-cms/types"; +import { renderFields } from "@webiny/api-headless-cms/utils/renderFields"; + +export interface CreateFormStatsTypeDefsParams { + model: CmsModel; + models: CmsModel[]; + plugins: CmsFieldTypePlugins; +} + +export const createFormStatsTypeDefs = (params: CreateFormStatsTypeDefsParams): string => { + const { model, models, plugins: fieldTypePlugins } = params; + const { fields } = model; + + const fieldTypes = renderFields({ + models, + model, + fields, + type: "manage", + fieldTypePlugins + }); + + const excludeFormStatsFields = ["id", "formVersion"]; + + const formOverallStatsFieldTypes = renderFields({ + models, + model, + fields: model.fields.filter(field => !excludeFormStatsFields.includes(field.fieldId)), + type: "manage", + fieldTypePlugins + }); + + return /* GraphQL */ ` + ${fieldTypes.map(f => f.typeDefs).join("\n")} + + type FbFormStats { + id: ID! + ${fieldTypes.map(f => f.fields).join("\n")} + } + + type FbFormOverallStats { + ${formOverallStatsFieldTypes.map(f => f.fields).join("\n")} + } + + type FbFormStatsResponse { + data: FbFormStats + error: FbError + } + + type FbFormOverallStatsResponse { + data: FbFormOverallStats + error: FbError + } + + extend type FbQuery { + getFormStats(formId: ID!): FbFormStatsResponse + getFormOverallStats(formId: ID!): FbFormOverallStatsResponse + } + `; +}; diff --git a/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts b/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts index 54622194085..617a2efd376 100644 --- a/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts +++ b/packages/api-form-builder/src/plugins/graphql/createFormsTypeDefs.ts @@ -44,15 +44,7 @@ export const createFormsTypeDefs = (params: CreateFormsTypeDefsParams): string = fieldTypePlugins }); - const excludeFormFields = [ - "formId", - "stats", - "overallStats", - "fields", - "steps", - "settings", - "triggers" - ]; + const excludeFormFields = ["formId", "fields", "steps", "settings", "triggers"]; const inputCreateFields = renderInputFields({ models, @@ -61,7 +53,7 @@ export const createFormsTypeDefs = (params: CreateFormsTypeDefsParams): string = fieldTypePlugins }); - const excludeUpdateFormFields = ["formId", "stats", "overallStats"]; + const excludeUpdateFormFields = ["formId"]; const inputUpdateFields = renderInputFields({ models, diff --git a/packages/api-form-builder/src/plugins/graphql/formStatsSchema.ts b/packages/api-form-builder/src/plugins/graphql/formStatsSchema.ts new file mode 100644 index 00000000000..751c9ebe8d2 --- /dev/null +++ b/packages/api-form-builder/src/plugins/graphql/formStatsSchema.ts @@ -0,0 +1,38 @@ +import { ErrorResponse, GraphQLSchemaPlugin, Response } from "@webiny/handler-graphql"; + +import { + createFormStatsTypeDefs, + CreateFormStatsTypeDefsParams +} from "~/plugins/graphql/createFormStatsTypeDefs"; +import { FormBuilderContext } from "~/types"; + +export const createFormStatsSchema = (params: CreateFormStatsTypeDefsParams) => { + const formStatsGraphQL = new GraphQLSchemaPlugin<FormBuilderContext>({ + typeDefs: createFormStatsTypeDefs(params), + resolvers: { + FbQuery: { + getFormStats: async (_, args: any, { formBuilder }) => { + try { + const formStats = await formBuilder.getFormStats(args.formId); + + return new Response(formStats); + } catch (e) { + return new ErrorResponse(e); + } + }, + getFormOverallStats: async (_, args: any, { formBuilder }) => { + try { + const formStats = await formBuilder.getFormOverallStats(args.formId); + + return new Response(formStats); + } catch (e) { + return new ErrorResponse(e); + } + } + } + } + }); + formStatsGraphQL.name = "fb.graphql.formStats"; + + return formStatsGraphQL; +}; diff --git a/packages/api-form-builder/src/plugins/graphql/formsSchema.ts b/packages/api-form-builder/src/plugins/graphql/formsSchema.ts index 6409ac16d9d..81aa8b1c359 100644 --- a/packages/api-form-builder/src/plugins/graphql/formsSchema.ts +++ b/packages/api-form-builder/src/plugins/graphql/formsSchema.ts @@ -17,19 +17,6 @@ export const createFormsSchema = (params: CreateFormsTypeDefsParams) => { typeDefs: createFormsTypeDefs(params), resolvers: { FbForm: { - overallStats: async (form: FbForm, _, { formBuilder }) => { - try { - return await formBuilder.getFormStats(form.id); - } catch (ex) { - console.log(`Could not fetch form "${form.id}" stats.`); - console.log(ex.message); - } - return { - views: 0, - submissions: 0, - conversionRate: 0 - }; - }, settings: async (form: FbForm, _, { formBuilder }) => { const settings = await formBuilder.getSettings({ auth: false }); diff --git a/packages/api-form-builder/src/types.ts b/packages/api-form-builder/src/types.ts index e56586a6038..24b7df9a135 100644 --- a/packages/api-form-builder/src/types.ts +++ b/packages/api-form-builder/src/types.ts @@ -115,7 +115,6 @@ export interface FbForm { status: FormStatus; fields: FbFormField[]; steps: FbFormStep[]; - stats: Omit<FbFormStats, "conversionRate">; settings: Record<string, any>; triggers: Record<string, any> | null; formId: string; @@ -134,12 +133,6 @@ interface FormUpdateInput { triggers: Record<string, any> | null; } -export interface FbFormStats { - submissions: number; - views: number; - conversionRate: number; -} - interface FbListSubmissionsOptions { limit?: number; after?: string; @@ -214,7 +207,6 @@ export interface OnFormAfterUnpublishTopicParams { export interface FormsCRUD { getForm(id: string, options?: FormBuilderGetFormOptions): Promise<FbForm>; - getFormStats(id: string): Promise<FbFormStats>; listForms(): Promise<FormBuilderStorageOperationsListFormsResponse>; createForm(data: FormCreateInput): Promise<FbForm>; updateForm(id: string, data: Partial<FormUpdateInput>): Promise<FbForm>; @@ -433,7 +425,12 @@ export interface FbFormSettingsPermission extends SecurityPermission { /** * The object representing form builder internals. */ -export interface FormBuilder extends SystemCRUD, SettingsCRUD, FormsCRUD, SubmissionsCRUD { +export interface FormBuilder + extends SystemCRUD, + SettingsCRUD, + FormsCRUD, + SubmissionsCRUD, + FormStatsCRUD { storageOperations: FormBuilderStorageOperations; } @@ -770,5 +767,121 @@ export interface FormBuilderStorageOperations extends FormBuilderSystemStorageOperations, FormBuilderSettingsStorageOperations { forms: FormBuilderFormStorageOperations; + formStats: FormBuilderFormStatsStorageOperations; submissions: FormBuilderSubmissionStorageOperations; } + +export interface FbFormStats { + id: string; + formId: string; + formVersion: number; + views: number; + submissions: number; + tenant: string; + locale: string; +} + +/** + * FormStats CRUD Lifecycle Events + */ +export interface OnFormStatsBeforeCreate { + formStats: FbFormStats; +} +export interface OnFormStatsAfterCreate { + formStats: FbFormStats; +} +export interface OnFormStatsBeforeUpdate { + original: FbFormStats; + formStats: FbFormStats; +} +export interface OnFormStatsAfterUpdate { + original: FbFormStats; + formStats: FbFormStats; +} +export interface OnFormStatsBeforeDelete { + ids: string[]; +} +export interface OnFormStatsAfterDelete { + ids: string[]; +} + +export interface FormStatsCRUD { + getFormStats(formRevisionId: string): Promise<FbFormStats | null>; + getFormOverallStats(id: string): Promise<Omit<FbFormStats, "id" | "formVersion"> | null>; + createFormStats(form: FbForm): Promise<FbFormStats>; + updateFormStats( + formRevisionId: string, + input: { views?: number; submissions?: number } + ): Promise<FbFormStats>; + deleteFormStats(formId: string): Promise<void>; + /** + * Lifecycle events + */ + onFormStatsBeforeCreate: Topic<OnFormStatsBeforeCreate>; + onFormStatsAfterCreate: Topic<OnFormStatsAfterCreate>; + onFormStatsBeforeUpdate: Topic<OnFormStatsBeforeUpdate>; + onFormStatsAfterUpdate: Topic<OnFormStatsAfterUpdate>; + onFormStatsBeforeDelete: Topic<OnFormStatsBeforeDelete>; + onFormStatsAfterDelete: Topic<OnFormStatsAfterDelete>; +} + +/** + * @category StorageOperations + * @category StorageOperationsParams + */ +export interface FormBuilderStorageOperationsGetFormStatsParams { + where: { id: string; tenant: string; locale: string }; +} + +/** + * @category StorageOperations + * @category StorageOperationsParams + */ +export interface FormBuilderStorageOperationsListFormStatsParams { + where: { formId: string; tenant: string; locale: string }; +} + +/** + * @category StorageOperations + * @category StorageOperationsParams + */ +export interface FormBuilderStorageOperationsCreateFormStatsParams { + formStats: FbFormStats; +} + +/** + * @category StorageOperations + * @category StorageOperationsParams + */ +export interface FormBuilderStorageOperationsUpdateFormStatsParams { + formStats: FbFormStats; +} + +/** + * @category StorageOperations + * @category StorageOperationsParams + */ +export interface FormBuilderStorageOperationsDeleteFormStatsParams { + ids: string[]; + tenant: string; + locale: string; +} + +/** + * @category StorageOperations + */ +export interface FormBuilderFormStatsStorageOperations { + getFormStats( + params: FormBuilderStorageOperationsGetFormStatsParams + ): Promise<FbFormStats | null>; + listFormStats( + params: FormBuilderStorageOperationsListFormStatsParams + ): Promise<FbFormStats[] | null>; + createFormStats( + params: FormBuilderStorageOperationsCreateFormStatsParams + ): Promise<FbFormStats>; + updateFormStats( + params: FormBuilderStorageOperationsUpdateFormStatsParams + ): Promise<FbFormStats>; + deleteFormStats(params: FormBuilderStorageOperationsDeleteFormStatsParams): Promise<void>; +} diff --git a/packages/api-headless-cms-ddb-es/src/operations/entry/index.ts b/packages/api-headless-cms-ddb-es/src/operations/entry/index.ts index f36806427c6..6289f1b795e 100644 --- a/packages/api-headless-cms-ddb-es/src/operations/entry/index.ts +++ b/packages/api-headless-cms-ddb-es/src/operations/entry/index.ts @@ -927,79 +927,48 @@ export const createEntriesStorageOperations = ( const { entries } = params; const model = getStorageOperationsModel(initialModel); /** - * First we need all the revisions of the entries we want to delete. - */ - const revisions = await dataLoaders.getAllEntryRevisions({ - model, - ids: entries - }); - /** - * Then we need to construct the queries for all the revisions and entries. + * We need to construct the queries for all the revisions and entries. */ const items: Record<string, WriteRequest>[] = []; const esItems: Record<string, WriteRequest>[] = []; for (const id of entries) { - /** - * Latest item. - */ - items.push( - entity.deleteBatch({ - PK: createPartitionKey({ - id, - locale: model.locale, - tenant: model.tenant - }), - SK: "L" - }) - ); - esItems.push( - esEntity.deleteBatch({ - PK: createPartitionKey({ - id, - locale: model.locale, - tenant: model.tenant - }), - SK: "L" - }) - ); - /** - * Published item. - */ + const partitionKey = createPartitionKey({ + id, + locale: model.locale, + tenant: model.tenant + }); + + const entryItems = await queryAll<CmsEntry>({ + entity, + partitionKey, + options: { + gte: " " + } + }); + + const esEntryItems = await queryAll<CmsEntry>({ + entity: esEntity, + partitionKey, + options: { + gte: " " + } + }); + items.push( - entity.deleteBatch({ - PK: createPartitionKey({ - id, - locale: model.locale, - tenant: model.tenant - }), - SK: "P" + ...entryItems.map(item => { + return entity.deleteBatch({ + PK: item.PK, + SK: item.SK + }); }) ); + esItems.push( - esEntity.deleteBatch({ - PK: createPartitionKey({ - id, - locale: model.locale, - tenant: model.tenant - }), - SK: "P" - }) - ); - } - /** - * Exact revisions of all the entries - */ - for (const revision of revisions) { - items.push( - entity.deleteBatch({ - PK: createPartitionKey({ - id: revision.id, - locale: model.locale, - tenant: model.tenant - }), - SK: createRevisionSortKey({ - version: revision.version - }) + ...esEntryItems.map(item => { + return esEntity.deleteBatch({ + PK: item.PK, + SK: item.SK + }); }) ); } diff --git a/packages/app-form-builder/src/admin/graphql.ts b/packages/app-form-builder/src/admin/graphql.ts index 45b91f7e907..b0b0e0d7f2d 100644 --- a/packages/app-form-builder/src/admin/graphql.ts +++ b/packages/app-form-builder/src/admin/graphql.ts @@ -5,6 +5,7 @@ import { FbFormSubmissionData, FbMetaResponse, FbRevisionModel, + FbFormOverallStats, FormBuilderImportExportSubTask } from "~/types"; @@ -115,11 +116,6 @@ export const GET_FORM = gql` form: getForm(revision: $revision) { data { ${BASE_FORM_FIELDS} - overallStats { - views - submissions - conversionRate - } } error { ${ERROR_FIELDS} @@ -159,6 +155,37 @@ export const GET_FORM_REVISIONS = gql` } `; +/** + * #################### + * Get Form Overall Stats Query + */ +export interface GetFormOverallStatsQueryResponse { + formBuilder: { + getFormOverallStats: { + data: FbFormOverallStats; + error: FbErrorResponse | null; + }; + }; +} +export interface GetFormOverallStatsQueryVariables { + id: string; +} +export const GET_FORM_OVERALL_STATS = gql` + query FbGetFormOverallStats($id: ID!) { + formBuilder { + getFormOverallStats(formId: $id) { + data { + views + submissions + } + error { + ${ERROR_FIELDS} + } + } + } + } +`; + /** * ############################ * List Form Submissions Query Response diff --git a/packages/app-form-builder/src/admin/plugins/formDetails/formSubmissions/FormSubmissionsOverview.tsx b/packages/app-form-builder/src/admin/plugins/formDetails/formSubmissions/FormSubmissionsOverview.tsx index d9eab8deb4c..d3b5a70759b 100644 --- a/packages/app-form-builder/src/admin/plugins/formDetails/formSubmissions/FormSubmissionsOverview.tsx +++ b/packages/app-form-builder/src/admin/plugins/formDetails/formSubmissions/FormSubmissionsOverview.tsx @@ -2,10 +2,10 @@ import * as React from "react"; import Block from "./Block"; import { Typography } from "@webiny/ui/Typography"; import styled from "@emotion/styled"; -import { FbFormModel } from "~/types"; +import { FbFormOverallStats } from "~/types"; interface FormSubmissionsOverviewProps { - form: FbFormModel; + stats: FbFormOverallStats; } const StatBox = styled("div")({ @@ -24,20 +24,26 @@ const ContentWrapper = styled("div")({ boxSizing: "border-box" }); -export const FormSubmissionsOverview = ({ form }: FormSubmissionsOverviewProps) => { +const calculateConversionRate = (submissions: number, views: number) => { + return views > 0 ? parseFloat(((submissions / views) * 100).toFixed(2)) : 0; +}; + +export const FormSubmissionsOverview = ({ stats }: FormSubmissionsOverviewProps) => { + const conversionRate = calculateConversionRate(stats.submissions, stats.views); + return ( <Block title="Overview"> <ContentWrapper> <StatBox> - <Typography use="headline2">{form.overallStats.submissions}</Typography> + <Typography use="headline2">{stats.submissions}</Typography> <Typography use="overline">Submissions</Typography> </StatBox> <StatBox> - <Typography use="headline2">{form.overallStats.views}</Typography> + <Typography use="headline2">{stats.views}</Typography> <Typography use="overline">Views</Typography> </StatBox> <StatBox> - <Typography use="headline2">{form.overallStats.conversionRate}%</Typography> + <Typography use="headline2">{conversionRate}%</Typography> <Typography use="overline">Conversion Rate</Typography> </StatBox> </ContentWrapper> diff --git a/packages/app-form-builder/src/admin/plugins/formDetails/formSubmissions/index.tsx b/packages/app-form-builder/src/admin/plugins/formDetails/formSubmissions/index.tsx index 409497894f5..f1ad9856080 100644 --- a/packages/app-form-builder/src/admin/plugins/formDetails/formSubmissions/index.tsx +++ b/packages/app-form-builder/src/admin/plugins/formDetails/formSubmissions/index.tsx @@ -18,7 +18,7 @@ export default [ { name: "forms-form-details-revision-content-submissions", type: "forms-form-details-revision-content", - render({ form, loading, security }) { + render({ form, stats, loading, security }) { const { getPermissions } = security; const fbFormPermissions = getPermissions("fb.form"); @@ -49,8 +49,10 @@ export default [ <div style={{ position: "relative" }}> {loading && <CircularProgress />} {form && + stats && renderPlugins("forms-form-details-submissions", { - form + form, + stats })} </div> </RenderBlock> @@ -61,8 +63,8 @@ export default [ { name: "forms-form-details-submissions-overview", type: "forms-form-details-submissions", - render({ form }) { - return <FormSubmissionsOverview form={form} />; + render({ stats }) { + return <FormSubmissionsOverview stats={stats} />; } } as FbFormDetailsSubmissionsPlugin, { diff --git a/packages/app-form-builder/src/admin/views/Forms/FormDetails.tsx b/packages/app-form-builder/src/admin/views/Forms/FormDetails.tsx index 05f820d95e5..58cfcd7f1a3 100644 --- a/packages/app-form-builder/src/admin/views/Forms/FormDetails.tsx +++ b/packages/app-form-builder/src/admin/views/Forms/FormDetails.tsx @@ -6,10 +6,13 @@ import styled from "@emotion/styled"; import { GET_FORM, GET_FORM_REVISIONS, + GET_FORM_OVERALL_STATS, GetFormRevisionQueryResponse, GetFormRevisionQueryVariables, GetFormRevisionsQueryResponse, - GetFormRevisionsQueryVariables + GetFormRevisionsQueryVariables, + GetFormOverallStatsQueryResponse, + GetFormOverallStatsQueryVariables } from "../../graphql"; import { useSnackbar } from "@webiny/app-admin/hooks/useSnackbar"; import { Tabs } from "@webiny/ui/Tabs"; @@ -105,6 +108,16 @@ const FormDetails = ({ onCreateForm }: FormDetailsProps) => { } ); + const getFormOverallStats = useQuery< + GetFormOverallStatsQueryResponse, + GetFormOverallStatsQueryVariables + >(GET_FORM_OVERALL_STATS, { + variables: { + id: formId || "" + }, + skip: !formId + }); + if (!formId) { return <EmptyFormDetails canCreate={canCreate} onCreateForm={onCreateForm} />; } @@ -114,6 +127,10 @@ const FormDetails = ({ onCreateForm }: FormDetailsProps) => { getRevisions.loading || !getRevisions.data ? [] : getRevisions.data.formBuilder.revisions.data; + const stats = + getFormOverallStats.loading || !getFormOverallStats.data + ? {} + : getFormOverallStats.data.formBuilder.getFormOverallStats.data; return ( <DetailsContainer> @@ -122,7 +139,14 @@ const FormDetails = ({ onCreateForm }: FormDetailsProps) => { <Tabs> {renderPlugins( "forms-form-details-revision-content", - { security, refreshForms, form, revisions, loading: getForm.loading }, + { + security, + refreshForms, + form, + revisions, + stats, + loading: getForm.loading + }, { wrapper: false } )} </Tabs> diff --git a/packages/app-form-builder/src/types.ts b/packages/app-form-builder/src/types.ts index 2e11cd9280f..36ffeb51b9f 100644 --- a/packages/app-form-builder/src/types.ts +++ b/packages/app-form-builder/src/types.ts @@ -197,6 +197,7 @@ export interface FbFormDetailsPluginRenderParams { security: Record<string, any>; refreshForms: () => Promise<void>; form: FbFormModel; + stats: FbFormOverallStats; revisions: FbRevisionModel[]; loading: boolean; } @@ -208,7 +209,7 @@ export type FbFormDetailsPluginType = Plugin & { export type FbFormDetailsSubmissionsPlugin = Plugin & { type: "forms-form-details-submissions"; - render: (props: { form: FbFormModel }) => React.ReactNode; + render: (props: { form: FbFormModel; stats: FbFormOverallStats }) => React.ReactNode; }; export interface FbFormModel { @@ -222,15 +223,15 @@ export interface FbFormModel { status: string; savedOn: string; revisions: FbRevisionModel[]; - overallStats: { - submissions: number; - views: number; - conversionRate: number; - }; createdBy: FbCreatedBy; triggers: Record<string, any>; } +export interface FbFormOverallStats { + views: number; + submissions: number; +} + export interface FbFormRenderModel extends Omit<FbFormModel, "fields"> { fields: FormRenderFbFormModelField[]; } diff --git a/packages/app-page-builder-elements/src/renderers/form/types.ts b/packages/app-page-builder-elements/src/renderers/form/types.ts index ac54d1a23d9..6cc51b6c028 100644 --- a/packages/app-page-builder-elements/src/renderers/form/types.ts +++ b/packages/app-page-builder-elements/src/renderers/form/types.ts @@ -59,11 +59,6 @@ export interface FormData { status: string; savedOn: string; revisions: FormDataRevision[]; - overallStats: { - submissions: number; - views: number; - conversionRate: number; - }; createdBy: FormDataCreatedBy; triggers: Record<string, any>; } From 6c92b208fde418c515954092f025ca0cc0b2b113 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone <leonardo.giacone@gmail.com> Date: Wed, 14 Feb 2024 11:38:40 +0100 Subject: [PATCH 35/37] fix: remove ownedBy where param --- packages/api-form-builder/src/plugins/crud/forms.crud.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api-form-builder/src/plugins/crud/forms.crud.ts b/packages/api-form-builder/src/plugins/crud/forms.crud.ts index e8e2d55e0eb..52d5da61d84 100644 --- a/packages/api-form-builder/src/plugins/crud/forms.crud.ts +++ b/packages/api-form-builder/src/plugins/crud/forms.crud.ts @@ -155,7 +155,7 @@ export const createFormsCrud = (params: CreateFormsCrudParams): FormsCRUD => { if (await formsPermissions.canAccessOnlyOwnRecords()) { const identity = context.security.getIdentity(); - listFormParams.where.ownedBy = identity.id; + listFormParams.where.createdBy = identity.id; } try { From 2ea20522c43810759a29bbd6aaa3c950309012cc Mon Sep 17 00:00:00 2001 From: Leonardo Giacone <leonardo.giacone@gmail.com> Date: Wed, 14 Feb 2024 11:38:54 +0100 Subject: [PATCH 36/37] fix: revisions tests --- .../api-form-builder/__tests__/forms.test.ts | 19 ++++++------------- .../__tests__/graphql/forms.ts | 4 ++-- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/api-form-builder/__tests__/forms.test.ts b/packages/api-form-builder/__tests__/forms.test.ts index 9f81ce40ecd..fcbafe0137e 100644 --- a/packages/api-form-builder/__tests__/forms.test.ts +++ b/packages/api-form-builder/__tests__/forms.test.ts @@ -296,7 +296,7 @@ describe('Form Builder "Form" Test', () => { await publishRevision({ revision: id }); // Get the published form - const [{ data: get }] = await getPublishedForm({ formId }); + const [{ data: get }] = await getPublishedForm({ parent: formId }); expect(get.formBuilder.getPublishedForm.data.id).toEqual(id); // Create a new revision @@ -304,7 +304,7 @@ describe('Form Builder "Form" Test', () => { const { id: id2 } = create2.data.formBuilder.createRevisionFrom.data; // Published form should still be #1 - const [published] = await getPublishedForm({ formId }); + const [published] = await getPublishedForm({ parent: formId }); expect(published.data.formBuilder.getPublishedForm.data.id).toEqual(id); // Latest revision should be #2 @@ -326,7 +326,7 @@ describe('Form Builder "Form" Test', () => { await publishRevision({ revision: id2 }); // Published form should now be #2 - const [published2] = await getPublishedForm({ formId }); + const [published2] = await getPublishedForm({ parent: formId }); expect(published2.data.formBuilder.getPublishedForm.data.id).toEqual(id2); // Increment views for #2 @@ -345,18 +345,11 @@ describe('Form Builder "Form" Test', () => { await unpublishRevision({ revision: id2 }); // There should be no published forms - const [published3] = await getPublishedForm({ formId }); - expect(published3).toEqual({ + const [published3] = await getPublishedForm({ parent: formId }); + expect(published3).toMatchObject({ data: { formBuilder: { - getPublishedForm: { - data: null, - error: { - message: `Form "${formId}" was not found!`, - code: "NOT_FOUND", - data: null - } - } + getPublishedForm: null } } }); diff --git a/packages/api-form-builder/__tests__/graphql/forms.ts b/packages/api-form-builder/__tests__/graphql/forms.ts index 7e4bf7402ad..a73b331ac4c 100644 --- a/packages/api-form-builder/__tests__/graphql/forms.ts +++ b/packages/api-form-builder/__tests__/graphql/forms.ts @@ -172,9 +172,9 @@ export const GET_FORM_REVISIONS = /* GraphQL */ ` `; export const GET_PUBLISHED_FORM = /* GraphQL */ ` - query GetPublishedForm($formId: ID) { + query GetPublishedForm($revision: ID, $parent: ID) { formBuilder { - getPublishedForm(formId: $formId) { + getPublishedForm(revision: $revision, parent: $parent) { data ${FORM_DATA_FIELD} error ${ERROR_FIELD} } From 01bccb0ad5a83410301bd0600d3d2c524c7cc62a Mon Sep 17 00:00:00 2001 From: Leonardo Giacone <leonardo.giacone@gmail.com> Date: Wed, 14 Feb 2024 14:37:26 +0100 Subject: [PATCH 37/37] chore: merge next --- packages/api-form-builder-so-ddb-es/src/index.ts | 7 ++++++- packages/api-form-builder-so-ddb-es/src/types.ts | 9 ++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/api-form-builder-so-ddb-es/src/index.ts b/packages/api-form-builder-so-ddb-es/src/index.ts index dc1182848da..2ba68d6be32 100644 --- a/packages/api-form-builder-so-ddb-es/src/index.ts +++ b/packages/api-form-builder-so-ddb-es/src/index.ts @@ -1,5 +1,5 @@ import WebinyError from "@webiny/error"; -import { FormBuilderStorageOperationsFactory, ENTITIES } from "~/types"; +import { FormBuilderStorageOperationsFactory, ENTITIES, FormBuilderContext } from "~/types"; import { createTable } from "~/definitions/table"; import { createSystemEntity } from "~/definitions/system"; import { createSettingsEntity } from "~/definitions/settings"; @@ -9,6 +9,8 @@ import { createSettingsStorageOperations } from "~/operations/settings"; import { createFormStorageOperations } from "~/operations/form"; import { createFormStatsStorageOperations } from "~/operations/formStats"; import { createElasticsearchTable } from "~/definitions/tableElasticsearch"; +import { createIndexTaskPlugin } from "~/tasks/createIndexTaskPlugin"; +import { elasticsearchIndexPlugins } from "~/elasticsearch/indices"; const reservedFields = ["PK", "SK", "index", "data", "TYPE", "__type", "GSI1_PK", "GSI1_SK"]; @@ -59,6 +61,9 @@ export const createFormBuilderStorageOperations: FormBuilderStorageOperationsFac }; return { + beforeInit: async (context: FormBuilderContext) => { + context.plugins.register([createIndexTaskPlugin(), elasticsearchIndexPlugins()]); + }, getTable: () => table, getEsTable: () => esTable, getEntities: () => entities, diff --git a/packages/api-form-builder-so-ddb-es/src/types.ts b/packages/api-form-builder-so-ddb-es/src/types.ts index 2342c54d5b4..8fd26c43fa6 100644 --- a/packages/api-form-builder-so-ddb-es/src/types.ts +++ b/packages/api-form-builder-so-ddb-es/src/types.ts @@ -3,7 +3,8 @@ import { FormBuilderSystemStorageOperations as BaseFormBuilderSystemStorageOperations, FormBuilderSubmissionStorageOperations, FormBuilderSettingsStorageOperations as BaseFormBuilderSettingsStorageOperations, - FormBuilderFormStorageOperations + FormBuilderFormStorageOperations, + FormBuilderContext } from "@webiny/api-form-builder/types"; import { DynamoDBClient } from "@webiny/aws-sdk/client-dynamodb"; import { Entity } from "@webiny/db-dynamodb/toolbox"; @@ -12,11 +13,9 @@ import { Client } from "@elastic/elasticsearch"; export type Attributes = Record<string, AttributeDefinition>; +export { FormBuilderContext }; + export enum ENTITIES { - FORM = "FormBuilderForm", - ES_FORM = "FormBuilderFormEs", - SUBMISSION = "FormBuilderSubmission", - ES_SUBMISSION = "FormBuilderSubmissionEs", SYSTEM = "FormBuilderSystem", SETTINGS = "FormBuilderSettings" }