Skip to content

Commit 82b3fd1

Browse files
Copilotadrians5j
andauthored
feat: Webiny SDK (#4925)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: adrians5j <5121148+adrians5j@users.noreply.github.com> Co-authored-by: adrians5j <adrian@webiny.com> Co-authored-by: Adrian Smijulj <adrian1358@gmail.com>
1 parent df7a8bb commit 82b3fd1

74 files changed

Lines changed: 3252 additions & 49 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { EntryBeforeCreateEventHandler as EntryBeforeCreateEventHandlerAbstraction } from "webiny/api/cms/entry";
2+
import { Logger } from "webiny/api/logger";
3+
4+
// List of common free email providers
5+
const PERSONAL_EMAIL_DOMAINS = [
6+
"gmail.com",
7+
"yahoo.com",
8+
"hotmail.com",
9+
"outlook.com",
10+
"aol.com",
11+
"icloud.com",
12+
"protonmail.com"
13+
];
14+
15+
class ContactSubmissionHookImpl implements EntryBeforeCreateEventHandlerAbstraction.Interface {
16+
public constructor(private logger: Logger.Interface) {}
17+
18+
public async handle(event: EntryBeforeCreateEventHandlerAbstraction.Event): Promise<void> {
19+
const { model, input, entry } = event.payload;
20+
21+
// 1. Check if this event is for our Contact Submission model
22+
if (model.modelId !== "contactSubmission") {
23+
return;
24+
}
25+
26+
this.logger.info(`Processing contact submission for model: ${model.modelId}`);
27+
28+
// 2. Get the email from the payload
29+
// Note: In Webiny CMS events, the entry data is typically in payload.values
30+
const email = input.values?.email as string;
31+
32+
if (!email) {
33+
this.logger.warn("No email found in contact submission");
34+
return;
35+
}
36+
37+
// 3. Analyze the email domain
38+
const domain = email.split("@")[1]?.toLowerCase();
39+
40+
let type = "work";
41+
if (domain && PERSONAL_EMAIL_DOMAINS.includes(domain)) {
42+
type = "personal";
43+
}
44+
45+
this.logger.info(`Classified email ${email} as ${type}`);
46+
47+
// 4. Update the entry with the classification
48+
// We can directly modify the payload.values object to set data before it's saved
49+
entry.values.emailType = type;
50+
}
51+
}
52+
53+
export default EntryBeforeCreateEventHandlerAbstraction.createImplementation({
54+
implementation: ContactSubmissionHookImpl,
55+
dependencies: [Logger]
56+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { ModelFactory } from "webiny/api/cms/model";
2+
3+
export const CONTACT_SUBMISSION_MODEL_ID = "contactSubmission";
4+
5+
class ContactSubmissionModelImpl implements ModelFactory.Interface {
6+
async execute(builder: ModelFactory.Builder) {
7+
return [
8+
builder
9+
.public({
10+
modelId: CONTACT_SUBMISSION_MODEL_ID,
11+
name: "Contact Submission",
12+
group: "ungrouped"
13+
})
14+
.description("Stores contact form submissions from the website")
15+
.fields(fields => ({
16+
name: fields
17+
.text()
18+
.renderer("text-input")
19+
.label("Name")
20+
.help("Full name of the person submitting the form")
21+
.required("Name is required")
22+
.minLength(2)
23+
.maxLength(100)
24+
.help("Enter your full name"),
25+
email: fields
26+
.text()
27+
.renderer("text-input")
28+
.label("Email")
29+
.help("Email address for contact")
30+
.required("Email is required")
31+
.email()
32+
.help("Enter a valid email address"),
33+
message: fields
34+
.longText()
35+
.renderer("long-text-text-area")
36+
.label("Message")
37+
.help("Message content from the contact form")
38+
.required("Message is required")
39+
.minLength(10)
40+
.maxLength(1000)
41+
.help("Enter your message..."),
42+
emailType: fields
43+
.text()
44+
.renderer("radio-buttons")
45+
.label("Email Type")
46+
.help("Automatically classified as Work or Personal")
47+
.predefinedValues([
48+
{ label: "Work", value: "work" },
49+
{ label: "Personal", value: "personal" }
50+
])
51+
}))
52+
.layout([["name", "email"], ["message"], ["emailType"]])
53+
.titleFieldId("name")
54+
.descriptionFieldId("message")
55+
.singularApiName("ContactSubmission")
56+
.pluralApiName("ContactSubmissions")
57+
];
58+
}
59+
}
60+
61+
export default ModelFactory.createImplementation({
62+
implementation: ContactSubmissionModelImpl,
63+
dependencies: []
64+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from "react";
2+
import { ContentEntryListConfig } from "webiny/admin/cms/entry/list";
3+
4+
// You can destructure config components to make the code more readable and easier to work with.
5+
const { Browser } = ContentEntryListConfig;
6+
7+
interface ContactSubmissionTableRow {
8+
values: {
9+
emailType: "work" | "personal";
10+
};
11+
}
12+
13+
export const EmailTypeCell = () => {
14+
// You can destructure child methods to make the code more readable and easier to work with.
15+
const { useTableRow, isFolderRow } = ContentEntryListConfig.Browser.Table.Column;
16+
// useTableRow() allows you to access the entire data of the current row.
17+
const { row } = useTableRow<ContactSubmissionTableRow>();
18+
19+
// isFolderRow() allows for custom rendering when the current row is a folder.
20+
if (isFolderRow(row)) {
21+
return <>{"-"}</>;
22+
}
23+
24+
const emailType = row.data.values.emailType;
25+
return emailType === "work" ? <>{"Business"}</> : <>{"Personal"}</>;
26+
};
27+
28+
const EmailTypeEntryListColumn = () => {
29+
return (
30+
<ContentEntryListConfig>
31+
<Browser.Table.Column
32+
name={"email"}
33+
path={"values.email"}
34+
header={"Email"}
35+
modelIds={["contactSubmission"]}
36+
/>
37+
<Browser.Table.Column
38+
name={"emailType"}
39+
header={"Email Type"}
40+
modelIds={["contactSubmission"]}
41+
cell={<EmailTypeCell />}
42+
/>
43+
</ContentEntryListConfig>
44+
);
45+
};
46+
47+
export default EmailTypeEntryListColumn;
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,26 @@ class ProductCategoryModelImpl implements ModelFactory.Interface {
66
async execute(builder: ModelFactory.Builder) {
77
return [
88
builder
9-
.public()
10-
.modelId(PRODUCT_CATEGORY_MODEL_ID)
11-
.name("Product Category")
9+
.public({
10+
modelId: PRODUCT_CATEGORY_MODEL_ID,
11+
name: "Product Category",
12+
group: "ungrouped"
13+
})
1214
.description("Product categories for organizing products")
13-
.group("ungrouped")
1415
.fields(fields => ({
1516
name: fields
1617
.text()
1718
.renderer("text-input")
1819
.label("Name")
19-
.helpText("Name of the product category")
20+
.help("Name of the product category")
2021
.required("Name is required")
2122
.minLength(2)
2223
.maxLength(100),
2324
slug: fields
2425
.text()
2526
.renderer("text-input")
2627
.label("Slug")
27-
.helpText("URL-friendly identifier")
28+
.help("URL-friendly identifier")
2829
.required("Slug is required")
2930
.unique(),
3031
description: fields
Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,31 @@ class ProductModelImpl implements ModelFactory.Interface {
66
async execute(builder: ModelFactory.Builder) {
77
return [
88
builder
9-
.public()
10-
.modelId(PRODUCT_MODEL_ID)
11-
.name("Product")
9+
.public({
10+
modelId: PRODUCT_MODEL_ID,
11+
name: "Product",
12+
group: "ungrouped"
13+
})
1214
.description("Products for our e-commerce store")
13-
.group("ungrouped")
1415
.fields(fields => ({
1516
name: fields
1617
.text()
1718
.renderer("text-input")
1819
.label("Name")
19-
.helpText("Product name")
20+
.help("Product name")
2021
.required("Name is required"),
2122
sku: fields
2223
.text()
2324
.renderer("text-input")
2425
.label("SKU")
25-
.helpText("Stock Keeping Unit - unique product identifier")
26+
.help("Stock Keeping Unit - unique product identifier")
2627
.required("SKU is required")
2728
.unique(),
2829
description: fields
2930
.longText()
3031
.renderer("long-text-text-area")
3132
.label("Description")
32-
.helpText("Detailed product description"),
33+
.help("Detailed product description"),
3334
price: fields
3435
.number()
3536
.renderer("number-input")

packages/admin-ui/src/DataTable/DataTable.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ interface DataTableColumn<T> {
5151
* Enable column visibility toggling.
5252
*/
5353
enableHiding?: boolean;
54+
/*
55+
* Accessor key for the column data path.
56+
*/
57+
accessorKey?: string;
5458
}
5559

5660
type DataTableColumns<T> = {
@@ -161,6 +165,7 @@ const defineColumns = <T,>(
161165

162166
const defaults: ColumnDef<T>[] = columnsList.map(column => {
163167
const {
168+
accessorKey,
164169
cell,
165170
className,
166171
enableHiding = true,
@@ -173,7 +178,7 @@ const defineColumns = <T,>(
173178

174179
return {
175180
id,
176-
accessorKey: id,
181+
accessorKey: accessorKey || id,
177182
header: () => header,
178183
cell: props => {
179184
if (cell && typeof cell === "function") {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { describe, expect, it } from "vitest";
2+
import { transformSortToArray } from "~/graphql/schema/cms/helpers";
3+
4+
/**
5+
* Test the sort transformation helper that converts object format to array format.
6+
* This transformation is needed because the CMS schema accepts sort as an object,
7+
* but the underlying GraphQL schemas expect an array format.
8+
*/
9+
describe("CMS Schema Helpers", () => {
10+
describe("transformSortToArray", () => {
11+
it("should transform single sort field from object to array", () => {
12+
const input = { createdOn: "desc" };
13+
const expected = ["createdOn_DESC"];
14+
15+
const result = transformSortToArray(input);
16+
17+
expect(result).toEqual(expected);
18+
});
19+
20+
it("should transform multiple sort fields from object to array", () => {
21+
const input = { createdOn: "desc", name: "asc" };
22+
const expected = ["createdOn_DESC", "name_ASC"];
23+
24+
const result = transformSortToArray(input);
25+
26+
expect(result).toEqual(expected);
27+
});
28+
29+
it("should handle uppercase direction values", () => {
30+
const input = { createdOn: "DESC" };
31+
const expected = ["createdOn_DESC"];
32+
33+
const result = transformSortToArray(input);
34+
35+
expect(result).toEqual(expected);
36+
});
37+
38+
it("should handle lowercase direction values", () => {
39+
const input = { modifiedOn: "asc" };
40+
const expected = ["modifiedOn_ASC"];
41+
42+
const result = transformSortToArray(input);
43+
44+
expect(result).toEqual(expected);
45+
});
46+
47+
it("should return undefined for undefined input", () => {
48+
const result = transformSortToArray(undefined);
49+
50+
expect(result).toBeUndefined();
51+
});
52+
53+
it("should return undefined for null input", () => {
54+
const result = transformSortToArray(null as any);
55+
56+
expect(result).toBeUndefined();
57+
});
58+
59+
it("should return empty array for empty object", () => {
60+
const result = transformSortToArray({});
61+
62+
expect(result).toEqual([]);
63+
});
64+
});
65+
});

0 commit comments

Comments
 (0)