Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stage Release #2764

Merged
merged 10 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,20 @@ In some case when sending null this will break the togglebutton UI, thus the rea
## Uploading Assets

To upload assets for your projects put them on the CDN, do not put them in the repository. Assets can be uploaded at https://console.cloud.google.com/storage/browser/assets.zesty.io?project=zesty-prod , upload to the respective folder that match your project name, for example, the SVGs and PNG that are being commited to manager-ui should be moved into this storage bucket under the `manager` folder, once they are uploaded they accessible from https://assets.zesty.io e.g. https://assets.zesty.io/website/assets/images/dxp_bottom_bg.svg

## Deeplinks

For in-app deeplinks the preferred url structure is to use url path parameters as opposed to query parameters. Generally, paths are best used used to deep link into a specific view (a combination of UI layout and elements rendered on screen) whereas query parameters are used to refine a view.

#### Example

```
Table view: /content/6-000-0000
Table view filtered to published items: /content/6-000-0000?status=published

Content edit view: /content/6-000-0000/7-000-0000
Content edit view that displays deactivated field: /content/6-000-0000/7-000-0000?showDeactivated=true

Comment in content edit view: /content/6-000-0000/7-000-0000/comment/12-000-0000/24-000-0000
Comment in content edit view that displays deleted replies: /content/6-000-0000/7-000-0000/comment/12-000-0000/24-000-0000?showDeleted=true
```
49 changes: 49 additions & 0 deletions cypress/e2e/content/actions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,55 @@ describe("Actions in content editor", () => {
cy.get("[data-cy=toast]").contains("Missing Data in Required Fields");
});

it("Must not save when exceeding or lacking characters", () => {
cy.waitOn("/v1/content/models*", () => {
cy.visit("/content/6-a4f5f1beaa-zc5l6v/7-ce9ca8cfb0-cc1mnz");
});

cy.get("#12-e6a5cfe3f6-k94nbg input").clear().type("aa");
cy.get("#SaveItemButton").click();
cy.getBySelector("FieldErrorsList").should("exist");
cy.getBySelector("FieldErrorsList")
.find("ol")
.find("li")
.first()
.contains("Requires 8 more characters.");
cy.get("#12-e6a5cfe3f6-k94nbg input")
.clear()
.type("Lorem ipsum dolor sit amet, consect");
cy.get("#SaveItemButton").click();
cy.getBySelector("FieldErrorsList").should("exist");
cy.getBySelector("FieldErrorsList")
.find("ol")
.find("li")
.first()
.contains("Exceeding by 5 characters.");
cy.get("#12-e6a5cfe3f6-k94nbg input").clear().type("Lorem ipsum");
cy.get("#SaveItemButton").click();
cy.get("[data-cy=toast]").contains("Item Saved: New Schema All Fields");
});

it.only("Must not save when regex is not matched", () => {
cy.waitOn("/v1/content/models*", () => {
cy.visit("/content/6-a4f5f1beaa-zc5l6v/7-ce9ca8cfb0-cc1mnz");
});

cy.get("#12-b6d09d92d0-7911ld textarea").first().clear().type("aa");
cy.get("#SaveItemButton").click();
cy.getBySelector("FieldErrorsList").should("exist");
cy.getBySelector("FieldErrorsList")
.find("ol")
.find("li")
.first()
.contains("Must be an email (e.g. hello@zesty.io)");
cy.get("#12-b6d09d92d0-7911ld textarea")
.first()
.clear()
.type("hello@zesty.io");
cy.get("#SaveItemButton").click();
cy.get("[data-cy=toast]").contains("Item Saved: New Schema All Fields");
});

/**
* NOTE: this depends upon `toggle` field on the schema being marked as being required and deactivated. Because it's deactivated it doesn't render in the content editor and the expectation is the content item should save. there fore there is nothing to do and confirm that this item saves successfully. Adding this notes because nothing really happens inside this test but it's important this test remains.
* */
Expand Down
84 changes: 84 additions & 0 deletions cypress/e2e/content/comments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
describe("Content Item: Comments", () => {
before(() => {
cy.waitOn("/v1/content/models*", () => {
cy.waitOn("/v1/comments*", () => {
cy.visit("/content/6-556370-8sh47g/7-b939a4-457q19");
});
});
cy.getBySelector("DuoModeToggle").click();
});

it("Creates an initial comment", () => {
cy.getBySelector("OpenCommentsButton").first().click();
cy.get("#commentInputField").click().type("This is a new comment.");
cy.getBySelector("SubmitNewComment").click();
cy.intercept("/v1/comments/*").as("getAllComments");
cy.wait("@getAllComments");
cy.getBySelector("CommentItem").should("have.length", 1);
});

it("Replies to a comment", () => {
cy.get("#commentInputField").click().type("Hello, this is a new reply!");
cy.getBySelector("SubmitNewComment").click();
cy.intercept("/v1/comments/*?showReplies=true&showResolved=true").as(
"getReplies"
);
cy.wait("@getReplies");
cy.getBySelector("CommentItem").should("have.length", 2);
});

it("Updates an existing comment", () => {
const UPDATED_TEXT = "I am updating this comment now.";

cy.getBySelector("CommentMenuButton").first().click();
cy.getBySelector("EditCommentButton").click();
cy.get("#commentInputField")
.click()
.type(`{selectall}{backspace}${UPDATED_TEXT}`);
cy.getBySelector("SubmitNewComment").click();
cy.intercept("/v1/comments/*?showReplies=true&showResolved=true").as(
"getReplies"
);
cy.wait("@getReplies");
cy.getBySelector("CommentItem").first().contains(UPDATED_TEXT);
});

it("Resolves a comment", () => {
cy.getBySelector("ResolveCommentButton").click();
cy.intercept("/v1/comments/*?showReplies=true&showResolved=true").as(
"getReplies"
);
cy.intercept("/v1/instances/*/comments?resource=*").as(
"getCommentResourceData"
);
cy.wait("@getReplies");
cy.wait("@getCommentResourceData");
cy.getBySelector("ResolveCommentButton").should("not.exist");
});

it("Reopens a comment when there is a new reply", () => {
cy.get("#commentInputField").click().type("Reopening ticket.");
cy.getBySelector("SubmitNewComment").click();
cy.intercept("/v1/comments/*?showReplies=true&showResolved=true").as(
"getReplies"
);
cy.intercept("/v1/instances/*/comments?resource=*").as(
"getCommentResourceData"
);
cy.wait("@getReplies");
cy.wait("@getCommentResourceData");
cy.getBySelector("ResolveCommentButton").should("exist");
});

it("Delete a comment", () => {
cy.getBySelector("CommentMenuButton").first().click();
cy.getBySelector("DeleteCommentButton").click();
cy.getBySelector("ConfirmDeleteCommentButton").click();
cy.intercept("/v1/instances/*/comments?resource=*").as(
"getCommentResourceData"
);
cy.wait("@getCommentResourceData");
cy.getBySelector("OpenCommentsButton").first().click();
cy.getBySelector("CommentItem").should("not.exist");
});
});
2 changes: 1 addition & 1 deletion cypress/e2e/content/content.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ describe("Content Specs", () => {
.clear()
.type("{rightArrow}12");

cy.get("#12-4e1914-kcqznz button").first().click();
cy.get("#12-4e1914-kcqznz button").eq(1).click();

cy.get("#12-4e1914-kcqznz input[type='text']").should("have.value", "11");

Expand Down
16 changes: 16 additions & 0 deletions cypress/e2e/schema/field.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ const SELECTORS = {
SYSTEM_FIELDS: "SystemFields",
DEFAULT_VALUE_CHECKBOX: "DefaultValueCheckbox",
DEFAULT_VALUE_INPUT: "DefaultValueInput",
CHARACTER_LIMIT_CHECKBOX: "CharacterLimitCheckbox",
MIN_CHARACTER_LIMIT_INPUT: "MinCharacterLimitInput",
MAX_CHARACTER_LIMIT_INPUT: "MaxCharacterLimitInput",
MIN_CHARACTER_ERROR_MSG: "MinCharacterErrorMsg",
MAX_CHARACTER_ERROR_MSG: "MaxCharacterErrorMsg",
};

/**
Expand Down Expand Up @@ -110,6 +115,17 @@ describe("Schema: Fields", () => {
.find("input")
.should("have.value", "default value");

// Set min/max character limits
cy.getBySelector(SELECTORS.CHARACTER_LIMIT_CHECKBOX).click();
cy.getBySelector(SELECTORS.MAX_CHARACTER_LIMIT_INPUT).clear().type("10000");
cy.getBySelector(SELECTORS.MAX_CHARACTER_ERROR_MSG).should("exist");
cy.getBySelector(SELECTORS.MAX_CHARACTER_LIMIT_INPUT).clear().type("20");
cy.getBySelector(SELECTORS.MAX_CHARACTER_ERROR_MSG).should("not.exist");
cy.getBySelector(SELECTORS.MIN_CHARACTER_LIMIT_INPUT).clear().type("10000");
cy.getBySelector(SELECTORS.MIN_CHARACTER_ERROR_MSG).should("exist");
cy.getBySelector(SELECTORS.MIN_CHARACTER_LIMIT_INPUT).clear().type("5");
cy.getBySelector(SELECTORS.MIN_CHARACTER_ERROR_MSG).should("not.exist");

// Click done
cy.getBySelector(SELECTORS.SAVE_FIELD_BUTTON).should("exist").click();
cy.getBySelector(SELECTORS.ADD_FIELD_MODAL).should("not.exist");
Expand Down
15 changes: 8 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.1",
"@zesty-io/material": "^0.15.2",
"chart.js": "^3.8.0",
"chartjs-adapter-moment": "^1.0.1",
"chartjs-plugin-datalabels": "^2.0.0",
Expand Down
63 changes: 52 additions & 11 deletions src/apps/content-editor/src/app/components/Editor/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,23 +80,19 @@ export default memo(function Editor({
throw new Error("Input is missing name attribute");
}

const isFieldRequired = activeFields.find(
(field) => field.name === name
)?.required;
const fieldDatatype = activeFields.find(
(field) => field.name === name
)?.datatype;
const fieldMaxLength = MaxLengths[fieldDatatype];
const field = activeFields?.find((field) => field.name === name);
const fieldMaxLength =
field?.settings?.maxCharLimit ?? MaxLengths[field?.datatype];
const errors = cloneDeep(fieldErrors);

// Remove the required field error message when a value has been added
if (isFieldRequired) {
if (fieldDatatype === "yes_no" && value !== null) {
if (field?.required) {
if (field?.datatype === "yes_no" && value !== null) {
errors[name] = {
...(errors[name] ?? {}),
MISSING_REQUIRED: false,
};
} else if (fieldDatatype !== "yes_no" && value) {
} else if (field?.datatype !== "yes_no" && value) {
errors[name] = {
...(errors[name] ?? {}),
MISSING_REQUIRED: false,
Expand All @@ -116,6 +112,48 @@ export default memo(function Editor({
}
}

if (field?.settings?.minCharLimit) {
if (value.length < field?.settings?.minCharLimit) {
errors[name] = {
...(errors[name] ?? []),
LACKING_MINLENGTH: field?.settings?.minCharLimit - value.length,
};
} else {
errors[name] = { ...(errors[name] ?? []), LACKING_MINLENGTH: 0 };
}
}

if (field?.settings?.regexMatchPattern) {
const regex = new RegExp(field?.settings?.regexMatchPattern);
if (!regex.test(value)) {
errors[name] = {
...(errors[name] ?? []),
REGEX_PATTERN_MISMATCH: field?.settings?.regexMatchErrorMessage,
};
} else {
errors[name] = {
...(errors[name] ?? []),
REGEX_PATTERN_MISMATCH: "",
};
}
}

if (field?.settings?.regexRestrictPattern) {
const regex = new RegExp(field?.settings?.regexRestrictPattern);
if (regex.test(value)) {
errors[name] = {
...(errors[name] ?? []),
REGEX_RESTRICT_PATTERN_MATCH:
field?.settings?.regexRestrictErrorMessage,
};
} else {
errors[name] = {
...(errors[name] ?? []),
REGEX_RESTRICT_PATTERN_MATCH: "",
};
}
}

onUpdateFieldErrors(errors);

// Always dispatch the data update
Expand Down Expand Up @@ -255,7 +293,10 @@ export default memo(function Editor({
item={item}
langID={item?.meta?.langID}
errors={fieldErrors[field.name]}
maxLength={MaxLengths[field.datatype]}
maxLength={
field.settings?.maxCharLimit ?? MaxLengths[field.datatype]
}
minLength={field.settings?.minCharLimit ?? 0}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ type FieldProps = {
onSave: () => void;
errors: Error;
maxLength: number;
minLength: number;
};
export const Field = ({
ZUID,
Expand All @@ -185,6 +186,7 @@ export const Field = ({
onSave,
errors,
maxLength,
minLength,
}: FieldProps) => {
const dispatch = useDispatch();
const allItems = useSelector((state: AppState) => state.content);
Expand Down Expand Up @@ -289,6 +291,7 @@ export const Field = ({
}
withLengthCounter
maxLength={maxLength}
minLength={minLength}
errors={errors}
aiType="text"
>
Expand Down Expand Up @@ -371,6 +374,7 @@ export const Field = ({
errors={errors}
aiType="paragraph"
maxLength={maxLength}
minLength={minLength}
>
<TextField
value={value}
Expand Down
Loading
Loading