@nan0web/ui-payload 3.3.0 → 3.3.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.
- package/package.json +60 -12
- package/src/{index.ts → index.js} +2 -9
- package/src/templates/{PayloadCollectionTemplate.spec.js → PayloadCollectionTemplate.test.js} +0 -1
- package/types/access.d.ts +24 -0
- package/types/components/BooleanCell.d.ts +19 -0
- package/types/components/ImageCell.d.ts +23 -0
- package/types/components/MapCell.d.ts +19 -0
- package/types/domain/PayloadApp.d.ts +39 -0
- package/types/domain/SeedModel.d.ts +58 -0
- package/types/index.d.ts +6 -0
- package/types/richtext/Nan0ComponentNode.d.ts +29 -0
- package/types/richtext/Nan0ElementNode.d.ts +34 -0
- package/types/richtext/Nan0HTMLFeature.d.ts +2 -0
- package/types/richtext/Nan0RawNode.d.ts +41 -0
- package/types/richtext/client/index.d.ts +11 -0
- package/types/richtext/fromNan0Html.d.ts +55 -0
- package/types/richtext/index.d.ts +2 -0
- package/types/richtext/toNan0Html.d.ts +7 -0
- package/types/templates/PayloadCollectionTemplate.d.ts +58 -0
- package/types/uiPayloadRegistry.d.ts +4 -0
- package/releases/3/3/v3.3.0/task.md +0 -64
- package/releases/3/3/v3.3.0/task.spec.js +0 -13
- package/src/components/BooleanCell.js +0 -8
- package/src/components/BooleanCell.tsx +0 -50
- package/src/components/ImageCell.js +0 -26
- package/src/components/ImageCell.tsx +0 -54
- package/src/components/MapCell.js +0 -6
- package/src/components/MapCell.tsx +0 -59
- package/src/richtext/nan0html-converter.spec.js +0 -63
package/package.json
CHANGED
|
@@ -1,19 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nan0web/ui-payload",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"description": "NaN0Web Universal Payload CMS UI Components Package (ImageCell, MapCell, BooleanCell, richtext)",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "src/index.
|
|
7
|
-
"types": "
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./types/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"src/**/*.js",
|
|
10
|
+
"src/**/*.ts",
|
|
11
|
+
"src/**/*.tsx",
|
|
12
|
+
"!src/**/*.spec.js",
|
|
13
|
+
"!src/**/*.spec.tsx",
|
|
14
|
+
"types/**/*.d.ts"
|
|
15
|
+
],
|
|
8
16
|
"exports": {
|
|
9
|
-
".":
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"./
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./types/index.d.ts",
|
|
19
|
+
"import": "./src/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./ui/payload": {
|
|
22
|
+
"types": "./types/index.d.ts",
|
|
23
|
+
"import": "./src/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./components/*": {
|
|
26
|
+
"types": "./types/components/*.d.ts",
|
|
27
|
+
"import": "./src/components/*.js"
|
|
28
|
+
},
|
|
29
|
+
"./templates": {
|
|
30
|
+
"types": "./types/templates/PayloadCollectionTemplate.d.ts",
|
|
31
|
+
"import": "./src/templates/PayloadCollectionTemplate.js"
|
|
32
|
+
},
|
|
33
|
+
"./templates/*": {
|
|
34
|
+
"types": "./types/templates/*.d.ts",
|
|
35
|
+
"import": "./src/templates/*.js"
|
|
36
|
+
},
|
|
37
|
+
"./richtext": {
|
|
38
|
+
"types": "./types/richtext/index.d.ts",
|
|
39
|
+
"import": "./src/richtext/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./richtext/client": {
|
|
42
|
+
"types": "./types/richtext/client/index.d.ts",
|
|
43
|
+
"import": "./src/richtext/client/index.js"
|
|
44
|
+
},
|
|
45
|
+
"./richtext/Nan0HTMLFeature.js": {
|
|
46
|
+
"types": "./types/richtext/Nan0HTMLFeature.d.ts",
|
|
47
|
+
"import": "./src/richtext/Nan0HTMLFeature.js"
|
|
48
|
+
},
|
|
49
|
+
"./richtext/*": {
|
|
50
|
+
"types": "./types/richtext/*.d.ts",
|
|
51
|
+
"import": "./src/richtext/*.js"
|
|
52
|
+
}
|
|
17
53
|
},
|
|
18
54
|
"dependencies": {
|
|
19
55
|
"@payloadcms/richtext-lexical": "3.86.0",
|
|
@@ -25,8 +61,20 @@
|
|
|
25
61
|
"@payloadcms/ui": "^3.0.0",
|
|
26
62
|
"react": "^19.0.0"
|
|
27
63
|
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"typescript": "^5.9.3"
|
|
66
|
+
},
|
|
28
67
|
"license": "MIT",
|
|
29
68
|
"scripts": {
|
|
30
|
-
"
|
|
69
|
+
"prebuild": "rm -rf types/",
|
|
70
|
+
"build": "tsc",
|
|
71
|
+
"clean": "rm -rf .cache/ && rm -rf dist/",
|
|
72
|
+
"clean:modules": "rm -rf node_modules",
|
|
73
|
+
"test": "node --test \"src/**/*.test.js\"",
|
|
74
|
+
"test:release": "node --test \"src/test/releases/**/*.test.js\"",
|
|
75
|
+
"release:spec": "node --test \"releases/**/*.spec.js\"",
|
|
76
|
+
"knip": "knip --production || true",
|
|
77
|
+
"audit": "pnpm audit --prod || true",
|
|
78
|
+
"test:all": "npm run test && npm run build && npm run test:release && npm run knip && npm run audit"
|
|
31
79
|
}
|
|
32
80
|
}
|
|
@@ -2,16 +2,9 @@ export { ImageCell } from './components/ImageCell.js'
|
|
|
2
2
|
export { BooleanCell } from './components/BooleanCell.js'
|
|
3
3
|
export { MapCell } from './components/MapCell.js'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
// richtext — pure functions only (node classes require 'lexical' context)
|
|
5
|
+
// richtext — pure functions only & Nan0HTMLFeature
|
|
7
6
|
export { fromNan0Html, inventoryNan0Html, FORMAT } from './richtext/fromNan0Html.js'
|
|
7
|
+
export { Nan0HTMLFeature } from './richtext/Nan0HTMLFeature.js'
|
|
8
8
|
|
|
9
9
|
// access control helpers
|
|
10
10
|
export { accessFor, publicAccess } from './access.js'
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access control helpers for Payload CMS collections.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Creates a Payload access control function checking user presence and optional role(s).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // Requires any authenticated user
|
|
9
|
+
* create: accessFor()
|
|
10
|
+
*
|
|
11
|
+
* // Requires specific role(s)
|
|
12
|
+
* update: accessFor('admin', 'editor')
|
|
13
|
+
* // or
|
|
14
|
+
* delete: accessFor(['admin'])
|
|
15
|
+
*
|
|
16
|
+
* @param {...(string | string[])} roles - Allowed role(s). If none specified, checks for any authenticated user.
|
|
17
|
+
* @returns {import('payload').Access} Payload Access control function
|
|
18
|
+
*/
|
|
19
|
+
export function accessFor(...roles: (string | string[])[]): import("payload").Access;
|
|
20
|
+
/**
|
|
21
|
+
* Shortcut alias for public read access.
|
|
22
|
+
* @type {() => boolean}
|
|
23
|
+
*/
|
|
24
|
+
export const publicAccess: () => boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} BooleanCellProps
|
|
3
|
+
* @property {boolean} [cellData]
|
|
4
|
+
* @property {boolean} [value]
|
|
5
|
+
* @property {any} [field]
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Custom BooleanCell renderer for Payload CMS admin tables.
|
|
9
|
+
* @param {BooleanCellProps} props
|
|
10
|
+
* @returns {React.JSX.Element}
|
|
11
|
+
*/
|
|
12
|
+
export function BooleanCell({ cellData, value, field }: BooleanCellProps): React.JSX.Element;
|
|
13
|
+
export default BooleanCell;
|
|
14
|
+
export type BooleanCellProps = {
|
|
15
|
+
cellData?: boolean | undefined;
|
|
16
|
+
value?: boolean | undefined;
|
|
17
|
+
field?: any;
|
|
18
|
+
};
|
|
19
|
+
import React from 'react';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} ImageCellProps
|
|
3
|
+
* @property {string | { url?: string; thumbnailURL?: string }} [cellData]
|
|
4
|
+
* @property {string | { url?: string; thumbnailURL?: string }} [value]
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Image preview cell for Payload CMS admin tables.
|
|
8
|
+
* @param {ImageCellProps} props
|
|
9
|
+
* @returns {React.JSX.Element | null}
|
|
10
|
+
*/
|
|
11
|
+
export function ImageCell(props: ImageCellProps): React.JSX.Element | null;
|
|
12
|
+
export default ImageCell;
|
|
13
|
+
export type ImageCellProps = {
|
|
14
|
+
cellData?: string | {
|
|
15
|
+
url?: string;
|
|
16
|
+
thumbnailURL?: string;
|
|
17
|
+
} | undefined;
|
|
18
|
+
value?: string | {
|
|
19
|
+
url?: string;
|
|
20
|
+
thumbnailURL?: string;
|
|
21
|
+
} | undefined;
|
|
22
|
+
};
|
|
23
|
+
import React from 'react';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} MapCellProps
|
|
3
|
+
* @property {any} [cellData]
|
|
4
|
+
* @property {any} [value]
|
|
5
|
+
* @property {any} [rowData]
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Map preview cell for Payload CMS admin tables with OpenStreetMap link.
|
|
9
|
+
* @param {MapCellProps} props
|
|
10
|
+
* @returns {React.JSX.Element}
|
|
11
|
+
*/
|
|
12
|
+
export function MapCell({ rowData }: MapCellProps): React.JSX.Element;
|
|
13
|
+
export default MapCell;
|
|
14
|
+
export type MapCellProps = {
|
|
15
|
+
cellData?: any;
|
|
16
|
+
value?: any;
|
|
17
|
+
rowData?: any;
|
|
18
|
+
};
|
|
19
|
+
import React from 'react';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} AppOptions
|
|
3
|
+
* @property {import('payload').Payload} [payload]
|
|
4
|
+
* @property {any} [config]
|
|
5
|
+
*/
|
|
6
|
+
/** @typedef {import('@nan0web/ui').ModelAsAppOptions & AppOptions} PayloadAppOptions */
|
|
7
|
+
/**
|
|
8
|
+
* PayloadApp
|
|
9
|
+
* Base application class for Payload CMS subcommands (SeedModel, TransformModel, etc.)
|
|
10
|
+
* @extends {ModelAsApp}
|
|
11
|
+
*/
|
|
12
|
+
export class PayloadApp extends ModelAsApp {
|
|
13
|
+
/**
|
|
14
|
+
* @param {Partial<ModelAsApp> | Record<string, any>} [data={}]
|
|
15
|
+
* @param {Partial<PayloadAppOptions>} [options={}]
|
|
16
|
+
*/
|
|
17
|
+
constructor(data?: Partial<ModelAsApp> | Record<string, any>, options?: Partial<PayloadAppOptions>);
|
|
18
|
+
_: {
|
|
19
|
+
payload: import("payload").BasePayload | null;
|
|
20
|
+
config: any;
|
|
21
|
+
adapter: import("@nan0web/ui").InputAdapter;
|
|
22
|
+
parentPath: string;
|
|
23
|
+
_isExplicit: boolean;
|
|
24
|
+
db: import("@nan0web/db").default | null | undefined;
|
|
25
|
+
plugins: Record<string, any>;
|
|
26
|
+
t: import("@nan0web/types/src/utils/TFunction").TFunction;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Lazily initializes and returns the Payload Local API instance.
|
|
30
|
+
* @returns {Promise<import('payload').Payload>}
|
|
31
|
+
*/
|
|
32
|
+
getPayloadInstance(): Promise<import("payload").Payload>;
|
|
33
|
+
}
|
|
34
|
+
export type AppOptions = {
|
|
35
|
+
payload?: import("payload").BasePayload | undefined;
|
|
36
|
+
config?: any;
|
|
37
|
+
};
|
|
38
|
+
export type PayloadAppOptions = import("@nan0web/ui").ModelAsAppOptions & AppOptions;
|
|
39
|
+
import { ModelAsApp } from '@nan0web/ui';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SeedModel - Universal Subcommand to seed DB-FS data into Payload CMS
|
|
3
|
+
* @extends {PayloadApp}
|
|
4
|
+
*/
|
|
5
|
+
export class SeedModel extends PayloadApp {
|
|
6
|
+
static alias: string;
|
|
7
|
+
static UI: {
|
|
8
|
+
title: string;
|
|
9
|
+
start: string;
|
|
10
|
+
scanning: string;
|
|
11
|
+
loading: string;
|
|
12
|
+
seeded: string;
|
|
13
|
+
done: string;
|
|
14
|
+
errorDb: string;
|
|
15
|
+
};
|
|
16
|
+
static dataDir: {
|
|
17
|
+
help: string;
|
|
18
|
+
default: string;
|
|
19
|
+
positional: boolean;
|
|
20
|
+
};
|
|
21
|
+
static output: {
|
|
22
|
+
help: string;
|
|
23
|
+
default: string;
|
|
24
|
+
alias: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* @param {Partial<SeedModel>} [data={}]
|
|
28
|
+
* @param {Partial<import('./PayloadApp.js').PayloadAppOptions>} [options={}]
|
|
29
|
+
*/
|
|
30
|
+
constructor(data?: Partial<SeedModel>, options?: Partial<import("./PayloadApp.js").PayloadAppOptions>);
|
|
31
|
+
/** @type {string} Target directory containing SSOT data files */ dataDir: string;
|
|
32
|
+
/** @type {string} Output path */ output: string;
|
|
33
|
+
/**
|
|
34
|
+
* Reads seed files via DB instance.
|
|
35
|
+
* @param {string} target
|
|
36
|
+
* @returns {Promise<string[]>}
|
|
37
|
+
*/
|
|
38
|
+
readSeedFiles(target: string): Promise<string[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Dynamically resolves collection slug from document metadata or URI structure.
|
|
41
|
+
* @param {string} uri
|
|
42
|
+
* @param {any} [doc]
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
resolveCollectionSlug(uri: string, doc?: any): string;
|
|
46
|
+
/**
|
|
47
|
+
* Base default normalization for DB-FS records.
|
|
48
|
+
* Can be overridden by domain-specific SeedModels (e.g. CardSeedModel, BankSeedModel).
|
|
49
|
+
* @param {any} rawRecord
|
|
50
|
+
* @returns {Object}
|
|
51
|
+
*/
|
|
52
|
+
normalizeRecord(rawRecord: any): any;
|
|
53
|
+
/**
|
|
54
|
+
* @returns {AsyncGenerator<import('@nan0web/ui/core').Intent, import('@nan0web/ui/core').ResultIntent, any>}
|
|
55
|
+
*/
|
|
56
|
+
run(): AsyncGenerator<import("@nan0web/ui/core").Intent, import("@nan0web/ui/core").ResultIntent, any>;
|
|
57
|
+
}
|
|
58
|
+
import { PayloadApp } from './PayloadApp.js';
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ImageCell } from "./components/ImageCell.js";
|
|
2
|
+
export { BooleanCell } from "./components/BooleanCell.js";
|
|
3
|
+
export { MapCell } from "./components/MapCell.js";
|
|
4
|
+
export { Nan0HTMLFeature } from "./richtext/Nan0HTMLFeature.js";
|
|
5
|
+
export { fromNan0Html, inventoryNan0Html, FORMAT } from "./richtext/fromNan0Html.js";
|
|
6
|
+
export { accessFor, publicAccess } from "./access.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function $createNan0ComponentNode({ component, props }: {
|
|
2
|
+
component: any;
|
|
3
|
+
props?: {} | undefined;
|
|
4
|
+
}): Nan0ComponentNode;
|
|
5
|
+
export function $isNan0ComponentNode(node: any): node is Nan0ComponentNode;
|
|
6
|
+
export class Nan0ComponentNode extends DecoratorNode<any> {
|
|
7
|
+
static clone(node: any): Nan0ComponentNode;
|
|
8
|
+
static importJSON(serializedNode: any): Nan0ComponentNode;
|
|
9
|
+
constructor({ component, props, key }: {
|
|
10
|
+
component: any;
|
|
11
|
+
props?: {} | undefined;
|
|
12
|
+
key: any;
|
|
13
|
+
});
|
|
14
|
+
/** @type {string} */ __component: string;
|
|
15
|
+
/** @type {Record<string, any>} */ __props: Record<string, any>;
|
|
16
|
+
exportJSON(): {
|
|
17
|
+
type: string;
|
|
18
|
+
version: number;
|
|
19
|
+
component: string;
|
|
20
|
+
props: Record<string, any>;
|
|
21
|
+
$?: Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
getComponent(): string;
|
|
24
|
+
getProps(): Record<string, any>;
|
|
25
|
+
createDOM(): HTMLDivElement;
|
|
26
|
+
updateDOM(): boolean;
|
|
27
|
+
decorate(): null;
|
|
28
|
+
}
|
|
29
|
+
import { DecoratorNode } from 'lexical';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function $createNan0ElementNode({ tag, attributes }: {
|
|
2
|
+
tag: any;
|
|
3
|
+
attributes?: {} | undefined;
|
|
4
|
+
}): Nan0ElementNode;
|
|
5
|
+
export function $isNan0ElementNode(node: any): node is Nan0ElementNode;
|
|
6
|
+
export class Nan0ElementNode extends ElementNode {
|
|
7
|
+
static clone(node: any): Nan0ElementNode;
|
|
8
|
+
static importJSON(serializedNode: any): Nan0ElementNode;
|
|
9
|
+
constructor({ tag, attributes, key }: {
|
|
10
|
+
tag: any;
|
|
11
|
+
attributes?: {} | undefined;
|
|
12
|
+
key: any;
|
|
13
|
+
});
|
|
14
|
+
/** @type {string} */ __tag: string;
|
|
15
|
+
/** @type {Record<string, any>} */ __attributes: Record<string, any>;
|
|
16
|
+
exportJSON(): {
|
|
17
|
+
type: string;
|
|
18
|
+
version: number;
|
|
19
|
+
tag: string;
|
|
20
|
+
attributes: Record<string, any>;
|
|
21
|
+
$?: Record<string, unknown> | undefined;
|
|
22
|
+
children: import("lexical").SerializedLexicalNode[];
|
|
23
|
+
direction: "ltr" | "rtl" | null;
|
|
24
|
+
format: import("lexical").ElementFormatType;
|
|
25
|
+
indent: number;
|
|
26
|
+
textFormat?: number;
|
|
27
|
+
textStyle?: string;
|
|
28
|
+
};
|
|
29
|
+
getTag(): string;
|
|
30
|
+
getAttributes(): Record<string, any>;
|
|
31
|
+
createDOM(config: any): HTMLElement;
|
|
32
|
+
updateDOM(): boolean;
|
|
33
|
+
}
|
|
34
|
+
import { ElementNode } from 'lexical';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function $createNan0RawNode({ source }: {
|
|
2
|
+
source: any;
|
|
3
|
+
}): Nan0RawNode;
|
|
4
|
+
export function $isNan0RawNode(node: any): node is Nan0RawNode;
|
|
5
|
+
export class Nan0RawNode extends ElementNode {
|
|
6
|
+
static clone(node: any): Nan0RawNode;
|
|
7
|
+
static importJSON(serializedNode: any): Nan0RawNode;
|
|
8
|
+
constructor({ source, key }: {
|
|
9
|
+
source: any;
|
|
10
|
+
key: any;
|
|
11
|
+
});
|
|
12
|
+
/** @type {{ tag: string, attributes: Record<string, any>, children: any[] }} */ __source: {
|
|
13
|
+
tag: string;
|
|
14
|
+
attributes: Record<string, any>;
|
|
15
|
+
children: any[];
|
|
16
|
+
};
|
|
17
|
+
exportJSON(): {
|
|
18
|
+
type: string;
|
|
19
|
+
version: number;
|
|
20
|
+
source: {
|
|
21
|
+
tag: string;
|
|
22
|
+
attributes: Record<string, any>;
|
|
23
|
+
children: any[];
|
|
24
|
+
};
|
|
25
|
+
$?: Record<string, unknown> | undefined;
|
|
26
|
+
children: import("lexical").SerializedLexicalNode[];
|
|
27
|
+
direction: "ltr" | "rtl" | null;
|
|
28
|
+
format: import("lexical").ElementFormatType;
|
|
29
|
+
indent: number;
|
|
30
|
+
textFormat?: number;
|
|
31
|
+
textStyle?: string;
|
|
32
|
+
};
|
|
33
|
+
getSource(): {
|
|
34
|
+
tag: string;
|
|
35
|
+
attributes: Record<string, any>;
|
|
36
|
+
children: any[];
|
|
37
|
+
};
|
|
38
|
+
createDOM(config: any): HTMLDivElement;
|
|
39
|
+
updateDOM(): boolean;
|
|
40
|
+
}
|
|
41
|
+
import { ElementNode } from 'lexical';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function Nan0HTMLFeatureClient(props?: {}): {
|
|
2
|
+
clientFeatureProps: {};
|
|
3
|
+
feature: () => {
|
|
4
|
+
nodes: (typeof Nan0ElementNode | typeof Nan0RawNode | typeof Nan0ComponentNode)[];
|
|
5
|
+
sanitizedClientFeatureProps: {};
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
export default Nan0HTMLFeatureClient;
|
|
9
|
+
import { Nan0ElementNode } from '../Nan0ElementNode.js';
|
|
10
|
+
import { Nan0RawNode } from '../Nan0RawNode.js';
|
|
11
|
+
import { Nan0ComponentNode } from '../Nan0ComponentNode.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursively walk the NaN0HTML AST and collect unknown constructs.
|
|
3
|
+
* @param {any} node
|
|
4
|
+
* @param {Array<{tag: string, attributes: Object, parent: string}>} out
|
|
5
|
+
* @param {string} parent
|
|
6
|
+
*/
|
|
7
|
+
export function inventoryNan0Html(node: any, out?: Array<{
|
|
8
|
+
tag: string;
|
|
9
|
+
attributes: any;
|
|
10
|
+
parent: string;
|
|
11
|
+
}>, parent?: string): {
|
|
12
|
+
tag: string;
|
|
13
|
+
attributes: any;
|
|
14
|
+
parent: string;
|
|
15
|
+
}[];
|
|
16
|
+
/**
|
|
17
|
+
* Convert a NaN0HTML AST into a Payload Lexical root state.
|
|
18
|
+
* @param {any} content NaN0HTML AST (array of blocks, or a single block)
|
|
19
|
+
* @returns {{ root: { type: string, version: number, children: any[] } }}
|
|
20
|
+
*/
|
|
21
|
+
export function fromNan0Html(content: any): {
|
|
22
|
+
root: {
|
|
23
|
+
type: string;
|
|
24
|
+
version: number;
|
|
25
|
+
children: any[];
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* fromNan0Html
|
|
30
|
+
* Converts a NaN0HTML AST (page.content / content) into a Payload Lexical state JSON.
|
|
31
|
+
*
|
|
32
|
+
* NaN0HTML AST shape:
|
|
33
|
+
* - array → sequence of blocks
|
|
34
|
+
* - string → text content
|
|
35
|
+
* - object → element with keys: `$attr` for attributes, lowercase tag keys for
|
|
36
|
+
* child elements, `Uppercase.With.Dot` keys for NaN0 components
|
|
37
|
+
* - `tag: true` → void element (e.g. `br`, `hr`)
|
|
38
|
+
*
|
|
39
|
+
* Strategy:
|
|
40
|
+
* - Clean elements (no attributes) map to native Lexical nodes where possible.
|
|
41
|
+
* - Elements carrying any `$`-attribute are preserved losslessly as a `nan0-element`
|
|
42
|
+
* node (tag + attributes + children) so round-trip back to NaN0HTML is lossless.
|
|
43
|
+
* - Unknown tags fall back to a `nan0-raw` node and are recorded in the inventory.
|
|
44
|
+
* - NaN0 components (`App.Header`, `Card.Details`, ...) become `nan0-component` nodes.
|
|
45
|
+
*
|
|
46
|
+
* The converter returns a plain structure; localization is handled at the Payload
|
|
47
|
+
* collection level, not here.
|
|
48
|
+
*/
|
|
49
|
+
export const FORMAT: Readonly<{
|
|
50
|
+
bold: number;
|
|
51
|
+
italic: number;
|
|
52
|
+
underline: number;
|
|
53
|
+
strikethrough: number;
|
|
54
|
+
}>;
|
|
55
|
+
export default fromNan0Html;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert Payload Lexical state (or root node) back into a NaN0HTML AST.
|
|
3
|
+
* @param {Object} lexicalState Lexical state object `{ root: { children: [...] } }` or node object
|
|
4
|
+
* @returns {any} NaN0HTML AST structure (Array, Object, or String)
|
|
5
|
+
*/
|
|
6
|
+
export function toNan0Html(lexicalState: any): any;
|
|
7
|
+
export default toNan0Html;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** @typedef {Object} Field */
|
|
2
|
+
/**
|
|
3
|
+
* PayloadCollectionTemplate - Model to generate Payload CMS CollectionConfig using CodeTemplate.
|
|
4
|
+
*/
|
|
5
|
+
export class PayloadCollectionTemplate extends Model {
|
|
6
|
+
static alias: string;
|
|
7
|
+
static collectionSlug: {
|
|
8
|
+
help: string;
|
|
9
|
+
default: string;
|
|
10
|
+
};
|
|
11
|
+
static useAsTitle: {
|
|
12
|
+
help: string;
|
|
13
|
+
default: string;
|
|
14
|
+
};
|
|
15
|
+
static labels: {
|
|
16
|
+
help: string;
|
|
17
|
+
default: {
|
|
18
|
+
singular: string;
|
|
19
|
+
plural: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
static group: {
|
|
23
|
+
help: string;
|
|
24
|
+
type: string;
|
|
25
|
+
default: string;
|
|
26
|
+
};
|
|
27
|
+
static fields: {
|
|
28
|
+
help: string;
|
|
29
|
+
default: never[];
|
|
30
|
+
};
|
|
31
|
+
static template: {
|
|
32
|
+
help: string;
|
|
33
|
+
default: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* @param {Partial<PayloadCollectionTemplate>} [data={}]
|
|
37
|
+
* @param {Partial<import('@nan0web/types').ModelOptions>} [options={}]
|
|
38
|
+
*/
|
|
39
|
+
constructor(data?: Partial<PayloadCollectionTemplate>, options?: Partial<import("@nan0web/types").ModelOptions>);
|
|
40
|
+
/** @type {string} Collection slug */ collectionSlug: string;
|
|
41
|
+
/** @type {string} Label/title field name */ useAsTitle: string;
|
|
42
|
+
/** @type {Object} Labels object */ labels: any;
|
|
43
|
+
/** @type {Object|string} Group object or string */ group: any | string;
|
|
44
|
+
/** @type {Array<Field>} Collection fields */ fields: Array<Field>;
|
|
45
|
+
/** @type {Object} Custom replace snippets */ snippets: any;
|
|
46
|
+
/**
|
|
47
|
+
* Compiles the CollectionConfig template using native CodeTemplate replace blocks.
|
|
48
|
+
* @returns {Promise<string>} Generated TS code for the collection
|
|
49
|
+
*/
|
|
50
|
+
compile(): Promise<string>;
|
|
51
|
+
/**
|
|
52
|
+
* Synchronously compiles the CollectionConfig template.
|
|
53
|
+
* @returns {string} Generated TS code for the collection
|
|
54
|
+
*/
|
|
55
|
+
compileSync(): string;
|
|
56
|
+
}
|
|
57
|
+
export type Field = any;
|
|
58
|
+
import { Model } from '@nan0web/types';
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
version: 3.3.0
|
|
3
|
-
type: feature
|
|
4
|
-
status: active
|
|
5
|
-
locale: uk
|
|
6
|
-
models: ["MarkdownBlockView", "MediaBlockView"]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# 🚀 Mission: Payload CMS UI Адаптери та Блок-Компоненти у @nan0web/ui-payload (v3.3.0)
|
|
10
|
-
|
|
11
|
-
## 🏁 Overview (Огляд)
|
|
12
|
-
Реалізація універсальних UI компонентів та адаптерів Payload CMS для `MarkdownBlock`, `MediaBlock` та надання поліморфного `uiPayloadRegistry`.
|
|
13
|
-
|
|
14
|
-
## 👥 User Stories (Сценарії)
|
|
15
|
-
- Як розробник, я хочу відображати блоки `MarkdownBlock` через готові React/Payload UI елементи з підтримкою вкладеного графіків/калькуляторів.
|
|
16
|
-
- Як розробник, я хочу реєструвати Payload UI блоки через `uiPayloadRegistry`, щоб OLMUI рендеринг працював без ручних процедурних розгалужень.
|
|
17
|
-
|
|
18
|
-
## 🏗 Data-Driven Architecture (Моделювання)
|
|
19
|
-
- `uiPayloadRegistry`:
|
|
20
|
-
- `Map<ModelConstructor, PayloadBlockViewComponent>`
|
|
21
|
-
- `MarkdownBlockView`:
|
|
22
|
-
- Компонент для `type: 'text/markdown'`
|
|
23
|
-
- `MediaBlockView`:
|
|
24
|
-
- Компонент для `static $upload = true`
|
|
25
|
-
|
|
26
|
-
## 🎯 Scope (Задачі)
|
|
27
|
-
- [ ] Створити `uiPayloadRegistry` для прив'язки моделей до Payload/React UI View класів.
|
|
28
|
-
- [ ] Створити універсальний компонент `MarkdownBlockView`.
|
|
29
|
-
- [ ] Створити універсальний компонент `MediaBlockView`.
|
|
30
|
-
|
|
31
|
-
## ✅ Acceptance Criteria (DoD)
|
|
32
|
-
- [ ] Контрактні тести (`task.spec.js`) написані і успішно проходять (Green).
|
|
33
|
-
- [ ] OLMUI рендеринг: 0% процедурних `switch/case` в елементах рендерингу.
|
|
34
|
-
|
|
35
|
-
## 📌 Context Checkpoint (2026-08-07)
|
|
36
|
-
|
|
37
|
-
### Implemented
|
|
38
|
-
- Added `fromNan0Html()` and `inventoryNan0Html()` conversion helpers.
|
|
39
|
-
- Added Lexical custom nodes: `Nan0ElementNode`, `Nan0RawNode`, and `Nan0ComponentNode`.
|
|
40
|
-
- Added `Nan0HTMLFeature()` with server-side HTML converters and client import-map entry.
|
|
41
|
-
- Added Payload Lexical dependencies and public richtext exports.
|
|
42
|
-
- Added `ImageCell` preview with fixed `192x108` dimensions.
|
|
43
|
-
- Added Payload 3.86-compatible client feature provider in `src/richtext/client/index.js`.
|
|
44
|
-
|
|
45
|
-
### Verification
|
|
46
|
-
- `pnpm --prefix packages/ui-payload test` passes.
|
|
47
|
-
- Root commit hook passed 43 tests.
|
|
48
|
-
- Industrial Bank seed and Payload admin integration were exercised.
|
|
49
|
-
|
|
50
|
-
### Current State
|
|
51
|
-
- Changes are intentionally uncommitted after removing the last agent-created commit.
|
|
52
|
-
- They are staged in `packages/ui-payload` and must be committed by the user.
|
|
53
|
-
- Other repository changes are unrelated and must not be included.
|
|
54
|
-
- Industrial Bank CMS import map uses `@nan0web/ui-payload#ImageCell`.
|
|
55
|
-
- CMS image paths `/img/...` and `/images/...` are mounted from `bank/public`.
|
|
56
|
-
|
|
57
|
-
### Next Steps
|
|
58
|
-
1. Commit only the staged `packages/ui-payload` changes.
|
|
59
|
-
2. Run the Industrial Bank CMS build and fix remaining unrelated TypeScript blockers, starting with `src/test-read.ts` using obsolete `DB({ rootDir })`.
|
|
60
|
-
3. Verify `/admin/collections/cards/<id>` editing and `ImageCell` rendering.
|
|
61
|
-
4. Add focused contract tests for Nan0HTML/Lexical conversion and custom nodes.
|
|
62
|
-
|
|
63
|
-
### New Chat Bootstrap
|
|
64
|
-
Start from branch `feature/payload-cms`, base `070afe7`, with staged changes in `packages/ui-payload`. Do not reset, clean, commit, or push without explicit user instruction.
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { uiPayloadRegistry } from '../../../../src/uiPayloadRegistry.js'
|
|
4
|
-
|
|
5
|
-
describe('Release v3.3.0: @nan0web/ui-payload Contract', () => {
|
|
6
|
-
it('uiPayloadRegistry provides polymorphic lookup for block views', () => {
|
|
7
|
-
class MockModel {}
|
|
8
|
-
class MockView {}
|
|
9
|
-
uiPayloadRegistry.set(MockModel, MockView)
|
|
10
|
-
|
|
11
|
-
assert.equal(uiPayloadRegistry.get(MockModel), MockView)
|
|
12
|
-
})
|
|
13
|
-
})
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import React from 'react'
|
|
4
|
-
import { useTranslation } from '@payloadcms/ui'
|
|
5
|
-
|
|
6
|
-
export const BooleanCell: React.FC<{ cellData: boolean; field: any }> = ({ cellData, field }) => {
|
|
7
|
-
const { i18n } = useTranslation()
|
|
8
|
-
const lang = i18n?.language?.toLowerCase().startsWith('en') ? 'en' : 'uk'
|
|
9
|
-
const val = Boolean(cellData)
|
|
10
|
-
|
|
11
|
-
const isHiddenField = field?.name?.toLowerCase() === 'hidden'
|
|
12
|
-
const customLabels = field?.admin?.custom?.labels
|
|
13
|
-
|
|
14
|
-
const defaultLabels = isHiddenField
|
|
15
|
-
? { true: { uk: '🙈 Приховано', en: '🙈 Hidden' }, false: { uk: '🌐 Активно', en: '🌐 Active' } }
|
|
16
|
-
: { true: { uk: '✅ Так', en: '✅ Yes' }, false: { uk: '❌ Ні', en: '❌ No' } }
|
|
17
|
-
|
|
18
|
-
const labels = customLabels || defaultLabels
|
|
19
|
-
const text = labels[val ? 'true' : 'false']?.[lang] || (val ? 'Yes' : 'No')
|
|
20
|
-
|
|
21
|
-
const isWarning = isHiddenField ? val : !val
|
|
22
|
-
const variant = isWarning ? 'warning' : 'success'
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<span
|
|
26
|
-
className={`badge bg-${variant}-subtle text-${variant}-emphasis border border-${variant}-subtle`}
|
|
27
|
-
style={{
|
|
28
|
-
display: 'inline-flex',
|
|
29
|
-
alignItems: 'center',
|
|
30
|
-
gap: '4px',
|
|
31
|
-
padding: '3px 8px',
|
|
32
|
-
borderRadius: '12px',
|
|
33
|
-
fontSize: '11px',
|
|
34
|
-
fontWeight: 600,
|
|
35
|
-
backgroundColor: isWarning
|
|
36
|
-
? 'var(--bs-warning-bg-subtle, rgba(255, 193, 7, 0.15))'
|
|
37
|
-
: 'var(--bs-success-bg-subtle, rgba(25, 135, 84, 0.15))',
|
|
38
|
-
color: isWarning
|
|
39
|
-
? 'var(--bs-warning-text-emphasis, #664d03)'
|
|
40
|
-
: 'var(--bs-success-text-emphasis, #0a3622)',
|
|
41
|
-
borderColor: isWarning
|
|
42
|
-
? 'var(--bs-warning-border-subtle, rgba(255, 193, 7, 0.3))'
|
|
43
|
-
: 'var(--bs-success-border-subtle, rgba(25, 135, 84, 0.3))',
|
|
44
|
-
}}
|
|
45
|
-
suppressHydrationWarning
|
|
46
|
-
>
|
|
47
|
-
{text}
|
|
48
|
-
</span>
|
|
49
|
-
)
|
|
50
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
|
|
3
|
-
export function ImageCell(props) {
|
|
4
|
-
const value = props.cellData || props.value
|
|
5
|
-
if (!value) return null
|
|
6
|
-
|
|
7
|
-
const srcValue = typeof value === 'object' ? value.url || value.thumbnailURL : value
|
|
8
|
-
if (!srcValue) return null
|
|
9
|
-
const src = srcValue.startsWith('http') || srcValue.startsWith('/') ? srcValue : `/${srcValue}`
|
|
10
|
-
|
|
11
|
-
return React.createElement('img', {
|
|
12
|
-
src,
|
|
13
|
-
alt: 'Thumbnail',
|
|
14
|
-
width: 192,
|
|
15
|
-
height: 108,
|
|
16
|
-
style: {
|
|
17
|
-
width: 192,
|
|
18
|
-
minWidth: 192,
|
|
19
|
-
height: 108,
|
|
20
|
-
minHeight: 108,
|
|
21
|
-
objectFit: 'cover',
|
|
22
|
-
borderRadius: 6,
|
|
23
|
-
display: 'block',
|
|
24
|
-
},
|
|
25
|
-
})
|
|
26
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import React, { useState } from 'react'
|
|
4
|
-
|
|
5
|
-
export const ImageCell: React.FC<{ cellData: string }> = ({ cellData }) => {
|
|
6
|
-
const [hasError, setHasError] = useState(false)
|
|
7
|
-
|
|
8
|
-
if (!cellData || typeof cellData !== 'string' || hasError) {
|
|
9
|
-
return (
|
|
10
|
-
<div
|
|
11
|
-
style={{
|
|
12
|
-
display: 'inline-flex',
|
|
13
|
-
alignItems: 'center',
|
|
14
|
-
justifyContent: 'center',
|
|
15
|
-
width: '192px',
|
|
16
|
-
height: '108px',
|
|
17
|
-
borderRadius: '6px',
|
|
18
|
-
background: 'rgba(255, 255, 255, 0.05)',
|
|
19
|
-
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
20
|
-
fontSize: '12px',
|
|
21
|
-
color: '#888',
|
|
22
|
-
}}
|
|
23
|
-
suppressHydrationWarning
|
|
24
|
-
>
|
|
25
|
-
<span>🖼️ Без фото</span>
|
|
26
|
-
</div>
|
|
27
|
-
)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const src = cellData.startsWith('http') || cellData.startsWith('/') ? cellData : `/${cellData}`
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<div style={{ display: 'inline-flex', alignItems: 'center' }} suppressHydrationWarning>
|
|
34
|
-
<img
|
|
35
|
-
src={src}
|
|
36
|
-
alt="Preview"
|
|
37
|
-
style={{
|
|
38
|
-
width: '192px',
|
|
39
|
-
minWidth: '192px',
|
|
40
|
-
height: '108px',
|
|
41
|
-
minHeight: '108px',
|
|
42
|
-
display: 'block',
|
|
43
|
-
objectFit: 'cover',
|
|
44
|
-
borderRadius: '6px',
|
|
45
|
-
border: '1px solid rgba(255, 255, 255, 0.25)',
|
|
46
|
-
background: '#111',
|
|
47
|
-
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.4)',
|
|
48
|
-
}}
|
|
49
|
-
onError={() => setHasError(true)}
|
|
50
|
-
suppressHydrationWarning
|
|
51
|
-
/>
|
|
52
|
-
</div>
|
|
53
|
-
)
|
|
54
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import React from 'react'
|
|
4
|
-
|
|
5
|
-
export const MapCell: React.FC<{ rowData: any }> = ({ rowData }) => {
|
|
6
|
-
const lat = rowData?.lat || rowData?.latitude || '50.4501'
|
|
7
|
-
const lon = rowData?.lng || rowData?.longitude || '30.5234'
|
|
8
|
-
const address = rowData?.address || rowData?.title || 'Відділення'
|
|
9
|
-
|
|
10
|
-
const zoom = 15
|
|
11
|
-
const latNum = parseFloat(String(lat)) || 50.4501
|
|
12
|
-
const lonNum = parseFloat(String(lon)) || 30.5234
|
|
13
|
-
|
|
14
|
-
const x = Math.floor(((lonNum + 180) / 360) * Math.pow(2, zoom))
|
|
15
|
-
const y = Math.floor(
|
|
16
|
-
((1 - Math.log(Math.tan((latNum * Math.PI) / 180) + 1 / Math.cos((latNum * Math.PI) / 180)) / Math.PI) / 2) *
|
|
17
|
-
Math.pow(2, zoom)
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
const osmTileUrl = `https://tile.openstreetmap.org/${zoom}/${x}/${y}.png`
|
|
21
|
-
const osmMapUrl = `https://www.openstreetmap.org/?mlat=${latNum}&mlon=${lonNum}#map=16/${latNum}/${lonNum}`
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<a
|
|
25
|
-
href={osmMapUrl}
|
|
26
|
-
target="_blank"
|
|
27
|
-
rel="noopener noreferrer"
|
|
28
|
-
style={{
|
|
29
|
-
display: 'inline-flex',
|
|
30
|
-
alignItems: 'center',
|
|
31
|
-
gap: '8px',
|
|
32
|
-
textDecoration: 'none',
|
|
33
|
-
color: 'inherit',
|
|
34
|
-
}}
|
|
35
|
-
suppressHydrationWarning
|
|
36
|
-
>
|
|
37
|
-
<img
|
|
38
|
-
src={osmTileUrl}
|
|
39
|
-
alt="OpenStreetMap"
|
|
40
|
-
style={{
|
|
41
|
-
width: '100px',
|
|
42
|
-
height: '56px',
|
|
43
|
-
objectFit: 'cover',
|
|
44
|
-
borderRadius: '4px',
|
|
45
|
-
border: '1px solid rgba(255, 255, 255, 0.2)',
|
|
46
|
-
background: '#222',
|
|
47
|
-
}}
|
|
48
|
-
onError={(e) => {
|
|
49
|
-
;(e.target as HTMLElement).style.display = 'none'
|
|
50
|
-
}}
|
|
51
|
-
suppressHydrationWarning
|
|
52
|
-
/>
|
|
53
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
|
|
54
|
-
<span style={{ fontSize: '12px', fontWeight: 600 }}>📍 {address}</span>
|
|
55
|
-
<span style={{ fontSize: '10px', color: '#1471d1' }}>🗺️ OpenStreetMap ({latNum.toFixed(4)}, {lonNum.toFixed(4)})</span>
|
|
56
|
-
</div>
|
|
57
|
-
</a>
|
|
58
|
-
)
|
|
59
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { fromNan0Html } from './fromNan0Html.js'
|
|
3
|
-
import { toNan0Html } from './toNan0Html.js'
|
|
4
|
-
|
|
5
|
-
describe('NaN0HTML ↔ Lexical Converter Round-trip', () => {
|
|
6
|
-
it('converts basic paragraphs and text formats', () => {
|
|
7
|
-
const ast = { p: [{ strong: 'Bold text' }, ' and ', { em: 'italic text' }] }
|
|
8
|
-
const lexical = fromNan0Html(ast)
|
|
9
|
-
expect(lexical.root.type).toBe('root')
|
|
10
|
-
expect(lexical.root.children.length).toBe(1)
|
|
11
|
-
expect(lexical.root.children[0].type).toBe('paragraph')
|
|
12
|
-
|
|
13
|
-
const backToAst = toNan0Html(lexical)
|
|
14
|
-
expect(backToAst).toEqual(ast)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('converts headings correctly', () => {
|
|
18
|
-
const ast = { h2: 'Title Heading' }
|
|
19
|
-
const lexical = fromNan0Html(ast)
|
|
20
|
-
expect(lexical.root.children[0].type).toBe('heading')
|
|
21
|
-
expect(lexical.root.children[0].tag).toBe('h2')
|
|
22
|
-
|
|
23
|
-
const backToAst = toNan0Html(lexical)
|
|
24
|
-
expect(backToAst).toEqual(ast)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('converts lists and items', () => {
|
|
28
|
-
const ast = { ul: [{ li: 'Item 1' }, { li: 'Item 2' }] }
|
|
29
|
-
const lexical = fromNan0Html(ast)
|
|
30
|
-
expect(lexical.root.children[0].type).toBe('list')
|
|
31
|
-
expect(lexical.root.children[0].listType).toBe('bullet')
|
|
32
|
-
|
|
33
|
-
const backToAst = toNan0Html(lexical)
|
|
34
|
-
expect(backToAst).toEqual(ast)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('converts links with attributes', () => {
|
|
38
|
-
const ast = { a: 'Click here', $href: 'https://example.com', $target: '_blank' }
|
|
39
|
-
const lexical = fromNan0Html(ast)
|
|
40
|
-
|
|
41
|
-
const backToAst = toNan0Html(lexical)
|
|
42
|
-
expect(backToAst).toEqual({ a: 'Click here', $href: 'https://example.com', $target: '_blank' })
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('preserves nan0-component losslessly', () => {
|
|
46
|
-
const ast = { 'Card.Details': { card: '$' } }
|
|
47
|
-
const lexical = fromNan0Html(ast)
|
|
48
|
-
expect(lexical.root.children[0].type).toBe('nan0-component')
|
|
49
|
-
expect(lexical.root.children[0].component).toBe('Card.Details')
|
|
50
|
-
|
|
51
|
-
const backToAst = toNan0Html(lexical)
|
|
52
|
-
expect(backToAst).toEqual(ast)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('preserves unknown tags as nan0-raw', () => {
|
|
56
|
-
const ast = { 'custom-widget': 'Widget content', $mode: 'test' }
|
|
57
|
-
const lexical = fromNan0Html(ast)
|
|
58
|
-
expect(lexical.root.children[0].type).toBe('nan0-raw')
|
|
59
|
-
|
|
60
|
-
const backToAst = toNan0Html(lexical)
|
|
61
|
-
expect(backToAst).toEqual({ 'custom-widget': 'Widget content', $mode: 'test' })
|
|
62
|
-
})
|
|
63
|
-
})
|