diff --git a/cypress/e2e/blocks/pages/AllBlocksPage.js b/cypress/e2e/blocks/pages/AllBlocksPage.js index eed2961531..05da3a879e 100644 --- a/cypress/e2e/blocks/pages/AllBlocksPage.js +++ b/cypress/e2e/blocks/pages/AllBlocksPage.js @@ -1,10 +1,14 @@ +const TIMEOUT = { + timeout: 15000, +}; + class AllBlocksPage { visit() { cy.visit("/blocks"); } get createBlockButton() { - return cy.getBySelector("create-block-button"); + return cy.getBySelector("create-block-button", TIMEOUT); } get onboardingDialog() { @@ -24,13 +28,13 @@ class AllBlocksPage { } clickOnboardingNextButton() { - this.onboardingNextButton.click(); + this.onboardingNextButton.click(TIMEOUT); } createBlock(name) { - this.createBlockButton.click(); - cy.getBySelector("create-model-display-name-input").type(name); - cy.getBySelector("create-model-submit-button").click(); + this.createBlockButton.click(TIMEOUT); + cy.getBySelector("create-model-display-name-input", TIMEOUT).type(name); + cy.getBySelector("create-model-submit-button", TIMEOUT).click(); } } diff --git a/cypress/e2e/blocks/tests/blocks.spec.js b/cypress/e2e/blocks/tests/blocks.spec.js index f4292a274e..8c4cbff4f4 100644 --- a/cypress/e2e/blocks/tests/blocks.spec.js +++ b/cypress/e2e/blocks/tests/blocks.spec.js @@ -1,18 +1,25 @@ import AllBlocksPage from "../pages/AllBlocksPage"; import BlockPage from "../pages/BlockPage"; import SchemaPage from "../../schema/pages/SchemaPage"; +import { API_ENDPOINTS } from "../../../support/api"; const CypressTestBlock = "Cypress Test Block"; const CypressTestVariant = "Cypress Test Variant"; +const TIMEOUT = { + timeout: 15_000, +}; + describe("All Blocks Tests", () => { before(() => { + deleteTestDataModels(); AllBlocksPage.visit(); }); after(() => { - SchemaPage.visit(); - SchemaPage.deleteModel(CypressTestBlock); + // SchemaPage.visit(); + // SchemaPage.deleteModel(CypressTestBlock); + deleteTestDataModels(); }); it("should show and traverse onboarding flow", () => { @@ -24,9 +31,10 @@ describe("All Blocks Tests", () => { AllBlocksPage.onboardingDialog.should("not.exist"); }); - it("creates new block with default values", () => { + // Skiped since starter blocks are introduced. + it.skip("creates new block with default values", () => { AllBlocksPage.createBlock(CypressTestBlock); - cy.contains(CypressTestBlock).should("exist"); + cy.contains(CypressTestBlock, TIMEOUT).should("exist"); SchemaPage.visit(); SchemaPage.addSingleLineTextFieldWithDefaultValue( CypressTestBlock, @@ -36,6 +44,26 @@ describe("All Blocks Tests", () => { AllBlocksPage.visit(); }); + it("creates new block with default values", () => { + AllBlocksPage.visit(); + cy.get('[data-cy="create_new_content_item"]').click(); + cy.get('[data-cy="starter-block-card"]').first().click(); + cy.get('[data-cy="select-block-type-next-button"]').click(); + cy.get('[data-cy="starter-block-form-label"] input') + .clear() + .type(CypressTestBlock); + cy.intercept("POST", "/v1/content/models").as("createModel"); + cy.get('[data-cy="starter-block-form-submit"]').click(); + cy.wait("@createModel").then((interception) => { + const ZUID = interception?.response?.body?.data?.ZUID; + Cypress.env("model_zuid", ZUID); + + console.debug("ZUID: ", ZUID); + // expect(res.data.label).to.eq(CypressTestBlock); + }); + AllBlocksPage.visit(); + }); + it("searches for a block", () => { AllBlocksPage.searchBlocksInput.type(CypressTestBlock); cy.contains(CypressTestBlock).should("exist"); @@ -50,16 +78,35 @@ describe("All Blocks Tests", () => { }); it("navigates to block detail page", () => { - cy.contains(CypressTestBlock).click(); - cy.contains("Start Creating Variants Now").should("exist"); + AllBlocksPage.visit(); + cy.contains(CypressTestBlock, TIMEOUT).click(); + cy.contains("Start Creating Variants Now", TIMEOUT).should("exist"); }); it("creates a variant with default values", () => { - cy.contains(CypressTestBlock).click(); + AllBlocksPage.visit(); + cy.contains(CypressTestBlock).click(TIMEOUT); BlockPage.createVariant(CypressTestVariant); cy.contains( new RegExp(`${CypressTestBlock}:\\s*${CypressTestVariant}`) ).should("exist"); - cy.get('input[name="foo"]').should("have.value", "Default Foo"); + // cy.get('input[name="foo"]').should("have.value", "Default Foo"); }); }); + +function deleteTestDataModels() { + cy.apiRequest({ + url: `${API_ENDPOINTS.devInstance}/content/models`, + }).then((response) => { + response?.data + ?.filter((resData) => + [CypressTestBlock, CypressTestVariant].includes(resData?.label) + ) + .forEach((forDelete) => { + cy.apiRequest({ + url: `${API_ENDPOINTS.devInstance}/content/models/${forDelete.ZUID}`, + method: "DELETE", + }); + }); + }); +} diff --git a/cypress/e2e/blocks/tests/starter-blocks.spec.js b/cypress/e2e/blocks/tests/starter-blocks.spec.js new file mode 100644 index 0000000000..08aea7203e --- /dev/null +++ b/cypress/e2e/blocks/tests/starter-blocks.spec.js @@ -0,0 +1,465 @@ +import { STARTER_BLOCKS } from "../../../../src/apps/schema/src/app/components/StarterBlocks/configs"; +import { API_ENDPOINTS } from "../../../support/api"; + +const TIMEOUT = { timeout: 60_000 }; +const testSufix = "------TEST"; + +const BLOCK_LABELS = STARTER_BLOCKS.map((block) => block.label); + +const ERRORS = { + label: "Display name is already in use. Please use another display name.", + name: "Reference ID is already in use. Please use another Reference ID.", +}; + +const TEST_DATA = { + existing: { + label: `Starter Block${testSufix}`, + name: `starter_block______test`, + description: "Starter Block Existing Description", + fields: [ + { + contentModelZUID: null, + datatype: "text", + description: "test description", + label: "starter block test label", + name: "starter_block_test_label", + required: false, + settings: { + defaultValue: "starter block test - default value", + list: true, + }, + sort: 1, + }, + ], + }, +}; + +const POP_UPS = { + formLoading: '[data-cy="starter-block-form-loading-backdrop"]', + noResults: '[data-cy="no-results-page"]', +}; + +const starterBlockFormField = `[data-cy="starter-block-form-fields-container"] > div[data-cy-status]`; +const formLabelErrorContainer = '[data-cy="starter-block-form-label-error"]'; +const formNameErrorContainer = '[data-cy="starter-block-form-name-error"]'; +const schemaPageTitleContainer = 'nav[data-cy="breadcrumbs"] + div > h3'; + +function schemaField() { + return cy + .getElement('[data-cy="SEOFields"]') + .parent() + .find("div[data-cy-status]", TIMEOUT); +} + +describe("Starter Blocks", () => { + before(() => { + deleteStarterBlocksTestData(); + createStarterBlocksTestData(); + }); + after(() => { + deleteStarterBlocksTestData(); + }); + + describe("Selection Dialogue", () => { + it("should render starter blocks", () => { + // openStarterBlocksDialogue(); + cy.visit("/schema"); + cy.getElement('[data-cy="create_new_content_item"]').click(TIMEOUT); + cy.getElement('[data-cy="model-type-block"]').click(); + cy.getElement('[data-cy="create-model-next-button"]').click(); + + cy.getElement('[data-cy="starter-blocks-container"]') + .should("be.visible") + .children() + .should("have.length", STARTER_BLOCKS.length); + + STARTER_BLOCKS.forEach((block) => { + cy.contains(block.label).should("exist"); + }); + }); + it("should be able to search starter blocks", () => { + const noResultsSearchTerm = "xxx___xxx___xxx"; + cy.getElement('[data-cy="starter-blocks-search"] input') + .clear() + .type("hero"); + cy.getElement('[data-cy="starter-blocks-container"]') + .children() + .should("have.length", 2); + + cy.getElement('[data-cy="starter-blocks-search"] input') + .clear() + .type(noResultsSearchTerm); + cy.getElement(POP_UPS.noResults).should("exist"); + }); + it("Pressing search again should clear search and focus on the search input", () => { + cy.getElement(POP_UPS.noResults) + .find("button") + .contains("Search Again", { matchCase: false }) + .click(TIMEOUT); + + cy.getElement('[data-cy="starter-blocks-search"] input') + .should("be.empty") + .and("be.focused"); + }); + }); + + describe("Starter Block Form", () => { + beforeEach(() => { + openStarterBlocksDialogue(); + }); + + it("Display an error message when attempting to use a name that's already in use.", () => { + cy.getElement('[data-cy="starter-block-card"]').first().click(TIMEOUT); + cy.getElement('[data-cy="select-block-type-next-button"]').click(); + + cy.getElement('[data-cy="starter-block-form-label"] input') + .clear() + .type(TEST_DATA.existing.label); + cy.getElement('[data-cy="starter-block-form-submit"]').click(); + cy.getElement(POP_UPS.formLoading).should("exist"); + + cy.getElement(formLabelErrorContainer).should("have.text", ERRORS.label); + cy.getElement(formNameErrorContainer).should("have.text", ERRORS.name); + }); + }); + + describe("Create Stater Blocks", () => { + beforeEach(() => { + openStarterBlocksDialogue(); + }); + + it(`[${STARTER_BLOCKS[0].label}]`, () => { + const BLOCK = STARTER_BLOCKS[0]; + + createStarterBlock(BLOCK?.label); + const TEST_LABEL = `${BLOCK?.label}${testSufix}`; + validatePrimaryDetails(BLOCK); + + cy.getElement('[data-cy="starter-block-form-fields-container"]').should( + "not.exist" + ); + + cy.intercept("POST", "/v1/content/models").as("createModel"); + cy.intercept("/v1/content/models").as("getModels"); + + cy.getElement('[data-cy="starter-block-form-label"] input') + .clear() + .type(TEST_LABEL); + cy.getElement('[data-cy="starter-block-form-submit"]').click(); + cy.getElement(POP_UPS.formLoading).should("exist"); + + cy.wait(["@createModel", "@getModels"], TIMEOUT); + + cy.getElement(schemaPageTitleContainer).should( + "contain.text", + TEST_LABEL, + { matchCase: false, ...TIMEOUT } + ); + + cy.getElement("div > button + div + p + span").should("have.length", 0); + }); + + it(`[${STARTER_BLOCKS[1].label}]`, () => { + const BLOCK = STARTER_BLOCKS[1]; + + createStarterBlock(BLOCK?.label); + const TEST_LABEL = `${BLOCK?.label}${testSufix}`; + validatePrimaryDetails(BLOCK); + + BLOCK.fields.forEach((field) => { + cy.getElement(starterBlockFormField).should( + "contain.text", + field.label + ); + }); + + fillOutFormAndSubmit(TEST_LABEL).then((res) => { + cy.getElement(schemaPageTitleContainer).should( + "contain.text", + TEST_LABEL, + { matchCase: false, ...TIMEOUT } + ); + + BLOCK.fields.forEach((field) => { + schemaField().should("contain.text", field.label); + }); + cy.wrap({ ...res }).as("createModelResponse"); + }); + + if (!!BLOCK?.code) { + cy.get("@createModelResponse").then((res) => { + const zuid = res?.webView?.ZUID; + + validateTemplateCode(zuid, BLOCK?.code); + }); + } + }); + + it(`[${STARTER_BLOCKS[2].label}]`, () => { + const BLOCK = STARTER_BLOCKS[2]; + + createStarterBlock(BLOCK?.label); + const TEST_LABEL = `${BLOCK?.label}${testSufix}`; + validatePrimaryDetails(BLOCK); + + BLOCK.fields.forEach((field) => { + cy.getElement(starterBlockFormField).should( + "contain.text", + field.label + ); + }); + + fillOutFormAndSubmit(TEST_LABEL).then((res) => { + cy.getElement(schemaPageTitleContainer).should( + "contain.text", + TEST_LABEL, + { matchCase: false, ...TIMEOUT } + ); + + BLOCK.fields.forEach((field) => { + schemaField().should("contain.text", field.label); + }); + cy.wrap({ ...res }).as("createModelResponse"); + }); + + if (!!BLOCK?.code) { + cy.get("@createModelResponse").then((res) => { + const zuid = res?.webView?.ZUID; + + validateTemplateCode(zuid, BLOCK?.code); + }); + } + }); + + it(`[${STARTER_BLOCKS[3].label}]`, () => { + const BLOCK = STARTER_BLOCKS[3]; + + createStarterBlock(BLOCK?.label); + const TEST_LABEL = `${BLOCK?.label}${testSufix}`; + validatePrimaryDetails(BLOCK); + + BLOCK.fields.forEach((field) => { + cy.getElement(starterBlockFormField).should( + "contain.text", + field.label + ); + }); + + fillOutFormAndSubmit(TEST_LABEL).then((res) => { + cy.getElement(schemaPageTitleContainer).should( + "contain.text", + TEST_LABEL, + { matchCase: false, ...TIMEOUT } + ); + + BLOCK.fields.forEach((field) => { + schemaField().should("contain.text", field.label); + }); + cy.wrap({ ...res }).as("createModelResponse"); + }); + + if (!!BLOCK?.code) { + cy.get("@createModelResponse").then((res) => { + const zuid = res?.webView?.ZUID; + + validateTemplateCode(zuid, BLOCK?.code); + }); + } + }); + + it(`[${STARTER_BLOCKS[4].label}]`, () => { + const BLOCK = STARTER_BLOCKS[4]; + + createStarterBlock(BLOCK?.label); + const TEST_LABEL = `${BLOCK?.label}${testSufix}`; + validatePrimaryDetails(BLOCK); + + BLOCK.fields.forEach((field) => { + cy.getElement(starterBlockFormField).should( + "contain.text", + field.label + ); + }); + + fillOutFormAndSubmit(TEST_LABEL).then((res) => { + cy.getElement(schemaPageTitleContainer).should( + "contain.text", + TEST_LABEL, + { matchCase: false, ...TIMEOUT } + ); + + BLOCK.fields.forEach((field) => { + schemaField().should("contain.text", field.label); + }); + + cy.wrap({ ...res }).as("createModelResponse"); + }); + + if (!!BLOCK?.code) { + cy.get("@createModelResponse").then((res) => { + const zuid = res?.webView?.ZUID; + + validateTemplateCode(zuid, BLOCK?.code); + }); + } + }); + + it(`[${STARTER_BLOCKS[5].label}]`, () => { + const BLOCK = STARTER_BLOCKS[5]; + + createStarterBlock(BLOCK?.label); + + const TEST_LABEL = `${BLOCK?.label}${testSufix}`; + + validatePrimaryDetails(BLOCK); + + BLOCK.fields.forEach((field) => { + cy.getElement(starterBlockFormField).should( + "contain.text", + field.label + ); + }); + + fillOutFormAndSubmit(TEST_LABEL).then((res) => { + cy.getElement(schemaPageTitleContainer).should( + "contain.text", + TEST_LABEL, + { matchCase: false, ...TIMEOUT } + ); + + BLOCK.fields.forEach((field) => { + schemaField().should("contain.text", field.label); + }); + + cy.wrap({ ...res }).as("createModelResponse"); + }); + + if (!!BLOCK?.code) { + cy.get("@createModelResponse").then((res) => { + const zuid = res?.webView?.ZUID; + + validateTemplateCode(zuid, BLOCK?.code); + }); + } + }); + }); +}); + +Cypress.Commands.add("getElement", (selector) => { + return cy.get(selector, TIMEOUT); +}); + +function validatePrimaryDetails(BLOCK) { + cy.location("href").then((path) => { + cy.getElement('[data-cy="starter-block-form-label"] input').should( + "have.value", + BLOCK?.label + ); + cy.getElement('[data-cy="starter-block-form-name"] input').should( + "have.value", + BLOCK?.name + ); + cy.getElement('[data-cy="starter-block-form-description"]').should( + "contain.text", + BLOCK?.description + ); + cy.getElement('[data-cy="starter-block-form-preview-link"]').should( + "have.prop", + "href", + BLOCK?.previewLink === "#" + ? `${path}${BLOCK?.previewLink}` + : BLOCK?.previewLink + ); + cy.getElement('[data-cy="starter-block-form-code-template-link"]').should( + "have.prop", + "href", + BLOCK?.codeTemplateLink === "#" + ? `${path}${BLOCK?.codeTemplateLink}` + : BLOCK?.codeTemplateLink + ); + cy.getElement('[data-cy="starter-block-form-code-reference-link"]').should( + "have.prop", + "href", + BLOCK?.codeReference === "#" + ? `${path}${BLOCK?.codeReference}` + : BLOCK?.codeReference + ); + }); +} + +function fillOutFormAndSubmit(fieldLabel = "") { + cy.intercept("POST", "/v1/content/models").as("createModel"); + cy.intercept("PUT", "/v1/web/views/*").as("updateWebView"); + cy.intercept("/v1/content/models").as("getModels"); + + cy.getElement('[data-cy="starter-block-form-label"] input') + .clear() + .type(fieldLabel); + cy.getElement('[data-cy="starter-block-form-submit"]').click(); + cy.getElement(POP_UPS.formLoading).should("exist"); + + return cy + .wait(["@createModel", "@getModels", "@updateWebView"], TIMEOUT) + .spread((createModel, getModels, updateWebView) => { + return cy.wrap({ + createModel: createModel?.response?.body?.data, + getModels: getModels?.response?.body?.data, + webView: updateWebView?.response?.body?.data, + }); + }); +} + +function validateTemplateCode(ZUID = "", code = "") { + cy.apiRequest({ + method: "GET", + url: `${API_ENDPOINTS.devInstance}/web/views/${ZUID}`, + }).then((response) => { + const viewCode = response?.data?.code; + expect(viewCode).to.equal(code); + }); +} + +function createStarterBlock(name) { + cy.getElement('[data-cy="starter-block-card"]') + .contains(name, { matchCase: false }) + .click(TIMEOUT); + + cy.getElement('[data-cy="select-block-type-next-button"]').click(); +} + +function openStarterBlocksDialogue() { + cy.visit("/blocks", { + onBeforeLoad(win) { + win.localStorage.setItem("zesty:blocks:onboarding", "false"); + }, + }); + cy.getElement('[data-cy="create_new_content_item"]').click(TIMEOUT); +} + +function deleteStarterBlocksTestData() { + cy.apiRequest({ + url: `${API_ENDPOINTS.devInstance}/content/models`, + }).then(({ status, data }) => { + const forDeleteZuids = data + ?.filter((item) => item?.label?.includes(testSufix)) + .map((del) => del?.ZUID); + + forDeleteZuids?.forEach((zuid) => { + cy.deleteModel(zuid); + }); + }); +} + +function createStarterBlocksTestData() { + cy.createModel({ + label: TEST_DATA.existing.label, + name: TEST_DATA.existing.name, + description: TEST_DATA.existing.description, + type: "block", + listed: true, + }).then(({ status, data }) => { + TEST_DATA.existing.fields.forEach((field) => { + cy.createField(data?.ZUID, { ...field }); + }); + }); +} diff --git a/cypress/e2e/content/actions.spec.js b/cypress/e2e/content/actions.spec.js index 1eb761b62e..9acacbb3b5 100644 --- a/cypress/e2e/content/actions.spec.js +++ b/cypress/e2e/content/actions.spec.js @@ -1,5 +1,6 @@ import moment from "moment"; - +import { API_ENDPOINTS } from "../../support/api"; +const TIMEOUT = { timeout: 15_000 }; const yesterdayTimestamp = moment() .hour(0) .minute(0) @@ -8,7 +9,17 @@ const yesterdayTimestamp = moment() .subtract(1, "day") .format("x"); +const SUFFIX = "---TEST"; + +const TEST_DATA = { + newItem: `new_item${SUFFIX}`, +}; + describe("Actions in content editor", () => { + before(() => { + cleanTestData(); + }); + const timestamp = Date.now(); it("Must not save when missing required Field", () => { @@ -16,10 +27,13 @@ describe("Actions in content editor", () => { cy.visit("/content/6-556370-8sh47g/7-82a5c7ffb0-07vj1c"); }); - cy.get("#12-13d590-9v2nr2 input").clear().should("have.value", ""); - cy.get("#SaveItemButton").click(); + cy.get("#12-13d590-9v2nr2 input", TIMEOUT).clear().should("have.value", ""); + cy.get("#SaveItemButton", TIMEOUT).trigger("click"); - cy.get("[data-cy=toast]").contains("Missing Data in Required Fields"); + cy.get("[data-cy=toast]", TIMEOUT).contains( + "Missing Data in Required Fields", + TIMEOUT + ); }); it("Must not save when exceeding or lacking characters", () => { @@ -27,8 +41,8 @@ describe("Actions in content editor", () => { cy.visit("/content/6-a4f5f1beaa-zc5l6v/7-ce9ca8cfb0-cc1mnz"); }); - cy.get("#12-e6a5cfe3f6-k94nbg input").clear().type("aa"); - cy.get("#SaveItemButton").click(); + cy.get("#12-e6a5cfe3f6-k94nbg input", TIMEOUT).clear().type("aa"); + cy.get("#SaveItemButton", TIMEOUT).trigger("click"); cy.getBySelector("FieldErrorsList").should("exist"); cy.getBySelector("FieldErrorsList") .find("ol") @@ -38,16 +52,21 @@ describe("Actions in content editor", () => { cy.get("#12-e6a5cfe3f6-k94nbg input") .clear() .type("Lorem ipsum dolor sit amet, consect"); - cy.get("#SaveItemButton").click(); + cy.get("#SaveItemButton", TIMEOUT).trigger("click"); cy.getBySelector("FieldErrorsList").should("exist"); cy.getBySelector("FieldErrorsList") .find("ol") .find("li") .first() .contains("Exceeding by 5 characters."); - cy.get("#12-e6a5cfe3f6-k94nbg input").clear().type("Lorem ipsum"); - cy.get("#SaveItemButton").click(); - cy.get("[data-cy=toast]").contains("Item Saved: New Schema All Fields"); + cy.get("#12-e6a5cfe3f6-k94nbg input", TIMEOUT) + .clear() + .wait(500) + .type("Lorem ipsum"); + cy.get("#SaveItemButton", TIMEOUT).click(); + cy.get("[data-cy=toast]", TIMEOUT).contains( + "Item Saved: New Schema All Fields" + ); }); it("Must not save when regex is not matched", () => { @@ -55,8 +74,11 @@ describe("Actions in content editor", () => { cy.visit("/content/6-a4f5f1beaa-zc5l6v/7-ce9ca8cfb0-cc1mnz"); }); - cy.get("#12-b6d09d92d0-7911ld textarea").first().clear().type("aa"); - cy.get("#SaveItemButton").click(); + cy.get("#12-b6d09d92d0-7911ld textarea", TIMEOUT) + .first() + .clear() + .type("aa"); + cy.get("#SaveItemButton", TIMEOUT).trigger("click"); cy.getBySelector("FieldErrorsList").should("exist"); cy.getBySelector("FieldErrorsList") .find("ol") @@ -66,9 +88,12 @@ describe("Actions in content editor", () => { cy.get("#12-b6d09d92d0-7911ld textarea") .first() .clear() + .wait(500) .type("hello@zesty.io"); - cy.get("#SaveItemButton").click(); - cy.get("[data-cy=toast]").contains("Item Saved: New Schema All Fields"); + cy.get("#SaveItemButton", TIMEOUT).click(); + cy.get("[data-cy=toast]", TIMEOUT).contains( + "Item Saved: New Schema All Fields" + ); }); /** @@ -83,17 +108,20 @@ describe("Actions in content editor", () => { cy.get("#12-f8efe4e0f5-xj7pj6 input").should("not.exist"); // Make an edit to enable save button - cy.get("#12-849844-t8v5l6 input").clear().type(timestamp); + cy.get("#12-849844-t8v5l6 input", TIMEOUT) + .clear() + .wait(500) + .type(TEST_DATA?.newItem); // save to api cy.waitOn( "/v1/content/models/6-0c960c-d1n0kx/items/7-c882ba84ce-c4smnp", () => { - cy.get("#SaveItemButton").click(); + cy.get("#SaveItemButton").click({ force: true, ...TIMEOUT }); } ); - cy.get("[data-cy=toast]").contains("Item Saved"); + cy.get("[data-cy=toast]", TIMEOUT).contains("Item Saved"); }); it("Saves homepage item metadata", () => { @@ -101,23 +129,24 @@ describe("Actions in content editor", () => { cy.visit("/content/6-a1a600-k0b6f0/7-a1be38-1b42ht/meta"); }); - cy.get("textarea") + cy.get("textarea", TIMEOUT) .first() - .type("{selectall}{backspace}This is an item meta description") + .wait(500) + .type("{selectall}{backspace}This is an item meta description", TIMEOUT) .should("have.value", "This is an item meta description"); cy.waitOn( "/v1/content/models/6-a1a600-k0b6f0/items/7-a1be38-1b42ht", () => { - cy.get("#SaveItemButton").click(); + cy.get("#SaveItemButton", TIMEOUT).trigger("click"); } ); - cy.get("[data-cy=toast]").contains("Item Saved"); + cy.get("[data-cy=toast]", TIMEOUT).contains("Item Saved"); }); it("Publishes an item", () => { - cy.getBySelector("PublishButton").click(); + cy.getBySelector("PublishButton", TIMEOUT).click(); cy.getBySelector("ConfirmPublishModal").should("exist"); cy.getBySelector("ConfirmPublishButton").click(); @@ -129,14 +158,14 @@ describe("Actions in content editor", () => { it("Unpublishes an item", () => { cy.getBySelector("ContentPublishedIndicator").should("exist"); - cy.getBySelector("PublishMenuButton").click(); - cy.getBySelector("UnpublishContentButton").click({ force: true }); + cy.getBySelector("PublishMenuButton", TIMEOUT).click(); + cy.getBySelector("UnpublishContentButton", TIMEOUT).click(); cy.getBySelector("ConfirmUnpublishButton").click(); cy.intercept("GET", "**/publishings").as("publish"); cy.wait("@publish"); - cy.getBySelector("PublishButton").should("exist"); + cy.getBySelector("PublishButton", TIMEOUT).should("exist"); }); it("Schedules a Publish for an item", () => { @@ -144,14 +173,14 @@ describe("Actions in content editor", () => { cy.visit("/content/6-a1a600-k0b6f0/7-a1be38-1b42ht/meta"); }); - cy.getBySelector("PublishMenuButton").click(); - cy.getBySelector("PublishScheduleButton").click({ force: true }); + cy.getBySelector("PublishMenuButton", TIMEOUT).click(); + cy.getBySelector("PublishScheduleButton").click(); cy.getBySelector("SchedulePublishButton").click(); cy.getBySelector("ContentScheduledIndicator").should("exist"); }); it("Unschedules a Publish for an item", () => { - cy.getBySelector("PublishMenuButton").click(); + cy.getBySelector("PublishMenuButton", TIMEOUT).click(); cy.getBySelector("PublishScheduleButton").click(); cy.getBySelector("UnschedulePublishButton").click(); cy.getBySelector("ContentScheduledIndicator").should("not.exist"); @@ -162,7 +191,7 @@ describe("Actions in content editor", () => { cy.visit("/content/6-a1a600-k0b6f0/7-a1be38-1b42ht/meta"); }); - cy.getBySelector("PublishMenuButton").click(); + cy.getBySelector("PublishMenuButton", TIMEOUT).click(); cy.getBySelector("PublishScheduleButton").click(); cy.getBySelector("PublishScheduleModal") .find("[data-cy='datePickerInputField']") @@ -174,7 +203,7 @@ describe("Actions in content editor", () => { cy.get( '.MuiPickersArrowSwitcher-root button[aria-label="Next month"]' ).should("not.be.disabled"); - cy.getBySelector("CancelSchedulePublishButton").click({ force: true }); + cy.getBySelector("CancelSchedulePublishButton").click(); }); it("Fills in default values for a new item", () => { @@ -182,12 +211,14 @@ describe("Actions in content editor", () => { cy.visit("/content/6-a1a600-k0b6f0/new"); }); - cy.get("#12-0c3934-8dz720 input").should( + cy.get("#12-0c3934-8dz720 input", TIMEOUT).should( "have.value", "default single line text field" ); - cy.get("#12-d39a38-85sqdt").contains("zesty-io-logo-horizontal-dark.png"); - cy.get("#12-bcd1dcc5f4-2rpm9p").contains( + cy.get("#12-d39a38-85sqdt", TIMEOUT).contains( + "zesty-io-logo-horizontal-dark.png" + ); + cy.get("#12-bcd1dcc5f4-2rpm9p", TIMEOUT).contains( "5 Tricks to Teach Your Pitbull: Fun & Easy Tips for You & Your Dog!" ); }); @@ -197,7 +228,7 @@ describe("Actions in content editor", () => { cy.visit("/content/6-a1a600-k0b6f0/new"); }); - cy.get("#12-0c3934-8dz720 input").should( + cy.get("#12-0c3934-8dz720 input", TIMEOUT).should( "have.value", "default single line text field" ); @@ -208,23 +239,27 @@ describe("Actions in content editor", () => { }); it("Creates a new item", () => { + cleanTestData(); cy.waitOn("/v1/content/models*", () => { cy.visit("/content/6-a1a600-k0b6f0/new"); }); - cy.get("input[name=title]", { timeout: 5000 }).click().type(timestamp); + cy.get("input[name=title]", TIMEOUT) + .wait(500) + .type(TEST_DATA?.newItem, TIMEOUT); cy.getBySelector("ManualMetaFlow").click(); cy.getBySelector("metaDescription") .find("textarea") .first() - .type(timestamp); - cy.getBySelector("CreateItemSaveButton").click(); + .wait(500) + .type(TEST_DATA?.newItem); + cy.getBySelector("CreateItemSaveButton", TIMEOUT).click(); - cy.contains("Created Item", { timeout: 5000 }).should("exist"); + cy.contains("Created Item", TIMEOUT).should("exist"); }); it("Saved item becomes publishable", () => { - cy.get("#PublishButton").should("exist"); + cy.get("#PublishButton", TIMEOUT).should("exist"); }); it("Displays a new item in the list", () => { @@ -232,12 +267,12 @@ describe("Actions in content editor", () => { cy.visit("/content/6-a1a600-k0b6f0"); }); - cy.contains(timestamp, { timeout: 5000 }).should("exist"); + cy.contains(TEST_DATA?.newItem, { timeout: 50_000 }).should("exist"); }); it("Deletes an item", () => { - cy.contains(timestamp).click(); - cy.getBySelector("ContentItemMoreButton").click(); + cy.contains(TEST_DATA?.newItem, TIMEOUT).click(); + cy.getBySelector("ContentItemMoreButton", TIMEOUT).click(); cy.getBySelector("DeleteContentItem").click(); cy.getBySelector("DeleteContentItemConfirmButton").click(); @@ -245,12 +280,12 @@ describe("Actions in content editor", () => { cy.visit("/content/6-a1a600-k0b6f0"); }); - cy.contains(timestamp).should("not.exist"); + cy.contains(TEST_DATA?.newItem).should("not.exist"); }); // TODO: Workflow request doesn't work it.skip("Makes a workflow request", () => { - cy.get("#MainNavigation").contains("Homepage").click({ force: true }); + cy.get("#MainNavigation", TIMEOUT).contains("Homepage").click(); cy.get("#WorkflowRequestButton").click(); cy.contains("Grant Test").click(); cy.get("#WorkflowRequestSendButton").click(); @@ -319,7 +354,7 @@ describe("Actions in content editor", () => { // Generate AI content for meta description cy.getBySelector("metaDescription") .find("textarea[name='metaDescription']") - .clear({ force: true }); + .clear(); cy.getBySelector("metaDescription").find("[data-cy='AIOpen']").click(); cy.getBySelector("AIGenerate").click(); @@ -333,3 +368,19 @@ describe("Actions in content editor", () => { cy.contains("Created Item", { timeout: 5000 }).should("exist"); }); }); + +function cleanTestData() { + cy.apiRequest({ + url: `${API_ENDPOINTS.devInstance}/content/models/6-a1a600-k0b6f0/items?limit=5000&page=1&lang=en-US`, + }).then((response) => { + const zuids = response?.data + ?.filter((resData) => resData?.data?.title?.includes(SUFFIX)) + ?.map((item) => item?.meta?.ZUID); + + cy.apiRequest({ + url: `${API_ENDPOINTS.devInstance}/content/models/6-a1a600-k0b6f0/items/batch`, + method: "DELETE", + body: JSON.stringify(zuids), + }); + }); +} diff --git a/cypress/e2e/schema/models.spec.js b/cypress/e2e/schema/models.spec.js index 9484ae677e..18749f91cf 100644 --- a/cypress/e2e/schema/models.spec.js +++ b/cypress/e2e/schema/models.spec.js @@ -1,19 +1,27 @@ const SEARCH_TERM = `cypress ${Date.now()}`; const TIMESTAMP = Date.now(); +const TIMEOUT = { + timeout: 15000, +}; + +const BLOCK_MODEL_NAME = "Test Block Model"; describe("Schema: Models", () => { before(() => { + cy.deleteModels([BLOCK_MODEL_NAME]); cy.waitOn("/v1/content/models*", () => { cy.visit("/schema"); }); }); it("Opens creation model with model type selector when triggered from All Models", () => { - cy.getBySelector("create-model-button-all-models").click(); - cy.contains("Select Model Type").should("be.visible"); + cy.getBySelector("create-model-button-all-models").click(TIMEOUT); + cy.contains("Select Model Type", TIMEOUT).should("be.visible"); cy.get("body").type("{esc}"); }); it("Opens creation model with model type pre-selected when triggered from Sidebar", () => { - cy.getBySelector(`create-model-button-sidebar-templateset`).click(); + cy.getBySelector(`create-model-button-sidebar-templateset`).click({ + timeout: 15000, + }); cy.contains("Create Single Page Model").should("be.visible"); cy.get("body").type("{esc}"); cy.getBySelector(`create-model-button-sidebar-pageset`).click(); @@ -24,8 +32,8 @@ describe("Schema: Models", () => { cy.get("body").type("{esc}"); }); it("Creates model", () => { - cy.getBySelector(`create-model-button-all-models`).click(); - cy.contains("Multi Page Model").click(); + cy.getBySelector(`create-model-button-all-models`).click(TIMEOUT); + cy.contains("Multi Page Model").click(TIMEOUT); cy.contains("Next").click(); cy.contains("Display Name").next().type("Cypress Test Model"); cy.contains("Reference ID") @@ -40,7 +48,9 @@ describe("Schema: Models", () => { .type("Cypress test (Group with visible fields in list)"); cy.get(".MuiAutocomplete-popper") - .contains("Cypress test (Group with visible fields in list)") + .contains("Cypress test (Group with visible fields in list)", { + timeout: 50_000, + }) .click(); cy.contains("Description").next().type("Cypress test model description"); @@ -52,7 +62,7 @@ describe("Schema: Models", () => { }); it("Renames model", () => { - cy.getBySelector(`model-header-menu`).click(); + cy.getBySelector(`model-header-menu`).click(TIMEOUT); cy.contains("Rename Model").click(); cy.get(".MuiDialog-container").within(() => { cy.get("label").contains("Display Name").next().type(" Updated"); @@ -64,8 +74,8 @@ describe("Schema: Models", () => { cy.contains("Cypress Test Model Updated").should("exist"); }); it("Deletes model", () => { - cy.getBySelector(`model-header-menu`).click(); - cy.contains("Delete Model").click(); + cy.getBySelector(`model-header-menu`).click(TIMEOUT); + cy.contains("Delete Model").click(TIMEOUT); cy.get(".MuiDialog-container").within(() => { cy.get(".MuiOutlinedInput-root").type("Cypress Test Model Updated"); }); @@ -121,19 +131,20 @@ describe("Schema: Models", () => { .contains("Model parenting itself"); }); - it("Can create a block model", () => { + //Skipped this. This has been tested on the starter block feature. + it.skip("Can create a block model", () => { cy.waitOn("/v1/content/models*", () => { cy.visit("/schema"); }); - cy.getBySelector(`create-model-button-all-models`).click(); + cy.getBySelector(`create-model-button-all-models`).click(TIMEOUT); cy.contains("Block Model").click(); cy.contains("Next").click(); - cy.contains("Display Name").next().type(`Block Test Model ${TIMESTAMP}`); + cy.contains("Display Name").next().type(BLOCK_MODEL_NAME); cy.contains("Reference ID") .next() .find("input") - .should("have.value", `block_test_model_${TIMESTAMP}`); + .should("have.value", BLOCK_MODEL_NAME); cy.contains("Description").next().type("Block test model description"); cy.get(".MuiDialog-container").within(() => { @@ -142,6 +153,6 @@ describe("Schema: Models", () => { cy.intercept("POST", "/models"); cy.intercept("GET", "/models"); - cy.contains(`Block Test Model ${TIMESTAMP}`).should("exist"); + cy.contains(BLOCK_MODEL_NAME, TIMEOUT).should("exist"); }); }); diff --git a/cypress/e2e/schema/pages/SchemaPage.js b/cypress/e2e/schema/pages/SchemaPage.js index 9d6defec5b..b605ded895 100644 --- a/cypress/e2e/schema/pages/SchemaPage.js +++ b/cypress/e2e/schema/pages/SchemaPage.js @@ -1,3 +1,7 @@ +const TIMEOUT = { + timeout: 15_000, +}; + class SchemaPage { visit() { cy.visit("/schema"); @@ -28,16 +32,16 @@ class SchemaPage { } deleteModel(name) { - cy.contains(name).click(); - this.modelHeaderMenuButton.click(); + cy.contains(name).click(TIMEOUT); + this.modelHeaderMenuButton.click(TIMEOUT); this.modelMenuDeleteButton.click(); this.deleteModelConfirmationInput.type(name); this.deleteModelConfirmationButton.click(); } addSingleLineTextFieldWithDefaultValue(modelName, fieldName, defaultValue) { - cy.contains(modelName).click(); - this.addFieldButton.click(); + cy.contains(modelName).click(TIMEOUT); + this.addFieldButton.click(TIMEOUT); cy.contains("Single Line Text").click(); cy.getBySelector("FieldFormInput_label").type(fieldName); this.rulesTabButton.click(); diff --git a/cypress/index.d.ts b/cypress/index.d.ts new file mode 100644 index 0000000000..3711b24aa4 --- /dev/null +++ b/cypress/index.d.ts @@ -0,0 +1,57 @@ +import "./support/commands"; + +declare global { + namespace Cypress { + interface Chainable { + waitOn(path: string, cb: () => void): Chainable; + login(): Chainable; + getBySelector( + selector: string, + ...args: any[] + ): Chainable>; + blockLock(): Chainable; + assertClipboardValue(value: string): Chainable; + blockAnnouncements(): Chainable; + getElement(selector: string): Chainable>; + apiRequest(options: { + url: string; + method?: string; + body?: any; + }): Chainable; + createModel(options: { + description: string; + label: string; + type: string; + name: string; + listed: boolean; + }): Chainable; + createStatusLabel( + name: string, + description: string, + color: string, + addPermissionRoles: string[], + removePermissionRoles: string[], + allowPublish: boolean + ): Chainable; + createField( + zuid: string, + payload: { + contentModelZUID: string; + datatype: string; + description: string; + label: string; + name: string; + required: boolean; + settings: { + defaultValue: string; + list: boolean; + }; + sort: number; + } + ): Chainable; + deleteStatusLabels(labels: string[]): Chainable; + deleteModel(zuid: string): Chainable; + deleteModels(models: string[]): Chainable; + } + } +} diff --git a/cypress/jsconfig.json b/cypress/jsconfig.json new file mode 100644 index 0000000000..6d2e8b5c27 --- /dev/null +++ b/cypress/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "types": ["cypress", "node"], + "jsx": "react-jsx", + "resolveJsonModule": true + } +} diff --git a/cypress/support/api.js b/cypress/support/api.js new file mode 100644 index 0000000000..195db18baf --- /dev/null +++ b/cypress/support/api.js @@ -0,0 +1,97 @@ +import instanceZUID from "../../src/utility/instanceZUID"; +import CONFIG from "../../src/shell/app.config"; + +export const API_ENDPOINTS = { + devInstance: `${ + CONFIG[process.env.NODE_ENV]?.API_INSTANCE_PROTOCOL + }${instanceZUID}${CONFIG[process.env.NODE_ENV]?.API_INSTANCE}`, +}; + +Cypress.Commands.add( + "apiRequest", + ({ method = "GET", url = "", body = undefined }) => { + return cy.getCookie(Cypress.env("COOKIE_NAME")).then((cookie) => { + const token = cookie?.value; + return cy + .request({ + url, + method, + headers: { authorization: `Bearer ${token}` }, + ...(body ? { body: body } : {}), + failOnStatusCode: false, + }) + .then((response) => { + return { + status: response?.isOkStatusCode ? "success" : "error", + data: response?.body?.data, + }; + }); + }); + } +); + +Cypress.Commands.add("deleteModel", (zuid) => { + cy.log(`[CLEAN UP] Content Models`); + return cy.apiRequest({ + url: `${API_ENDPOINTS.devInstance}/content/models/${zuid}`, + method: "DELETE", + }); +}); + +Cypress.Commands.add("deleteModels", (models = []) => { + cy.log(`[CLEAN UP] Content Models`); + cy.apiRequest({ + url: `${API_ENDPOINTS.devInstance}/content/models`, + }).then((response) => { + response?.data + ?.filter((resData) => [...models].includes(resData?.label)) + .forEach((forDelete) => { + cy.apiRequest({ + url: `${API_ENDPOINTS.devInstance}/content/models/${forDelete.ZUID}`, + method: "DELETE", + }); + }); + }); +}); + +Cypress.Commands.add("createModel", (payload) => { + return cy.apiRequest({ + url: `${API_ENDPOINTS.devInstance}/content/models`, + method: "POST", + body: payload, + }); +}); + +Cypress.Commands.add("createField", (zuid, payload) => { + return cy.apiRequest({ + url: `${API_ENDPOINTS.devInstance}/content/models/${zuid}/fields`, + method: "POST", + body: payload, + }); +}); + +Cypress.Commands.add("deleteStatusLabels", (labels = []) => { + cy.log(`[CLEAN UP]: Status Labels`); + cy.apiRequest({ + url: `${API_ENDPOINTS.devInstance}/env/labels?showDeleted=true`, + }).then((response) => { + response?.data + ?.filter( + (resData) => !resData?.deletedAt && [...labels].includes(resData?.name) + ) + .forEach((labelForDelete) => { + cy.apiRequest({ + url: `${API_ENDPOINTS.devInstance}/env/labels/${labelForDelete.ZUID}`, + method: "DELETE", + }); + }); + }); +}); + +Cypress.Commands.add("createStatusLabel", (payload) => { + cy.apiRequest({ + url: `${API_ENDPOINTS?.devInstance}/env/labels`, + method: "POST", + body: payload, + }); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 3ecb8d9bef..3944591ab5 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,3 +1,5 @@ +import "./api"; + Cypress.Commands.add("login", () => { const formBody = new FormData(); diff --git a/public/images/block_contact_us_form.png b/public/images/block_contact_us_form.png new file mode 100644 index 0000000000..42e892e090 Binary files /dev/null and b/public/images/block_contact_us_form.png differ diff --git a/public/images/block_feature_side_by_side_image.png b/public/images/block_feature_side_by_side_image.png new file mode 100644 index 0000000000..de89a4a2e9 Binary files /dev/null and b/public/images/block_feature_side_by_side_image.png differ diff --git a/public/images/block_hero_image_below.png b/public/images/block_hero_image_below.png new file mode 100644 index 0000000000..eb3d18507a Binary files /dev/null and b/public/images/block_hero_image_below.png differ diff --git a/public/images/block_hero_side_by_side_image.png b/public/images/block_hero_side_by_side_image.png new file mode 100644 index 0000000000..44a58cf9b4 Binary files /dev/null and b/public/images/block_hero_side_by_side_image.png differ diff --git a/public/images/block_single_testimonial.png b/public/images/block_single_testimonial.png new file mode 100644 index 0000000000..bfce6008c0 Binary files /dev/null and b/public/images/block_single_testimonial.png differ diff --git a/public/images/sb__avatar__1.png b/public/images/sb__avatar__1.png new file mode 100644 index 0000000000..e39f17013d Binary files /dev/null and b/public/images/sb__avatar__1.png differ diff --git a/public/images/sb__company__logo__1.png b/public/images/sb__company__logo__1.png new file mode 100644 index 0000000000..1a9ea7e7b4 Binary files /dev/null and b/public/images/sb__company__logo__1.png differ diff --git a/public/images/sb__hero__image__1.png b/public/images/sb__hero__image__1.png new file mode 100644 index 0000000000..b2e54d98b7 Binary files /dev/null and b/public/images/sb__hero__image__1.png differ diff --git a/public/images/sb__hero__mage__2.png b/public/images/sb__hero__mage__2.png new file mode 100644 index 0000000000..ae7298a86f Binary files /dev/null and b/public/images/sb__hero__mage__2.png differ diff --git a/public/images/sb__person__image__1.png b/public/images/sb__person__image__1.png new file mode 100644 index 0000000000..2104654fae Binary files /dev/null and b/public/images/sb__person__image__1.png differ diff --git a/src/apps/content-editor/src/app/components/Editor/Field/Field.tsx b/src/apps/content-editor/src/app/components/Editor/Field/Field.tsx index 603952dd67..6f03e8c29a 100644 --- a/src/apps/content-editor/src/app/components/Editor/Field/Field.tsx +++ b/src/apps/content-editor/src/app/components/Editor/Field/Field.tsx @@ -71,7 +71,7 @@ import { import { ResolvedOption } from "./ResolvedOption"; import { LinkOption } from "./LinkOption"; import { FieldTypeMedia } from "../../FieldTypeMedia"; -import { debounce } from "lodash"; +import { debounce, parseInt } from "lodash"; const AIFieldShell = withAI(FieldShell); diff --git a/src/apps/content-editor/src/app/views/ItemList/TableCells/SortCell.tsx b/src/apps/content-editor/src/app/views/ItemList/TableCells/SortCell.tsx index 05ea0041d1..57e8a4038e 100644 --- a/src/apps/content-editor/src/app/views/ItemList/TableCells/SortCell.tsx +++ b/src/apps/content-editor/src/app/views/ItemList/TableCells/SortCell.tsx @@ -1,6 +1,7 @@ import { GridRenderCellParams } from "@mui/x-data-grid-pro"; import { useStagedChanges } from "../StagedChangesContext"; import { FieldTypeSort } from "../../../../../../../shell/components/FieldTypeSort"; +import { parseInt } from "lodash"; export const SortCell = ({ params }: { params: GridRenderCellParams }) => { const { stagedChanges, updateStagedChanges } = useStagedChanges(); diff --git a/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx b/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx index fea9fa7101..264424f3a2 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx @@ -40,6 +40,7 @@ import { FieldTypeColor } from "../../../../../../shell/components/FieldTypeColo import { FieldTypeSort } from "../../../../../../shell/components/FieldTypeSort"; import { RelationalFieldBase } from "../../../../../../shell/components/RelationalFieldBase"; import moment from "moment"; +import { parseInt } from "lodash"; type DefaultValueInputProps = { type: string; diff --git a/src/apps/schema/src/app/components/CreateModelDialogue.tsx b/src/apps/schema/src/app/components/CreateModelDialogue.tsx index e13eea9a6c..03169d705d 100644 --- a/src/apps/schema/src/app/components/CreateModelDialogue.tsx +++ b/src/apps/schema/src/app/components/CreateModelDialogue.tsx @@ -23,7 +23,7 @@ import { useEffect, useReducer, useState } from "react"; import CloseRoundedIcon from "@mui/icons-material/CloseRounded"; import MenuBookRoundedIcon from "@mui/icons-material/MenuBookRounded"; import InfoRoundedIcon from "@mui/icons-material/InfoRounded"; -import { isEmpty, cloneDeep } from "lodash"; +import { isEmpty } from "lodash"; import { theme } from "@zesty-io/material"; import { @@ -41,8 +41,8 @@ import { withCursorPosition } from "../../../../../shell/components/withCursorPo import { formatPathPart } from "../../../../../utility/formatPathPart"; import { AppState } from "../../../../../shell/store/types"; import { SelectModelParentInput } from "./SelectModelParentInput"; -import { SelectBlockGroupInput } from "./SelectBlockGroupInput"; import { isZestyEmail } from "../../../../../utility/isZestyEmail"; +import StarterBlocks from "./StarterBlocks"; interface Props { onClose: () => void; @@ -80,6 +80,8 @@ const modelTypes = [ }, ]; +const largeWidth = ["block"]; + const TextFieldWithCursorPosition = withCursorPosition(TextField); export const CreateModelDialogue = ({ onClose, modelType = "" }: Props) => { @@ -363,104 +365,116 @@ export const CreateModelDialogue = ({ onClose, modelType = "" }: Props) => { ); } else { return ( - - - - - x.key === model.type) - .key as keyof typeof modelIconMap - ] - } - /> - - - Create {modelTypes.find((x) => x.key === model.type).name} - - - {modelTypes.find((x) => x.key === model.type).description} - - - - onClose()}> - - - - - - - - - Display Name - - + {model?.type === "block" ? ( + + ) : ( + + + + + x.key === model.type) + .key as keyof typeof modelIconMap + ] + } /> - - - - updateModel({ label: event.target.value }) - } - fullWidth - autoFocus - data-cy="create-model-display-name-input" - /> - - - - Reference ID - - + + {`Create ${ + modelTypes.find((x) => x.key === model.type).name + }`} + + + { + modelTypes.find((x) => x.key === model.type) + .description + } + + + + onClose()}> + + + + + + + + + Display Name + + + + + + updateModel({ label: event.target.value }) + } + fullWidth + autoFocus + data-cy="create-model-display-name-input" /> - - - - updateModel({ name: event.target.value }) - } - fullWidth - /> - - - updateModel({ - parentZUID: value, - }) - } - tooltip="Selecting a parent affects default routing and content navigation in the UI" - /> - {/* Block grouping will be implemented at a different point */} - {/* {model.type === "block" && ( + + + + Reference ID + + + + + + updateModel({ name: event.target.value }) + } + fullWidth + /> + + + updateModel({ + parentZUID: value, + }) + } + tooltip="Selecting a parent affects default routing and content navigation in the UI" + /> + {/* Block grouping will be implemented at a different point */} + {/* {model.type === "block" && ( { onNewGroupNameChange={() => {}} /> )} */} - - - Description - - + + Description + + + + + + updateModel({ description: event.target.value }) + } + fullWidth + multiline + rows={4} + /> + + + + updateModel({ listed: event.target.checked }) + } /> - - - - updateModel({ description: event.target.value }) + + List this model + + Listed models have their content items available to + programmatic navigation calls. + + + + + + + + - - - - updateModel({ listed: event.target.checked }) + onClick={() => + createModel({ + ...model, + }) } - /> - - List this model - - Listed models have their content items available to - programmatic navigation calls. - - - + > + Create Model + + - - - - - createModel({ - ...model, - }) - } - > - Create Model - - - + )} + ); } }; @@ -559,8 +575,8 @@ export const CreateModelDialogue = ({ onClose, modelType = "" }: Props) => { }} PaperProps={{ sx: { - maxWidth: "640px", - width: 640, + maxWidth: largeWidth?.includes(model?.type) ? "1080px" : "640px", + width: largeWidth?.includes(model?.type) ? 1080 : 640, maxHeight: "min(100%, 1000px)", m: 0, }, diff --git a/src/apps/schema/src/app/components/StarterBlocks/StarterBlockForm.tsx b/src/apps/schema/src/app/components/StarterBlocks/StarterBlockForm.tsx new file mode 100644 index 0000000000..35132f46ad --- /dev/null +++ b/src/apps/schema/src/app/components/StarterBlocks/StarterBlockForm.tsx @@ -0,0 +1,522 @@ +import React, { useCallback, useState } from "react"; +import { useHistory } from "react-router"; +import { + Box, + Button, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + Link, + Stack, + Typography, + Tooltip, + FormControl, + OutlinedInput, + FormHelperText, + CircularProgress, + Backdrop, +} from "@mui/material"; + +import CloseRoundedIcon from "@mui/icons-material/CloseRounded"; +import InfoRoundedIcon from "@mui/icons-material/InfoRounded"; +import OpenInNewRoundedIcon from "@mui/icons-material/OpenInNewRounded"; +import { useCreateStarterBlockModelMutation } from "../../../../../../shell/services/instance"; +import { + ContentModel, + ContentModelField, +} from "../../../../../../shell/services/types"; +import { Field } from "../Field"; +import { OG_IMAGE_FIELD, StarterBlockProps } from "./configs"; + +type TextInputFieldProps = { + label: string; + name: string; + placeholder?: string; + required?: boolean; + error?: string; + value?: string; + toolTip?: string; + onChange?: (val: string) => void; +}; + +type StarterBlockFormProps = { + block: StarterBlockProps; + onClose: () => void; + setActiveStep: (step: "selection" | "form") => void; +}; + +type CreateStarterBlockFieldsProps = Omit< + ContentModelField, + | "ZUID" + | "contentModelZUID" + | "datatypeOptions" + | "createdAt" + | "updatedAt" + | "deletedAt" +>; + +const TextInputField: React.FC = ({ + label = "Label", + name, + placeholder = "Text", + required = false, + error = "", + value, + toolTip, + onChange, + ...other +}) => { + function handleChage(e: React.ChangeEvent) { + const val = e.target.value; + onChange(val); + } + return ( + + + + {label} + {required && ( + + + + )} + + + + {error} + + + + ); +}; + +export const StarterBlockForm: React.FC = ({ + block, + onClose, + setActiveStep, +}) => { + const history = useHistory(); + const [isLoading, setIsLoading] = useState(false); + const [blockModelData, setBlockModelData] = useState({ ...block }); + + const [error, setError] = useState>({ + label: "", + name: "", + }); + + const [createStarterBlock] = useCreateStarterBlockModelMutation(); + + function cleanString(str: string) { + return str.toLowerCase().replace(/\W/g, "_"); + } + function parseErrorMessage(str: string) { + return str.split(":")[2] || str; + } + + const handleBlockLabelChange = (val: string) => { + setBlockModelData((prev: any) => ({ + ...prev, + label: val, + name: cleanString(val), + })); + }; + + const handleBlockNameChange = (val: string) => { + setBlockModelData((prev: any) => ({ + ...prev, + name: cleanString(val), + })); + }; + + const handleFormSubmit = useCallback( + async (e: React.FormEvent) => { + e.preventDefault(); + + if (!blockModelData?.label || !blockModelData?.name) return; + setError({ label: "", name: "" }); + setIsLoading(true); + + try { + const formData = new FormData(e.currentTarget); + const { label, name } = Object.fromEntries(formData) as { + label: string; + name: string; + }; + + const createBlockModelPayload: Partial = { + label, + name, + type: "block", + description: block?.description, + parentZUID: "", + listed: true, + }; + + const fieldsPayload: CreateStarterBlockFieldsProps[] = [ + ...blockModelData?.fields, + OG_IMAGE_FIELD, + ]?.map((field, index) => { + return { + name: field?.name, + label: field?.label, + description: field?.description, + datatype: field?.datatype, + sort: index + 1, + settings: { + ...field?.settings, + list: field?.name === "og_image" ? false : true, + }, + }; + }); + + const createStarterBlockRes: any = await createStarterBlock({ + modelData: createBlockModelPayload, + fields: fieldsPayload, + code: block?.code, + }); + + if (createStarterBlockRes?.error) { + const errorMessage = parseErrorMessage( + createStarterBlockRes?.error?.error + ); + setError({ + name: errorMessage.includes("name") + ? "Reference ID is already in use. Please use another Reference ID." + : errorMessage, + label: + errorMessage.includes("label") || + (errorMessage.includes("name") && cleanString(label) === name) + ? "Display name is already in use. Please use another display name." + : "", + }); + throw new Error(errorMessage); + } + const ZUID = createStarterBlockRes?.data?.model?.ZUID; + + setIsLoading(false); + onClose(); + history.push(`/schema/${ZUID}`); + } catch (err) { + setIsLoading(false); + console.error("Error during form submission:", err); + } + }, + [blockModelData, createStarterBlock, onClose, history] + ); + + return ( + + + + + + {block?.label} + + + onClose()}> + + + + + + + + {block?.label} + + + + + Details + + + + + + {!!blockModelData?.fields?.length && ( + <> + + Fields + + + {block?.fields?.map((field: any, index: number) => ( + + ))} + + + )} + + + + Resources + + + theme.palette.primary.main, + "&:hover": { + textDecorationColor: (theme) => + theme.palette.primary.main, + }, + }} + > + Preview + + + theme.palette.primary.main, + "&:hover": { + textDecorationColor: (theme) => + theme.palette.primary.main, + }, + }} + > + Code Template + + + + + Description + + + + {block?.description} + + + See Bootstrap Template + + + + + + + + + + + + + + + + Creating Model + + + This may take a couple of minutes. + + + + + ); +}; diff --git a/src/apps/schema/src/app/components/StarterBlocks/StarterBlocksSelection.tsx b/src/apps/schema/src/app/components/StarterBlocks/StarterBlocksSelection.tsx new file mode 100644 index 0000000000..16b8435249 --- /dev/null +++ b/src/apps/schema/src/app/components/StarterBlocks/StarterBlocksSelection.tsx @@ -0,0 +1,300 @@ +import { + Box, + Button, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + Link, + Stack, + Typography, + TextField, + InputAdornment, + Grid, + styled, + CardActionArea, + CardMedia, + CardContent, + alpha, + Card, +} from "@mui/material"; +import { useCallback, useEffect, useRef, useState } from "react"; +import CloseRoundedIcon from "@mui/icons-material/CloseRounded"; +import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; +import SearchIcon from "@mui/icons-material/Search"; + +import { NoResults } from "../NoResults"; +import { StarterBlockProps, STARTER_BLOCKS } from "./configs"; + +const CardStyles = styled(Card)(({ theme }) => ({ + borderRadius: theme.spacing(1), + border: "1px solid", + borderColor: theme.palette.grey[100], + boxShadow: "none", + width: "100%", + aspectRatio: "1/.8", + backgroundColor: theme.palette.grey[100], + position: "relative", + "&.active": { + outline: "2px solid", + outlineColor: theme.palette.primary.main, + outlineOffset: "-1px", + "& .MuiCardContent-root": { + backgroundColor: alpha(theme.palette.primary.light, 0.3), + "& .card-content": { + opacity: 0.8, + }, + }, + }, +})); + +const BlockItem = ({ + block, + isActive, + onClick, +}: { + block: StarterBlockProps; + isActive: boolean; + onClick: () => void; +}) => { + return ( + + + + theme.spacing(1), + borderColor: "transparent", + borderStyle: "solid", + overflow: "hidden", + }} + > + + + + {block?.label} + + + + + ); +}; + +export type StarterBlocksSelectionProps = { + onClose: () => void; + setActiveStep: (step: "selection" | "form") => void; + selectBlockType: (blockType: StarterBlockProps) => void; +}; + +export const StarterBlocksSelection: React.FC = ({ + onClose, + setActiveStep, + selectBlockType, +}) => { + const searchRef = useRef(); + const [filteredBlockTypes, setFilteredBlockTypes] = + useState(STARTER_BLOCKS); + const [activeBlock, setActiveBlock] = useState(null); + + const [search, setSearch] = useState(""); + + const handleBlockSelect = (blockType: any) => { + return () => { + selectBlockType(blockType); + setActiveBlock(blockType); + }; + }; + + const handleSearchRetry = () => { + setSearch(""); + searchRef.current?.focus(); + }; + + const handleNext = useCallback(() => { + if (!!activeBlock) { + setActiveStep("form"); + } + }, [activeBlock, setActiveStep]); + + useEffect(() => { + if (!search) return setFilteredBlockTypes(STARTER_BLOCKS); + const filtered = STARTER_BLOCKS.filter((block) => { + const searchString = `${block.label.toLowerCase()}`; + + return searchString.includes(search.toLowerCase()); + }); + setFilteredBlockTypes(filtered); + }, [search, STARTER_BLOCKS]); + return ( + + + + + + Select a Block Type + + + Start with a blank block or select from our selection of pre + designed blocks + + + {" "} + + Learn Blocks basics with a tutorial + + + + onClose()}> + + + + + + + setSearch(event.target.value)} + sx={{ width: { xs: "100%", sm: "100%", md: "60%", lg: "60%" } }} + InputProps={{ + startAdornment: ( + + + + ), + }} + inputRef={searchRef} + /> + + + + {!filteredBlockTypes?.length ? ( + + + + ) : ( + + {filteredBlockTypes?.map((block, index) => ( + + + + ))} + + )} + + + + + + + + ); +}; diff --git a/src/apps/schema/src/app/components/StarterBlocks/configs.ts b/src/apps/schema/src/app/components/StarterBlocks/configs.ts new file mode 100644 index 0000000000..633ff562b6 --- /dev/null +++ b/src/apps/schema/src/app/components/StarterBlocks/configs.ts @@ -0,0 +1,910 @@ +import { + ContentModelFieldDataType, + FieldSettings, +} from "../../../../../../shell/services/types"; + +type StarterBlockFieldProps = { + label: string; + name: string; + description: string; + datatype: ContentModelFieldDataType; + settings: Partial; +}; + +type StarterBlockProps = { + label: string; + image: string; + name: string; + description: string; + previewLink?: string; + codeTemplateLink?: string; + codeReference?: string; + fields?: StarterBlockFieldProps[]; + code?: string | null; +}; + +const side_by_side_hero_image___code = ` + + +
+
+
+
+
+
+

{{this.title}}

+

{{this.sub_title}}

+
+
+ + {{this.button_container_sub_text}} +
+
+
+ {{this.star_rating_title}} + + ★★★★★ + +
+
+
{{this.star_rating_sub_text}}
+
+
+
+
+
+
+ +
+
+
+
+
+`; + +const hero_image_below___code = ` + +
+ +
+
+
+
+
+
+ + +
+
+
+
+`; + +const contact_us_form___code = ` + +
+
+
+
+
+
+
+
+

{{this.title}}

+

+ Book a free consultation call with one of our experts and get + help with your next moves. It's always good to talk to an + expert. It's free! +

+
+
+
    +
  • + + + + Not sure which technology to choose? +
  • +
  • + + + + Need advice on the next steps? +
  • +
  • + + + + Hesitating on how to plan the execution? +
  • +
+
+
+
+
+ Avatar +
+
{{this.person_name}}
+ {{this.person_title}} + {{this.person_email}} +
+
+
+
+
+
+
+
+
+
+
+
+

{{this.form_header_text}}

+
+
+
+ + +
Please enter email.
+
+
+ + +
Please enter email.
+
+
+ + +
Please write message.
+
+
+ +
+
+
+
+
+
+
+
+
+
+`; + +const feature_side_by_side_image___code = ` + +
+
+
+
+
+ + {{this.feature_intro_text}} + +
+

{{this.title}}

+

+ {{this.sub_title}} +

+
+ + + {{this.cta_button_text}} + + + + +
+
+
+
+
+ landing +
+
+
+
+
+
+`; + +const single_testimonial___code = ` + +
+
+
+
+
+
+ textmonial +
+
+
+
+

+ “{{this.quote}}” +

+
+
+
+
{{this.person_name}}
+ {{this.position}} @{{this.company_name}} +
+ {{this.job_title}} +
+
+
+ brand +
+
+
+
+
+
+
+
+`; + +const OG_IMAGE_FIELD: StarterBlockFieldProps = { + name: "og_image", + label: "Meta Image", + description: + "This field allows you to set an open graph image via the SEO tab. An Open Graph (OG) image is an image that appears on a social media post when a web page is shared.", + datatype: "images", + settings: { + defaultValue: null, + group_id: "", + limit: 1, + list: false, + }, +}; + +const STARTER_BLOCKS: StarterBlockProps[] = [ + { + label: "Blank", + image: `${location.origin}/images/blockPlaceholder.png`, + name: "blank", + description: "A blank block", + previewLink: "#", + codeTemplateLink: "#", + codeReference: "#", + fields: [], + code: null, + }, + { + label: "Side by Side Hero Image", + image: `${location.origin}/images/block_hero_side_by_side_image.png`, + name: "side_by_side_hero_image", + description: + "A hero with text and CTA buttons on the left and an image on the right", + previewLink: "#", + codeTemplateLink: "#", + codeReference: "https://block.codescandy.com/blocks/hero-snippet-3.html", + fields: [ + { + name: "title", + label: "Title", + description: "The primary heading displayed in the left section.", + datatype: "text", + settings: { + defaultValue: "Protect What Matters Most with ABC Insurance", + }, + }, + { + label: "Sub Title", + name: "sub_title", + description: + "The supporting text displayed below the title in the left section.", + datatype: "textarea", + settings: { + defaultValue: + "Discover personalized insurance plans designed to secure your future and give you peace of mind.", + }, + }, + { + label: "CTA Button 1 Text", + name: "cta_button_1_text", + description: + "The text for the first call-to-action button in the left section.", + datatype: "text", + settings: { + defaultValue: "Get a Quote", + }, + }, + { + label: "CTA Button 1 URL", + name: "cta_button_1_url", + description: "The URL the first call-to-action button links to.", + datatype: "link", + settings: { + defaultValue: "https://abcinsurance.com/get-a-quote", + }, + }, + { + label: "CTA Button 2 Text", + name: "cta_button_2_text", + description: + "The text for the second call-to-action button in the left section.", + datatype: "text", + settings: { + defaultValue: "Learn More", + }, + }, + { + label: "CTA Button 2 URL", + name: "cta_button_2_url", + description: "The URL the second call-to-action button links to.", + datatype: "link", + settings: { + defaultValue: "https://abcinsurance.com/learn-more", + }, + }, + { + label: "Button Container Sub Text", + name: "button_container_sub_text", + description: + "Supporting text below the call-to-action buttons in the left section.", + datatype: "text", + settings: { + defaultValue: "No hidden fees, no obligations.", + }, + }, + { + label: "Hero Image", + name: "hero_image", + description: + "The image displayed on the right side of the hero section.", + datatype: "images", + settings: { + defaultValue: `${location.origin}/images/sb__hero__image__1.png`, + }, + }, + { + label: "Star Rating Number", + name: "star_rating_number", + description: "A numerical rating displayed in the hero section.", + datatype: "number", + settings: { + defaultValue: "3.5", + }, + }, + { + label: "Star Rating Title", + name: "star_rating_title", + description: "A title describing the star rating.", + datatype: "text", + settings: { + defaultValue: "Customer Satisfaction", + }, + }, + { + label: "Star Rating Sub Text", + name: "star_rating_sub_text", + description: "Additional details about the star rating.", + datatype: "text", + settings: { + defaultValue: "Rated by 5,000+ happy customers.", + }, + }, + ], + code: side_by_side_hero_image___code, + }, + { + label: "Hero Image Below", + name: "hero_image_below", + description: + "A hero with text and CTA buttons on the top and an image below", + previewLink: "#", + codeTemplateLink: "#", + image: `${location.origin}/images/block_hero_image_below.png`, + codeReference: "https://block.codescandy.com/blocks/hero-snippet-2.html", + fields: [ + { + label: "Top CTA Button Text", + name: "top_cta_button_text", + description: "The text for the top call-to-action button.", + datatype: "text", + settings: { + defaultValue: "Get Started", + }, + }, + { + label: "Top CTA Button URL", + name: "top_cta_button_url", + description: "The URL the top call-to-action button links to.", + datatype: "link", + settings: { + defaultValue: "https://abcinsurance.com/get-started", + }, + }, + { + name: "title", + label: "Title", + description: "The primary heading displayed in the hero section.", + datatype: "text", + settings: { + defaultValue: "Secure Your Future with ABC Insurance", + }, + }, + { + label: "Sub Title", + name: "sub_title", + description: + "The supporting text displayed below the title in the hero section.", + datatype: "textarea", + settings: { + defaultValue: "Affordable plans tailored to your needs.", + }, + }, + { + label: "CTA Button 1 Text", + name: "cta_button_1_text", + description: "The text for the first call-to-action button", + datatype: "text", + settings: { + defaultValue: "Get a Quote.", + }, + }, + { + label: "CTA Button 1 URL", + name: "cta_button_1_url", + description: "The URL the first call-to-action button links to.", + datatype: "link", + settings: { + defaultValue: "https://abcinsurance.com/get-a-quote", + }, + }, + { + label: "CTA Button 2 Text", + name: "cta_button_2_text", + description: "The text for the second call-to-action button.", + datatype: "text", + settings: { + defaultValue: "Learn More", + }, + }, + { + label: "CTA Button 2 URL", + name: "cta_button_2_url", + description: "The URL the second call-to-action button links to.", + datatype: "link", + settings: { + defaultValue: "https://abcinsurance.com/learn-more", + }, + }, + { + label: "Hero Image", + name: "hero_image", + description: "The image displayed in the hero section.", + datatype: "images", + settings: { + defaultValue: `${location.origin}/images/sb__hero__image__1.png`, + }, + }, + ], + code: hero_image_below___code, + }, + { + label: "Contact Us Form", + name: "contact_us_form", + description: "A simple contact us form to capture leads from any page", + previewLink: "#", + codeTemplateLink: "#", + image: `${location.origin}/images/block_contact_us_form.png`, + codeReference: "https://block.codescandy.com/contact-2.html", + fields: [ + { + label: "Title", + name: "title", + description: "The main heading for the contact us form.", + datatype: "text", + settings: { + defaultValue: "Get in Touch", + }, + }, + { + label: "Body Text", + name: "body_text", + description: + "A detailed text providing context or instructions for the form.", + datatype: "textarea", + settings: { + defaultValue: + "Have questions? Reach out to us, and we'll get back to you shortly.", + }, + }, + { + label: "Name", + name: "name", + description: "A label for the name input field.", + datatype: "text", + settings: { + defaultValue: "Your Name", + }, + }, + { + label: "Person Name", + name: "person_name", + description: "The name of the primary contact person.", + datatype: "text", + settings: { + defaultValue: "Sarah Johnson", + }, + }, + { + label: "Person Title", + name: "person_title", + description: "The title or role of the primary contact person.", + datatype: "text", + settings: { + defaultValue: "Customer Support Specialist", + }, + }, + { + label: "Person Email", + name: "person_email", + description: "The email address of the primary contact person.", + datatype: "text", + settings: { + defaultValue: "support@abcinsurance.com", + }, + }, + { + label: "Person Photo", + name: "person_photo", + description: "A photo of the primary contact person.", + datatype: "images", + settings: { + defaultValue: `${location.origin}/images/sb__avatar__1.png`, + }, + }, + { + label: "Form Header Text", + name: "form_header_text", + description: "A subheading or introduction for the contact form.", + datatype: "text", + settings: { + defaultValue: "We're here to help!", + }, + }, + { + label: "Form CTA Button Text", + name: "form_cta_button_text", + description: "The text for the call-to-action button on the form.", + datatype: "text", + settings: { + defaultValue: "Submit", + }, + }, + ], + code: contact_us_form___code, + }, + { + label: "Feature Side By Side Image", + name: "feature_side_by_side_image", + description: "A feature with text on the left and image on the right", + image: `${location.origin}/images/block_feature_side_by_side_image.png`, + previewLink: "#", + codeTemplateLink: "#", + codeReference: "https://block.codescandy.com/blocks/features.html", + fields: [ + { + label: "Feature Intro Text", + name: "feature_intro_text", + description: "A short introductory text for the feature section.", + datatype: "text", + settings: { + defaultValue: "Discover our unique insurance features", + }, + }, + { + label: "Feature Intro Link", + name: "feature_intro_link", + description: "The URL linked to the introductory feature text.", + datatype: "link", + settings: { + defaultValue: "https://abcinsurance.com/features", + }, + }, + { + label: "Title", + name: "title", + description: "The main heading for the feature section.", + datatype: "text", + settings: { + defaultValue: "Comprehensive Coverage Plans", + }, + }, + { + label: "Sub Title", + name: "sub_title", + description: + "The supporting text providing more detail about the feature.", + datatype: "textarea", + settings: { + defaultValue: + "Tailored insurance plans to fit your needs and budget, giving you complete peace of mind.", + }, + }, + { + label: "CTA Button Text", + name: "cta_button_text", + description: + "The text for the call-to-action button in the feature section.", + datatype: "text", + settings: { + defaultValue: "Learn About Features", + }, + }, + { + label: "CTA Button URL", + name: "cta_button_url", + description: "The URL the call-to-action button links to.", + datatype: "link", + settings: { + defaultValue: "https://abcinsurance.com/learn-features", + }, + }, + { + label: "Hero Image", + name: "hero_image", + description: "The image displayed alongside the feature section.", + datatype: "images", + settings: { + defaultValue: `${location.origin}/images/sb__hero__image__2.png`, + }, + }, + ], + code: feature_side_by_side_image___code, + }, + { + label: "Single Testimonial", + name: "single_testimonial", + description: + "A single testimonial quote with a person's name, image, and company details", + image: `${location.origin}/images/block_single_testimonial.png`, + previewLink: "#", + codeTemplateLink: "#", + codeReference: "https://block.codescandy.com/blocks/testimonails.html", + fields: [ + { + label: "Quote", + name: "quote", + description: "The testimonial or quote provided by a person.", + datatype: "textarea", + settings: { + defaultValue: + "ABC Insurance has transformed my life with its reliable and affordable plans.", + }, + }, + { + label: "Person Image", + name: "person_image", + description: "The image of the person providing the quote.", + datatype: "images", + settings: { + defaultValue: `${location.origin}/images/sb__person__image__1.png`, + }, + }, + { + label: "Person Name", + name: "person_name", + description: "The name of the person providing the quote.", + datatype: "text", + settings: { + defaultValue: "John Doe", + }, + }, + { + label: "Job Title", + name: "job_title", + description: "The job title of the person providing the quote.", + datatype: "text", + settings: { + defaultValue: "Financial Advisor", + }, + }, + { + label: "Position", + name: "position", + description: "The person's position in their company.", + datatype: "text", + settings: { + defaultValue: "Senior Manager", + }, + }, + { + label: "Company Name", + name: "company_name", + description: "The name of the company the person represents.", + datatype: "text", + settings: { + defaultValue: "ABC Insurance", + }, + }, + { + label: "Company Logo", + name: "company_logo", + description: "The logo of the company the person represents.", + datatype: "images", + settings: { + defaultValue: `${location.origin}/images/sb__company__logo__1.png`, + }, + }, + ], + code: single_testimonial___code, + }, +]; + +export { + StarterBlockFieldProps, + StarterBlockProps, + STARTER_BLOCKS, + OG_IMAGE_FIELD, +}; diff --git a/src/apps/schema/src/app/components/StarterBlocks/index.tsx b/src/apps/schema/src/app/components/StarterBlocks/index.tsx new file mode 100644 index 0000000000..332d7f0857 --- /dev/null +++ b/src/apps/schema/src/app/components/StarterBlocks/index.tsx @@ -0,0 +1,37 @@ +import { useState } from "react"; + +import { StarterBlockForm } from "./StarterBlockForm"; +import { StarterBlocksSelection } from "./StarterBlocksSelection"; + +type StarterBlocksDialogueProps = { + onClose: () => void; +}; + +const StarterBlocksDialogue: React.FC = ({ + onClose, +}) => { + const [blockType, setBlockType] = useState(null); + const [activeStep, setActiveStep] = useState<"selection" | "form">( + "selection" + ); + + return ( + <> + {activeStep === "selection" ? ( + + ) : activeStep === "form" ? ( + + ) : null} + + ); +}; + +export default StarterBlocksDialogue; diff --git a/src/shell/components/FieldTypeDate/index.tsx b/src/shell/components/FieldTypeDate/index.tsx index 75ad182d85..7d7134bace 100644 --- a/src/shell/components/FieldTypeDate/index.tsx +++ b/src/shell/components/FieldTypeDate/index.tsx @@ -17,6 +17,7 @@ import { Typography, Stack, Box, TextField } from "@mui/material"; import format from "date-fns/format"; import CalendarTodayRoundedIcon from "@mui/icons-material/CalendarTodayRounded"; import moment from "moment"; +import { parseInt } from "lodash"; export interface FieldTypeDateProps extends DatePickerProps { name: string; diff --git a/src/shell/components/NavTree/components/NavTreeItem.tsx b/src/shell/components/NavTree/components/NavTreeItem.tsx index a186c96aab..7bed9973a8 100644 --- a/src/shell/components/NavTree/components/NavTreeItem.tsx +++ b/src/shell/components/NavTree/components/NavTreeItem.tsx @@ -125,6 +125,11 @@ export const NavTreeItem: FC = React.memo( // Makes sure that the add new content icon color does not change when tree item is selected color: "common.white", }, + + ".MuiMenu-root .MuiList-root .MuiListItemText-root .MuiTypography-root": + { + color: "common.black", + }, }, "& .MuiCollapse-root.MuiTreeItem-group": { // This makes sure that the whole row is highlighted while still maintaining tree item depth diff --git a/src/shell/services/instance.ts b/src/shell/services/instance.ts index 71379e29d8..c90bdb24b5 100644 --- a/src/shell/services/instance.ts +++ b/src/shell/services/instance.ts @@ -636,6 +636,86 @@ export const instanceApi = createApi({ }), invalidatesTags: ["Groups"], }), + createStarterBlockModel: builder.mutation< + any, + { + modelData: Partial; + fields: Omit< + ContentModelField, + | "ZUID" + | "contentModelZUID" + | "datatypeOptions" + | "createdAt" + | "updatedAt" + | "deletedAt" + >[]; + code: string; + } + >({ + async queryFn( + { modelData, fields, code }, + _queryApi, + _extraOptions, + fetchWithBQ + ) { + try { + const createModelResponse: any = await fetchWithBQ({ + url: `content/models`, + method: "POST", + body: modelData, + }); + + if (!!createModelResponse?.error) + return { + error: { + data: {}, + error: createModelResponse?.error?.data?.error, + }, + }; + + const modelZUID = createModelResponse?.data?.data?.ZUID; + + const contentModelFields = fields.map((field) => ({ + url: `content/models/${modelZUID}/fields`, + method: "POST", + body: { contentModelZUID: modelZUID, ...field }, + })); + const contentModelFieldsResponse: any = await batchApiRequests( + contentModelFields, + fetchWithBQ + ); + + const webViewsResponse: any = await fetchWithBQ({ + url: "web/views", + }); + + const webViewData = webViewsResponse?.data?.data + ?.filter((view: any) => view?.contentModelZUID === modelZUID) + .sort((a: any, b: any) => a?.varsion - b?.version)[0]; + + const updateCodeResponse: any = await fetchWithBQ({ + url: `web/views/${webViewData?.ZUID}`, + method: "PUT", + body: { + ...webViewData, + code: code, + }, + }); + + return { + data: { + model: createModelResponse?.data?.data, + fields: contentModelFieldsResponse, + webView: updateCodeResponse?.data?.data, + }, + }; + } catch (error) { + console.error("Error Creating Starter block: ", error); + return error; + } + }, + invalidatesTags: ["ContentModels", "WebViews", "ContentModelFields"], + }), }), }); @@ -689,4 +769,5 @@ export const { useGetGroupsQuery, useGetGroupByZUIDQuery, useCreateGroupMutation, + useCreateStarterBlockModelMutation, } = instanceApi;