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 @@
+
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 (
+
+
+ {liveDomain && (
+
+ )}
+
+ );
+};
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 && (
-
- }
- fullWidth
- sx={{
- maxWidth: "196px",
- flexShrink: 0,
- }}
- >
- Upload
-
- }
- variant="outlined"
- onClick={() => {
- openMediaBrowser({
- limit,
- callback: addZestyImage,
- });
- }}
- sx={{
- maxWidth: "196px",
- flexShrink: 0,
- }}
+ {isDragActive ? (
+ "Drop your files here to Upload"
+ ) : (
+ <>
+ Drag and drop your files here
or
+ >
+ )}
+
+ {!isDragActive && (
+
- Add from Media
-
- {isBynderSessionValid && (
- )}
-
+ }
+ variant="outlined"
+ onClick={() => {
+ openMediaBrowser({
+ limit,
+ callback: addZestyImage,
+ });
+ }}
+ sx={{
+ maxWidth: "196px",
+ flexShrink: 0,
+ }}
+ >
+ Add from Media
+
+ {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 && (
+ }
+ fullWidth
+ >
+ Upload
+
)}
-
-
-
+
+ {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 && (
- }
- fullWidth
- >
- Upload
-
- )}
-
- {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 && (
-
-
-
+
- )}
-
- {
+
-
-
-
+
+
+ )}
{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 && (
+
+
+
+ )}
+ >
+ );
+ }
+
+ // 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}
>