@nan0web/ui-payload 3.3.0
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/LICENSE +15 -0
- package/package.json +32 -0
- package/releases/3/3/v3.3.0/task.md +64 -0
- package/releases/3/3/v3.3.0/task.spec.js +13 -0
- package/src/access.js +39 -0
- package/src/components/BooleanCell.js +8 -0
- package/src/components/BooleanCell.tsx +50 -0
- package/src/components/ImageCell.js +26 -0
- package/src/components/ImageCell.tsx +54 -0
- package/src/components/MapCell.js +6 -0
- package/src/components/MapCell.tsx +59 -0
- package/src/domain/PayloadApp.js +48 -0
- package/src/domain/SeedModel.js +149 -0
- package/src/index.ts +17 -0
- package/src/richtext/Nan0ComponentNode.js +98 -0
- package/src/richtext/Nan0ElementNode.js +93 -0
- package/src/richtext/Nan0HTMLFeature.js +94 -0
- package/src/richtext/Nan0RawNode.js +92 -0
- package/src/richtext/client/index.js +20 -0
- package/src/richtext/fromNan0Html.js +294 -0
- package/src/richtext/index.js +10 -0
- package/src/richtext/nan0html-converter.spec.js +63 -0
- package/src/richtext/toNan0Html.js +188 -0
- package/src/templates/PayloadCollectionTemplate.js +167 -0
- package/src/templates/PayloadCollectionTemplate.spec.js +26 -0
- package/src/uiPayloadRegistry.js +4 -0
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* toNan0Html
|
|
3
|
+
* Converts a Payload Lexical state JSON back into a NaN0HTML AST.
|
|
4
|
+
*
|
|
5
|
+
* Reconstructs:
|
|
6
|
+
* - Native Lexical nodes (paragraph, heading, list, listitem, text, link, etc.) -> NaN0HTML tags/strings
|
|
7
|
+
* - nan0-element -> exact tag + attributes + converted children
|
|
8
|
+
* - nan0-component -> component name + props object
|
|
9
|
+
* - nan0-raw -> restored raw tag + attributes + children
|
|
10
|
+
*/
|
|
11
|
+
import { FORMAT } from './fromNan0Html.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert Payload Lexical state (or root node) back into a NaN0HTML AST.
|
|
15
|
+
* @param {Object} lexicalState Lexical state object `{ root: { children: [...] } }` or node object
|
|
16
|
+
* @returns {any} NaN0HTML AST structure (Array, Object, or String)
|
|
17
|
+
*/
|
|
18
|
+
export function toNan0Html(lexicalState) {
|
|
19
|
+
if (!lexicalState) return null
|
|
20
|
+
|
|
21
|
+
const root = lexicalState.root || lexicalState
|
|
22
|
+
if (root.type === 'root' && Array.isArray(root.children)) {
|
|
23
|
+
return convertNodes(root.children)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return convertNode(root)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Convert an array of Lexical nodes.
|
|
31
|
+
*/
|
|
32
|
+
function convertNodes(nodes) {
|
|
33
|
+
if (!Array.isArray(nodes) || nodes.length === 0) return []
|
|
34
|
+
const result = []
|
|
35
|
+
|
|
36
|
+
for (const node of nodes) {
|
|
37
|
+
const converted = convertNode(node)
|
|
38
|
+
if (converted !== null && converted !== undefined) {
|
|
39
|
+
result.push(converted)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (result.length === 1) return result[0]
|
|
44
|
+
return result
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Convert a single Lexical node into its NaN0HTML AST representation.
|
|
49
|
+
*/
|
|
50
|
+
function convertNode(node) {
|
|
51
|
+
if (!node || typeof node !== 'object') return null
|
|
52
|
+
|
|
53
|
+
switch (node.type) {
|
|
54
|
+
case 'text':
|
|
55
|
+
return convertTextNode(node)
|
|
56
|
+
|
|
57
|
+
case 'paragraph':
|
|
58
|
+
return wrapWithTag('p', convertNodes(node.children))
|
|
59
|
+
|
|
60
|
+
case 'heading': {
|
|
61
|
+
const tag = node.tag || 'h1'
|
|
62
|
+
return wrapWithTag(tag, convertNodes(node.children))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
case 'blockquote':
|
|
66
|
+
return wrapWithTag('blockquote', convertNodes(node.children))
|
|
67
|
+
|
|
68
|
+
case 'horizontalrule':
|
|
69
|
+
return { hr: true }
|
|
70
|
+
|
|
71
|
+
case 'linebreak':
|
|
72
|
+
return { br: true }
|
|
73
|
+
|
|
74
|
+
case 'list': {
|
|
75
|
+
const tag = node.listType === 'number' || node.tag === 'ol' ? 'ol' : 'ul'
|
|
76
|
+
const content = convertNodes(node.children)
|
|
77
|
+
const attrs = {}
|
|
78
|
+
if (node.start && node.start !== 1) {
|
|
79
|
+
attrs.$start = node.start
|
|
80
|
+
}
|
|
81
|
+
return wrapWithTag(tag, content, attrs)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case 'listitem':
|
|
85
|
+
return wrapWithTag('li', convertNodes(node.children))
|
|
86
|
+
|
|
87
|
+
case 'link': {
|
|
88
|
+
const fields = node.fields || {}
|
|
89
|
+
const attrs = {}
|
|
90
|
+
if (fields.url) attrs.$href = fields.url
|
|
91
|
+
if (fields.newTab) attrs.$target = '_blank'
|
|
92
|
+
if (fields.rel) attrs.$rel = fields.rel
|
|
93
|
+
return wrapWithTag('a', convertNodes(node.children), attrs)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
case 'upload': {
|
|
97
|
+
const fields = node.fields || {}
|
|
98
|
+
const attrs = {}
|
|
99
|
+
if (fields.value) attrs.$src = typeof fields.value === 'string' ? fields.value : fields.value.id || ''
|
|
100
|
+
return wrapWithTag('img', true, attrs)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
case 'table':
|
|
104
|
+
return wrapWithTag('table', convertNodes(node.children))
|
|
105
|
+
|
|
106
|
+
case 'tablerow':
|
|
107
|
+
return wrapWithTag('tr', convertNodes(node.children))
|
|
108
|
+
|
|
109
|
+
case 'tablecell': {
|
|
110
|
+
const tag = node.header ? 'th' : 'td'
|
|
111
|
+
return wrapWithTag(tag, convertNodes(node.children))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
case 'nan0-element': {
|
|
115
|
+
const tag = node.tag || 'div'
|
|
116
|
+
const attrs = formatAttributes(node.attributes || {})
|
|
117
|
+
const content = convertNodes(node.children)
|
|
118
|
+
return wrapWithTag(tag, content, attrs)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case 'nan0-component': {
|
|
122
|
+
const compName = node.component || 'Component'
|
|
123
|
+
const props = node.props || {}
|
|
124
|
+
return { [compName]: props }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
case 'nan0-raw': {
|
|
128
|
+
const source = node.source || {}
|
|
129
|
+
const tag = source.tag || 'div'
|
|
130
|
+
const attrs = formatAttributes(source.attributes || {})
|
|
131
|
+
const content = Array.isArray(source.children) ? convertNodes(source.children) : (source.children || [])
|
|
132
|
+
return wrapWithTag(tag, content, attrs)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
default:
|
|
136
|
+
if (Array.isArray(node.children)) {
|
|
137
|
+
return convertNodes(node.children)
|
|
138
|
+
}
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Format text node taking inline format flags into account.
|
|
145
|
+
*/
|
|
146
|
+
function convertTextNode(node) {
|
|
147
|
+
const text = node.text || ''
|
|
148
|
+
const format = node.format || 0
|
|
149
|
+
let result = text
|
|
150
|
+
|
|
151
|
+
if (format & FORMAT.bold) {
|
|
152
|
+
result = { strong: result }
|
|
153
|
+
}
|
|
154
|
+
if (format & FORMAT.italic) {
|
|
155
|
+
result = { em: result }
|
|
156
|
+
}
|
|
157
|
+
if (format & FORMAT.underline) {
|
|
158
|
+
result = { u: result }
|
|
159
|
+
}
|
|
160
|
+
if (format & FORMAT.strikethrough) {
|
|
161
|
+
result = { s: result }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return result
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Prefix attribute object keys with `$`.
|
|
169
|
+
*/
|
|
170
|
+
function formatAttributes(attrs) {
|
|
171
|
+
const result = {}
|
|
172
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
173
|
+
const key = k.startsWith('$') ? k : `$${k}`
|
|
174
|
+
result[key] = v
|
|
175
|
+
}
|
|
176
|
+
return result
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Wrap content with a tag and optional $attributes.
|
|
181
|
+
*/
|
|
182
|
+
function wrapWithTag(tag, content, attrs = {}) {
|
|
183
|
+
const element = { ...attrs }
|
|
184
|
+
element[tag] = content
|
|
185
|
+
return element
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export default toNan0Html
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Model } from '@nan0web/types'
|
|
2
|
+
import { CodeTemplate } from '@nan0web/ui'
|
|
3
|
+
|
|
4
|
+
/** @typedef {Object} Field */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* PayloadCollectionTemplate - Model to generate Payload CMS CollectionConfig using CodeTemplate.
|
|
8
|
+
*/
|
|
9
|
+
export class PayloadCollectionTemplate extends Model {
|
|
10
|
+
static alias = 'payload-collection-template'
|
|
11
|
+
|
|
12
|
+
static collectionSlug = {
|
|
13
|
+
help: 'Collection slug identifier',
|
|
14
|
+
default: 'item',
|
|
15
|
+
}
|
|
16
|
+
static useAsTitle = {
|
|
17
|
+
help: 'Field name used as title in admin',
|
|
18
|
+
default: 'title',
|
|
19
|
+
}
|
|
20
|
+
static labels = {
|
|
21
|
+
help: 'Localized labels object (singular/plural)',
|
|
22
|
+
default: { singular: 'Item', plural: 'Items' },
|
|
23
|
+
}
|
|
24
|
+
static group = {
|
|
25
|
+
help: 'Admin group configuration',
|
|
26
|
+
type: 'any',
|
|
27
|
+
default: 'Content',
|
|
28
|
+
}
|
|
29
|
+
static fields = {
|
|
30
|
+
help: 'Collection fields array',
|
|
31
|
+
default: [],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static template = {
|
|
35
|
+
help: 'Raw JavaScript template with CodeTemplate @replace blocks',
|
|
36
|
+
default: `/**
|
|
37
|
+
* @replace imports
|
|
38
|
+
* Custom imports block
|
|
39
|
+
*/
|
|
40
|
+
/** @replace */
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @replace collectionSlug
|
|
44
|
+
*/
|
|
45
|
+
const collectionSlug = 'item'
|
|
46
|
+
/** @replace */
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @replace labels
|
|
50
|
+
*/
|
|
51
|
+
const labels = { singular: { uk: 'Item', en: 'Item' }, plural: { uk: 'Items', en: 'Items' } }
|
|
52
|
+
/** @replace */
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @replace useAsTitle
|
|
56
|
+
*/
|
|
57
|
+
const useAsTitle = 'title'
|
|
58
|
+
/** @replace */
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @replace group
|
|
62
|
+
*/
|
|
63
|
+
const group = { uk: 'Content', en: 'Content' }
|
|
64
|
+
/** @replace */
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @replace fields
|
|
68
|
+
*/
|
|
69
|
+
const fields = []
|
|
70
|
+
/** @replace */
|
|
71
|
+
|
|
72
|
+
import { accessFor, publicAccess } from '@nan0web/ui-payload'
|
|
73
|
+
|
|
74
|
+
/** @type {import('payload').CollectionConfig} */
|
|
75
|
+
export const collectionConfig = {
|
|
76
|
+
slug: collectionSlug,
|
|
77
|
+
labels,
|
|
78
|
+
admin: {
|
|
79
|
+
useAsTitle,
|
|
80
|
+
group,
|
|
81
|
+
},
|
|
82
|
+
access: {
|
|
83
|
+
read: publicAccess,
|
|
84
|
+
create: accessFor('admin', 'editor'),
|
|
85
|
+
update: accessFor('admin', 'editor'),
|
|
86
|
+
delete: accessFor('admin'),
|
|
87
|
+
},
|
|
88
|
+
fields,
|
|
89
|
+
}
|
|
90
|
+
`,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {Partial<PayloadCollectionTemplate>} [data={}]
|
|
95
|
+
* @param {Partial<import('@nan0web/types').ModelOptions>} [options={}]
|
|
96
|
+
*/
|
|
97
|
+
constructor(data = {}, options = {}) {
|
|
98
|
+
super(data, options)
|
|
99
|
+
/** @type {string} Collection slug */ this.collectionSlug
|
|
100
|
+
/** @type {string} Label/title field name */ this.useAsTitle
|
|
101
|
+
/** @type {Object} Labels object */ this.labels
|
|
102
|
+
/** @type {Object|string} Group object or string */ this.group
|
|
103
|
+
/** @type {Array<Field>} Collection fields */ this.fields
|
|
104
|
+
/** @type {Object} Custom replace snippets */ this.snippets
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Compiles the CollectionConfig template using native CodeTemplate replace blocks.
|
|
109
|
+
* @returns {Promise<string>} Generated TS code for the collection
|
|
110
|
+
*/
|
|
111
|
+
async compile() {
|
|
112
|
+
const templateContent = PayloadCollectionTemplate.template.default
|
|
113
|
+
/** @type {Record<string, string>} */
|
|
114
|
+
const input = {
|
|
115
|
+
collectionSlug: `const collectionSlug = '${this.collectionSlug}'`,
|
|
116
|
+
useAsTitle: `const useAsTitle = '${this.useAsTitle}'`,
|
|
117
|
+
labels: `const labels = ${JSON.stringify(this.labels, null, 2)}`,
|
|
118
|
+
group: `const group = ${JSON.stringify(this.group, null, 2)}`,
|
|
119
|
+
fields: `const fields = ${JSON.stringify(this.fields, null, 2)}`,
|
|
120
|
+
.../** @type {Record<string, string>} */ (this.snippets || {}),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const app = new CodeTemplate({
|
|
124
|
+
template: templateContent,
|
|
125
|
+
input,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const gen = app.run()
|
|
129
|
+
let step = await gen.next()
|
|
130
|
+
while (!step.done) {
|
|
131
|
+
const res = /** @type {import('@nan0web/ui').ResultIntent | undefined} */ (step.value)
|
|
132
|
+
if (res?.data?.output) {
|
|
133
|
+
return res.data.output
|
|
134
|
+
}
|
|
135
|
+
step = await gen.next()
|
|
136
|
+
}
|
|
137
|
+
const finalRes = /** @type {import('@nan0web/ui').ResultIntent | undefined} */ (step.value)
|
|
138
|
+
return finalRes?.data?.output || templateContent
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Synchronously compiles the CollectionConfig template.
|
|
143
|
+
* @returns {string} Generated TS code for the collection
|
|
144
|
+
*/
|
|
145
|
+
compileSync() {
|
|
146
|
+
const templateContent = PayloadCollectionTemplate.template.default
|
|
147
|
+
const groupVal = typeof this.group === 'string' ? `'${this.group}'` : JSON.stringify(this.group, null, 2)
|
|
148
|
+
/** @type {Record<string, string>} */
|
|
149
|
+
const input = {
|
|
150
|
+
collectionSlug: `const collectionSlug = '${this.collectionSlug}'`,
|
|
151
|
+
useAsTitle: `const useAsTitle = '${this.useAsTitle}'`,
|
|
152
|
+
labels: `const labels = ${JSON.stringify(this.labels, null, 2)}`,
|
|
153
|
+
group: `const group = ${groupVal}`,
|
|
154
|
+
fields: `const fields = ${JSON.stringify(this.fields, null, 2)}`,
|
|
155
|
+
.../** @type {Record<string, string>} */ (this.snippets || {}),
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let output = templateContent
|
|
159
|
+
for (const [key, replacement] of Object.entries(input)) {
|
|
160
|
+
const blockRegex = new RegExp(`(\\/\\*\\*\\s*\\n?\\s*\\*\\s*@replace\\s+${key}\\s*\\n?[\\s\\S]*?\\*\\/)([\\s\\S]*?)(\\/\\*\\*\\s*@replace\\s*\\*\\/)`, 'g')
|
|
161
|
+
if (blockRegex.test(output)) {
|
|
162
|
+
output = output.replace(blockRegex, `$1\n${replacement}\n$3`)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return output
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { PayloadCollectionTemplate } from './PayloadCollectionTemplate.js'
|
|
4
|
+
|
|
5
|
+
describe('PayloadCollectionTemplate', () => {
|
|
6
|
+
it('compiles Payload CMS collection code using CodeTemplate', async () => {
|
|
7
|
+
const tpl = new PayloadCollectionTemplate({
|
|
8
|
+
collectionSlug: 'card',
|
|
9
|
+
useAsTitle: 'name',
|
|
10
|
+
labels: { singular: { uk: 'Картка', en: 'Card' }, plural: { uk: 'Картки', en: 'Cards' } },
|
|
11
|
+
group: { uk: 'Продукти', en: 'Products' },
|
|
12
|
+
fields: [
|
|
13
|
+
{ name: 'id', type: 'text', required: true },
|
|
14
|
+
{ name: 'name', type: 'text', localized: true },
|
|
15
|
+
],
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const output = await tpl.compile()
|
|
19
|
+
|
|
20
|
+
assert.ok(output.includes("const collectionSlug = 'card'"))
|
|
21
|
+
assert.ok(output.includes("const useAsTitle = 'name'"))
|
|
22
|
+
assert.ok(output.includes('"uk": "Картка"'))
|
|
23
|
+
assert.ok(output.includes('"name": "id"'))
|
|
24
|
+
|
|
25
|
+
})
|
|
26
|
+
})
|