Skip to content

Commit 1d955e8

Browse files
adrians5jclaude
andauthored
fix: port the last 6.4.x fixes missing on next (#6009)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2d35951 commit 1d955e8

13 files changed

Lines changed: 753 additions & 25 deletions

File tree

‎packages/admin-ui/src/FilePicker/FilePicker.stories.tsx‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,36 @@ export const CompactFullExample: Story = {
218218
}
219219
};
220220

221+
/**
222+
* Reproduces the Website Builder style sidebar at its 206px value column: the preview stacks,
223+
* the file name truncates in the middle so the extension and version suffix survive, and the
224+
* actions spell themselves out instead of costing a fixed block of the row.
225+
*/
226+
export const CompactWithLongFileNameInNarrowContainer: Story = {
227+
args: {
228+
...Compact.args
229+
},
230+
render: args => {
231+
const [selectedFile, setSelectedFile] = useState<FileItemDto | null>({
232+
name: "tide-cc-statuses-2026-08-21-final-v2.png",
233+
mimeType: "image/png",
234+
size: 674161,
235+
url: "https://picsum.photos/1200/800"
236+
});
237+
238+
return (
239+
<div style={{ width: 206 }}>
240+
<FilePicker
241+
{...args}
242+
value={selectedFile}
243+
onSelectItem={() => setSelectedFile(getFile())}
244+
onRemoveItem={() => setSelectedFile(null)}
245+
/>
246+
</div>
247+
);
248+
}
249+
};
250+
221251
export const Documentation: Story = {
222252
render: args => {
223253
const [selectedFile, setSelectedFile] = useState(args.value);

‎packages/admin-ui/src/FilePicker/primitives/components/previews/ItemDescription.tsx‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@ const ItemDescription = ({ className, disabled, item, small, ...props }: ItemDes
1414
const formattedSize = item.size && bytes.format(item.size, { unitSeparator: " " });
1515

1616
return (
17-
<div
18-
className={cn("flex flex-col gap-xxs overflow-hidden flex-1 min-w-0", className)}
19-
{...props}
20-
>
17+
<div className={cn("flex flex-col gap-xxs overflow-hidden min-w-0", className)} {...props}>
2118
<Text
2219
size="sm"
2320
as="div"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { describe, expect, it } from "vitest";
2+
import { useHandler } from "./helpers/useHandler";
3+
import { CreateFolderUseCase } from "@webiny/api-aco/features/folder/CreateFolder/index.js";
4+
import { DeleteFolderUseCase } from "@webiny/api-aco/features/folder/DeleteFolder/index.js";
5+
6+
describe("folder audit logs", () => {
7+
it.each([
8+
["wb:page", "PAGE_FOLDER"],
9+
["wb:redirect", "REDIRECT_FOLDER"]
10+
])(
11+
"should store an audit log when a %s folder is deleted",
12+
async (folderType, expectedEntity) => {
13+
const { handler } = useHandler();
14+
const context = await handler();
15+
const { container } = context;
16+
17+
const folder = await container.resolve(CreateFolderUseCase).execute({
18+
title: "Folder",
19+
slug: "folder",
20+
type: folderType,
21+
parentId: null
22+
});
23+
expect(folder.isOk()).toBe(true);
24+
25+
const deleted = await container.resolve(DeleteFolderUseCase).execute(folder.value.id);
26+
expect(deleted.isOk()).toBe(true);
27+
28+
const logs = await context.listAuditLogs({
29+
app: "WEBSITE_BUILDER",
30+
entityId: folder.value.id,
31+
limit: 10
32+
} as any);
33+
34+
expect(logs.error).toBeUndefined();
35+
expect(logs.items.map(log => `${log.entity}:${log.action}`)).toEqual([
36+
`${expectedEntity}:DELETE`
37+
]);
38+
}
39+
);
40+
});

‎packages/api-audit-logs/src/subscriptions/aco/handlers/AuditLogFolderAfterDeleteHandler.ts‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,39 @@ class AuditLogFolderAfterDeleteHandlerImpl implements FolderAfterDeleteEventHand
99
async handle(event: FolderAfterDeleteEventHandler.Event): Promise<void> {
1010
try {
1111
const { folder } = event.payload;
12-
if (folder.type === "PbPage") {
12+
if (folder.type === "FmFile") {
1313
const recordResult = await this.recordAuditLog.execute({
14-
audit: AUDIT.PAGE_BUILDER.PAGE_FOLDER.DELETE,
14+
audit: AUDIT.FILE_MANAGER.FILE_FOLDER.DELETE,
1515
message: "Folder deleted",
1616
content: folder,
1717
entityId: folder.id
1818
});
1919
if (recordResult.isFail()) {
2020
throw recordResult.error;
2121
}
22-
} else if (folder.type === "FmFile") {
22+
} else if (folder.type.startsWith("cms:")) {
2323
const recordResult = await this.recordAuditLog.execute({
24-
audit: AUDIT.FILE_MANAGER.FILE_FOLDER.DELETE,
24+
audit: AUDIT.HEADLESS_CMS.MODEL_FOLDER.DELETE,
2525
message: "Folder deleted",
2626
content: folder,
2727
entityId: folder.id
2828
});
2929
if (recordResult.isFail()) {
3030
throw recordResult.error;
3131
}
32-
} else if (folder.type.startsWith("cms:")) {
32+
} else if (folder.type === "wb:page") {
3333
const recordResult = await this.recordAuditLog.execute({
34-
audit: AUDIT.HEADLESS_CMS.MODEL_FOLDER.DELETE,
34+
audit: AUDIT.WEBSITE_BUILDER.PAGE_FOLDER.DELETE,
35+
message: "Folder deleted",
36+
content: folder,
37+
entityId: folder.id
38+
});
39+
if (recordResult.isFail()) {
40+
throw recordResult.error;
41+
}
42+
} else if (folder.type === "wb:redirect") {
43+
const recordResult = await this.recordAuditLog.execute({
44+
audit: AUDIT.WEBSITE_BUILDER.REDIRECT_FOLDER.DELETE,
3545
message: "Folder deleted",
3646
content: folder,
3747
entityId: folder.id

‎packages/api-headless-cms-utils-os/src/operations/entry/elasticsearch/fullTextSearch.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ const defaultFullTextSearch: CmsEntryOpenSearchFullTextSearch.Interface = {
1414
query.must.push({
1515
query_string: {
1616
allow_leading_wildcard: true,
17+
/**
18+
* Searchable JSON fields are dynamically mapped, so their subfields can be of any type (date, number, boolean...).
19+
* Wildcard queries are not allowed on those types, so we need to ignore them instead of failing the whole query.
20+
*/
21+
lenient: true,
1722
fields: Object.values(fields).map(createFieldPath),
1823
query: `*${prepareTerm(term)}*`,
1924
default_operator: "and"
@@ -85,7 +90,13 @@ export const applyFullTextSearch = (params: Params): void => {
8590
} else if (field.systemField) {
8691
return field.path || field.field.storageId;
8792
}
88-
const path = `values.${field.path || field.field.storageId}`;
93+
/**
94+
* Fields can live inside object fields, so we need to prepend the storage ids of all the parents.
95+
* Root fields have the `values` parent only.
96+
*/
97+
const parents =
98+
field.parents.length > 0 ? field.parents.map(p => p.storageId) : ["values"];
99+
const path = [...parents, field.path || field.field.storageId].join(".");
89100
/*
90101
* Searchable JSON fields are indexed as objects, so the searchable values live on the
91102
* nested keys. Target all of them with a wildcard.
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { beforeEach, describe, expect, it } from "vitest";
2+
import { ListLatestEntriesUseCase } from "~/features/contentEntry/ListEntries/index.js";
3+
import { CreateEntryUseCase } from "~/features/contentEntry/CreateEntry/index.js";
4+
import { GetModelUseCase } from "~/features/contentModel/GetModel/index.js";
5+
import { createAuthorWithSearchableJsonContextHandler } from "~tests/__helpers/handler/authorWithSearchableJson/context.js";
6+
import type { IAuthorWithSearchableJsonCmsEntryValues } from "~tests/__helpers/models/authorWithSearchableJson.js";
7+
import { AUTHOR_WITH_SEARCHABLE_JSON_MODEL_ID } from "~tests/__helpers/models/authorWithSearchableJson.js";
8+
import type { CmsContext, CmsModel } from "~/types/index.js";
9+
10+
interface ValueTypeCase {
11+
type: string;
12+
summer: Record<string, any>;
13+
winter: Record<string, any>;
14+
}
15+
16+
/**
17+
* Values stored in the searchable-json field are dynamically mapped by OpenSearch.
18+
* Non-text values (dates, numbers, booleans) get non-text mappings, and wildcard queries
19+
* are not allowed on those. Full-text search must not break because of it.
20+
*
21+
* Each case contains only one non-text value type, so each type is verified on its own.
22+
*/
23+
const valueTypeCases: ValueTypeCase[] = [
24+
{
25+
type: "date",
26+
summer: {
27+
activeTo: "2026-08-31T23:59:59.000Z",
28+
address: { validUntil: "2027-01-01T00:00:00.000Z" }
29+
},
30+
winter: {
31+
activeTo: "2027-02-28T23:59:59.000Z",
32+
address: { validUntil: "2027-06-01T00:00:00.000Z" }
33+
}
34+
},
35+
{
36+
type: "integer",
37+
summer: {
38+
priority: 1,
39+
address: { zip: 10000 }
40+
},
41+
winter: {
42+
priority: 2,
43+
address: { zip: 20000 }
44+
}
45+
},
46+
{
47+
type: "float",
48+
summer: {
49+
discount: 0.15,
50+
address: { latitude: 45.815 }
51+
},
52+
winter: {
53+
discount: 0.25,
54+
address: { latitude: 46.305 }
55+
}
56+
},
57+
{
58+
type: "boolean",
59+
summer: {
60+
active: true,
61+
address: { verified: false }
62+
},
63+
winter: {
64+
active: false,
65+
address: { verified: true }
66+
}
67+
},
68+
{
69+
type: "array",
70+
summer: {
71+
dates: ["2026-06-01T00:00:00.000Z", "2026-07-01T00:00:00.000Z"],
72+
integers: [1, 2],
73+
floats: [1.5, 2.5],
74+
flags: [true, false],
75+
periods: [{ from: "2026-06-01T00:00:00.000Z", count: 1, open: true }]
76+
},
77+
winter: {
78+
dates: ["2026-12-01T00:00:00.000Z", "2027-01-01T00:00:00.000Z"],
79+
integers: [3, 4],
80+
floats: [3.5, 4.5],
81+
flags: [false, true],
82+
periods: [{ from: "2026-12-01T00:00:00.000Z", count: 2, open: false }]
83+
}
84+
},
85+
{
86+
type: "mixed",
87+
summer: {
88+
activeTo: "2026-08-31T23:59:59.000Z",
89+
priority: 1,
90+
discount: 0.15,
91+
active: true,
92+
tags: ["beach", "sun"],
93+
periods: [{ from: "2026-06-01T00:00:00.000Z", count: 1, open: true }],
94+
address: {
95+
validUntil: "2027-01-01T00:00:00.000Z",
96+
zip: 10000,
97+
latitude: 45.815,
98+
verified: false,
99+
geo: { updatedOn: "2026-01-01T00:00:00.000Z", precision: 3, exact: true }
100+
}
101+
},
102+
winter: {
103+
activeTo: "2027-02-28T23:59:59.000Z",
104+
priority: 2,
105+
discount: 0.25,
106+
active: false,
107+
tags: ["snow", "ski"],
108+
periods: [{ from: "2026-12-01T00:00:00.000Z", count: 2, open: false }],
109+
address: {
110+
validUntil: "2027-06-01T00:00:00.000Z",
111+
zip: 20000,
112+
latitude: 46.305,
113+
verified: true,
114+
geo: { updatedOn: "2026-02-01T00:00:00.000Z", precision: 5, exact: false }
115+
}
116+
}
117+
}
118+
];
119+
120+
describe.each(valueTypeCases)(
121+
"searchable-json field - full text search with $type values",
122+
({ summer, winter }) => {
123+
let context: CmsContext;
124+
let model: CmsModel;
125+
126+
beforeEach(async () => {
127+
const contextHandler = createAuthorWithSearchableJsonContextHandler();
128+
context = await contextHandler.handler();
129+
130+
model = (
131+
await context.container
132+
.resolve(GetModelUseCase)
133+
.execute(AUTHOR_WITH_SEARCHABLE_JSON_MODEL_ID)
134+
).value;
135+
136+
const createEntry = context.container.resolve(CreateEntryUseCase);
137+
138+
await createEntry.execute<IAuthorWithSearchableJsonCmsEntryValues>(model, {
139+
values: {
140+
name: "John Doe",
141+
info: {
142+
...summer,
143+
title: "Summer campaign",
144+
address: {
145+
...summer.address,
146+
city: "Anytown"
147+
}
148+
}
149+
}
150+
});
151+
await createEntry.execute<IAuthorWithSearchableJsonCmsEntryValues>(model, {
152+
values: {
153+
name: "Jacob Doe",
154+
info: {
155+
...winter,
156+
title: "Winter campaign",
157+
address: {
158+
...winter.address,
159+
city: "Othertown"
160+
}
161+
}
162+
}
163+
});
164+
});
165+
166+
const search = async (term: string) => {
167+
const result = await context.container
168+
.resolve(ListLatestEntriesUseCase)
169+
.execute<IAuthorWithSearchableJsonCmsEntryValues>(model, {
170+
search: term
171+
});
172+
if (result.isFail()) {
173+
throw new Error(result.error.message);
174+
}
175+
return result.value.entries.map(entry => entry.values.name).sort();
176+
};
177+
178+
it("should find entries by a text value in the searchable-json field", async () => {
179+
expect(await search("Summer")).toEqual(["John Doe"]);
180+
expect(await search("campaign")).toEqual(["Jacob Doe", "John Doe"]);
181+
});
182+
183+
it("should find entries by a nested text value in the searchable-json field", async () => {
184+
expect(await search("Othertown")).toEqual(["Jacob Doe"]);
185+
});
186+
187+
it("should find entries by the title field", async () => {
188+
expect(await search("Jacob")).toEqual(["Jacob Doe"]);
189+
});
190+
191+
it("should find nothing when term does not match", async () => {
192+
expect(await search("Autumn")).toEqual([]);
193+
});
194+
}
195+
);

0 commit comments

Comments
 (0)