diff --git a/cypress/e2e/content/content.spec.js b/cypress/e2e/content/content.spec.js index 0d629acca7..5762d0c1f6 100644 --- a/cypress/e2e/content/content.spec.js +++ b/cypress/e2e/content/content.spec.js @@ -184,7 +184,7 @@ describe("Content Specs", () => { }); it("Currency Field", () => { - cy.get("#12-b35c68-jd1s8s input[type=number]") + cy.get("#12-b35c68-jd1s8s input") .focus() .clear() .type("100.00") diff --git a/cypress/e2e/schema/field.spec.js b/cypress/e2e/schema/field.spec.js index 4ec52339ce..92625bf5b0 100644 --- a/cypress/e2e/schema/field.spec.js +++ b/cypress/e2e/schema/field.spec.js @@ -17,12 +17,14 @@ const SELECTORS = { FIELD_SELECT_MEDIA: "FieldItem_images", FIELD_SELECT_BOOLEAN: "FieldItem_yes_no", FIELD_SELECT_ONE_TO_ONE: "FieldItem_one_to_one", + FIELD_SELECT_CURRENCY: "FieldItem_currency", MEDIA_CHECKBOX_LIMIT: "MediaCheckbox_limit", MEDIA_CHECKBOX_LOCK: "MediaCheckbox_group_id", DROPDOWN_ADD_OPTION: "DropdownAddOption", DROPDOWN_DELETE_OPTION: "DeleteOption", AUTOCOMPLETE_MODEL_ZUID: "Autocomplete_relatedModelZUID", AUTOCOMPLETE_FIELED_ZUID: "Autocomplete_relatedFieldZUID", + AUTOCOMPLETE_FIELD_CURRENCY: "Autocomplete_currency", INPUT_LABEL: "FieldFormInput_label", INPUT_NAME: "FieldFormInput_name", INPUT_OPTION_LABEL: "OptionLabel", @@ -357,6 +359,44 @@ describe("Schema: Fields", () => { cy.getBySelector(`Field_${fieldName}`).should("exist"); }); + it("Creates a currency field", () => { + cy.intercept("**/fields?showDeleted=true").as("getFields"); + + const fieldLabel = `Currency ${timestamp}`; + const fieldName = `currency_${timestamp}`; + + // Open the add field modal + cy.getBySelector(SELECTORS.ADD_FIELD_BTN).should("exist").click(); + cy.getBySelector(SELECTORS.ADD_FIELD_MODAL).should("exist"); + + // Select one-to-one relationship field + cy.getBySelector(SELECTORS.FIELD_SELECT_CURRENCY).should("exist").click(); + + // Select default currency + cy.getBySelector(SELECTORS.AUTOCOMPLETE_FIELD_CURRENCY).type("phil"); + cy.get("[role=listbox] [role=option]").first().click(); + + // Fill up fields + cy.getBySelector(SELECTORS.INPUT_LABEL).should("exist").type(fieldLabel); + + // Navigate to rules tab and add default value + cy.getBySelector(SELECTORS.RULES_TAB_BTN).click(); + // click on the default value checkbox + cy.getBySelector(SELECTORS.DEFAULT_VALUE_CHECKBOX).click(); + // enter a default value + cy.getBySelector(SELECTORS.DEFAULT_VALUE_INPUT).type("1000.50"); + // Verify default currency + cy.getBySelector(SELECTORS.DEFAULT_VALUE_INPUT).contains("PHP"); + // Click done + cy.getBySelector(SELECTORS.SAVE_FIELD_BUTTON).should("exist").click(); + cy.getBySelector(SELECTORS.ADD_FIELD_MODAL).should("not.exist"); + + cy.wait("@getFields"); + + // Check if field exists + cy.getBySelector(`Field_${fieldName}`).should("exist"); + }); + it("Creates a field via add another field button", () => { cy.intercept("**/fields?showDeleted=true").as("getFields"); diff --git a/package-lock.json b/package-lock.json index 73d29b28d9..4435c8d046 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,6 @@ "requires": true, "packages": { "": { - "name": "manager-ui", "version": "1.0.1", "license": "Commons Clause License Condition v1.0", "dependencies": { @@ -31,7 +30,7 @@ "@tinymce/tinymce-react": "^4.3.0", "@welldone-software/why-did-you-render": "^6.1.1", "@zesty-io/core": "1.10.0", - "@zesty-io/material": "^0.15.2", + "@zesty-io/material": "^0.15.3", "chart.js": "^3.8.0", "chartjs-adapter-moment": "^1.0.1", "chartjs-plugin-datalabels": "^2.0.0", @@ -3912,9 +3911,9 @@ } }, "node_modules/@zesty-io/material": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@zesty-io/material/-/material-0.15.2.tgz", - "integrity": "sha512-m5dLNBpZtPtXUlo57In+k2ldo8OtAUPLvjLATV7S4qC6GKVSZHpq4Sqvab3YZ8C2dskcHGTos4aOJicgI3/UKA==", + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/@zesty-io/material/-/material-0.15.3.tgz", + "integrity": "sha512-zJOagsYexWOq1Bw+L0X1nSKTL+aZkB7MZPCqW53uqAwnKMZ3mejpsOjJwvzoYtm+6oJ6RHQJu0xmNbPXCcjLKA==", "dependencies": { "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", @@ -18397,9 +18396,9 @@ } }, "@zesty-io/material": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@zesty-io/material/-/material-0.15.2.tgz", - "integrity": "sha512-m5dLNBpZtPtXUlo57In+k2ldo8OtAUPLvjLATV7S4qC6GKVSZHpq4Sqvab3YZ8C2dskcHGTos4aOJicgI3/UKA==", + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/@zesty-io/material/-/material-0.15.3.tgz", + "integrity": "sha512-zJOagsYexWOq1Bw+L0X1nSKTL+aZkB7MZPCqW53uqAwnKMZ3mejpsOjJwvzoYtm+6oJ6RHQJu0xmNbPXCcjLKA==", "requires": { "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", diff --git a/package.json b/package.json index 8ec0d7b68e..0a1397272c 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@tinymce/tinymce-react": "^4.3.0", "@welldone-software/why-did-you-render": "^6.1.1", "@zesty-io/core": "1.10.0", - "@zesty-io/material": "^0.15.2", + "@zesty-io/material": "^0.15.3", "chart.js": "^3.8.0", "chartjs-adapter-moment": "^1.0.1", "chartjs-plugin-datalabels": "^2.0.0", diff --git a/public/images/flags/ad.svg b/public/images/flags/ad.svg new file mode 100644 index 0000000000..067ab772f6 --- /dev/null +++ b/public/images/flags/ad.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/ae.svg b/public/images/flags/ae.svg new file mode 100644 index 0000000000..651ac8523d --- /dev/null +++ b/public/images/flags/ae.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/flags/af.svg b/public/images/flags/af.svg new file mode 100644 index 0000000000..521ac4cfd8 --- /dev/null +++ b/public/images/flags/af.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/ag.svg b/public/images/flags/ag.svg new file mode 100644 index 0000000000..243c3d8f9e --- /dev/null +++ b/public/images/flags/ag.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/images/flags/ai.svg b/public/images/flags/ai.svg new file mode 100644 index 0000000000..628ad9be93 --- /dev/null +++ b/public/images/flags/ai.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/al.svg b/public/images/flags/al.svg new file mode 100644 index 0000000000..1135b4b80a --- /dev/null +++ b/public/images/flags/al.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/am.svg b/public/images/flags/am.svg new file mode 100644 index 0000000000..99fa4dc597 --- /dev/null +++ b/public/images/flags/am.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/ao.svg b/public/images/flags/ao.svg new file mode 100644 index 0000000000..b1863bd0f6 --- /dev/null +++ b/public/images/flags/ao.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/aq.svg b/public/images/flags/aq.svg new file mode 100644 index 0000000000..53840cccb0 --- /dev/null +++ b/public/images/flags/aq.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/ar.svg b/public/images/flags/ar.svg new file mode 100644 index 0000000000..d20cbbdcdc --- /dev/null +++ b/public/images/flags/ar.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/arab.svg b/public/images/flags/arab.svg new file mode 100644 index 0000000000..96d27157e9 --- /dev/null +++ b/public/images/flags/arab.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/as.svg b/public/images/flags/as.svg new file mode 100644 index 0000000000..3543556725 --- /dev/null +++ b/public/images/flags/as.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/at.svg b/public/images/flags/at.svg new file mode 100644 index 0000000000..9d2775c083 --- /dev/null +++ b/public/images/flags/at.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/flags/au.svg b/public/images/flags/au.svg new file mode 100644 index 0000000000..96e80768bb --- /dev/null +++ b/public/images/flags/au.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/flags/aw.svg b/public/images/flags/aw.svg new file mode 100644 index 0000000000..413b7c45b6 --- /dev/null +++ b/public/images/flags/aw.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/ax.svg b/public/images/flags/ax.svg new file mode 100644 index 0000000000..0584d713b5 --- /dev/null +++ b/public/images/flags/ax.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/az.svg b/public/images/flags/az.svg new file mode 100644 index 0000000000..3557522110 --- /dev/null +++ b/public/images/flags/az.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/flags/ba.svg b/public/images/flags/ba.svg new file mode 100644 index 0000000000..93bd9cf937 --- /dev/null +++ b/public/images/flags/ba.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/images/flags/bb.svg b/public/images/flags/bb.svg new file mode 100644 index 0000000000..cecd5cc334 --- /dev/null +++ b/public/images/flags/bb.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/flags/bd.svg b/public/images/flags/bd.svg new file mode 100644 index 0000000000..16b794debd --- /dev/null +++ b/public/images/flags/bd.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/flags/be.svg b/public/images/flags/be.svg new file mode 100644 index 0000000000..ac706a0b5a --- /dev/null +++ b/public/images/flags/be.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/bf.svg b/public/images/flags/bf.svg new file mode 100644 index 0000000000..4713822584 --- /dev/null +++ b/public/images/flags/bf.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/bg.svg b/public/images/flags/bg.svg new file mode 100644 index 0000000000..af2d0d07c3 --- /dev/null +++ b/public/images/flags/bg.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/bh.svg b/public/images/flags/bh.svg new file mode 100644 index 0000000000..7a2ea549b6 --- /dev/null +++ b/public/images/flags/bh.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/flags/bi.svg b/public/images/flags/bi.svg new file mode 100644 index 0000000000..a4434a955f --- /dev/null +++ b/public/images/flags/bi.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/flags/bj.svg b/public/images/flags/bj.svg new file mode 100644 index 0000000000..0846724d17 --- /dev/null +++ b/public/images/flags/bj.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/images/flags/bl.svg b/public/images/flags/bl.svg new file mode 100644 index 0000000000..f84cbbaeb1 --- /dev/null +++ b/public/images/flags/bl.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/bm.svg b/public/images/flags/bm.svg new file mode 100644 index 0000000000..bab3e0abe0 --- /dev/null +++ b/public/images/flags/bm.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/bn.svg b/public/images/flags/bn.svg new file mode 100644 index 0000000000..4b416ebb73 --- /dev/null +++ b/public/images/flags/bn.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/bo.svg b/public/images/flags/bo.svg new file mode 100644 index 0000000000..46dc76735e --- /dev/null +++ b/public/images/flags/bo.svg @@ -0,0 +1,674 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/bq.svg b/public/images/flags/bq.svg new file mode 100644 index 0000000000..0e6bc76e62 --- /dev/null +++ b/public/images/flags/bq.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/br.svg b/public/images/flags/br.svg new file mode 100644 index 0000000000..22c908e7e3 --- /dev/null +++ b/public/images/flags/br.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/bs.svg b/public/images/flags/bs.svg new file mode 100644 index 0000000000..5cc918e5ad --- /dev/null +++ b/public/images/flags/bs.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/bt.svg b/public/images/flags/bt.svg new file mode 100644 index 0000000000..798c79b381 --- /dev/null +++ b/public/images/flags/bt.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/bv.svg b/public/images/flags/bv.svg new file mode 100644 index 0000000000..40e16d9482 --- /dev/null +++ b/public/images/flags/bv.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/bw.svg b/public/images/flags/bw.svg new file mode 100644 index 0000000000..3435608d6c --- /dev/null +++ b/public/images/flags/bw.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/by.svg b/public/images/flags/by.svg new file mode 100644 index 0000000000..7e90ff255c --- /dev/null +++ b/public/images/flags/by.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/bz.svg b/public/images/flags/bz.svg new file mode 100644 index 0000000000..25386a51a4 --- /dev/null +++ b/public/images/flags/bz.svg @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/ca.svg b/public/images/flags/ca.svg new file mode 100644 index 0000000000..89da5b7b55 --- /dev/null +++ b/public/images/flags/ca.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/flags/cc.svg b/public/images/flags/cc.svg new file mode 100644 index 0000000000..ddfd180382 --- /dev/null +++ b/public/images/flags/cc.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/cd.svg b/public/images/flags/cd.svg new file mode 100644 index 0000000000..b9cf528941 --- /dev/null +++ b/public/images/flags/cd.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/cefta.svg b/public/images/flags/cefta.svg new file mode 100644 index 0000000000..f748d08a12 --- /dev/null +++ b/public/images/flags/cefta.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/cf.svg b/public/images/flags/cf.svg new file mode 100644 index 0000000000..a6cd3670f2 --- /dev/null +++ b/public/images/flags/cf.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/flags/cg.svg b/public/images/flags/cg.svg new file mode 100644 index 0000000000..f5a0e42d45 --- /dev/null +++ b/public/images/flags/cg.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/images/flags/ch.svg b/public/images/flags/ch.svg new file mode 100644 index 0000000000..b42d6709cf --- /dev/null +++ b/public/images/flags/ch.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/flags/ci.svg b/public/images/flags/ci.svg new file mode 100644 index 0000000000..e400f0c1cd --- /dev/null +++ b/public/images/flags/ci.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/ck.svg b/public/images/flags/ck.svg new file mode 100644 index 0000000000..18e547b17d --- /dev/null +++ b/public/images/flags/ck.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/flags/cl.svg b/public/images/flags/cl.svg new file mode 100644 index 0000000000..5b3c72fa7c --- /dev/null +++ b/public/images/flags/cl.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/cm.svg b/public/images/flags/cm.svg new file mode 100644 index 0000000000..70adc8b681 --- /dev/null +++ b/public/images/flags/cm.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/flags/cn.svg b/public/images/flags/cn.svg new file mode 100644 index 0000000000..10d3489a0e --- /dev/null +++ b/public/images/flags/cn.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/flags/co.svg b/public/images/flags/co.svg new file mode 100644 index 0000000000..ebd0a0fb2d --- /dev/null +++ b/public/images/flags/co.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/cp.svg b/public/images/flags/cp.svg new file mode 100644 index 0000000000..b8aa9cfd69 --- /dev/null +++ b/public/images/flags/cp.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/cr.svg b/public/images/flags/cr.svg new file mode 100644 index 0000000000..5a409eebb2 --- /dev/null +++ b/public/images/flags/cr.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/cu.svg b/public/images/flags/cu.svg new file mode 100644 index 0000000000..053c9ee3a0 --- /dev/null +++ b/public/images/flags/cu.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/cv.svg b/public/images/flags/cv.svg new file mode 100644 index 0000000000..aec8994902 --- /dev/null +++ b/public/images/flags/cv.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/cw.svg b/public/images/flags/cw.svg new file mode 100644 index 0000000000..bb0ece22e4 --- /dev/null +++ b/public/images/flags/cw.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/images/flags/cx.svg b/public/images/flags/cx.svg new file mode 100644 index 0000000000..374ff2dab5 --- /dev/null +++ b/public/images/flags/cx.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/flags/cy.svg b/public/images/flags/cy.svg new file mode 100644 index 0000000000..7e3d883da8 --- /dev/null +++ b/public/images/flags/cy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/flags/cz.svg b/public/images/flags/cz.svg new file mode 100644 index 0000000000..7913de3895 --- /dev/null +++ b/public/images/flags/cz.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/de.svg b/public/images/flags/de.svg new file mode 100644 index 0000000000..71aa2d2c30 --- /dev/null +++ b/public/images/flags/de.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/dg.svg b/public/images/flags/dg.svg new file mode 100644 index 0000000000..f163caf947 --- /dev/null +++ b/public/images/flags/dg.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/dj.svg b/public/images/flags/dj.svg new file mode 100644 index 0000000000..9b00a82056 --- /dev/null +++ b/public/images/flags/dj.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/dk.svg b/public/images/flags/dk.svg new file mode 100644 index 0000000000..563277f81d --- /dev/null +++ b/public/images/flags/dk.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/dm.svg b/public/images/flags/dm.svg new file mode 100644 index 0000000000..f692094ddb --- /dev/null +++ b/public/images/flags/dm.svg @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/do.svg b/public/images/flags/do.svg new file mode 100644 index 0000000000..b1be393ed1 --- /dev/null +++ b/public/images/flags/do.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/dz.svg b/public/images/flags/dz.svg new file mode 100644 index 0000000000..5ff29a74a0 --- /dev/null +++ b/public/images/flags/dz.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/eac.svg b/public/images/flags/eac.svg new file mode 100644 index 0000000000..aaf8133f35 --- /dev/null +++ b/public/images/flags/eac.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/ec.svg b/public/images/flags/ec.svg new file mode 100644 index 0000000000..397bfd9822 --- /dev/null +++ b/public/images/flags/ec.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/ee.svg b/public/images/flags/ee.svg new file mode 100644 index 0000000000..8b98c2c429 --- /dev/null +++ b/public/images/flags/ee.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/eg.svg b/public/images/flags/eg.svg new file mode 100644 index 0000000000..00d1fa59ee --- /dev/null +++ b/public/images/flags/eg.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/eh.svg b/public/images/flags/eh.svg new file mode 100644 index 0000000000..6aec72883c --- /dev/null +++ b/public/images/flags/eh.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/images/flags/er.svg b/public/images/flags/er.svg new file mode 100644 index 0000000000..3f4f3f2921 --- /dev/null +++ b/public/images/flags/er.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/flags/es-ct.svg b/public/images/flags/es-ct.svg new file mode 100644 index 0000000000..4d85911402 --- /dev/null +++ b/public/images/flags/es-ct.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/flags/es-ga.svg b/public/images/flags/es-ga.svg new file mode 100644 index 0000000000..31657813ea --- /dev/null +++ b/public/images/flags/es-ga.svg @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/es-pv.svg b/public/images/flags/es-pv.svg new file mode 100644 index 0000000000..21c8759ec0 --- /dev/null +++ b/public/images/flags/es-pv.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/es.svg b/public/images/flags/es.svg new file mode 100644 index 0000000000..acdf927f23 --- /dev/null +++ b/public/images/flags/es.svg @@ -0,0 +1,544 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/et.svg b/public/images/flags/et.svg new file mode 100644 index 0000000000..3f99be4860 --- /dev/null +++ b/public/images/flags/et.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/images/flags/eu.svg b/public/images/flags/eu.svg new file mode 100644 index 0000000000..b0874c1ed4 --- /dev/null +++ b/public/images/flags/eu.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/fi.svg b/public/images/flags/fi.svg new file mode 100644 index 0000000000..470be2d07c --- /dev/null +++ b/public/images/flags/fi.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/fj.svg b/public/images/flags/fj.svg new file mode 100644 index 0000000000..23fbe57a8d --- /dev/null +++ b/public/images/flags/fj.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/fk.svg b/public/images/flags/fk.svg new file mode 100644 index 0000000000..c65bf96de9 --- /dev/null +++ b/public/images/flags/fk.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/fm.svg b/public/images/flags/fm.svg new file mode 100644 index 0000000000..c1b7c97784 --- /dev/null +++ b/public/images/flags/fm.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/flags/fo.svg b/public/images/flags/fo.svg new file mode 100644 index 0000000000..f802d285ac --- /dev/null +++ b/public/images/flags/fo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/images/flags/fr.svg b/public/images/flags/fr.svg new file mode 100644 index 0000000000..4110e59e4c --- /dev/null +++ b/public/images/flags/fr.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/ga.svg b/public/images/flags/ga.svg new file mode 100644 index 0000000000..76edab429c --- /dev/null +++ b/public/images/flags/ga.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/gb-eng.svg b/public/images/flags/gb-eng.svg new file mode 100644 index 0000000000..12e3b67d56 --- /dev/null +++ b/public/images/flags/gb-eng.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/gb-nir.svg b/public/images/flags/gb-nir.svg new file mode 100644 index 0000000000..e6be8dbc2d --- /dev/null +++ b/public/images/flags/gb-nir.svg @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/gb-sct.svg b/public/images/flags/gb-sct.svg new file mode 100644 index 0000000000..f50cd322ac --- /dev/null +++ b/public/images/flags/gb-sct.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/flags/gb-wls.svg b/public/images/flags/gb-wls.svg new file mode 100644 index 0000000000..6e15fd0158 --- /dev/null +++ b/public/images/flags/gb-wls.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/flags/gb.svg b/public/images/flags/gb.svg new file mode 100644 index 0000000000..799138319d --- /dev/null +++ b/public/images/flags/gb.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/gd.svg b/public/images/flags/gd.svg new file mode 100644 index 0000000000..cb51e9618e --- /dev/null +++ b/public/images/flags/gd.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/ge.svg b/public/images/flags/ge.svg new file mode 100644 index 0000000000..d8126ec8d8 --- /dev/null +++ b/public/images/flags/ge.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/flags/gf.svg b/public/images/flags/gf.svg new file mode 100644 index 0000000000..f8fe94c659 --- /dev/null +++ b/public/images/flags/gf.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/gg.svg b/public/images/flags/gg.svg new file mode 100644 index 0000000000..f8216c8bc1 --- /dev/null +++ b/public/images/flags/gg.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/flags/gh.svg b/public/images/flags/gh.svg new file mode 100644 index 0000000000..5c3e3e69ab --- /dev/null +++ b/public/images/flags/gh.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/flags/gi.svg b/public/images/flags/gi.svg new file mode 100644 index 0000000000..e2b590afef --- /dev/null +++ b/public/images/flags/gi.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/gl.svg b/public/images/flags/gl.svg new file mode 100644 index 0000000000..eb5a52e9e4 --- /dev/null +++ b/public/images/flags/gl.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/flags/gm.svg b/public/images/flags/gm.svg new file mode 100644 index 0000000000..8fe9d66920 --- /dev/null +++ b/public/images/flags/gm.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/images/flags/gn.svg b/public/images/flags/gn.svg new file mode 100644 index 0000000000..40d6ad4f03 --- /dev/null +++ b/public/images/flags/gn.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/gp.svg b/public/images/flags/gp.svg new file mode 100644 index 0000000000..ee55c4bcd3 --- /dev/null +++ b/public/images/flags/gp.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/gq.svg b/public/images/flags/gq.svg new file mode 100644 index 0000000000..134e442173 --- /dev/null +++ b/public/images/flags/gq.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/gr.svg b/public/images/flags/gr.svg new file mode 100644 index 0000000000..599741eec8 --- /dev/null +++ b/public/images/flags/gr.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/images/flags/gs.svg b/public/images/flags/gs.svg new file mode 100644 index 0000000000..1536e073ec --- /dev/null +++ b/public/images/flags/gs.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/gt.svg b/public/images/flags/gt.svg new file mode 100644 index 0000000000..f7cffbdc7a --- /dev/null +++ b/public/images/flags/gt.svg @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/gu.svg b/public/images/flags/gu.svg new file mode 100644 index 0000000000..0d66e1bfa8 --- /dev/null +++ b/public/images/flags/gu.svg @@ -0,0 +1,23 @@ + + + + + + + + + + G + U + A + M + + + + + + + + + + diff --git a/public/images/flags/gw.svg b/public/images/flags/gw.svg new file mode 100644 index 0000000000..d470bac9f7 --- /dev/null +++ b/public/images/flags/gw.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/gy.svg b/public/images/flags/gy.svg new file mode 100644 index 0000000000..569fb56275 --- /dev/null +++ b/public/images/flags/gy.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/flags/hk.svg b/public/images/flags/hk.svg new file mode 100644 index 0000000000..4fd55bc14b --- /dev/null +++ b/public/images/flags/hk.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/flags/hm.svg b/public/images/flags/hm.svg new file mode 100644 index 0000000000..815c482085 --- /dev/null +++ b/public/images/flags/hm.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/flags/hn.svg b/public/images/flags/hn.svg new file mode 100644 index 0000000000..11fde67db9 --- /dev/null +++ b/public/images/flags/hn.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/hr.svg b/public/images/flags/hr.svg new file mode 100644 index 0000000000..44fed27d54 --- /dev/null +++ b/public/images/flags/hr.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/ht.svg b/public/images/flags/ht.svg new file mode 100644 index 0000000000..5d48eb93b2 --- /dev/null +++ b/public/images/flags/ht.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/hu.svg b/public/images/flags/hu.svg new file mode 100644 index 0000000000..baddf7f5ea --- /dev/null +++ b/public/images/flags/hu.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/ic.svg b/public/images/flags/ic.svg new file mode 100644 index 0000000000..81e6ee2e13 --- /dev/null +++ b/public/images/flags/ic.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/id.svg b/public/images/flags/id.svg new file mode 100644 index 0000000000..3b7c8fcfd9 --- /dev/null +++ b/public/images/flags/id.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/flags/ie.svg b/public/images/flags/ie.svg new file mode 100644 index 0000000000..049be14de1 --- /dev/null +++ b/public/images/flags/ie.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/il.svg b/public/images/flags/il.svg new file mode 100644 index 0000000000..f43be7e8ed --- /dev/null +++ b/public/images/flags/il.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/images/flags/im.svg b/public/images/flags/im.svg new file mode 100644 index 0000000000..f06f3d6fe1 --- /dev/null +++ b/public/images/flags/im.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/in.svg b/public/images/flags/in.svg new file mode 100644 index 0000000000..bc47d74911 --- /dev/null +++ b/public/images/flags/in.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/io.svg b/public/images/flags/io.svg new file mode 100644 index 0000000000..77016679ef --- /dev/null +++ b/public/images/flags/io.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/iq.svg b/public/images/flags/iq.svg new file mode 100644 index 0000000000..259da9adc5 --- /dev/null +++ b/public/images/flags/iq.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/images/flags/ir.svg b/public/images/flags/ir.svg new file mode 100644 index 0000000000..8c6d516216 --- /dev/null +++ b/public/images/flags/ir.svg @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/is.svg b/public/images/flags/is.svg new file mode 100644 index 0000000000..a6588afaef --- /dev/null +++ b/public/images/flags/is.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/images/flags/it.svg b/public/images/flags/it.svg new file mode 100644 index 0000000000..20a8bfdcc8 --- /dev/null +++ b/public/images/flags/it.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/je.svg b/public/images/flags/je.svg new file mode 100644 index 0000000000..611180d42a --- /dev/null +++ b/public/images/flags/je.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/jm.svg b/public/images/flags/jm.svg new file mode 100644 index 0000000000..269df03836 --- /dev/null +++ b/public/images/flags/jm.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/flags/jo.svg b/public/images/flags/jo.svg new file mode 100644 index 0000000000..d6f927d44f --- /dev/null +++ b/public/images/flags/jo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/images/flags/jp.svg b/public/images/flags/jp.svg new file mode 100644 index 0000000000..cc1c181ce9 --- /dev/null +++ b/public/images/flags/jp.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/flags/ke.svg b/public/images/flags/ke.svg new file mode 100644 index 0000000000..3a67ca3ccd --- /dev/null +++ b/public/images/flags/ke.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/kg.svg b/public/images/flags/kg.svg new file mode 100644 index 0000000000..68c210b1cf --- /dev/null +++ b/public/images/flags/kg.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/flags/kh.svg b/public/images/flags/kh.svg new file mode 100644 index 0000000000..c658838f4e --- /dev/null +++ b/public/images/flags/kh.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/ki.svg b/public/images/flags/ki.svg new file mode 100644 index 0000000000..0c80328071 --- /dev/null +++ b/public/images/flags/ki.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/km.svg b/public/images/flags/km.svg new file mode 100644 index 0000000000..414d65e47f --- /dev/null +++ b/public/images/flags/km.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/images/flags/kn.svg b/public/images/flags/kn.svg new file mode 100644 index 0000000000..47fe64d617 --- /dev/null +++ b/public/images/flags/kn.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/images/flags/kp.svg b/public/images/flags/kp.svg new file mode 100644 index 0000000000..4d1dbab246 --- /dev/null +++ b/public/images/flags/kp.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/flags/kr.svg b/public/images/flags/kr.svg new file mode 100644 index 0000000000..6947eab2b3 --- /dev/null +++ b/public/images/flags/kr.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/kw.svg b/public/images/flags/kw.svg new file mode 100644 index 0000000000..3dd89e9962 --- /dev/null +++ b/public/images/flags/kw.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/ky.svg b/public/images/flags/ky.svg new file mode 100644 index 0000000000..74a2fea2a1 --- /dev/null +++ b/public/images/flags/ky.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/kz.svg b/public/images/flags/kz.svg new file mode 100644 index 0000000000..04a47f53e8 --- /dev/null +++ b/public/images/flags/kz.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/la.svg b/public/images/flags/la.svg new file mode 100644 index 0000000000..6aea6b72b4 --- /dev/null +++ b/public/images/flags/la.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/images/flags/lb.svg b/public/images/flags/lb.svg new file mode 100644 index 0000000000..8619f2410e --- /dev/null +++ b/public/images/flags/lb.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/flags/lc.svg b/public/images/flags/lc.svg new file mode 100644 index 0000000000..bb256541c6 --- /dev/null +++ b/public/images/flags/lc.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/flags/li.svg b/public/images/flags/li.svg new file mode 100644 index 0000000000..68ea26fa30 --- /dev/null +++ b/public/images/flags/li.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/lk.svg b/public/images/flags/lk.svg new file mode 100644 index 0000000000..2c5cdbe09d --- /dev/null +++ b/public/images/flags/lk.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/lr.svg b/public/images/flags/lr.svg new file mode 100644 index 0000000000..e482ab9d74 --- /dev/null +++ b/public/images/flags/lr.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/images/flags/ls.svg b/public/images/flags/ls.svg new file mode 100644 index 0000000000..a7c01a98ff --- /dev/null +++ b/public/images/flags/ls.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/flags/lt.svg b/public/images/flags/lt.svg new file mode 100644 index 0000000000..90ec5d240e --- /dev/null +++ b/public/images/flags/lt.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/lu.svg b/public/images/flags/lu.svg new file mode 100644 index 0000000000..cc12206812 --- /dev/null +++ b/public/images/flags/lu.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/lv.svg b/public/images/flags/lv.svg new file mode 100644 index 0000000000..6a9e75ec97 --- /dev/null +++ b/public/images/flags/lv.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/flags/ly.svg b/public/images/flags/ly.svg new file mode 100644 index 0000000000..1eaa51e468 --- /dev/null +++ b/public/images/flags/ly.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/ma.svg b/public/images/flags/ma.svg new file mode 100644 index 0000000000..7ce56eff70 --- /dev/null +++ b/public/images/flags/ma.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/flags/mc.svg b/public/images/flags/mc.svg new file mode 100644 index 0000000000..9cb6c9e8a0 --- /dev/null +++ b/public/images/flags/mc.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/flags/md.svg b/public/images/flags/md.svg new file mode 100644 index 0000000000..6dc441e177 --- /dev/null +++ b/public/images/flags/md.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/me.svg b/public/images/flags/me.svg new file mode 100644 index 0000000000..d891890746 --- /dev/null +++ b/public/images/flags/me.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/mf.svg b/public/images/flags/mf.svg new file mode 100644 index 0000000000..6305edc1c2 --- /dev/null +++ b/public/images/flags/mf.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/mg.svg b/public/images/flags/mg.svg new file mode 100644 index 0000000000..5fa2d2440d --- /dev/null +++ b/public/images/flags/mg.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/mh.svg b/public/images/flags/mh.svg new file mode 100644 index 0000000000..7b9f490755 --- /dev/null +++ b/public/images/flags/mh.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/mk.svg b/public/images/flags/mk.svg new file mode 100644 index 0000000000..4f5cae77ed --- /dev/null +++ b/public/images/flags/mk.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/ml.svg b/public/images/flags/ml.svg new file mode 100644 index 0000000000..6f6b71695c --- /dev/null +++ b/public/images/flags/ml.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/mm.svg b/public/images/flags/mm.svg new file mode 100644 index 0000000000..42b4dee2b8 --- /dev/null +++ b/public/images/flags/mm.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/images/flags/mn.svg b/public/images/flags/mn.svg new file mode 100644 index 0000000000..152c2fcb0f --- /dev/null +++ b/public/images/flags/mn.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/images/flags/mo.svg b/public/images/flags/mo.svg new file mode 100644 index 0000000000..d39985d05f --- /dev/null +++ b/public/images/flags/mo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/flags/mp.svg b/public/images/flags/mp.svg new file mode 100644 index 0000000000..ff59ebf87b --- /dev/null +++ b/public/images/flags/mp.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/mq.svg b/public/images/flags/mq.svg new file mode 100644 index 0000000000..b221951e36 --- /dev/null +++ b/public/images/flags/mq.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/mr.svg b/public/images/flags/mr.svg new file mode 100644 index 0000000000..7558234cbf --- /dev/null +++ b/public/images/flags/mr.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/flags/ms.svg b/public/images/flags/ms.svg new file mode 100644 index 0000000000..faf07b07fd --- /dev/null +++ b/public/images/flags/ms.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/mt.svg b/public/images/flags/mt.svg new file mode 100644 index 0000000000..c597266c36 --- /dev/null +++ b/public/images/flags/mt.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/mu.svg b/public/images/flags/mu.svg new file mode 100644 index 0000000000..82d7a3bec5 --- /dev/null +++ b/public/images/flags/mu.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/flags/mv.svg b/public/images/flags/mv.svg new file mode 100644 index 0000000000..10450f9845 --- /dev/null +++ b/public/images/flags/mv.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/flags/mw.svg b/public/images/flags/mw.svg new file mode 100644 index 0000000000..d83ddb2178 --- /dev/null +++ b/public/images/flags/mw.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/images/flags/mx.svg b/public/images/flags/mx.svg new file mode 100644 index 0000000000..f98a89e173 --- /dev/null +++ b/public/images/flags/mx.svg @@ -0,0 +1,382 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/my.svg b/public/images/flags/my.svg new file mode 100644 index 0000000000..89576f69ea --- /dev/null +++ b/public/images/flags/my.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/mz.svg b/public/images/flags/mz.svg new file mode 100644 index 0000000000..2ee6ec14b4 --- /dev/null +++ b/public/images/flags/mz.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/na.svg b/public/images/flags/na.svg new file mode 100644 index 0000000000..35b9f783e1 --- /dev/null +++ b/public/images/flags/na.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/images/flags/nc.svg b/public/images/flags/nc.svg new file mode 100644 index 0000000000..068f0c69aa --- /dev/null +++ b/public/images/flags/nc.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/ne.svg b/public/images/flags/ne.svg new file mode 100644 index 0000000000..39a82b8277 --- /dev/null +++ b/public/images/flags/ne.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/flags/nf.svg b/public/images/flags/nf.svg new file mode 100644 index 0000000000..c8b30938d7 --- /dev/null +++ b/public/images/flags/nf.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/flags/ng.svg b/public/images/flags/ng.svg new file mode 100644 index 0000000000..81eb35f78e --- /dev/null +++ b/public/images/flags/ng.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/flags/ni.svg b/public/images/flags/ni.svg new file mode 100644 index 0000000000..6dcdc9a806 --- /dev/null +++ b/public/images/flags/ni.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/nl.svg b/public/images/flags/nl.svg new file mode 100644 index 0000000000..e90f5b0351 --- /dev/null +++ b/public/images/flags/nl.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/no.svg b/public/images/flags/no.svg new file mode 100644 index 0000000000..a5f2a152a9 --- /dev/null +++ b/public/images/flags/no.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/np.svg b/public/images/flags/np.svg new file mode 100644 index 0000000000..8d71d106bb --- /dev/null +++ b/public/images/flags/np.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/nr.svg b/public/images/flags/nr.svg new file mode 100644 index 0000000000..ff394c4112 --- /dev/null +++ b/public/images/flags/nr.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/images/flags/nu.svg b/public/images/flags/nu.svg new file mode 100644 index 0000000000..4067bafff0 --- /dev/null +++ b/public/images/flags/nu.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/images/flags/nz.svg b/public/images/flags/nz.svg new file mode 100644 index 0000000000..935d8a749d --- /dev/null +++ b/public/images/flags/nz.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/om.svg b/public/images/flags/om.svg new file mode 100644 index 0000000000..c003f86e46 --- /dev/null +++ b/public/images/flags/om.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/pa.svg b/public/images/flags/pa.svg new file mode 100644 index 0000000000..8dc03bc61b --- /dev/null +++ b/public/images/flags/pa.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/images/flags/pc.svg b/public/images/flags/pc.svg new file mode 100644 index 0000000000..882197da67 --- /dev/null +++ b/public/images/flags/pc.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/pe.svg b/public/images/flags/pe.svg new file mode 100644 index 0000000000..33e6cfd417 --- /dev/null +++ b/public/images/flags/pe.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/flags/pf.svg b/public/images/flags/pf.svg new file mode 100644 index 0000000000..e06b236e82 --- /dev/null +++ b/public/images/flags/pf.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/pg.svg b/public/images/flags/pg.svg new file mode 100644 index 0000000000..237cb6eeed --- /dev/null +++ b/public/images/flags/pg.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/flags/ph.svg b/public/images/flags/ph.svg new file mode 100644 index 0000000000..65489e1cb2 --- /dev/null +++ b/public/images/flags/ph.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/flags/pk.svg b/public/images/flags/pk.svg new file mode 100644 index 0000000000..491e58ab16 --- /dev/null +++ b/public/images/flags/pk.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/flags/pl.svg b/public/images/flags/pl.svg new file mode 100644 index 0000000000..0fa5145241 --- /dev/null +++ b/public/images/flags/pl.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/flags/pm.svg b/public/images/flags/pm.svg new file mode 100644 index 0000000000..19a9330a31 --- /dev/null +++ b/public/images/flags/pm.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/pn.svg b/public/images/flags/pn.svg new file mode 100644 index 0000000000..07958aca12 --- /dev/null +++ b/public/images/flags/pn.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/pr.svg b/public/images/flags/pr.svg new file mode 100644 index 0000000000..ec51831dcd --- /dev/null +++ b/public/images/flags/pr.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/ps.svg b/public/images/flags/ps.svg new file mode 100644 index 0000000000..b33824a5dd --- /dev/null +++ b/public/images/flags/ps.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/flags/pt.svg b/public/images/flags/pt.svg new file mode 100644 index 0000000000..445cf7f536 --- /dev/null +++ b/public/images/flags/pt.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/pw.svg b/public/images/flags/pw.svg new file mode 100644 index 0000000000..9f89c5f148 --- /dev/null +++ b/public/images/flags/pw.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/flags/py.svg b/public/images/flags/py.svg new file mode 100644 index 0000000000..38e2051eb2 --- /dev/null +++ b/public/images/flags/py.svg @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/qa.svg b/public/images/flags/qa.svg new file mode 100644 index 0000000000..901f3fa761 --- /dev/null +++ b/public/images/flags/qa.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/flags/re.svg b/public/images/flags/re.svg new file mode 100644 index 0000000000..64e788e011 --- /dev/null +++ b/public/images/flags/re.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/ro.svg b/public/images/flags/ro.svg new file mode 100644 index 0000000000..fda0f7bec9 --- /dev/null +++ b/public/images/flags/ro.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/rs.svg b/public/images/flags/rs.svg new file mode 100644 index 0000000000..2f971025b8 --- /dev/null +++ b/public/images/flags/rs.svg @@ -0,0 +1,292 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/ru.svg b/public/images/flags/ru.svg new file mode 100644 index 0000000000..cf243011ae --- /dev/null +++ b/public/images/flags/ru.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/rw.svg b/public/images/flags/rw.svg new file mode 100644 index 0000000000..06e26ae44e --- /dev/null +++ b/public/images/flags/rw.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/sa.svg b/public/images/flags/sa.svg new file mode 100644 index 0000000000..c0a148663b --- /dev/null +++ b/public/images/flags/sa.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/sb.svg b/public/images/flags/sb.svg new file mode 100644 index 0000000000..6066f94cd1 --- /dev/null +++ b/public/images/flags/sb.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/sc.svg b/public/images/flags/sc.svg new file mode 100644 index 0000000000..9a46b369b3 --- /dev/null +++ b/public/images/flags/sc.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/sd.svg b/public/images/flags/sd.svg new file mode 100644 index 0000000000..12818b4110 --- /dev/null +++ b/public/images/flags/sd.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/se.svg b/public/images/flags/se.svg new file mode 100644 index 0000000000..8ba745acaf --- /dev/null +++ b/public/images/flags/se.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/flags/sg.svg b/public/images/flags/sg.svg new file mode 100644 index 0000000000..c4dd4ac9eb --- /dev/null +++ b/public/images/flags/sg.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/sh-ac.svg b/public/images/flags/sh-ac.svg new file mode 100644 index 0000000000..22b365832e --- /dev/null +++ b/public/images/flags/sh-ac.svg @@ -0,0 +1,689 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/sh-hl.svg b/public/images/flags/sh-hl.svg new file mode 100644 index 0000000000..b92e703f27 --- /dev/null +++ b/public/images/flags/sh-hl.svg @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/sh-ta.svg b/public/images/flags/sh-ta.svg new file mode 100644 index 0000000000..a103aac05f --- /dev/null +++ b/public/images/flags/sh-ta.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/sh.svg b/public/images/flags/sh.svg new file mode 100644 index 0000000000..7aba0aec8a --- /dev/null +++ b/public/images/flags/sh.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/si.svg b/public/images/flags/si.svg new file mode 100644 index 0000000000..66a390dcd2 --- /dev/null +++ b/public/images/flags/si.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/sj.svg b/public/images/flags/sj.svg new file mode 100644 index 0000000000..bb2799ce73 --- /dev/null +++ b/public/images/flags/sj.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/sk.svg b/public/images/flags/sk.svg new file mode 100644 index 0000000000..81476940eb --- /dev/null +++ b/public/images/flags/sk.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/flags/sl.svg b/public/images/flags/sl.svg new file mode 100644 index 0000000000..a07baf75b4 --- /dev/null +++ b/public/images/flags/sl.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/sm.svg b/public/images/flags/sm.svg new file mode 100644 index 0000000000..00e9286c44 --- /dev/null +++ b/public/images/flags/sm.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/sn.svg b/public/images/flags/sn.svg new file mode 100644 index 0000000000..7c0673d6d6 --- /dev/null +++ b/public/images/flags/sn.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/flags/so.svg b/public/images/flags/so.svg new file mode 100644 index 0000000000..a581ac63cf --- /dev/null +++ b/public/images/flags/so.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/flags/sr.svg b/public/images/flags/sr.svg new file mode 100644 index 0000000000..5e71c40026 --- /dev/null +++ b/public/images/flags/sr.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/flags/ss.svg b/public/images/flags/ss.svg new file mode 100644 index 0000000000..b257aa0b3e --- /dev/null +++ b/public/images/flags/ss.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/flags/st.svg b/public/images/flags/st.svg new file mode 100644 index 0000000000..1294bcb70e --- /dev/null +++ b/public/images/flags/st.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/images/flags/sv.svg b/public/images/flags/sv.svg new file mode 100644 index 0000000000..c811e912f0 --- /dev/null +++ b/public/images/flags/sv.svg @@ -0,0 +1,594 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/sx.svg b/public/images/flags/sx.svg new file mode 100644 index 0000000000..18f7a1397b --- /dev/null +++ b/public/images/flags/sx.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/sy.svg b/public/images/flags/sy.svg new file mode 100644 index 0000000000..5225550525 --- /dev/null +++ b/public/images/flags/sy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/flags/sz.svg b/public/images/flags/sz.svg new file mode 100644 index 0000000000..294a2cc1a8 --- /dev/null +++ b/public/images/flags/sz.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/tc.svg b/public/images/flags/tc.svg new file mode 100644 index 0000000000..63f13c359b --- /dev/null +++ b/public/images/flags/tc.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/td.svg b/public/images/flags/td.svg new file mode 100644 index 0000000000..fa3bd927c1 --- /dev/null +++ b/public/images/flags/td.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/tf.svg b/public/images/flags/tf.svg new file mode 100644 index 0000000000..fba233563f --- /dev/null +++ b/public/images/flags/tf.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/images/flags/tg.svg b/public/images/flags/tg.svg new file mode 100644 index 0000000000..c63a6d1a94 --- /dev/null +++ b/public/images/flags/tg.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/images/flags/th.svg b/public/images/flags/th.svg new file mode 100644 index 0000000000..1e93a61e95 --- /dev/null +++ b/public/images/flags/th.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/tj.svg b/public/images/flags/tj.svg new file mode 100644 index 0000000000..9fba246cde --- /dev/null +++ b/public/images/flags/tj.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/tk.svg b/public/images/flags/tk.svg new file mode 100644 index 0000000000..05d3e86ce6 --- /dev/null +++ b/public/images/flags/tk.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/tl.svg b/public/images/flags/tl.svg new file mode 100644 index 0000000000..3d0701a2c8 --- /dev/null +++ b/public/images/flags/tl.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/tm.svg b/public/images/flags/tm.svg new file mode 100644 index 0000000000..8b656cc2b8 --- /dev/null +++ b/public/images/flags/tm.svg @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/tn.svg b/public/images/flags/tn.svg new file mode 100644 index 0000000000..5735c1984d --- /dev/null +++ b/public/images/flags/tn.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/flags/to.svg b/public/images/flags/to.svg new file mode 100644 index 0000000000..d072337066 --- /dev/null +++ b/public/images/flags/to.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/images/flags/tr.svg b/public/images/flags/tr.svg new file mode 100644 index 0000000000..b96da21f0e --- /dev/null +++ b/public/images/flags/tr.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/flags/tt.svg b/public/images/flags/tt.svg new file mode 100644 index 0000000000..bc24938cf8 --- /dev/null +++ b/public/images/flags/tt.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/tv.svg b/public/images/flags/tv.svg new file mode 100644 index 0000000000..675210ec55 --- /dev/null +++ b/public/images/flags/tv.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/flags/tw.svg b/public/images/flags/tw.svg new file mode 100644 index 0000000000..57fd98b433 --- /dev/null +++ b/public/images/flags/tw.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/tz.svg b/public/images/flags/tz.svg new file mode 100644 index 0000000000..a2cfbca42a --- /dev/null +++ b/public/images/flags/tz.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/flags/ua.svg b/public/images/flags/ua.svg new file mode 100644 index 0000000000..a339eb1b9c --- /dev/null +++ b/public/images/flags/ua.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/flags/ug.svg b/public/images/flags/ug.svg new file mode 100644 index 0000000000..737eb2ce1a --- /dev/null +++ b/public/images/flags/ug.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/um.svg b/public/images/flags/um.svg new file mode 100644 index 0000000000..9e9eddaa4a --- /dev/null +++ b/public/images/flags/um.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/flags/un.svg b/public/images/flags/un.svg new file mode 100644 index 0000000000..e57793bc79 --- /dev/null +++ b/public/images/flags/un.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/images/flags/us.svg b/public/images/flags/us.svg new file mode 100644 index 0000000000..9cfd0c927f --- /dev/null +++ b/public/images/flags/us.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/flags/uy.svg b/public/images/flags/uy.svg new file mode 100644 index 0000000000..62c36f8e5e --- /dev/null +++ b/public/images/flags/uy.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/uz.svg b/public/images/flags/uz.svg new file mode 100644 index 0000000000..0ccca1b1b4 --- /dev/null +++ b/public/images/flags/uz.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/va.svg b/public/images/flags/va.svg new file mode 100644 index 0000000000..87e0fbbdcc --- /dev/null +++ b/public/images/flags/va.svg @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/vc.svg b/public/images/flags/vc.svg new file mode 100644 index 0000000000..f26c2d8da9 --- /dev/null +++ b/public/images/flags/vc.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/flags/ve.svg b/public/images/flags/ve.svg new file mode 100644 index 0000000000..314e7f5f7f --- /dev/null +++ b/public/images/flags/ve.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/vg.svg b/public/images/flags/vg.svg new file mode 100644 index 0000000000..0ee90fb28c --- /dev/null +++ b/public/images/flags/vg.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/vi.svg b/public/images/flags/vi.svg new file mode 100644 index 0000000000..4270257799 --- /dev/null +++ b/public/images/flags/vi.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/vn.svg b/public/images/flags/vn.svg new file mode 100644 index 0000000000..7e4bac8f4a --- /dev/null +++ b/public/images/flags/vn.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/flags/vu.svg b/public/images/flags/vu.svg new file mode 100644 index 0000000000..91e1236a0a --- /dev/null +++ b/public/images/flags/vu.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/wf.svg b/public/images/flags/wf.svg new file mode 100644 index 0000000000..054c57df99 --- /dev/null +++ b/public/images/flags/wf.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/ws.svg b/public/images/flags/ws.svg new file mode 100644 index 0000000000..0e758a7a95 --- /dev/null +++ b/public/images/flags/ws.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/xk.svg b/public/images/flags/xk.svg new file mode 100644 index 0000000000..551e7a4145 --- /dev/null +++ b/public/images/flags/xk.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/xx.svg b/public/images/flags/xx.svg new file mode 100644 index 0000000000..9333be3635 --- /dev/null +++ b/public/images/flags/xx.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/flags/ye.svg b/public/images/flags/ye.svg new file mode 100644 index 0000000000..1c9e6d6392 --- /dev/null +++ b/public/images/flags/ye.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/flags/yt.svg b/public/images/flags/yt.svg new file mode 100644 index 0000000000..e7776b3078 --- /dev/null +++ b/public/images/flags/yt.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/flags/za.svg b/public/images/flags/za.svg new file mode 100644 index 0000000000..d563adb90c --- /dev/null +++ b/public/images/flags/za.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/zm.svg b/public/images/flags/zm.svg new file mode 100644 index 0000000000..13239f5e23 --- /dev/null +++ b/public/images/flags/zm.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/flags/zw.svg b/public/images/flags/zw.svg new file mode 100644 index 0000000000..6399ab4ab3 --- /dev/null +++ b/public/images/flags/zw.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/apps/code-editor/src/app/components/FileActions/components/EditorActions/EditorActions.js b/src/apps/code-editor/src/app/components/FileActions/components/EditorActions/EditorActions.js index d2f6d33212..ec1caf87bb 100644 --- a/src/apps/code-editor/src/app/components/FileActions/components/EditorActions/EditorActions.js +++ b/src/apps/code-editor/src/app/components/FileActions/components/EditorActions/EditorActions.js @@ -4,7 +4,9 @@ import { Save } from "./Save"; import { Publish } from "./Publish"; import styles from "./EditorActions.less"; +import { usePermission } from "../../../../../../../../shell/hooks/use-permissions"; export const EditorActions = memo(function EditorActions(props) { + const canPublish = usePermission("PUBLISH"); return (
- + {canPublish && ( + + )}
); }); diff --git a/src/apps/code-editor/src/app/components/FileList/FileList.js b/src/apps/code-editor/src/app/components/FileList/FileList.js index f7c85c180c..823999426a 100644 --- a/src/apps/code-editor/src/app/components/FileList/FileList.js +++ b/src/apps/code-editor/src/app/components/FileList/FileList.js @@ -17,9 +17,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCloudUploadAlt } from "@fortawesome/free-solid-svg-icons"; import { resolvePathPart, publishFile } from "../../../store/files"; import { collapseNavItem } from "../../../store/navCode"; - +import { usePermission } from "../../../../../../shell/hooks/use-permissions"; import styles from "./FileList.less"; export const FileList = memo(function FileList(props) { + const canPublish = usePermission("PUBLISH"); // const [branch, setBranch] = useState(props.branch); const [shownFiles, setShownFiles] = useState( props.navCode.tree.sort(byLabel) @@ -47,14 +48,20 @@ export const FileList = memo(function FileList(props) { }; const actions = [ - !file.isLive} - onClick={(file) => props.dispatch(publishFile(file.ZUID, file.status))} - />, + ...(canPublish + ? [ + !file.isLive} + onClick={(file) => + props.dispatch(publishFile(file.ZUID, file.status)) + } + />, + ] + : []), ]; return ( diff --git a/src/apps/content-editor/src/app/ContentEditor.js b/src/apps/content-editor/src/app/ContentEditor.js index 24a5e88ca3..3d22f11e63 100644 --- a/src/apps/content-editor/src/app/ContentEditor.js +++ b/src/apps/content-editor/src/app/ContentEditor.js @@ -31,6 +31,7 @@ import Analytics from "./views/Analytics"; import { ResizableContainer } from "../../../../shell/components/ResizeableContainer"; import { StagedChangesProvider } from "./views/ItemList/StagedChangesContext"; import { SelectedItemsProvider } from "./views/ItemList/SelectedItemsContext"; +import { TableSortProvider } from "./views/ItemList/TableSortProvider"; // Makes sure that other apps using legacy theme does not get affected with the palette let customTheme = createTheme(legacyTheme, { @@ -174,7 +175,9 @@ export default function ContentEditor() { render={() => ( - + + + )} diff --git a/src/apps/content-editor/src/app/components/APIEndpoints.tsx b/src/apps/content-editor/src/app/components/APIEndpoints.tsx new file mode 100644 index 0000000000..52b43eff1d --- /dev/null +++ b/src/apps/content-editor/src/app/components/APIEndpoints.tsx @@ -0,0 +1,89 @@ +import { useSelector } from "react-redux"; +import { useParams } from "react-router"; +import { + MenuList, + MenuItem, + ListItemIcon, + Typography, + Chip, +} from "@mui/material"; +import { DesignServicesRounded, VisibilityRounded } from "@mui/icons-material"; + +import { AppState } from "../../../../../shell/store/types"; +import { ContentItem } from "../../../../../shell/services/types"; +import { useGetDomainsQuery } from "../../../../../shell/services/accounts"; +import { ApiType } from "../../../../schema/src/app/components/ModelApi"; + +type APIEndpointsProps = { + type: Extract; +}; +export const APIEndpoints = ({ type }: APIEndpointsProps) => { + const { itemZUID } = useParams<{ + itemZUID: string; + }>(); + const item = useSelector( + (state: AppState) => state.content[itemZUID] as ContentItem + ); + const instance = useSelector((state: AppState) => state.instance); + const { data: domains } = useGetDomainsQuery(); + + const apiTypeEndpointMap: Partial> = { + "quick-access": `/-/instant/${itemZUID}.json`, + "site-generators": item ? `/${item?.web?.path}/?toJSON` : "/?toJSON", + }; + + const liveDomain = domains?.find((domain) => domain.branch == "live"); + + return ( + + { + window.open( + // @ts-expect-error config not typed + `${CONFIG.URL_PREVIEW_PROTOCOL}${instance.randomHashID}${CONFIG.URL_PREVIEW}${apiTypeEndpointMap[type]}`, + "_blank" + ); + }} + > + + + + + {/* @ts-expect-error config not typed */} + {`${instance.randomHashID}${CONFIG.URL_PREVIEW}${apiTypeEndpointMap[type]}`} + + + + {liveDomain && ( + { + window.open( + `https://${liveDomain.domain}${apiTypeEndpointMap[type]}`, + "_blank" + ); + }} + > + + + + + {`${liveDomain.domain}${apiTypeEndpointMap[type]}`} + + + + )} + + ); +}; diff --git a/src/apps/content-editor/src/app/components/ContentBreadcrumbs.tsx b/src/apps/content-editor/src/app/components/ContentBreadcrumbs.tsx index 3d4dcbd0ab..dfedaa2610 100644 --- a/src/apps/content-editor/src/app/components/ContentBreadcrumbs.tsx +++ b/src/apps/content-editor/src/app/components/ContentBreadcrumbs.tsx @@ -4,7 +4,7 @@ import { useGetContentNavItemsQuery, } from "../../../../../shell/services/instance"; import { Home } from "@zesty-io/material"; -import { useHistory, useParams } from "react-router"; +import { useHistory, useParams, useLocation } from "react-router"; import { useMemo } from "react"; import { ContentNavItem } from "../../../../../shell/services/types"; import { MODEL_ICON } from "../../../../../shell/constants"; @@ -18,8 +18,12 @@ export const ContentBreadcrumbs = () => { itemZUID: string; }>(); const history = useHistory(); + const location = useLocation(); const breadcrumbData = useMemo(() => { + const isInMultipageTableView = !["new", "import"].includes( + location?.pathname?.split("/")?.pop() + ); let activeItem: ContentNavItem; const crumbs = []; @@ -52,6 +56,12 @@ export const ContentBreadcrumbs = () => { parent = null; } } + + if (!itemZUID && isInMultipageTableView) { + // Remove the model as a breadcrumb item when viewing in multipage table view + crumbs?.pop(); + } + return crumbs.map((item) => ({ node: , onClick: () => { @@ -62,7 +72,7 @@ export const ContentBreadcrumbs = () => { } }, })); - }, [nav, itemZUID]); + }, [nav, itemZUID, modelZUID, location]); return ( { if (fields?.length) { - return fields.filter((field) => !field.deletedAt); + return fields.filter( + (field) => !field.deletedAt && field.name !== "og_image" + ); } return []; diff --git a/src/apps/content-editor/src/app/components/Editor/Editor.less b/src/apps/content-editor/src/app/components/Editor/Editor.less index 60565bf620..e60308712b 100644 --- a/src/apps/content-editor/src/app/components/Editor/Editor.less +++ b/src/apps/content-editor/src/app/components/Editor/Editor.less @@ -3,7 +3,6 @@ .Fields { display: flex; flex-direction: column; - height: 100%; width: 100%; scrollbar-width: none; /* FireFox */ 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 886b96dc93..64c2e47f12 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 @@ -482,6 +482,7 @@ export const Field = ({ ), }); }} + settings={settings} name={name} onChange={onChange} lockedToGroupId={ @@ -881,10 +882,9 @@ export const Field = ({ errors={errors} > !!error)} /> diff --git a/src/apps/content-editor/src/app/components/FieldTypeMedia.tsx b/src/apps/content-editor/src/app/components/FieldTypeMedia.tsx index 6ea4f8af00..024b364f1e 100644 --- a/src/apps/content-editor/src/app/components/FieldTypeMedia.tsx +++ b/src/apps/content-editor/src/app/components/FieldTypeMedia.tsx @@ -1,4 +1,11 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { + useCallback, + useEffect, + useMemo, + useState, + useImperativeHandle, + forwardRef, +} from "react"; import { useDispatch, useSelector } from "react-redux"; import { Box, @@ -26,7 +33,7 @@ import { } from "@mui/icons-material"; import { alpha } from "@mui/material/styles"; import { CompactView, Modal, Login } from "@bynder/compact-view"; -import { Bynder } from "@zesty-io/material"; +import { Bynder, FileReplace } from "@zesty-io/material"; import { useGetBinsQuery, @@ -43,6 +50,8 @@ import styles from "../../../../media/src/app/components/Thumbnail/Loading.less" import cx from "classnames"; import { FileTypePreview } from "../../../../media/src/app/components/FileModal/FileTypePreview"; import { useGetInstanceSettingsQuery } from "../../../../../shell/services/instance"; +import { ReplaceFileModal } from "../../../../media/src/app/components/FileModal/ReplaceFileModal"; +import { showReportDialog } from "@sentry/react"; type FieldTypeMediaProps = { images: string[]; @@ -53,283 +62,520 @@ type FieldTypeMediaProps = { hasError?: boolean; hideDrag?: boolean; lockedToGroupId: string | null; + settings?: any; }; -export const FieldTypeMedia = ({ - images, - limit, - openMediaBrowser, - onChange, - name, - hasError, - hideDrag, - lockedToGroupId, -}: FieldTypeMediaProps) => { - const [draggedIndex, setDraggedIndex] = useState(null); - const [hoveredIndex, setHoveredIndex] = useState(null); - const [localImageZUIDs, setLocalImageZUIDs] = useState(images); - const instanceId = useSelector((state: any) => state.instance.ID); - const ecoId = useSelector((state: any) => state.instance.ecoID); - const { data: bins } = useGetBinsQuery({ instanceId, ecoId }); - const defaultBin = bins?.find((bin) => bin.default); - const dispatch = useDispatch(); - const [showFileModal, setShowFileModal] = useState(""); - const [imageToReplace, setImageToReplace] = useState(""); - const [isBynderOpen, setIsBynderOpen] = useState(false); - const { data: rawInstanceSettings } = useGetInstanceSettingsQuery(); - - const bynderPortalUrlSetting = rawInstanceSettings?.find( - (setting) => setting.key === "bynder_portal_url" - ); - const bynderTokenSetting = rawInstanceSettings?.find( - (setting) => setting.key === "bynder_token" - ); - // Checks if the bynder portal and token are set - const isBynderSessionValid = - localStorage.getItem("cvrt") && localStorage.getItem("cvad"); +export const FieldTypeMedia = forwardRef( + ( + { + images, + limit, + openMediaBrowser, + onChange, + name, + hasError, + hideDrag, + lockedToGroupId, + settings, + }: FieldTypeMediaProps, + ref + ) => { + const [draggedIndex, setDraggedIndex] = useState(null); + const [hoveredIndex, setHoveredIndex] = useState(null); + const [localImageZUIDs, setLocalImageZUIDs] = useState(images); + const instanceId = useSelector((state: any) => state.instance.ID); + const ecoId = useSelector((state: any) => state.instance.ecoID); + const { data: bins } = useGetBinsQuery({ instanceId, ecoId }); + const defaultBin = bins?.find((bin) => bin.default); + const dispatch = useDispatch(); + const [showFileModal, setShowFileModal] = useState(""); + const [imageToReplace, setImageToReplace] = useState(""); + const [isBynderOpen, setIsBynderOpen] = useState(false); + const { data: rawInstanceSettings } = useGetInstanceSettingsQuery(); + const [selectionError, setSelectionError] = useState(""); + + const bynderPortalUrlSetting = rawInstanceSettings?.find( + (setting) => setting.key === "bynder_portal_url" + ); + const bynderTokenSetting = rawInstanceSettings?.find( + (setting) => setting.key === "bynder_token" + ); + // Checks if the bynder portal and token are set + const isBynderSessionValid = + localStorage.getItem("cvrt") && localStorage.getItem("cvad"); + + useEffect(() => { + setLocalImageZUIDs(images); + }, [images]); + + useEffect(() => { + if (bynderPortalUrlSetting?.value) { + localStorage.setItem("cvad", bynderPortalUrlSetting.value); + } else { + localStorage.removeItem("cvad"); + } + }, [bynderPortalUrlSetting]); - useEffect(() => { - setLocalImageZUIDs(images); - }, [images]); + useEffect(() => { + if (bynderTokenSetting?.value) { + localStorage.setItem("cvrt", bynderTokenSetting.value); + } else { + localStorage.removeItem("cvrt"); + } + }, [bynderTokenSetting]); + + useImperativeHandle(ref, () => ({ + triggerOpenMediaBrowser() { + openMediaBrowser({ + limit, + callback: addZestyImage, + }); + }, + })); + + const addZestyImage = (selectedImages: any[]) => { + const removedImages: any[] = []; + const filteredSelectedImages = selectedImages?.filter((selectedImage) => { + //remove any images that do not match the file extension + if (settings?.fileExtensions) { + if ( + settings?.fileExtensions?.includes( + `.${fileExtension(selectedImage.filename)}` + ) + ) { + return true; + } else { + removedImages.push(selectedImage); + return false; + } + } else { + return true; + } + }); - useEffect(() => { - if (bynderPortalUrlSetting?.value) { - localStorage.setItem("cvad", bynderPortalUrlSetting.value); - } else { - localStorage.removeItem("cvad"); - } - }, [bynderPortalUrlSetting]); + if (removedImages.length) { + const filenames = removedImages.map((image) => image.filename); + const formattedFilenames = + filenames.length > 1 + ? filenames.slice(0, -1).join(", ") + " and " + filenames.slice(-1) + : filenames[0]; + + setSelectionError( + `Could not add ${formattedFilenames}. ${settings?.fileExtensionsErrorMessage}` + ); + } else { + setSelectionError(""); + } - useEffect(() => { - if (bynderTokenSetting?.value) { - localStorage.setItem("cvrt", bynderTokenSetting.value); - } else { - localStorage.removeItem("cvrt"); - } - }, [bynderTokenSetting]); + const newImageZUIDs = filteredSelectedImages?.map((image) => image.id); - const addZestyImage = (selectedImages: any[]) => { - const newImageZUIDs = selectedImages.map((image) => image.id); - // remove any duplicates - const filteredImageZUIDs = newImageZUIDs.filter( - (zuid) => !images.includes(zuid) - ); + // remove any duplicates + const filteredImageZUIDs = newImageZUIDs.filter( + (zuid) => !images.includes(zuid) + ); - onChange([...images, ...filteredImageZUIDs].join(","), name); - }; + // Do not trigger onChange if no images are added + if (![...images, ...filteredImageZUIDs]?.length) return; - const addBynderAsset = (selectedAsset: any[]) => { - if (images.length > limit) return; + onChange([...images, ...filteredImageZUIDs].join(","), name); + }; - const newBynderAssets = selectedAsset - .slice(0, limit - images.length) - .map((asset) => asset.originalUrl); - const filteredBynderAssets = newBynderAssets.filter( - (asset) => !images.includes(asset) - ); + const addBynderAsset = (selectedAsset: any[]) => { + if (images.length > limit) return; - onChange([...images, ...filteredBynderAssets].join(","), name); - }; + const removedAssets: any[] = []; + const filteredBynderAssets = selectedAsset?.filter((asset) => { + if (settings?.fileExtensions) { + const assetExtension = `.${asset.extensions[0]}`; + if (settings?.fileExtensions?.includes(assetExtension)) { + return true; + } else { + removedAssets.push(asset); + return false; + } + } else { + return true; + } + }); - const removeImage = (imageId: string) => { - const newImageZUIDs = images.filter((image) => image !== imageId); + if (removedAssets.length) { + const filenames = removedAssets.map((asset) => asset.name); + const formattedFilenames = + filenames.length > 1 + ? filenames.slice(0, -1).join(", ") + " and " + filenames.slice(-1) + : filenames[0]; + + setSelectionError( + `Could not add ${formattedFilenames}. ${settings?.fileExtensionsErrorMessage}` + ); + } else { + setSelectionError(""); + } - onChange(newImageZUIDs.join(","), name); - }; + const newBynderAssets = filteredBynderAssets + .slice(0, limit - images.length) + .map((asset) => asset.originalUrl); + const filteredBynderAssetsUrls = newBynderAssets.filter( + (asset) => !images.includes(asset) + ); - const replaceImage = (images: any[]) => { - const imageZUID = images.map((image) => image.id)?.[0]; - let imageToReplace: string; - setImageToReplace((value: string) => { - imageToReplace = value; - return ""; - }); - // if selected replacement image is already in the list of images, do nothing - if (localImageZUIDs.includes(imageZUID)) return; - const newImageZUIDs = localImageZUIDs.map((zuid) => { - if (zuid === imageToReplace) { - return imageZUID; - } + onChange([...images, ...filteredBynderAssetsUrls].join(","), name); + }; - return zuid; - }); - onChange(newImageZUIDs.join(","), name); - }; + const removeImage = (imageId: string) => { + const newImageZUIDs = images.filter((image) => image !== imageId); - const replaceBynderAsset = (selectedAsset: any) => { - // Prevent adding bynder asset that has already been added - if (localImageZUIDs.includes(selectedAsset.originalUrl)) return; + onChange(newImageZUIDs.join(","), name); + }; - const newImages = localImageZUIDs.map((image) => { - if (image === imageToReplace) { - return selectedAsset.originalUrl; + const replaceImage = (images: any[]) => { + const imageZUID = images.map((image) => image.id)?.[0]; + let imageToReplace: string; + setImageToReplace((value: string) => { + imageToReplace = value; + return ""; + }); + // if selected replacement image is already in the list of images, do nothing + if (localImageZUIDs.includes(imageZUID)) return; + // if extension is not allowed set error message + if (settings?.fileExtensions) { + if ( + !settings?.fileExtensions?.includes( + `.${fileExtension(images[0].filename)}` + ) + ) { + setSelectionError( + `Could not replace. ${settings?.fileExtensionsErrorMessage}` + ); + return; + } else { + setSelectionError(""); + } } + const newImageZUIDs = localImageZUIDs.map((zuid) => { + if (zuid === imageToReplace) { + return imageZUID; + } - return image; - }); + return zuid; + }); - setImageToReplace(""); - onChange(newImages.join(","), name); - }; + onChange(newImageZUIDs.join(","), name); + }; + + const replaceBynderAsset = (selectedAsset: any) => { + // Prevent adding bynder asset that has already been added + if (localImageZUIDs.includes(selectedAsset.originalUrl)) return; + + const assetExtension = `.${selectedAsset.extensions[0]}`; + if ( + settings?.fileExtensions && + !settings?.fileExtensions?.includes(assetExtension) + ) { + setSelectionError( + `Could not replace. ${settings?.fileExtensionsErrorMessage}` + ); + return; + } else { + setSelectionError(""); + } - const onDrop = useCallback( - (acceptedFiles: File[]) => { - if (!defaultBin) return; + const newImages = localImageZUIDs.map((image) => { + if (image === imageToReplace) { + return selectedAsset.originalUrl; + } - openMediaBrowser({ - limit, - callback: addZestyImage, + return image; }); - dispatch( - fileUploadStage( - acceptedFiles.map((file) => { - return { - file, - bin_id: defaultBin.id, - group_id: lockedToGroupId ? lockedToGroupId : defaultBin.id, - }; - }) - ) - ); - }, - [defaultBin, dispatch, addZestyImage] - ); - - const handleReorder = () => { - const newLocalImages = [...localImageZUIDs]; - const draggedField = newLocalImages[draggedIndex]; - newLocalImages.splice(draggedIndex, 1); - newLocalImages.splice(hoveredIndex, 0, draggedField); - - setDraggedIndex(null); - setHoveredIndex(null); - setLocalImageZUIDs(newLocalImages); - onChange(newLocalImages.join(","), name); - }; + setImageToReplace(""); + onChange(newImages.join(","), name); + }; + + const onDrop = useCallback( + (acceptedFiles: File[]) => { + if (!defaultBin) return; + + openMediaBrowser({ + limit, + callback: addZestyImage, + }); + + dispatch( + fileUploadStage( + acceptedFiles.map((file) => { + return { + file, + bin_id: defaultBin.id, + group_id: lockedToGroupId ? lockedToGroupId : defaultBin.id, + }; + }) + ) + ); + }, + [defaultBin, dispatch, addZestyImage] + ); - const sortedImages = useMemo(() => { - if (draggedIndex === null || hoveredIndex === null) { - return localImageZUIDs; - } else { - const newImages = [...localImageZUIDs]; - const draggedImage = newImages[draggedIndex]; - newImages.splice(draggedIndex, 1); - newImages.splice(hoveredIndex, 0, draggedImage); - return newImages; - } - }, [draggedIndex, hoveredIndex, localImageZUIDs]); + const handleReorder = () => { + const newLocalImages = [...localImageZUIDs]; + const draggedField = newLocalImages[draggedIndex]; + newLocalImages.splice(draggedIndex, 1); + newLocalImages.splice(hoveredIndex, 0, draggedField); + + setDraggedIndex(null); + setHoveredIndex(null); + setLocalImageZUIDs(newLocalImages); + onChange(newLocalImages.join(","), name); + }; + + const sortedImages = useMemo(() => { + if (draggedIndex === null || hoveredIndex === null) { + return localImageZUIDs; + } else { + const newImages = [...localImageZUIDs]; + const draggedImage = newImages[draggedIndex]; + newImages.splice(draggedIndex, 1); + newImages.splice(hoveredIndex, 0, draggedImage); + return newImages; + } + }, [draggedIndex, hoveredIndex, localImageZUIDs]); - const { getRootProps, getInputProps, open, isDragActive } = useDropzone({ - onDrop, - }); + const { getRootProps, getInputProps, open, isDragActive } = useDropzone({ + onDrop, + }); - if (!images?.length) - return ( - <> -
evt.stopPropagation(), - onKeyDown: (evt) => evt.stopPropagation(), - })} - > - - `1px dashed ${theme.palette.primary.main}`, - borderRadius: "8px", - backgroundColor: (theme) => - alpha(theme.palette.primary.main, 0.04), - borderColor: hasError ? "error.main" : "primary.main", - }} + if (!images?.length) + return ( + <> +
evt.stopPropagation(), + onKeyDown: (evt) => evt.stopPropagation(), + })} > - - {isDragActive ? ( - - ) : ( - - )} - + + `1px dashed ${theme.palette.primary.main}`, + borderRadius: "8px", + backgroundColor: (theme) => + alpha(theme.palette.primary.main, 0.04), + borderColor: hasError ? "error.main" : "primary.main", + }} + > + {isDragActive ? ( - "Drop your files here to Upload" + ) : ( - <> - Drag and drop your files here
or - + )} -
- {!isDragActive && ( - - - - {isBynderSessionValid && ( - )} - + + {isBynderSessionValid && ( + + )} + + )} +
+ + {selectionError && ( + + {selectionError} + + )} +
+ setIsBynderOpen(false)}> + + { + if (assets?.length) { + addBynderAsset(assets); + setIsBynderOpen(false); + } + }} + /> + + + + ); + + return ( + <> + + hasError ? `1px solid ${theme.palette.error.main}` : "none", + }} + > + {sortedImages.map((image, index) => { + const isBynderAsset = image.includes("bynder.com"); + + return ( + setShowFileModal(imageZUID)} + onRemove={removeImage} + onReplace={(imageZUID) => { + setImageToReplace(imageZUID); + + if (isBynderAsset) { + setIsBynderOpen(true); + } else { + openMediaBrowser({ + callback: replaceImage, + isReplace: true, + }); + } + }} + hideDrag={hideDrag || limit === 1} + isBynderAsset={isBynderAsset} + isBynderSessionValid={!!isBynderSessionValid} + /> + ); + })} + {limit > images.length && ( + + {!isBynderSessionValid && ( + )} - -
-
+ + {isBynderSessionValid && ( + + )} + + )} + + {selectionError && ( + + {selectionError} + + )} + {showFileModal && ( + setShowFileModal("")} + currentFiles={ + sortedImages?.filter( + (image) => typeof image === "string" + ) as string[] + } + onFileChange={(fileId) => { + setShowFileModal(fileId); + }} + /> + )} setIsBynderOpen(false)}> { if (assets?.length) { - addBynderAsset(assets); + if (imageToReplace) { + replaceBynderAsset(assets[0]); + } else { + addBynderAsset(assets); + } + setIsBynderOpen(false); } }} @@ -338,123 +584,8 @@ export const FieldTypeMedia = ({ ); - - return ( - <> - - hasError ? `1px solid ${theme.palette.error.main}` : "none", - }} - > - {sortedImages.map((image, index) => { - const isBynderAsset = image.includes("bynder.com"); - - return ( - setShowFileModal(imageZUID)} - onRemove={removeImage} - onReplace={(imageZUID) => { - setImageToReplace(imageZUID); - - if (isBynderAsset) { - setIsBynderOpen(true); - } else { - openMediaBrowser({ - callback: replaceImage, - isReplace: true, - }); - } - }} - hideDrag={hideDrag || limit === 1} - isBynderAsset={isBynderAsset} - isBynderSessionValid={!!isBynderSessionValid} - /> - ); - })} - {limit > images.length && ( - - {!isBynderSessionValid && ( - - )} - - {isBynderSessionValid && ( - - )} - - )} - - {showFileModal && ( - setShowFileModal("")} - currentFiles={ - sortedImages?.filter( - (image) => typeof image === "string" - ) as string[] - } - onFileChange={(fileId) => { - setShowFileModal(fileId); - }} - /> - )} - setIsBynderOpen(false)}> - - { - if (assets?.length) { - if (imageToReplace) { - replaceBynderAsset(assets[0]); - } else { - addBynderAsset(assets); - } - - setIsBynderOpen(false); - } - }} - /> - - - - ); -}; + } +); type MediaItemProps = { imageZUID: string; @@ -462,14 +593,15 @@ type MediaItemProps = { setDraggedIndex?: (index: number) => void; setHoveredIndex?: (index: number) => void; index: number; - onPreview: (imageZUID: string) => void; - onRemove: (imageZUID: string) => void; - onReplace: (imageZUID: string) => void; + onPreview?: (imageZUID: string) => void; + onRemove?: (imageZUID: string) => void; + onReplace?: (imageZUID: string) => void; hideDrag?: boolean; isBynderAsset: boolean; isBynderSessionValid: boolean; + hideActionButtons?: boolean; }; -const MediaItem = ({ +export const MediaItem = ({ imageZUID, onReorder, setDraggedIndex, @@ -481,6 +613,7 @@ const MediaItem = ({ hideDrag, isBynderAsset, isBynderSessionValid, + hideActionButtons, }: MediaItemProps) => { const [isDragging, setIsDragging] = useState(false); const [isDraggable, setIsDraggable] = useState(false); @@ -489,6 +622,7 @@ const MediaItem = ({ skip: imageZUID?.substr(0, 4) === "http", }); const [showRenameFileModal, setShowRenameFileModal] = useState(false); + const [isReplaceFileModalOpen, setIsReplaceFileModalOpen] = useState(false); const [isCopied, setIsCopied] = useState(false); const [isCopiedZuid, setIsCopiedZuid] = useState(false); const [newFilename, setNewFilename] = useState(""); @@ -587,7 +721,7 @@ const MediaItem = ({ onClick={() => { if (isURL) return; - onPreview(imageZUID); + onPreview && onPreview(imageZUID); }} alignItems="center" sx={{ @@ -649,6 +783,7 @@ const MediaItem = ({ )} @@ -656,7 +791,9 @@ const MediaItem = ({ )} - - {!isBynderAsset || (isBynderAsset && isBynderSessionValid) ? ( - + {!hideActionButtons && ( + + {!isBynderAsset || (isBynderAsset && isBynderSessionValid) ? ( + + { + event.stopPropagation(); + onReplace && onReplace(imageZUID); + }} + > + + + + ) : ( + <> + )} + {!isURL && ( + + + + + + )} + { event.stopPropagation(); - onReplace(imageZUID); + setAnchorEl(event.currentTarget); }} > - - - - ) : ( - <> - )} - {!isURL && ( - - - + - )} - - { + { event.stopPropagation(); - setAnchorEl(event.currentTarget); + setAnchorEl(null); + }} + PaperProps={{ + style: { + width: "288px", + }, + }} + anchorOrigin={{ + vertical: "bottom", + horizontal: "right", + }} + transformOrigin={{ + vertical: "top", + horizontal: "right", }} > - - - - { - event.stopPropagation(); - setAnchorEl(null); - }} - PaperProps={{ - style: { - width: "288px", - }, - }} - anchorOrigin={{ - vertical: "bottom", - horizontal: "right", - }} - transformOrigin={{ - vertical: "top", - horizontal: "right", - }} - > - {!isURL && !isBynderAsset && ( + {!isURL && !isBynderAsset && ( + <> + { + event.stopPropagation(); + setAnchorEl(null); + setShowRenameFileModal(true); + }} + > + + + + Rename + + { + event.stopPropagation(); + setAnchorEl(null); + setIsReplaceFileModalOpen(true); + }} + > + + + + Replace File + + { + event.stopPropagation(); + handleCopyClick(imageZUID, true); + }} + > + + {isCopiedZuid ? : } + + Copy ZUID + + + )} { event.stopPropagation(); - setAnchorEl(null); - setShowRenameFileModal(true); + handleCopyClick(isURL ? imageZUID : data?.url, false); }} > - + {isCopied ? : } - Rename + Copy File Url - )} - {!isURL && !isBynderAsset && ( { event.stopPropagation(); - handleCopyClick(imageZUID, true); + setAnchorEl(null); + onRemove && onRemove(imageZUID); }} > - {isCopiedZuid ? : } + - Copy ZUID + Remove - )} - { - event.stopPropagation(); - handleCopyClick(isURL ? imageZUID : data?.url, false); - }} - > - - {isCopied ? : } - - Copy File Url - - { - event.stopPropagation(); - setAnchorEl(null); - onRemove(imageZUID); - }} - > - - - - Remove - - - + + + )} {showRenameFileModal && ( @@ -806,6 +957,13 @@ const MediaItem = ({ extension={fileExtension(data.filename)} /> )} + {isReplaceFileModalOpen && ( + setIsReplaceFileModalOpen(false)} + onCancel={() => setIsReplaceFileModalOpen(false)} + /> + )} ); }; diff --git a/src/apps/content-editor/src/app/views/ItemCreate/ItemCreate.tsx b/src/apps/content-editor/src/app/views/ItemCreate/ItemCreate.tsx index 6259b29f40..7af2c7adbb 100644 --- a/src/apps/content-editor/src/app/views/ItemCreate/ItemCreate.tsx +++ b/src/apps/content-editor/src/app/views/ItemCreate/ItemCreate.tsx @@ -6,14 +6,13 @@ import isEmpty from "lodash/isEmpty"; import { createSelector } from "@reduxjs/toolkit"; import { cloneDeep } from "lodash"; -import { Divider, Box, Stack } from "@mui/material"; +import { Divider, Box, Stack, ThemeProvider } from "@mui/material"; +import { theme } from "@zesty-io/material"; import { WithLoader } from "@zesty-io/core/WithLoader"; import { NotFound } from "../../../../../../shell/components/NotFound"; import { Header } from "./Header"; import { Editor } from "../../components/Editor"; -import { ItemSettings } from "../ItemEdit/Meta/ItemSettings"; -import { DataSettings } from "../ItemEdit/Meta/ItemSettings/DataSettings"; import { fetchFields } from "../../../../../../shell/store/fields"; import { createItem, @@ -35,6 +34,8 @@ import { ContentModelField, } from "../../../../../../shell/services/types"; import { SchedulePublish } from "../../../../../../shell/components/SchedulePublish"; +import { Meta } from "../ItemEdit/Meta"; +import { SocialMediaPreview } from "../ItemEdit/Meta/SocialMediaPreview"; export type ActionAfterSave = | "" @@ -79,6 +80,7 @@ export const ItemCreate = () => { const [willRedirect, setWillRedirect] = useState(true); const [fieldErrors, setFieldErrors] = useState({}); const [saveClicked, setSaveClicked] = useState(false); + const [hasSEOErrors, setHasSEOErrors] = useState(false); const [ createPublishing, @@ -102,7 +104,14 @@ export const ItemCreate = () => { // if item doesn't exist, generate a new one useEffect(() => { if (isEmpty(item) && !saving) { - dispatch(generateItem(modelZUID)); + const initialData = fields?.reduce((accu, curr) => { + if (!curr.deletedAt) { + accu[curr.name] = null; + } + return accu; + }, {}); + + dispatch(generateItem(modelZUID, initialData)); } }, [modelZUID, item, saving]); @@ -148,7 +157,7 @@ export const ItemCreate = () => { const save = async (action: ActionAfterSave) => { setSaveClicked(true); - if (hasErrors) return; + if (hasErrors || hasSEOErrors) return; setSaving(true); @@ -339,8 +348,10 @@ export const ItemCreate = () => { className={styles.ItemCreate} bgcolor="grey.50" alignItems="center" + direction="row" + gap={4} > - + { setFieldErrors(errors); }} /> - - -

Meta Settings

- {model && model?.type === "dataset" ? ( - - ) : ( - - )} + { + setHasSEOErrors(hasErrors); + }} + isSaving={saving} + />
+ + + + +
{isScheduleDialogOpen && !isLoadingNewItem && ( diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Content/Actions/Actions.js b/src/apps/content-editor/src/app/views/ItemEdit/Content/Actions/Actions.js index cd2e3c32e0..d06352b6d5 100644 --- a/src/apps/content-editor/src/app/views/ItemEdit/Content/Actions/Actions.js +++ b/src/apps/content-editor/src/app/views/ItemEdit/Content/Actions/Actions.js @@ -20,9 +20,9 @@ export function Actions(props) { return ; } - const canPublish = usePermission("PUBLISH"); - const canDelete = usePermission("DELETE"); - const canUpdate = usePermission("UPDATE"); + const canPublish = usePermission("PUBLISH", props.itemZUID); + const canDelete = usePermission("DELETE", props.itemZUID); + const canUpdate = usePermission("UPDATE", props.itemZUID); const domain = useDomain(); const { publishing } = props.item; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/ItemEdit.js b/src/apps/content-editor/src/app/views/ItemEdit/ItemEdit.js index ace3b56c03..a16a79b9fb 100644 --- a/src/apps/content-editor/src/app/views/ItemEdit/ItemEdit.js +++ b/src/apps/content-editor/src/app/views/ItemEdit/ItemEdit.js @@ -27,7 +27,6 @@ import { WithLoader } from "@zesty-io/core/WithLoader"; import { PendingEditsModal } from "../../components/PendingEditsModal"; import { LockedItem } from "../../components/LockedItem"; import { Content } from "./Content"; -import { Meta } from "./Meta"; import { ItemHead } from "./ItemHead"; import { NotFound } from "../NotFound"; @@ -47,6 +46,7 @@ import { import { DuoModeContext } from "../../../../../../shell/contexts/duoModeContext"; import { useLocalStorage } from "react-use"; import { FreestyleWrapper } from "./FreestyleWrapper"; +import { Meta } from "./Meta"; const selectItemHeadTags = createSelector( (state) => state.headTags, @@ -86,6 +86,7 @@ export default function ItemEdit() { const [notFound, setNotFound] = useState(""); const [saveClicked, setSaveClicked] = useState(false); const [fieldErrors, setFieldErrors] = useState({}); + const [hasSEOErrors, setHasSEOErrors] = useState(false); const { data: fields, isLoading: isLoadingFields } = useGetContentModelFieldsQuery(modelZUID); const [showDuoModeLS, setShowDuoModeLS] = useLocalStorage( @@ -101,7 +102,9 @@ export default function ItemEdit() { (setting.key === "basic_content_api_key" && setting.value) || (setting.key === "headless_authorization_key" && setting.value) || (setting.key === "authorization_key" && setting.value) || - (setting.key === "x_frame_options" && setting.value) + (setting.key === "x_frame_options" && + !!setting.value && + setting.value !== "sameorigin") ); }) || model?.type === "dataset"; @@ -227,7 +230,7 @@ export default function ItemEdit() { async function save() { setSaveClicked(true); - if (hasErrors) return; + if (hasErrors || hasSEOErrors) return; setSaving(true); try { @@ -429,17 +432,10 @@ export default function ItemEdit() { path="/content/:modelZUID/:itemZUID/meta" render={() => ( { + setHasSEOErrors(hasErrors); + }} + isSaving={saving} /> )} /> diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ContentInsights/MatchedWords.tsx b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ContentInsights/MatchedWords.tsx new file mode 100644 index 0000000000..b6c027907a --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ContentInsights/MatchedWords.tsx @@ -0,0 +1,85 @@ +import { Box, Stack, Typography, Chip } from "@mui/material"; +import { Check } from "@mui/icons-material"; +import { useMemo } from "react"; +import { useSelector } from "react-redux"; +import { useParams } from "react-router"; + +import { + stripDashesAndSlashes, + stripDoubleSpace, + stripPunctuation, +} from "./index"; +import { AppState } from "../../../../../../../../shell/store/types"; + +type MatchedWordsProps = { + uniqueNonCommonWordsArray: string[]; +}; +export const MatchedWords = ({ + uniqueNonCommonWordsArray, +}: MatchedWordsProps) => { + const { itemZUID } = useParams<{ + itemZUID: string; + }>(); + const item = useSelector((state: AppState) => state.content[itemZUID]); + + const contentAndMetaWordMatches = useMemo(() => { + const textMetaFieldNames = [ + "metaDescription", + "metaTitle", + "metaKeywords", + "pathPart", + ]; + + if ( + item?.web && + Object.values(item.web)?.length && + uniqueNonCommonWordsArray?.length + ) { + const metaWords = Object.entries(item.web)?.reduce( + (accu: string[], [fieldName, value]) => { + if (textMetaFieldNames.includes(fieldName) && !!value) { + const cleanedValue = stripDoubleSpace( + stripPunctuation( + stripDashesAndSlashes(value.trim().toLowerCase()) + ) + ); + + accu = [...accu, ...cleanedValue?.split(" ")]; + + return accu; + } + + return accu; + }, + [] + ); + + const uniqueMetaWords = Array.from(new Set(metaWords)); + + return uniqueMetaWords.filter((metaWord) => + uniqueNonCommonWordsArray.includes(metaWord) + ); + } + + return []; + }, [uniqueNonCommonWordsArray, item?.web]); + + return ( + + + Content and Meta Matched Words + + + {contentAndMetaWordMatches?.map((word) => ( + } + variant="outlined" + /> + ))} + + + ); +}; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ContentInsights/MostMentionedWords.tsx b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ContentInsights/MostMentionedWords.tsx new file mode 100644 index 0000000000..7a70626ff2 --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ContentInsights/MostMentionedWords.tsx @@ -0,0 +1,108 @@ +import { + Box, + Stack, + Typography, + Chip, + TextField, + InputAdornment, +} from "@mui/material"; +import { Search, Add, Remove } from "@mui/icons-material"; +import { useMemo, useState } from "react"; +import { COMMON_WORDS } from "."; + +type MostMentionedWordsProps = { + wordsArray: string[]; +}; +export const MostMentionedWords = ({ wordsArray }: MostMentionedWordsProps) => { + const [filterKeyword, setFilterKeyword] = useState(""); + const [showAll, setShowAll] = useState(false); + + const wordCount = useMemo(() => { + if (!!wordsArray?.length) { + const wordsWithCount = wordsArray?.reduce( + (accu: Record, word) => { + if (!COMMON_WORDS.includes(word)) { + if (word in accu) { + accu[word] += 1; + } else { + accu[word] = 1; + } + } + + return accu; + }, + {} + ); + + return Object.entries(wordsWithCount ?? {})?.sort( + ([, a], [, b]) => b - a + ); + } + + return []; + }, [wordsArray]); + + const filteredWords = useMemo(() => { + if (!!filterKeyword) { + return wordCount?.filter(([word]) => + word.includes(filterKeyword.toLowerCase().trim()) + ); + } + + return wordCount; + }, [filterKeyword, wordCount]); + + return ( + + + + Most Mentioned Words in Content Item + + + Check that your focus keywords are occurring a minimum 2 times. + + + setFilterKeyword(evt.target.value)} + size="small" + placeholder="Filter words" + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + {filteredWords + ?.slice(0, showAll ? undefined : 9) + ?.map(([word, count]) => ( + + {word} + + {count} + + + } + size="small" + variant="outlined" + /> + ))} + {filteredWords?.length > 10 && ( + : } + onClick={() => setShowAll(!showAll)} + /> + )} + + + ); +}; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ContentInsights/WordCount.tsx b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ContentInsights/WordCount.tsx new file mode 100644 index 0000000000..4478d5b120 --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ContentInsights/WordCount.tsx @@ -0,0 +1,63 @@ +import { Stack, Box, Typography, Divider } from "@mui/material"; + +type WordCountProps = { + totalWords: number; + totalUniqueWords: number; + totalUniqueNonCommonWords: number; +}; +export const WordCount = ({ + totalWords, + totalUniqueWords, + totalUniqueNonCommonWords, +}: WordCountProps) => { + return ( + + + + Words + + + {totalWords} + + + + + + Unique Words + + + {totalUniqueWords} + + + + + + Non Common Words + + + {totalUniqueNonCommonWords} + + + + ); +}; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ContentInsights/index.tsx b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ContentInsights/index.tsx new file mode 100644 index 0000000000..a03f3ede41 --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ContentInsights/index.tsx @@ -0,0 +1,268 @@ +import { useMemo, useState } from "react"; +import { useSelector } from "react-redux"; +import { useParams } from "react-router"; + +import { AppState } from "../../../../../../../../shell/store/types"; +import { useGetContentModelFieldsQuery } from "../../../../../../../../shell/services/instance"; +import { WordCount } from "./WordCount"; +import { MatchedWords } from "./MatchedWords"; +import { MostMentionedWords } from "./MostMentionedWords"; + +export const COMMON_WORDS: Readonly = [ + "null", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + "our", + "one", + "two", + "three", + "four", + "five", + "I'm", + "that's", + "it's", + "aren't", + "we've", + "i've", + "didn't", + "don't", + "you'll", + "you're", + "we're", + "Here's", + "about", + "actually", + "always", + "even", + "given", + "into", + "just", + "not", + "Im", + "thats", + "its", + "arent", + "weve", + "ive", + "didnt", + "dont", + "the", + "of", + "to", + "and", + "a", + "in", + "is", + "it", + "you", + "that", + "he", + "was", + "for", + "on", + "are", + "with", + "as", + "I", + "his", + "they", + "be", + "at", + "one", + "have", + "this", + "from", + "or", + "had", + "by", + "but", + "some", + "what", + "there", + "we", + "can", + "out", + "were", + "all", + "your", + "when", + "up", + "use", + "how", + "said", + "an", + "each", + "she", + "which", + "do", + "their", + "if", + "will", + "way", + "many", + "then", + "them", + "would", + "like", + "so", + "these", + "her", + "see", + "him", + "has", + "more", + "could", + "go", + "come", + "did", + "my", + "no", + "get", + "me", + "say", + "too", + "here", + "must", + "such", + "try", + "us", + "own", + "oh", + "any", + "youll", + "youre", + "also", + "than", + "those", + "though", + "thing", + "things", +] as const; + +// clean up functions +const stripTags = (string: string) => { + return string.replace(/(<([^>]+)>)/gi, ""); +}; + +const stripEncoded = (string: string) => { + return string.replace(/(&(.*?);)/gi, " "); +}; + +const stripHidden = (string: string) => { + return string.replace(/(\r|\n|\t)/gi, " "); +}; + +const stripZUIDs = (string: string) => { + return string.replace(/(\d-(.*?)-(.*?))(,| )/gi, " "); +}; + +export const stripPunctuation = (string: string) => { + return string.replace(/("|,|:|;|\. |!)/gi, " "); +}; + +export const stripDoubleSpace = (string: string) => { + return string.replace(/\s\s+/g, " "); +}; + +export const stripDashesAndSlashes = (string: string) => { + return string.replace(/-|\//g, " "); +}; + +const findMatch = (needle: string, haystack: string[]) => { + let truth = false; + haystack.forEach((word) => { + if (word.toLowerCase() == needle.toLowerCase()) truth = true; + }); + return truth; +}; + +export const ContentInsights = ({}) => { + const { itemZUID, modelZUID } = useParams<{ + itemZUID: string; + modelZUID: string; + }>(); + const item = useSelector((state: AppState) => state.content[itemZUID]); + const { data: modelFields } = useGetContentModelFieldsQuery(modelZUID, { + skip: !modelZUID, + }); + const [showAllWords, setShowAllWords] = useState(false); + + const textFieldNames = useMemo(() => { + if (modelFields?.length) { + const textFieldTypes = [ + "text", + "wysiwyg_basic", + "wysiwyg_advanced", + "article_writer", + "markdown", + "textarea", + ]; + + return modelFields.reduce((accu: string[], curr) => { + if (textFieldTypes.includes(curr.datatype) && !curr.deletedAt) { + accu = [...accu, curr.name]; + return accu; + } + + return accu; + }, []); + } + }, [modelFields]); + + const contentItemWordsArray = useMemo(() => { + if ( + item?.data && + Object.values(item.data)?.length && + textFieldNames?.length + ) { + let words: string[] = []; + + textFieldNames.forEach((fieldName) => { + let value = item?.data[fieldName]; + + if (!!value) { + value = stripDoubleSpace( + stripPunctuation( + stripHidden( + stripEncoded( + stripTags(stripZUIDs(String(value).trim().toLowerCase())) + ) + ) + ) + ); + + words = [...words, ...value.split(" ")]; + } + }); + + return words; + } + + return []; + }, [textFieldNames, item?.data]); + + const uniqueWordsArray = Array.from(new Set(contentItemWordsArray)); + const uniqueNonCommonWordsArray = Array.from( + uniqueWordsArray?.filter((word) => !COMMON_WORDS.includes(word)) + ); + + return ( + <> + + + + + ); +}; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/ContentInsights/ContentInsights.js b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/ContentInsights/ContentInsights.js deleted file mode 100644 index 4fcaee4823..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/ContentInsights/ContentInsights.js +++ /dev/null @@ -1,417 +0,0 @@ -import React, { useState } from "react"; - -import Divider from "@mui/material/Divider"; -import Button from "@mui/material/Button"; - -import Card from "@mui/material/Card"; -import CardHeader from "@mui/material/CardHeader"; -import CardContent from "@mui/material/CardContent"; -import SavedSearchIcon from "@mui/icons-material/SavedSearch"; - -import { faCheck, faSearchDollar } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import cx from "classnames"; - -import styles from "./ContentInsights.less"; -export function ContentInsights(props) { - const [showAllWords, setShowAllWords] = useState(false); - - // clean up functions - const stripTags = (string) => { - return string.replace(/(<([^>]+)>)/gi, ""); - }; - const stripEncoded = (string) => { - return string.replace(/(&(.*?);)/gi, " "); - }; - const stripHidden = (string) => { - return string.replace(/(\r|\n|\t)/gi, " "); - }; - const stripZUIDs = (string) => { - return string.replace(/(\d-(.*?)-(.*?))(,| )/gi, " "); - }; - const stripPunctuation = (string) => { - return string.replace(/("|,|:|;|\. |!)/gi, " "); - }; - const stripDoubleSpace = (string) => { - return string.replace(/\s\s+/g, " "); - }; - const stripDashesAndSlashes = (string) => { - return string.replace(/-|\//g, " "); - }; - const commonwords = [ - "null", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "0", - "our", - "one", - "two", - "three", - "four", - "five", - "I'm", - "that's", - "it's", - "aren't", - "we've", - "i've", - "didn't", - "don't", - "you'll", - "you're", - "we're", - "Here's", - "about", - "actually", - "always", - "even", - "given", - "into", - "just", - "not", - "Im", - "thats", - "its", - "arent", - "weve", - "ive", - "didnt", - "dont", - "the", - "of", - "to", - "and", - "a", - "in", - "is", - "it", - "you", - "that", - "he", - "was", - "for", - "on", - "are", - "with", - "as", - "I", - "his", - "they", - "be", - "at", - "one", - "have", - "this", - "from", - "or", - "had", - "by", - "but", - "some", - "what", - "there", - "we", - "can", - "out", - "were", - "all", - "your", - "when", - "up", - "use", - "how", - "said", - "an", - "each", - "she", - "which", - "do", - "their", - "if", - "will", - "way", - "many", - "then", - "them", - "would", - "like", - "so", - "these", - "her", - "see", - "him", - "has", - "more", - "could", - "go", - "come", - "did", - "my", - "no", - "get", - "me", - "say", - "too", - "here", - "must", - "such", - "try", - "us", - "own", - "oh", - "any", - "youll", - "youre", - "also", - "than", - "those", - "though", - "thing", - "things", - ]; - - const findMatch = (needle, haystack) => { - let truth = false; - haystack.forEach((word) => { - if (word.toLowerCase() == needle.toLowerCase()) truth = true; - }); - return truth; - }; - - let combinedString = ""; - let wordCount = {}; - let metaWordCount = {}; - - // Working with Content - // Content: combine all the text content we find from the item - for (const [key, value] of Object.entries(props.content)) { - combinedString += " " + value; - } - - // Content: clean the string up by remove values that are not considered word content - combinedString = stripDoubleSpace( - stripPunctuation( - stripHidden(stripEncoded(stripTags(stripZUIDs(combinedString)))) - ) - ); - combinedString = combinedString.toLowerCase(); - - // Meta: build combined string - let combinedMetaString = - props.meta.metaTitle + - " " + - props.meta.path + - " " + - props.meta.metaDescription; - // Meta: clean the string - combinedMetaString = stripDoubleSpace( - stripPunctuation(stripDashesAndSlashes(combinedMetaString.toLowerCase())) - ); - - // Content: get total word counts with initial split - let splitWords = combinedString.split(" "); - let totalWords = splitWords.length; - - // Content & Meta: remove common words - commonwords.forEach((commonWord) => { - let re = new RegExp("\\b" + commonWord.toLowerCase() + "\\b", "ig"); - combinedString = combinedString.replace(re, ""); - combinedMetaString = combinedMetaString.replace(re, ""); - }); - // Content: strip left over double spaces - combinedString = stripDoubleSpace(combinedString); - splitWords = combinedString.split(" "); - let totalNonCommonWords = splitWords.length; - - // Content: use split words to tally total count of numbers - splitWords.forEach((word) => { - if (!wordCount.hasOwnProperty(word)) { - wordCount[word] = 1; - } else { - wordCount[word]++; - } - }); - - // Meta: use split meta words to tally total count of numbers - let splitMetaWords = combinedMetaString.split(" "); - splitMetaWords.forEach((word) => { - if (!metaWordCount.hasOwnProperty(word)) { - metaWordCount[word] = 1; - } else { - metaWordCount[word]++; - } - }); - // Meta: Build Word Lists for output - let metaWordArray = []; - for (const [key, value] of Object.entries(metaWordCount)) { - if (key != "") { - metaWordArray.push({ - word: key, - count: value, - match: findMatch(key, splitWords), - }); - } - } - // Content: Sort Word List by more occuring - metaWordArray = metaWordArray.sort((a, b) => { - return b.count - a.count; - }); - - // Content: Build Word Lists for output - let wordArray = []; - for (const [key, value] of Object.entries(wordCount)) { - if (key != "") { - wordArray.push({ - word: key, - count: value, - match: findMatch(key, splitMetaWords), - }); - } - } - let totalUniqueNonCommonWords = wordArray.length; - // Content: Sort Word List by more occuring - wordArray = wordArray.sort((a, b) => { - return b.count - a.count; - }); - - return ( - - } - title="Content Insights" - sx={{ - backgroundColor: "grey.100", - }} - titleTypographyProps={{ - sx: { - color: "text.primary", - }, - }} - > - -
-
- Total Words {totalWords} -
-
- Non-Common* Words {totalNonCommonWords} -
-
- Unique Words* {totalUniqueNonCommonWords} -
-
- -

Content and Meta Matched Words

-
- {metaWordArray.map((item, i) => { - if (item.match) { - return ( -
- - - - {item.word} -
- ); - } - })} -
- -

- Word occurrences from this Content Item (not the fully rendered page) -

-
- {wordArray.map((item, i) => { - if (item.count > 1) { - return ( -
- {item.count} - {item.word} -
- ); - } - })} - {!showAllWords && ( - - )} - {showAllWords && ( - - )} -
- {showAllWords && ( -
- {wordArray.map((item, i) => { - if (item.count == 1) { - return ( -
- {item.count} - {item.word} -
- ); - } - })} -
- )} - -

Word occurrences from the URL, Meta Title and Description

-
- {metaWordArray.map((item, i) => { - return ( -
- {item.count} - {item.word} -
- ); - })} -
-
-
- ); -} diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/ContentInsights/ContentInsights.less b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/ContentInsights/ContentInsights.less deleted file mode 100644 index 394af8d17c..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/ContentInsights/ContentInsights.less +++ /dev/null @@ -1,83 +0,0 @@ -@import "~@zesty-io/core/typography.less"; -@import "~@zesty-io/core/colors.less"; -@roundPx: 6px; -.ContentInsights { - .toggleButton { - padding: 0px 10px !important; - margin-bottom: auto; - } - .level { - display: flex; - div { - flex: 1; - text-align: center; - background-color: lighten(@zesty-light-gray, 15%); - border: 1px lighten(@zesty-light-gray, 15%) solid; - color: @zesty-gray; - border-radius: 4px; - margin-right: 8px; - overflow: hidden; - padding-top: 4px; - &:last-child { - margin-right: 0px; - } - span { - display: block; - font-size: 2em; - background: white; - color: @zesty-dark-blue; - padding: 8px 0px; - margin-top: 4px; - } - } - } - .wordBank { - display: flex; - flex-direction: row; - flex-wrap: wrap; - margin-top: 8px; - .wordGroup { - display: flex; - width: min-content; - font-size: 1em; - margin-right: 8px; - margin-bottom: 8px; - border: 1px lighten(@zesty-light-gray, 15%) solid; - border-radius: @roundPx; - overflow: hidden; - - strong { - width: 20px; - flex: 1; - display: inline-block; - background: lighten(@zesty-light-gray, 15%); - color: @zesty-dark-blue; - - padding: 5px 8px; - font-weight: 600; - } - span { - flex: 5; - flex-direction: column; - flex-basis: 100%; - display: inline-block; - color: @zesty-dark-blue; - background: white; - overflow: hidden; - text-overflow: ellipsis; - - padding: 5px 8px; - } - &.hidden { - display: none; - } - &.matched { - border: 1px lighten(@zesty-green, 20%) solid; - strong { - background: lighten(@zesty-green, 20%); - color: darken(@zesty-green, 20%); - } - } - } - } -} diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/ContentInsights/index.js b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/ContentInsights/index.js deleted file mode 100644 index 9ac2579447..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/ContentInsights/index.js +++ /dev/null @@ -1 +0,0 @@ -export { ContentInsights } from "./ContentInsights"; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/DataSettings.js b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/DataSettings.js deleted file mode 100644 index f8796526e4..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/DataSettings.js +++ /dev/null @@ -1,48 +0,0 @@ -import { Component } from "react"; -import cx from "classnames"; - -import { MetaTitle } from "./settings/MetaTitle"; -import MetaDescription from "./settings/MetaDescription"; -import { MetaKeywords } from "./settings/MetaKeywords"; -import { MetaLinkText } from "./settings/MetaLinkText"; -import { Stack } from "@mui/material"; - -import styles from "./ItemSettings.less"; -export class DataSettings extends Component { - onChange = (value, name) => { - if (!name) { - throw new Error("Input is missing name attribute"); - } - this.props.dispatch({ - type: "SET_ITEM_WEB", - itemZUID: this.props.item.meta.ZUID, - key: name, - value: value, - }); - }; - - render() { - let web = this.props.item.web || {}; - return ( -
-
- - - - - - -
-
- ); - } -} diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/ItemSettings.js b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/ItemSettings.js deleted file mode 100644 index ca4d408705..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/ItemSettings.js +++ /dev/null @@ -1,227 +0,0 @@ -import { - memo, - Fragment, - useCallback, - useMemo, - useState, - useEffect, -} from "react"; -import { useSelector } from "react-redux"; -import { useDispatch } from "react-redux"; - -import { MetaTitle } from "./settings/MetaTitle"; -import MetaDescription from "./settings/MetaDescription"; -import { MetaKeywords } from "./settings/MetaKeywords"; -import { MetaLinkText } from "./settings/MetaLinkText"; -import { ItemRoute } from "./settings/ItemRoute"; -import { ContentInsights } from "./ContentInsights"; -import { ItemParent } from "./settings/ItemParent"; -import { CanonicalTag } from "./settings/CanonicalTag"; -import { SitemapPriority } from "./settings/SitemapPriority"; -import { useDomain } from "shell/hooks/use-domain"; - -import Card from "@mui/material/Card"; -import CardHeader from "@mui/material/CardHeader"; -import CardContent from "@mui/material/CardContent"; -import SearchIcon from "@mui/icons-material/Search"; - -import styles from "./ItemSettings.less"; -import { fetchGlobalItem } from "../../../../../../../../shell/store/content"; - -export const MaxLengths = { - metaLinkText: 150, - metaTitle: 150, - metaDescription: 160, - metaKeywords: 255, -}; - -export const ItemSettings = memo( - function ItemSettings(props) { - const showSiteNameInMetaTitle = useSelector( - (state) => - state.settings.instance.find( - (setting) => setting.key === "show_in_title" - )?.value - ); - const dispatch = useDispatch(); - const domain = useDomain(); - let { data, meta, web } = props.item; - const [errors, setErrors] = useState({}); - - data = data || {}; - meta = meta || {}; - web = web || {}; - - const siteName = useMemo(() => dispatch(fetchGlobalItem())?.site_name, []); - - const onChange = useCallback( - (value, name) => { - if (!name) { - throw new Error("Input is missing name attribute"); - } - - if (MaxLengths[name]) { - setErrors({ - ...errors, - [name]: { - EXCEEDING_MAXLENGTH: - value?.length > MaxLengths[name] - ? value?.length - MaxLengths[name] - : 0, - }, - }); - } - - props.dispatch({ - type: "SET_ITEM_WEB", - itemZUID: meta.ZUID, - key: name, - value: value, - }); - }, - [meta.ZUID, errors] - ); - - useEffect(() => { - if (props.saving) { - setErrors({}); - return; - } - }, [props.saving]); - - return ( -
-
- {web.pathPart !== "zesty_home" && ( - - - - - )} - - - - - - {props.item && ( - - )} -
- -
- ); - }, - (prevProps, nextProps) => { - // NOTE We want to update children when the `item` changes but only when `content` length changes - - // If the model we are viewing changes we need to re-render - if (prevProps.modelZUID !== nextProps.modelZUID) { - return false; - } - - // If the item referential equailty of the item changes we want to re-render - // should mean the item has updated data - if (prevProps.item !== nextProps.item) { - return false; - } - - // Avoid referential equality check and compare content length to see if new ones where added - let prevItemsLen = Object.keys(prevProps["content"]).length; - let nextItemsLen = Object.keys(nextProps["content"]).length; - if (prevItemsLen !== nextItemsLen) { - return false; - } - - /** - * We ignore changes to the `instance` object and `dispatch` - * as these values should not change. - */ - - return true; - } -); diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/ItemSettings.less b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/ItemSettings.less deleted file mode 100644 index 3f0eb24a4f..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/ItemSettings.less +++ /dev/null @@ -1,79 +0,0 @@ -@import "~@zesty-io/core/colors.less"; -@import "~@zesty-io/core/typography.less"; - -.Meta { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 32px; - // flex-direction: row; - // margin: 32px; - - .DataSettings { - flex: 0.6; - } - - .MetaMain { - flex: 3; - // margin-right: 32px; - min-width: 0px; - - &.DataSettings { - margin-right: 0; - } - - article { - margin: 16px 0; - } - } - - .MetaSide { - flex: 2; - margin: 16px 0; - min-width: 0px; - - .SearchResult { - border-radius: 5px; - background: white; - border: 1px @zesty-light-gray solid; - padding: 16px; - - .GoogleTitle { - font-family: Roboto, "Gibson", arial, sans-serif; - color: #1a0dab; // sampled from google - word-wrap: break-word; - font-size: 18px; - line-height: 21.6px; - font-weight: 400; - } - .GoogleLink { - color: #006621; // sampled from google - display: block; - font-family: Roboto, arial, sans-serif; - font-size: 14px; - font-style: normal; - font-weight: 400; - height: auto; - line-height: 16.8px; - padding: 2px 0 3px 0; - - a { - text-decoration: none; - color: #006621; // sampled from google - } - - span.Icon { - margin-left: 5px; - } - } - .GoogleDesc { - word-wrap: break-word; - color: rgb(84, 84, 84); - font-family: Roboto, arial, sans-serif; - font-size: 13px; - font-weight: 400; - height: auto; - line-height: 18.2px; - } - } - } -} diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/index.js b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/index.js deleted file mode 100644 index 57750330fa..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/index.js +++ /dev/null @@ -1 +0,0 @@ -export { ItemSettings } from "./ItemSettings"; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/ItemParent.js b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/ItemParent.js deleted file mode 100644 index c6748bec6c..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/ItemParent.js +++ /dev/null @@ -1,263 +0,0 @@ -import { memo, Fragment, useState, useEffect } from "react"; -import { connect, useSelector } from "react-redux"; -import { debounce, uniqBy } from "lodash"; -import { notify } from "shell/store/notifications"; - -import { Select, Option } from "@zesty-io/core/Select"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faHome } from "@fortawesome/free-solid-svg-icons"; - -import { searchItems } from "shell/store/content"; -import { FieldShell } from "../../../../../components/Editor/Field/FieldShell"; - -import styles from "./ItemParent.less"; -export const ItemParent = connect((state) => { - return { - nav: state.navContent.raw, - }; -})( - memo( - function ItemParent(props) { - const items = useSelector((state) => state.content); - const [loading, setLoading] = useState(false); - const [parent, setParent] = useState({ - meta: { - ZUID: "0", // "0" = root level route - path: "/", - }, - }); - - const [parents, setParents] = useState( - parentOptions(props.currentItemLangID, props.path, items) - ); - - const onSearch = debounce((term) => { - if (term) { - setLoading(true); - props.dispatch(searchItems(term)).then((res) => { - setLoading(false); - setParents( - parentOptions(props.currentItemLangID, props.path, { - ...items, - // needs to reduce and converts this data as the same format of the items to - // prevent having an issue on having an itemZUID with an incorrect format - // the reason is that the item has a format of {[itemZUID]:data} - // while the res.data has a value of an array which cause the needs of converting - // the response to an object with a zuid as a key - ...res?.data.reduce((acc, curr) => { - return { ...acc, [curr.meta.ZUID]: curr }; - }, {}), - }) - ); - }); - } - }, 250); - - /** - * Recurse nav linked list to find current items parent - * @param {*} zuid - * @param {*} count - */ - const findNavParent = (zuid, count = 0) => { - count++; - const navEntry = props.nav.find((el) => el.ZUID === zuid); - if (navEntry) { - // This first item should be the model we are resolving for so - // continue on up the nav tree - if (count === 0) { - return findNavParent(navEntry.parentZUID, count); - } else { - if (navEntry.type === "item") { - return navEntry; - } else if (navEntry.parentZUID) { - return findNavParent(navEntry.parentZUID, count); - } else { - return { ZUID: "0" }; - } - } - } else { - return { ZUID: "0" }; - } - }; - - useEffect(() => { - let parentZUID = props.parentZUID; - - // If it's a new item chase down the parentZUID within navigation - // This way we avoid an API request - if (props.itemZUID && props.itemZUID.slice(0, 3) === "new") { - const result = findNavParent(props.modelZUID); - - // change for preselection - parentZUID = result.ZUID; - - // Update redux store so if the item is saved we know it's parent - props.onChange(parentZUID, "parentZUID"); - } - - // Try to preselect parent - if (parentZUID && parentZUID != "0" && parentZUID !== null) { - const item = items[parentZUID]; - if (item && item.meta && item.meta.ZUID && item.meta.path) { - setParent(item); - } else { - props.dispatch(searchItems(parentZUID)).then((res) => { - if (res) { - if (res.data) { - if (Array.isArray(res.data) && res.data.length) { - // Handles cases where the model's parent is the homepage. This is no longer possible for newly created models but - // there are some old models that still have the homepage as their parent models. - if (res.data[0]?.web?.path === "/") { - setParent({ - meta: { - ZUID: "0", // "0" = root level route - path: "/", - }, - }); - } else { - setParent(res.data[0]); - /** - * // HACK Because we pre-load all item publishings and store them in the same reducer as the `content` - * we can't use array length comparision to determine a new parent has been added. Also since updates to the item - * currently being edited cause a new `content` object to be created in it's reducer we can't use - * referential equality checks to determine re-rendering. This scenario causes either the parent to not be pre-selected - * or a performance issue. To work around this we maintain the `parents` state internal and add the new parent we load from the - * API to allow it to be pre-selected while avoiding re-renders on changes to this item. - */ - - setParents( - parentOptions(props.currentItemLangID, props.path, { - ...items, - [res.data[0].meta.ZUID]: res.data[0], - }) - ); - } - } else { - props.dispatch( - notify({ - kind: "warn", - heading: `Cannot Save: ${props.metaTitle}`, - messsage: "Set page parent in SEO Tab", - }) - ); - } - } else { - props.dispatch( - notify({ - kind: "warn", - message: `API failed to return data. Try Again.`, - }) - ); - } - } else { - props.dispatch( - notify({ - kind: "warn", - heading: `"Cannot Save: ${props.metaTitle}`, - message: `Page's Parent does not exist or has been deleted`, - }) - ); - } - }); - } - } - }, []); - - return ( -
- - {/* - Delay rendering select until we have a parent. - Sometimes we have to resolve the parent from the API asynchronously - */} - {!parent ? ( - "Loading item parent" - ) : ( - - )} - -
- ); - }, - (prevProps, nextProps) => { - let isEqual = true; - - // Shallow compare all props - Object.keys(prevProps).forEach((key) => { - // ignore content, we'll check it seperately after - if (key !== "content") { - if (prevProps[key] !== nextProps[key]) { - isEqual = false; - } - } - }); - - return isEqual; - } - ) -); - -function parentOptions(currentItemLangID, path, items) { - const options = Object.entries(items) - ?.reduce((acc, [itemZUID, itemData]) => { - if ( - itemZUID.slice(0, 3) !== "new" && // Exclude new items - itemData?.meta?.ZUID && // must have a ZUID - itemData?.web?.path && // must have a path - itemData?.web.path !== "/" && // Exclude homepage - itemData?.web.path !== path && // Exclude current item - itemData?.meta?.langID === currentItemLangID // display only relevant language options - ) { - acc.push({ - value: itemZUID, - text: itemData.web.path, - }); - } - - return acc; - }, []) - .sort((a, b) => { - if (a.text > b.text) { - return 1; - } else if (a.text < b.text) { - return -1; - } else { - return 0; - } - }); - - return uniqBy(options, "value"); -} diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/ItemParent.less b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/ItemParent.less deleted file mode 100644 index c088a932ec..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/ItemParent.less +++ /dev/null @@ -1,9 +0,0 @@ -@import "~@zesty-io/core/colors.less"; - -.ItemParent { - label { - display: flex; - color: @zesty-dark-blue; - font-size: 14px; - } -} diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/ItemRoute.js b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/ItemRoute.js deleted file mode 100644 index da3fc8c513..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/ItemRoute.js +++ /dev/null @@ -1,186 +0,0 @@ -import { memo, Fragment, useCallback, useState, useEffect } from "react"; -import { connect, useDispatch } from "react-redux"; -import debounce from "lodash/debounce"; -import { searchItems } from "shell/store/content"; -import { notify } from "shell/store/notifications"; - -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faSpinner, faCheck, faTimes } from "@fortawesome/free-solid-svg-icons"; -import TextField from "@mui/material/TextField"; -import { InputIcon } from "@zesty-io/core/InputIcon"; -import { AppLink } from "@zesty-io/core/AppLink"; - -import styles from "./ItemRoute.less"; -import { withCursorPosition } from "../../../../../../../../../shell/components/withCursorPosition"; -import { FieldShell } from "../../../../../components/Editor/Field/FieldShell"; -const TextFieldWithCursorPosition = withCursorPosition(TextField); - -export const ItemRoute = connect((state) => { - return { - content: state.content, - }; -})( - memo(function ItemRoute(props) { - const dispatch = useDispatch(); - const [pathPart, setPathPart] = useState(props.path_part); - const [loading, setLoading] = useState(false); - const [unique, setUnique] = useState(true); - - const validate = useCallback( - debounce((path) => { - if (!path) { - setUnique(false); - return; - } - - const parent = props.content[props.parentZUID]; - const fullPath = parent ? parent.web.path + path : path; - - setLoading(true); - - return dispatch(searchItems(fullPath)) - .then((res) => { - if (res) { - if (res.data) { - if (Array.isArray(res.data) && res.data.length) { - // check list of partial matches for exact path match - const matches = res.data.filter((item) => { - /** - * Exclude currently viewed item zuid, as it's currently saved path would match. - * Check if other results have a matching path, if so then it is already taken and - * can not be used. - * Result paths come with leading and trailing slashes - */ - return ( - item.meta.ZUID !== props.ZUID && - item.web.path === "/" + fullPath + "/" - ); - }); - if (matches.length) { - props.dispatch( - notify({ - kind: "warn", - message: ( -

- URL {matches[0].web.path} is - unavailable. Used by  - - {matches[0].web.metaLinkText || - matches[0].web.metaTitle} - -

- ), - }) - ); - } - - setUnique(!matches.length); - } else { - setUnique(true); - } - } else { - props.dispatch( - notify({ - kind: "warn", - message: `API failed to return data ${res.status}`, - }) - ); - } - } else { - props.dispatch( - notify({ - kind: "warn", - message: `API failed to return response ${res.status}`, - }) - ); - } - }) - .finally(() => setLoading(false)); - }, 500), - [props.parentZUID] - ); - - const onChange = (evt) => { - // All URLs are lowercased - // Replace ampersand characters with 'and' - // Only allow alphanumeric characters - const path = evt.target.value - .trim() - .toLowerCase() - .replace(/\&/g, "and") - .replace(/[^a-zA-Z0-9]/g, "-"); - - validate(path); - setPathPart(path); - - props.dispatch({ - type: "SET_ITEM_WEB", - itemZUID: props.ZUID, - key: "pathPart", - value: path, - }); - }; - - // update internal state if external path part changes - useEffect(() => { - if (props.path_part) { - validate(props.path_part); - setPathPart(props.path_part); - } - }, [props.path_part, props.parentZUID]); - - return ( -
- -
- {props.path_part === "zesty_home" ? ( -

- -  Homepage -

- ) : ( - - - - {loading && ( - - - - )} - - {!loading && unique && ( - - - - )} - - {!loading && !unique && ( - - - - )} - - )} -
-
-
- ); - }) -); diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/ItemRoute.less b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/ItemRoute.less deleted file mode 100644 index c35a9c0d31..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/ItemRoute.less +++ /dev/null @@ -1,39 +0,0 @@ -@import "~@zesty-io/core/keyframes.less"; -@import "~@zesty-io/core/colors.less"; - -.ItemRoute { - label { - display: flex; - color: @zesty-dark-blue; - font-size: 14px; - } - .Path { - display: flex; - .Parent { - border-radius: 3px; - background-color: #4c5567; - align-items: center; - display: flex; - padding: 0 8px; - color: #fff; - } - .Checking { - margin: 0; - background-color: #404759; - border: 1px solid #404759; - i { - animation: spin 2s linear infinite; - } - } - .Valid { - margin: 0; - background-color: #63a41e; - border: 1px solid #63a41e; - } - .Invalid { - margin: 0; - background-color: #9a2803; - border: 1px solid #9a2803; - } - } -} diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaDescription.js b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaDescription.js deleted file mode 100644 index 9d786d1385..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaDescription.js +++ /dev/null @@ -1,86 +0,0 @@ -import { useState, useEffect } from "react"; -import { connect } from "react-redux"; -import { TextField } from "@mui/material"; - -import { notify } from "shell/store/notifications"; -import { FieldShell } from "../../../../../components/Editor/Field/FieldShell"; -import { MaxLengths } from "../ItemSettings"; - -import styles from "./MetaDescription.less"; -export default connect()(function MetaDescription({ - meta_description, - onChange, - dispatch, - errors, -}) { - const [error, setError] = useState(""); - - useEffect(() => { - if (meta_description) { - let message = ""; - - if (!(meta_description.indexOf("\u0152") === -1)) { - message = - "Found OE ligature. These special characters are not allowed in meta descriptions."; - } else if (!(meta_description.indexOf("\u0153") === -1)) { - message = - "Found oe ligature. These special characters are not allowed in meta descriptions."; - } else if (!(meta_description.indexOf("\xAB") === -1)) { - message = - "Found << character. These special characters are not allowed in meta descriptions."; - } else if (!(meta_description.indexOf("\xBB") === -1)) { - message = - "Found >> character. These special characters are not allowed in meta descriptions."; - } else if (/[\u201C\u201D\u201E]/.test(meta_description)) { - message = - "Found Microsoft smart double quotes and apostrophe. These special characters are not allowed in meta descriptions."; - } else if (/[\u2018\u2019\u201A]/.test(meta_description)) { - message = - "Found Microsoft Smart single quotes and apostrophe. These special characters are not allowed in meta descriptions."; - } - - setError(message); - } - }, [meta_description]); - - if (error) { - dispatch( - notify({ - kind: "warn", - message: error, - }) - ); - } - - return ( -
- - onChange(evt.target.value, "metaDescription")} - multiline - rows={6} - /> - -
- ); -}); diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaDescription.less b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaDescription.less deleted file mode 100644 index 4e94f5b229..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaDescription.less +++ /dev/null @@ -1,13 +0,0 @@ -@import "~@zesty-io/core/colors.less"; - -.MetaDescription { - display: flex; - flex-direction: column; - label { - display: flex; - } - .error { - background-color: @zesty-highlight; - padding: 8px; - } -} diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaKeywords.less b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaKeywords.less deleted file mode 100644 index 5e8029ecc2..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaKeywords.less +++ /dev/null @@ -1,7 +0,0 @@ -.MetaKeywords { - display: flex; - flex-direction: column; - label { - display: flex; - } -} diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaLinkText.js b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaLinkText.js deleted file mode 100644 index 07fe483b7d..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaLinkText.js +++ /dev/null @@ -1,37 +0,0 @@ -import { memo } from "react"; - -import { TextField } from "@mui/material"; -import { FieldShell } from "../../../../../components/Editor/Field/FieldShell"; -import { MaxLengths } from "../ItemSettings"; - -import styles from "./MetaLinkText.less"; -export const MetaLinkText = memo(function MetaLinkText({ - meta_link_text, - onChange, - errors, -}) { - return ( -
- - onChange(evt.target.value, "metaLinkText")} - /> - -
- ); -}); diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaLinkText.less b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaLinkText.less deleted file mode 100644 index ddbb8b4eda..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaLinkText.less +++ /dev/null @@ -1,7 +0,0 @@ -.MetaLinkText { - display: flex; - flex-direction: column; - label { - display: flex; - } -} diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaTitle.js b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaTitle.js deleted file mode 100644 index 65dd215ff8..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaTitle.js +++ /dev/null @@ -1,35 +0,0 @@ -import { memo } from "react"; - -import { TextField } from "@mui/material"; - -import { FieldShell } from "../../../../../components/Editor/Field/FieldShell"; -import { MaxLengths } from "../ItemSettings"; -import styles from "./MetaTitle.less"; -export const MetaTitle = memo(function MetaTitle({ - meta_title, - onChange, - errors, -}) { - return ( -
- - onChange(evt.target.value, "metaTitle")} - /> - -
- ); -}); diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaTitle.less b/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaTitle.less deleted file mode 100644 index 7ed0d77cec..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaTitle.less +++ /dev/null @@ -1,7 +0,0 @@ -.MetaTitle { - display: flex; - flex-direction: column; - label { - display: flex; - } -} diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/Meta.js b/src/apps/content-editor/src/app/views/ItemEdit/Meta/Meta.js deleted file mode 100644 index 34f572cf6e..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/Meta.js +++ /dev/null @@ -1,26 +0,0 @@ -import { Header } from "../components/Header"; - -import { ItemSettings } from "./ItemSettings"; -import { DataSettings } from "./ItemSettings/DataSettings"; - -import styles from "./Meta.less"; -export function Meta(props) { - return ( -
-
- {props.model && props.model?.type === "dataset" ? ( - - ) : ( - - )} -
-
- ); -} diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/Meta.less b/src/apps/content-editor/src/app/views/ItemEdit/Meta/Meta.less deleted file mode 100644 index f73ba1b173..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/Meta.less +++ /dev/null @@ -1,10 +0,0 @@ -.MetaEdit { - display: flex; - flex: 1; - background-color: #f9fafb; - overflow: scroll; - flex-direction: column; - .MetaWrap { - margin: 4px 32px 20px 32px; - } -} diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/FacebookPreview.tsx b/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/FacebookPreview.tsx new file mode 100644 index 0000000000..13f5803f57 --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/FacebookPreview.tsx @@ -0,0 +1,80 @@ +import { useEffect } from "react"; +import { Typography, Box, Stack } from "@mui/material"; +import { ImageRounded } from "@mui/icons-material"; +import { useLocation, useParams } from "react-router"; +import { useSelector } from "react-redux"; + +import { useDomain } from "../../../../../../../../shell/hooks/use-domain"; +import { AppState } from "../../../../../../../../shell/store/types"; +import { useImageURL } from "./useImageURL"; + +type FacebookPreviewProps = {}; +export const FacebookPreview = ({}: FacebookPreviewProps) => { + const [imageURL, setImageDimensions] = useImageURL(); + const { itemZUID, modelZUID } = useParams<{ + itemZUID: string; + modelZUID: string; + }>(); + const domain = useDomain(); + const location = useLocation(); + const isCreateItemPage = location?.pathname?.split("/")?.pop() === "new"; + const item = useSelector( + (state: AppState) => + state.content[isCreateItemPage ? `new:${modelZUID}` : itemZUID] + ); + + useEffect(() => { + setImageDimensions({ height: 290, type: "fit" }); + }, []); + + return ( + + {!!imageURL ? ( + theme.palette.grey[100], + objectFit: "cover", + alignSelf: "center", + }} + height={290} + width="100%" + src={imageURL} + flexShrink={0} + /> + ) : ( + + + + )} + + + {domain.replace(/http:\/\/|https:\/\//gm, "")} + + + {item?.web?.metaTitle || "Meta Title"} + + + + ); +}; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/GooglePreview.tsx b/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/GooglePreview.tsx new file mode 100644 index 0000000000..37dcdb97ff --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/GooglePreview.tsx @@ -0,0 +1,173 @@ +import { Fragment, useMemo, useEffect } from "react"; +import { Box, Stack, Typography, Breadcrumbs } from "@mui/material"; +import { + MoreVertRounded, + ArrowForwardIosRounded, + ImageRounded, +} from "@mui/icons-material"; +import { useSelector } from "react-redux"; +import { useLocation, useParams } from "react-router"; + +import { useGetInstanceQuery } from "../../../../../../../../shell/services/accounts"; +import { InstanceAvatar } from "../../../../../../../../shell/components/global-sidebar/components/InstanceAvatar"; +import { useDomain } from "../../../../../../../../shell/hooks/use-domain"; +import { AppState } from "../../../../../../../../shell/store/types"; +import { useGetContentModelFieldsQuery } from "../../../../../../../../shell/services/instance"; +import { useImageURL } from "./useImageURL"; + +type GooglePreviewProps = {}; +export const GooglePreview = ({}: GooglePreviewProps) => { + const { modelZUID, itemZUID } = useParams<{ + modelZUID: string; + itemZUID: string; + }>(); + const { data: instance, isLoading: isLoadingInstance } = + useGetInstanceQuery(); + const domain = useDomain(); + const location = useLocation(); + const isCreateItemPage = location?.pathname?.split("/")?.pop() === "new"; + const [imageURL, setImageDimensions] = useImageURL(); + const { data: modelFields } = useGetContentModelFieldsQuery(modelZUID); + const items = useSelector((state: AppState) => state.content); + const item = items[isCreateItemPage ? `new:${modelZUID}` : itemZUID]; + const parent = items[item?.web?.parentZUID]; + + const fullPathArray = useMemo(() => { + let path: string[] = [domain]; + + if (parent) { + path = [ + ...path, + ...(parent.web?.path?.split("/") || []), + item?.web?.pathPart, + ]; + } else { + path = [...path, item?.web?.pathPart]; + } + + // Remove empty strings + return path.filter((i) => !!i); + }, [domain, parent, item?.web]); + + useEffect(() => { + setImageDimensions({ width: 82, height: 82 }); + }, []); + + return ( + + + + + + + {instance?.name} + + + + {fullPathArray.map((path, index) => ( + + + {path} + + {index < fullPathArray?.length - 1 && ( + + )} + + ))} + + theme.palette.text.secondary, + }} + /> + + + + + {item?.web?.metaTitle || "Meta Title"} + + + {item?.web?.metaDescription || "Meta Description"} + + + {!!imageURL ? ( + theme.palette.grey[100], + objectFit: "cover", + }} + width={82} + height={82} + src={imageURL} + flexShrink={0} + borderRadius={2} + /> + ) : ( + + + + )} + + ); +}; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/LinkedInPreview.tsx b/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/LinkedInPreview.tsx new file mode 100644 index 0000000000..23c2292cd5 --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/LinkedInPreview.tsx @@ -0,0 +1,80 @@ +import { useEffect } from "react"; +import { Typography, Box, Stack } from "@mui/material"; +import { ImageRounded } from "@mui/icons-material"; +import { useLocation, useParams } from "react-router"; +import { useSelector } from "react-redux"; + +import { useDomain } from "../../../../../../../../shell/hooks/use-domain"; +import { AppState } from "../../../../../../../../shell/store/types"; +import { useImageURL } from "./useImageURL"; + +type LinkedInPreviewProps = {}; +export const LinkedInPreview = ({}: LinkedInPreviewProps) => { + const [imageURL, setImageDimensions] = useImageURL(); + const { itemZUID, modelZUID } = useParams<{ + itemZUID: string; + modelZUID: string; + }>(); + const domain = useDomain(); + const location = useLocation(); + const isCreateItemPage = location?.pathname?.split("/")?.pop() === "new"; + const item = useSelector( + (state: AppState) => + state.content[isCreateItemPage ? `new:${modelZUID}` : itemZUID] + ); + + useEffect(() => { + setImageDimensions({ height: 290, type: "fit" }); + }, []); + + return ( + + {!!imageURL ? ( + theme.palette.grey[100], + objectFit: "cover", + alignSelf: "center", + }} + height={290} + width="100%" + src={imageURL} + flexShrink={0} + /> + ) : ( + + + + )} + + + {item?.web?.metaTitle || "Meta Title"} + + + {domain.replace(/http:\/\/|https:\/\//gm, "")} + + + + ); +}; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/TwitterPreview.tsx b/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/TwitterPreview.tsx new file mode 100644 index 0000000000..ef4ae40e52 --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/TwitterPreview.tsx @@ -0,0 +1,114 @@ +import { useEffect } from "react"; +import { Typography, Box, Stack } from "@mui/material"; +import { ImageRounded } from "@mui/icons-material"; +import { useLocation, useParams } from "react-router"; +import { useSelector } from "react-redux"; + +import { useDomain } from "../../../../../../../../shell/hooks/use-domain"; +import { AppState } from "../../../../../../../../shell/store/types"; +import { useImageURL } from "./useImageURL"; + +type TwitterPreviewProps = {}; +export const TwitterPreview = ({}: TwitterPreviewProps) => { + const [imageURL, setImageDimensions] = useImageURL(); + const { itemZUID, modelZUID } = useParams<{ + itemZUID: string; + modelZUID: string; + }>(); + const domain = useDomain(); + const location = useLocation(); + const isCreateItemPage = location?.pathname?.split("/")?.pop() === "new"; + const item = useSelector( + (state: AppState) => + state.content[isCreateItemPage ? `new:${modelZUID}` : itemZUID] + ); + + useEffect(() => { + setImageDimensions({ width: 128, height: 128 }); + }, []); + + return ( + + {!!imageURL ? ( + theme.palette.grey[100], + objectFit: "cover", + }} + width={128} + height={128} + src={imageURL} + flexShrink={0} + /> + ) : ( + + + + )} + + + {domain.replace(/http:\/\/|https:\/\//gm, "")} + + + {item?.web?.metaTitle || "Meta Title"} + + + {item?.web?.metaDescription || "Meta Description"} + + + + ); +}; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/index.tsx b/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/index.tsx new file mode 100644 index 0000000000..67a3fa847c --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/index.tsx @@ -0,0 +1,69 @@ +import { useState } from "react"; +import { Tab, Tabs, Box } from "@mui/material"; +import { Google, Twitter, Facebook, LinkedIn } from "@mui/icons-material"; +import { GooglePreview } from "./GooglePreview"; +import { TwitterPreview } from "./TwitterPreview"; +import { FacebookPreview } from "./FacebookPreview"; +import { LinkedInPreview } from "./LinkedInPreview"; + +enum SocialMediaTab { + Google, + Twitter, + Facebook, + LinkedIn, +} +type SocialMediaPreviewProps = {}; +export const SocialMediaPreview = ({}: SocialMediaPreviewProps) => { + const [activeTab, setActiveTab] = useState( + SocialMediaTab.Google + ); + + return ( + <> + `2px solid ${theme?.palette?.border} `, + }} + > + setActiveTab(value)} + sx={{ + position: "relative", + top: "2px", + }} + > + } + iconPosition="start" + label="Google" + value={SocialMediaTab.Google} + /> + } + iconPosition="start" + label="Twitter (X)" + value={SocialMediaTab.Twitter} + /> + } + iconPosition="start" + label="Facebook" + value={SocialMediaTab.Facebook} + /> + } + iconPosition="start" + label="LinkedIn" + value={SocialMediaTab.LinkedIn} + /> + + + {activeTab === SocialMediaTab.Google && } + {activeTab === SocialMediaTab.Twitter && } + {activeTab === SocialMediaTab.Facebook && } + {activeTab === SocialMediaTab.LinkedIn && } + + ); +}; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/useImageURL.ts b/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/useImageURL.ts new file mode 100644 index 0000000000..ca4e913d5b --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/SocialMediaPreview/useImageURL.ts @@ -0,0 +1,83 @@ +import { useMemo, useState } from "react"; +import { useSelector } from "react-redux"; +import { useLocation, useParams } from "react-router"; +import { useGetContentModelFieldsQuery } from "../../../../../../../../shell/services/instance"; +import { AppState } from "../../../../../../../../shell/store/types"; + +type ImageDimension = { + type?: "crop" | "fit"; + width?: number; + height?: number; +}; +type UseImageURLProps = [string, (imageDimensions: ImageDimension) => void]; +export const useImageURL: () => UseImageURLProps = () => { + const { modelZUID, itemZUID } = useParams<{ + modelZUID: string; + itemZUID: string; + }>(); + const { data: modelFields } = useGetContentModelFieldsQuery(modelZUID); + const location = useLocation(); + const isCreateItemPage = location?.pathname?.split("/")?.pop() === "new"; + const item = useSelector( + (state: AppState) => + state.content[isCreateItemPage ? `new:${modelZUID}` : itemZUID] + ); + const [imageDimensions, setImageDimensions] = useState({}); + + const imageURL: string = useMemo(() => { + if (!item?.data || !modelFields?.length) return; + + let matchedURL: string | null = null; + + if ("og_image" in item?.data) { + matchedURL = !!item?.data?.["og_image"] + ? (item?.data?.["og_image"] as string) + : null; + } else { + // Find possible image fields that can be used + const matchedFields = modelFields.filter( + (field) => + !field.deletedAt && + field.datatype === "images" && + field?.name !== "og_image" && + (field.label.toLowerCase().includes("image") || + field.name.toLocaleLowerCase().includes("image")) + ); + + if (matchedFields?.length) { + // Find the first matched field that already stores an image and make sure + // to find the first valid image in that field + matchedFields.forEach((field) => { + if (!matchedURL && !!item?.data?.[field.name]) { + matchedURL = String(item?.data?.[field.name])?.split(",")?.[0]; + } + }); + } + } + + if (matchedURL?.startsWith("3-")) { + const params = { + type: imageDimensions?.type || "crop", + ...(!!imageDimensions?.width && { + w: String(imageDimensions.width), + }), + ...(!!imageDimensions?.height && { + h: String(imageDimensions.height), + }), + }; + const url = new URL( + `${ + // @ts-ignore + CONFIG.SERVICE_MEDIA_RESOLVER + }/resolve/${matchedURL}/getimage/` + ); + url.search = new URLSearchParams(params)?.toString(); + + return url.toString(); + } else { + return matchedURL; + } + }, [item?.data, modelFields, imageDimensions]); + + return [imageURL, setImageDimensions]; +}; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/index.js b/src/apps/content-editor/src/app/views/ItemEdit/Meta/index.js deleted file mode 100644 index f75005249b..0000000000 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/index.js +++ /dev/null @@ -1 +0,0 @@ -export { Meta } from "./Meta"; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/index.tsx b/src/apps/content-editor/src/app/views/ItemEdit/Meta/index.tsx new file mode 100644 index 0000000000..386d07849d --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/index.tsx @@ -0,0 +1,242 @@ +import { useState, useCallback, useMemo, useEffect } from "react"; +import { Stack, Box, Typography, ThemeProvider, Divider } from "@mui/material"; +import { theme } from "@zesty-io/material"; +import { useParams, useLocation } from "react-router"; +import { useSelector, useDispatch } from "react-redux"; + +import { ContentInsights } from "./ContentInsights"; +import { useGetContentModelQuery } from "../../../../../../../shell/services/instance"; +import { AppState } from "../../../../../../../shell/store/types"; +import { Error } from "../../../components/Editor/Field/FieldShell"; +import { fetchGlobalItem } from "../../../../../../../shell/store/content"; + +// Fields +import { MetaImage } from "./settings/MetaImage"; +import { CanonicalTag } from "./settings/CanonicalTag"; +import { ItemParent } from "./settings/ItemParent"; +import { ItemRoute } from "./settings/ItemRoute"; +import MetaDescription from "./settings/MetaDescription"; +import { MetaKeywords } from "./settings/MetaKeywords"; +import { MetaLinkText } from "./settings/MetaLinkText"; +import { MetaTitle } from "./settings/MetaTitle"; +import { SitemapPriority } from "./settings/SitemapPriority"; +import { cloneDeep } from "lodash"; +import { SocialMediaPreview } from "./SocialMediaPreview"; + +export const MaxLengths: Record = { + metaLinkText: 150, + metaTitle: 150, + metaDescription: 160, + metaKeywords: 255, +}; +const REQUIRED_FIELDS = [ + "metaTitle", + "metaDescription", + "parentZUID", + "pathPart", +] as const; + +type Errors = Record; +type MetaProps = { + isSaving: boolean; + onUpdateSEOErrors: (hasErrors: boolean) => void; +}; +export const Meta = ({ isSaving, onUpdateSEOErrors }: MetaProps) => { + const dispatch = useDispatch(); + const location = useLocation(); + const isCreateItemPage = location?.pathname?.split("/")?.pop() === "new"; + const { modelZUID, itemZUID } = useParams<{ + modelZUID: string; + itemZUID: string; + }>(); + const { data: model } = useGetContentModelQuery(modelZUID, { + skip: !modelZUID, + }); + const { meta, data, web } = useSelector( + (state: AppState) => + state.content[isCreateItemPage ? `new:${modelZUID}` : itemZUID] + ); + const [errors, setErrors] = useState({}); + + // @ts-expect-error untyped + const siteName = useMemo(() => dispatch(fetchGlobalItem())?.site_name, []); + + const handleOnChange = useCallback( + (value, name) => { + if (!name) { + throw new Error("Input is missing name attribute"); + } + + const currentErrors = cloneDeep(errors); + + if (REQUIRED_FIELDS.includes(name)) { + currentErrors[name] = { + ...currentErrors?.[name], + MISSING_REQUIRED: !value, + }; + } + + if (MaxLengths[name]) { + currentErrors[name] = { + ...currentErrors?.[name], + EXCEEDING_MAXLENGTH: + value?.length > MaxLengths[name] + ? value?.length - MaxLengths[name] + : 0, + }; + } + + setErrors(currentErrors); + + dispatch({ + // The og_image is stored as an ordinary field item and not a SEO field item + type: name === "og_image" ? "SET_ITEM_DATA" : "SET_ITEM_WEB", + itemZUID: meta?.ZUID, + key: name, + value: value, + }); + }, + [meta?.ZUID, errors] + ); + + useEffect(() => { + if (isSaving) { + setErrors({}); + return; + } + }, [isSaving]); + + useEffect(() => { + const hasErrors = Object.values(errors) + ?.map((error) => { + return Object.values(error) ?? []; + }) + ?.flat() + .some((error) => !!error); + + onUpdateSEOErrors(hasErrors); + }, [errors]); + + return ( + + + + + + + SEO & Open Graph Settings + + + Specify this page's title and description. You can see how + they'll look in search engine results pages (SERPs) and social + media content in the preview on the right. + + + + + + + {model?.type !== "dataset" && web?.pathPart !== "zesty_home" && ( + + + + URL Settings + + + Define the URL of your web page + + + + { + setErrors({ + ...errors, + [name]: { + ...errors?.[name], + ...error, + }, + }); + }} + /> + + )} + + + + Advanced Settings + + + Optimize your content item's SEO further + + + {model?.type !== "dataset" && ( + <> + + {!!web && ( + + )} + + )} + + + + + {model?.type !== "dataset" && !isCreateItemPage && ( + + + + + + )} + + + ); +}; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/CanonicalTag.js b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/CanonicalTag.js similarity index 96% rename from src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/CanonicalTag.js rename to src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/CanonicalTag.js index b6f5f4982e..2bcb1aebe5 100644 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/CanonicalTag.js +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/CanonicalTag.js @@ -2,7 +2,7 @@ import { memo, useState } from "react"; import { TextField, Select, MenuItem } from "@mui/material"; -import { FieldShell } from "../../../../../components/Editor/Field/FieldShell"; +import { FieldShell } from "../../../../components/Editor/Field/FieldShell"; const CANONICAL_OPTS = [ { @@ -52,7 +52,7 @@ export const CanonicalTag = memo(function CanonicalTag(props) { settings={{ label: "Canonical Tag", }} - customTooltip="Canonical tags help search engines understand authoritative links and can help prevent duplicate content issues. Zesty.io auto creates tags on demand based on your settings." + customTooltip="Canonical tags help search engines understand authoritative links and can help prevent duplicate content issues. Zesty.io auto-creates tags on demand based on your settings." withInteractiveTooltip={false} > {zestyStore.getState().instance.settings.seo[ diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/CanonicalTag.less b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/CanonicalTag.less similarity index 100% rename from src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/CanonicalTag.less rename to src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/CanonicalTag.less diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/ItemParent.tsx b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/ItemParent.tsx new file mode 100644 index 0000000000..2d6ef2d1ad --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/ItemParent.tsx @@ -0,0 +1,258 @@ +import { Autocomplete, TextField, ListItem } from "@mui/material"; +import { FieldShell } from "../../../../components/Editor/Field/FieldShell"; +import { useDispatch, useSelector } from "react-redux"; +import { AppState } from "../../../../../../../../shell/store/types"; +import { + ContentItemWithDirtyAndPublishing, + ContentNavItem, +} from "../../../../../../../../shell/services/types"; +import { debounce, uniqBy } from "lodash"; +import { useEffect, useState } from "react"; +import { useLocation, useParams } from "react-router"; +import { notify } from "../../../../../../../../shell/store/notifications"; +import { searchItems } from "../../../../../../../../shell/store/content"; +import { useGetContentNavItemsQuery } from "../../../../../../../../shell/services/instance"; + +type ParentOption = { + value: string; + text: string; +}; +const getParentOptions = ( + currentItemLangID: number, + path: string, + items: Record +) => { + const options: ParentOption[] = Object.entries(items) + ?.reduce((acc, [itemZUID, itemData]) => { + if ( + itemZUID.slice(0, 3) !== "new" && // Exclude new items + itemData?.meta?.ZUID && // must have a ZUID + itemData?.web?.path && // must have a path + itemData?.web.path !== "/" && // Exclude homepage + itemData?.web.path !== path && // Exclude current item + itemData?.meta?.langID === currentItemLangID // display only relevant language options + ) { + acc.push({ + value: itemZUID, + text: itemData.web.path, + }); + } + + return acc; + }, []) + .sort((a, b) => { + if (a.text > b.text) { + return 1; + } else if (a.text < b.text) { + return -1; + } else { + return 0; + } + }); + + // Insert the home route + options.unshift({ + text: "/", + value: "0", // 0 = root level + }); + + return uniqBy(options, "value"); +}; + +const findNavParent = (zuid: string, nav: ContentNavItem[], count = 0): any => { + count++; + const navEntry = nav?.find((el: any) => el.ZUID === zuid); + if (navEntry) { + // This first item should be the model we are resolving for so + // continue on up the nav tree + if (count === 0) { + return findNavParent(navEntry.parentZUID, nav, count); + } else { + if (navEntry.type === "item") { + return navEntry.ZUID; + } else if (navEntry.parentZUID) { + return findNavParent(navEntry.parentZUID, nav, count); + } else { + return "0"; + } + } + } else { + return "0"; + } +}; + +type ItemParentProps = { + onChange: (value: string, name: string) => void; +}; +export const ItemParent = ({ onChange }: ItemParentProps) => { + const { modelZUID, itemZUID } = useParams<{ + modelZUID: string; + itemZUID: string; + }>(); + const dispatch = useDispatch(); + const location = useLocation(); + const isCreateItemPage = location?.pathname?.split("/")?.pop() === "new"; + const items = useSelector((state: AppState) => state.content); + const item = items[isCreateItemPage ? `new:${modelZUID}` : itemZUID]; + const { data: rawNavData } = useGetContentNavItemsQuery(); + const [selectedParent, setSelectedParent] = useState({ + value: "0", // "0" = root level route + text: "/", + }); + const [options, setOptions] = useState( + getParentOptions(item?.meta?.langID, item?.web?.path, items) + ); + const [isLoadingOptions, setIsLoadingOptions] = useState(false); + + const handleSearchOptions = debounce((filterTerm) => { + if (filterTerm) { + dispatch(searchItems(filterTerm)) + // @ts-expect-error untyped + .then((res) => { + setOptions( + getParentOptions(item?.meta?.langID, item?.web?.path, { + ...items, + // needs to reduce and converts this data as the same format of the items to + // prevent having an issue on having an itemZUID with an incorrect format + // the reason is that the item has a format of {[itemZUID]:data} + // while the res.data has a value of an array which cause the needs of converting + // the response to an object with a zuid as a key + ...res?.data.reduce( + ( + acc: Record, + curr: ContentItemWithDirtyAndPublishing + ) => { + return { ...acc, [curr.meta.ZUID]: curr }; + }, + {} + ), + }) + ); + }) + .finally(() => setIsLoadingOptions(false)); + } + }, 1000); + + useEffect(() => { + let { parentZUID } = item?.web; + const { ZUID: itemZUID } = item?.meta; + + // If it's a new item chase down the parentZUID within navigation + // This way we avoid an API request + if (itemZUID && itemZUID.slice(0, 3) === "new") { + const result = findNavParent(modelZUID, rawNavData); + + // change for preselection + parentZUID = result; + + // Update redux store so if the item is saved we know it's parent + onChange(parentZUID, "parentZUID"); + } + + // Try to preselect parent + if (parentZUID && parentZUID !== "0") { + const parentItem = items[parentZUID]; + if (parentItem?.meta?.ZUID && parentItem?.web?.path) { + setSelectedParent({ + value: parentItem.meta.ZUID, + text: parentItem.web.path, + }); + } else { + dispatch(searchItems(parentZUID)) + // @ts-expect-error untyped + .then((res) => { + if (res?.data) { + if (Array.isArray(res.data) && res.data.length) { + // Handles cases where the model's parent is the homepage. This is no longer possible for newly created models but + // there are some old models that still have the homepage as their parent models. + if (res.data[0]?.web?.path === "/") { + setSelectedParent({ + value: "0", // "0" = root level route + text: "/", + }); + } else { + setSelectedParent({ + value: res.data?.[0]?.meta?.ZUID, + text: res.data?.[0]?.web?.path, + }); + /** + * // HACK Because we pre-load all item publishings and store them in the same reducer as the `content` + * we can't use array length comparision to determine a new parent has been added. Also since updates to the item + * currently being edited cause a new `content` object to be created in it's reducer we can't use + * referential equality checks to determine re-rendering. This scenario causes either the parent to not be pre-selected + * or a performance issue. To work around this we maintain the `parents` state internal and add the new parent we load from the + * API to allow it to be pre-selected while avoiding re-renders on changes to this item. + */ + + setOptions( + getParentOptions(item?.meta?.langID, item?.web?.path, { + ...items, + [res.data[0].meta.ZUID]: res.data[0], + }) + ); + } + } else { + dispatch( + notify({ + kind: "warn", + heading: `Cannot Save: ${item?.web?.metaTitle}`, + message: `Page's Parent does not exist or has been deleted`, + }) + ); + } + } else { + dispatch( + notify({ + kind: "warn", + message: `API failed to return data. Try Again.`, + }) + ); + } + }); + } + } + }, []); + + return ( + + } + renderOption={(props, value) => ( + + {value.text} + + )} + getOptionLabel={(option) => option.text} + onInputChange={(_, filterTerm) => { + if (filterTerm !== "/") { + setIsLoadingOptions(!!filterTerm); + handleSearchOptions(filterTerm); + } + }} + onChange={(_, value) => { + // Always default to homepage when no parent is selected + setSelectedParent(value !== null ? value : { text: "/", value: "0" }); + onChange(value !== null ? value.value : "0", "parentZUID"); + }} + loading={isLoadingOptions} + sx={{ + "& .MuiOutlinedInput-root": { + padding: "2px", + }, + }} + /> + + ); +}; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/ItemRoute.tsx b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/ItemRoute.tsx new file mode 100644 index 0000000000..b0c3e64efd --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/ItemRoute.tsx @@ -0,0 +1,199 @@ +import { ChangeEvent, useCallback, useEffect, useState } from "react"; +import { + TextField, + InputAdornment, + CircularProgress, + Typography, +} from "@mui/material"; +import { useDispatch, useSelector } from "react-redux"; +import { useLocation, useParams } from "react-router"; +import { debounce } from "lodash"; +import { CheckCircleRounded, CancelRounded } from "@mui/icons-material"; + +import { withCursorPosition } from "../../../../../../../../shell/components/withCursorPosition"; +import { FieldShell } from "../../../../components/Editor/Field/FieldShell"; +import { searchItems } from "../../../../../../../../shell/store/content"; +import { notify } from "../../../../../../../../shell/store/notifications"; +import { AppState } from "../../../../../../../../shell/store/types"; +import { ContentItemWithDirtyAndPublishing } from "../../../../../../../../shell/services/types"; +import { useDomain } from "../../../../../../../../shell/hooks/use-domain"; +import { Error } from "../../../../components/Editor/Field/FieldShell"; +import { hasErrors } from "./util"; + +const TextFieldWithCursorPosition = withCursorPosition(TextField); + +type ItemRouteProps = { + onChange: (value: string, name: string) => void; + error: Error; + onUpdateErrors: (name: string, error: Error) => void; +}; +export const ItemRoute = ({ + onChange, + error, + onUpdateErrors, +}: ItemRouteProps) => { + const dispatch = useDispatch(); + const { itemZUID, modelZUID } = useParams<{ + itemZUID: string; + modelZUID: string; + }>(); + const domain = useDomain(); + const location = useLocation(); + const isCreateItemPage = location?.pathname?.split("/")?.pop() === "new"; + const items = useSelector((state: AppState) => state.content); + const item = items[isCreateItemPage ? `new:${modelZUID}` : itemZUID]; + const [pathPart, setPathPart] = useState(item?.web?.pathPart); + const [isLoading, setIsLoading] = useState(false); + const [isUnique, setIsUnique] = useState(true); + + const parent = items[item?.web?.parentZUID]; + + const validate = useCallback( + debounce((path) => { + if (!path) { + setIsUnique(false); + return; + } + + const fullPath = parent ? `${parent.web?.path}${path}/` : `/${path}/`; + + setIsLoading(true); + + return ( + dispatch(searchItems(fullPath)) + // @ts-expect-error untyped + .then((res) => { + if (res?.data) { + if (Array.isArray(res.data) && res.data.length) { + // check list of partial matches for exact path match + const matches = res.data.filter( + (_item: ContentItemWithDirtyAndPublishing) => { + /** + * Exclude currently viewed item zuid, as it's currently saved path would match. + * Check if other results have a matching path, if so then it is already taken and + * can not be used. + * Result paths come with leading and trailing slashes + */ + return ( + _item.meta.ZUID !== item?.meta?.ZUID && + _item.web.path === fullPath + ); + } + ); + + setIsUnique(!matches.length); + onUpdateErrors("pathPart", { + CUSTOM_ERROR: !!matches.length + ? "This URL Path Part is already taken. Please enter a new different URL Path part." + : "", + }); + } else { + setIsUnique(true); + onUpdateErrors("pathPart", { + CUSTOM_ERROR: "", + }); + } + } else { + dispatch( + notify({ + kind: "warn", + message: `API failed to return data ${res.status}`, + }) + ); + } + }) + .finally(() => setIsLoading(false)) + ); + }, 1000), + [parent] + ); + + const handleInputChange = (evt: ChangeEvent) => { + // All URLs are lowercased + // Replace ampersand characters with 'and' + // Only allow alphanumeric characters + const path = evt.target.value + .trim() + .toLowerCase() + .replace(/\&/g, "and") + .replace(/[^a-zA-Z0-9]/g, "-"); + + validate(path); + setPathPart(path); + + onChange(path, "pathPart"); + }; + + // Revalidate when parent path changes + useEffect(() => { + validate(pathPart); + }, [parent?.web, pathPart]); + + useEffect(() => { + setPathPart(item?.web?.pathPart || ""); + }, [item?.web]); + + return ( + + + ), + }} + helperText={ + !!pathPart && + isUnique && ( + + {domain} + {parent ? parent.web?.path + pathPart : `/${pathPart}`} + + ) + } + error={hasErrors(error)} + /> + + ); +}; + +type AdornmentProps = { + isLoading: boolean; + isUnique: boolean; +}; +const Adornment = ({ isLoading, isUnique }: AdornmentProps) => { + if (isLoading) { + return ( + + + + ); + } + + return ( + + {isUnique ? ( + + ) : ( + + )} + + ); +}; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/MetaDescription.tsx b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/MetaDescription.tsx new file mode 100644 index 0000000000..48da1becb8 --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/MetaDescription.tsx @@ -0,0 +1,94 @@ +import { useState, useEffect } from "react"; +import { connect, useDispatch } from "react-redux"; +import { TextField, Box } from "@mui/material"; + +import { notify } from "../../../../../../../../shell/store/notifications"; +import { FieldShell } from "../../../../components/Editor/Field/FieldShell"; +import { MaxLengths } from ".."; +import { hasErrors } from "./util"; +import { Error } from "../../../../components/Editor/Field/FieldShell"; + +type MetaDescriptionProps = { + value: string; + onChange: (value: string, name: string) => void; + error: Error; +}; +export default connect()(function MetaDescription({ + value, + onChange, + error, +}: MetaDescriptionProps) { + const dispatch = useDispatch(); + const [contentValidationError, setContentValidationError] = useState(""); + + useEffect(() => { + if (value) { + let message = ""; + + if (!(value.indexOf("\u0152") === -1)) { + message = + "Found OE ligature. These special characters are not allowed in meta descriptions."; + } else if (!(value.indexOf("\u0153") === -1)) { + message = + "Found oe ligature. These special characters are not allowed in meta descriptions."; + } else if (!(value.indexOf("\xAB") === -1)) { + message = + "Found << character. These special characters are not allowed in meta descriptions."; + } else if (!(value.indexOf("\xBB") === -1)) { + message = + "Found >> character. These special characters are not allowed in meta descriptions."; + } else if (/[\u201C\u201D\u201E]/.test(value)) { + message = + "Found Microsoft smart double quotes and apostrophe. These special characters are not allowed in meta descriptions."; + } else if (/[\u2018\u2019\u201A]/.test(value)) { + message = + "Found Microsoft Smart single quotes and apostrophe. These special characters are not allowed in meta descriptions."; + } + + setContentValidationError(message); + } + }, [value]); + + if (contentValidationError) { + dispatch( + notify({ + kind: "warn", + message: contentValidationError, + }) + ); + } + + return ( + + + onChange(evt.target.value, "metaDescription")} + multiline + rows={6} + error={hasErrors(error) || !!contentValidationError} + /> + + + ); +}); diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/MetaImage.tsx b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/MetaImage.tsx new file mode 100644 index 0000000000..29b0502328 --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/MetaImage.tsx @@ -0,0 +1,281 @@ +import { useState, useEffect, useMemo, useRef } from "react"; +import { Dialog, IconButton, Stack } from "@mui/material"; +import { LoadingButton } from "@mui/lab"; +import { AddRounded, Close, EditRounded } from "@mui/icons-material"; +import { MemoryRouter, useLocation, useParams } from "react-router"; +import { useDispatch, useSelector } from "react-redux"; + +import { FieldShell } from "../../../../components/Editor/Field/FieldShell"; +import { AppState } from "../../../../../../../../shell/store/types"; +import { + useCreateContentModelFieldMutation, + useGetContentModelFieldsQuery, + useUndeleteContentModelFieldMutation, +} from "../../../../../../../../shell/services/instance"; +import { fetchItem } from "../../../../../../../../shell/store/content"; +import { + FieldTypeMedia, + MediaItem, +} from "../../../../components/FieldTypeMedia"; +import { MediaApp } from "../../../../../../../media/src/app"; + +type MetaImageProps = { + onChange: (value: string, name: string) => void; +}; +export const MetaImage = ({ onChange }: MetaImageProps) => { + const dispatch = useDispatch(); + const location = useLocation(); + const isCreateItemPage = location?.pathname?.split("/")?.pop() === "new"; + const { modelZUID, itemZUID } = useParams<{ + modelZUID: string; + itemZUID: string; + }>(); + const item = useSelector( + (state: AppState) => + state.content[isCreateItemPage ? `new:${modelZUID}` : itemZUID] + ); + const fieldTypeMedia = useRef(null); + const { data: modelFields } = useGetContentModelFieldsQuery(modelZUID); + const [ + createContentModelField, + { + isLoading: isCreatingOgImageField, + isSuccess: isOgImageFieldCreated, + error: ogImageFieldCreationError, + }, + ] = useCreateContentModelFieldMutation(); + const [ + undeleteContentModelField, + { isLoading: isUndeletingField, isSuccess: isFieldUndeleted }, + ] = useUndeleteContentModelFieldMutation(); + const [imageModal, setImageModal] = useState(null); + const [autoOpenMediaBrowser, setAutoOpenMediaBrowser] = useState(false); + + const isBynderSessionValid = + localStorage.getItem("cvrt") && localStorage.getItem("cvad"); + + const usableTemporaryMetaImage = useMemo(() => { + if (modelFields?.length) { + const matchedFields = modelFields.filter( + (field) => + !field.deletedAt && + field.datatype === "images" && + field?.name !== "og_image" && + (field.label.toLowerCase().includes("image") || + field.name.toLocaleLowerCase().includes("image")) + ); + let image = ""; + + // Find the first matched field that already stores an image + matchedFields?.forEach((field) => { + if (!image && !!item?.data?.[field.name]) { + image = String(item?.data?.[field.name]); + } + }); + + return image?.split(",")?.[0]; + } + }, [modelFields, item]); + + const handleCreateOgImageField = () => { + const existingOgImageField = modelFields?.find( + (field) => field.name === "og_image" + ); + + if (!!existingOgImageField && !!existingOgImageField.deletedAt) { + // If the og_image field already exists in the model but was deactivated, reactivate it + undeleteContentModelField({ + modelZUID, + fieldZUID: existingOgImageField.ZUID, + }); + } else { + // If the model has no og_image field yet, create it + createContentModelField({ + modelZUID, + body: { + contentModelZUID: modelZUID, + datatype: "images", + 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.", + label: "Meta Image", + name: "og_image", + required: false, + settings: { + defaultValue: null, + group_id: "", + limit: 1, + list: false, + }, + sort: modelFields?.length, // Adds it to the end of the current model's field list + }, + }); + } + }; + + useEffect(() => { + if ( + (!isCreatingOgImageField && isOgImageFieldCreated) || + (!isUndeletingField && isFieldUndeleted) + ) { + // Initiate the empty og_image field + onChange(null, "og_image"); + } + }, [ + isOgImageFieldCreated, + isCreatingOgImageField, + isUndeletingField, + isFieldUndeleted, + ]); + + useEffect(() => { + // Automatically opens the media browser when the og_image field has no value + if (autoOpenMediaBrowser && "og_image" in item?.data) { + if (!item?.data?.["og_image"]) { + fieldTypeMedia.current?.triggerOpenMediaBrowser(); + } + setAutoOpenMediaBrowser(false); + } + }, [item?.data, autoOpenMediaBrowser]); + + // If there is already a field named og_image + if ("og_image" in item?.data) { + const ogImageValue = item?.data?.["og_image"]; + + return ( + <> + + { + setImageModal(opts); + }} + onChange={onChange} + lockedToGroupId={null} + settings={{ + fileExtensions: [ + ".png", + ".jpg", + ".jpeg", + ".svg", + ".gif", + ".tif", + ".webp", + ], + fileExtensionsErrorMessage: + "Only files with the following extensions are allowed: .png, .jpg, .jpeg, .svg, .gif, .tif, .webp", + }} + /> + + {imageModal && ( + + setImageModal(null)} + > + setImageModal(null)} + > + + + { + imageModal.callback(images); + setImageModal(null); + }} + isReplace={imageModal.isReplace} + /> + + + )} + + ); + } + + // If there is a media field with an API ID containing the word "image" and is storing a file + if (!("og_image" in item?.data) && !!usableTemporaryMetaImage) { + return ( + + + + } + variant="outlined" + sx={{ width: "fit-content" }} + onClick={() => { + handleCreateOgImageField(); + setAutoOpenMediaBrowser(true); + }} + > + Customize Image + + + + ); + } + + // If no image field + return ( + + } + variant="outlined" + sx={{ width: "fit-content", mt: 0.75 }} + onClick={handleCreateOgImageField} + > + Add Meta Image + + + ); +}; diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaKeywords.js b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/MetaKeywords.tsx similarity index 59% rename from src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaKeywords.js rename to src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/MetaKeywords.tsx index 30f238dde3..85856fefb1 100644 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/MetaKeywords.js +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/MetaKeywords.tsx @@ -1,17 +1,23 @@ import { memo } from "react"; -import { TextField } from "@mui/material"; +import { TextField, Box } from "@mui/material"; -import { FieldShell } from "../../../../../components/Editor/Field/FieldShell"; -import { MaxLengths } from "../ItemSettings"; -import styles from "./MetaKeywords.less"; +import { FieldShell } from "../../../../components/Editor/Field/FieldShell"; +import { MaxLengths } from ".."; +import { Error } from "../../../../components/Editor/Field/FieldShell"; + +type MetaKeywordsProps = { + value: string; + onChange: (value: string, name: string) => void; + error: Error; +}; export const MetaKeywords = memo(function MetaKeywords({ - meta_keywords, + value, onChange, - errors, -}) { + error, +}: MetaKeywordsProps) { return ( -
+ onChange(evt.target.value, "metaKeywords")} /> -
+ ); }); diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/MetaLinkText.tsx b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/MetaLinkText.tsx new file mode 100644 index 0000000000..17dc9a1f5a --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/MetaLinkText.tsx @@ -0,0 +1,40 @@ +import { memo } from "react"; + +import { TextField, Box } from "@mui/material"; +import { FieldShell } from "../../../../components/Editor/Field/FieldShell"; +import { MaxLengths } from ".."; +import { Error } from "../../../../components/Editor/Field/FieldShell"; + +type MetaLinkTextProps = { + value: string; + onChange: (value: string, name: string) => void; + error: Error; +}; +export const MetaLinkText = memo(function MetaLinkText({ + value, + onChange, + error, +}: MetaLinkTextProps) { + return ( + + + onChange(evt.target.value, "metaLinkText")} + /> + + + ); +}); diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/MetaTitle.tsx b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/MetaTitle.tsx new file mode 100644 index 0000000000..02de2c9ed9 --- /dev/null +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/MetaTitle.tsx @@ -0,0 +1,44 @@ +import { memo } from "react"; + +import { TextField, Box } from "@mui/material"; + +import { FieldShell } from "../../../../components/Editor/Field/FieldShell"; +import { MaxLengths } from ".."; +import { hasErrors } from "./util"; +import { Error } from "../../../../components/Editor/Field/FieldShell"; + +type MetaTitleProps = { + value: string; + onChange: (value: string, name: string) => void; + error: Error; +}; +export const MetaTitle = memo(function MetaTitle({ + value, + onChange, + error, +}: MetaTitleProps) { + return ( + + + onChange(evt.target.value, "metaTitle")} + error={hasErrors(error)} + /> + + + ); +}); diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/SitemapPriority.js b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/SitemapPriority.js similarity index 91% rename from src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/SitemapPriority.js rename to src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/SitemapPriority.js index 1f0f13f8e4..0a7ce62d9f 100644 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/ItemSettings/settings/SitemapPriority.js +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/SitemapPriority.js @@ -2,8 +2,9 @@ import { memo } from "react"; import { Select, MenuItem } from "@mui/material"; -import { FieldShell } from "../../../../../components/Editor/Field/FieldShell"; +import { FieldShell } from "../../../../components/Editor/Field/FieldShell"; import styles from "./SitemapPriority.less"; + export const SitemapPriority = memo(function SitemapPriority(props) { return (
@@ -11,7 +12,7 @@ export const SitemapPriority = memo(function SitemapPriority(props) { settings={{ label: "Sitemap Priority", }} - customTooltip="Sitemap priority helps search engines understand how often they should crawl pages on your site." + customTooltip="Sitemap priority helps search engines understand how often they should crawl the pages on your site." withInteractiveTooltip={false} > { + setNewFile(evt.target.files[0]); + }} + hidden + accept={acceptedExtension} + style={{ display: "none" }} + /> + + ); +}; diff --git a/src/apps/media/src/app/components/FileModal/index.tsx b/src/apps/media/src/app/components/FileModal/index.tsx index e726957cd5..282c6877d0 100644 --- a/src/apps/media/src/app/components/FileModal/index.tsx +++ b/src/apps/media/src/app/components/FileModal/index.tsx @@ -18,6 +18,7 @@ import { useGetFileQuery } from "../../../../../../shell/services/mediaManager"; import { OTFEditor } from "./OTFEditor"; import { File } from "../../../../../../shell/services/types"; import { useParams } from "../../../../../../shell/hooks/useParams"; +import { ReplaceFileModal } from "./ReplaceFileModal"; const styledModal = { position: "absolute", @@ -48,6 +49,8 @@ export const FileModal: FC = ({ const location = useLocation(); const { data, isLoading, isError, isFetching } = useGetFileQuery(fileId); const [showEdit, setShowEdit] = useState(false); + const [showReplaceFileModal, setShowReplaceFileModal] = useState(false); + const [fileToReplace, setFileToReplace] = useState(null); const [params, setParams] = useParams(); const [adjacentFiles, setAdjacentFiles] = useState({ prevFile: null, @@ -150,128 +153,145 @@ export const FileModal: FC = ({ }; }, []); + if (isFetching || (!data && !isError)) { + return ( + + + + ); + } + + if (showReplaceFileModal) { + return ( + setShowReplaceFileModal(false)} + onClose={handleCloseModal} + /> + ); + } + + if (!data) { + return <>; + } + return ( - <> - {data && !isError && !isFetching ? ( - + {adjacentFiles.nextFile && ( + { + handleArrow(adjacentFiles.nextFile); + }} + sx={{ + position: "absolute", + right: -72, + top: "50%", }} > - {adjacentFiles.nextFile && ( - { - handleArrow(adjacentFiles.nextFile); - }} - sx={{ - position: "absolute", - right: -72, - top: "50%", - }} - > - - - )} - {adjacentFiles.prevFile && ( - - { - handleArrow(adjacentFiles.prevFile); - }} - sx={{ - position: "absolute", - left: -72, - top: "50%", - }} - > - - - - )} - + + )} + {adjacentFiles.prevFile && ( + + { + handleArrow(adjacentFiles.prevFile); + }} sx={{ - display: "flex", - justifyContent: "space-between", - p: 0, - overflow: "hidden", + position: "absolute", + left: -72, + top: "50%", }} > - {/* */} - - - - - - {showEdit ? ( - - ) : ( - - )} - - {/* */} - - - ) : isFetching || (!data && !isError) ? ( - + + + )} + + {/* */} + - - - ) : ( - <> - )} - + + + + + {showEdit ? ( + + ) : ( + { + setShowReplaceFileModal(true); + }} + /> + )} + + {/* */} + + ); }; diff --git a/src/apps/media/src/app/components/Thumbnail/ThumbnailContent.tsx b/src/apps/media/src/app/components/Thumbnail/ThumbnailContent.tsx index 6ce30ef6a5..009ff0de1e 100644 --- a/src/apps/media/src/app/components/Thumbnail/ThumbnailContent.tsx +++ b/src/apps/media/src/app/components/Thumbnail/ThumbnailContent.tsx @@ -17,16 +17,20 @@ interface Props { filename: string; onFilenameChange?: (value: string) => void; onTitleChange?: (value: string) => void; - isEditable?: boolean; isSelected?: boolean; + isFilenameEditable?: boolean; + isTitleEditable?: boolean; + title?: string; } export const ThumbnailContent: FC = ({ filename, onFilenameChange, onTitleChange, - isEditable, isSelected, + isFilenameEditable, + isTitleEditable, + title, }) => { const styledCardContent = { px: onFilenameChange ? 0 : 1, @@ -48,13 +52,16 @@ export const ThumbnailContent: FC = ({ {onFilenameChange ? ( - + ) => onFilenameChange(e.target.value.replace(" ", "-")) } @@ -79,15 +86,16 @@ export const ThumbnailContent: FC = ({ }, }} /> - + void; onTitleChange?: (value: string) => void; onClick?: () => void; + showRemove?: boolean; + isFilenameEditable?: boolean; + isTitleEditable?: boolean; + title?: string; } export const Thumbnail: FC = ({ src, url, filename, - isEditable, + isDraggable, showVideo, onRemove, onFilenameChange, @@ -83,6 +87,10 @@ export const Thumbnail: FC = ({ onTitleChange, imageHeight, selectable, + showRemove = true, + isFilenameEditable, + isTitleEditable, + title, }) => { const theme = useTheme(); const imageEl = useRef(); @@ -119,6 +127,10 @@ export const Thumbnail: FC = ({ }; const RemoveIcon = () => { + if (!showRemove) { + return <>; + } + return ( <> {onRemove && ( @@ -331,7 +343,7 @@ export const Thumbnail: FC = ({ sx={styledCard} elevation={0} onClick={isSelecting ? handleSelect : onClick} - draggable={!isEditable} + draggable={!isDraggable} onDragStart={(evt) => onDragStart(evt)} data-cy={id} > @@ -408,10 +420,12 @@ export const Thumbnail: FC = ({ file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -425,7 +439,7 @@ export const Thumbnail: FC = ({ sx={styledCard} elevation={0} onClick={isSelecting ? handleSelect : onClick} - draggable={!isEditable} + draggable={!isDraggable} data-cy={id} onDragStart={(evt) => onDragStart(evt)} > @@ -503,8 +517,9 @@ export const Thumbnail: FC = ({ filename={filename} onFilenameChange={onFilenameChange} onTitleChange={onTitleChange} - isEditable={isEditable} isSelected={selectedFiles.some((file) => file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -516,7 +531,7 @@ export const Thumbnail: FC = ({ sx={styledCard} elevation={0} onClick={isSelecting ? handleSelect : onClick} - draggable={!isEditable} + draggable={!isDraggable} onDragStart={(evt) => onDragStart(evt)} > = ({ filename={filename} onFilenameChange={onFilenameChange} onTitleChange={onTitleChange} - isEditable={isEditable} isSelected={selectedFiles.some((file) => file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -588,7 +604,7 @@ export const Thumbnail: FC = ({ sx={styledCard} elevation={0} onClick={isSelecting ? handleSelect : onClick} - draggable={!isEditable} + draggable={!isDraggable} onDragStart={(evt) => onDragStart(evt)} > = ({ filename={filename} onFilenameChange={onFilenameChange} onTitleChange={onTitleChange} - isEditable={isEditable} isSelected={selectedFiles.some((file) => file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -663,7 +680,7 @@ export const Thumbnail: FC = ({ elevation={0} onClick={isSelecting ? handleSelect : onClick} data-cy={id} - draggable={!isEditable} + draggable={!isDraggable} onDragStart={(evt) => onDragStart(evt)} > = ({ filename={filename} onFilenameChange={onFilenameChange} onTitleChange={onTitleChange} - isEditable={isEditable} isSelected={selectedFiles.some((file) => file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -737,7 +755,7 @@ export const Thumbnail: FC = ({ elevation={0} onClick={isSelecting ? handleSelect : onClick} data-cy={id} - draggable={!isEditable} + draggable={!isDraggable} onDragStart={(evt) => onDragStart(evt)} > = ({ filename={filename} onFilenameChange={onFilenameChange} onTitleChange={onTitleChange} - isEditable={isEditable} isSelected={selectedFiles.some((file) => file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -812,7 +831,7 @@ export const Thumbnail: FC = ({ elevation={0} onClick={isSelecting ? handleSelect : onClick} data-cy={id} - draggable={!isEditable} + draggable={!isDraggable} onDragStart={(evt) => onDragStart(evt)} > = ({ filename={filename} onFilenameChange={onFilenameChange} onTitleChange={onTitleChange} - isEditable={isEditable} isSelected={selectedFiles.some((file) => file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -889,7 +909,7 @@ export const Thumbnail: FC = ({ elevation={0} onClick={isSelecting ? handleSelect : onClick} data-cy={id} - draggable={!isEditable} + draggable={!isDraggable} onDragStart={(evt) => onDragStart(evt)} > = ({ filename={filename} onFilenameChange={onFilenameChange} onTitleChange={onTitleChange} - isEditable={isEditable} isSelected={selectedFiles.some((file) => file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -972,7 +993,7 @@ export const Thumbnail: FC = ({ elevation={0} data-cy={id} onClick={isSelecting ? handleSelect : onClick} - draggable={!isEditable} + draggable={!isDraggable} onDragStart={(evt) => onDragStart(evt)} > = ({ filename={filename} onFilenameChange={onFilenameChange} onTitleChange={onTitleChange} - isEditable={isEditable} isSelected={selectedFiles.some((file) => file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -1069,7 +1091,7 @@ export const Thumbnail: FC = ({ elevation={0} data-cy={id} onClick={isSelecting ? handleSelect : onClick} - draggable={!isEditable} + draggable={!isDraggable} onDragStart={(evt) => onDragStart(evt)} > = ({ filename={filename} onFilenameChange={onFilenameChange} onTitleChange={onTitleChange} - isEditable={isEditable} isSelected={selectedFiles.some((file) => file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -1147,7 +1170,7 @@ export const Thumbnail: FC = ({ elevation={0} onClick={isSelecting ? handleSelect : onClick} data-cy={id} - draggable={!isEditable} + draggable={!isDraggable} onDragStart={(evt) => onDragStart(evt)} > = ({ filename={filename} onFilenameChange={onFilenameChange} onTitleChange={onTitleChange} - isEditable={isEditable} isSelected={selectedFiles.some((file) => file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -1220,7 +1244,7 @@ export const Thumbnail: FC = ({ elevation={0} onClick={isSelecting ? handleSelect : onClick} data-cy={id} - draggable={!isEditable} + draggable={!isDraggable} onDragStart={(evt) => onDragStart(evt)} > = ({ filename={filename} onFilenameChange={onFilenameChange} onTitleChange={onTitleChange} - isEditable={isEditable} isSelected={selectedFiles.some((file) => file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -1293,7 +1318,7 @@ export const Thumbnail: FC = ({ elevation={0} onClick={isSelecting ? handleSelect : onClick} data-cy={id} - draggable={!isEditable} + draggable={!isDraggable} onDragStart={(evt) => onDragStart(evt)} > = ({ filename={filename} onFilenameChange={onFilenameChange} onTitleChange={onTitleChange} - isEditable={isEditable} isSelected={selectedFiles.some((file) => file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -1366,7 +1392,7 @@ export const Thumbnail: FC = ({ elevation={0} data-cy={id} onClick={isSelecting ? handleSelect : onClick} - draggable={!isEditable} + draggable={!isDraggable} onDragStart={(evt) => onDragStart(evt)} > = ({ filename={filename} onFilenameChange={onFilenameChange} onTitleChange={onTitleChange} - isEditable={isEditable} isSelected={selectedFiles.some((file) => file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -1439,7 +1466,7 @@ export const Thumbnail: FC = ({ elevation={0} data-cy={id} onClick={isSelecting ? handleSelect : onClick} - draggable={!isEditable} + draggable={!isDraggable} onDragStart={(evt) => onDragStart(evt)} > = ({ filename={filename} onFilenameChange={onFilenameChange} onTitleChange={onTitleChange} - isEditable={isEditable} isSelected={selectedFiles.some((file) => file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -1515,7 +1543,7 @@ export const Thumbnail: FC = ({ elevation={0} data-cy={id} onClick={isSelecting ? handleSelect : onClick} - draggable={!isEditable} + draggable={!isDraggable} onDragStart={(evt) => onDragStart(evt)} > = ({ filename={filename} onFilenameChange={onFilenameChange} onTitleChange={onTitleChange} - isEditable={isEditable} isSelected={selectedFiles.some((file) => file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -1595,7 +1624,7 @@ export const Thumbnail: FC = ({ elevation={0} onClick={isSelecting ? handleSelect : onClick} data-cy={id} - draggable={!isEditable} + draggable={!isDraggable} onDragStart={(evt) => onDragStart(evt)} > = ({ filename={filename} onFilenameChange={onFilenameChange} onTitleChange={onTitleChange} - isEditable={isEditable} isSelected={selectedFiles.some((file) => file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -1675,7 +1705,7 @@ export const Thumbnail: FC = ({ elevation={0} onClick={isSelecting ? handleSelect : onClick} data-cy={id} - draggable={!isEditable} + draggable={!isDraggable} onDragStart={(evt) => onDragStart(evt)} > = ({ filename={filename} onFilenameChange={onFilenameChange} onTitleChange={onTitleChange} - isEditable={isEditable} isSelected={selectedFiles.some((file) => file.id === id)} + isTitleEditable={isTitleEditable} + isFilenameEditable={isFilenameEditable} /> ); @@ -1759,6 +1790,6 @@ export const Thumbnail: FC = ({ }; Thumbnail.defaultProps = { - isEditable: false, + isDraggable: false, showVideo: true, }; diff --git a/src/apps/media/src/app/components/UploadModal.tsx b/src/apps/media/src/app/components/UploadModal.tsx index d7cee6d083..3c2c4e0eec 100644 --- a/src/apps/media/src/app/components/UploadModal.tsx +++ b/src/apps/media/src/app/components/UploadModal.tsx @@ -31,9 +31,13 @@ import pluralizeWord from "../../../../../utility/pluralizeWord"; export const UploadModal: FC = () => { const dispatch = useDispatch(); - const uploads = useSelector((state: AppState) => state.mediaRevamp.uploads); + const uploads = useSelector((state: AppState) => + state.mediaRevamp.uploads.filter((upload) => !upload.replacementFile) + ); const filesToUpload = useSelector((state: AppState) => - state.mediaRevamp.uploads.filter((upload) => upload.status !== "failed") + state.mediaRevamp.uploads.filter( + (upload) => upload.status !== "failed" && !upload.replacementFile + ) ); const ids = filesToUpload.length && { currentBinId: filesToUpload[0].bin_id, @@ -186,8 +190,14 @@ const UploadErrors = () => { type UploadHeaderTextProps = { uploads: Upload[]; + headerKeyword?: string; + showCount?: boolean; }; -const UploadHeaderText = ({ uploads }: UploadHeaderTextProps) => { +export const UploadHeaderText = ({ + uploads, + headerKeyword = "File", + showCount = true, +}: UploadHeaderTextProps) => { const filesUploading = uploads?.filter( (upload) => upload.status === "inProgress" ); @@ -225,12 +235,18 @@ const UploadHeaderText = ({ uploads }: UploadHeaderTextProps) => { )} + {showCount ? ( + filesUploading?.length > 0 ? ( + filesUploading.length + ) : ( + filesUploaded.length + ) + ) : ( + <> + )}{" "} {filesUploading?.length > 0 - ? filesUploading.length - : filesUploaded.length}{" "} - {filesUploading?.length > 0 - ? pluralizeWord("File", filesUploading.length) - : pluralizeWord("File", filesUploaded.length)}{" "} + ? pluralizeWord(headerKeyword, filesUploading.length) + : pluralizeWord(headerKeyword, filesUploaded.length)}{" "} {filesUploading?.length > 0 ? "Uploading" : "Uploaded"} diff --git a/src/apps/media/src/app/components/UploadThumbnail.tsx b/src/apps/media/src/app/components/UploadThumbnail.tsx index 83504e031f..35d7f0b5c2 100644 --- a/src/apps/media/src/app/components/UploadThumbnail.tsx +++ b/src/apps/media/src/app/components/UploadThumbnail.tsx @@ -13,13 +13,23 @@ import { Upload, fileUploadSetFilename, deleteUpload, + replaceFile, } from "../../../../../shell/store/media-revamp"; +import { File as ZestyMediaFile } from "../../../../../shell/services/types"; interface Props { file: Upload; + action?: "new" | "replace"; + originalFile?: ZestyMediaFile; + showRemove?: boolean; } -export const UploadThumbnail: FC = ({ file }) => { +export const UploadThumbnail: FC = ({ + file, + action = "new", + originalFile, + showRemove = true, +}) => { const dispatch = useDispatch(); const { data: bin } = mediaManagerApi.useGetBinQuery(file.bin_id, { @@ -28,7 +38,11 @@ export const UploadThumbnail: FC = ({ file }) => { useEffect(() => { if (bin && file.status === "staged") { - dispatch(uploadFile(file, bin[0])); + if (action === "new") { + dispatch(uploadFile(file, bin[0])); + } else { + dispatch(replaceFile(file, originalFile)); + } } }, [bin]); @@ -61,10 +75,14 @@ export const UploadThumbnail: FC = ({ file }) => { > { if (file.status === "success") { dispatch( diff --git a/src/apps/schema/src/app/components/AddFieldModal/DefaultValue.tsx b/src/apps/schema/src/app/components/AddFieldModal/DefaultValue.tsx index 4a57292a00..d0b6a5427a 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/DefaultValue.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/DefaultValue.tsx @@ -27,6 +27,7 @@ type DefaultValueProps = { relatedFieldZUID: string; }; options: FieldSettingsOptions[]; + currency?: string; }; export const DefaultValue = ({ @@ -39,6 +40,7 @@ export const DefaultValue = ({ mediaRules, relationshipFields, options, + currency, }: DefaultValueProps) => { return ( @@ -69,15 +71,16 @@ export const DefaultValue = ({ variant="body3" color="text.secondary" fontWeight="600" - sx={{ mb: 1, display: "block" }} + sx={{ display: "block" }} > Set a predefined value for this field } /> - - {isDefaultValueEnabled && ( + + {isDefaultValueEnabled && ( + @@ -102,8 +106,8 @@ export const DefaultValue = ({ - )} - + + )} ); }; diff --git a/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx b/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx index 633776d5a9..91c52679db 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/DefaultValueInput.tsx @@ -65,6 +65,7 @@ type DefaultValueInputProps = { relatedFieldZUID: string; }; options: FieldSettingsOptions[]; + currency?: string; }; export const DefaultValueInput = ({ @@ -75,6 +76,7 @@ export const DefaultValueInput = ({ mediaRules, relationshipFields: { relatedModelZUID, relatedFieldZUID }, options, + currency, }: DefaultValueInputProps) => { const [imageModal, setImageModal] = useState(null); const dispatch = useDispatch(); @@ -573,12 +575,11 @@ export const DefaultValueInput = ({ return ( ); case "date": diff --git a/src/apps/schema/src/app/components/AddFieldModal/FieldFormInput.tsx b/src/apps/schema/src/app/components/AddFieldModal/FieldFormInput.tsx index a41e038880..7517f00c6d 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/FieldFormInput.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/FieldFormInput.tsx @@ -14,6 +14,10 @@ import { Button, IconButton, Stack, + AutocompleteProps, + InputProps, + OutlinedInputProps, + FilledInputProps, } from "@mui/material"; import { SelectChangeEvent } from "@mui/material/Select"; import InfoRoundedIcon from "@mui/icons-material/InfoRounded"; @@ -25,6 +29,7 @@ import { FormValue } from "./views/FieldForm"; import { FieldSettingsOptions } from "../../../../../../shell/services/types"; import { convertDropdownValue } from "../../utils"; import { withCursorPosition } from "../../../../../../shell/components/withCursorPosition"; +import { Currency } from "../../../../../../shell/components/FieldTypeCurrency/currencies"; const TextFieldWithCursorPosition = withCursorPosition(TextField); @@ -49,7 +54,10 @@ export type FieldNames = | "regexRestrictPattern" | "regexRestrictErrorMessage" | "minValue" - | "maxValue"; + | "maxValue" + | "currency" + | "fileExtensions" + | "fileExtensionsErrorMessage"; type FieldType = | "input" | "checkbox" @@ -78,7 +86,14 @@ export interface DropdownOptions { label: string; value: string; } -interface FieldFormInputProps { +export type AutocompleteConfig = { + inputProps?: + | Partial + | Partial + | Partial; + maxHeight?: number; +}; +type FieldFormInputProps = { fieldConfig: InputField; errorMsg?: string | [string, string][]; onDataChange: ({ @@ -89,9 +104,13 @@ interface FieldFormInputProps { value: FormValue; }) => void; prefillData?: FormValue; - dropdownOptions?: DropdownOptions[]; + dropdownOptions?: DropdownOptions[] | Currency[]; disabled?: boolean; -} + autocompleteConfig?: AutocompleteConfig; +} & Pick< + AutocompleteProps, + "renderOption" | "filterOptions" +>; export const FieldFormInput = ({ fieldConfig, errorMsg, @@ -99,6 +118,9 @@ export const FieldFormInput = ({ prefillData, dropdownOptions, disabled, + renderOption, + filterOptions, + autocompleteConfig, }: FieldFormInputProps) => { const options = fieldConfig.type === "options" || @@ -212,9 +234,19 @@ export const FieldFormInput = ({ {fieldConfig.type === "autocomplete" && ( <> - - {fieldConfig.label} - + + + {fieldConfig.label} + + {fieldConfig.tooltip && ( + + + + )} + )} isOptionEqualToValue={(option, value) => @@ -246,16 +284,22 @@ export const FieldFormInput = ({ height: "40px", }, }} + renderOption={renderOption} + filterOptions={filterOptions} + slotProps={{ + paper: { + sx: { + "& .MuiAutocomplete-listbox": { + maxHeight: autocompleteConfig?.maxHeight || "40vh", + boxSizing: "border-box", + }, + }, + }, + }} /> {prefillData && !dropdownOptions.find((option) => option.value === prefillData) && ( - + {fieldConfig.name === "group_id" && "The folder this was locked to has been deleted"} {fieldConfig.name === "relatedModelZUID" && @@ -324,12 +368,9 @@ export const FieldFormInput = ({ error={Boolean(errorMsg)} helperText={ errorMsg && ( - + {errorMsg} - + ) } type={fieldConfig.inputType || "text"} @@ -445,9 +486,9 @@ const KeyValueInput = ({ handleDataChanged("value", e.target?.value); }} helperText={ - + {labelErrorMsg} - + } error={Boolean(labelErrorMsg)} disabled={disabledFields.includes("value")} @@ -463,9 +504,9 @@ const KeyValueInput = ({ handleDataChanged("key", e.target?.value); }} helperText={ - + {valueErrorMsg} - + } error={Boolean(valueErrorMsg)} disabled={disabledFields.includes("key")} diff --git a/src/apps/schema/src/app/components/AddFieldModal/MediaRules.tsx b/src/apps/schema/src/app/components/AddFieldModal/MediaRules.tsx index 18223ec1a1..101eb794fc 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/MediaRules.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/MediaRules.tsx @@ -4,14 +4,22 @@ import { Checkbox, Typography, Stack, + InputLabel, + Autocomplete, + TextField, + Chip, } from "@mui/material"; -import { FormValue } from "./views/FieldForm"; +import { Errors, FormValue } from "./views/FieldForm"; import { CustomGroup } from "../hooks/useMediaRules"; - import { InputField } from "./FieldFormInput"; import { FieldFormInput, FieldNames } from "./FieldFormInput"; +import { useEffect, useState } from "react"; + +type MediaFieldName = Extract< + FieldNames, + "limit" | "group_id" | "fileExtensions" +>; -type MediaFieldName = Extract; const MediaLabelsConfig: { [key in MediaFieldName]: { label: string; subLabel: string }; } = { @@ -24,8 +32,83 @@ const MediaLabelsConfig: { label: "Lock to a folder", subLabel: "Ensures files can only be selected from a specific folder", }, + fileExtensions: { + label: "Limit File Types", + subLabel: "Ensures only certain file types can be accepted", + }, }; +const ExtensionPresets = [ + { + label: "Images", + value: [".png", ".jpg", ".jpeg", ".svg", ".gif", ".tif", ".webp"], + }, + { + label: "Videos", + value: [ + ".mob", + ".avi", + ".wmv", + ".mp4", + ".mpeg", + ".mkv", + ".m4v", + ".mpg", + ".webm", + ], + }, + { + label: "Audios", + value: [ + ".mp3", + ".flac", + ".wav", + ".m4a", + ".aac", + ".ape", + ".opus", + ".aiff", + ".aif", + ], + }, + { + label: "Documents", + value: [".doc", ".pdf", ".docx", ".txt", ".rtf", ".odt", ".pages"], + }, + { + label: "Presentations", + value: [ + ".ppt", + ".pptx", + ".key", + ".odp", + ".pps", + ".ppsx", + ".sldx", + ".potx", + ".otp", + ".sxi", + ], + }, + { + label: "Spreadsheets", + value: [ + ".xls", + ".xlsx", + ".csv", + ".tsv", + ".numbers", + ".ods", + ".xlsm", + ".xlsb", + ".xlt", + ".xltx", + ], + }, +] as const; + +const RestrictedExtensions = [".exe", ".dmg"]; + interface Props { fieldConfig: InputField[]; groups: CustomGroup[]; @@ -37,13 +120,90 @@ interface Props { value: FormValue; }) => void; fieldData: { [key: string]: FormValue }; + errors: Errors; } + export const MediaRules = ({ fieldConfig, onDataChange, groups, fieldData, + errors, }: Props) => { + const [inputValue, setInputValue] = useState(""); + const [autoFill, setAutoFill] = useState( + !fieldData.fileExtensionsErrorMessage + ); + const [extensionsError, setExtensionsError] = useState(false); + + useEffect(() => { + if (autoFill) { + onDataChange({ + inputName: "fileExtensionsErrorMessage", + value: + "Only files with the following extensions are allowed: " + + (fieldData["fileExtensions"] as string[])?.join(", "), + }); + } + }, [autoFill, fieldData["fileExtensions"]]); + + const handleInputChange = ( + event: any, + newInputValue: string, + ruleName: string + ) => { + const formattedInput = newInputValue.trim().toLowerCase(); + if (formattedInput && formattedInput[0] !== ".") { + setInputValue(`.${formattedInput}`); + } else { + setInputValue(formattedInput); + } + }; + + const handleKeyDown = (event: any, ruleName: string) => { + if ( + (event.key === "Enter" || event.key === "," || event.key === " ") && + inputValue + ) { + event.preventDefault(); + const newOption = inputValue.toLowerCase().trim(); + if ( + newOption && + !(fieldData[ruleName] as string[]).includes(newOption) && + !RestrictedExtensions.includes(newOption) + ) { + onDataChange({ + inputName: ruleName, + value: [...(fieldData[ruleName] as string[]), newOption], + }); + setInputValue(""); + } + } else if (event.key === "Backspace" && !inputValue) { + const newTags = [...(fieldData[ruleName] as string[])]; + newTags.pop(); + if (!newTags.length) { + setExtensionsError(true); + } + onDataChange({ + inputName: ruleName, + value: newTags, + }); + } + }; + + const handleDelete = (option: string, ruleName: string) => { + const newTags = (fieldData[ruleName] as string[]).filter( + (item) => item.trim() !== option.trim() + ); + if (!newTags.length) { + setExtensionsError(true); + } + onDataChange({ + inputName: ruleName, + value: newTags, + }); + }; + return ( {fieldConfig?.map((rule: InputField, key: number) => { - if (rule.name === "defaultValue") return; + if ( + rule.name === "defaultValue" || + rule.name === "fileExtensionsErrorMessage" + ) + return null; return ( @@ -101,7 +274,9 @@ export const MediaRules = ({ } /> - {Boolean(fieldData[rule.name]) && ( + {Boolean( + fieldData[rule.name] && rule.name !== "fileExtensions" + ) && ( )} + + {Boolean(fieldData[rule.name]) && rule.name === "fileExtensions" && ( + + Extensions * + ( + handleKeyDown(event, rule.name)} + /> + )} + onInputChange={(event, newInputValue) => + handleInputChange(event, newInputValue, rule.name) + } + renderTags={(tagValue, getTagProps) => + tagValue.map((option, index) => ( + handleDelete(option, rule.name)} + clickable={false} + sx={{ + backgroundColor: "common.white", + borderColor: "grey.300", + borderWidth: 1, + borderStyle: "solid", + }} + /> + )) + } + /> + {errors["fileExtensions"] && extensionsError && ( + + {errors["fileExtensions"]} + + )} + + Add: + {ExtensionPresets.map((preset) => ( + { + const newTags = fieldData[rule.name] as string[]; + const tags = new Set(newTags); + preset.value.forEach((tag) => tags.add(tag)); + onDataChange({ + inputName: rule.name, + value: Array.from(tags), + }); + }} + sx={{ + backgroundColor: "common.white", + borderColor: "grey.300", + borderWidth: 1, + borderStyle: "solid", + }} + /> + ))} + + + Custom Error Message * + + { + setAutoFill(false); + onDataChange({ + inputName: "fileExtensionsErrorMessage", + value: e.target.value, + }); + }} + /> + {errors["fileExtensionsErrorMessage"] && ( + + {errors["fileExtensionsErrorMessage"]} + + )} + + )} ); })} diff --git a/src/apps/schema/src/app/components/AddFieldModal/views/FieldForm.tsx b/src/apps/schema/src/app/components/AddFieldModal/views/FieldForm.tsx index c315404b73..661edcf3ff 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/views/FieldForm.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/views/FieldForm.tsx @@ -13,6 +13,11 @@ import { Button, Grid, Stack, + ListItem, + FilledInputProps, + InputProps, + OutlinedInputProps, + InputAdornment, } from "@mui/material"; import LoadingButton from "@mui/lab/LoadingButton"; import { isEmpty } from "lodash"; @@ -27,7 +32,11 @@ import PauseCircleOutlineRoundedIcon from "@mui/icons-material/PauseCircleOutlin import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; import { FieldIcon } from "../../Field/FieldIcon"; -import { FieldFormInput, DropdownOptions } from "../FieldFormInput"; +import { + FieldFormInput, + DropdownOptions, + AutocompleteConfig, +} from "../FieldFormInput"; import { useMediaRules } from "../../hooks/useMediaRules"; import { MediaRules } from "../MediaRules"; import { @@ -64,6 +73,11 @@ import { DefaultValue } from "../DefaultValue"; import { CharacterLimit } from "../CharacterLimit"; import { Rules } from "./Rules"; import { MaxLengths } from "../../../../../../content-editor/src/app/components/Editor/Editor"; +import { + Currency, + currencies, +} from "../../../../../../../shell/components/FieldTypeCurrency/currencies"; +import getFlagEmoji from "../../../../../../../utility/getFlagEmoji"; type ActiveTab = "details" | "rules" | "learn"; type Params = { @@ -223,6 +237,12 @@ export const FieldForm = ({ formFields[field.name] = fieldData.settings[field.name] ?? null; } else if (field.name === "maxValue") { formFields[field.name] = fieldData.settings[field.name] ?? null; + } else if (field.name === "currency") { + formFields[field.name] = fieldData.settings?.currency ?? "USD"; + } else if (field.name === "fileExtensions") { + formFields[field.name] = fieldData.settings[field.name] ?? null; + } else if (field.name === "fileExtensionsErrorMessage") { + formFields[field.name] = fieldData.settings[field.name] ?? null; } else { formFields[field.name] = fieldData[field.name] as FormValue; } @@ -249,7 +269,9 @@ export const FieldForm = ({ field.name === "regexRestrictPattern" || field.name === "regexRestrictErrorMessage" || field.name === "minValue" || - field.name === "maxValue" + field.name === "maxValue" || + field.name === "fileExtensions" || + field.name === "fileExtensionsErrorMessage" ) { formFields[field.name] = null; } else { @@ -393,6 +415,26 @@ export const FieldForm = ({ } } + if (inputName === "currency" && !formData.currency) { + newErrorsObj[inputName] = "Please select a currency"; + } + + if ( + inputName === "fileExtensions" && + formData.fileExtensions !== null && + !(formData.fileExtensions as string[])?.length + ) { + newErrorsObj[inputName] = "This field is required"; + } + + if ( + inputName === "fileExtensionsErrorMessage" && + formData.fileExtensions !== null && + formData.fileExtensionsErrorMessage === "" + ) { + newErrorsObj[inputName] = "This field is required"; + } + if ( inputName in errors && ![ @@ -405,6 +447,9 @@ export const FieldForm = ({ "regexRestrictErrorMessage", "minValue", "maxValue", + "currency", + "fileExtensions", + "fileExtensionsErrorMessage", ].includes(inputName) ) { const { maxLength, label, validate } = FORM_CONFIG[type].details.find( @@ -508,7 +553,9 @@ export const FieldForm = ({ errors.regexRestrictPattern || errors.regexRestrictErrorMessage || errors.minValue || - errors.maxValue + errors.maxValue || + errors.fileExtensions || + errors.fileExtensionsErrorMessage ) { setActiveTab("rules"); } else { @@ -562,6 +609,16 @@ export const FieldForm = ({ ...(formData.maxValue !== null && { maxValue: formData.maxValue as number, }), + ...(formData.currency !== null && { + currency: formData.currency as string, + }), + ...(formData.fileExtensions && { + fileExtensions: formData.fileExtensions as string[], + }), + ...(formData.fileExtensionsErrorMessage && { + fileExtensionsErrorMessage: + formData.fileExtensionsErrorMessage as string, + }), }, sort: isUpdateField ? fieldData.sort : sort, // Just use the length since sort starts at 0 }; @@ -757,6 +814,9 @@ export const FieldForm = ({ let dropdownOptions: DropdownOptions[]; let disabled = false; + let renderOption: any; + let filterOptions: any; + let autocompleteConfig: AutocompleteConfig = {}; if (fieldConfig.name === "relatedModelZUID") { dropdownOptions = modelsOptions; @@ -768,6 +828,74 @@ export const FieldForm = ({ disabled = isFetchingSelectedModelFields; } + if (fieldConfig.name === "currency") { + const selectedValue = currencies.find( + (currency) => currency.value === formData.currency + ); + dropdownOptions = currencies; + renderOption = (props: any, value: Currency) => ( + + + + {value.value} {value.symbol_native}   + + {value.label} + + ); + filterOptions = (options: Currency[], state: any) => { + if (state.inputValue) { + return options.filter( + (option) => + option.label + ?.toLowerCase() + .includes(state.inputValue.toLowerCase()) || + option.value + ?.toLowerCase() + .includes(state.inputValue.toLowerCase()) + ); + } else { + return options; + } + }; + autocompleteConfig.inputProps = { + startAdornment: !!selectedValue && ( + + + + {selectedValue.value} {selectedValue.symbol_native} + + + ), + }; + autocompleteConfig.maxHeight = 256; + } + return ( ); })} diff --git a/src/apps/schema/src/app/components/AddFieldModal/views/Rules.tsx b/src/apps/schema/src/app/components/AddFieldModal/views/Rules.tsx index 8c7d473707..a92b5caf27 100644 --- a/src/apps/schema/src/app/components/AddFieldModal/views/Rules.tsx +++ b/src/apps/schema/src/app/components/AddFieldModal/views/Rules.tsx @@ -48,18 +48,6 @@ export const Rules = ({ return ( - {type === "images" && ( - - )} - + {type === "images" && ( + + )} + {(type === "text" || type === "textarea") && ( <> { + const { contentModelZUID } = useParams<{ contentModelZUID: string }>(); + const { data: modelData } = useGetContentModelQuery(contentModelZUID, { + skip: !contentModelZUID, + }); + + const filteredApiTypes = useMemo(() => { + if (modelData?.type === "dataset") { + return apiTypes.filter((apiType) => apiType !== "site-generators"); + } + + return apiTypes; + }, [modelData]); + return ( { height: "100%", }} > - {apiTypes.map((apiType) => ( + {filteredApiTypes?.map((apiType) => ( ))} diff --git a/src/apps/schema/src/app/components/ModelApi/ApiDetails.tsx b/src/apps/schema/src/app/components/ModelApi/ApiDetails.tsx index 0e14b4327a..89822d8800 100644 --- a/src/apps/schema/src/app/components/ModelApi/ApiDetails.tsx +++ b/src/apps/schema/src/app/components/ModelApi/ApiDetails.tsx @@ -8,7 +8,7 @@ import { Stack, CircularProgress, } from "@mui/material"; -import { useHistory, useLocation } from "react-router"; +import { useHistory, useLocation, useParams } from "react-router"; import { SvgIconComponent } from "@mui/icons-material"; import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; import BoltRoundedIcon from "@mui/icons-material/BoltRounded"; @@ -24,8 +24,12 @@ import { useSelector } from "react-redux"; import { AppState } from "../../../../../../shell/store/types"; import { ApiDomainEndpoints } from "./ApiDomainEndpoints"; import CodeRoundedIcon from "@mui/icons-material/CodeRounded"; -import { useGetInstanceSettingsQuery } from "../../../../../../shell/services/instance"; +import { + useGetContentModelQuery, + useGetInstanceSettingsQuery, +} from "../../../../../../shell/services/instance"; import { HeadlessSwitcher } from "./HeadlessSwitcher"; +import { useEffect, useMemo } from "react"; const apiTypeIconMap: Record = { "quick-access": BoltRoundedIcon, @@ -46,6 +50,10 @@ const apiTypesWithEndpoints = [ export const ApiDetails = () => { const history = useHistory(); const location = useLocation(); + const { contentModelZUID } = useParams<{ contentModelZUID: string }>(); + const { data: modelData } = useGetContentModelQuery(contentModelZUID, { + skip: !contentModelZUID, + }); const installedApps = useSelector((state: AppState) => state.apps.installed); const selectedType = location.pathname.split("/").pop() as ApiType; const { data: instanceSettings, isFetching } = useGetInstanceSettingsQuery( @@ -56,6 +64,14 @@ export const ApiDetails = () => { instanceSettings?.find((setting) => setting.key === "mode")?.value !== "traditional"; + const filteredApiTypes = useMemo(() => { + if (modelData?.type === "dataset") { + return apiTypes.filter((apiType) => apiType !== "site-generators"); + } + + return apiTypes; + }, [modelData]); + const handleVisualLayoutClick = () => { const layoutApp = installedApps?.find( (app: any) => app?.name === "layouts" @@ -67,6 +83,12 @@ export const ApiDetails = () => { } }; + useEffect(() => { + if (selectedType === "site-generators" && modelData?.type === "dataset") { + history.replace(`${location.pathname.split("/").slice(0, -1).join("/")}`); + } + }, [selectedType, modelData]); + return ( { - {apiTypes.map((type) => ( + {filteredApiTypes?.map((type) => ( = { rules: [...COMMON_RULES], }, currency: { - details: [...COMMON_FIELDS], + details: [ + { + name: "currency", + type: "autocomplete", + label: "Currency", + required: true, + gridSize: 12, + tooltip: + "The selected currency code, symbol, and flag will be displayed for this field in the content item and can be accessed through the field settings via the API.", + placeholder: "Select a Currency", + autoFocus: true, + }, + { + ...COMMON_FIELDS[0], + autoFocus: false, + }, + ...COMMON_FIELDS.slice(1), + ], rules: [...COMMON_RULES, ...INPUT_RANGE_RULES], }, date: { @@ -556,6 +573,20 @@ const FORM_CONFIG: Record = { required: false, gridSize: 12, }, + { + name: "fileExtensions", + type: "input", + label: "File Extensions", + required: false, + gridSize: 12, + }, + { + name: "fileExtensionsErrorMessage", + type: "input", + label: "File extensions error message", + required: false, + gridSize: 12, + }, ...COMMON_RULES, ], }, diff --git a/src/shell/components/CascadingMenuItem/index.tsx b/src/shell/components/CascadingMenuItem/index.tsx index b929507ad7..abd0bdce44 100644 --- a/src/shell/components/CascadingMenuItem/index.tsx +++ b/src/shell/components/CascadingMenuItem/index.tsx @@ -1,4 +1,4 @@ -import React, { FC, useState } from "react"; +import React, { FC, useEffect, useState } from "react"; import { MenuItem, MenuItemProps, @@ -23,11 +23,37 @@ export const CascadingMenuItem: FC = ({ ...props }) => { const [anchorEl, setAnchorEl] = useState(null); + const [isChildHovered, setIsChildHovered] = useState(false); + const [isParentHovered, setIsParentHovered] = useState(false); + + /** Note: This essentially adds a small delay to allow a user to move their mouse + * to the child component instead of just immediately closing it outright + */ + useEffect(() => { + let timeoutId: NodeJS.Timeout; + + if (!isParentHovered) { + timeoutId = setTimeout(() => { + if (!isChildHovered) { + setAnchorEl(null); + } + }, 100); + } + + return () => { + clearTimeout(timeoutId); + }; + }, [isParentHovered, isChildHovered]); return ( setAnchorEl(evt.currentTarget)} - onMouseLeave={() => setAnchorEl(null)} + onMouseEnter={(evt) => { + setAnchorEl(evt.currentTarget); + setIsParentHovered(true); + }} + onMouseLeave={() => { + setIsParentHovered(false); + }} sx={{ // HACK: Prevents the menu item to be in active style state when the sub-menu is opened. "&.MuiMenuItem-root": { @@ -51,7 +77,17 @@ export const CascadingMenuItem: FC = ({ }} {...PopperProps} > - + { + setIsChildHovered(true); + }} + onMouseLeave={() => { + setIsChildHovered(false); + setAnchorEl(null); + }} + > {children} diff --git a/src/shell/components/FieldTypeCurrency/FieldTypeCurrency.js b/src/shell/components/FieldTypeCurrency/FieldTypeCurrency.js deleted file mode 100644 index 19ccb43fea..0000000000 --- a/src/shell/components/FieldTypeCurrency/FieldTypeCurrency.js +++ /dev/null @@ -1,69 +0,0 @@ -import React, { useState } from "react"; - -import { Input } from "@zesty-io/core/Input"; -import { Select, Option } from "@zesty-io/core/Select"; -import { currencies } from "./currencies"; - -import styles from "./FieldTypeCurrency.less"; -export const FieldTypeCurrency = React.memo(function FieldTypeCurrency(props) { - // console.log("FieldTypeCurrency:render"); - - const [monetaryValue, setMonetaryValue] = useState(props.value || "0.00"); - const [currency, setCurrency] = useState( - (props.code && currencies[props.code]) || currencies["USD"] - ); - - return ( - - ); -}); diff --git a/src/shell/components/FieldTypeCurrency/FieldTypeCurrency.less b/src/shell/components/FieldTypeCurrency/FieldTypeCurrency.less deleted file mode 100644 index 03c4c02fe0..0000000000 --- a/src/shell/components/FieldTypeCurrency/FieldTypeCurrency.less +++ /dev/null @@ -1,52 +0,0 @@ -@import "~@zesty-io/core/colors.less"; -@import "~@zesty-io/core/typography.less"; - -.FieldTypeCurrency { - display: flex; - flex-direction: column; - max-width: 300px; - - .FieldTypeCurrencyLabel { - display: flex; - justify-content: space-between; - margin-bottom: 3px; - font-size: @font-size-label; - span { - display: flex; - } - } - - .CurrencyFields { - display: flex; - .SelectCurrency { - width: 100px; - span > span { - background: @zesty-tab-blue; - color: @white; - border: @zesty-tab-blue; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - // &:active, - // &:focus, - // &:hover { - // span > span { - // color: #5b667d; - // } - // } - - ul { - left: 0; - min-width: 320px; - } - } - .CurrencyInput { - width: 100%; - border-radius: 0px 8px 8px 0px; - } - } -} -.CurrencyInput { - border: 1px solid #f2f4f7; -} diff --git a/src/shell/components/FieldTypeCurrency/currencies.js b/src/shell/components/FieldTypeCurrency/currencies.ts similarity index 62% rename from src/shell/components/FieldTypeCurrency/currencies.js rename to src/shell/components/FieldTypeCurrency/currencies.ts index 10102d6fa9..ab439b5265 100644 --- a/src/shell/components/FieldTypeCurrency/currencies.js +++ b/src/shell/components/FieldTypeCurrency/currencies.ts @@ -1,1064 +1,1193 @@ -export const currencies = { - USD: { - symbol: " $ ", - name: "US Dollar", - symbol_native: "$", - decimal_digits: 2, - rounding: 0, - code: "USD", - name_plural: "US dollars", - }, - CAD: { - symbol: "CA$ ", - name: "Canadian Dollar", - symbol_native: "$", - decimal_digits: 2, - rounding: 0, - code: "CAD", - name_plural: "Canadian dollars", - }, - EUR: { - symbol: " € ", - name: "Euro", - symbol_native: "€", - decimal_digits: 2, - rounding: 0, - code: "EUR", - name_plural: "euros", - }, - AED: { - symbol: "AED", - name: "United Arab Emirates Dirham", - symbol_native: "د.إ.‏", - decimal_digits: 2, - rounding: 0, - code: "AED", - name_plural: "UAE dirhams", - }, - AFN: { +export type Currency = { + symbol: string; + label: string; + symbol_native: string; + decimal_digits: number; + rounding: number; + value: string; + name_plural: string; + countryCode: string; +}; + +export const currencies: Currency[] = [ + { symbol: "Af ", - name: "Afghan Afghani", + label: "Afghan Afghani", symbol_native: "؋", decimal_digits: 0, rounding: 0, - code: "AFN", + value: "AFN", name_plural: "Afghan Afghanis", + countryCode: "AF", }, - ALL: { + { symbol: "ALL", - name: "Albanian Lek", + label: "Albanian Lek", symbol_native: "Lek", decimal_digits: 0, rounding: 0, - code: "ALL", + value: "ALL", name_plural: "Albanian lekë", + countryCode: "AL", }, - AMD: { - symbol: "AMD", - name: "Armenian Dram", - symbol_native: "դր.", - decimal_digits: 0, + { + symbol: "DA", + label: "Algerian Dinar", + symbol_native: "د.ج.‏", + decimal_digits: 2, rounding: 0, - code: "AMD", - name_plural: "Armenian drams", + value: "DZD", + name_plural: "Algerian dinars", + countryCode: "DZ", }, - ARS: { + { symbol: "AR$", - name: "Argentine Peso", + label: "Argentine Peso", symbol_native: "$", decimal_digits: 2, rounding: 0, - code: "ARS", + value: "ARS", name_plural: "Argentine pesos", + countryCode: "AR", }, - AUD: { + { + symbol: "AMD", + label: "Armenian Dram", + symbol_native: "դր.", + decimal_digits: 0, + rounding: 0, + value: "AMD", + name_plural: "Armenian drams", + countryCode: "AM", + }, + { symbol: "AU$", - name: "Australian Dollar", + label: "Australian Dollar", symbol_native: "$", decimal_digits: 2, rounding: 0, - code: "AUD", + value: "AUD", name_plural: "Australian dollars", + countryCode: "AU", }, - AZN: { + { symbol: "man.", - name: "Azerbaijani Manat", + label: "Azerbaijani Manat", symbol_native: "ман.", decimal_digits: 2, rounding: 0, - code: "AZN", + value: "AZN", name_plural: "Azerbaijani manats", + countryCode: "AZ", }, - BAM: { - symbol: "KM", - name: "Bosnia-Herzegovina Convertible Mark", - symbol_native: "KM", - decimal_digits: 2, + { + symbol: "BD", + label: "Bahraini Dinar", + symbol_native: "د.ب.‏", + decimal_digits: 3, rounding: 0, - code: "BAM", - name_plural: "Bosnia-Herzegovina convertible marks", + value: "BHD", + name_plural: "Bahraini dinars", + countryCode: "BH", }, - BDT: { + { symbol: "Tk", - name: "Bangladeshi Taka", + label: "Bangladeshi Taka", symbol_native: "৳", decimal_digits: 2, rounding: 0, - code: "BDT", + value: "BDT", name_plural: "Bangladeshi takas", + countryCode: "BD", }, - BGN: { - symbol: "BGN", - name: "Bulgarian Lev", - symbol_native: "лв.", - decimal_digits: 2, - rounding: 0, - code: "BGN", - name_plural: "Bulgarian leva", - }, - BHD: { - symbol: "BD", - name: "Bahraini Dinar", - symbol_native: "د.ب.‏", - decimal_digits: 3, - rounding: 0, - code: "BHD", - name_plural: "Bahraini dinars", - }, - BIF: { - symbol: "FBu", - name: "Burundian Franc", - symbol_native: "FBu", + { + symbol: "BYR", + label: "Belarusian Ruble", + symbol_native: "BYR", decimal_digits: 0, rounding: 0, - code: "BIF", - name_plural: "Burundian francs", + value: "BYR", + name_plural: "Belarusian rubles", + countryCode: "BY", }, - BND: { - symbol: "BN$", - name: "Brunei Dollar", + { + symbol: "BZ$", + label: "Belize Dollar", symbol_native: "$", decimal_digits: 2, rounding: 0, - code: "BND", - name_plural: "Brunei dollars", + value: "BZD", + name_plural: "Belize dollars", + countryCode: "BZ", }, - BOB: { + { symbol: "Bs", - name: "Bolivian Boliviano", + label: "Bolivian Boliviano", symbol_native: "Bs", decimal_digits: 2, rounding: 0, - code: "BOB", + value: "BOB", name_plural: "Bolivian bolivianos", + countryCode: "BO", }, - BRL: { - symbol: "R$", - name: "Brazilian Real", - symbol_native: "R$", + { + symbol: "KM", + label: "Bosnia-Herzegovina Convertible Mark", + symbol_native: "KM", decimal_digits: 2, rounding: 0, - code: "BRL", - name_plural: "Brazilian reals", + value: "BAM", + name_plural: "Bosnia-Herzegovina convertible marks", + countryCode: "BA", }, - BWP: { + { symbol: "BWP", - name: "Botswanan Pula", + label: "Botswanan Pula", symbol_native: "P", decimal_digits: 2, rounding: 0, - code: "BWP", + value: "BWP", name_plural: "Botswanan pulas", + countryCode: "BW", }, - BYR: { - symbol: "BYR", - name: "Belarusian Ruble", - symbol_native: "BYR", - decimal_digits: 0, + { + symbol: "R$", + label: "Brazilian Real", + symbol_native: "R$", + decimal_digits: 2, rounding: 0, - code: "BYR", - name_plural: "Belarusian rubles", + value: "BRL", + name_plural: "Brazilian reals", + countryCode: "BR", }, - BZD: { - symbol: "BZ$", - name: "Belize Dollar", + { + symbol: "£", + label: "British Pound Sterling", + symbol_native: "£", + decimal_digits: 2, + rounding: 0, + value: "GBP", + name_plural: "British pounds sterling", + countryCode: "GB", + }, + { + symbol: "BN$", + label: "Brunei Dollar", symbol_native: "$", decimal_digits: 2, rounding: 0, - code: "BZD", - name_plural: "Belize dollars", + value: "BND", + name_plural: "Brunei dollars", + countryCode: "BN", }, - CDF: { - symbol: "CDF", - name: "Congolese Franc", - symbol_native: "FrCD", + { + symbol: "BGN", + label: "Bulgarian Lev", + symbol_native: "лв.", decimal_digits: 2, rounding: 0, - code: "CDF", - name_plural: "Congolese francs", + value: "BGN", + name_plural: "Bulgarian leva", + countryCode: "BG", }, - CHF: { - symbol: "CHF", - name: "Swiss Franc", - symbol_native: "CHF", + { + symbol: "FBu", + label: "Burundian Franc", + symbol_native: "FBu", + decimal_digits: 0, + rounding: 0, + value: "BIF", + name_plural: "Burundian francs", + countryCode: "BI", + }, + { + symbol: "KHR", + label: "Cambodian Riel", + symbol_native: "៛", decimal_digits: 2, - rounding: 0.05, - code: "CHF", - name_plural: "Swiss francs", + rounding: 0, + value: "KHR", + name_plural: "Cambodian riels", + countryCode: "KH", + }, + { + symbol: "CA$ ", + label: "Canadian Dollar", + symbol_native: "$", + decimal_digits: 2, + rounding: 0, + value: "CAD", + name_plural: "Canadian dollars", + countryCode: "CA", + }, + { + symbol: "CV$", + label: "Cape Verdean Escudo", + symbol_native: "CV$", + decimal_digits: 2, + rounding: 0, + value: "CVE", + name_plural: "Cape Verdean escudos", + countryCode: "CV", }, - CLP: { + { + symbol: "CFA", + label: "CFA Franc BCEAO", + symbol_native: "CFA", + decimal_digits: 0, + rounding: 0, + value: "XOF", + name_plural: "CFA francs BCEAO", + countryCode: "CI", + }, + { + symbol: "FCFA", + label: "CFA Franc BEAC", + symbol_native: "FCFA", + decimal_digits: 0, + rounding: 0, + value: "XAF", + name_plural: "CFA francs BEAC", + countryCode: "CF", + }, + { symbol: "CL$", - name: "Chilean Peso", + label: "Chilean Peso", symbol_native: "$", decimal_digits: 0, rounding: 0, - code: "CLP", + value: "CLP", name_plural: "Chilean pesos", + countryCode: "CL", }, - CNY: { + { symbol: "CN¥", - name: "Chinese Yuan", + label: "Chinese Yuan", symbol_native: "CN¥", decimal_digits: 2, rounding: 0, - code: "CNY", + value: "CNY", name_plural: "Chinese yuan", + countryCode: "CN", }, - COP: { + { symbol: "CO$", - name: "Colombian Peso", + label: "Colombian Peso", symbol_native: "$", decimal_digits: 0, rounding: 0, - code: "COP", + value: "COP", name_plural: "Colombian pesos", + countryCode: "CO", + }, + { + symbol: "CF", + label: "Comorian Franc", + symbol_native: "FC", + decimal_digits: 0, + rounding: 0, + value: "KMF", + name_plural: "Comorian francs", + countryCode: "KM", }, - CRC: { + { + symbol: "CDF", + label: "Congolese Franc", + symbol_native: "FrCD", + decimal_digits: 2, + rounding: 0, + value: "CDF", + name_plural: "Congolese francs", + countryCode: "CD", + }, + { symbol: "₡", - name: "Costa Rican Colón", + label: "Costa Rican Colón", symbol_native: "₡", decimal_digits: 0, rounding: 0, - code: "CRC", + value: "CRC", name_plural: "Costa Rican colóns", + countryCode: "CR", }, - CVE: { - symbol: "CV$", - name: "Cape Verdean Escudo", - symbol_native: "CV$", + { + symbol: "kn", + label: "Croatian Kuna", + symbol_native: "kn", decimal_digits: 2, rounding: 0, - code: "CVE", - name_plural: "Cape Verdean escudos", + value: "HRK", + name_plural: "Croatian kunas", + countryCode: "HR", }, - CZK: { + { symbol: "Kč", - name: "Czech Republic Koruna", + label: "Czech Republic Koruna", symbol_native: "Kč", decimal_digits: 2, rounding: 0, - code: "CZK", + value: "CZK", name_plural: "Czech Republic korunas", + countryCode: "CZ", }, - DJF: { - symbol: "Fdj", - name: "Djiboutian Franc", - symbol_native: "Fdj", - decimal_digits: 0, - rounding: 0, - code: "DJF", - name_plural: "Djiboutian francs", - }, - DKK: { + { symbol: "Dkr", - name: "Danish Krone", + label: "Danish Krone", symbol_native: "kr", decimal_digits: 2, rounding: 0, - code: "DKK", + value: "DKK", name_plural: "Danish kroner", + countryCode: "DK", }, - DOP: { + { + symbol: "Fdj", + label: "Djiboutian Franc", + symbol_native: "Fdj", + decimal_digits: 0, + rounding: 0, + value: "DJF", + name_plural: "Djiboutian francs", + countryCode: "DJ", + }, + { symbol: "RD$", - name: "Dominican Peso", + label: "Dominican Peso", symbol_native: "RD$", decimal_digits: 2, rounding: 0, - code: "DOP", + value: "DOP", name_plural: "Dominican pesos", + countryCode: "DO", }, - DZD: { - symbol: "DA", - name: "Algerian Dinar", - symbol_native: "د.ج.‏", - decimal_digits: 2, - rounding: 0, - code: "DZD", - name_plural: "Algerian dinars", - }, - EEK: { - symbol: "Ekr", - name: "Estonian Kroon", - symbol_native: "kr", - decimal_digits: 2, - rounding: 0, - code: "EEK", - name_plural: "Estonian kroons", - }, - EGP: { + { symbol: "EGP", - name: "Egyptian Pound", + label: "Egyptian Pound", symbol_native: "ج.م.‏", decimal_digits: 2, rounding: 0, - code: "EGP", + value: "EGP", name_plural: "Egyptian pounds", + countryCode: "EG", }, - ERN: { + { symbol: "Nfk", - name: "Eritrean Nakfa", + label: "Eritrean Nakfa", symbol_native: "Nfk", decimal_digits: 2, rounding: 0, - code: "ERN", + value: "ERN", name_plural: "Eritrean nakfas", + countryCode: "ER", }, - ETB: { + { + symbol: "Ekr", + label: "Estonian Kroon", + symbol_native: "kr", + decimal_digits: 2, + rounding: 0, + value: "EEK", + name_plural: "Estonian kroons", + countryCode: "EE", + }, + { symbol: "Br", - name: "Ethiopian Birr", + label: "Ethiopian Birr", symbol_native: "Br", decimal_digits: 2, rounding: 0, - code: "ETB", + value: "ETB", name_plural: "Ethiopian birrs", + countryCode: "ET", }, - GBP: { - symbol: "£", - name: "British Pound Sterling", - symbol_native: "£", + { + symbol: " € ", + label: "Euro", + symbol_native: "€", decimal_digits: 2, rounding: 0, - code: "GBP", - name_plural: "British pounds sterling", + value: "EUR", + name_plural: "euros", + countryCode: "EU", }, - GEL: { + { symbol: "GEL", - name: "Georgian Lari", + label: "Georgian Lari", symbol_native: "GEL", decimal_digits: 2, rounding: 0, - code: "GEL", + value: "GEL", name_plural: "Georgian laris", + countryCode: "GE", }, - GHS: { + { symbol: "GH₵", - name: "Ghanaian Cedi", + label: "Ghanaian Cedi", symbol_native: "GH₵", decimal_digits: 2, rounding: 0, - code: "GHS", + value: "GHS", name_plural: "Ghanaian cedis", + countryCode: "GH", }, - GNF: { - symbol: "FG", - name: "Guinean Franc", - symbol_native: "FG", - decimal_digits: 0, - rounding: 0, - code: "GNF", - name_plural: "Guinean francs", - }, - GTQ: { + { symbol: "GTQ", - name: "Guatemalan Quetzal", + label: "Guatemalan Quetzal", symbol_native: "Q", decimal_digits: 2, rounding: 0, - code: "GTQ", + value: "GTQ", name_plural: "Guatemalan quetzals", + countryCode: "GT", }, - HKD: { - symbol: "HK$", - name: "Hong Kong Dollar", - symbol_native: "$", - decimal_digits: 2, + { + symbol: "FG", + label: "Guinean Franc", + symbol_native: "FG", + decimal_digits: 0, rounding: 0, - code: "HKD", - name_plural: "Hong Kong dollars", + value: "GNF", + name_plural: "Guinean francs", + countryCode: "GN", }, - HNL: { + { symbol: "HNL", - name: "Honduran Lempira", + label: "Honduran Lempira", symbol_native: "L", decimal_digits: 2, rounding: 0, - code: "HNL", + value: "HNL", name_plural: "Honduran lempiras", + countryCode: "HN", }, - HRK: { - symbol: "kn", - name: "Croatian Kuna", - symbol_native: "kn", + { + symbol: "HK$", + label: "Hong Kong Dollar", + symbol_native: "$", decimal_digits: 2, rounding: 0, - code: "HRK", - name_plural: "Croatian kunas", + value: "HKD", + name_plural: "Hong Kong dollars", + countryCode: "HK", }, - HUF: { + { symbol: "Ft", - name: "Hungarian Forint", + label: "Hungarian Forint", symbol_native: "Ft", decimal_digits: 0, rounding: 0, - code: "HUF", + value: "HUF", name_plural: "Hungarian forints", + countryCode: "HU", }, - IDR: { - symbol: "Rp", - name: "Indonesian Rupiah", - symbol_native: "Rp", + { + symbol: "Ikr", + label: "Icelandic Króna", + symbol_native: "kr", decimal_digits: 0, rounding: 0, - code: "IDR", - name_plural: "Indonesian rupiahs", - }, - ILS: { - symbol: "₪", - name: "Israeli New Sheqel", - symbol_native: "₪", - decimal_digits: 2, - rounding: 0, - code: "ILS", - name_plural: "Israeli new sheqels", + value: "ISK", + name_plural: "Icelandic krónur", + countryCode: "IS", }, - INR: { + { symbol: "Rs", - name: "Indian Rupee", - symbol_native: "টকা", + label: "Indian Rupee", + symbol_native: "₹", decimal_digits: 2, rounding: 0, - code: "INR", + value: "INR", name_plural: "Indian rupees", + countryCode: "IN", }, - IQD: { - symbol: "IQD", - name: "Iraqi Dinar", - symbol_native: "د.ع.‏", + { + symbol: "Rp", + label: "Indonesian Rupiah", + symbol_native: "Rp", decimal_digits: 0, rounding: 0, - code: "IQD", - name_plural: "Iraqi dinars", + value: "IDR", + name_plural: "Indonesian rupiahs", + countryCode: "ID", }, - IRR: { + { symbol: "IRR", - name: "Iranian Rial", + label: "Iranian Rial", symbol_native: "﷼", decimal_digits: 0, rounding: 0, - code: "IRR", + value: "IRR", name_plural: "Iranian rials", + countryCode: "IR", }, - ISK: { - symbol: "Ikr", - name: "Icelandic Króna", - symbol_native: "kr", + { + symbol: "IQD", + label: "Iraqi Dinar", + symbol_native: "د.ع.‏", decimal_digits: 0, rounding: 0, - code: "ISK", - name_plural: "Icelandic krónur", + value: "IQD", + name_plural: "Iraqi dinars", + countryCode: "IQ", + }, + { + symbol: "₪", + label: "Israeli New Sheqel", + symbol_native: "₪", + decimal_digits: 2, + rounding: 0, + value: "ILS", + name_plural: "Israeli new sheqels", + countryCode: "IL", }, - JMD: { + { symbol: "J$", - name: "Jamaican Dollar", + label: "Jamaican Dollar", symbol_native: "$", decimal_digits: 2, rounding: 0, - code: "JMD", + value: "JMD", name_plural: "Jamaican dollars", + countryCode: "JM", }, - JOD: { - symbol: "JD", - name: "Jordanian Dinar", - symbol_native: "د.أ.‏", - decimal_digits: 3, - rounding: 0, - code: "JOD", - name_plural: "Jordanian dinars", - }, - JPY: { + { symbol: "¥", - name: "Japanese Yen", + label: "Japanese Yen", symbol_native: "¥", decimal_digits: 0, rounding: 0, - code: "JPY", + value: "JPY", name_plural: "Japanese yen", + countryCode: "JP", }, - KES: { - symbol: "Ksh", - name: "Kenyan Shilling", - symbol_native: "Ksh", - decimal_digits: 2, - rounding: 0, - code: "KES", - name_plural: "Kenyan shillings", - }, - KHR: { - symbol: "KHR", - name: "Cambodian Riel", - symbol_native: "៛", - decimal_digits: 2, - rounding: 0, - code: "KHR", - name_plural: "Cambodian riels", - }, - KMF: { - symbol: "CF", - name: "Comorian Franc", - symbol_native: "FC", - decimal_digits: 0, - rounding: 0, - code: "KMF", - name_plural: "Comorian francs", - }, - KRW: { - symbol: "₩", - name: "South Korean Won", - symbol_native: "₩", - decimal_digits: 0, - rounding: 0, - code: "KRW", - name_plural: "South Korean won", - }, - KWD: { - symbol: "KD", - name: "Kuwaiti Dinar", - symbol_native: "د.ك.‏", + { + symbol: "JD", + label: "Jordanian Dinar", + symbol_native: "د.أ.‏", decimal_digits: 3, rounding: 0, - code: "KWD", - name_plural: "Kuwaiti dinars", + value: "JOD", + name_plural: "Jordanian dinars", + countryCode: "JO", }, - KZT: { + { symbol: "KZT", - name: "Kazakhstani Tenge", + label: "Kazakhstani Tenge", symbol_native: "тңг.", - decimal_digits: 2, - rounding: 0, - code: "KZT", - name_plural: "Kazakhstani tenges", - }, - LBP: { - symbol: "LB£", - name: "Lebanese Pound", - symbol_native: "ل.ل.‏", - decimal_digits: 0, + decimal_digits: 2, rounding: 0, - code: "LBP", - name_plural: "Lebanese pounds", + value: "KZT", + name_plural: "Kazakhstani tenges", + countryCode: "KZ", }, - LKR: { - symbol: "SLRs", - name: "Sri Lankan Rupee", - symbol_native: "SL Re", + { + symbol: "Ksh", + label: "Kenyan Shilling", + symbol_native: "Ksh", decimal_digits: 2, rounding: 0, - code: "LKR", - name_plural: "Sri Lankan rupees", + value: "KES", + name_plural: "Kenyan shillings", + countryCode: "KE", }, - LTL: { - symbol: "Lt", - name: "Lithuanian Litas", - symbol_native: "Lt", - decimal_digits: 2, + { + symbol: "KD", + label: "Kuwaiti Dinar", + symbol_native: "د.ك.‏", + decimal_digits: 3, rounding: 0, - code: "LTL", - name_plural: "Lithuanian litai", + value: "KWD", + name_plural: "Kuwaiti dinars", + countryCode: "KW", }, - LVL: { + { symbol: "Ls", - name: "Latvian Lats", + label: "Latvian Lats", symbol_native: "Ls", decimal_digits: 2, rounding: 0, - code: "LVL", + value: "LVL", name_plural: "Latvian lati", + countryCode: "LV", + }, + { + symbol: "LB£", + label: "Lebanese Pound", + symbol_native: "ل.ل.‏", + decimal_digits: 0, + rounding: 0, + value: "LBP", + name_plural: "Lebanese pounds", + countryCode: "LB", }, - LYD: { + { symbol: "LD", - name: "Libyan Dinar", + label: "Libyan Dinar", symbol_native: "د.ل.‏", decimal_digits: 3, rounding: 0, - code: "LYD", + value: "LYD", name_plural: "Libyan dinars", + countryCode: "LY", }, - MAD: { - symbol: "MAD", - name: "Moroccan Dirham", - symbol_native: "د.م.‏", + { + symbol: "Lt", + label: "Lithuanian Litas", + symbol_native: "Lt", decimal_digits: 2, rounding: 0, - code: "MAD", - name_plural: "Moroccan dirhams", + value: "LTL", + name_plural: "Lithuanian litai", + countryCode: "LT", }, - MDL: { - symbol: "MDL", - name: "Moldovan Leu", - symbol_native: "MDL", + { + symbol: "MOP$", + label: "Macanese Pataca", + symbol_native: "MOP$", decimal_digits: 2, rounding: 0, - code: "MDL", - name_plural: "Moldovan lei", - }, - MGA: { - symbol: "MGA", - name: "Malagasy Ariary", - symbol_native: "MGA", - decimal_digits: 0, - rounding: 0, - code: "MGA", - name_plural: "Malagasy Ariaries", + value: "MOP", + name_plural: "Macanese patacas", + countryCode: "MO", }, - MKD: { + { symbol: "MKD", - name: "Macedonian Denar", - symbol_native: "MKD", + label: "Macedonian Denar", + symbol_native: "ден", decimal_digits: 2, rounding: 0, - code: "MKD", + value: "MKD", name_plural: "Macedonian denari", + countryCode: "MK", }, - MMK: { - symbol: "MMK", - name: "Myanma Kyat", - symbol_native: "K", + { + symbol: "MGA", + label: "Malagasy Ariary", + symbol_native: "MGA", decimal_digits: 0, rounding: 0, - code: "MMK", - name_plural: "Myanma kyats", + value: "MGA", + name_plural: "Malagasy Ariaries", + countryCode: "MG", }, - MOP: { - symbol: "MOP$", - name: "Macanese Pataca", - symbol_native: "MOP$", + { + symbol: "RM", + label: "Malaysian Ringgit", + symbol_native: "RM", decimal_digits: 2, rounding: 0, - code: "MOP", - name_plural: "Macanese patacas", + value: "MYR", + name_plural: "Malaysian ringgits", + countryCode: "MY", }, - MUR: { + { symbol: "MURs", - name: "Mauritian Rupee", + label: "Mauritian Rupee", symbol_native: "MURs", decimal_digits: 0, rounding: 0, - code: "MUR", + value: "MUR", name_plural: "Mauritian rupees", + countryCode: "MU", }, - MXN: { + { symbol: "MX$", - name: "Mexican Peso", + label: "Mexican Peso", symbol_native: "$", decimal_digits: 2, rounding: 0, - code: "MXN", + value: "MXN", name_plural: "Mexican pesos", + countryCode: "MX", }, - MYR: { - symbol: "RM", - name: "Malaysian Ringgit", - symbol_native: "RM", + { + symbol: "MDL", + label: "Moldovan Leu", + symbol_native: "MDL", decimal_digits: 2, rounding: 0, - code: "MYR", - name_plural: "Malaysian ringgits", + value: "MDL", + name_plural: "Moldovan lei", + countryCode: "MD", + }, + { + symbol: "MAD", + label: "Moroccan Dirham", + symbol_native: "د.م.‏", + decimal_digits: 2, + rounding: 0, + value: "MAD", + name_plural: "Moroccan dirhams", + countryCode: "MA", }, - MZN: { + { symbol: "MTn", - name: "Mozambican Metical", + label: "Mozambican Metical", symbol_native: "MTn", decimal_digits: 2, rounding: 0, - code: "MZN", + value: "MZN", name_plural: "Mozambican meticals", + countryCode: "MZ", + }, + { + symbol: "MMK", + label: "Myanma Kyat", + symbol_native: "K", + decimal_digits: 0, + rounding: 0, + value: "MMK", + name_plural: "Myanma kyats", + countryCode: "MM", }, - NAD: { + { symbol: "N$", - name: "Namibian Dollar", + label: "Namibian Dollar", symbol_native: "N$", decimal_digits: 2, rounding: 0, - code: "NAD", + value: "NAD", name_plural: "Namibian dollars", + countryCode: "NA", }, - NGN: { - symbol: "₦", - name: "Nigerian Naira", - symbol_native: "₦", + { + symbol: "NPRs", + label: "Nepalese Rupee", + symbol_native: "नेरू", decimal_digits: 2, rounding: 0, - code: "NGN", - name_plural: "Nigerian nairas", + value: "NPR", + name_plural: "Nepalese rupees", + countryCode: "NP", }, - NIO: { - symbol: "C$", - name: "Nicaraguan Córdoba", - symbol_native: "C$", + { + symbol: "NT$", + label: "New Taiwan Dollar", + symbol_native: "NT$", decimal_digits: 2, rounding: 0, - code: "NIO", - name_plural: "Nicaraguan córdobas", + value: "TWD", + name_plural: "New Taiwan dollars", + countryCode: "TW", }, - NOK: { - symbol: "Nkr", - name: "Norwegian Krone", - symbol_native: "kr", + { + symbol: "NZ$", + label: "New Zealand Dollar", + symbol_native: "$", decimal_digits: 2, rounding: 0, - code: "NOK", - name_plural: "Norwegian kroner", + value: "NZD", + name_plural: "New Zealand dollars", + countryCode: "NZ", }, - NPR: { - symbol: "NPRs", - name: "Nepalese Rupee", - symbol_native: "नेरू", + { + symbol: "C$", + label: "Nicaraguan Córdoba", + symbol_native: "C$", decimal_digits: 2, rounding: 0, - code: "NPR", - name_plural: "Nepalese rupees", + value: "NIO", + name_plural: "Nicaraguan córdobas", + countryCode: "NI", }, - NZD: { - symbol: "NZ$", - name: "New Zealand Dollar", - symbol_native: "$", + { + symbol: "₦", + label: "Nigerian Naira", + symbol_native: "₦", decimal_digits: 2, rounding: 0, - code: "NZD", - name_plural: "New Zealand dollars", + value: "NGN", + name_plural: "Nigerian nairas", + countryCode: "NG", + }, + { + symbol: "Nkr", + label: "Norwegian Krone", + symbol_native: "kr", + decimal_digits: 2, + rounding: 0, + value: "NOK", + name_plural: "Norwegian kroner", + countryCode: "NO", }, - OMR: { + { symbol: "OMR", - name: "Omani Rial", + label: "Omani Rial", symbol_native: "ر.ع.‏", decimal_digits: 3, rounding: 0, - code: "OMR", + value: "OMR", name_plural: "Omani rials", + countryCode: "OM", + }, + { + symbol: "PKRs", + label: "Pakistani Rupee", + symbol_native: "₨", + decimal_digits: 0, + rounding: 0, + value: "PKR", + name_plural: "Pakistani rupees", + countryCode: "PK", }, - PAB: { + { symbol: "B/.", - name: "Panamanian Balboa", + label: "Panamanian Balboa", symbol_native: "B/.", decimal_digits: 2, rounding: 0, - code: "PAB", + value: "PAB", name_plural: "Panamanian balboas", + countryCode: "PA", + }, + { + symbol: "₲", + label: "Paraguayan Guarani", + symbol_native: "₲", + decimal_digits: 0, + rounding: 0, + value: "PYG", + name_plural: "Paraguayan guaranis", + countryCode: "PY", }, - PEN: { + { symbol: "S/.", - name: "Peruvian Nuevo Sol", + label: "Peruvian Nuevo Sol", symbol_native: "S/.", decimal_digits: 2, rounding: 0, - code: "PEN", + value: "PEN", name_plural: "Peruvian nuevos soles", + countryCode: "PE", }, - PHP: { + { symbol: "₱", - name: "Philippine Peso", + label: "Philippine Peso", symbol_native: "₱", decimal_digits: 2, rounding: 0, - code: "PHP", + value: "PHP", name_plural: "Philippine pesos", + countryCode: "PH", }, - PKR: { - symbol: "PKRs", - name: "Pakistani Rupee", - symbol_native: "₨", - decimal_digits: 0, - rounding: 0, - code: "PKR", - name_plural: "Pakistani rupees", - }, - PLN: { + { symbol: "zł", - name: "Polish Zloty", + label: "Polish Zloty", symbol_native: "zł", decimal_digits: 2, rounding: 0, - code: "PLN", + value: "PLN", name_plural: "Polish zlotys", + countryCode: "PL", }, - PYG: { - symbol: "₲", - name: "Paraguayan Guarani", - symbol_native: "₲", - decimal_digits: 0, - rounding: 0, - code: "PYG", - name_plural: "Paraguayan guaranis", - }, - QAR: { + { symbol: "QR", - name: "Qatari Rial", + label: "Qatari Rial", symbol_native: "ر.ق.‏", decimal_digits: 2, rounding: 0, - code: "QAR", + value: "QAR", name_plural: "Qatari rials", + countryCode: "QA", }, - RON: { + { symbol: "RON", - name: "Romanian Leu", + label: "Romanian Leu", symbol_native: "RON", decimal_digits: 2, rounding: 0, - code: "RON", + value: "RON", name_plural: "Romanian lei", + countryCode: "RO", }, - RSD: { - symbol: "din.", - name: "Serbian Dinar", - symbol_native: "дин.", - decimal_digits: 0, - rounding: 0, - code: "RSD", - name_plural: "Serbian dinars", - }, - RUB: { + { symbol: "RUB", - name: "Russian Ruble", + label: "Russian Ruble", symbol_native: "руб.", decimal_digits: 2, rounding: 0, - code: "RUB", + value: "RUB", name_plural: "Russian rubles", + countryCode: "RU", }, - RWF: { + { symbol: "RWF", - name: "Rwandan Franc", + label: "Rwandan Franc", symbol_native: "FR", decimal_digits: 0, rounding: 0, - code: "RWF", + value: "RWF", name_plural: "Rwandan francs", + countryCode: "RW", }, - SAR: { + { symbol: "SR", - name: "Saudi Riyal", + label: "Saudi Riyal", symbol_native: "ر.س.‏", decimal_digits: 2, rounding: 0, - code: "SAR", + value: "SAR", name_plural: "Saudi riyals", + countryCode: "SA", }, - SDG: { - symbol: "SDG", - name: "Sudanese Pound", - symbol_native: "SDG", - decimal_digits: 2, - rounding: 0, - code: "SDG", - name_plural: "Sudanese pounds", - }, - SEK: { - symbol: "Skr", - name: "Swedish Krona", - symbol_native: "kr", - decimal_digits: 2, + { + symbol: "din.", + label: "Serbian Dinar", + symbol_native: "дин.", + decimal_digits: 0, rounding: 0, - code: "SEK", - name_plural: "Swedish kronor", + value: "RSD", + name_plural: "Serbian dinars", + countryCode: "RS", }, - SGD: { + { symbol: "S$", - name: "Singapore Dollar", + label: "Singapore Dollar", symbol_native: "$", decimal_digits: 2, rounding: 0, - code: "SGD", + value: "SGD", name_plural: "Singapore dollars", + countryCode: "SG", }, - SOS: { + { symbol: "Ssh", - name: "Somali Shilling", + label: "Somali Shilling", symbol_native: "Ssh", decimal_digits: 0, rounding: 0, - code: "SOS", + value: "SOS", name_plural: "Somali shillings", + countryCode: "SO", + }, + { + symbol: "R", + label: "South African Rand", + symbol_native: "R", + decimal_digits: 2, + rounding: 0, + value: "ZAR", + name_plural: "South African rand", + countryCode: "ZA", + }, + { + symbol: "₩", + label: "South Korean Won", + symbol_native: "₩", + decimal_digits: 0, + rounding: 0, + value: "KRW", + name_plural: "South Korean won", + countryCode: "KR", + }, + { + symbol: "SLRs", + label: "Sri Lankan Rupee", + symbol_native: "SL Re", + decimal_digits: 2, + rounding: 0, + value: "LKR", + name_plural: "Sri Lankan rupees", + countryCode: "LK", + }, + { + symbol: "SDG", + label: "Sudanese Pound", + symbol_native: "SDG", + decimal_digits: 2, + rounding: 0, + value: "SDG", + name_plural: "Sudanese pounds", + countryCode: "SD", + }, + { + symbol: "Skr", + label: "Swedish Krona", + symbol_native: "kr", + decimal_digits: 2, + rounding: 0, + value: "SEK", + name_plural: "Swedish kronor", + countryCode: "SE", + }, + { + symbol: "CHF", + label: "Swiss Franc", + symbol_native: "CHF", + decimal_digits: 2, + rounding: 0.05, + value: "CHF", + name_plural: "Swiss francs", + countryCode: "CH", }, - SYP: { + { symbol: "SY£", - name: "Syrian Pound", + label: "Syrian Pound", symbol_native: "ل.س.‏", decimal_digits: 0, rounding: 0, - code: "SYP", + value: "SYP", name_plural: "Syrian pounds", + countryCode: "SY", + }, + { + symbol: "TSh", + label: "Tanzanian Shilling", + symbol_native: "TSh", + decimal_digits: 0, + rounding: 0, + value: "TZS", + name_plural: "Tanzanian shillings", + countryCode: "TZ", }, - THB: { + { symbol: "฿", - name: "Thai Baht", + label: "Thai Baht", symbol_native: "฿", decimal_digits: 2, rounding: 0, - code: "THB", + value: "THB", name_plural: "Thai baht", + countryCode: "TH", }, - TND: { - symbol: "DT", - name: "Tunisian Dinar", - symbol_native: "د.ت.‏", - decimal_digits: 3, - rounding: 0, - code: "TND", - name_plural: "Tunisian dinars", - }, - TOP: { + { symbol: "T$", - name: "Tongan Paʻanga", + label: "Tongan Paʻanga", symbol_native: "T$", decimal_digits: 2, rounding: 0, - code: "TOP", + value: "TOP", name_plural: "Tongan paʻanga", + countryCode: "TO", }, - TRY: { - symbol: "TL", - name: "Turkish Lira", - symbol_native: "TL", - decimal_digits: 2, - rounding: 0, - code: "TRY", - name_plural: "Turkish Lira", - }, - TTD: { + { symbol: "TT$", - name: "Trinidad and Tobago Dollar", + label: "Trinidad and Tobago Dollar", symbol_native: "$", decimal_digits: 2, rounding: 0, - code: "TTD", + value: "TTD", name_plural: "Trinidad and Tobago dollars", + countryCode: "TT", }, - TWD: { - symbol: "NT$", - name: "New Taiwan Dollar", - symbol_native: "NT$", + { + symbol: "DT", + label: "Tunisian Dinar", + symbol_native: "د.ت.‏", + decimal_digits: 3, + rounding: 0, + value: "TND", + name_plural: "Tunisian dinars", + countryCode: "TN", + }, + { + symbol: "TL", + label: "Turkish Lira", + symbol_native: "TL", decimal_digits: 2, rounding: 0, - code: "TWD", - name_plural: "New Taiwan dollars", + value: "TRY", + name_plural: "Turkish Lira", + countryCode: "TR", }, - TZS: { - symbol: "TSh", - name: "Tanzanian Shilling", - symbol_native: "TSh", + { + symbol: "USh", + label: "Ugandan Shilling", + symbol_native: "USh", decimal_digits: 0, rounding: 0, - code: "TZS", - name_plural: "Tanzanian shillings", + value: "UGX", + name_plural: "Ugandan shillings", + countryCode: "UG", }, - UAH: { + { symbol: "₴", - name: "Ukrainian Hryvnia", + label: "Ukrainian Hryvnia", symbol_native: "₴", decimal_digits: 2, rounding: 0, - code: "UAH", + value: "UAH", name_plural: "Ukrainian hryvnias", + countryCode: "UA", }, - UGX: { - symbol: "USh", - name: "Ugandan Shilling", - symbol_native: "USh", - decimal_digits: 0, + { + symbol: "AED", + label: "United Arab Emirates Dirham", + symbol_native: "د.إ.‏", + decimal_digits: 2, rounding: 0, - code: "UGX", - name_plural: "Ugandan shillings", + value: "AED", + name_plural: "UAE dirhams", + countryCode: "AE", + }, + { + symbol: " $ ", + label: "United States Dollar", + symbol_native: "$", + decimal_digits: 2, + rounding: 0, + value: "USD", + name_plural: "US dollars", + countryCode: "US", }, - UYU: { + { symbol: "$U", - name: "Uruguayan Peso", + label: "Uruguayan Peso", symbol_native: "$", decimal_digits: 2, rounding: 0, - code: "UYU", + value: "UYU", name_plural: "Uruguayan pesos", + countryCode: "UY", }, - UZS: { + { symbol: "UZS", - name: "Uzbekistan Som", + label: "Uzbekistan Som", symbol_native: "UZS", decimal_digits: 0, rounding: 0, - code: "UZS", + value: "UZS", name_plural: "Uzbekistan som", + countryCode: "UZ", }, - VEF: { + { symbol: "Bs.F.", - name: "Venezuelan Bolívar", + label: "Venezuelan Bolívar", symbol_native: "Bs.F.", decimal_digits: 2, rounding: 0, - code: "VEF", + value: "VEF", name_plural: "Venezuelan bolívars", + countryCode: "VE", }, - VND: { + { symbol: "₫", - name: "Vietnamese Dong", + label: "Vietnamese Dong", symbol_native: "₫", decimal_digits: 0, rounding: 0, - code: "VND", + value: "VND", name_plural: "Vietnamese dong", + countryCode: "VN", }, - XAF: { - symbol: "FCFA", - name: "CFA Franc BEAC", - symbol_native: "FCFA", - decimal_digits: 0, - rounding: 0, - code: "XAF", - name_plural: "CFA francs BEAC", - }, - XOF: { - symbol: "CFA", - name: "CFA Franc BCEAO", - symbol_native: "CFA", - decimal_digits: 0, - rounding: 0, - code: "XOF", - name_plural: "CFA francs BCEAO", - }, - YER: { + { symbol: "YR", - name: "Yemeni Rial", + label: "Yemeni Rial", symbol_native: "ر.ي.‏", decimal_digits: 0, rounding: 0, - code: "YER", + value: "YER", name_plural: "Yemeni rials", + countryCode: "YE", }, - ZAR: { - symbol: "R", - name: "South African Rand", - symbol_native: "R", - decimal_digits: 2, - rounding: 0, - code: "ZAR", - name_plural: "South African rand", - }, - ZMK: { + { symbol: "ZK", - name: "Zambian Kwacha", + label: "Zambian Kwacha", symbol_native: "ZK", decimal_digits: 0, rounding: 0, - code: "ZMK", + value: "ZMK", name_plural: "Zambian kwachas", + countryCode: "ZM", }, -}; +]; diff --git a/src/shell/components/FieldTypeCurrency/index.js b/src/shell/components/FieldTypeCurrency/index.js deleted file mode 100644 index ad300a9373..0000000000 --- a/src/shell/components/FieldTypeCurrency/index.js +++ /dev/null @@ -1 +0,0 @@ -export { FieldTypeCurrency } from "./FieldTypeCurrency"; diff --git a/src/shell/components/FieldTypeCurrency/index.tsx b/src/shell/components/FieldTypeCurrency/index.tsx new file mode 100644 index 0000000000..a74cad89c9 --- /dev/null +++ b/src/shell/components/FieldTypeCurrency/index.tsx @@ -0,0 +1,64 @@ +import { useMemo } from "react"; +import { TextField, Typography, Box, Stack } from "@mui/material"; + +import { currencies } from "./currencies"; +import { NumberFormatInput } from "../NumberFormatInput"; + +type FieldTypeCurrencyProps = { + name: string; + value: string; + currency: string; + error: boolean; + onChange: (value: string, name: string) => void; +}; +export const FieldTypeCurrency = ({ + name, + currency, + value, + error, + onChange, + ...otherProps +}: FieldTypeCurrencyProps) => { + const selectedCurrency = useMemo(() => { + return currencies.find((_currency) => _currency.value === currency); + }, [currency]); + + return ( + onChange(evt?.target?.value?.value, name)} + InputProps={{ + inputComponent: NumberFormatInput as any, + inputProps: { + thousandSeparator: true, + valueIsNumericString: true, + }, + startAdornment: ( + + {selectedCurrency?.symbol_native} + + ), + endAdornment: ( + + + + {selectedCurrency.value} + + + ), + }} + /> + ); +}; diff --git a/src/shell/components/FieldTypeNumber.tsx b/src/shell/components/FieldTypeNumber.tsx index aacf8169f1..5329ea7020 100644 --- a/src/shell/components/FieldTypeNumber.tsx +++ b/src/shell/components/FieldTypeNumber.tsx @@ -66,8 +66,10 @@ export const FieldTypeNumber = ({ value={value || 0} name={name} required={required} - onChange={(evt) => { - onChange(+evt.target.value?.toString()?.replace(/^0+/, "") ?? 0, name); + onChange={(evt: any) => { + const value = evt?.target?.value?.floatValue ?? 0; + + onChange(+value?.toString()?.replace(/^0+/, "") ?? 0, name); }} onKeyDown={(evt) => { if ((evt.key === "Backspace" || evt.key === "Delete") && value === 0) { diff --git a/src/shell/components/Filters/FilterButton.tsx b/src/shell/components/Filters/FilterButton.tsx index 04f3874e6f..917d7d5e1e 100644 --- a/src/shell/components/Filters/FilterButton.tsx +++ b/src/shell/components/Filters/FilterButton.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; import { Button, ButtonGroup, Typography } from "@mui/material"; -import ArrowDropDownOutlinedIcon from "@mui/icons-material/ArrowDropDownOutlined"; +import KeyboardArrowDownRoundedIcon from "@mui/icons-material/KeyboardArrowDownRounded"; import CheckIcon from "@mui/icons-material/Check"; import CloseRoundedIcon from "@mui/icons-material/CloseRounded"; @@ -53,7 +53,7 @@ export const FilterButton: FC = ({ variant="outlined" size="small" color="inherit" - endIcon={} + endIcon={} onClick={onOpenMenu} data-cy={`${filterId}_default`} sx={{ diff --git a/src/shell/components/InviteMembersModal/index.tsx b/src/shell/components/InviteMembersModal/index.tsx index 5c5b5ce2b0..5e6b9a7035 100644 --- a/src/shell/components/InviteMembersModal/index.tsx +++ b/src/shell/components/InviteMembersModal/index.tsx @@ -21,7 +21,7 @@ import { useGetCurrentUserRolesQuery, } from "../../services/accounts"; import { LoadingButton } from "@mui/lab"; -import { NoPermission } from "./NoPermission"; +import { NoPermission } from "../NoPermission"; import instanzeZUID from "../../../utility/instanceZUID"; import { ConfirmationModal } from "./ConfirmationDialog"; diff --git a/src/shell/components/InviteMembersModal/NoPermission.tsx b/src/shell/components/NoPermission.tsx similarity index 80% rename from src/shell/components/InviteMembersModal/NoPermission.tsx rename to src/shell/components/NoPermission.tsx index b22c8d1bc3..9ca0ec879f 100644 --- a/src/shell/components/InviteMembersModal/NoPermission.tsx +++ b/src/shell/components/NoPermission.tsx @@ -15,14 +15,20 @@ import { } from "@mui/material"; import ErrorRoundedIcon from "@mui/icons-material/ErrorRounded"; -import { useGetUsersRolesQuery } from "../../services/accounts"; -import { MD5 } from "../../../utility/md5"; +import { useGetUsersRolesQuery } from "../services/accounts"; +import { MD5 } from "../../utility/md5"; type NoPermissionProps = { onClose: () => void; + headerTitle?: string; + headerSubtitle?: string; }; -export const NoPermission = ({ onClose }: NoPermissionProps) => { +export const NoPermission = ({ + onClose, + headerSubtitle, + headerTitle, +}: NoPermissionProps) => { const { data: users } = useGetUsersRolesQuery(); const ownersAndAdmins = useMemo(() => { @@ -51,12 +57,14 @@ export const NoPermission = ({ onClose }: NoPermissionProps) => { }} /> - You do not have permission to invite users + {headerTitle + ? headerTitle + : "You do not have permission to invite users"} - Contact your instance owners or administrators listed below to change - your role to Admin or Owner on this instance for user invitation - priveleges. + {headerSubtitle + ? headerSubtitle + : "Contact your instance owners or administrators listed below to change your role to Admin or Owner on this instance for user invitation priveleges."} diff --git a/src/shell/components/NumberFormatInput/index.tsx b/src/shell/components/NumberFormatInput/index.tsx index 6efa07b581..db7effd76a 100644 --- a/src/shell/components/NumberFormatInput/index.tsx +++ b/src/shell/components/NumberFormatInput/index.tsx @@ -3,10 +3,17 @@ import { NumericFormatProps, InputAttributes, NumericFormat, + NumberFormatValues, } from "react-number-format"; +export type NumberFormatInputEvent = { + target: { + name: string; + value: NumberFormatValues; + }; +}; type NumberFormatInputProps = { - onChange: (event: { target: { name: string; value: number } }) => void; + onChange: (event: NumberFormatInputEvent) => void; name: string; }; export const NumberFormatInput = forwardRef< @@ -23,7 +30,7 @@ export const NumberFormatInput = forwardRef< onChange({ target: { name: props.name, - value: values.floatValue || 0, + value: values, }, }); }} diff --git a/src/shell/components/withCursorPosition/index.tsx b/src/shell/components/withCursorPosition/index.tsx index 94084b9b32..4bb01d4276 100644 --- a/src/shell/components/withCursorPosition/index.tsx +++ b/src/shell/components/withCursorPosition/index.tsx @@ -13,11 +13,19 @@ export const withCursorPosition = (WrappedComponent: ComponentType) => const inputRef = useRef(null); useEffect(() => { - inputRef.current?.setSelectionRange(cursorPosition, cursorPosition); + /* + In Safari, setting the cursor position can cause the input to refocus, + leading to a poor user experience if the input isn't already focused. + This conditional check ensures the cursor position is only set if the input is focused, + preventing unnecessary refocusing on value changes, which is a problem in Safari. + */ + if (document.activeElement === inputRef.current) { + inputRef.current?.setSelectionRange(cursorPosition, cursorPosition); + } }, [props.value]); const handleChange = (e: React.ChangeEvent) => { - setCursorPosition(e.target.selectionStart); + setCursorPosition(e.target.selectionStart || 0); props.onChange && props.onChange(e); }; diff --git a/src/shell/hooks/use-permissions.js b/src/shell/hooks/use-permissions.js index 23c00df880..f2886bcdfc 100644 --- a/src/shell/hooks/use-permissions.js +++ b/src/shell/hooks/use-permissions.js @@ -33,9 +33,14 @@ export function usePermission(action, zuid = instanceZUID) { return true; } - const granularRole = role?.granularRoles?.find( - (r) => r.resourceZUID === zuid - ); + /* + If the user is not a super user, check granular roles. + First check specific resource, if not found check instance level. + TODO: Check additional granular roles for parent resources depending on resource type (e.g. content model when checking content item) + */ + const granularRole = + role?.granularRoles?.find((r) => r.resourceZUID === zuid) || + role?.granularRoles?.find((r) => r.resourceZUID === instanceZUID); // Check system switch (action) { diff --git a/src/shell/services/marketing.ts b/src/shell/services/marketing.ts index 3a0f367010..a23b553684 100644 --- a/src/shell/services/marketing.ts +++ b/src/shell/services/marketing.ts @@ -43,7 +43,7 @@ export const marketingApi = createApi({ video_link, start_date_and_time, end_date_and_time, - created_at: currVal?.version?.history?.data?.pop()?.createdAt, + created_at: currVal?.version?.createdAt, }, ]; } diff --git a/src/shell/services/types.ts b/src/shell/services/types.ts index 62c7cd1aac..b8658b0d48 100644 --- a/src/shell/services/types.ts +++ b/src/shell/services/types.ts @@ -60,6 +60,8 @@ export interface File { deleted_at?: string; deleted_from_storage_at?: string; thumbnail: string; + storage_driver: string; + storage_name: string; } export type ModelType = "pageset" | "templateset" | "dataset"; @@ -203,12 +205,16 @@ export interface FieldSettings { regexRestrictErrorMessage?: string; minValue?: number; maxValue?: number; + currency?: string; + fileExtensions?: string[]; + fileExtensionsErrorMessage?: string; } export type ContentModelFieldValue = | string | number | boolean + | string[] | FieldSettings | FieldSettingsOptions[]; diff --git a/src/shell/services/util.js b/src/shell/services/util.js index a8e232048c..1b3236a582 100644 --- a/src/shell/services/util.js +++ b/src/shell/services/util.js @@ -10,5 +10,13 @@ export const prepareHeaders = (headers) => { return headers; }; -export const generateThumbnail = (file) => - `${file.url}?width=300&height=300&fit=bounds`; +export const generateThumbnail = (file) => { + if (!!file.updated_at && !isNaN(new Date(file.updated_at).getTime())) { + // Prevents browser image cache when a certain file has been already replaced + return `${file.url}?width=300&height=300&fit=bounds&versionHash=${new Date( + file.updated_at + ).getTime()}`; + } + + return `${file.url}?width=300&height=300&fit=bounds`; +}; diff --git a/src/shell/store/content.js b/src/shell/store/content.js index 1a89b29c52..6a7fa247a1 100644 --- a/src/shell/store/content.js +++ b/src/shell/store/content.js @@ -208,13 +208,13 @@ export function content(state = {}, action) { } // create the new item in the store -export function generateItem(modelZUID) { +export function generateItem(modelZUID, data = {}) { return (dispatch, getState) => { const state = getState(); const itemZUID = `new:${modelZUID}`; const item = { dirty: false, - data: {}, + data, web: { canonicalTagMode: 1, }, diff --git a/src/shell/store/media-revamp.ts b/src/shell/store/media-revamp.ts index f75d83af73..239303430b 100644 --- a/src/shell/store/media-revamp.ts +++ b/src/shell/store/media-revamp.ts @@ -27,12 +27,18 @@ export type UploadFile = { loading?: boolean; bin_id?: string; group_id?: string; + replacementFile?: boolean; }; type FileUploadStart = StoreFile & { file: File }; type FileUploadSuccess = StoreFile & FileBase & { id: string }; type FileUploadProgress = { uploadID: string; progress: number }; -type FileUploadStageArg = { file: File; bin_id: string; group_id: string }; +type FileUploadStageArg = { + file: File; + bin_id: string; + group_id: string; + replacementFile?: boolean; +}; type StagedUpload = { status: "staged"; @@ -146,6 +152,7 @@ const mediaSlice = createSlice({ uploadID: uuidv4(), url: URL.createObjectURL(file.file), filename: file.file.name, + replacementFile: file.replacementFile, ...file, }; }); @@ -334,11 +341,11 @@ type FileAugmentation = { group_id?: string; }; -async function getSignedUrl(file: any, bin: Bin) { +async function getSignedUrl(filename: string, storageName: string) { try { return request( //@ts-expect-error - `${CONFIG.SERVICE_MEDIA_STORAGE}/signed-url/${bin.storage_name}/${file.file.name}` + `${CONFIG.SERVICE_MEDIA_STORAGE}/signed-url/${storageName}/${filename}` ).then((res) => res.data.url); } catch (err) { console.error(err); @@ -349,6 +356,152 @@ async function getSignedUrl(file: any, bin: Bin) { } } +export function replaceFile(newFile: UploadFile, originalFile: FileBase) { + return async (dispatch: Dispatch, getState: () => AppState) => { + const bodyData = new FormData(); + const req = new XMLHttpRequest(); + const file = { + progress: 0, + loading: true, + ...newFile, + }; + + bodyData.append("file", file.file, originalFile.filename); + bodyData.append("file_id", originalFile.id); + + req.upload.addEventListener("progress", function (e) { + file.progress = (e.loaded / e.total) * 100; + + dispatch(fileUploadProgress(file)); + }); + + function handleError() { + dispatch(fileUploadError(file)); + dispatch( + notify({ + message: "Failed uploading file", + kind: "error", + }) + ); + } + + req.addEventListener("abort", handleError); + req.addEventListener("error", handleError); + req.addEventListener("load", (_) => { + if (req.status === 200) { + dispatch( + notify({ + message: `File Replaced: ${originalFile.filename}`, + kind: "success", + }) + ); + const successFile = { + ...originalFile, + uploadID: file.uploadID, + progress: 100, + loading: false, + url: URL.createObjectURL(file.file), + }; + dispatch(fileUploadSuccess(successFile)); + } else { + dispatch( + notify({ + message: "Failed uploading file", + kind: "error", + }) + ); + dispatch(fileUploadError(file)); + } + }); + + // Use signed url flow for large files + if (file.file.size > 32000000) { + /** + * GAE has an inherent 32mb limit at their global nginx load balancer + * We use a signed url for large file uploads directly to the assocaited bucket + */ + + const signedUrl = await getSignedUrl( + originalFile?.filename, + originalFile?.storage_name + ); + req.open("PUT", signedUrl); + + // The sent content-type needs to match what was provided when generating the signed url + // @see https://medium.com/imersotechblog/upload-files-to-google-cloud-storage-gcs-from-the-browser-159810bb11e3 + req.setRequestHeader("Content-Type", file.file.type); + + req.addEventListener("load", () => { + if (req.status === 200) { + return request( + //@ts-expect-error + `${CONFIG.SERVICE_MEDIA_MANAGER}/file/${originalFile?.id}/purge?triggerUpdate=true`, + { + method: "POST", + json: true, + } + ) + .then((res) => { + if (res.status === 200) { + const state: State = getState().mediaRevamp; + if (state.uploads.length) { + dispatch( + fileUploadSuccess({ + ...res.data, + uploadID: file.uploadID, + }) + ); + } else { + dispatch( + notify({ + message: `Successfully uploaded file`, + kind: "success", + }) + ); + } + } else { + throw res; + } + }) + .catch((err) => { + dispatch(fileUploadError(file)); + dispatch( + notify({ + message: + "Failed creating file record after signed url upload", + kind: "error", + }) + ); + }); + } else { + dispatch(fileUploadError(file)); + dispatch( + notify({ + message: "Failed uploading file to signed url", + kind: "error", + }) + ); + } + }); + + // When sending directly to bucket it needs to be just the file + // and not the extra meta data for the zesty services + req.send(file.file); + } else { + req.withCredentials = true; + req.open( + "PUT", + //@ts-expect-error + `${CONFIG.SERVICE_MEDIA_STORAGE}/replace/${originalFile?.storage_driver}/${originalFile?.storage_name}` + ); + + req.send(bodyData); + } + + dispatch(fileUploadStart(file)); + }; +} + //type FileMonstrosity = {file: File } & FileAugmentation & FileBase export function uploadFile(fileArg: UploadFile, bin: Bin) { return async (dispatch: Dispatch, getState: () => AppState) => { @@ -417,7 +570,7 @@ export function uploadFile(fileArg: UploadFile, bin: Bin) { * We use a signed url for large file uploads directly to the assocaited bucket */ - const signedUrl = await getSignedUrl(file, bin); + const signedUrl = await getSignedUrl(file.file.name, bin.storage_name); req.open("PUT", signedUrl); // The sent content-type needs to match what was provided when generating the signed url @@ -596,16 +749,18 @@ export function dismissFileUploads() { ); } if (successfulUploads.length) { - dispatch( - notify({ - message: `Successfully uploaded ${successfulUploads.length} files${ - inProgressUploads.length - ? `...${inProgressUploads.length} files still in progress` - : "" - }`, - kind: "success", - }) - ); + if (!successfulUploads[0].replacementFile) { + dispatch( + notify({ + message: `Successfully uploaded ${successfulUploads.length} files${ + inProgressUploads.length + ? `...${inProgressUploads.length} files still in progress` + : "" + }`, + kind: "success", + }) + ); + } } if (failedUploads.length) { dispatch( @@ -622,6 +777,12 @@ export function dismissFileUploads() { kind: "warn", }) ); + } else { + successfulUploads?.forEach((upload) => { + dispatch( + mediaManagerApi.util.invalidateTags([{ type: "File", id: upload.id }]) + ); + }); } dispatch(fileUploadReset()); }; diff --git a/src/shell/store/notifications.ts b/src/shell/store/notifications.ts index 271ac555b4..7c6e89514d 100644 --- a/src/shell/store/notifications.ts +++ b/src/shell/store/notifications.ts @@ -26,7 +26,7 @@ type NotifyArgs = { kind: "warn" | "error" | "success"; HTML?: unknown; heading?: string; - message?: string; + message: string; }; type Notification = NotifyArgs & { diff --git a/src/shell/store/products.js b/src/shell/store/products.js index 6b5b9befd3..c0e5fece04 100644 --- a/src/shell/store/products.js +++ b/src/shell/store/products.js @@ -20,6 +20,7 @@ export function fetchProducts() { // seo: 31-71cfc74-s30 case "31-71cfc74-0wn3r": case "31-71cfc74-4dm13": + case "31-71cfc74-4cc4dm13": data = [ "launchpad", "content", @@ -35,6 +36,7 @@ export function fetchProducts() { ]; break; case "31-71cfc74-d3v3l0p3r": + case "31-71cfc74-d3vc0n": data = [ "launchpad", "content", diff --git a/src/shell/store/types.ts b/src/shell/store/types.ts index b3c678b2e7..268bf7dc07 100644 --- a/src/shell/store/types.ts +++ b/src/shell/store/types.ts @@ -1,6 +1,10 @@ import { UIState } from "./ui"; import { State as MediaRevampState } from "./media-revamp"; -import { InstalledApp, ModelType } from "../services/types"; +import { + InstalledApp, + ModelType, + ContentItemWithDirtyAndPublishing, +} from "../services/types"; /* TODO The UI state is well typed but the rest of the application state is entirely @@ -32,7 +36,7 @@ export type AppState = { languages: any; models: any; fields: any; - content: any; + content: Record; contentVersions: any; mediaRevamp: MediaRevampState; media: any; diff --git a/src/utility/getFlagEmoji.ts b/src/utility/getFlagEmoji.ts new file mode 100644 index 0000000000..dd4b9854ec --- /dev/null +++ b/src/utility/getFlagEmoji.ts @@ -0,0 +1,9 @@ +export default (countryCode: string) => { + // Convert country code to flag emoji. + // Unicode flag emojis are made up of regional indicator symbols, which are a sequence of two letters. + const baseOffset = 0x1f1e6; + return ( + String.fromCodePoint(baseOffset + (countryCode.charCodeAt(0) - 65)) + + String.fromCodePoint(baseOffset + (countryCode.charCodeAt(1) - 65)) + ); +};