From 4931327c2d1dd02b32d5b3f68872a89930441cf5 Mon Sep 17 00:00:00 2001 From: Leonardo Giacone Date: Fri, 28 Mar 2025 10:00:16 +0100 Subject: [PATCH 01/12] fix(api-aco): `extensions` default model field (#4580) --- packages/api-aco/src/folder/folder.model.ts | 20 ++++++++++++++++++- .../src/createFieldsList.ts | 4 ++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/api-aco/src/folder/folder.model.ts b/packages/api-aco/src/folder/folder.model.ts index 073b7837eb6..95e4d809e56 100644 --- a/packages/api-aco/src/folder/folder.model.ts +++ b/packages/api-aco/src/folder/folder.model.ts @@ -113,6 +113,17 @@ const permissionsField = () => } }); +const extensionsField = () => + createModelField({ + label: "Extensions", + fieldId: "extensions", + type: "object", + settings: { + layout: [], + fields: [] + } + }); + export const FOLDER_MODEL_ID = "acoFolder"; export const createFolderModel = () => { @@ -127,6 +138,13 @@ export const createFolderModel = () => { // flp: true }, titleFieldId: "title", - fields: [titleField(), slugField(), typeField(), parentIdField(), permissionsField()] + fields: [ + titleField(), + slugField(), + typeField(), + parentIdField(), + permissionsField(), + extensionsField() + ] }); }; diff --git a/packages/app-headless-cms-common/src/createFieldsList.ts b/packages/app-headless-cms-common/src/createFieldsList.ts index f7228d2385d..bda6855183b 100644 --- a/packages/app-headless-cms-common/src/createFieldsList.ts +++ b/packages/app-headless-cms-common/src/createFieldsList.ts @@ -47,10 +47,10 @@ export function createFieldsList({ }) .filter(Boolean); /** - * If there are no fields, let's always load the `id` field. + * If there are no fields, let's always load the `_empty` field. */ if (fields.length === 0) { - fields.push("id"); + fields.push("_empty"); } return fields.join("\n"); } From fdf4a5e06080e16be06ca5b538fd17ed70603706 Mon Sep 17 00:00:00 2001 From: Adrian Smijulj Date: Fri, 28 Mar 2025 10:02:54 +0100 Subject: [PATCH 02/12] fix: expand PB / FM APIs to allow GQL API-based data migrations (#4581) --- .../src/plugins/graphqlFileStorageS3.ts | 1 + .../src/operations/pages/index.ts | 40 +++- .../src/operations/pages/index.ts | 16 ++ .../__tests__/graphql/createPage.test.ts | 209 ++++++++++++++++++ .../__tests__/graphql/graphql/pages.ts | 14 ++ .../__tests__/graphql/useGqlHandler.ts | 4 + .../src/graphql/crud/categories.crud.ts | 11 +- .../src/graphql/crud/categories/validation.ts | 10 +- .../src/graphql/crud/menus.crud.ts | 11 +- .../src/graphql/crud/menus/validation.ts | 10 +- .../src/graphql/crud/pages.crud.ts | 163 +++++++++++++- .../src/graphql/crud/utils/formatDate.ts | 12 + .../src/graphql/crud/utils/getDate.ts | 14 ++ .../src/graphql/crud/utils/getIdentity.ts | 16 ++ .../src/graphql/graphql/base.gql.ts | 6 + .../src/graphql/graphql/categories.gql.ts | 2 + .../src/graphql/graphql/menus.gql.ts | 2 + .../src/graphql/graphql/pages.gql.ts | 26 +++ .../api-page-builder/src/graphql/types.ts | 121 ++++++++++ .../prerendering/hooks/afterSettingsUpdate.ts | 57 +---- 20 files changed, 670 insertions(+), 75 deletions(-) create mode 100644 packages/api-page-builder/__tests__/graphql/createPage.test.ts create mode 100644 packages/api-page-builder/src/graphql/crud/utils/formatDate.ts create mode 100644 packages/api-page-builder/src/graphql/crud/utils/getDate.ts create mode 100644 packages/api-page-builder/src/graphql/crud/utils/getIdentity.ts diff --git a/packages/api-file-manager-s3/src/plugins/graphqlFileStorageS3.ts b/packages/api-file-manager-s3/src/plugins/graphqlFileStorageS3.ts index 43ffb13fc85..80fba5e9171 100644 --- a/packages/api-file-manager-s3/src/plugins/graphqlFileStorageS3.ts +++ b/packages/api-file-manager-s3/src/plugins/graphqlFileStorageS3.ts @@ -25,6 +25,7 @@ const plugin: GraphQLSchemaPlugin = { } input PreSignedPostPayloadInput { + id: ID name: String! type: String! size: Long! diff --git a/packages/api-page-builder-so-ddb-es/src/operations/pages/index.ts b/packages/api-page-builder-so-ddb-es/src/operations/pages/index.ts index b523ec37d03..239b80bef5b 100644 --- a/packages/api-page-builder-so-ddb-es/src/operations/pages/index.ts +++ b/packages/api-page-builder-so-ddb-es/src/operations/pages/index.ts @@ -69,6 +69,7 @@ export interface CreatePageStorageOperationsParams { elasticsearch: Client; plugins: PluginsContainer; } + export const createPageStorageOperations = ( params: CreatePageStorageOperationsParams ): PageStorageOperations => { @@ -86,6 +87,11 @@ export const createPageStorageOperations = ( SK: createLatestSortKey() }; + const publishedKeys = { + ...versionKeys, + SK: createPublishedSortKey() + }; + const entityBatch = createEntityWriteBatch({ entity, put: [ @@ -103,17 +109,41 @@ export const createPageStorageOperations = ( }); const esData = getESLatestPageData(plugins, page, input); - try { - await entityBatch.execute(); - await put({ - entity: esEntity, - item: { + const elasticsearchEntityBatch = createEntityWriteBatch({ + entity: esEntity, + put: [ + { index: configurations.es(page).index, data: esData, ...latestKeys } + ] + }); + + if (page.status === "published") { + entityBatch.put({ + ...page, + ...publishedKeys, + TYPE: createPublishedType() + }); + + entityBatch.put({ + ...page, + TYPE: createPublishedPathType(), + PK: createPathPartitionKey(page), + SK: createPathSortKey(page) }); + + elasticsearchEntityBatch.put({ + index: configurations.es(page).index, + data: getESPublishedPageData(plugins, page), + ...publishedKeys + }); + } + try { + await entityBatch.execute(); + await elasticsearchEntityBatch.execute(); return page; } catch (ex) { throw new WebinyError( diff --git a/packages/api-page-builder-so-ddb/src/operations/pages/index.ts b/packages/api-page-builder-so-ddb/src/operations/pages/index.ts index 1938bace62b..75fb2868e0a 100644 --- a/packages/api-page-builder-so-ddb/src/operations/pages/index.ts +++ b/packages/api-page-builder-so-ddb/src/operations/pages/index.ts @@ -81,6 +81,7 @@ export interface CreatePageStorageOperationsParams { entity: Entity; plugins: PluginsContainer; } + export const createPageStorageOperations = ( params: CreatePageStorageOperationsParams ): PageStorageOperations => { @@ -98,6 +99,11 @@ export const createPageStorageOperations = ( SK: createLatestSortKey(page) }; + const publishedKeys = { + PK: createPublishedPartitionKey(page), + SK: createPublishedSortKey(page) + }; + const titleLC = page.title.toLowerCase(); /** * We need to create @@ -122,6 +128,16 @@ export const createPageStorageOperations = ( ] }); + if (page.status === "published") { + entityBatch.put({ + ...page, + ...publishedKeys, + GSI1_PK: createPathPartitionKey(page), + GSI1_SK: page.path, + TYPE: createPublishedType() + }); + } + try { await entityBatch.execute(); return page; diff --git a/packages/api-page-builder/__tests__/graphql/createPage.test.ts b/packages/api-page-builder/__tests__/graphql/createPage.test.ts new file mode 100644 index 00000000000..dea0a692297 --- /dev/null +++ b/packages/api-page-builder/__tests__/graphql/createPage.test.ts @@ -0,0 +1,209 @@ +import useGqlHandler from "./useGqlHandler"; + +jest.setTimeout(100000); + +describe("CRUD Test", () => { + const handler = useGqlHandler(); + + const { createCategory, createPageV2, getPage, getPublishedPage } = handler; + + it("creating pages via the new createPagesV2 mutation", async () => { + await createCategory({ + data: { + slug: `slug`, + name: `name`, + url: `/some-url/`, + layout: `layout` + } + }); + + const page = { + id: "67e15c96026bd2000222d698#0001", + pid: "67e15c96026bd2000222d698", + category: "slug", + version: 1, + title: "Welcome to Webiny", + path: "/welcome-to-webiny", + content: { + id: "Fv1PpPWu-", + type: "document", + data: { + settings: {} + }, + elements: [] + }, + status: "published", + publishedOn: "2025-03-24T13:22:30.918Z", + settings: { + general: { + snippet: null, + tags: null, + layout: "static", + image: null + }, + social: { + meta: [], + title: null, + description: null, + image: null + }, + seo: { + title: null, + description: null, + meta: [] + } + }, + createdOn: "2025-03-24T13:22:30.363Z", + createdBy: { + id: "67e15c7d026bd2000222d67a", + displayName: "ad min", + type: "admin" + } + }; + + // The V2 of the createPage mutation should allow us to create pages with + // predefined `createdOn`, `createdBy`, `id`, and also immediately have the + // page published. + await createPageV2({ data: page }); + + const [getPageResponse] = await getPage({ id: page.id }); + + expect(getPageResponse).toMatchObject({ + data: { + pageBuilder: { + getPage: { + data: { + id: "67e15c96026bd2000222d698#0001", + pid: "67e15c96026bd2000222d698", + editor: "page-builder", + category: { + slug: "slug" + }, + version: 1, + title: "Welcome to Webiny", + path: "/welcome-to-webiny", + url: "https://www.test.com/welcome-to-webiny", + content: { + id: "Fv1PpPWu-", + type: "document", + data: { + settings: {} + }, + elements: [] + }, + savedOn: "2025-03-24T13:22:30.363Z", + status: "published", + locked: true, + publishedOn: "2025-03-24T13:22:30.918Z", + revisions: [ + { + id: "67e15c96026bd2000222d698#0001", + status: "published", + locked: true, + version: 1 + } + ], + settings: { + general: { + snippet: null, + tags: null, + layout: "static", + image: null + }, + social: { + meta: [], + title: null, + description: null, + image: null + }, + seo: { + title: null, + description: null, + meta: [] + } + }, + createdFrom: null, + createdOn: "2025-03-24T13:22:30.363Z", + createdBy: { + id: "67e15c7d026bd2000222d67a", + displayName: "ad min", + type: "admin" + } + }, + error: null + } + } + } + }); + + const [getPublishedPageResponse] = await getPublishedPage({ id: page.id }); + + expect(getPublishedPageResponse).toMatchObject({ + data: { + pageBuilder: { + getPublishedPage: { + data: { + id: "67e15c96026bd2000222d698#0001", + pid: "67e15c96026bd2000222d698", + editor: "page-builder", + category: { + slug: "slug" + }, + version: 1, + title: "Welcome to Webiny", + path: "/welcome-to-webiny", + url: "https://www.test.com/welcome-to-webiny", + content: { + id: "Fv1PpPWu-", + type: "document", + data: { + settings: {} + }, + elements: [] + }, + savedOn: "2025-03-24T13:22:30.363Z", + status: "published", + locked: true, + publishedOn: "2025-03-24T13:22:30.918Z", + revisions: [ + { + id: "67e15c96026bd2000222d698#0001", + status: "published", + locked: true, + version: 1 + } + ], + settings: { + general: { + snippet: null, + tags: null, + layout: "static", + image: null + }, + social: { + meta: [], + title: null, + description: null, + image: null + }, + seo: { + title: null, + description: null, + meta: [] + } + }, + createdFrom: null, + createdOn: "2025-03-24T13:22:30.363Z", + createdBy: { + id: "67e15c7d026bd2000222d67a", + displayName: "ad min", + type: "admin" + } + }, + error: null + } + } + } + }); + }); +}); diff --git a/packages/api-page-builder/__tests__/graphql/graphql/pages.ts b/packages/api-page-builder/__tests__/graphql/graphql/pages.ts index 9e2cfeaa999..34ae1f7c1c0 100644 --- a/packages/api-page-builder/__tests__/graphql/graphql/pages.ts +++ b/packages/api-page-builder/__tests__/graphql/graphql/pages.ts @@ -198,7 +198,21 @@ export const createPageCreateGraphQl = (params: CreateDataFieldsParams = {}) => `; }; +export const createPageCreateV2GraphQl = (params: CreateDataFieldsParams = {}) => { + return /* GraphQL */ ` + mutation CreatePageV2($data: PbCreatePageV2Input!) { + pageBuilder { + createPageV2(data: $data) { + data ${createDataFields(params)} + error ${ERROR_FIELD} + } + } + } + `; +}; + export const CREATE_PAGE = createPageCreateGraphQl(); +export const CREATE_PAGE_V2 = createPageCreateV2GraphQl(); export const createPageUpdateGraphQl = (params: CreateDataFieldsParams = {}) => { return /* GraphQL */ ` diff --git a/packages/api-page-builder/__tests__/graphql/useGqlHandler.ts b/packages/api-page-builder/__tests__/graphql/useGqlHandler.ts index d20f93a6350..7c681647fd4 100644 --- a/packages/api-page-builder/__tests__/graphql/useGqlHandler.ts +++ b/packages/api-page-builder/__tests__/graphql/useGqlHandler.ts @@ -26,6 +26,7 @@ import { } from "./graphql/pageElements"; import { CREATE_PAGE, + CREATE_PAGE_V2, DELETE_PAGE, DUPLICATE_PAGE, GET_PAGE, @@ -241,6 +242,9 @@ export default ({ permissions, identity, plugins }: Params = {}) => { async createPage(variables: Record) { return invoke({ body: { query: CREATE_PAGE, variables } }); }, + async createPageV2(variables: Record) { + return invoke({ body: { query: CREATE_PAGE_V2, variables } }); + }, async duplicatePage(variables: Record) { return invoke({ body: { query: DUPLICATE_PAGE, variables } }); }, diff --git a/packages/api-page-builder/src/graphql/crud/categories.crud.ts b/packages/api-page-builder/src/graphql/crud/categories.crud.ts index 158335ce06e..f94e6e68ac6 100644 --- a/packages/api-page-builder/src/graphql/crud/categories.crud.ts +++ b/packages/api-page-builder/src/graphql/crud/categories.crud.ts @@ -23,6 +23,8 @@ import { import { createZodError, removeUndefinedValues } from "@webiny/utils"; import { CategoriesPermissions } from "~/graphql/crud/permissions/CategoriesPermissions"; import { PagesPermissions } from "~/graphql/crud/permissions/PagesPermissions"; +import { getDate } from "~/graphql/crud/utils/getDate"; +import { getIdentity } from "~/graphql/crud/utils/getIdentity"; export interface CreateCategoriesCrudParams { context: PbContext; @@ -172,17 +174,14 @@ export const createCategoriesCrud = (params: CreateCategoriesCrudParams): Catego } const identity = context.security.getIdentity(); + const currentDateTime = new Date(); const data = validationResult.data; const category: Category = { ...data, - createdOn: new Date().toISOString(), - createdBy: { - id: identity.id, - type: identity.type, - displayName: identity.displayName - }, + createdOn: getDate(input.createdOn, currentDateTime), + createdBy: getIdentity(input.createdBy, identity), tenant: getTenantId(), locale: getLocaleCode() }; diff --git a/packages/api-page-builder/src/graphql/crud/categories/validation.ts b/packages/api-page-builder/src/graphql/crud/categories/validation.ts index 9dd72aee443..bd1b10e31fc 100644 --- a/packages/api-page-builder/src/graphql/crud/categories/validation.ts +++ b/packages/api-page-builder/src/graphql/crud/categories/validation.ts @@ -8,7 +8,15 @@ const baseValidation = zod.object({ export const createCategoryCreateValidation = () => { return baseValidation.extend({ - slug: zod.string().min(1).max(100) + slug: zod.string().min(1).max(100), + createdOn: zod.date().optional(), + createdBy: zod + .object({ + id: zod.string(), + type: zod.string(), + displayName: zod.string().nullable() + }) + .optional() }); }; diff --git a/packages/api-page-builder/src/graphql/crud/menus.crud.ts b/packages/api-page-builder/src/graphql/crud/menus.crud.ts index 8bdb7804c9a..2efa4b84f62 100644 --- a/packages/api-page-builder/src/graphql/crud/menus.crud.ts +++ b/packages/api-page-builder/src/graphql/crud/menus.crud.ts @@ -23,6 +23,8 @@ import { } from "~/graphql/crud/menus/validation"; import { createZodError, removeUndefinedValues } from "@webiny/utils"; import { MenusPermissions } from "~/graphql/crud/permissions/MenusPermissions"; +import { getIdentity } from "./utils/getIdentity"; +import { getDate } from "./utils/getDate"; export interface CreateMenuCrudParams { context: PbContext; @@ -175,16 +177,13 @@ export const createMenuCrud = (params: CreateMenuCrudParams): MenusCrud => { } const identity = context.security.getIdentity(); + const currentDateTime = new Date(); const menu: Menu = { ...data, items: data.items || [], - createdOn: new Date().toISOString(), - createdBy: { - id: identity.id, - type: identity.type, - displayName: identity.displayName - }, + createdOn: getDate(input.createdOn, currentDateTime), + createdBy: getIdentity(input.createdBy, identity), tenant: getTenantId(), locale: getLocaleCode() }; diff --git a/packages/api-page-builder/src/graphql/crud/menus/validation.ts b/packages/api-page-builder/src/graphql/crud/menus/validation.ts index b048c915bef..f150ccf9a29 100644 --- a/packages/api-page-builder/src/graphql/crud/menus/validation.ts +++ b/packages/api-page-builder/src/graphql/crud/menus/validation.ts @@ -8,7 +8,15 @@ const baseValidation = zod.object({ export const createMenuCreateValidation = () => { return baseValidation.extend({ - slug: zod.string().min(1).max(100) + slug: zod.string().min(1).max(100), + createdOn: zod.date().optional(), + createdBy: zod + .object({ + id: zod.string(), + type: zod.string(), + displayName: zod.string().nullable() + }) + .optional() }); }; diff --git a/packages/api-page-builder/src/graphql/crud/pages.crud.ts b/packages/api-page-builder/src/graphql/crud/pages.crud.ts index 7f8fb4a8e84..5105867920c 100644 --- a/packages/api-page-builder/src/graphql/crud/pages.crud.ts +++ b/packages/api-page-builder/src/graphql/crud/pages.crud.ts @@ -52,6 +52,8 @@ import { import { createCompression } from "~/graphql/crud/pages/compression"; import { PagesPermissions } from "./permissions/PagesPermissions"; import { PageContent } from "./pages/PageContent"; +import { getDate } from "~/graphql/crud/utils/getDate"; +import { getIdentity } from "~/graphql/crud/utils/getIdentity"; const STATUS_DRAFT = "draft"; const STATUS_PUBLISHED = "published"; @@ -289,12 +291,12 @@ export const createPageCrud = (params: CreatePageCrudParams): PagesCrud => { async processPageContent(page) { return processPageContent(page, pageElementProcessors); }, - async createPage(this: PageBuilderContextObject, slug, meta): Promise { + async createPage(this: PageBuilderContextObject, categorySlug, meta): Promise { await pagesPermissions.ensure({ rwd: "w" }); - const category = await this.getCategory(slug); + const category = await this.getCategory(categorySlug); if (!category) { - throw new NotFoundError(`Category with slug "${slug}" not found.`); + throw new NotFoundError(`Category with slug "${categorySlug}" not found.`); } const title = "Untitled"; @@ -399,7 +401,160 @@ export const createPageCrud = (params: CreatePageCrudParams): PagesCrud => { await storageOperations.pages.create({ input: { - slug + slug: categorySlug + }, + page: await compressPage(page) + }); + await onPageAfterCreate.publish({ page, meta }); + + return page; + } catch (ex) { + throw new WebinyError( + ex.message || "Could not create new page.", + ex.code || "CREATE_PAGE_ERROR", + { + ...(ex.data || {}), + page + } + ); + } + }, + + async createPageV2(this: PageBuilderContextObject, input, meta): Promise { + await pagesPermissions.ensure({ rwd: "w" }); + + const categorySlug = input.category; + if (!categorySlug) { + throw new WebinyError("Category slug is missing.", "CATEGORY_SLUG_MISSING"); + } + + const category = await this.getCategory(categorySlug); + if (!category) { + throw new NotFoundError(`Category with slug "${categorySlug}" not found.`); + } + + const title = input.title || "Untitled"; + + let pagePath = input.path; + if (!pagePath) { + if (category.slug === "static") { + pagePath = normalizePath("untitled-" + uniqid.time()) as string; + } else { + pagePath = normalizePath( + [category.url, "untitled-" + uniqid.time()].join("/").replace(/\/\//g, "/") + ) as string; + } + } + + const result = await createPageCreateValidation().safeParseAsync({ + category: category.slug + }); + if (!result.success) { + throw createZodError(result.error); + } + + const currentIdentity = context.security.getIdentity(); + const currentDateTime = new Date(); + + let pageId = "", + version = 1; + if (input.id) { + const splitId = input.id.split("#"); + pageId = splitId[0]; + version = Number(splitId[1]); + } else if (input.pid) { + pageId = input.pid; + } else { + pageId = mdbid(); + } + + if (input.version) { + version = input.version; + } + + const id = createIdentifier({ + id: pageId, + version: 1 + }); + + const rawSettings = input.settings || { + general: { + layout: category.layout + }, + social: { + description: null, + image: null, + meta: [], + title: null + }, + seo: { + title: null, + description: null, + meta: [] + } + }; + + const validation = createPageSettingsUpdateValidation(); + const settingsValidationResult = validation.safeParse(rawSettings); + if (!settingsValidationResult.success) { + throw createZodError(settingsValidationResult.error); + } + + const settings = settingsValidationResult.data; + const status = input.status || STATUS_DRAFT; + const locked = status !== STATUS_DRAFT; + + let publishedOn = null; + if (status === STATUS_PUBLISHED) { + publishedOn = getDate(input.publishedOn, currentDateTime); + } + + const page: Page = { + id, + pid: pageId, + locale: getLocaleCode(), + tenant: getTenantId(), + editor: DEFAULT_EDITOR, + category: category.slug, + title, + path: pagePath, + version, + status, + locked, + publishedOn, + createdFrom: null, + settings: { + ...settings, + general: { + ...settings.general, + tags: settings.general?.tags || undefined + }, + social: { + ...settings.social, + meta: settings.social?.meta || [] + }, + seo: { + ...settings.seo, + meta: settings.seo?.meta || [] + } + }, + createdOn: getDate(input.createdOn, currentDateTime), + savedOn: getDate(input.createdOn, currentDateTime), + createdBy: getIdentity(input.createdBy, currentIdentity), + ownedBy: getIdentity(input.createdBy, currentIdentity), + content: input.content || PageContent.createEmpty().getValue(), + webinyVersion: context.WEBINY_VERSION + }; + + try { + await onPageBeforeCreate.publish({ + page, + meta + }); + + await storageOperations.pages.create({ + input: { + slug: categorySlug }, page: await compressPage(page) }); diff --git a/packages/api-page-builder/src/graphql/crud/utils/formatDate.ts b/packages/api-page-builder/src/graphql/crud/utils/formatDate.ts new file mode 100644 index 00000000000..b3063eb352f --- /dev/null +++ b/packages/api-page-builder/src/graphql/crud/utils/formatDate.ts @@ -0,0 +1,12 @@ +/** + * Should not be used by users as method is prone to breaking changes. + * @internal + */ +export const formatDate = (date?: Date | string | null): string | null => { + if (!date) { + return null; + } else if (date instanceof Date) { + return date.toISOString(); + } + return new Date(date).toISOString(); +}; diff --git a/packages/api-page-builder/src/graphql/crud/utils/getDate.ts b/packages/api-page-builder/src/graphql/crud/utils/getDate.ts new file mode 100644 index 00000000000..2d4cd08fc5f --- /dev/null +++ b/packages/api-page-builder/src/graphql/crud/utils/getDate.ts @@ -0,0 +1,14 @@ +import { formatDate } from "./formatDate"; + +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-page-builder/src/graphql/crud/utils/getIdentity.ts b/packages/api-page-builder/src/graphql/crud/utils/getIdentity.ts new file mode 100644 index 00000000000..994e94d0662 --- /dev/null +++ b/packages/api-page-builder/src/graphql/crud/utils/getIdentity.ts @@ -0,0 +1,16 @@ +import { SecurityIdentity } from "@webiny/api-security/types"; + +export const getIdentity = ( + input: SecurityIdentity | null | undefined, + defaultValue: T | null = null +): 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; +}; diff --git a/packages/api-page-builder/src/graphql/graphql/base.gql.ts b/packages/api-page-builder/src/graphql/graphql/base.gql.ts index ad5016f52fc..0175ff29e75 100644 --- a/packages/api-page-builder/src/graphql/graphql/base.gql.ts +++ b/packages/api-page-builder/src/graphql/graphql/base.gql.ts @@ -23,6 +23,12 @@ export const createBaseGraphQL = (): GraphQLSchemaPlugin => { type: String } + input PbIdentityInput { + id: ID! + displayName: String! + type: String! + } + type PbError { code: String message: String diff --git a/packages/api-page-builder/src/graphql/graphql/categories.gql.ts b/packages/api-page-builder/src/graphql/graphql/categories.gql.ts index 7f17ec6db47..a729d829931 100644 --- a/packages/api-page-builder/src/graphql/graphql/categories.gql.ts +++ b/packages/api-page-builder/src/graphql/graphql/categories.gql.ts @@ -29,6 +29,8 @@ export const createCategoryGraphQL = (): GraphQLSchemaPlugin => { slug: String! url: String! layout: String! + createdBy: PbIdentityInput + createdOn: DateTime } # Response types diff --git a/packages/api-page-builder/src/graphql/graphql/menus.gql.ts b/packages/api-page-builder/src/graphql/graphql/menus.gql.ts index d37aceaaa01..c9c98e21124 100644 --- a/packages/api-page-builder/src/graphql/graphql/menus.gql.ts +++ b/packages/api-page-builder/src/graphql/graphql/menus.gql.ts @@ -23,6 +23,8 @@ export const createMenuGraphQL = (): GraphQLSchemaPlugin => { slug: String! description: String items: [JSON] + createdBy: PbIdentityInput + createdOn: DateTime } # Response types diff --git a/packages/api-page-builder/src/graphql/graphql/pages.gql.ts b/packages/api-page-builder/src/graphql/graphql/pages.gql.ts index 5f910fdafab..1c938afc965 100644 --- a/packages/api-page-builder/src/graphql/graphql/pages.gql.ts +++ b/packages/api-page-builder/src/graphql/graphql/pages.gql.ts @@ -109,6 +109,24 @@ const createBasePageGraphQL = (): GraphQLSchemaPlugin => { dataBindings: [DataBindingInput!] } + input PbCreatePageV2Input { + id: ID + pid: ID + category: ID + title: String + version: Int + path: String + content: JSON + savedOn: DateTime + status: String + publishedOn: DateTime + settings: PbPageSettingsInput + createdOn: DateTime + createdBy: PbIdentityInput + dataSources: [DataSourceInput!] + dataBindings: [DataBindingInput!] + } + input PbPageSettingsInput { _empty: String } @@ -242,6 +260,8 @@ const createBasePageGraphQL = (): GraphQLSchemaPlugin => { extend type PbMutation { createPage(from: ID, category: String, meta: JSON): PbPageResponse + createPageV2(data: PbCreatePageV2Input!): PbPageResponse + # Update page by given ID. updatePage(id: ID!, data: PbUpdatePageInput!): PbPageResponse @@ -457,6 +477,12 @@ const createBasePageGraphQL = (): GraphQLSchemaPlugin => { return context.pageBuilder.createPage(category as string, meta); }); }, + createPageV2: async (_, args: any, context) => { + return resolve(() => { + const { data } = args; + return context.pageBuilder.createPageV2(data); + }); + }, deletePage: async (_, args: any, context) => { return resolve(async () => { diff --git a/packages/api-page-builder/src/graphql/types.ts b/packages/api-page-builder/src/graphql/types.ts index fc2ad2e23c7..a61f56163ae 100644 --- a/packages/api-page-builder/src/graphql/types.ts +++ b/packages/api-page-builder/src/graphql/types.ts @@ -9,6 +9,7 @@ import { Context as BaseContext } from "@webiny/handler/types"; import { BlockCategory, Category, + CreatedBy, DefaultSettings, DynamicDocument, Menu, @@ -17,6 +18,7 @@ import { PageElement, PageSettings, PageSpecialType, + PageStatus, PageTemplate, PageTemplateInput, Settings, @@ -32,8 +34,10 @@ export interface ListPagesParamsWhere { category?: string; status?: string; tags?: { query: string[]; rule?: "any" | "all" }; + [key: string]: any; } + export interface ListPagesParams { limit?: number; after?: string | null; @@ -75,6 +79,7 @@ export interface OnPageBeforeCreateTopicParams { page: TPage; meta?: Record; } + /** * @category Lifecycle events */ @@ -82,6 +87,7 @@ export interface OnPageAfterCreateTopicParams { page: TPage; meta?: Record; } + /** * @category Lifecycle events */ @@ -90,6 +96,7 @@ export interface OnPageBeforeUpdateTopicParams { page: TPage; input: Record; } + /** * @category Lifecycle events */ @@ -98,6 +105,7 @@ export interface OnPageAfterUpdateTopicParams { page: TPage; input: Record; } + /** * @category Lifecycle events */ @@ -105,6 +113,7 @@ export interface OnPageBeforeCreateFromTopicParams { original: TPage; page: TPage; } + /** * @category Lifecycle events */ @@ -112,6 +121,7 @@ export interface OnPageAfterCreateFromTopicParams { original: TPage; page: TPage; } + /** * @category Lifecycle events */ @@ -121,6 +131,7 @@ export interface OnPageBeforeDeleteTopicParams { publishedPage: TPage | null; deleteMethod: "deleteAll" | "delete"; } + /** * @category Lifecycle events */ @@ -130,6 +141,7 @@ export interface OnPageAfterDeleteTopicParams { publishedPage: TPage | null; deleteMethod: "deleteAll" | "delete"; } + /** * @category Lifecycle events */ @@ -138,6 +150,7 @@ export interface OnPageBeforePublishTopicParams { latestPage: TPage; publishedPage: TPage | null; } + /** * @category Lifecycle events */ @@ -146,6 +159,7 @@ export interface OnPageAfterPublishTopicParams { latestPage: TPage; publishedPage: TPage | null; } + /** * @category Lifecycle events */ @@ -153,6 +167,7 @@ export interface OnPageBeforeUnpublishTopicParams { page: TPage; latestPage: TPage; } + /** * @category Lifecycle events */ @@ -193,34 +208,54 @@ export interface PageElementProcessor { */ export interface PagesCrud { addPageElementProcessor(processor: PageElementProcessor): void; + processPageContent(content: Page): Promise; + getPage(id: string, options?: GetPagesOptions): Promise; + listLatestPages( args: ListPagesParams, options?: ListLatestPagesOptions ): Promise<[TPage[], ListMeta]>; + listPublishedPages( args: ListPagesParams ): Promise<[TPage[], ListMeta]>; + listPagesTags(args: { search: { query: string } }): Promise; + getPublishedPageById(args: { id: string; preview?: boolean; }): Promise; + getPublishedPageByPath(args: { path: string }): Promise; + listPageRevisions(id: string): Promise; + createPage( category: string, meta?: Record ): Promise; + + createPageV2( + data: PbCreatePageV2Input, + meta?: Record + ): Promise; + createPageFrom( page: string, meta?: Record ): Promise; + updatePage(id: string, data: PbUpdatePageInput): Promise; + deletePage(id: string): Promise<[TPage, TPage]>; + publishPage(id: string): Promise; + unpublishPage(id: string): Promise; + prerendering: { render(args: RenderParams): Promise; flush(args: FlushParams): Promise; @@ -249,12 +284,14 @@ export interface ListPageElementsParams { export interface OnPageElementBeforeCreateTopicParams { pageElement: PageElement; } + /** * @category Lifecycle events */ export interface OnPageElementAfterCreateTopicParams { pageElement: PageElement; } + /** * @category Lifecycle events */ @@ -262,6 +299,7 @@ export interface OnPageElementBeforeUpdateTopicParams { original: PageElement; pageElement: PageElement; } + /** * @category Lifecycle events */ @@ -269,12 +307,14 @@ export interface OnPageElementAfterUpdateTopicParams { original: PageElement; pageElement: PageElement; } + /** * @category Lifecycle events */ export interface OnPageElementBeforeDeleteTopicParams { pageElement: PageElement; } + /** * @category Lifecycle events */ @@ -287,10 +327,15 @@ export interface OnPageElementAfterDeleteTopicParams { */ export interface PageElementsCrud { getPageElement(id: string): Promise; + listPageElements(params?: ListPageElementsParams): Promise; + createPageElement(data: Record): Promise; + updatePageElement(id: string, data: Record): Promise; + deletePageElement(id: string): Promise; + /** * Lifecycle events */ @@ -308,12 +353,14 @@ export interface PageElementsCrud { export interface OnCategoryBeforeCreateTopicParams { category: Category; } + /** * @category Lifecycle events */ export interface OnCategoryAfterCreateTopicParams { category: Category; } + /** * @category Lifecycle events */ @@ -321,6 +368,7 @@ export interface OnCategoryBeforeUpdateTopicParams { original: Category; category: Category; } + /** * @category Lifecycle events */ @@ -328,12 +376,14 @@ export interface OnCategoryAfterUpdateTopicParams { original: Category; category: Category; } + /** * @category Lifecycle events */ export interface OnCategoryBeforeDeleteTopicParams { category: Category; } + /** * @category Lifecycle events */ @@ -346,10 +396,15 @@ export interface OnCategoryAfterDeleteTopicParams { */ export interface CategoriesCrud { getCategory(slug: string, options?: { auth: boolean }): Promise; + listCategories(): Promise; + createCategory(data: PbCategoryInput): Promise; + updateCategory(slug: string, data: PbCategoryInput): Promise; + deleteCategory(slug: string): Promise; + onCategoryBeforeCreate: Topic; onCategoryAfterCreate: Topic; onCategoryBeforeUpdate: Topic; @@ -373,6 +428,7 @@ export interface OnMenuBeforeCreateTopicParams { menu: Menu; input: Record; } + /** * @category Lifecycle events */ @@ -380,6 +436,7 @@ export interface OnMenuAfterCreateTopicParams { menu: Menu; input: Record; } + /** * @category Lifecycle events */ @@ -387,6 +444,7 @@ export interface OnMenuBeforeUpdateTopicParams { original: Menu; menu: Menu; } + /** * @category Lifecycle events */ @@ -394,12 +452,14 @@ export interface OnMenuAfterUpdateTopicParams { original: Menu; menu: Menu; } + /** * @category Lifecycle events */ export interface OnMenuBeforeDeleteTopicParams { menu: Menu; } + /** * @category Lifecycle events */ @@ -412,17 +472,26 @@ interface CreateMenuInput { slug: string; description: string; items: any[]; + createdOn?: Date | string; + createdBy?: CreatedBy; } + /** * @category Menu */ export interface MenusCrud { getMenu(slug: string, options?: MenuGetOptions): Promise; + getPublicMenu(slug: string): Promise; + listMenus(params?: ListMenuParams): Promise; + createMenu(data: CreateMenuInput): Promise; + updateMenu(slug: string, data: Record): Promise; + deleteMenu(slug: string): Promise; + onMenuBeforeCreate: Topic; onMenuAfterCreate: Topic; onMenuBeforeUpdate: Topic; @@ -444,6 +513,7 @@ export interface SettingsUpdateTopicMetaParams { pages: [PageSpecialType, string | null | undefined, string, Page][]; }; } + /** * @category Lifecycle events */ @@ -452,6 +522,7 @@ export interface OnSettingsBeforeUpdateTopicParams { settings: Settings; meta: SettingsUpdateTopicMetaParams; } + /** * @category Lifecycle events */ @@ -484,20 +555,26 @@ export interface SettingsCrud { export interface OnSystemBeforeInstallTopicParams { tenant: string; } + /** * @category Lifecycle events */ export interface OnSystemAfterInstallTopicParams { tenant: string; } + /** * @category System */ export interface SystemCrud { getSystem: () => Promise; + getSystemVersion(): Promise; + setSystemVersion(version: string): Promise; + installSystem(args: { name: string; insertDemoData: boolean }): Promise; + /** * Lifecycle events */ @@ -518,12 +595,14 @@ export interface PbBlockCategoryInput { export interface OnBeforeBlockCategoryCreateTopicParams { blockCategory: BlockCategory; } + /** * @category Lifecycle events */ export interface OnAfterBlockCategoryCreateTopicParams { blockCategory: BlockCategory; } + /** * @category Lifecycle events */ @@ -531,6 +610,7 @@ export interface OnBeforeBlockCategoryUpdateTopicParams { original: BlockCategory; blockCategory: BlockCategory; } + /** * @category Lifecycle events */ @@ -538,12 +618,14 @@ export interface OnAfterBlockCategoryUpdateTopicParams { original: BlockCategory; blockCategory: BlockCategory; } + /** * @category Lifecycle events */ export interface OnBeforeBlockCategoryDeleteTopicParams { blockCategory: BlockCategory; } + /** * @category Lifecycle events */ @@ -556,10 +638,15 @@ export interface OnAfterBlockCategoryDeleteTopicParams { */ export interface BlockCategoriesCrud { getBlockCategory(slug: string, options?: { auth: boolean }): Promise; + listBlockCategories(): Promise; + createBlockCategory(data: PbBlockCategoryInput): Promise; + updateBlockCategory(slug: string, data: PbBlockCategoryInput): Promise; + deleteBlockCategory(slug: string): Promise; + /** * Lifecycle events */ @@ -577,6 +664,7 @@ export interface ListPageBlocksParams { blockCategory?: string; }; } + /** * @category Lifecycle events */ @@ -633,10 +721,15 @@ export type PageBlockUpdateInput = Partial; */ export interface PageBlocksCrud { getPageBlock(id: string): Promise; + listPageBlocks(params?: ListPageBlocksParams): Promise; + createPageBlock(data: PageBlockCreateInput): Promise; + updatePageBlock(id: string, data: PageBlockUpdateInput): Promise; + deletePageBlock(id: string): Promise; + resolvePageBlocks(content: Record | null): Promise; /** @@ -653,6 +746,7 @@ export interface PageBlocksCrud { export interface ListPageTemplatesParams { sort?: string[]; } + /** * @category Lifecycle events */ @@ -742,17 +836,25 @@ export interface PageTemplatesCrud { auth: boolean; } ): Promise; + listPageTemplates(params?: ListPageTemplatesParams): Promise; + createPageTemplate(data: PageTemplateInput): Promise; + createPageFromTemplate(data: CreatePageFromTemplateParams): Promise; + createTemplateFromPage( pageId: string, data: Pick ): Promise; + // Copy relevant data from page template to page instance, by reference. copyTemplateDataToPage(template: PageTemplate, page: Page): void; + updatePageTemplate(id: string, data: Record): Promise; + deletePageTemplate(id: string): Promise; + resolvePageTemplate(content: PageContentWithTemplate): Promise; /** @@ -878,6 +980,7 @@ export interface FlushParams { export interface PrerenderingHandlers { render(args: RenderParams): Promise; + flush(args: FlushParams): Promise; } @@ -886,6 +989,8 @@ export interface PbCategoryInput { slug: string; url: string; layout: string; + createdOn?: Date | string; + createdBy?: CreatedBy; } export interface PbUpdatePageInput extends DynamicDocument { @@ -895,3 +1000,19 @@ export interface PbUpdatePageInput extends DynamicDocument { settings?: PageSettings; content?: Record | null; } + +export interface PbCreatePageV2Input extends DynamicDocument { + title?: string; + category?: string; + path?: string; + status?: PageStatus; + publishedOn?: Date | string; + settings?: PageSettings; + content?: Record | null; + version?: number; + id?: string; + pid?: string; + createdOn?: Date | string; + createdBy?: CreatedBy; + ownedBy?: CreatedBy; +} diff --git a/packages/api-page-builder/src/prerendering/hooks/afterSettingsUpdate.ts b/packages/api-page-builder/src/prerendering/hooks/afterSettingsUpdate.ts index 3f22b5db150..9a01f0a8ae7 100644 --- a/packages/api-page-builder/src/prerendering/hooks/afterSettingsUpdate.ts +++ b/packages/api-page-builder/src/prerendering/hooks/afterSettingsUpdate.ts @@ -1,63 +1,16 @@ -import { PathItem, PbContext } from "~/graphql/types"; +import { PbContext } from "~/graphql/types"; import { ContextPlugin } from "@webiny/api"; export default () => { return new ContextPlugin(async ({ pageBuilder }) => { - pageBuilder.onSettingsAfterUpdate.subscribe(async params => { - const { settings, meta } = params; - if (!settings) { - return; - } - - /** - * If a change on pages settings (home, notFound) has been made, let's rerender accordingly. - */ - const toRender: PathItem[] = []; - - for (let i = 0; i < meta.diff.pages.length; i++) { - const [type, prevPageId, , page] = meta.diff.pages[i]; - switch (type) { - case "home": - toRender.push({ path: "/" }); - break; - case "notFound": - // Render the new "not found" page and store it into the NOT_FOUND_FOLDER. - toRender.push({ - path: page.path, - tags: [{ key: "notFoundPage", value: true }] - }); - - if (prevPageId) { - // Render the old "not found" page, to remove any notion of the "not found" concept - // from the snapshot, as well as the PS#RENDER record in the database. - const prevPage = await pageBuilder.getPublishedPageById({ - id: prevPageId - }); - - toRender.push({ path: prevPage.path }); - } - - break; - } - } - - // Render homepage/not-found page - if (toRender.length > 0) { - await pageBuilder.prerendering.render({ paths: toRender }); - } - - /** - * TODO: right now, on each update of settings, we trigger a complete site rebuild. - * This is from ideal, and we need to implement better checks if full rerender is necessary. - * Why full site rerender? Settings contain logo, favicon, site title, social stuff, and that's - * used on all pages. - */ + pageBuilder.onSettingsAfterUpdate.subscribe(async () => { + // On every update of settings, we trigger a full website prerendering. + // Might not be ideal for large websites, but it's a simple solution for now. await pageBuilder.prerendering.render({ // This flag is for backwards compatibility with the original custom queue implementation // using the "cron job" type of Lambda worker, executed periodically. queue: true, - // We want to rerender everything, but exclude homepage/not-found page, if they were changed. - paths: [{ path: "*", exclude: toRender.map(task => task.path) }] + paths: [{ path: "*" }] }); }); }); From 2f2dbebb4bbf728ced489516d9a64296e0e27e23 Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Wed, 2 Apr 2025 11:59:54 +0200 Subject: [PATCH 03/12] fix(app-page-builder): add missing render plugin loaders --- .../src/admin/plugins/routes.tsx | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/app-page-builder/src/admin/plugins/routes.tsx b/packages/app-page-builder/src/admin/plugins/routes.tsx index 8cd18ad6eb2..dc3339deb89 100644 --- a/packages/app-page-builder/src/admin/plugins/routes.tsx +++ b/packages/app-page-builder/src/admin/plugins/routes.tsx @@ -90,10 +90,12 @@ const plugins: RoutePlugin[] = [ element={ - - - - + + + + + + } @@ -128,10 +130,12 @@ const plugins: RoutePlugin[] = [ element={ - - - - + + + + + + } @@ -183,10 +187,12 @@ const plugins: RoutePlugin[] = [ element={ - - - - + + + + + + } From 94b0ad04b2253c059776d08dd48e3f8201c19373 Mon Sep 17 00:00:00 2001 From: adrians5j Date: Wed, 9 Apr 2025 06:06:19 +0200 Subject: [PATCH 04/12] wip --- .../Content/Background/Background.tsx | 16 ++---- .../pageEditor/config/TopBar/Title/Styled.ts | 57 ------------------- .../pageEditor/config/TopBar/Title/Title.tsx | 56 ++++++++---------- 3 files changed, 29 insertions(+), 100 deletions(-) delete mode 100644 packages/app-page-builder/src/pageEditor/config/TopBar/Title/Styled.ts diff --git a/packages/app-page-builder/src/editor/defaultConfig/Content/Background/Background.tsx b/packages/app-page-builder/src/editor/defaultConfig/Content/Background/Background.tsx index 65dff05dc85..720514ab392 100644 --- a/packages/app-page-builder/src/editor/defaultConfig/Content/Background/Background.tsx +++ b/packages/app-page-builder/src/editor/defaultConfig/Content/Background/Background.tsx @@ -1,15 +1,6 @@ import React, { useCallback } from "react"; -import { css } from "emotion"; import { useActiveElement } from "~/editor/hooks/useActiveElement"; -const backgroundStyle = css` - position: fixed; - top: 0; - left: 0; - width: 100%; - min-height: 100%; -`; - export const Background = () => { const [activeElement, setActiveElement] = useActiveElement(); @@ -20,5 +11,10 @@ export const Background = () => { setActiveElement(null); }, [activeElement]); - return
; + return ( +
+ ); }; diff --git a/packages/app-page-builder/src/pageEditor/config/TopBar/Title/Styled.ts b/packages/app-page-builder/src/pageEditor/config/TopBar/Title/Styled.ts deleted file mode 100644 index 9cc99b3970c..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/TopBar/Title/Styled.ts +++ /dev/null @@ -1,57 +0,0 @@ -import styled from "@emotion/styled"; -import { css } from "emotion"; - -export const TitleInputWrapper = styled("div")({ - width: "100%", - display: "flex", - alignItems: "center", - position: "relative", - "> .mdc-text-field--upgraded": { - height: 35, - marginTop: "0 !important", - paddingLeft: 10, - paddingRight: 40 - } -}); - -export const TitleWrapper = styled("div")({ - height: 50, - display: "flex", - alignItems: "baseline", - justifyContent: "flex-start", - flexDirection: "column", - color: "var(--mdc-theme-text-primary-on-background)", - position: "relative", - width: "100%", - marginLeft: 10 -}); - -export const PageTitle = styled("div")({ - fontFamily: "var(--mdc-typography-font-family)", - border: "1px solid transparent", - fontSize: 20, - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "ellipsis", - width: "100%", - lineHeight: "120%", - "&:hover": { - border: "1px solid var(--mdc-theme-on-background)" - } -}); - -export const pageTitleWrapper = css({ - maxWidth: "calc(100% - 100px)" -}); - -export const PageVersion = styled("span")({ - fontSize: 20, - color: "var(--mdc-theme-text-secondary-on-background)", - marginLeft: 5, - lineHeight: "120%" -}); - -export const PageMeta = styled("div")({ - height: 20, - margin: "-2px 2px 2px 2px" -}); diff --git a/packages/app-page-builder/src/pageEditor/config/TopBar/Title/Title.tsx b/packages/app-page-builder/src/pageEditor/config/TopBar/Title/Title.tsx index f574e26d516..50d441e164a 100644 --- a/packages/app-page-builder/src/pageEditor/config/TopBar/Title/Title.tsx +++ b/packages/app-page-builder/src/pageEditor/config/TopBar/Title/Title.tsx @@ -1,20 +1,10 @@ import React, { useState, useCallback, SyntheticEvent } from "react"; import { useSnackbar } from "@webiny/app-admin/hooks/useSnackbar"; -import { Input } from "@webiny/ui/Input"; -import { Tooltip } from "@webiny/ui/Tooltip"; -import { Typography } from "@webiny/ui/Typography"; -import { - PageMeta, - PageTitle, - pageTitleWrapper, - PageVersion, - TitleInputWrapper, - TitleWrapper -} from "./Styled"; import { useEventActionHandler } from "~/editor/hooks/useEventActionHandler"; import { usePage } from "~/pageEditor/hooks/usePage"; import { PageAtomType } from "~/pageEditor/state"; import { UpdateDocumentActionEvent } from "~/editor/recoil/actions"; +import { Heading, Input, Tooltip } from "@webiny/admin-ui"; declare global { interface Window { @@ -41,7 +31,7 @@ export const Title = () => { const handler = useEventActionHandler(); const [page] = usePage(); const { showSnackbar } = useSnackbar(); - const { pageTitle, pageVersion, pageLocked } = extractPageInfo(page); + const { pageTitle, pageVersion } = extractPageInfo(page); const [editTitle, setEdit] = useState(false); const [stateTitle, setTitle] = useState(null); let title = stateTitle === null ? pageTitle : stateTitle; @@ -102,35 +92,35 @@ export const Title = () => { const autoFocus = !window.Cypress; return editTitle ? ( - +
- +
) : ( - - - - {`(status: ${pageLocked ? "published" : "draft"})`} - - -
- Rename} - > - +
+ {title} - - - {`(v${pageVersion})`} -
- + + } + /> +
(v{pageVersion})
+
); }; From 78eb71047762e4ca8e5e363853c681cd0d83c9fb Mon Sep 17 00:00:00 2001 From: adrians5j Date: Wed, 9 Apr 2025 06:35:57 +0200 Subject: [PATCH 05/12] wip --- .../DisplayModeSelector.tsx | 115 ++++-------------- .../responsiveMode/icons/laptop_mac.svg | 1 - .../responsiveMode/icons/phone_iphone.svg | 1 - .../responsiveMode/icons/star_rate.svg | 1 - .../responsiveMode/icons/tablet_mac.svg | 1 - .../editor/plugins/responsiveMode/index.tsx | 9 +- 6 files changed, 29 insertions(+), 99 deletions(-) delete mode 100644 packages/app-page-builder/src/editor/plugins/responsiveMode/icons/laptop_mac.svg delete mode 100644 packages/app-page-builder/src/editor/plugins/responsiveMode/icons/phone_iphone.svg delete mode 100644 packages/app-page-builder/src/editor/plugins/responsiveMode/icons/star_rate.svg delete mode 100644 packages/app-page-builder/src/editor/plugins/responsiveMode/icons/tablet_mac.svg diff --git a/packages/app-page-builder/src/editor/defaultConfig/TopBar/DisplayModeSelector/DisplayModeSelector.tsx b/packages/app-page-builder/src/editor/defaultConfig/TopBar/DisplayModeSelector/DisplayModeSelector.tsx index f527179d8cf..d59cacd516c 100644 --- a/packages/app-page-builder/src/editor/defaultConfig/TopBar/DisplayModeSelector/DisplayModeSelector.tsx +++ b/packages/app-page-builder/src/editor/defaultConfig/TopBar/DisplayModeSelector/DisplayModeSelector.tsx @@ -1,85 +1,10 @@ import React, { useMemo } from "react"; -import { css } from "emotion"; import classNames from "classnames"; import { IconButton } from "@webiny/ui/Button"; -import { Tooltip } from "@webiny/ui/Tooltip"; -import { Typography } from "@webiny/ui/Typography"; +import { Heading, Tooltip, Icon } from "@webiny/admin-ui"; import { DisplayMode } from "~/types"; import { useDisplayMode } from "~/editor"; -const classes = { - wrapper: css({ - height: "100%", - display: "flex", - justifyContent: "center", - alignItems: "center", - - "& .action-wrapper": { - height: "100%", - display: "flex", - justifyContent: "center", - alignItems: "center", - borderRight: "1px solid var(--mdc-theme-background)", - "&:first-child": { - borderLeft: "1px solid var(--mdc-theme-background)" - }, - "&.active": { - backgroundColor: "var(--mdc-theme-background)", - "& .mdc-icon-button": { - color: "var(--mdc-theme-text-primary-on-background)" - } - } - } - }), - dimensionIndicator: css({ - height: "100%", - display: "flex", - alignItems: "center", - padding: "0px 16px", - borderRight: "1px solid var(--mdc-theme-background)", - - "& span": { - color: "var(--mdc-theme-text-primary-on-background)" - }, - "& .width": { - marginRight: 8, - "& span:last-child": { - marginLeft: 4 - } - }, - "& .height": { - "& span:last-child": { - marginLeft: 4 - } - } - }), - tooltip: css({ - textAlign: "left", - textTransform: "initial", - "& .tooltip__title": { - "& span": { - fontWeight: 900 - } - }, - "& .tooltip__info": { - display: "flex", - "& span": { - fontWeight: 600 - }, - "& svg": { - fill: "var(--mdc-theme-surface)", - marginRight: 4 - } - }, - "& .tooltip__body": { - marginTop: 4, - "& span": { - fontWeight: 600 - } - } - }) -}; - export const DisplayModeSelector = () => { const { displayMode, displayModes, setDisplayMode } = useDisplayMode(); @@ -88,30 +13,38 @@ export const DisplayModeSelector = () => { return ( setDisplayMode(mode as DisplayMode)} + /> + } content={ -
-
- {tooltip.title} -
-
- {tooltip.subTitleIcon} - {tooltip.subTitle} -
-
- {tooltip.body} +
+ {tooltip.title} +
+ {tooltip.subTitleIcon && ( + + )} + + {tooltip.subTitle}
+ {tooltip.body}
} - placement={"bottom"} + side={"bottom"} className={classNames("action-wrapper", { active: mode === displayMode })} - > - setDisplayMode(mode as DisplayMode)} /> - + /> ); }); }, [setDisplayMode, displayMode]); - return
{responsiveBarContent}
; + return
{responsiveBarContent}
; }; diff --git a/packages/app-page-builder/src/editor/plugins/responsiveMode/icons/laptop_mac.svg b/packages/app-page-builder/src/editor/plugins/responsiveMode/icons/laptop_mac.svg deleted file mode 100644 index 319463bccbc..00000000000 --- a/packages/app-page-builder/src/editor/plugins/responsiveMode/icons/laptop_mac.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/app-page-builder/src/editor/plugins/responsiveMode/icons/phone_iphone.svg b/packages/app-page-builder/src/editor/plugins/responsiveMode/icons/phone_iphone.svg deleted file mode 100644 index 9cc47fb76c4..00000000000 --- a/packages/app-page-builder/src/editor/plugins/responsiveMode/icons/phone_iphone.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/app-page-builder/src/editor/plugins/responsiveMode/icons/star_rate.svg b/packages/app-page-builder/src/editor/plugins/responsiveMode/icons/star_rate.svg deleted file mode 100644 index 45172ed6338..00000000000 --- a/packages/app-page-builder/src/editor/plugins/responsiveMode/icons/star_rate.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/app-page-builder/src/editor/plugins/responsiveMode/icons/tablet_mac.svg b/packages/app-page-builder/src/editor/plugins/responsiveMode/icons/tablet_mac.svg deleted file mode 100644 index 001b8a45343..00000000000 --- a/packages/app-page-builder/src/editor/plugins/responsiveMode/icons/tablet_mac.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/app-page-builder/src/editor/plugins/responsiveMode/index.tsx b/packages/app-page-builder/src/editor/plugins/responsiveMode/index.tsx index eb96f4e8496..c566e626091 100644 --- a/packages/app-page-builder/src/editor/plugins/responsiveMode/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/responsiveMode/index.tsx @@ -1,11 +1,12 @@ import React from "react"; import { css } from "emotion"; import { PbEditorResponsiveModePlugin, DisplayMode } from "~/types"; + // Icons -import { ReactComponent as DesktopIcon } from "./icons/laptop_mac.svg"; -import { ReactComponent as TabletIcon } from "./icons/tablet_mac.svg"; -import { ReactComponent as MobileIcon } from "./icons/phone_iphone.svg"; -import { ReactComponent as StarIcon } from "./icons/star_rate.svg"; +import { ReactComponent as DesktopIcon } from "@webiny/icons/laptop_mac.svg"; +import { ReactComponent as TabletIcon } from "@webiny/icons/tablet_mac.svg"; +import { ReactComponent as MobileIcon } from "@webiny/icons/phone_iphone.svg"; +import { ReactComponent as StarIcon } from "@webiny/icons/star_rate.svg"; const rotateStyle = css({ transform: "rotate(90deg)" From 30eec44eb3ed06ab26a16caa86d1429ed44e7a0a Mon Sep 17 00:00:00 2001 From: adrians5j Date: Wed, 9 Apr 2025 11:21:34 +0200 Subject: [PATCH 06/12] wip --- packages/admin-ui/src/HeaderBar/HeaderBar.tsx | 8 +-- packages/admin-ui/src/theme.scss | 1 + packages/admin-ui/tailwind.config.theme.js | 1 + .../src/editor/components/Editor/Editor.tsx | 1 + .../src/editor/config/Content/Layout.tsx | 25 ++++----- .../src/editor/config/Sidebar/Layout.tsx | 52 ++++++++++--------- .../src/editor/config/Toolbar/Layout.tsx | 25 ++++----- .../src/editor/config/TopBar/Divider.tsx | 12 +---- .../src/editor/config/TopBar/Layout.tsx | 44 +++------------- .../DropdownActions/PageOptionsDropdown.tsx | 12 +---- .../config/Content/BlocksBrowser/AddBlock.tsx | 19 +++---- 11 files changed, 70 insertions(+), 130 deletions(-) diff --git a/packages/admin-ui/src/HeaderBar/HeaderBar.tsx b/packages/admin-ui/src/HeaderBar/HeaderBar.tsx index 9a258887a6f..2477d444133 100644 --- a/packages/admin-ui/src/HeaderBar/HeaderBar.tsx +++ b/packages/admin-ui/src/HeaderBar/HeaderBar.tsx @@ -1,16 +1,16 @@ import React from "react"; -import { makeDecoratable } from "~/utils"; +import { cn, makeDecoratable } from "~/utils"; import { Separator } from "~/Separator"; -interface HeaderBarProps { +interface HeaderBarProps extends React.HTMLAttributes { start?: React.ReactNode; middle?: React.ReactNode; end?: React.ReactNode; } -const HeaderBarBase = ({ start, middle, end }: HeaderBarProps) => { +const HeaderBarBase = ({ start, middle, end, className, ...props }: HeaderBarProps) => { return ( -
+
{ }, []); const classes = { + "wby-bg-neutral-dimmed": true, "pb-editor": true, "pb-editor-dragging": isDragging, "pb-editor-resizing": isResizing diff --git a/packages/app-page-builder/src/editor/config/Content/Layout.tsx b/packages/app-page-builder/src/editor/config/Content/Layout.tsx index 21d25d0725c..1a41b66e42e 100644 --- a/packages/app-page-builder/src/editor/config/Content/Layout.tsx +++ b/packages/app-page-builder/src/editor/config/Content/Layout.tsx @@ -1,26 +1,21 @@ import React from "react"; -import { css } from "emotion"; -import { Elevation } from "@webiny/ui/Elevation"; +import { cn } from "@webiny/admin-ui"; export interface LayoutProps { className?: string; children: React.ReactNode; } -const contentContainerWrapper = css` - margin: 95px 65px 50px 85px; - padding: 0; - position: absolute; - width: calc(100vw - 415px); - top: 0; - box-sizing: border-box; - z-index: 1; -`; - -export const Layout = ({ className = contentContainerWrapper, children }: LayoutProps) => { +export const Layout = ({ className, children }: LayoutProps) => { return ( - +
{children} - +
); }; diff --git a/packages/app-page-builder/src/editor/config/Sidebar/Layout.tsx b/packages/app-page-builder/src/editor/config/Sidebar/Layout.tsx index 4a1f3b68002..f42bf560a05 100644 --- a/packages/app-page-builder/src/editor/config/Sidebar/Layout.tsx +++ b/packages/app-page-builder/src/editor/config/Sidebar/Layout.tsx @@ -1,37 +1,39 @@ import React from "react"; -import { css } from "emotion"; import { makeDecoratable } from "@webiny/app-admin"; -import { Elevation } from "@webiny/ui/Elevation"; import { Tabs } from "@webiny/ui/Tabs"; import { Sidebar } from "./Sidebar"; import { SidebarHighlight } from "./SidebarHighlight"; +import { cn } from "@webiny/admin-ui"; -const rightSideBar = css({ - boxShadow: "1px 0px 5px 0px rgba(128,128,128,1)", - position: "fixed", - right: 0, - top: 65, - height: "100%", - width: 300, - zIndex: 1 -}); +// const rightSideBar = css({ +// boxShadow: "1px 0px 5px 0px rgba(128,128,128,1)", +// position: "fixed", +// right: 0, +// top: 65, +// height: "100%", +// width: 300, +// zIndex: 1 +// }); export interface LayoutProps { className?: string; } -export const Layout = makeDecoratable( - "SidebarLayout", - ({ className = rightSideBar }: LayoutProps) => { - const { activeGroup, setActiveGroup } = Sidebar.useActiveGroup(); +export const Layout = makeDecoratable("SidebarLayout", ({ className }: LayoutProps) => { + const { activeGroup, setActiveGroup } = Sidebar.useActiveGroup(); - return ( - - - - - - - ); - } -); + return ( +
+ + + + +
+ ); +}); diff --git a/packages/app-page-builder/src/editor/config/Toolbar/Layout.tsx b/packages/app-page-builder/src/editor/config/Toolbar/Layout.tsx index 2e5567d82e2..4e7e5e458b3 100644 --- a/packages/app-page-builder/src/editor/config/Toolbar/Layout.tsx +++ b/packages/app-page-builder/src/editor/config/Toolbar/Layout.tsx @@ -12,18 +12,6 @@ const ToolbarDrawerContainer = styled("div")({ zIndex: 2 }); -const ToolbarContainer = styled("div")({ - position: "fixed", - display: "inline-block", - padding: "2px 1px 2px 3px", - width: 50, - top: 64, - height: "calc(100vh - 68px)", - backgroundColor: "var(--mdc-theme-surface)", - boxShadow: "1px 0px 5px 0px var(--mdc-theme-on-background)", - zIndex: 3 -}); - const ToolbarActions = styled("div")({ position: "relative", display: "flex", @@ -44,16 +32,21 @@ export const Layout = () => { ))} - +
-
+
-
+
- +
); }; diff --git a/packages/app-page-builder/src/editor/config/TopBar/Divider.tsx b/packages/app-page-builder/src/editor/config/TopBar/Divider.tsx index 9174fffa7d7..f565d974dff 100644 --- a/packages/app-page-builder/src/editor/config/TopBar/Divider.tsx +++ b/packages/app-page-builder/src/editor/config/TopBar/Divider.tsx @@ -1,15 +1,7 @@ import React from "react"; -import styled from "@emotion/styled"; import { makeDecoratable } from "@webiny/app-admin"; - -const StyledDivider = styled.div` - width: 1px; - margin: 0 5px; - height: 100%; - flex: none; - background-color: var(--mdc-theme-on-background); -`; +import { Separator } from "@webiny/admin-ui"; export const Divider = makeDecoratable("TopBarDivider", () => { - return ; + return ; }); diff --git a/packages/app-page-builder/src/editor/config/TopBar/Layout.tsx b/packages/app-page-builder/src/editor/config/TopBar/Layout.tsx index 5c9ec2e88c2..94a94291f89 100644 --- a/packages/app-page-builder/src/editor/config/TopBar/Layout.tsx +++ b/packages/app-page-builder/src/editor/config/TopBar/Layout.tsx @@ -1,48 +1,20 @@ import React from "react"; -import { css } from "emotion"; import { makeDecoratable } from "@webiny/app-admin"; -import { TopAppBar, TopAppBarSection } from "@webiny/ui/TopAppBar"; +import {cn, HeaderBar} from "@webiny/admin-ui"; import { TopBar } from "./TopBar"; -const topBar = css` - box-shadow: 1px 0px 5px 0px rgba(128, 128, 128, 1); - height: 64px; - display: flex; - > * { - flex: 1; - } -`; - -const centerTopBar = css` - &.mdc-top-app-bar__section { - padding-top: 0; - padding-bottom: 0; - } -`; - -const oneThird = { width: "33%" }; - export interface TopBarLayoutProps { - fixed?: boolean; className?: string; } -export const Layout = makeDecoratable("TopBarLayout", (props: TopBarLayoutProps) => { +export const Layout = makeDecoratable("TopBarLayout", ({className}: TopBarLayoutProps) => { return ( - - - - - - - - - - - + start={} + middle={} + end={} + /> ); }); diff --git a/packages/app-page-builder/src/editor/defaultConfig/TopBar/DropdownActions/PageOptionsDropdown.tsx b/packages/app-page-builder/src/editor/defaultConfig/TopBar/DropdownActions/PageOptionsDropdown.tsx index 41c0ed1a21f..cb8beec5554 100644 --- a/packages/app-page-builder/src/editor/defaultConfig/TopBar/DropdownActions/PageOptionsDropdown.tsx +++ b/packages/app-page-builder/src/editor/defaultConfig/TopBar/DropdownActions/PageOptionsDropdown.tsx @@ -1,18 +1,10 @@ -import React, { Fragment } from "react"; -import { css } from "emotion"; +import React from "react"; import { Menu } from "@webiny/ui/Menu"; import { IconButton } from "@webiny/ui/Button"; import { ReactComponent as MoreVerticalIcon } from "@webiny/icons/more_vert.svg"; import { TopBar } from "~/editor/config/TopBar/TopBar"; import { useEditorConfig } from "~/editor/config"; -const menuStyles = css` - .disabled { - opacity: 0.5; - pointer-events: none; - } -`; - export const PageOptionsDropdown = () => { const { elements } = useEditorConfig(); const dropdownActions = elements.filter( @@ -26,11 +18,9 @@ export const PageOptionsDropdown = () => { return ( } />} > {/* We need to have more than 1 element in the children to force the Menu to render as a regular Menu. */} - ); diff --git a/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/AddBlock.tsx b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/AddBlock.tsx index 90dadd52692..29b741afaa5 100644 --- a/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/AddBlock.tsx +++ b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/AddBlock.tsx @@ -1,23 +1,16 @@ import React from "react"; -import styled from "@emotion/styled"; -import { ButtonFloating } from "@webiny/ui/Button"; +import { Button } from "@webiny/admin-ui"; import { ReactComponent as AddIcon } from "@webiny/icons/add.svg"; import { useBlocksBrowser } from "./useBlocksBrowser"; -const SIDEBAR_WIDTH = 300; -const BottomRight = styled("div")({ - position: "fixed", - zIndex: 101, - bottom: 20, - right: 20 + SIDEBAR_WIDTH -}); - export const AddBlock = () => { const { openBrowser } = useBlocksBrowser(); return ( - - } /> - +