frappe-ui 1.0.0-beta.68 → 1.0.0-beta.69
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/experimental/Calendar/Calendar.vue +2 -2
- package/experimental/Calendar/stories/CustomHeader.vue +2 -2
- package/package.json +7 -1
- package/scripts/migrate-list-v1.js +436 -0
- package/scripts/migrate-navigation-v1.js +300 -0
- package/scripts/migrate-overlays-v1.js +519 -0
- package/src/components/Combobox/Combobox.cy.ts +2 -6
- package/src/components/Combobox/Combobox.md +4 -0
- package/src/components/Combobox/Combobox.vue +6 -1
- package/src/components/Combobox/types.ts +2 -0
- package/src/components/ContextMenu/ContextMenu.api.md +36 -0
- package/src/components/ContextMenu/ContextMenu.cy.ts +141 -29
- package/src/components/ContextMenu/ContextMenu.md +5 -0
- package/src/components/ContextMenu/ContextMenu.vue +81 -8
- package/src/components/ContextMenu/types.ts +19 -7
- package/src/components/DatePicker/DatePicker.api.md +0 -7
- package/src/components/DatePicker/DatePicker.cy.ts +11 -15
- package/src/components/DatePicker/DatePicker.md +4 -0
- package/src/components/DatePicker/DatePicker.vue +17 -6
- package/src/components/DatePicker/DateRangePicker.cy.ts +22 -8
- package/src/components/DatePicker/DateRangePicker.vue +4 -1
- package/src/components/DatePicker/DateTimePicker.cy.ts +4 -4
- package/src/components/DatePicker/DateTimePicker.vue +5 -3
- package/src/components/DatePicker/stories/Examples.vue +2 -2
- package/src/components/DatePicker/stories/Range.vue +3 -3
- package/src/components/DatePicker/types.ts +15 -5
- package/src/components/DesktopShell/stories/Default.vue +1 -1
- package/src/components/Dropdown/Dropdown.api.md +30 -0
- package/src/components/Dropdown/Dropdown.cy.ts +23 -3
- package/src/components/Dropdown/Dropdown.md +3 -0
- package/src/components/Dropdown/Dropdown.vue +11 -4
- package/src/components/Dropdown/types.ts +20 -9
- package/src/components/HoverCard/HoverCard.api.md +5 -5
- package/src/components/HoverCard/HoverCard.cy.ts +88 -1
- package/src/components/HoverCard/HoverCard.md +2 -2
- package/src/components/HoverCard/HoverCard.vue +65 -18
- package/src/components/HoverCard/index.ts +1 -0
- package/src/components/HoverCard/stories/Delays.vue +1 -1
- package/src/components/HoverCard/types.ts +11 -4
- package/src/components/Menu/Menu.api.md +1 -1
- package/src/components/Menu/Menu.vue +3 -3
- package/src/components/Menu/MenuItemContent.vue +2 -2
- package/src/components/Menu/types.test.ts +49 -0
- package/src/components/Menu/types.ts +9 -10
- package/src/components/MobileNav/MobileNavItem.vue +5 -20
- package/src/components/MultiSelect/MultiSelect.cy.ts +4 -1
- package/src/components/MultiSelect/MultiSelect.md +2 -2
- package/src/components/MultiSelect/MultiSelect.vue +6 -1
- package/src/components/MultiSelect/types.ts +2 -0
- package/src/components/Popover/Popover.api.md +5 -5
- package/src/components/Popover/Popover.cy.ts +9 -6
- package/src/components/Popover/Popover.md +3 -3
- package/src/components/Popover/Popover.vue +4 -9
- package/src/components/Popover/types.ts +2 -2
- package/src/components/Select/Select.cy.ts +2 -6
- package/src/components/Select/Select.md +2 -2
- package/src/components/Select/Select.vue +6 -0
- package/src/components/Select/types.ts +2 -0
- package/src/components/SettingsDialog/SettingsDialog.api.md +4 -4
- package/src/components/SettingsDialog/SettingsDialog.cy.ts +84 -1
- package/src/components/SettingsDialog/SettingsDialog.vue +15 -15
- package/src/components/SettingsDialog/index.ts +1 -0
- package/src/components/SettingsDialog/types.ts +3 -2
- package/src/components/Sidebar/Sidebar.api.md +5 -4
- package/src/components/Sidebar/Sidebar.cy.ts +22 -8
- package/src/components/Sidebar/Sidebar.md +2 -2
- package/src/components/Sidebar/Sidebar.vue +2 -2
- package/src/components/Sidebar/SidebarItem.vue +2 -2
- package/src/components/Sidebar/stories/Default.vue +1 -1
- package/src/components/Sidebar/stories/Section.vue +1 -1
- package/src/components/Sidebar/types.ts +4 -7
- package/src/components/SidebarRail/SidebarRail.api.md +3 -3
- package/src/components/SidebarRail/SidebarRail.cy.ts +88 -14
- package/src/components/SidebarRail/SidebarRail.md +4 -2
- package/src/components/SidebarRail/SidebarRailItem.vue +46 -41
- package/src/components/SidebarRail/types.ts +2 -2
- package/src/components/TabButtons/TabButtons.api.md +2 -2
- package/src/components/TabButtons/TabButtons.cy.ts +6 -6
- package/src/components/TabButtons/TabButtons.md +1 -1
- package/src/components/TabButtons/TabButtons.vue +4 -4
- package/src/components/TabButtons/types.ts +2 -2
- package/src/components/Tabs/TabTrigger.vue +2 -4
- package/src/components/Tabs/Tabs.cy.ts +3 -3
- package/src/components/Tabs/Tabs.md +1 -1
- package/src/components/Tabs/types.ts +1 -1
- package/src/components/TimePicker/TimePicker.api.md +1 -1
- package/src/components/TimePicker/TimePicker.cy.ts +48 -62
- package/src/components/TimePicker/TimePicker.md +4 -1
- package/src/components/TimePicker/TimePicker.vue +27 -15
- package/src/components/Tooltip/Tooltip.api.md +6 -6
- package/src/components/Tooltip/Tooltip.cy.ts +5 -5
- package/src/components/Tooltip/Tooltip.playground.vue +2 -2
- package/src/components/Tooltip/Tooltip.vue +6 -4
- package/src/components/Tooltip/TooltipProvider.vue +5 -5
- package/src/components/Tooltip/stories/Group.vue +1 -1
- package/src/components/Tooltip/types.ts +1 -1
- package/src/components/shared/picker/PickerShell.vue +11 -14
- package/src/components/shared/picker/types.ts +7 -3
- package/src/composables/useKeyboardShortcut.spec.ts +40 -1
- package/src/composables/useKeyboardShortcut.ts +19 -10
- package/src/molecules/list/List.cy.ts +146 -11
- package/src/molecules/list/ListGroup.vue +5 -5
- package/src/molecules/list/ListHeaderCellSort.vue +11 -9
- package/src/molecules/list/ListRowBase.vue +2 -2
- package/src/molecules/list/ListRows.vue +41 -18
- package/src/molecules/list/index.ts +0 -4
- package/src/molecules/list/list.api.md +14 -8
- package/src/molecules/list/list.md +17 -10
- package/src/molecules/list/stories/Columns.vue +3 -3
- package/src/molecules/list/stories/Feed.vue +5 -2
- package/src/molecules/list/style.css +2 -2
- package/src/molecules/list/types.ts +0 -8
- package/src/molecules/list/useVirtualRows.ts +46 -27
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
@update:modelValue="(val) => onMonthYearChange(val)"
|
|
28
28
|
:clearable="false"
|
|
29
29
|
>
|
|
30
|
-
<template #trigger="{
|
|
30
|
+
<template #trigger="{ open, setOpen }">
|
|
31
31
|
<Button
|
|
32
32
|
variant="ghost"
|
|
33
33
|
class="text-lg-medium text-ink-gray-7"
|
|
34
34
|
:label="currentMonthYear"
|
|
35
35
|
iconRight="lucide-chevron-down"
|
|
36
|
-
@click="
|
|
36
|
+
@click="setOpen(!open)"
|
|
37
37
|
/>
|
|
38
38
|
</template>
|
|
39
39
|
</DatePicker>
|
|
@@ -110,13 +110,13 @@ const events = ref([
|
|
|
110
110
|
@update:modelValue="(val) => headerProps.onMonthYearChange(val)"
|
|
111
111
|
:clearable="false"
|
|
112
112
|
>
|
|
113
|
-
<template #trigger="{
|
|
113
|
+
<template #trigger="{ setOpen, open }">
|
|
114
114
|
<Button
|
|
115
115
|
variant="ghost"
|
|
116
116
|
class="text-lg-medium text-ink-gray-7"
|
|
117
117
|
:label="headerProps.currentMonthYear"
|
|
118
118
|
iconRight="lucide-chevron-down"
|
|
119
|
-
@click="
|
|
119
|
+
@click="setOpen(!open)"
|
|
120
120
|
/>
|
|
121
121
|
</template>
|
|
122
122
|
</DatePicker>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-ui",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.69",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.19.0"
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
},
|
|
21
21
|
"bin": {
|
|
22
22
|
"base-props-v1": "./scripts/migrate-base-props-v1.js",
|
|
23
|
+
"list-v1": "./scripts/migrate-list-v1.js",
|
|
24
|
+
"navigation-v1": "./scripts/migrate-navigation-v1.js",
|
|
25
|
+
"overlays-v1": "./scripts/migrate-overlays-v1.js",
|
|
23
26
|
"tokens-v2": "./tailwind/migrate-tokens-v2.js",
|
|
24
27
|
"shortcuts-v1": "./scripts/migrate-shortcuts-v1.js",
|
|
25
28
|
"editor-v1": "./scripts/migrate-editor-v1.js",
|
|
@@ -107,6 +110,9 @@
|
|
|
107
110
|
"scripts/migrate-shortcuts-v1.js",
|
|
108
111
|
"scripts/migrate-base-props-v1.js",
|
|
109
112
|
"scripts/migrate-editor-v1.js",
|
|
113
|
+
"scripts/migrate-list-v1.js",
|
|
114
|
+
"scripts/migrate-navigation-v1.js",
|
|
115
|
+
"scripts/migrate-overlays-v1.js",
|
|
110
116
|
"experimental",
|
|
111
117
|
"experimental.ts",
|
|
112
118
|
"tsconfig.base.json"
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Migrate the list family's pre-v1 row selectors and slot names.
|
|
4
|
+
*
|
|
5
|
+
* Usage: list-v1 [--dry-run] <dir-or-file...>
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
|
|
11
|
+
const USAGE = 'Usage: list-v1 [--dry-run] <dir-or-file...>'
|
|
12
|
+
const EXTENSIONS = new Set([
|
|
13
|
+
'.css',
|
|
14
|
+
'.html',
|
|
15
|
+
'.js',
|
|
16
|
+
'.jsx',
|
|
17
|
+
'.md',
|
|
18
|
+
'.ts',
|
|
19
|
+
'.tsx',
|
|
20
|
+
'.vue',
|
|
21
|
+
])
|
|
22
|
+
const COMPONENTS = new Set(['ListGroup', 'ListHeaderCellSort', 'ListRow'])
|
|
23
|
+
const VOID_ELEMENTS = new Set([
|
|
24
|
+
'area',
|
|
25
|
+
'base',
|
|
26
|
+
'br',
|
|
27
|
+
'col',
|
|
28
|
+
'embed',
|
|
29
|
+
'hr',
|
|
30
|
+
'img',
|
|
31
|
+
'input',
|
|
32
|
+
'link',
|
|
33
|
+
'meta',
|
|
34
|
+
'source',
|
|
35
|
+
'track',
|
|
36
|
+
'wbr',
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
const kebab = (name) =>
|
|
40
|
+
name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
|
41
|
+
|
|
42
|
+
function knownComponent(name) {
|
|
43
|
+
return [...COMPONENTS].find(
|
|
44
|
+
(component) => name === component || name === kebab(component),
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function lineAt(source, index) {
|
|
49
|
+
return source.slice(0, index).split('\n').length
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Only the start of an import has to be code. Its module string is expected to
|
|
53
|
+
// be masked, and checking every character would reject every real import.
|
|
54
|
+
function codeMask(source, { templateStrings = true } = {}) {
|
|
55
|
+
const mask = new Uint8Array(source.length)
|
|
56
|
+
let i = 0
|
|
57
|
+
while (i < source.length) {
|
|
58
|
+
const char = source[i]
|
|
59
|
+
const next = source[i + 1]
|
|
60
|
+
if (char === '/' && next === '/') {
|
|
61
|
+
const end = source.indexOf('\n', i + 2)
|
|
62
|
+
const stop = end === -1 ? source.length : end
|
|
63
|
+
mask.fill(1, i, stop)
|
|
64
|
+
i = stop
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
if (char === '/' && next === '*') {
|
|
68
|
+
const end = source.indexOf('*/', i + 2)
|
|
69
|
+
const stop = end === -1 ? source.length : end + 2
|
|
70
|
+
mask.fill(1, i, stop)
|
|
71
|
+
i = stop
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
char === "'" ||
|
|
76
|
+
char === '"' ||
|
|
77
|
+
(templateStrings && char === '`')
|
|
78
|
+
) {
|
|
79
|
+
const quote = char
|
|
80
|
+
mask[i] = 1
|
|
81
|
+
i += 1
|
|
82
|
+
while (i < source.length) {
|
|
83
|
+
mask[i] = 1
|
|
84
|
+
if (source[i] === '\\') {
|
|
85
|
+
if (i + 1 < source.length) mask[i + 1] = 1
|
|
86
|
+
i += 2
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
if (source[i] === quote) {
|
|
90
|
+
i += 1
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
i += 1
|
|
94
|
+
}
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
i += 1
|
|
98
|
+
}
|
|
99
|
+
return mask
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function importedBindings(clause) {
|
|
103
|
+
const bindings = []
|
|
104
|
+
const trimmed = clause.trim()
|
|
105
|
+
if (!trimmed.startsWith('{') && !trimmed.startsWith('*')) {
|
|
106
|
+
const name = trimmed.split(',')[0].trim()
|
|
107
|
+
if (name) bindings.push({ imported: 'default', local: name })
|
|
108
|
+
}
|
|
109
|
+
const named = /\{([\s\S]*?)\}/.exec(clause)?.[1]
|
|
110
|
+
if (!named) return bindings
|
|
111
|
+
for (const part of named.split(',')) {
|
|
112
|
+
const match = /^\s*(?:type\s+)?([\w$]+)(?:\s+as\s+([\w$]+))?\s*$/.exec(part)
|
|
113
|
+
if (match)
|
|
114
|
+
bindings.push({ imported: match[1], local: match[2] || match[1] })
|
|
115
|
+
}
|
|
116
|
+
return bindings
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function componentAliases(source, { markdown = false } = {}) {
|
|
120
|
+
const aliases = new Map()
|
|
121
|
+
|
|
122
|
+
const mask = codeMask(source, { templateStrings: !markdown })
|
|
123
|
+
const imports = /\bimport\s+([\s\S]*?)\s+from\s*(['"])([^'"]+)\2/g
|
|
124
|
+
for (const match of source.matchAll(imports)) {
|
|
125
|
+
if (mask[match.index]) continue
|
|
126
|
+
const moduleName = match[3]
|
|
127
|
+
for (const binding of importedBindings(match[1])) {
|
|
128
|
+
if (moduleName === 'frappe-ui/list' && COMPONENTS.has(binding.imported)) {
|
|
129
|
+
aliases.set(binding.local, binding.imported)
|
|
130
|
+
aliases.set(kebab(binding.local), binding.imported)
|
|
131
|
+
} else if (binding.local && COMPONENTS.has(binding.local)) {
|
|
132
|
+
// A local component with the same name shadows a global registration.
|
|
133
|
+
aliases.delete(binding.local)
|
|
134
|
+
aliases.delete(kebab(binding.local))
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const name of COMPONENTS) {
|
|
140
|
+
const declaration = new RegExp(
|
|
141
|
+
`\\b(?:const|let|var|class|function)\\s+${name}\\b`,
|
|
142
|
+
'g',
|
|
143
|
+
)
|
|
144
|
+
let match
|
|
145
|
+
while ((match = declaration.exec(source))) {
|
|
146
|
+
if (mask[match.index]) continue
|
|
147
|
+
aliases.delete(name)
|
|
148
|
+
aliases.delete(kebab(name))
|
|
149
|
+
break
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return aliases
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function findTagEnd(source, start) {
|
|
156
|
+
let quote
|
|
157
|
+
for (let i = start; i < source.length; i += 1) {
|
|
158
|
+
const char = source[i]
|
|
159
|
+
if (quote) {
|
|
160
|
+
if (char === quote && source[i - 1] !== '\\') quote = undefined
|
|
161
|
+
} else if (char === "'" || char === '"') {
|
|
162
|
+
quote = char
|
|
163
|
+
} else if (char === '>') {
|
|
164
|
+
return i
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return -1
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function replaceStaticSlot(tag, from, to) {
|
|
171
|
+
let migrated = tag
|
|
172
|
+
.replace(new RegExp(`(^|\\s)#${from}(?=[.\\s=/>])`, 'g'), `$1#${to}`)
|
|
173
|
+
.replace(
|
|
174
|
+
new RegExp(`(^|\\s)v-slot:${from}(?=[.\\s=/>])`, 'g'),
|
|
175
|
+
`$1v-slot:${to}`,
|
|
176
|
+
)
|
|
177
|
+
migrated = migrated.replace(
|
|
178
|
+
new RegExp(`(\\sslot\\s*=\\s*)(['"])${from}\\2`, 'g'),
|
|
179
|
+
`$1$2${to}$2`,
|
|
180
|
+
)
|
|
181
|
+
return migrated
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function replaceListRowUtilities(tag) {
|
|
185
|
+
return tag
|
|
186
|
+
.replaceAll('data-[state=selected]', 'data-[selected]')
|
|
187
|
+
.replaceAll('data-[active]', 'data-[state=active]')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function migrateMarkup(source, aliases) {
|
|
191
|
+
let migrated = ''
|
|
192
|
+
let cursor = 0
|
|
193
|
+
const stack = []
|
|
194
|
+
const refusals = []
|
|
195
|
+
|
|
196
|
+
while (cursor < source.length) {
|
|
197
|
+
const start = source.indexOf('<', cursor)
|
|
198
|
+
if (start === -1) break
|
|
199
|
+
migrated += source.slice(cursor, start)
|
|
200
|
+
if (source.startsWith('<!--', start)) {
|
|
201
|
+
const commentEnd = source.indexOf('-->', start + 4)
|
|
202
|
+
const next = commentEnd === -1 ? source.length : commentEnd + 3
|
|
203
|
+
migrated += source.slice(start, next)
|
|
204
|
+
cursor = next
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
const remainder = source.slice(start)
|
|
208
|
+
const close = /^<\/([A-Za-z][\w.-]*)/.exec(remainder)
|
|
209
|
+
const open = /^<([A-Za-z][\w.-]*)/.exec(remainder)
|
|
210
|
+
if (!close && !open) {
|
|
211
|
+
migrated += '<'
|
|
212
|
+
cursor = start + 1
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
const end = findTagEnd(source, start + 1)
|
|
216
|
+
if (end === -1) {
|
|
217
|
+
migrated += source.slice(start)
|
|
218
|
+
refusals.push({
|
|
219
|
+
line: lineAt(source, start),
|
|
220
|
+
message: `unterminated <${(close || open)[1]}> tag must be checked manually`,
|
|
221
|
+
})
|
|
222
|
+
cursor = source.length
|
|
223
|
+
break
|
|
224
|
+
}
|
|
225
|
+
const tag = source.slice(start, end + 1)
|
|
226
|
+
if (close) {
|
|
227
|
+
const at = stack.map((entry) => entry.name).lastIndexOf(close[1])
|
|
228
|
+
if (at !== -1) stack.splice(at)
|
|
229
|
+
migrated += tag
|
|
230
|
+
cursor = end + 1
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const name = open[1]
|
|
235
|
+
if (name === 'script' || name === 'style') {
|
|
236
|
+
const close = new RegExp(`</${name}\\s*>`, 'gi')
|
|
237
|
+
close.lastIndex = end + 1
|
|
238
|
+
const match = close.exec(source)
|
|
239
|
+
const next = match ? close.lastIndex : source.length
|
|
240
|
+
migrated += source.slice(start, next)
|
|
241
|
+
cursor = next
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
const component = aliases.get(name)
|
|
245
|
+
const ambiguousComponent = component ? undefined : knownComponent(name)
|
|
246
|
+
let nextTag = tag
|
|
247
|
+
if (component === 'ListRow') nextTag = replaceListRowUtilities(nextTag)
|
|
248
|
+
if (
|
|
249
|
+
ambiguousComponent === 'ListRow' &&
|
|
250
|
+
/data-\[(?:active|state=selected)\]/.test(tag)
|
|
251
|
+
) {
|
|
252
|
+
refusals.push({
|
|
253
|
+
line: lineAt(source, start),
|
|
254
|
+
message:
|
|
255
|
+
'<ListRow> state utility found but no frappe-ui/list import; check by hand',
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const parentEntry = stack.at(-1)
|
|
260
|
+
const parent = parentEntry?.component
|
|
261
|
+
const ambiguousParent = parentEntry?.ambiguousComponent
|
|
262
|
+
if (name === 'template' && (parent || ambiguousParent)) {
|
|
263
|
+
if (/#\[|v-slot:\[/.test(tag)) {
|
|
264
|
+
const owner = parent || ambiguousParent
|
|
265
|
+
if (owner === 'ListGroup' || owner === 'ListHeaderCellSort') {
|
|
266
|
+
refusals.push({
|
|
267
|
+
line: lineAt(source, start),
|
|
268
|
+
message: `dynamic slot under <${owner}> must be checked manually`,
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
} else if (parent === 'ListGroup') {
|
|
272
|
+
nextTag = replaceStaticSlot(nextTag, 'header', 'label')
|
|
273
|
+
} else if (parent === 'ListHeaderCellSort') {
|
|
274
|
+
nextTag = replaceStaticSlot(nextTag, 'suffix', 'sort-indicator')
|
|
275
|
+
} else if (
|
|
276
|
+
ambiguousParent === 'ListGroup' &&
|
|
277
|
+
/(?:#|v-slot:)header(?=[.\s=/>])|slot\s*=\s*(['"])header\1/.test(tag)
|
|
278
|
+
) {
|
|
279
|
+
refusals.push({
|
|
280
|
+
line: lineAt(source, start),
|
|
281
|
+
message:
|
|
282
|
+
'<ListGroup> #header found but no frappe-ui/list import; check by hand',
|
|
283
|
+
})
|
|
284
|
+
} else if (
|
|
285
|
+
ambiguousParent === 'ListHeaderCellSort' &&
|
|
286
|
+
/(?:#|v-slot:)suffix(?=[.\s=/>])|slot\s*=\s*(['"])suffix\1/.test(tag)
|
|
287
|
+
) {
|
|
288
|
+
refusals.push({
|
|
289
|
+
line: lineAt(source, start),
|
|
290
|
+
message:
|
|
291
|
+
'<ListHeaderCellSort> #suffix found but no frappe-ui/list import; check by hand',
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
migrated += nextTag
|
|
297
|
+
if (
|
|
298
|
+
!/\/\s*>$/.test(tag) &&
|
|
299
|
+
!VOID_ELEMENTS.has(name.toLowerCase())
|
|
300
|
+
) {
|
|
301
|
+
stack.push({ name, component, ambiguousComponent })
|
|
302
|
+
}
|
|
303
|
+
cursor = end + 1
|
|
304
|
+
}
|
|
305
|
+
migrated += source.slice(cursor)
|
|
306
|
+
return { migrated, refusals }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const LIST_ROW_SLOT = String.raw`\[data-slot\s*=\s*(?:["']?list-row["']?)\]`
|
|
310
|
+
const OLD_SELECTED = String.raw`\[data-state\s*=\s*(?:["']?selected["']?)\]`
|
|
311
|
+
|
|
312
|
+
function migrateAnchoredSelectors(source) {
|
|
313
|
+
return source
|
|
314
|
+
.replace(
|
|
315
|
+
new RegExp(`(${LIST_ROW_SLOT})(${OLD_SELECTED})`, 'g'),
|
|
316
|
+
'$1[data-selected]',
|
|
317
|
+
)
|
|
318
|
+
.replace(
|
|
319
|
+
new RegExp(`(${OLD_SELECTED})(${LIST_ROW_SLOT})`, 'g'),
|
|
320
|
+
'[data-selected]$2',
|
|
321
|
+
)
|
|
322
|
+
.replace(
|
|
323
|
+
new RegExp(`(${LIST_ROW_SLOT})\\[data-active\\]`, 'g'),
|
|
324
|
+
"$1[data-state='active']",
|
|
325
|
+
)
|
|
326
|
+
.replace(
|
|
327
|
+
new RegExp(`\\[data-active\\](${LIST_ROW_SLOT})`, 'g'),
|
|
328
|
+
"[data-state='active']$1",
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function migrateList(source, options = {}) {
|
|
333
|
+
const aliases = componentAliases(source, options)
|
|
334
|
+
const selectorsMigrated = migrateAnchoredSelectors(source)
|
|
335
|
+
return options.markup === false
|
|
336
|
+
? { migrated: selectorsMigrated, refusals: [] }
|
|
337
|
+
: migrateMarkup(selectorsMigrated, aliases)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function filesIn(target, seen = new Set()) {
|
|
341
|
+
let stat
|
|
342
|
+
try {
|
|
343
|
+
stat = fs.lstatSync(target)
|
|
344
|
+
} catch (error) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
`${target}: ${error.code === 'ENOENT' ? 'not found' : error.message}`,
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
if (stat.isSymbolicLink()) return []
|
|
350
|
+
if (stat.isFile()) {
|
|
351
|
+
// Migration guides intentionally contain the old spelling in "before"
|
|
352
|
+
// examples. Rewriting them makes the guide false and prevents idempotent
|
|
353
|
+
// own-tree runs.
|
|
354
|
+
if (path.basename(target).toLowerCase() === 'migration.md') return []
|
|
355
|
+
return EXTENSIONS.has(path.extname(target)) ? [target] : []
|
|
356
|
+
}
|
|
357
|
+
if (!stat.isDirectory()) return []
|
|
358
|
+
const real = fs.realpathSync(target)
|
|
359
|
+
if (seen.has(real)) return []
|
|
360
|
+
seen.add(real)
|
|
361
|
+
return fs
|
|
362
|
+
.readdirSync(target, { withFileTypes: true })
|
|
363
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
364
|
+
.flatMap((entry) => {
|
|
365
|
+
if (['node_modules', '.git', 'dist', 'build'].includes(entry.name))
|
|
366
|
+
return []
|
|
367
|
+
return filesIn(path.join(target, entry.name), seen)
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function run(argv) {
|
|
372
|
+
const dryRun = argv.includes('--dry-run')
|
|
373
|
+
const unknown = argv.find((arg) => arg.startsWith('-') && arg !== '--dry-run')
|
|
374
|
+
if (unknown) {
|
|
375
|
+
console.error(`Unknown option: ${unknown}\n${USAGE}`)
|
|
376
|
+
return 1
|
|
377
|
+
}
|
|
378
|
+
const targets = argv.filter((arg) => arg !== '--dry-run')
|
|
379
|
+
if (!targets.length) {
|
|
380
|
+
console.error(USAGE)
|
|
381
|
+
return 1
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let files
|
|
385
|
+
try {
|
|
386
|
+
files = [
|
|
387
|
+
...new Set(targets.flatMap((target) => filesIn(path.resolve(target)))),
|
|
388
|
+
]
|
|
389
|
+
} catch (error) {
|
|
390
|
+
console.error(error.message)
|
|
391
|
+
return 1
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
let changed = 0
|
|
395
|
+
let refusalCount = 0
|
|
396
|
+
for (const file of files) {
|
|
397
|
+
const source = fs.readFileSync(file, 'utf8')
|
|
398
|
+
const extension = path.extname(file).toLowerCase()
|
|
399
|
+
const result = migrateList(source, {
|
|
400
|
+
markdown: extension === '.md',
|
|
401
|
+
markup: ['.html', '.md', '.vue'].includes(extension),
|
|
402
|
+
})
|
|
403
|
+
if (result.refusals.length) {
|
|
404
|
+
refusalCount += result.refusals.length
|
|
405
|
+
for (const refusal of result.refusals) {
|
|
406
|
+
console.error(`${file}:${refusal.line}: ${refusal.message}`)
|
|
407
|
+
}
|
|
408
|
+
continue
|
|
409
|
+
}
|
|
410
|
+
if (result.migrated === source) continue
|
|
411
|
+
changed += 1
|
|
412
|
+
if (!dryRun) fs.writeFileSync(file, result.migrated)
|
|
413
|
+
console.log(`${dryRun ? 'Would migrate' : 'Migrated'} ${file}`)
|
|
414
|
+
}
|
|
415
|
+
console.log(
|
|
416
|
+
`${dryRun ? 'Would migrate' : 'Migrated'} ${changed} file${changed === 1 ? '' : 's'}.`,
|
|
417
|
+
)
|
|
418
|
+
if (refusalCount) {
|
|
419
|
+
console.error(
|
|
420
|
+
`Refused ${refusalCount} dynamic slot${refusalCount === 1 ? '' : 's'}; no refused file was changed.`,
|
|
421
|
+
)
|
|
422
|
+
return 1
|
|
423
|
+
}
|
|
424
|
+
return 0
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const script = fileURLToPath(import.meta.url)
|
|
428
|
+
let isMain = false
|
|
429
|
+
if (process.argv[1]) {
|
|
430
|
+
try {
|
|
431
|
+
isMain = fs.realpathSync(process.argv[1]) === fs.realpathSync(script)
|
|
432
|
+
} catch {
|
|
433
|
+
isMain = false
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (isMain) process.exitCode = run(process.argv.slice(2))
|