@webiny/app-website-builder 6.5.0-beta.0 → 6.5.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import { FileManager } from "@webiny/app-admin";
4
4
  import { useBreakpoint } from "../BaseEditor/hooks/useBreakpoint.js";
5
5
  import { getAssetCropParam, normalizeToAsset } from "@webiny/website-builder-sdk";
6
6
  import { fileManagerItemToValue } from "../shared/fileManagerItemToValue.js";
7
- const isEditableImage = (asset)=>!!asset?.src && "string" == typeof asset.type && asset.type.startsWith("image/") && "image/svg+xml" !== asset.type;
7
+ const isEditableImage = (asset)=>!!asset?.src && "string" == typeof asset.mimeType && asset.mimeType.startsWith("image/") && "image/svg+xml" !== asset.mimeType;
8
8
  const hasImageEdit = (image)=>{
9
9
  if (!image?.width || !image?.height) return false;
10
10
  const c = image.crop;
@@ -1 +1 @@
1
- {"version":3,"file":"inputRenderers/FileInput.js","sources":["../../src/inputRenderers/FileInput.tsx"],"sourcesContent":["import React, { useState } from \"react\";\nimport {\n FilePicker,\n ImageEditor,\n RichItemPreview,\n getCroppedImageRenderStyles,\n type ImageEditorValue\n} from \"@webiny/admin-ui\";\nimport type { ElementInputRendererProps } from \"~/BaseEditor/index.js\";\nimport { FileManager, type FileManagerFileItem } from \"@webiny/app-admin\";\nimport { useBreakpoint } from \"~/BaseEditor/hooks/useBreakpoint.js\";\nimport {\n normalizeToAsset,\n getAssetCropParam,\n type Asset,\n type AssetImage\n} from \"@webiny/website-builder-sdk\";\nimport type { FileInput } from \"@webiny/website-builder-sdk\";\nimport { fileManagerItemToValue } from \"~/shared/fileManagerItemToValue.js\";\n\nconst isEditableImage = (asset: Asset | undefined): asset is Asset => {\n return (\n !!asset?.src &&\n typeof asset.type === \"string\" &&\n asset.type.startsWith(\"image/\") &&\n asset.type !== \"image/svg+xml\"\n );\n};\n\n/** True when the image carries a non-trivial crop or focal point worth previewing. */\nconst hasImageEdit = (image: AssetImage | undefined): boolean => {\n if (!image?.width || !image?.height) {\n return false;\n }\n const c = image.crop;\n const fullCrop = !c || (c.top === 0 && c.left === 0 && c.bottom === 0 && c.right === 0);\n const fp = image.focalPoint;\n const centeredFocal = !fp || (fp.x === 0.5 && fp.y === 0.5);\n return !fullCrop || !centeredFocal;\n};\n\n/** Map the stored asset image into the shared `ImageEditor` value (focalPoint → hotspot). */\nconst toEditorValue = (image: AssetImage | undefined): ImageEditorValue | undefined => {\n if (!image) {\n return undefined;\n }\n return {\n crop: image.crop,\n hotspot: image.focalPoint\n ? { x: image.focalPoint.x, y: image.focalPoint.y, width: 1, height: 1 }\n : undefined,\n alt: image.alt,\n caption: image.caption\n };\n};\n\nexport const FileInputRenderer = ({\n value,\n onChange,\n label,\n ...props\n}: ElementInputRendererProps) => {\n const input = props.input as FileInput;\n const { isBaseBreakpoint } = useBreakpoint();\n const [editorOpen, setEditorOpen] = useState(false);\n\n const asset = normalizeToAsset(value) ?? undefined;\n const editable = isEditableImage(asset);\n\n const onFileChange = (file: FileManagerFileItem) => {\n onChange(({ value }) => {\n value.set(fileManagerItemToValue(file));\n });\n };\n\n const onRemove = () => {\n onChange(({ value }) => {\n if (isBaseBreakpoint) {\n value.set(undefined);\n } else {\n value.set(null);\n }\n });\n };\n\n // Save the per-usage crop / focal point / alt override onto the element value,\n // writing the unified asset shape (hotspot → focalPoint).\n const onSaveEdit = (edit: ImageEditorValue) => {\n onChange(({ value }) => {\n if (!asset) {\n return;\n }\n const image: AssetImage = {\n width: asset.image?.width,\n height: asset.image?.height,\n crop: edit.crop,\n focalPoint: edit.hotspot ? { x: edit.hotspot.x, y: edit.hotspot.y } : undefined,\n alt: edit.alt,\n caption: edit.caption\n };\n const updated = { ...asset, image };\n const cropParam = getAssetCropParam(updated);\n if (cropParam) {\n const sep = asset.src.includes(\"?\") ? \"&\" : \"?\";\n updated.url = `${asset.src}${sep}crop=${cropParam}`;\n } else {\n updated.url = asset.src;\n }\n value.set(updated);\n });\n };\n\n return (\n <FileManager\n accept={input.allowedFileTypes}\n onChange={onFileChange}\n render={({ showFileManager }) => (\n <>\n <FilePicker\n variant={\"secondary\"}\n label={label}\n description={input.description}\n type=\"compact\"\n value={asset?.src}\n // Reflect the per-usage crop + focal point in the thumbnail.\n renderFilePreview={\n editable && asset?.src && hasImageEdit(asset.image)\n ? previewProps => (\n <RichItemPreview\n {...previewProps}\n renderImage={({ name }) => {\n const { wrapper, image } =\n getCroppedImageRenderStyles(\n asset.image?.width ?? 0,\n asset.image?.height ?? 0,\n asset.image?.crop,\n asset.image?.focalPoint\n ? {\n x: asset.image.focalPoint.x,\n y: asset.image.focalPoint.y,\n width: 1,\n height: 1\n }\n : undefined,\n { boxWidth: 1, boxHeight: 1, fit: \"cover\" }\n );\n return (\n <div style={wrapper}>\n <img\n src={asset.src as string}\n alt={name}\n draggable={false}\n style={image}\n />\n </div>\n );\n }}\n />\n )\n : undefined\n }\n onSelectItem={() => showFileManager()}\n onRemoveItem={onRemove}\n // The \"Edit\" (pencil) action edits the image (crop / focal\n // point / alt). Replacing the file is done by clicking the\n // preview. For non-images there is nothing to edit, so the\n // pencil is hidden.\n onEditItem={editable ? () => setEditorOpen(true) : undefined}\n />\n {editable ? (\n <ImageEditor\n open={editorOpen}\n onClose={() => setEditorOpen(false)}\n image={{\n src: asset.src,\n width: asset.image?.width ?? 0,\n height: asset.image?.height ?? 0\n }}\n value={toEditorValue(asset.image)}\n onSave={onSaveEdit}\n />\n ) : null}\n </>\n )}\n />\n );\n};\n"],"names":["isEditableImage","asset","hasImageEdit","image","c","fullCrop","fp","centeredFocal","toEditorValue","undefined","FileInputRenderer","value","onChange","label","props","input","isBaseBreakpoint","useBreakpoint","editorOpen","setEditorOpen","useState","normalizeToAsset","editable","onFileChange","file","fileManagerItemToValue","onRemove","onSaveEdit","edit","updated","cropParam","getAssetCropParam","sep","FileManager","showFileManager","FilePicker","previewProps","RichItemPreview","name","wrapper","getCroppedImageRenderStyles","ImageEditor"],"mappings":"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"}
1
+ {"version":3,"file":"inputRenderers/FileInput.js","sources":["../../src/inputRenderers/FileInput.tsx"],"sourcesContent":["import React, { useState } from \"react\";\nimport {\n FilePicker,\n ImageEditor,\n RichItemPreview,\n getCroppedImageRenderStyles,\n type ImageEditorValue\n} from \"@webiny/admin-ui\";\nimport type { ElementInputRendererProps } from \"~/BaseEditor/index.js\";\nimport { FileManager, type FileManagerFileItem } from \"@webiny/app-admin\";\nimport { useBreakpoint } from \"~/BaseEditor/hooks/useBreakpoint.js\";\nimport {\n normalizeToAsset,\n getAssetCropParam,\n type Asset,\n type AssetImage\n} from \"@webiny/website-builder-sdk\";\nimport type { FileInput } from \"@webiny/website-builder-sdk\";\nimport { fileManagerItemToValue } from \"~/shared/fileManagerItemToValue.js\";\n\nconst isEditableImage = (asset: Asset | undefined): asset is Asset => {\n return (\n !!asset?.src &&\n typeof asset.mimeType === \"string\" &&\n asset.mimeType.startsWith(\"image/\") &&\n asset.mimeType !== \"image/svg+xml\"\n );\n};\n\n/** True when the image carries a non-trivial crop or focal point worth previewing. */\nconst hasImageEdit = (image: AssetImage | undefined): boolean => {\n if (!image?.width || !image?.height) {\n return false;\n }\n const c = image.crop;\n const fullCrop = !c || (c.top === 0 && c.left === 0 && c.bottom === 0 && c.right === 0);\n const fp = image.focalPoint;\n const centeredFocal = !fp || (fp.x === 0.5 && fp.y === 0.5);\n return !fullCrop || !centeredFocal;\n};\n\n/** Map the stored asset image into the shared `ImageEditor` value (focalPoint → hotspot). */\nconst toEditorValue = (image: AssetImage | undefined): ImageEditorValue | undefined => {\n if (!image) {\n return undefined;\n }\n return {\n crop: image.crop,\n hotspot: image.focalPoint\n ? { x: image.focalPoint.x, y: image.focalPoint.y, width: 1, height: 1 }\n : undefined,\n alt: image.alt,\n caption: image.caption\n };\n};\n\nexport const FileInputRenderer = ({\n value,\n onChange,\n label,\n ...props\n}: ElementInputRendererProps) => {\n const input = props.input as FileInput;\n const { isBaseBreakpoint } = useBreakpoint();\n const [editorOpen, setEditorOpen] = useState(false);\n\n const asset = normalizeToAsset(value) ?? undefined;\n const editable = isEditableImage(asset);\n\n const onFileChange = (file: FileManagerFileItem) => {\n onChange(({ value }) => {\n value.set(fileManagerItemToValue(file));\n });\n };\n\n const onRemove = () => {\n onChange(({ value }) => {\n if (isBaseBreakpoint) {\n value.set(undefined);\n } else {\n value.set(null);\n }\n });\n };\n\n // Save the per-usage crop / focal point / alt override onto the element value,\n // writing the unified asset shape (hotspot → focalPoint).\n const onSaveEdit = (edit: ImageEditorValue) => {\n onChange(({ value }) => {\n if (!asset) {\n return;\n }\n const image: AssetImage = {\n width: asset.image?.width,\n height: asset.image?.height,\n crop: edit.crop,\n focalPoint: edit.hotspot ? { x: edit.hotspot.x, y: edit.hotspot.y } : undefined,\n alt: edit.alt,\n caption: edit.caption\n };\n const updated = { ...asset, image };\n const cropParam = getAssetCropParam(updated);\n if (cropParam) {\n const sep = asset.src.includes(\"?\") ? \"&\" : \"?\";\n updated.url = `${asset.src}${sep}crop=${cropParam}`;\n } else {\n updated.url = asset.src;\n }\n value.set(updated);\n });\n };\n\n return (\n <FileManager\n accept={input.allowedFileTypes}\n onChange={onFileChange}\n render={({ showFileManager }) => (\n <>\n <FilePicker\n variant={\"secondary\"}\n label={label}\n description={input.description}\n type=\"compact\"\n value={asset?.src}\n // Reflect the per-usage crop + focal point in the thumbnail.\n renderFilePreview={\n editable && asset?.src && hasImageEdit(asset.image)\n ? previewProps => (\n <RichItemPreview\n {...previewProps}\n renderImage={({ name }) => {\n const { wrapper, image } =\n getCroppedImageRenderStyles(\n asset.image?.width ?? 0,\n asset.image?.height ?? 0,\n asset.image?.crop,\n asset.image?.focalPoint\n ? {\n x: asset.image.focalPoint.x,\n y: asset.image.focalPoint.y,\n width: 1,\n height: 1\n }\n : undefined,\n { boxWidth: 1, boxHeight: 1, fit: \"cover\" }\n );\n return (\n <div style={wrapper}>\n <img\n src={asset.src as string}\n alt={name}\n draggable={false}\n style={image}\n />\n </div>\n );\n }}\n />\n )\n : undefined\n }\n onSelectItem={() => showFileManager()}\n onRemoveItem={onRemove}\n // The \"Edit\" (pencil) action edits the image (crop / focal\n // point / alt). Replacing the file is done by clicking the\n // preview. For non-images there is nothing to edit, so the\n // pencil is hidden.\n onEditItem={editable ? () => setEditorOpen(true) : undefined}\n />\n {editable ? (\n <ImageEditor\n open={editorOpen}\n onClose={() => setEditorOpen(false)}\n image={{\n src: asset.src,\n width: asset.image?.width ?? 0,\n height: asset.image?.height ?? 0\n }}\n value={toEditorValue(asset.image)}\n onSave={onSaveEdit}\n />\n ) : null}\n </>\n )}\n />\n );\n};\n"],"names":["isEditableImage","asset","hasImageEdit","image","c","fullCrop","fp","centeredFocal","toEditorValue","undefined","FileInputRenderer","value","onChange","label","props","input","isBaseBreakpoint","useBreakpoint","editorOpen","setEditorOpen","useState","normalizeToAsset","editable","onFileChange","file","fileManagerItemToValue","onRemove","onSaveEdit","edit","updated","cropParam","getAssetCropParam","sep","FileManager","showFileManager","FilePicker","previewProps","RichItemPreview","name","wrapper","getCroppedImageRenderStyles","ImageEditor"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webiny/app-website-builder",
3
- "version": "6.5.0-beta.0",
3
+ "version": "6.5.0-beta.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js",
@@ -19,28 +19,28 @@
19
19
  "@lexical/utils": "0.48.0",
20
20
  "@monaco-editor/react": "4.7.0",
21
21
  "@types/react": "18.3.31",
22
- "@webiny/admin-ui": "6.5.0-beta.0",
23
- "@webiny/app": "6.5.0-beta.0",
24
- "@webiny/app-aco": "6.5.0-beta.0",
25
- "@webiny/app-admin": "6.5.0-beta.0",
26
- "@webiny/app-headless-cms": "6.5.0-beta.0",
27
- "@webiny/app-headless-cms-common": "6.5.0-beta.0",
28
- "@webiny/app-utils": "6.5.0-beta.0",
22
+ "@webiny/admin-ui": "6.5.0-beta.1",
23
+ "@webiny/app": "6.5.0-beta.1",
24
+ "@webiny/app-aco": "6.5.0-beta.1",
25
+ "@webiny/app-admin": "6.5.0-beta.1",
26
+ "@webiny/app-headless-cms": "6.5.0-beta.1",
27
+ "@webiny/app-headless-cms-common": "6.5.0-beta.1",
28
+ "@webiny/app-utils": "6.5.0-beta.1",
29
29
  "@webiny/di": "1.0.2",
30
- "@webiny/feature": "6.5.0-beta.0",
31
- "@webiny/form": "6.5.0-beta.0",
32
- "@webiny/frontend-settings": "6.5.0-beta.0",
33
- "@webiny/icons": "6.5.0-beta.0",
34
- "@webiny/languages": "6.5.0-beta.0",
35
- "@webiny/lexical-converter": "6.5.0-beta.0",
36
- "@webiny/lexical-editor": "6.5.0-beta.0",
37
- "@webiny/lexical-nodes": "6.5.0-beta.0",
38
- "@webiny/lexical-theme": "6.5.0-beta.0",
39
- "@webiny/react-composition": "6.5.0-beta.0",
40
- "@webiny/react-properties": "6.5.0-beta.0",
41
- "@webiny/utils": "6.5.0-beta.0",
42
- "@webiny/validation": "6.5.0-beta.0",
43
- "@webiny/website-builder-sdk": "6.5.0-beta.0",
30
+ "@webiny/feature": "6.5.0-beta.1",
31
+ "@webiny/form": "6.5.0-beta.1",
32
+ "@webiny/frontend-settings": "6.5.0-beta.1",
33
+ "@webiny/icons": "6.5.0-beta.1",
34
+ "@webiny/languages": "6.5.0-beta.1",
35
+ "@webiny/lexical-converter": "6.5.0-beta.1",
36
+ "@webiny/lexical-editor": "6.5.0-beta.1",
37
+ "@webiny/lexical-nodes": "6.5.0-beta.1",
38
+ "@webiny/lexical-theme": "6.5.0-beta.1",
39
+ "@webiny/react-composition": "6.5.0-beta.1",
40
+ "@webiny/react-properties": "6.5.0-beta.1",
41
+ "@webiny/utils": "6.5.0-beta.1",
42
+ "@webiny/validation": "6.5.0-beta.1",
43
+ "@webiny/website-builder-sdk": "6.5.0-beta.1",
44
44
  "classnames": "2.5.1",
45
45
  "deep-equal": "2.2.3",
46
46
  "graphql": "16.14.2",
@@ -61,7 +61,7 @@
61
61
  "devDependencies": {
62
62
  "@types/deep-equal": "1.0.4",
63
63
  "@types/is-hotkey": "0.1.10",
64
- "@webiny/build-tools": "6.5.0-beta.0",
64
+ "@webiny/build-tools": "6.5.0-beta.1",
65
65
  "execa": "5.1.1",
66
66
  "rimraf": "6.1.3",
67
67
  "typescript": "7.0.2",
@@ -7,7 +7,7 @@ class PageListDataSource extends FolderAwareDataSource {
7
7
  }), this.listPagesUseCase = listPagesUseCase, this.cache = cache;
8
8
  }
9
9
  get rows() {
10
- return this.queryMatcher.filter(this.cache.getItems());
10
+ return this.queryMatcher.select(this.cache.getItems());
11
11
  }
12
12
  async fetch(params) {
13
13
  return this.listPagesUseCase.execute({
@@ -1 +1 @@
1
- {"version":3,"file":"presentation/pages/PageList/PageListDataSource.js","sources":["../../../../src/presentation/pages/PageList/PageListDataSource.ts"],"sourcesContent":["import { FolderAwareDataSource } from \"@webiny/app-admin/presentation/listPresenter/FolderAwareDataSource.js\";\nimport type {\n FetchParams,\n FetchResult\n} from \"@webiny/app-admin/presentation/listPresenter/FolderAwareDataSource.js\";\nimport type { IGetDescendantFoldersUseCase } from \"@webiny/app-aco/features/folders/getDescendantFolders/abstractions.js\";\nimport type { Page } from \"~/domain/Page/Page.js\";\nimport type { IListPagesUseCase } from \"~/features/pages/listPages/abstractions.js\";\nimport type { IListCache } from \"@webiny/app-admin/features/listCache/index.js\";\n\nexport class PageListDataSource extends FolderAwareDataSource<Page> {\n constructor(\n private listPagesUseCase: IListPagesUseCase,\n private cache: IListCache<Page>,\n getDescendantFoldersUseCase?: IGetDescendantFoldersUseCase\n ) {\n super({\n keyField: \"entryId\",\n getDescendantFolders: getDescendantFoldersUseCase\n });\n }\n\n get rows(): Page[] {\n return this.queryMatcher.filter(this.cache.getItems());\n }\n\n async fetch(params: FetchParams): Promise<FetchResult<Page>> {\n return this.listPagesUseCase.execute({\n where: params.where,\n sort: params.sort,\n search: params.search,\n limit: params.limit,\n after: params.after\n });\n }\n}\n"],"names":["PageListDataSource","FolderAwareDataSource","listPagesUseCase","cache","getDescendantFoldersUseCase","params"],"mappings":";AAUO,MAAMA,2BAA2BC;IACpC,YACYC,gBAAmC,EACnCC,KAAuB,EAC/BC,2BAA0D,CAC5D;QACE,KAAK,CAAC;YACF,UAAU;YACV,sBAAsBA;QAC1B,SAPQF,gBAAgB,GAAhBA,kBAAAA,IAAAA,CACAC,KAAK,GAALA;IAOZ;IAEA,IAAI,OAAe;QACf,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ;IACvD;IAEA,MAAM,MAAME,MAAmB,EAA8B;QACzD,OAAO,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC;YACjC,OAAOA,OAAO,KAAK;YACnB,MAAMA,OAAO,IAAI;YACjB,QAAQA,OAAO,MAAM;YACrB,OAAOA,OAAO,KAAK;YACnB,OAAOA,OAAO,KAAK;QACvB;IACJ;AACJ"}
1
+ {"version":3,"file":"presentation/pages/PageList/PageListDataSource.js","sources":["../../../../src/presentation/pages/PageList/PageListDataSource.ts"],"sourcesContent":["import { FolderAwareDataSource } from \"@webiny/app-admin/presentation/listPresenter/FolderAwareDataSource.js\";\nimport type {\n FetchParams,\n FetchResult\n} from \"@webiny/app-admin/presentation/listPresenter/FolderAwareDataSource.js\";\nimport type { IGetDescendantFoldersUseCase } from \"@webiny/app-aco/features/folders/getDescendantFolders/abstractions.js\";\nimport type { Page } from \"~/domain/Page/Page.js\";\nimport type { IListPagesUseCase } from \"~/features/pages/listPages/abstractions.js\";\nimport type { IListCache } from \"@webiny/app-admin/features/listCache/index.js\";\n\nexport class PageListDataSource extends FolderAwareDataSource<Page> {\n constructor(\n private listPagesUseCase: IListPagesUseCase,\n private cache: IListCache<Page>,\n getDescendantFoldersUseCase?: IGetDescendantFoldersUseCase\n ) {\n super({\n keyField: \"entryId\",\n getDescendantFolders: getDescendantFoldersUseCase\n });\n }\n\n get rows(): Page[] {\n return this.queryMatcher.select(this.cache.getItems());\n }\n\n async fetch(params: FetchParams): Promise<FetchResult<Page>> {\n return this.listPagesUseCase.execute({\n where: params.where,\n sort: params.sort,\n search: params.search,\n limit: params.limit,\n after: params.after\n });\n }\n}\n"],"names":["PageListDataSource","FolderAwareDataSource","listPagesUseCase","cache","getDescendantFoldersUseCase","params"],"mappings":";AAUO,MAAMA,2BAA2BC;IACpC,YACYC,gBAAmC,EACnCC,KAAuB,EAC/BC,2BAA0D,CAC5D;QACE,KAAK,CAAC;YACF,UAAU;YACV,sBAAsBA;QAC1B,SAPQF,gBAAgB,GAAhBA,kBAAAA,IAAAA,CACAC,KAAK,GAALA;IAOZ;IAEA,IAAI,OAAe;QACf,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ;IACvD;IAEA,MAAM,MAAME,MAAmB,EAA8B;QACzD,OAAO,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC;YACjC,OAAOA,OAAO,KAAK;YACnB,MAAMA,OAAO,IAAI;YACjB,QAAQA,OAAO,MAAM;YACrB,OAAOA,OAAO,KAAK;YACnB,OAAOA,OAAO,KAAK;QACvB;IACJ;AACJ"}
@@ -0,0 +1,291 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { observable } from "mobx";
3
+ import { Container } from "@webiny/di";
4
+ import { ListCache } from "@webiny/app-admin/features/listCache/index.js";
5
+ import { ListPresenter } from "@webiny/app-admin/presentation/listPresenter/ListPresenter.js";
6
+ import { FolderTreePresenter } from "@webiny/app-aco/presentation/folderTree/abstractions.js";
7
+ import { GetDescendantFoldersUseCase } from "@webiny/app-aco/features/folders/getDescendantFolders/abstractions.js";
8
+ import { Confirmation } from "@webiny/app-admin/features/confirmation/abstractions.js";
9
+ import { Page } from "../../../domain/Page/Page.js";
10
+ import { ListPagesGateway } from "../../../features/pages/listPages/abstractions.js";
11
+ import { ListPagesUseCase } from "../../../features/pages/listPages/ListPagesUseCase.js";
12
+ import { ListPagesRepository } from "../../../features/pages/listPages/ListPagesRepository.js";
13
+ import { FullPageCache, PageListCache } from "../../../features/pages/shared/abstractions.js";
14
+ import { PublishPageGateway } from "../../../features/pages/publishPage/abstractions.js";
15
+ import { PublishPageUseCase } from "../../../features/pages/publishPage/PublishPageUseCase.js";
16
+ import { PublishPageRepository } from "../../../features/pages/publishPage/PublishPageRepository.js";
17
+ import { DeletePageUseCase } from "../../../features/pages/deletePage/abstractions.js";
18
+ import { UnpublishPageUseCase } from "../../../features/pages/unpublishPage/abstractions.js";
19
+ import { MovePageUseCase } from "../../../features/pages/movePage/abstractions.js";
20
+ import { DuplicatePageUseCase } from "../../../features/pages/duplicatePage/abstractions.js";
21
+ import { PageListPresenter } from "./abstractions.js";
22
+ import { PageListPresenter as external_PageListPresenter_js_PageListPresenter } from "./PageListPresenter.js";
23
+ const TOTAL_PAGES = 300;
24
+ const PAGE_SIZE = 50;
25
+ const BASE_TIME = Date.UTC(2026, 0, 1);
26
+ const minutes = (n)=>new Date(BASE_TIME + 60000 * n).toISOString();
27
+ function createPages(count) {
28
+ return Array.from({
29
+ length: count
30
+ }, (_, i)=>Page.create({
31
+ id: `page-${i}#0001`,
32
+ entryId: `page-${i}`,
33
+ location: {
34
+ folderId: "root"
35
+ },
36
+ properties: {
37
+ title: `Page ${i}`
38
+ },
39
+ createdOn: minutes(count - i),
40
+ savedOn: minutes(i)
41
+ }));
42
+ }
43
+ function createGateway(pages) {
44
+ return {
45
+ execute: vi.fn(async (params)=>{
46
+ const [sort = "savedOn_DESC"] = params.sort ?? [];
47
+ const [field, direction] = sort.split("_");
48
+ const multiplier = "ASC" === direction ? 1 : -1;
49
+ const folderId = params.where?.location?.folderId;
50
+ const sorted = pages.filter((page)=>!folderId || page.location.folderId === folderId).sort((a, b)=>multiplier * String(a[field]).localeCompare(String(b[field])));
51
+ const offset = params.after ? Number(params.after) : 0;
52
+ const limit = params.limit ?? PAGE_SIZE;
53
+ const data = sorted.slice(offset, offset + limit);
54
+ const next = offset + data.length;
55
+ const hasMoreItems = next < sorted.length;
56
+ return {
57
+ data,
58
+ meta: {
59
+ cursor: hasMoreItems ? String(next) : null,
60
+ hasMoreItems,
61
+ totalCount: sorted.length
62
+ }
63
+ };
64
+ })
65
+ };
66
+ }
67
+ function createFolderTreePresenter() {
68
+ const vm = observable({
69
+ folders: [],
70
+ tree: [],
71
+ currentFolderId: null,
72
+ currentFolder: null,
73
+ loading: false,
74
+ operation: {
75
+ active: false,
76
+ mode: null
77
+ },
78
+ isRootFolder: true,
79
+ currentFolderTitle: "All pages",
80
+ childFolders: [],
81
+ loadingNodeIds: []
82
+ });
83
+ return {
84
+ get vm () {
85
+ return vm;
86
+ },
87
+ selectFolder: vi.fn(),
88
+ createFolder: vi.fn(),
89
+ editFolder: vi.fn(),
90
+ deleteFolder: vi.fn().mockResolvedValue(void 0),
91
+ moveFolder: vi.fn().mockResolvedValue(void 0),
92
+ loadChildFolders: vi.fn().mockResolvedValue(void 0),
93
+ canManageStructure: vi.fn().mockReturnValue(true),
94
+ getAncestorIds: vi.fn().mockReturnValue([]),
95
+ submitOperation: vi.fn().mockResolvedValue(true),
96
+ cancelOperation: vi.fn(),
97
+ onFolderChange: vi.fn().mockReturnValue(()=>{})
98
+ };
99
+ }
100
+ const noopUseCase = {
101
+ execute: vi.fn()
102
+ };
103
+ function range(from, to) {
104
+ const step = from <= to ? 1 : -1;
105
+ const ids = [];
106
+ for(let i = from; step > 0 ? i <= to : i >= to; i += step)ids.push(`page-${i}`);
107
+ return ids;
108
+ }
109
+ describe("PageListPresenter", ()=>{
110
+ let presenter;
111
+ let cache;
112
+ const publishGateway = {
113
+ execute: vi.fn(async (id)=>{
114
+ const page = cache.getItem((p)=>p.id === id);
115
+ return {
116
+ ...page,
117
+ status: "published"
118
+ };
119
+ })
120
+ };
121
+ const rowIds = ()=>presenter.list.vm.rows.map((row)=>row.entryId);
122
+ const waitForQuery = async (field, direction)=>{
123
+ await vi.waitFor(()=>{
124
+ const sort = presenter.list.vm.appliedQuery?.sort;
125
+ expect(sort).toEqual({
126
+ field,
127
+ direction
128
+ });
129
+ expect(presenter.list.vm.pagination.loading).toBe(false);
130
+ });
131
+ };
132
+ const initAndWait = async ()=>{
133
+ presenter.init();
134
+ await waitForQuery("savedOn", "DESC");
135
+ };
136
+ const loadAll = async ()=>{
137
+ while(presenter.list.vm.pagination.hasMore)await presenter.list.actions.loadMore();
138
+ };
139
+ beforeEach(()=>{
140
+ const container = new Container();
141
+ cache = new ListCache("entryId");
142
+ container.registerInstance(PageListCache, cache);
143
+ container.registerInstance(ListPagesGateway, createGateway(createPages(TOTAL_PAGES)));
144
+ container.register(ListPagesRepository).inSingletonScope();
145
+ container.register(ListPagesUseCase);
146
+ container.register(ListPresenter);
147
+ container.registerInstance(FolderTreePresenter, createFolderTreePresenter());
148
+ container.registerInstance(Confirmation, {
149
+ confirm: vi.fn()
150
+ });
151
+ container.registerInstance(GetDescendantFoldersUseCase, {
152
+ execute: ()=>[]
153
+ });
154
+ container.registerInstance(DeletePageUseCase, noopUseCase);
155
+ container.registerInstance(FullPageCache, new ListCache("id"));
156
+ container.registerInstance(PublishPageGateway, publishGateway);
157
+ container.register(PublishPageRepository).inSingletonScope();
158
+ container.register(PublishPageUseCase);
159
+ container.registerInstance(UnpublishPageUseCase, noopUseCase);
160
+ container.registerInstance(MovePageUseCase, noopUseCase);
161
+ container.registerInstance(DuplicatePageUseCase, noopUseCase);
162
+ container.register(external_PageListPresenter_js_PageListPresenter);
163
+ presenter = container.resolve(PageListPresenter);
164
+ });
165
+ describe("pagination", ()=>{
166
+ it("should show the most recently saved pages first", async ()=>{
167
+ await initAndWait();
168
+ expect(rowIds()).toEqual(range(299, 250));
169
+ });
170
+ it("should append the next batch of pages to the bottom of the list", async ()=>{
171
+ await initAndWait();
172
+ await presenter.list.actions.loadMore();
173
+ expect(rowIds()).toEqual(range(299, 200));
174
+ });
175
+ it("should keep all pages in order after loading everything", async ()=>{
176
+ await initAndWait();
177
+ await loadAll();
178
+ expect(rowIds()).toEqual(range(299, 0));
179
+ });
180
+ });
181
+ describe("changing the sort", ()=>{
182
+ it("should show only the first batch, in the new direction", async ()=>{
183
+ await initAndWait();
184
+ presenter.list.actions.sort.set("savedOn", "ASC");
185
+ await waitForQuery("savedOn", "ASC");
186
+ expect(rowIds()).toEqual(range(0, 49));
187
+ });
188
+ it("should show only the first batch, sorted by the new field", async ()=>{
189
+ await initAndWait();
190
+ presenter.list.actions.sort.set("createdOn", "DESC");
191
+ await waitForQuery("createdOn", "DESC");
192
+ expect(rowIds()).toEqual(range(0, 49));
193
+ });
194
+ it("should paginate in the new order", async ()=>{
195
+ await initAndWait();
196
+ presenter.list.actions.sort.set("savedOn", "ASC");
197
+ await waitForQuery("savedOn", "ASC");
198
+ await presenter.list.actions.loadMore();
199
+ expect(rowIds()).toEqual(range(0, 99));
200
+ });
201
+ it("should restore the original order when switching back", async ()=>{
202
+ await initAndWait();
203
+ await presenter.list.actions.loadMore();
204
+ presenter.list.actions.sort.set("savedOn", "ASC");
205
+ await waitForQuery("savedOn", "ASC");
206
+ presenter.list.actions.sort.set("savedOn", "DESC");
207
+ await waitForQuery("savedOn", "DESC");
208
+ expect(rowIds()).toEqual(range(299, 250));
209
+ });
210
+ });
211
+ describe("local cache changes", ()=>{
212
+ const newPage = ()=>Page.create({
213
+ id: "page-new#0001",
214
+ entryId: "page-new",
215
+ location: {
216
+ folderId: "root"
217
+ },
218
+ properties: {
219
+ title: "New page"
220
+ },
221
+ createdOn: minutes(TOTAL_PAGES + 1),
222
+ savedOn: minutes(TOTAL_PAGES + 1)
223
+ });
224
+ it("should show a created page at the top when it sorts first", async ()=>{
225
+ await initAndWait();
226
+ cache.addItems([
227
+ newPage()
228
+ ]);
229
+ expect(rowIds()).toEqual([
230
+ "page-new",
231
+ ...range(299, 250)
232
+ ]);
233
+ });
234
+ it("should not show a created page that sorts past the loaded pages", async ()=>{
235
+ await initAndWait();
236
+ presenter.list.actions.sort.set("savedOn", "ASC");
237
+ await waitForQuery("savedOn", "ASC");
238
+ cache.addItems([
239
+ newPage()
240
+ ]);
241
+ expect(rowIds()).toEqual(range(0, 49));
242
+ });
243
+ it("should show a created page in its sort position once everything is loaded", async ()=>{
244
+ await initAndWait();
245
+ presenter.list.actions.sort.set("savedOn", "ASC");
246
+ await waitForQuery("savedOn", "ASC");
247
+ await loadAll();
248
+ cache.addItems([
249
+ newPage()
250
+ ]);
251
+ expect(rowIds()).toEqual([
252
+ ...range(0, 299),
253
+ "page-new"
254
+ ]);
255
+ });
256
+ it("should move an updated page to its new sort position", async ()=>{
257
+ await initAndWait();
258
+ const page = cache.getItem((p)=>"page-260" === p.entryId);
259
+ cache.addItems([
260
+ Page.create({
261
+ ...page,
262
+ savedOn: minutes(TOTAL_PAGES + 1)
263
+ })
264
+ ]);
265
+ expect(rowIds()).toEqual([
266
+ "page-260",
267
+ ...range(299, 261),
268
+ ...range(259, 250)
269
+ ]);
270
+ });
271
+ });
272
+ describe("publishing", ()=>{
273
+ it("should keep a published page in its position", async ()=>{
274
+ await initAndWait();
275
+ const before = rowIds();
276
+ await presenter.publishPage("page-260#0001");
277
+ expect(rowIds()).toEqual(before);
278
+ const published = presenter.list.vm.rows.find((row)=>"page-260" === row.entryId);
279
+ expect(published?.status).toBe("published");
280
+ });
281
+ it("should keep a published page in its position after loading more pages", async ()=>{
282
+ await initAndWait();
283
+ await presenter.list.actions.loadMore();
284
+ const before = rowIds();
285
+ await presenter.publishPage("page-210#0001");
286
+ expect(rowIds()).toEqual(before);
287
+ });
288
+ });
289
+ });
290
+
291
+ //# sourceMappingURL=PageListPresenter.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presentation/pages/PageList/PageListPresenter.test.js","sources":["../../../../src/presentation/pages/PageList/PageListPresenter.test.ts"],"sourcesContent":["import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { observable } from \"mobx\";\nimport { Container } from \"@webiny/di\";\nimport { ListCache } from \"@webiny/app-admin/features/listCache/index.js\";\nimport { ListPresenter } from \"@webiny/app-admin/presentation/listPresenter/ListPresenter.js\";\nimport { FolderTreePresenter } from \"@webiny/app-aco/presentation/folderTree/abstractions.js\";\nimport { GetDescendantFoldersUseCase } from \"@webiny/app-aco/features/folders/getDescendantFolders/abstractions.js\";\nimport { Confirmation } from \"@webiny/app-admin/features/confirmation/abstractions.js\";\nimport { Page } from \"~/domain/Page/Page.js\";\nimport {\n ListPagesGateway,\n type IListPagesGatewayParams,\n type IListPagesGatewayResult\n} from \"~/features/pages/listPages/abstractions.js\";\nimport { ListPagesUseCase } from \"~/features/pages/listPages/ListPagesUseCase.js\";\nimport { ListPagesRepository } from \"~/features/pages/listPages/ListPagesRepository.js\";\nimport { FullPageCache, PageListCache } from \"~/features/pages/shared/abstractions.js\";\nimport { PublishPageGateway } from \"~/features/pages/publishPage/abstractions.js\";\nimport { PublishPageUseCase as PublishPageUseCaseImpl } from \"~/features/pages/publishPage/PublishPageUseCase.js\";\nimport { PublishPageRepository } from \"~/features/pages/publishPage/PublishPageRepository.js\";\nimport { DeletePageUseCase } from \"~/features/pages/deletePage/abstractions.js\";\nimport { UnpublishPageUseCase } from \"~/features/pages/unpublishPage/abstractions.js\";\nimport { MovePageUseCase } from \"~/features/pages/movePage/abstractions.js\";\nimport { DuplicatePageUseCase } from \"~/features/pages/duplicatePage/abstractions.js\";\nimport { PageListPresenter as Abstraction } from \"./abstractions.js\";\nimport { PageListPresenter } from \"./PageListPresenter.js\";\n\nconst TOTAL_PAGES = 300;\nconst PAGE_SIZE = 50;\nconst BASE_TIME = Date.UTC(2026, 0, 1);\n\nconst minutes = (n: number) => new Date(BASE_TIME + n * 60_000).toISOString();\n\n/**\n * A higher index means a more recently *saved* page, but a less recently *created* one,\n * so sorting by `savedOn` and by `createdOn` produce opposite orders.\n */\nfunction createPages(count: number): Page[] {\n return Array.from({ length: count }, (_, i) =>\n Page.create({\n id: `page-${i}#0001`,\n entryId: `page-${i}`,\n location: { folderId: \"root\" },\n properties: { title: `Page ${i}` },\n createdOn: minutes(count - i),\n savedOn: minutes(i)\n })\n );\n}\n\n/**\n * Simulates the API: filters by folder, sorts by the requested field, and paginates\n * with an offset-based cursor.\n */\nfunction createGateway(pages: Page[]): ListPagesGateway.Interface {\n return {\n execute: vi.fn(\n async (params: IListPagesGatewayParams): Promise<IListPagesGatewayResult> => {\n const [sort = \"savedOn_DESC\"] = params.sort ?? [];\n const [field, direction] = sort.split(\"_\") as [keyof Page, \"ASC\" | \"DESC\"];\n const multiplier = direction === \"ASC\" ? 1 : -1;\n\n const folderId = (params.where?.location as { folderId?: string })?.folderId;\n const sorted = pages\n .filter(page => !folderId || page.location.folderId === folderId)\n .sort((a, b) => multiplier * String(a[field]).localeCompare(String(b[field])));\n\n const offset = params.after ? Number(params.after) : 0;\n const limit = params.limit ?? PAGE_SIZE;\n const data = sorted.slice(offset, offset + limit);\n const next = offset + data.length;\n const hasMoreItems = next < sorted.length;\n\n return {\n data,\n meta: {\n cursor: hasMoreItems ? String(next) : null,\n hasMoreItems,\n totalCount: sorted.length\n }\n };\n }\n )\n };\n}\n\nfunction createFolderTreePresenter(): FolderTreePresenter.Interface {\n const vm = observable({\n folders: [],\n tree: [],\n currentFolderId: null as string | null,\n currentFolder: null,\n loading: false,\n operation: { active: false, mode: null },\n isRootFolder: true,\n currentFolderTitle: \"All pages\",\n childFolders: [],\n loadingNodeIds: []\n });\n\n return {\n get vm() {\n return vm as unknown as FolderTreePresenter.Interface[\"vm\"];\n },\n selectFolder: vi.fn(),\n createFolder: vi.fn(),\n editFolder: vi.fn(),\n deleteFolder: vi.fn().mockResolvedValue(undefined),\n moveFolder: vi.fn().mockResolvedValue(undefined),\n loadChildFolders: vi.fn().mockResolvedValue(undefined),\n canManageStructure: vi.fn().mockReturnValue(true),\n getAncestorIds: vi.fn().mockReturnValue([]),\n submitOperation: vi.fn().mockResolvedValue(true),\n cancelOperation: vi.fn(),\n onFolderChange: vi.fn().mockReturnValue(() => {})\n };\n}\n\nconst noopUseCase = { execute: vi.fn() };\n\n/**\n * Expected entry IDs for a contiguous range of page indexes, in the given order.\n */\nfunction range(from: number, to: number): string[] {\n const step = from <= to ? 1 : -1;\n const ids: string[] = [];\n for (let i = from; step > 0 ? i <= to : i >= to; i += step) {\n ids.push(`page-${i}`);\n }\n return ids;\n}\n\ndescribe(\"PageListPresenter\", () => {\n let presenter: Abstraction.Interface;\n let cache: ListCache<Page>;\n\n /**\n * Simulates the publish API: publishing is not a content modification, so the page\n * comes back with its `savedOn` unchanged.\n */\n const publishGateway: PublishPageGateway.Interface = {\n execute: vi.fn(async (id: string) => {\n const page = cache.getItem(p => p.id === id)!;\n return { ...page, status: \"published\" } as any;\n })\n };\n\n const rowIds = () => presenter.list.vm.rows.map(row => row.entryId);\n\n const waitForQuery = async (field: string, direction: \"ASC\" | \"DESC\") => {\n await vi.waitFor(() => {\n const sort = presenter.list.vm.appliedQuery?.sort;\n expect(sort).toEqual({ field, direction });\n expect(presenter.list.vm.pagination.loading).toBe(false);\n });\n };\n\n const initAndWait = async () => {\n presenter.init();\n await waitForQuery(\"savedOn\", \"DESC\");\n };\n\n const loadAll = async () => {\n while (presenter.list.vm.pagination.hasMore) {\n await presenter.list.actions.loadMore();\n }\n };\n\n beforeEach(() => {\n const container = new Container();\n\n cache = new ListCache<Page>(\"entryId\");\n container.registerInstance(PageListCache, cache);\n container.registerInstance(ListPagesGateway, createGateway(createPages(TOTAL_PAGES)));\n container.register(ListPagesRepository).inSingletonScope();\n container.register(ListPagesUseCase);\n\n container.register(ListPresenter);\n container.registerInstance(FolderTreePresenter, createFolderTreePresenter());\n container.registerInstance(Confirmation, { confirm: vi.fn() } as Confirmation.Interface);\n container.registerInstance(GetDescendantFoldersUseCase, { execute: () => [] });\n container.registerInstance(DeletePageUseCase, noopUseCase as any);\n container.registerInstance(FullPageCache, new ListCache<Page>(\"id\"));\n container.registerInstance(PublishPageGateway, publishGateway);\n container.register(PublishPageRepository).inSingletonScope();\n container.register(PublishPageUseCaseImpl);\n container.registerInstance(UnpublishPageUseCase, noopUseCase as any);\n container.registerInstance(MovePageUseCase, noopUseCase as any);\n container.registerInstance(DuplicatePageUseCase, noopUseCase as any);\n\n container.register(PageListPresenter);\n\n presenter = container.resolve(Abstraction);\n });\n\n describe(\"pagination\", () => {\n it(\"should show the most recently saved pages first\", async () => {\n await initAndWait();\n\n expect(rowIds()).toEqual(range(299, 250));\n });\n\n it(\"should append the next batch of pages to the bottom of the list\", async () => {\n await initAndWait();\n\n await presenter.list.actions.loadMore();\n\n expect(rowIds()).toEqual(range(299, 200));\n });\n\n it(\"should keep all pages in order after loading everything\", async () => {\n await initAndWait();\n\n await loadAll();\n\n expect(rowIds()).toEqual(range(299, 0));\n });\n });\n\n describe(\"changing the sort\", () => {\n it(\"should show only the first batch, in the new direction\", async () => {\n await initAndWait();\n\n presenter.list.actions.sort.set(\"savedOn\", \"ASC\");\n await waitForQuery(\"savedOn\", \"ASC\");\n\n expect(rowIds()).toEqual(range(0, 49));\n });\n\n it(\"should show only the first batch, sorted by the new field\", async () => {\n await initAndWait();\n\n presenter.list.actions.sort.set(\"createdOn\", \"DESC\");\n await waitForQuery(\"createdOn\", \"DESC\");\n\n // `createdOn` runs opposite to `savedOn`, so the newest created page is page-0.\n expect(rowIds()).toEqual(range(0, 49));\n });\n\n it(\"should paginate in the new order\", async () => {\n await initAndWait();\n\n presenter.list.actions.sort.set(\"savedOn\", \"ASC\");\n await waitForQuery(\"savedOn\", \"ASC\");\n await presenter.list.actions.loadMore();\n\n expect(rowIds()).toEqual(range(0, 99));\n });\n\n it(\"should restore the original order when switching back\", async () => {\n await initAndWait();\n await presenter.list.actions.loadMore();\n\n presenter.list.actions.sort.set(\"savedOn\", \"ASC\");\n await waitForQuery(\"savedOn\", \"ASC\");\n\n presenter.list.actions.sort.set(\"savedOn\", \"DESC\");\n await waitForQuery(\"savedOn\", \"DESC\");\n\n expect(rowIds()).toEqual(range(299, 250));\n });\n });\n\n describe(\"local cache changes\", () => {\n const newPage = () =>\n Page.create({\n id: \"page-new#0001\",\n entryId: \"page-new\",\n location: { folderId: \"root\" },\n properties: { title: \"New page\" },\n createdOn: minutes(TOTAL_PAGES + 1),\n savedOn: minutes(TOTAL_PAGES + 1)\n });\n\n it(\"should show a created page at the top when it sorts first\", async () => {\n await initAndWait();\n\n cache.addItems([newPage()]);\n\n expect(rowIds()).toEqual([\"page-new\", ...range(299, 250)]);\n });\n\n it(\"should not show a created page that sorts past the loaded pages\", async () => {\n await initAndWait();\n presenter.list.actions.sort.set(\"savedOn\", \"ASC\");\n await waitForQuery(\"savedOn\", \"ASC\");\n\n cache.addItems([newPage()]);\n\n // The new page is the most recently saved, so in ASC order it belongs at the very\n // end, after 250 pages that are not loaded yet.\n expect(rowIds()).toEqual(range(0, 49));\n });\n\n it(\"should show a created page in its sort position once everything is loaded\", async () => {\n await initAndWait();\n presenter.list.actions.sort.set(\"savedOn\", \"ASC\");\n await waitForQuery(\"savedOn\", \"ASC\");\n await loadAll();\n\n cache.addItems([newPage()]);\n\n expect(rowIds()).toEqual([...range(0, 299), \"page-new\"]);\n });\n\n it(\"should move an updated page to its new sort position\", async () => {\n await initAndWait();\n\n const page = cache.getItem(p => p.entryId === \"page-260\")!;\n cache.addItems([Page.create({ ...page, savedOn: minutes(TOTAL_PAGES + 1) })]);\n\n expect(rowIds()).toEqual([\"page-260\", ...range(299, 261), ...range(259, 250)]);\n });\n });\n\n describe(\"publishing\", () => {\n it(\"should keep a published page in its position\", async () => {\n await initAndWait();\n const before = rowIds();\n\n await presenter.publishPage(\"page-260#0001\");\n\n expect(rowIds()).toEqual(before);\n const published = presenter.list.vm.rows.find(row => row.entryId === \"page-260\");\n expect(published?.status).toBe(\"published\");\n });\n\n it(\"should keep a published page in its position after loading more pages\", async () => {\n await initAndWait();\n await presenter.list.actions.loadMore();\n const before = rowIds();\n\n await presenter.publishPage(\"page-210#0001\");\n\n expect(rowIds()).toEqual(before);\n });\n });\n});\n"],"names":["TOTAL_PAGES","PAGE_SIZE","BASE_TIME","Date","minutes","n","createPages","count","Array","_","i","Page","createGateway","pages","vi","params","sort","field","direction","multiplier","folderId","sorted","page","a","b","String","offset","Number","limit","data","next","hasMoreItems","createFolderTreePresenter","vm","observable","undefined","noopUseCase","range","from","to","step","ids","describe","presenter","cache","publishGateway","id","p","rowIds","row","waitForQuery","expect","initAndWait","loadAll","beforeEach","container","Container","ListCache","PageListCache","ListPagesGateway","ListPagesRepository","ListPagesUseCase","ListPresenter","FolderTreePresenter","Confirmation","GetDescendantFoldersUseCase","DeletePageUseCase","FullPageCache","PublishPageGateway","PublishPageRepository","PublishPageUseCaseImpl","UnpublishPageUseCase","MovePageUseCase","DuplicatePageUseCase","PageListPresenter","Abstraction","it","newPage","before","published"],"mappings":"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"}
@@ -7,7 +7,7 @@ class RedirectListDataSource extends FolderAwareDataSource {
7
7
  }), this.listRedirectsUseCase = listRedirectsUseCase, this.cache = cache;
8
8
  }
9
9
  get rows() {
10
- return this.queryMatcher.filter(this.cache.getItems());
10
+ return this.queryMatcher.select(this.cache.getItems());
11
11
  }
12
12
  async fetch(params) {
13
13
  return this.listRedirectsUseCase.execute({
@@ -1 +1 @@
1
- {"version":3,"file":"presentation/redirects/RedirectList/RedirectListDataSource.js","sources":["../../../../src/presentation/redirects/RedirectList/RedirectListDataSource.ts"],"sourcesContent":["import { FolderAwareDataSource } from \"@webiny/app-admin/presentation/listPresenter/FolderAwareDataSource.js\";\nimport type {\n FetchParams,\n FetchResult\n} from \"@webiny/app-admin/presentation/listPresenter/FolderAwareDataSource.js\";\nimport type { IListRedirectsUseCase } from \"~/features/redirects/listRedirects/abstractions.js\";\nimport type { IGetDescendantFoldersUseCase } from \"@webiny/app-aco/features/folders/getDescendantFolders/abstractions.js\";\nimport type { IListCache } from \"@webiny/app-admin/features/listCache/index.js\";\nimport type { Redirect } from \"~/domain/Redirect/Redirect.js\";\n\nexport class RedirectListDataSource extends FolderAwareDataSource<Redirect> {\n constructor(\n private listRedirectsUseCase: IListRedirectsUseCase,\n private cache: IListCache<Redirect>,\n getDescendantFoldersUseCase?: IGetDescendantFoldersUseCase\n ) {\n super({\n keyField: \"id\",\n getDescendantFolders: getDescendantFoldersUseCase\n });\n }\n\n get rows(): Redirect[] {\n return this.queryMatcher.filter(this.cache.getItems());\n }\n\n async fetch(params: FetchParams): Promise<FetchResult<Redirect>> {\n return this.listRedirectsUseCase.execute({\n search: params.search,\n where: params.where,\n sort: params.sort,\n limit: params.limit,\n after: params.after\n });\n }\n}\n"],"names":["RedirectListDataSource","FolderAwareDataSource","listRedirectsUseCase","cache","getDescendantFoldersUseCase","params"],"mappings":";AAUO,MAAMA,+BAA+BC;IACxC,YACYC,oBAA2C,EAC3CC,KAA2B,EACnCC,2BAA0D,CAC5D;QACE,KAAK,CAAC;YACF,UAAU;YACV,sBAAsBA;QAC1B,SAPQF,oBAAoB,GAApBA,sBAAAA,IAAAA,CACAC,KAAK,GAALA;IAOZ;IAEA,IAAI,OAAmB;QACnB,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ;IACvD;IAEA,MAAM,MAAME,MAAmB,EAAkC;QAC7D,OAAO,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC;YACrC,QAAQA,OAAO,MAAM;YACrB,OAAOA,OAAO,KAAK;YACnB,MAAMA,OAAO,IAAI;YACjB,OAAOA,OAAO,KAAK;YACnB,OAAOA,OAAO,KAAK;QACvB;IACJ;AACJ"}
1
+ {"version":3,"file":"presentation/redirects/RedirectList/RedirectListDataSource.js","sources":["../../../../src/presentation/redirects/RedirectList/RedirectListDataSource.ts"],"sourcesContent":["import { FolderAwareDataSource } from \"@webiny/app-admin/presentation/listPresenter/FolderAwareDataSource.js\";\nimport type {\n FetchParams,\n FetchResult\n} from \"@webiny/app-admin/presentation/listPresenter/FolderAwareDataSource.js\";\nimport type { IListRedirectsUseCase } from \"~/features/redirects/listRedirects/abstractions.js\";\nimport type { IGetDescendantFoldersUseCase } from \"@webiny/app-aco/features/folders/getDescendantFolders/abstractions.js\";\nimport type { IListCache } from \"@webiny/app-admin/features/listCache/index.js\";\nimport type { Redirect } from \"~/domain/Redirect/Redirect.js\";\n\nexport class RedirectListDataSource extends FolderAwareDataSource<Redirect> {\n constructor(\n private listRedirectsUseCase: IListRedirectsUseCase,\n private cache: IListCache<Redirect>,\n getDescendantFoldersUseCase?: IGetDescendantFoldersUseCase\n ) {\n super({\n keyField: \"id\",\n getDescendantFolders: getDescendantFoldersUseCase\n });\n }\n\n get rows(): Redirect[] {\n return this.queryMatcher.select(this.cache.getItems());\n }\n\n async fetch(params: FetchParams): Promise<FetchResult<Redirect>> {\n return this.listRedirectsUseCase.execute({\n search: params.search,\n where: params.where,\n sort: params.sort,\n limit: params.limit,\n after: params.after\n });\n }\n}\n"],"names":["RedirectListDataSource","FolderAwareDataSource","listRedirectsUseCase","cache","getDescendantFoldersUseCase","params"],"mappings":";AAUO,MAAMA,+BAA+BC;IACxC,YACYC,oBAA2C,EAC3CC,KAA2B,EACnCC,2BAA0D,CAC5D;QACE,KAAK,CAAC;YACF,UAAU;YACV,sBAAsBA;QAC1B,SAPQF,oBAAoB,GAApBA,sBAAAA,IAAAA,CACAC,KAAK,GAALA;IAOZ;IAEA,IAAI,OAAmB;QACnB,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ;IACvD;IAEA,MAAM,MAAME,MAAmB,EAAkC;QAC7D,OAAO,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC;YACrC,QAAQA,OAAO,MAAM;YACrB,OAAOA,OAAO,KAAK;YACnB,MAAMA,OAAO,IAAI;YACjB,OAAOA,OAAO,KAAK;YACnB,OAAOA,OAAO,KAAK;QACvB;IACJ;AACJ"}
@@ -0,0 +1,273 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { observable } from "mobx";
3
+ import { Container } from "@webiny/di";
4
+ import { ListCache } from "@webiny/app-admin/features/listCache/index.js";
5
+ import { ListPresenter } from "@webiny/app-admin/presentation/listPresenter/ListPresenter.js";
6
+ import { FolderTreePresenter } from "@webiny/app-aco/presentation/folderTree/abstractions.js";
7
+ import { GetDescendantFoldersUseCase } from "@webiny/app-aco/features/folders/getDescendantFolders/abstractions.js";
8
+ import { Redirect } from "../../../domain/Redirect/Redirect.js";
9
+ import { RedirectsListCache } from "../../../features/redirects/shared/abstractions.js";
10
+ import { ListRedirectsGateway } from "../../../features/redirects/listRedirects/abstractions.js";
11
+ import { ListRedirectsUseCase } from "../../../features/redirects/listRedirects/ListRedirectsUseCase.js";
12
+ import { ListRedirectsRepository } from "../../../features/redirects/listRedirects/ListRedirectsRepository.js";
13
+ import { CreateRedirectGateway, CreateRedirectUseCase } from "../../../features/redirects/createRedirect/abstractions.js";
14
+ import { CreateRedirectUseCase as CreateRedirectUseCase_js_CreateRedirectUseCase } from "../../../features/redirects/createRedirect/CreateRedirectUseCase.js";
15
+ import { CreateRedirectRepository } from "../../../features/redirects/createRedirect/CreateRedirectRepository.js";
16
+ import { UpdateRedirectGateway, UpdateRedirectUseCase } from "../../../features/redirects/updateRedirect/abstractions.js";
17
+ import { UpdateRedirectUseCase as UpdateRedirectUseCase_js_UpdateRedirectUseCase } from "../../../features/redirects/updateRedirect/UpdateRedirectUseCase.js";
18
+ import { UpdateRedirectRepository } from "../../../features/redirects/updateRedirect/UpdateRedirectRepository.js";
19
+ import { CreateRedirectPresenter, EditRedirectPresenter, RedirectListPresenter } from "./abstractions.js";
20
+ import { RedirectListPresenter as external_RedirectListPresenter_js_RedirectListPresenter } from "./RedirectListPresenter.js";
21
+ const TOTAL_REDIRECTS = 120;
22
+ const PAGE_SIZE = 50;
23
+ const BASE_TIME = Date.UTC(2026, 0, 1);
24
+ const minutes = (n)=>new Date(BASE_TIME + 60000 * n).toISOString();
25
+ function createRedirects(count) {
26
+ return Array.from({
27
+ length: count
28
+ }, (_, i)=>Redirect.create({
29
+ id: `redirect-${i}`,
30
+ location: {
31
+ folderId: "root"
32
+ },
33
+ redirectFrom: `/from-${i}`,
34
+ redirectTo: `/to-${i}`,
35
+ redirectType: "permanent",
36
+ isEnabled: i % 2 === 0,
37
+ createdOn: minutes(i),
38
+ savedOn: minutes(count - i)
39
+ }));
40
+ }
41
+ function sortRedirects(redirects, sort = "createdOn_DESC") {
42
+ const [field, direction] = sort.split("_");
43
+ const multiplier = "ASC" === direction ? 1 : -1;
44
+ return [
45
+ ...redirects
46
+ ].sort((a, b)=>{
47
+ const left = String(a[field]);
48
+ const right = String(b[field]);
49
+ return left === right ? 0 : multiplier * (left < right ? -1 : 1);
50
+ });
51
+ }
52
+ function createListGateway(redirects) {
53
+ return {
54
+ execute: vi.fn(async (params)=>{
55
+ const sorted = sortRedirects(redirects, params.sort?.[0]);
56
+ const offset = params.after ? Number(params.after) : 0;
57
+ const limit = params.limit ?? PAGE_SIZE;
58
+ const data = sorted.slice(offset, offset + limit);
59
+ const next = offset + data.length;
60
+ const hasMoreItems = next < sorted.length;
61
+ return {
62
+ data,
63
+ meta: {
64
+ cursor: hasMoreItems ? String(next) : null,
65
+ hasMoreItems,
66
+ totalCount: sorted.length
67
+ }
68
+ };
69
+ })
70
+ };
71
+ }
72
+ function createFolderTreePresenter() {
73
+ const vm = observable({
74
+ folders: [],
75
+ tree: [],
76
+ currentFolderId: null,
77
+ currentFolder: null,
78
+ loading: false,
79
+ operation: {
80
+ active: false,
81
+ mode: null
82
+ },
83
+ isRootFolder: true,
84
+ currentFolderTitle: "All redirects",
85
+ childFolders: [],
86
+ loadingNodeIds: []
87
+ });
88
+ return {
89
+ get vm () {
90
+ return vm;
91
+ },
92
+ selectFolder: vi.fn(),
93
+ createFolder: vi.fn(),
94
+ editFolder: vi.fn(),
95
+ deleteFolder: vi.fn().mockResolvedValue(void 0),
96
+ moveFolder: vi.fn().mockResolvedValue(void 0),
97
+ loadChildFolders: vi.fn().mockResolvedValue(void 0),
98
+ canManageStructure: vi.fn().mockReturnValue(true),
99
+ getAncestorIds: vi.fn().mockReturnValue([]),
100
+ submitOperation: vi.fn().mockResolvedValue(true),
101
+ cancelOperation: vi.fn(),
102
+ onFolderChange: vi.fn().mockReturnValue(()=>{})
103
+ };
104
+ }
105
+ function range(from, to) {
106
+ const step = from <= to ? 1 : -1;
107
+ const ids = [];
108
+ for(let i = from; step > 0 ? i <= to : i >= to; i += step)ids.push(`redirect-${i}`);
109
+ return ids;
110
+ }
111
+ describe("RedirectListPresenter", ()=>{
112
+ let presenter;
113
+ let container;
114
+ let redirects;
115
+ const rowIds = ()=>presenter.vm.list.rows.map((row)=>row.id);
116
+ const waitForQuery = async (field, direction)=>{
117
+ await vi.waitFor(()=>{
118
+ expect(presenter.vm.list.appliedQuery?.sort).toEqual({
119
+ field,
120
+ direction
121
+ });
122
+ expect(presenter.vm.list.pagination.loading).toBe(false);
123
+ });
124
+ };
125
+ const initAndWait = async ()=>{
126
+ presenter.init();
127
+ await waitForQuery("createdOn", "DESC");
128
+ };
129
+ const sortAndWait = async (field, direction)=>{
130
+ presenter.actions.sort.set(field, direction);
131
+ await waitForQuery(field, direction);
132
+ };
133
+ const loadAll = async ()=>{
134
+ while(presenter.vm.list.pagination.hasMore)await presenter.actions.loadMore();
135
+ };
136
+ beforeEach(()=>{
137
+ container = new Container();
138
+ redirects = createRedirects(TOTAL_REDIRECTS);
139
+ container.registerInstance(RedirectsListCache, new ListCache("id"));
140
+ container.registerInstance(ListRedirectsGateway, createListGateway(redirects));
141
+ container.register(ListRedirectsRepository).inSingletonScope();
142
+ container.register(ListRedirectsUseCase);
143
+ container.registerInstance(CreateRedirectGateway, {
144
+ execute: vi.fn(async (params)=>Redirect.create({
145
+ ...params,
146
+ id: "redirect-new",
147
+ createdOn: minutes(TOTAL_REDIRECTS + 1),
148
+ savedOn: minutes(TOTAL_REDIRECTS + 1)
149
+ }))
150
+ });
151
+ container.register(CreateRedirectRepository).inSingletonScope();
152
+ container.register(CreateRedirectUseCase_js_CreateRedirectUseCase);
153
+ container.registerInstance(UpdateRedirectGateway, {
154
+ execute: vi.fn(async (params)=>{
155
+ const existing = redirects.find((r)=>r.id === params.id);
156
+ return Redirect.create({
157
+ ...existing,
158
+ ...params,
159
+ savedOn: minutes(TOTAL_REDIRECTS + 1)
160
+ });
161
+ })
162
+ });
163
+ container.register(UpdateRedirectRepository).inSingletonScope();
164
+ container.register(UpdateRedirectUseCase_js_UpdateRedirectUseCase);
165
+ container.register(ListPresenter);
166
+ container.registerInstance(FolderTreePresenter, createFolderTreePresenter());
167
+ container.registerInstance(GetDescendantFoldersUseCase, {
168
+ execute: ()=>[]
169
+ });
170
+ container.registerInstance(CreateRedirectPresenter, {
171
+ vm: {},
172
+ init: vi.fn(),
173
+ save: vi.fn()
174
+ });
175
+ container.registerInstance(EditRedirectPresenter, {
176
+ vm: {},
177
+ loadRedirect: vi.fn(),
178
+ save: vi.fn()
179
+ });
180
+ container.register(external_RedirectListPresenter_js_RedirectListPresenter);
181
+ presenter = container.resolve(RedirectListPresenter);
182
+ });
183
+ describe("pagination", ()=>{
184
+ it("should show the most recently created redirects first", async ()=>{
185
+ await initAndWait();
186
+ expect(rowIds()).toEqual(range(119, 70));
187
+ });
188
+ it("should append the next batch of redirects to the bottom of the list", async ()=>{
189
+ await initAndWait();
190
+ await presenter.actions.loadMore();
191
+ expect(rowIds()).toEqual(range(119, 20));
192
+ });
193
+ it("should keep all redirects in order after loading everything", async ()=>{
194
+ await initAndWait();
195
+ await loadAll();
196
+ expect(rowIds()).toEqual(range(119, 0));
197
+ });
198
+ });
199
+ describe("changing the sort", ()=>{
200
+ it("should show only the first batch, sorted by the new field", async ()=>{
201
+ await initAndWait();
202
+ await sortAndWait("savedOn", "DESC");
203
+ expect(rowIds()).toEqual(range(0, 49));
204
+ });
205
+ it("should show only the first batch when sorting by a boolean field", async ()=>{
206
+ await initAndWait();
207
+ await sortAndWait("isEnabled", "DESC");
208
+ const expected = sortRedirects(redirects, "isEnabled_DESC").slice(0, PAGE_SIZE).map((r)=>r.id);
209
+ expect(rowIds()).toEqual(expected);
210
+ });
211
+ it("should restore the original order when switching back", async ()=>{
212
+ await initAndWait();
213
+ await presenter.actions.loadMore();
214
+ await sortAndWait("createdOn", "ASC");
215
+ await sortAndWait("createdOn", "DESC");
216
+ expect(rowIds()).toEqual(range(119, 70));
217
+ });
218
+ });
219
+ describe("local cache changes", ()=>{
220
+ const createRedirect = ()=>container.resolve(CreateRedirectUseCase).execute({
221
+ location: {
222
+ folderId: "root"
223
+ },
224
+ redirectFrom: "/new",
225
+ redirectTo: "/target",
226
+ redirectType: "permanent",
227
+ isEnabled: true
228
+ });
229
+ const updateRedirect = (id)=>{
230
+ const existing = redirects.find((r)=>r.id === id);
231
+ return container.resolve(UpdateRedirectUseCase).execute({
232
+ id,
233
+ redirectFrom: existing.redirectFrom,
234
+ redirectTo: "/changed",
235
+ redirectType: existing.redirectType,
236
+ isEnabled: existing.isEnabled
237
+ });
238
+ };
239
+ it("should show a created redirect at the top when it sorts first", async ()=>{
240
+ await initAndWait();
241
+ await createRedirect();
242
+ expect(rowIds()).toEqual([
243
+ "redirect-new",
244
+ ...range(119, 70)
245
+ ]);
246
+ });
247
+ it("should not show a created redirect that sorts past the loaded redirects", async ()=>{
248
+ await initAndWait();
249
+ await sortAndWait("createdOn", "ASC");
250
+ await createRedirect();
251
+ expect(rowIds()).toEqual(range(0, 49));
252
+ });
253
+ it("should keep an updated redirect in its position when sorted by createdOn", async ()=>{
254
+ await initAndWait();
255
+ await updateRedirect("redirect-100");
256
+ expect(rowIds()).toEqual(range(119, 70));
257
+ const updated = presenter.vm.list.rows.find((row)=>"redirect-100" === row.id);
258
+ expect(updated?.redirectTo).toBe("/changed");
259
+ });
260
+ it("should move an updated redirect to the top when sorted by savedOn", async ()=>{
261
+ await initAndWait();
262
+ await sortAndWait("savedOn", "DESC");
263
+ await updateRedirect("redirect-30");
264
+ expect(rowIds()).toEqual([
265
+ "redirect-30",
266
+ ...range(0, 29),
267
+ ...range(31, 49)
268
+ ]);
269
+ });
270
+ });
271
+ });
272
+
273
+ //# sourceMappingURL=RedirectListPresenter.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presentation/redirects/RedirectList/RedirectListPresenter.test.js","sources":["../../../../src/presentation/redirects/RedirectList/RedirectListPresenter.test.ts"],"sourcesContent":["import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { observable } from \"mobx\";\nimport { Container } from \"@webiny/di\";\nimport { ListCache } from \"@webiny/app-admin/features/listCache/index.js\";\nimport { ListPresenter } from \"@webiny/app-admin/presentation/listPresenter/ListPresenter.js\";\nimport { FolderTreePresenter } from \"@webiny/app-aco/presentation/folderTree/abstractions.js\";\nimport { GetDescendantFoldersUseCase } from \"@webiny/app-aco/features/folders/getDescendantFolders/abstractions.js\";\nimport { Redirect } from \"~/domain/Redirect/Redirect.js\";\nimport { RedirectsListCache } from \"~/features/redirects/shared/abstractions.js\";\nimport {\n ListRedirectsGateway,\n type ListRedirectsGatewayParams,\n type ListRedirectsGatewayResult\n} from \"~/features/redirects/listRedirects/abstractions.js\";\nimport { ListRedirectsUseCase } from \"~/features/redirects/listRedirects/ListRedirectsUseCase.js\";\nimport { ListRedirectsRepository } from \"~/features/redirects/listRedirects/ListRedirectsRepository.js\";\nimport {\n CreateRedirectGateway,\n CreateRedirectUseCase as CreateRedirectUseCaseAbstraction\n} from \"~/features/redirects/createRedirect/abstractions.js\";\nimport { CreateRedirectUseCase } from \"~/features/redirects/createRedirect/CreateRedirectUseCase.js\";\nimport { CreateRedirectRepository } from \"~/features/redirects/createRedirect/CreateRedirectRepository.js\";\nimport {\n UpdateRedirectGateway,\n UpdateRedirectUseCase as UpdateRedirectUseCaseAbstraction,\n type UpdateRedirectParams\n} from \"~/features/redirects/updateRedirect/abstractions.js\";\nimport { UpdateRedirectUseCase } from \"~/features/redirects/updateRedirect/UpdateRedirectUseCase.js\";\nimport { UpdateRedirectRepository } from \"~/features/redirects/updateRedirect/UpdateRedirectRepository.js\";\nimport {\n CreateRedirectPresenter,\n EditRedirectPresenter,\n RedirectListPresenter as Abstraction\n} from \"./abstractions.js\";\nimport { RedirectListPresenter } from \"./RedirectListPresenter.js\";\n\nconst TOTAL_REDIRECTS = 120;\nconst PAGE_SIZE = 50;\nconst BASE_TIME = Date.UTC(2026, 0, 1);\n\nconst minutes = (n: number) => new Date(BASE_TIME + n * 60_000).toISOString();\n\n/**\n * A higher index means a more recently *created* redirect, but a less recently *saved* one,\n * so sorting by `createdOn` and by `savedOn` produce opposite orders.\n */\nfunction createRedirects(count: number): Redirect[] {\n return Array.from({ length: count }, (_, i) =>\n Redirect.create({\n id: `redirect-${i}`,\n location: { folderId: \"root\" },\n redirectFrom: `/from-${i}`,\n redirectTo: `/to-${i}`,\n redirectType: \"permanent\",\n isEnabled: i % 2 === 0,\n createdOn: minutes(i),\n savedOn: minutes(count - i)\n })\n );\n}\n\n/**\n * Sorts like the API: by the requested field, ties keep the original (id) order.\n */\nfunction sortRedirects(redirects: Redirect[], sort = \"createdOn_DESC\"): Redirect[] {\n const [field, direction] = sort.split(\"_\") as [keyof Redirect, \"ASC\" | \"DESC\"];\n const multiplier = direction === \"ASC\" ? 1 : -1;\n return [...redirects].sort((a, b) => {\n const left = String(a[field]);\n const right = String(b[field]);\n return left === right ? 0 : multiplier * (left < right ? -1 : 1);\n });\n}\n\nfunction createListGateway(redirects: Redirect[]): ListRedirectsGateway.Interface {\n return {\n execute: vi.fn(\n async (params: ListRedirectsGatewayParams): Promise<ListRedirectsGatewayResult> => {\n const sorted = sortRedirects(redirects, params.sort?.[0]);\n const offset = params.after ? Number(params.after) : 0;\n const limit = params.limit ?? PAGE_SIZE;\n const data = sorted.slice(offset, offset + limit);\n const next = offset + data.length;\n const hasMoreItems = next < sorted.length;\n return {\n data,\n meta: {\n cursor: hasMoreItems ? String(next) : null,\n hasMoreItems,\n totalCount: sorted.length\n }\n };\n }\n )\n };\n}\n\nfunction createFolderTreePresenter(): FolderTreePresenter.Interface {\n const vm = observable({\n folders: [],\n tree: [],\n currentFolderId: null as string | null,\n currentFolder: null,\n loading: false,\n operation: { active: false, mode: null },\n isRootFolder: true,\n currentFolderTitle: \"All redirects\",\n childFolders: [],\n loadingNodeIds: []\n });\n\n return {\n get vm() {\n return vm as unknown as FolderTreePresenter.Interface[\"vm\"];\n },\n selectFolder: vi.fn(),\n createFolder: vi.fn(),\n editFolder: vi.fn(),\n deleteFolder: vi.fn().mockResolvedValue(undefined),\n moveFolder: vi.fn().mockResolvedValue(undefined),\n loadChildFolders: vi.fn().mockResolvedValue(undefined),\n canManageStructure: vi.fn().mockReturnValue(true),\n getAncestorIds: vi.fn().mockReturnValue([]),\n submitOperation: vi.fn().mockResolvedValue(true),\n cancelOperation: vi.fn(),\n onFolderChange: vi.fn().mockReturnValue(() => {})\n };\n}\n\n/**\n * Expected redirect IDs for a contiguous range of indexes, in the given order.\n */\nfunction range(from: number, to: number): string[] {\n const step = from <= to ? 1 : -1;\n const ids: string[] = [];\n for (let i = from; step > 0 ? i <= to : i >= to; i += step) {\n ids.push(`redirect-${i}`);\n }\n return ids;\n}\n\ndescribe(\"RedirectListPresenter\", () => {\n let presenter: Abstraction.Interface;\n let container: Container;\n let redirects: Redirect[];\n\n const rowIds = () => presenter.vm.list.rows.map(row => row.id);\n\n const waitForQuery = async (field: string, direction: \"ASC\" | \"DESC\") => {\n await vi.waitFor(() => {\n expect(presenter.vm.list.appliedQuery?.sort).toEqual({ field, direction });\n expect(presenter.vm.list.pagination.loading).toBe(false);\n });\n };\n\n const initAndWait = async () => {\n presenter.init();\n await waitForQuery(\"createdOn\", \"DESC\");\n };\n\n const sortAndWait = async (field: string, direction: \"ASC\" | \"DESC\") => {\n presenter.actions.sort.set(field, direction);\n await waitForQuery(field, direction);\n };\n\n const loadAll = async () => {\n while (presenter.vm.list.pagination.hasMore) {\n await presenter.actions.loadMore();\n }\n };\n\n beforeEach(() => {\n container = new Container();\n redirects = createRedirects(TOTAL_REDIRECTS);\n\n container.registerInstance(RedirectsListCache, new ListCache<Redirect>(\"id\"));\n\n container.registerInstance(ListRedirectsGateway, createListGateway(redirects));\n container.register(ListRedirectsRepository).inSingletonScope();\n container.register(ListRedirectsUseCase);\n\n container.registerInstance(CreateRedirectGateway, {\n execute: vi.fn(async params =>\n Redirect.create({\n ...params,\n id: \"redirect-new\",\n createdOn: minutes(TOTAL_REDIRECTS + 1),\n savedOn: minutes(TOTAL_REDIRECTS + 1)\n })\n )\n });\n container.register(CreateRedirectRepository).inSingletonScope();\n container.register(CreateRedirectUseCase);\n\n container.registerInstance(UpdateRedirectGateway, {\n execute: vi.fn(async (params: UpdateRedirectParams) => {\n const existing = redirects.find(r => r.id === params.id)!;\n return Redirect.create({\n ...existing,\n ...params,\n savedOn: minutes(TOTAL_REDIRECTS + 1)\n });\n })\n });\n container.register(UpdateRedirectRepository).inSingletonScope();\n container.register(UpdateRedirectUseCase);\n\n container.register(ListPresenter);\n container.registerInstance(FolderTreePresenter, createFolderTreePresenter());\n container.registerInstance(GetDescendantFoldersUseCase, { execute: () => [] });\n container.registerInstance(CreateRedirectPresenter, {\n vm: {},\n init: vi.fn(),\n save: vi.fn()\n } as unknown as CreateRedirectPresenter.Interface);\n container.registerInstance(EditRedirectPresenter, {\n vm: {},\n loadRedirect: vi.fn(),\n save: vi.fn()\n } as unknown as EditRedirectPresenter.Interface);\n\n container.register(RedirectListPresenter);\n\n presenter = container.resolve(Abstraction);\n });\n\n describe(\"pagination\", () => {\n it(\"should show the most recently created redirects first\", async () => {\n await initAndWait();\n\n expect(rowIds()).toEqual(range(119, 70));\n });\n\n it(\"should append the next batch of redirects to the bottom of the list\", async () => {\n await initAndWait();\n\n await presenter.actions.loadMore();\n\n expect(rowIds()).toEqual(range(119, 20));\n });\n\n it(\"should keep all redirects in order after loading everything\", async () => {\n await initAndWait();\n\n await loadAll();\n\n expect(rowIds()).toEqual(range(119, 0));\n });\n });\n\n describe(\"changing the sort\", () => {\n it(\"should show only the first batch, sorted by the new field\", async () => {\n await initAndWait();\n\n await sortAndWait(\"savedOn\", \"DESC\");\n\n // `savedOn` runs opposite to `createdOn`, so the most recently saved is redirect-0.\n expect(rowIds()).toEqual(range(0, 49));\n });\n\n it(\"should show only the first batch when sorting by a boolean field\", async () => {\n await initAndWait();\n\n await sortAndWait(\"isEnabled\", \"DESC\");\n\n const expected = sortRedirects(redirects, \"isEnabled_DESC\")\n .slice(0, PAGE_SIZE)\n .map(r => r.id);\n expect(rowIds()).toEqual(expected);\n });\n\n it(\"should restore the original order when switching back\", async () => {\n await initAndWait();\n await presenter.actions.loadMore();\n\n await sortAndWait(\"createdOn\", \"ASC\");\n await sortAndWait(\"createdOn\", \"DESC\");\n\n expect(rowIds()).toEqual(range(119, 70));\n });\n });\n\n describe(\"local cache changes\", () => {\n const createRedirect = () =>\n container.resolve(CreateRedirectUseCaseAbstraction).execute({\n location: { folderId: \"root\" },\n redirectFrom: \"/new\",\n redirectTo: \"/target\",\n redirectType: \"permanent\",\n isEnabled: true\n });\n\n const updateRedirect = (id: string) => {\n const existing = redirects.find(r => r.id === id)!;\n return container.resolve(UpdateRedirectUseCaseAbstraction).execute({\n id,\n redirectFrom: existing.redirectFrom,\n redirectTo: \"/changed\",\n redirectType: existing.redirectType,\n isEnabled: existing.isEnabled\n });\n };\n\n it(\"should show a created redirect at the top when it sorts first\", async () => {\n await initAndWait();\n\n await createRedirect();\n\n expect(rowIds()).toEqual([\"redirect-new\", ...range(119, 70)]);\n });\n\n it(\"should not show a created redirect that sorts past the loaded redirects\", async () => {\n await initAndWait();\n await sortAndWait(\"createdOn\", \"ASC\");\n\n await createRedirect();\n\n // The new redirect is the most recently created, so in ASC order it belongs at the\n // very end, after redirects that are not loaded yet.\n expect(rowIds()).toEqual(range(0, 49));\n });\n\n it(\"should keep an updated redirect in its position when sorted by createdOn\", async () => {\n await initAndWait();\n\n await updateRedirect(\"redirect-100\");\n\n expect(rowIds()).toEqual(range(119, 70));\n const updated = presenter.vm.list.rows.find(row => row.id === \"redirect-100\");\n expect(updated?.redirectTo).toBe(\"/changed\");\n });\n\n it(\"should move an updated redirect to the top when sorted by savedOn\", async () => {\n await initAndWait();\n await sortAndWait(\"savedOn\", \"DESC\");\n\n await updateRedirect(\"redirect-30\");\n\n expect(rowIds()).toEqual([\"redirect-30\", ...range(0, 29), ...range(31, 49)]);\n });\n });\n});\n"],"names":["TOTAL_REDIRECTS","PAGE_SIZE","BASE_TIME","Date","minutes","n","createRedirects","count","Array","_","i","Redirect","sortRedirects","redirects","sort","field","direction","multiplier","a","b","left","String","right","createListGateway","vi","params","sorted","offset","Number","limit","data","next","hasMoreItems","createFolderTreePresenter","vm","observable","undefined","range","from","to","step","ids","describe","presenter","container","rowIds","row","waitForQuery","expect","initAndWait","sortAndWait","loadAll","beforeEach","Container","RedirectsListCache","ListCache","ListRedirectsGateway","ListRedirectsRepository","ListRedirectsUseCase","CreateRedirectGateway","CreateRedirectRepository","CreateRedirectUseCase","UpdateRedirectGateway","existing","r","UpdateRedirectRepository","UpdateRedirectUseCase","ListPresenter","FolderTreePresenter","GetDescendantFoldersUseCase","CreateRedirectPresenter","EditRedirectPresenter","RedirectListPresenter","Abstraction","it","expected","createRedirect","CreateRedirectUseCaseAbstraction","updateRedirect","id","UpdateRedirectUseCaseAbstraction","updated"],"mappings":"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"}
@@ -6,7 +6,7 @@ const fileManagerItemToValue = (file)=>{
6
6
  src,
7
7
  url: src,
8
8
  name: file.name,
9
- type: file.type,
9
+ mimeType: file.type,
10
10
  size: file.size
11
11
  };
12
12
  if ("image" === getAssetCategory(file.type)) {
@@ -19,6 +19,8 @@ const fileManagerItemToValue = (file)=>{
19
19
  alt: meta?.alt ?? void 0,
20
20
  caption: meta?.caption ?? void 0
21
21
  };
22
+ asset.width = asset.image.width;
23
+ asset.height = asset.image.height;
22
24
  }
23
25
  return asset;
24
26
  };
@@ -1 +1 @@
1
- {"version":3,"file":"shared/fileManagerItemToValue.js","sources":["../../src/shared/fileManagerItemToValue.ts"],"sourcesContent":["import type { FileManagerFileItem } from \"@webiny/app-admin\";\nimport { getAssetCategory, type Asset } from \"@webiny/website-builder-sdk\";\n\n/**\n * Build the unified {@link Asset} value from a picked File Manager item.\n * For images, the per-usage `image` edit is seeded from the asset-level default\n * (set in File Manager); from here it lives on the element value and is editable\n * per placement.\n */\nexport const fileManagerItemToValue = (file: FileManagerFileItem): Asset => {\n const src = file.src || \"\";\n const asset: Asset = {\n id: file.id,\n src,\n url: src,\n name: file.name,\n type: file.type,\n size: file.size\n };\n\n if (getAssetCategory(file.type) === \"image\") {\n const meta = file.metadata?.image;\n asset.image = {\n width: meta?.width ?? file.width,\n height: meta?.height ?? file.height,\n crop: meta?.crop ?? undefined,\n focalPoint: meta?.focalPoint ?? undefined,\n alt: meta?.alt ?? undefined,\n caption: meta?.caption ?? undefined\n };\n }\n\n return asset;\n};\n"],"names":["fileManagerItemToValue","file","src","asset","getAssetCategory","meta","undefined"],"mappings":";AASO,MAAMA,yBAAyB,CAACC;IACnC,MAAMC,MAAMD,KAAK,GAAG,IAAI;IACxB,MAAME,QAAe;QACjB,IAAIF,KAAK,EAAE;QACXC;QACA,KAAKA;QACL,MAAMD,KAAK,IAAI;QACf,MAAMA,KAAK,IAAI;QACf,MAAMA,KAAK,IAAI;IACnB;IAEA,IAAIG,AAAgC,YAAhCA,iBAAiBH,KAAK,IAAI,GAAe;QACzC,MAAMI,OAAOJ,KAAK,QAAQ,EAAE;QAC5BE,MAAM,KAAK,GAAG;YACV,OAAOE,MAAM,SAASJ,KAAK,KAAK;YAChC,QAAQI,MAAM,UAAUJ,KAAK,MAAM;YACnC,MAAMI,MAAM,QAAQC;YACpB,YAAYD,MAAM,cAAcC;YAChC,KAAKD,MAAM,OAAOC;YAClB,SAASD,MAAM,WAAWC;QAC9B;IACJ;IAEA,OAAOH;AACX"}
1
+ {"version":3,"file":"shared/fileManagerItemToValue.js","sources":["../../src/shared/fileManagerItemToValue.ts"],"sourcesContent":["import type { FileManagerFileItem } from \"@webiny/app-admin\";\nimport { getAssetCategory, type Asset } from \"@webiny/website-builder-sdk\";\n\n/**\n * Build the unified {@link Asset} value from a picked File Manager item.\n * For images, the per-usage `image` edit is seeded from the asset-level default\n * (set in File Manager); from here it lives on the element value and is editable\n * per placement.\n */\nexport const fileManagerItemToValue = (file: FileManagerFileItem): Asset => {\n const src = file.src || \"\";\n const asset: Asset = {\n id: file.id,\n src,\n url: src,\n name: file.name,\n mimeType: file.type,\n size: file.size\n };\n\n if (getAssetCategory(file.type) === \"image\") {\n const meta = file.metadata?.image;\n asset.image = {\n width: meta?.width ?? file.width,\n height: meta?.height ?? file.height,\n crop: meta?.crop ?? undefined,\n focalPoint: meta?.focalPoint ?? undefined,\n alt: meta?.alt ?? undefined,\n caption: meta?.caption ?? undefined\n };\n // Root dimensions for 6.4 frontends, which read `width`/`height` there.\n asset.width = asset.image.width;\n asset.height = asset.image.height;\n }\n\n return asset;\n};\n"],"names":["fileManagerItemToValue","file","src","asset","getAssetCategory","meta","undefined"],"mappings":";AASO,MAAMA,yBAAyB,CAACC;IACnC,MAAMC,MAAMD,KAAK,GAAG,IAAI;IACxB,MAAME,QAAe;QACjB,IAAIF,KAAK,EAAE;QACXC;QACA,KAAKA;QACL,MAAMD,KAAK,IAAI;QACf,UAAUA,KAAK,IAAI;QACnB,MAAMA,KAAK,IAAI;IACnB;IAEA,IAAIG,AAAgC,YAAhCA,iBAAiBH,KAAK,IAAI,GAAe;QACzC,MAAMI,OAAOJ,KAAK,QAAQ,EAAE;QAC5BE,MAAM,KAAK,GAAG;YACV,OAAOE,MAAM,SAASJ,KAAK,KAAK;YAChC,QAAQI,MAAM,UAAUJ,KAAK,MAAM;YACnC,MAAMI,MAAM,QAAQC;YACpB,YAAYD,MAAM,cAAcC;YAChC,KAAKD,MAAM,OAAOC;YAClB,SAASD,MAAM,WAAWC;QAC9B;QAEAH,MAAM,KAAK,GAAGA,MAAM,KAAK,CAAC,KAAK;QAC/BA,MAAM,MAAM,GAAGA,MAAM,KAAK,CAAC,MAAM;IACrC;IAEA,OAAOA;AACX"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { fileManagerItemToValue } from "./fileManagerItemToValue.js";
3
+ describe("fileManagerItemToValue", ()=>{
4
+ it("writes mimeType and image dimensions at the root and in image", ()=>{
5
+ const value = fileManagerItemToValue({
6
+ id: "file-1",
7
+ src: "https://cdn.test/files/file-1/cat.jpg",
8
+ name: "cat.jpg",
9
+ type: "image/jpeg",
10
+ size: 1234,
11
+ width: 1200,
12
+ height: 800
13
+ });
14
+ expect(value).toEqual({
15
+ id: "file-1",
16
+ src: "https://cdn.test/files/file-1/cat.jpg",
17
+ url: "https://cdn.test/files/file-1/cat.jpg",
18
+ name: "cat.jpg",
19
+ mimeType: "image/jpeg",
20
+ size: 1234,
21
+ width: 1200,
22
+ height: 800,
23
+ image: {
24
+ width: 1200,
25
+ height: 800,
26
+ crop: void 0,
27
+ focalPoint: void 0,
28
+ alt: void 0,
29
+ caption: void 0
30
+ }
31
+ });
32
+ });
33
+ it("prefers the File Manager image metadata for dimensions", ()=>{
34
+ const value = fileManagerItemToValue({
35
+ id: "file-2",
36
+ src: "https://cdn.test/files/file-2/dog.png",
37
+ name: "dog.png",
38
+ type: "image/png",
39
+ size: 10,
40
+ width: 100,
41
+ height: 100,
42
+ metadata: {
43
+ image: {
44
+ width: 640,
45
+ height: 480,
46
+ alt: "A dog"
47
+ }
48
+ }
49
+ });
50
+ expect(value.width).toBe(640);
51
+ expect(value.height).toBe(480);
52
+ expect(value.image).toMatchObject({
53
+ width: 640,
54
+ height: 480,
55
+ alt: "A dog"
56
+ });
57
+ });
58
+ it("leaves dimensions out for non-image files", ()=>{
59
+ const value = fileManagerItemToValue({
60
+ id: "file-3",
61
+ src: "https://cdn.test/files/file-3/doc.pdf",
62
+ name: "doc.pdf",
63
+ type: "application/pdf",
64
+ size: 99
65
+ });
66
+ expect(value.mimeType).toBe("application/pdf");
67
+ expect(value.width).toBeUndefined();
68
+ expect(value.height).toBeUndefined();
69
+ expect(value.image).toBeUndefined();
70
+ });
71
+ });
72
+
73
+ //# sourceMappingURL=fileManagerItemToValue.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared/fileManagerItemToValue.test.js","sources":["../../src/shared/fileManagerItemToValue.test.ts"],"sourcesContent":["import { describe, it, expect } from \"vitest\";\nimport { fileManagerItemToValue } from \"./fileManagerItemToValue.js\";\n\ndescribe(\"fileManagerItemToValue\", () => {\n it(\"writes mimeType and image dimensions at the root and in image\", () => {\n const value = fileManagerItemToValue({\n id: \"file-1\",\n src: \"https://cdn.test/files/file-1/cat.jpg\",\n name: \"cat.jpg\",\n type: \"image/jpeg\",\n size: 1234,\n width: 1200,\n height: 800\n });\n\n expect(value).toEqual({\n id: \"file-1\",\n src: \"https://cdn.test/files/file-1/cat.jpg\",\n url: \"https://cdn.test/files/file-1/cat.jpg\",\n name: \"cat.jpg\",\n mimeType: \"image/jpeg\",\n size: 1234,\n width: 1200,\n height: 800,\n image: {\n width: 1200,\n height: 800,\n crop: undefined,\n focalPoint: undefined,\n alt: undefined,\n caption: undefined\n }\n });\n });\n\n it(\"prefers the File Manager image metadata for dimensions\", () => {\n const value = fileManagerItemToValue({\n id: \"file-2\",\n src: \"https://cdn.test/files/file-2/dog.png\",\n name: \"dog.png\",\n type: \"image/png\",\n size: 10,\n width: 100,\n height: 100,\n metadata: { image: { width: 640, height: 480, alt: \"A dog\" } }\n });\n\n expect(value.width).toBe(640);\n expect(value.height).toBe(480);\n expect(value.image).toMatchObject({ width: 640, height: 480, alt: \"A dog\" });\n });\n\n it(\"leaves dimensions out for non-image files\", () => {\n const value = fileManagerItemToValue({\n id: \"file-3\",\n src: \"https://cdn.test/files/file-3/doc.pdf\",\n name: \"doc.pdf\",\n type: \"application/pdf\",\n size: 99\n });\n\n expect(value.mimeType).toBe(\"application/pdf\");\n expect(value.width).toBeUndefined();\n expect(value.height).toBeUndefined();\n expect(value.image).toBeUndefined();\n });\n});\n"],"names":["describe","it","value","fileManagerItemToValue","expect","undefined"],"mappings":";;AAGAA,SAAS,0BAA0B;IAC/BC,GAAG,iEAAiE;QAChE,MAAMC,QAAQC,uBAAuB;YACjC,IAAI;YACJ,KAAK;YACL,MAAM;YACN,MAAM;YACN,MAAM;YACN,OAAO;YACP,QAAQ;QACZ;QAEAC,OAAOF,OAAO,OAAO,CAAC;YAClB,IAAI;YACJ,KAAK;YACL,KAAK;YACL,MAAM;YACN,UAAU;YACV,MAAM;YACN,OAAO;YACP,QAAQ;YACR,OAAO;gBACH,OAAO;gBACP,QAAQ;gBACR,MAAMG;gBACN,YAAYA;gBACZ,KAAKA;gBACL,SAASA;YACb;QACJ;IACJ;IAEAJ,GAAG,0DAA0D;QACzD,MAAMC,QAAQC,uBAAuB;YACjC,IAAI;YACJ,KAAK;YACL,MAAM;YACN,MAAM;YACN,MAAM;YACN,OAAO;YACP,QAAQ;YACR,UAAU;gBAAE,OAAO;oBAAE,OAAO;oBAAK,QAAQ;oBAAK,KAAK;gBAAQ;YAAE;QACjE;QAEAC,OAAOF,MAAM,KAAK,EAAE,IAAI,CAAC;QACzBE,OAAOF,MAAM,MAAM,EAAE,IAAI,CAAC;QAC1BE,OAAOF,MAAM,KAAK,EAAE,aAAa,CAAC;YAAE,OAAO;YAAK,QAAQ;YAAK,KAAK;QAAQ;IAC9E;IAEAD,GAAG,6CAA6C;QAC5C,MAAMC,QAAQC,uBAAuB;YACjC,IAAI;YACJ,KAAK;YACL,MAAM;YACN,MAAM;YACN,MAAM;QACV;QAEAC,OAAOF,MAAM,QAAQ,EAAE,IAAI,CAAC;QAC5BE,OAAOF,MAAM,KAAK,EAAE,aAAa;QACjCE,OAAOF,MAAM,MAAM,EAAE,aAAa;QAClCE,OAAOF,MAAM,KAAK,EAAE,aAAa;IACrC;AACJ"}