@planningcenter/tapestry-migration-cli 3.4.1-rc.8 → 3.4.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/dist/tapestry-react-shim.cjs +34 -64
- package/package.json +3 -3
- package/src/components/dropdown/index.test.ts +177 -4
- package/src/components/dropdown/index.ts +24 -2
- package/src/components/dropdown/transforms/auditSpreadProps.test.ts +110 -0
- package/src/components/dropdown/transforms/auditSpreadProps.ts +10 -0
- package/src/components/dropdown/transforms/dividerToSeparator.test.ts +134 -0
- package/src/components/dropdown/transforms/dividerToSeparator.ts +67 -0
- package/src/components/dropdown/transforms/itemToAction.test.ts +19 -1
- package/src/components/dropdown/transforms/itemToAction.ts +14 -2
- package/src/components/dropdown/transforms/linkToLink.test.ts +19 -1
- package/src/components/dropdown/transforms/linkToLink.ts +13 -2
- package/src/components/dropdown/transforms/moveDropdownImport.test.ts +93 -0
- package/src/components/dropdown/transforms/moveDropdownImport.ts +13 -0
- package/src/components/dropdown/transforms/placementIdToMenu.test.ts +135 -0
- package/src/components/dropdown/transforms/placementIdToMenu.ts +96 -0
- package/src/components/dropdown/transforms/unsupportedProps.test.ts +145 -0
- package/src/components/dropdown/transforms/unsupportedProps.ts +12 -0
- package/src/components/dropdown/transforms/unsupportedPropsDivider.test.ts +143 -0
- package/src/components/dropdown/transforms/unsupportedPropsDivider.ts +26 -0
- package/src/components/dropdown/transforms/unsupportedPropsItem.test.ts +123 -0
- package/src/components/dropdown/transforms/unsupportedPropsItem.ts +12 -0
- package/src/components/dropdown/transforms/unsupportedPropsLink.test.ts +107 -0
- package/src/components/dropdown/transforms/unsupportedPropsLink.ts +12 -0
- package/src/components/dropdown/transforms/wrapMenu.test.ts +153 -0
- package/src/components/dropdown/transforms/wrapMenu.ts +54 -0
- package/src/components/dropdown/transforms/wrapTrigger.test.ts +283 -0
- package/src/components/dropdown/transforms/wrapTrigger.ts +98 -0
- package/src/components/input/transforms/unsupportedProps.test.ts +14 -13
- package/src/components/shared/conditions/isChildOf.test.ts +89 -0
- package/src/components/shared/conditions/isChildOf.ts +43 -0
- package/src/components/shared/helpers/unsupportedPropsHelpers.ts +36 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./wrapTrigger"
|
|
5
|
+
|
|
6
|
+
const j = jscodeshift.withParser("tsx")
|
|
7
|
+
|
|
8
|
+
function applyTransform(source: string): string | null {
|
|
9
|
+
const fileInfo = { path: "test.tsx", source }
|
|
10
|
+
return transform(
|
|
11
|
+
fileInfo,
|
|
12
|
+
{ j, jscodeshift: j, report: () => {}, stats: () => {} },
|
|
13
|
+
{}
|
|
14
|
+
) as string | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("wrapTrigger transform", () => {
|
|
18
|
+
describe("title prop", () => {
|
|
19
|
+
it("converts title string to DropdownTrigger + DropdownButton", () => {
|
|
20
|
+
const input = `
|
|
21
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
22
|
+
|
|
23
|
+
export default function Test() {
|
|
24
|
+
return (
|
|
25
|
+
<Dropdown title="Actions">
|
|
26
|
+
<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
|
|
27
|
+
</Dropdown>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
`.trim()
|
|
31
|
+
|
|
32
|
+
const result = applyTransform(input)
|
|
33
|
+
expect(result).toContain("<DropdownTrigger>")
|
|
34
|
+
expect(result).toContain("</DropdownTrigger>")
|
|
35
|
+
expect(result).toContain('<DropdownButton label="Actions"')
|
|
36
|
+
expect(result).not.toContain('title="Actions"')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("converts dynamic title expression to DropdownButton label", () => {
|
|
40
|
+
const input = `
|
|
41
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
42
|
+
|
|
43
|
+
export default function Test() {
|
|
44
|
+
return (
|
|
45
|
+
<Dropdown title={menuTitle}>
|
|
46
|
+
<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
|
|
47
|
+
</Dropdown>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
`.trim()
|
|
51
|
+
|
|
52
|
+
const result = applyTransform(input)
|
|
53
|
+
expect(result).toContain("DropdownButton")
|
|
54
|
+
expect(result).toContain("label={menuTitle}")
|
|
55
|
+
expect(result).not.toContain("title=")
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("preserves existing Dropdown children after trigger insertion", () => {
|
|
59
|
+
const input = `
|
|
60
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
61
|
+
|
|
62
|
+
export default function Test() {
|
|
63
|
+
return (
|
|
64
|
+
<Dropdown title="Actions">
|
|
65
|
+
<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
|
|
66
|
+
<Dropdown.Item onSelect={fn2}>Delete</Dropdown.Item>
|
|
67
|
+
</Dropdown>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
`.trim()
|
|
71
|
+
|
|
72
|
+
const result = applyTransform(input)
|
|
73
|
+
expect(result).toContain("<DropdownTrigger>")
|
|
74
|
+
expect(result).toContain("Dropdown.Item")
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe("triggerElement prop", () => {
|
|
79
|
+
it("wraps triggerElement JSX value in DropdownTrigger", () => {
|
|
80
|
+
const input = `
|
|
81
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
82
|
+
|
|
83
|
+
export default function Test() {
|
|
84
|
+
return (
|
|
85
|
+
<Dropdown triggerElement={<DropdownButton label="Actions" />}>
|
|
86
|
+
<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
|
|
87
|
+
</Dropdown>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
`.trim()
|
|
91
|
+
|
|
92
|
+
const result = applyTransform(input)
|
|
93
|
+
expect(result).toContain("<DropdownTrigger>")
|
|
94
|
+
expect(result).toContain("</DropdownTrigger>")
|
|
95
|
+
expect(result).toContain("DropdownButton")
|
|
96
|
+
expect(result).not.toContain("triggerElement=")
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("triggerElement takes precedence over title", () => {
|
|
100
|
+
const input = `
|
|
101
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
102
|
+
|
|
103
|
+
export default function Test() {
|
|
104
|
+
return (
|
|
105
|
+
<Dropdown title="Ignored" triggerElement={<DropdownButton label="Used" />}>
|
|
106
|
+
<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
|
|
107
|
+
</Dropdown>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
`.trim()
|
|
111
|
+
|
|
112
|
+
const result = applyTransform(input)
|
|
113
|
+
expect(result).toContain('label="Used"')
|
|
114
|
+
expect(result).not.toContain("title=")
|
|
115
|
+
expect(result).not.toContain("triggerElement=")
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe("attribute preservation", () => {
|
|
120
|
+
it("preserves other Dropdown props", () => {
|
|
121
|
+
const input = `
|
|
122
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
123
|
+
|
|
124
|
+
export default function Test() {
|
|
125
|
+
return (
|
|
126
|
+
<Dropdown title="Actions" onClose={handleClose} placement="bottom-end">
|
|
127
|
+
<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
|
|
128
|
+
</Dropdown>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
`.trim()
|
|
132
|
+
|
|
133
|
+
const result = applyTransform(input)
|
|
134
|
+
expect(result).toContain("onClose={handleClose}")
|
|
135
|
+
expect(result).toContain('placement="bottom-end"')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it("does not touch the tapestry-react import statement", () => {
|
|
139
|
+
const input = `
|
|
140
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
141
|
+
|
|
142
|
+
export default function Test() {
|
|
143
|
+
return (
|
|
144
|
+
<Dropdown title="Actions">
|
|
145
|
+
<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
|
|
146
|
+
</Dropdown>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
`.trim()
|
|
150
|
+
|
|
151
|
+
const result = applyTransform(input)
|
|
152
|
+
expect(result).toContain(
|
|
153
|
+
'import { Dropdown } from "@planningcenter/tapestry-react"'
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it("adds DropdownTrigger and DropdownButton imports from tapestry when title is used", () => {
|
|
158
|
+
const input = `
|
|
159
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
160
|
+
|
|
161
|
+
export default function Test() {
|
|
162
|
+
return (
|
|
163
|
+
<Dropdown title="Actions">
|
|
164
|
+
<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
|
|
165
|
+
</Dropdown>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
`.trim()
|
|
169
|
+
|
|
170
|
+
const result = applyTransform(input)
|
|
171
|
+
expect(result).toContain('from "@planningcenter/tapestry"')
|
|
172
|
+
expect(result).toContain("DropdownTrigger")
|
|
173
|
+
expect(result).toContain("DropdownButton")
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it("adds only DropdownTrigger import when triggerElement is provided", () => {
|
|
177
|
+
const input = `
|
|
178
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
179
|
+
|
|
180
|
+
export default function Test() {
|
|
181
|
+
return (
|
|
182
|
+
<Dropdown triggerElement={<MyButton />}>
|
|
183
|
+
<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
|
|
184
|
+
</Dropdown>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
`.trim()
|
|
188
|
+
|
|
189
|
+
const result = applyTransform(input)
|
|
190
|
+
const tapestryImport =
|
|
191
|
+
result?.match(/import {[^}]+} from "@planningcenter\/tapestry"/)?.[0] ??
|
|
192
|
+
""
|
|
193
|
+
expect(tapestryImport).toContain("DropdownTrigger")
|
|
194
|
+
expect(tapestryImport).not.toContain("DropdownButton")
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe("multiple dropdowns", () => {
|
|
199
|
+
it("transforms all Dropdown elements in the file", () => {
|
|
200
|
+
const input = `
|
|
201
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
202
|
+
|
|
203
|
+
export default function Test() {
|
|
204
|
+
return (
|
|
205
|
+
<div>
|
|
206
|
+
<Dropdown title="First">
|
|
207
|
+
<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
|
|
208
|
+
</Dropdown>
|
|
209
|
+
<Dropdown title="Second">
|
|
210
|
+
<Dropdown.Item onSelect={fn2}>Delete</Dropdown.Item>
|
|
211
|
+
</Dropdown>
|
|
212
|
+
</div>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
`.trim()
|
|
216
|
+
|
|
217
|
+
const result = applyTransform(input)
|
|
218
|
+
const count = (result?.match(/<DropdownTrigger>/g) || []).length
|
|
219
|
+
expect(count).toBe(2)
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe("no changes scenarios", () => {
|
|
224
|
+
it("returns null when Dropdown has no title or triggerElement", () => {
|
|
225
|
+
const input = `
|
|
226
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
227
|
+
|
|
228
|
+
export default function Test() {
|
|
229
|
+
return (
|
|
230
|
+
<Dropdown onClose={handleClose}>
|
|
231
|
+
<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
|
|
232
|
+
</Dropdown>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
`.trim()
|
|
236
|
+
|
|
237
|
+
const result = applyTransform(input)
|
|
238
|
+
expect(result).toBe(null)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it("returns null when Dropdown is not imported from tapestry-react", () => {
|
|
242
|
+
const input = `
|
|
243
|
+
import { Dropdown } from "some-other-library"
|
|
244
|
+
|
|
245
|
+
export default function Test() {
|
|
246
|
+
return (
|
|
247
|
+
<Dropdown title="Actions">
|
|
248
|
+
<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
|
|
249
|
+
</Dropdown>
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
`.trim()
|
|
253
|
+
|
|
254
|
+
const result = applyTransform(input)
|
|
255
|
+
expect(result).toBe(null)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it("returns null for empty file", () => {
|
|
259
|
+
expect(applyTransform("")).toBe(null)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it("wraps non-JSX triggerElement expression as a child with a TODO comment", () => {
|
|
263
|
+
const input = `
|
|
264
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
265
|
+
|
|
266
|
+
export default function Test() {
|
|
267
|
+
return (
|
|
268
|
+
<Dropdown triggerElement={myTrigger}>
|
|
269
|
+
<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
|
|
270
|
+
</Dropdown>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
`.trim()
|
|
274
|
+
|
|
275
|
+
const result = applyTransform(input)
|
|
276
|
+
expect(result).toContain("<DropdownTrigger>")
|
|
277
|
+
expect(result).toContain("{myTrigger}")
|
|
278
|
+
expect(result).not.toContain("triggerElement=")
|
|
279
|
+
expect(result).toContain("TODO")
|
|
280
|
+
expect(result).toContain("triggerElement")
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JSXAttribute,
|
|
3
|
+
JSXElement,
|
|
4
|
+
JSXExpressionContainer,
|
|
5
|
+
Transform,
|
|
6
|
+
} from "jscodeshift"
|
|
7
|
+
|
|
8
|
+
import { addComment } from "../../shared/actions/addComment"
|
|
9
|
+
import { getAttribute } from "../../shared/actions/getAttribute"
|
|
10
|
+
import { removeAttribute } from "../../shared/actions/removeAttribute"
|
|
11
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
12
|
+
import { addImport } from "../../shared/transformFactories/helpers/manageImports"
|
|
13
|
+
|
|
14
|
+
const transform: Transform = attributeTransformFactory({
|
|
15
|
+
targetComponent: "Dropdown",
|
|
16
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
17
|
+
transform: (element, { j, source }) => {
|
|
18
|
+
const titleAttr = getAttribute({ element, name: "title" })
|
|
19
|
+
const triggerElementAttr = getAttribute({ element, name: "triggerElement" })
|
|
20
|
+
|
|
21
|
+
if (!titleAttr && !triggerElementAttr) return false
|
|
22
|
+
|
|
23
|
+
let triggerChild: JSXElement | JSXExpressionContainer | null = null
|
|
24
|
+
|
|
25
|
+
if (triggerElementAttr) {
|
|
26
|
+
const attrValue = triggerElementAttr.value
|
|
27
|
+
if (
|
|
28
|
+
attrValue?.type === "JSXExpressionContainer" &&
|
|
29
|
+
attrValue.expression.type !== "JSXEmptyExpression"
|
|
30
|
+
) {
|
|
31
|
+
if (attrValue.expression.type === "JSXElement") {
|
|
32
|
+
triggerChild = attrValue.expression
|
|
33
|
+
} else {
|
|
34
|
+
triggerChild = j.jsxExpressionContainer(attrValue.expression)
|
|
35
|
+
addComment({
|
|
36
|
+
element,
|
|
37
|
+
j,
|
|
38
|
+
scope: "triggerElement",
|
|
39
|
+
source,
|
|
40
|
+
text: "triggerElement was not a JSX element — verify this renders correctly inside DropdownTrigger",
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!triggerChild && titleAttr) {
|
|
47
|
+
const labelValue = titleAttr.value
|
|
48
|
+
if (!labelValue) return false
|
|
49
|
+
|
|
50
|
+
const labelAttr: JSXAttribute = j.jsxAttribute(
|
|
51
|
+
j.jsxIdentifier("label"),
|
|
52
|
+
labelValue
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
triggerChild = j.jsxElement(
|
|
56
|
+
j.jsxOpeningElement(
|
|
57
|
+
j.jsxIdentifier("DropdownButton"),
|
|
58
|
+
[labelAttr],
|
|
59
|
+
true
|
|
60
|
+
),
|
|
61
|
+
null,
|
|
62
|
+
[]
|
|
63
|
+
)
|
|
64
|
+
addImport({
|
|
65
|
+
component: "DropdownButton",
|
|
66
|
+
fromPackage: "@planningcenter/tapestry-react",
|
|
67
|
+
j,
|
|
68
|
+
pkg: "@planningcenter/tapestry",
|
|
69
|
+
source,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!triggerChild) return false
|
|
74
|
+
|
|
75
|
+
const triggerWrapper = j.jsxElement(
|
|
76
|
+
j.jsxOpeningElement(j.jsxIdentifier("DropdownTrigger"), [], false),
|
|
77
|
+
j.jsxClosingElement(j.jsxIdentifier("DropdownTrigger")),
|
|
78
|
+
[triggerChild]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
removeAttribute("title", { element, j, source })
|
|
82
|
+
removeAttribute("triggerElement", { element, j, source })
|
|
83
|
+
|
|
84
|
+
element.children = [triggerWrapper, ...(element.children ?? [])]
|
|
85
|
+
|
|
86
|
+
addImport({
|
|
87
|
+
component: "DropdownTrigger",
|
|
88
|
+
fromPackage: "@planningcenter/tapestry-react",
|
|
89
|
+
j,
|
|
90
|
+
pkg: "@planningcenter/tapestry",
|
|
91
|
+
source,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
return true
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
export default transform
|
|
@@ -156,19 +156,6 @@ function Test() {
|
|
|
156
156
|
expect(result).toContain("TODO: tapestry-migration (hover)")
|
|
157
157
|
})
|
|
158
158
|
|
|
159
|
-
it("should flag autoFocus", () => {
|
|
160
|
-
const input = `
|
|
161
|
-
import { Input } from "@planningcenter/tapestry-react"
|
|
162
|
-
|
|
163
|
-
function Test() {
|
|
164
|
-
return <Input autoFocus label="Name" />
|
|
165
|
-
}
|
|
166
|
-
`.trim()
|
|
167
|
-
|
|
168
|
-
const result = applyTransform(input)
|
|
169
|
-
expect(result).toContain("TODO: tapestry-migration (autoFocus)")
|
|
170
|
-
})
|
|
171
|
-
|
|
172
159
|
it("should flag readOnlyBackgroundColor", () => {
|
|
173
160
|
const input = `
|
|
174
161
|
import { Input } from "@planningcenter/tapestry-react"
|
|
@@ -231,6 +218,20 @@ function Test() {
|
|
|
231
218
|
expect(result).not.toContain("TODO: tapestry-migration")
|
|
232
219
|
})
|
|
233
220
|
|
|
221
|
+
it("should not flag autoFocus", () => {
|
|
222
|
+
const input = `
|
|
223
|
+
import { Input } from "@planningcenter/tapestry-react"
|
|
224
|
+
|
|
225
|
+
function Test() {
|
|
226
|
+
return <Input autoFocus label="Name" />
|
|
227
|
+
}
|
|
228
|
+
`.trim()
|
|
229
|
+
|
|
230
|
+
const result = applyTransform(input)
|
|
231
|
+
expect(result).not.toContain("TODO: tapestry-migration")
|
|
232
|
+
expect(result).toContain("autoFocus")
|
|
233
|
+
})
|
|
234
|
+
|
|
234
235
|
it("should not flag standard HTML attributes", () => {
|
|
235
236
|
const input = `
|
|
236
237
|
import { Input } from "@planningcenter/tapestry-react"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import jscodeshift, { JSXElement } from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import { isChildOf } from "./isChildOf"
|
|
5
|
+
|
|
6
|
+
const j = jscodeshift.withParser("tsx")
|
|
7
|
+
|
|
8
|
+
function getElements(source: string): {
|
|
9
|
+
elements: JSXElement[]
|
|
10
|
+
source: ReturnType<typeof j>
|
|
11
|
+
} {
|
|
12
|
+
const parsed = j(source)
|
|
13
|
+
const elements = parsed
|
|
14
|
+
.find(j.JSXElement)
|
|
15
|
+
.paths()
|
|
16
|
+
.map((p) => p.value)
|
|
17
|
+
return { elements, source: parsed }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("isChildOf", () => {
|
|
21
|
+
it("returns true for a direct child of the parent", () => {
|
|
22
|
+
const { source, elements } = getElements(`<Dropdown><Divider /></Dropdown>`)
|
|
23
|
+
const divider = elements.find(
|
|
24
|
+
(el) =>
|
|
25
|
+
el.openingElement.name.type === "JSXIdentifier" &&
|
|
26
|
+
el.openingElement.name.name === "Divider"
|
|
27
|
+
)!
|
|
28
|
+
const condition = isChildOf("Dropdown", source, j)
|
|
29
|
+
expect(condition(divider)).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("returns true for a deeply nested descendant", () => {
|
|
33
|
+
const { source, elements } = getElements(
|
|
34
|
+
`<Dropdown><DropdownMenu><Divider /></DropdownMenu></Dropdown>`
|
|
35
|
+
)
|
|
36
|
+
const divider = elements.find(
|
|
37
|
+
(el) =>
|
|
38
|
+
el.openingElement.name.type === "JSXIdentifier" &&
|
|
39
|
+
el.openingElement.name.name === "Divider"
|
|
40
|
+
)!
|
|
41
|
+
const condition = isChildOf("Dropdown", source, j)
|
|
42
|
+
expect(condition(divider)).toBe(true)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("returns false for an element outside the parent", () => {
|
|
46
|
+
const { source, elements } = getElements(`<div><Divider /></div>`)
|
|
47
|
+
const divider = elements.find(
|
|
48
|
+
(el) =>
|
|
49
|
+
el.openingElement.name.type === "JSXIdentifier" &&
|
|
50
|
+
el.openingElement.name.name === "Divider"
|
|
51
|
+
)!
|
|
52
|
+
const condition = isChildOf("Dropdown", source, j)
|
|
53
|
+
expect(condition(divider)).toBe(false)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("returns false when no parent element exists in source", () => {
|
|
57
|
+
const { source, elements } = getElements(`<Divider />`)
|
|
58
|
+
const divider = elements[0]
|
|
59
|
+
const condition = isChildOf("Dropdown", source, j)
|
|
60
|
+
expect(condition(divider)).toBe(false)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("returns true for child inside one of multiple parents", () => {
|
|
64
|
+
const { source, elements } = getElements(
|
|
65
|
+
`<div><Dropdown><Divider /></Dropdown></div>`
|
|
66
|
+
)
|
|
67
|
+
const divider = elements.find(
|
|
68
|
+
(el) =>
|
|
69
|
+
el.openingElement.name.type === "JSXIdentifier" &&
|
|
70
|
+
el.openingElement.name.name === "Divider"
|
|
71
|
+
)!
|
|
72
|
+
const condition = isChildOf("Dropdown", source, j)
|
|
73
|
+
expect(condition(divider)).toBe(true)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("handles sibling elements — child inside parent returns true, sibling does not", () => {
|
|
77
|
+
const { source, elements } = getElements(
|
|
78
|
+
`<div><Dropdown><Divider /></Dropdown><Divider /></div>`
|
|
79
|
+
)
|
|
80
|
+
const [insideDivider, outsideDivider] = elements.filter(
|
|
81
|
+
(el) =>
|
|
82
|
+
el.openingElement.name.type === "JSXIdentifier" &&
|
|
83
|
+
el.openingElement.name.name === "Divider"
|
|
84
|
+
)
|
|
85
|
+
const condition = isChildOf("Dropdown", source, j)
|
|
86
|
+
expect(condition(insideDivider)).toBe(true)
|
|
87
|
+
expect(condition(outsideDivider)).toBe(false)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Collection, JSCodeshift, JSXElement } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { TransformCondition } from "../types"
|
|
4
|
+
|
|
5
|
+
function elementKey(element: JSXElement): string | null {
|
|
6
|
+
const loc = element.loc
|
|
7
|
+
if (!loc) return null
|
|
8
|
+
return `${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function collectDescendantKeys(element: JSXElement, result: Set<string>): void {
|
|
12
|
+
for (const child of element.children ?? []) {
|
|
13
|
+
if (child.type === "JSXElement") {
|
|
14
|
+
const key = elementKey(child)
|
|
15
|
+
if (key) result.add(key)
|
|
16
|
+
collectDescendantKeys(child, result)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns a condition that is true only when the element is a descendant of a
|
|
23
|
+
* JSX element with the given local name. Matches by source location so the
|
|
24
|
+
* condition works across separate jscodeshift parses of the same source text.
|
|
25
|
+
*/
|
|
26
|
+
export function isChildOf(
|
|
27
|
+
parentLocalName: string,
|
|
28
|
+
source: Collection,
|
|
29
|
+
j: JSCodeshift
|
|
30
|
+
): TransformCondition {
|
|
31
|
+
const descendantKeys = new Set<string>()
|
|
32
|
+
|
|
33
|
+
source
|
|
34
|
+
.find(j.JSXOpeningElement, { name: { name: parentLocalName } })
|
|
35
|
+
.forEach((path) => {
|
|
36
|
+
collectDescendantKeys(path.parent.value as JSXElement, descendantKeys)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
return (element: JSXElement) => {
|
|
40
|
+
const key = elementKey(element)
|
|
41
|
+
return key !== null && descendantKeys.has(key)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -56,6 +56,7 @@ export const CHECKBOX_RADIO_SUPPORTED_PROPS = [
|
|
|
56
56
|
|
|
57
57
|
export const INPUT_SPECIFIC_PROPS = [
|
|
58
58
|
"autoComplete",
|
|
59
|
+
"autoFocus",
|
|
59
60
|
"autoWidth",
|
|
60
61
|
"defaultValue",
|
|
61
62
|
"description",
|
|
@@ -197,3 +198,38 @@ export const DATE_PICKER_SUPPORTED_PROPS = [
|
|
|
197
198
|
...COMMON_PROPS,
|
|
198
199
|
...DATE_PICKER_SPECIFIC_PROPS,
|
|
199
200
|
]
|
|
201
|
+
|
|
202
|
+
export const DROPDOWN_SPECIFIC_PROPS = ["onClose", "onOpen"]
|
|
203
|
+
|
|
204
|
+
export const DROPDOWN_SUPPORTED_PROPS = [
|
|
205
|
+
...COMMON_PROPS.filter((prop) => prop !== "size"),
|
|
206
|
+
...DROPDOWN_SPECIFIC_PROPS,
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
export const DROPDOWN_ITEM_BASE_PROPS = [
|
|
210
|
+
"destructive",
|
|
211
|
+
"staffOnly",
|
|
212
|
+
"text",
|
|
213
|
+
"textValue",
|
|
214
|
+
"value",
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
// Dropdown.Item (old) → DropdownAction (new)
|
|
218
|
+
// onSelect→onAction, value→id, text→textValue handled by itemToAction
|
|
219
|
+
export const DROPDOWN_ITEM_SUPPORTED_PROPS = [
|
|
220
|
+
...COMMON_PROPS,
|
|
221
|
+
...DROPDOWN_ITEM_BASE_PROPS,
|
|
222
|
+
"disabled",
|
|
223
|
+
"onAction",
|
|
224
|
+
"onSelect",
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
// Dropdown.Link (old) → DropdownLink (new)
|
|
228
|
+
// to→href handled by linkToLink; disabled intentionally NOT included
|
|
229
|
+
export const DROPDOWN_LINK_SUPPORTED_PROPS = [
|
|
230
|
+
...COMMON_PROPS,
|
|
231
|
+
...DROPDOWN_ITEM_BASE_PROPS,
|
|
232
|
+
"external",
|
|
233
|
+
"href",
|
|
234
|
+
"to",
|
|
235
|
+
]
|