@planningcenter/tapestry-migration-cli 3.4.1 → 3.5.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/README.md +22 -0
- package/dist/tapestry-react-shim.cjs +5 -1
- package/package.json +3 -3
- package/src/availableComponents.ts +13 -0
- package/src/components/dropdown/index.test.ts +67 -0
- package/src/components/dropdown/index.ts +4 -0
- package/src/components/dropdown/transforms/placementIdToMenu.test.ts +6 -1
- package/src/components/dropdown/transforms/placementIdToMenu.ts +1 -1
- package/src/components/dropdown/transforms/variantToKind.test.ts +292 -0
- package/src/components/dropdown/transforms/variantToKind.ts +138 -0
- package/src/components/dropdown/transforms/wrapIconTrigger.test.ts +336 -0
- package/src/components/dropdown/transforms/wrapIconTrigger.ts +233 -0
- package/src/index.ts +5 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/tapestry-migration-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "CLI tool for Tapestry migrations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@emotion/react": "^11.14.0",
|
|
33
|
-
"@planningcenter/tapestry": "^3.
|
|
33
|
+
"@planningcenter/tapestry": "^3.5.0",
|
|
34
34
|
"@planningcenter/tapestry-react": "^4.11.5",
|
|
35
35
|
"@types/jscodeshift": "^17.3.0",
|
|
36
36
|
"@types/node": "^20.0.0",
|
|
@@ -50,5 +50,5 @@
|
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "958f1f269ca37d78e9a9c0ec66c208a7115331dc"
|
|
54
54
|
}
|
|
@@ -200,6 +200,73 @@ export default function Test() {
|
|
|
200
200
|
expect(result).toContain("TODO: tapestry-migration (variant)")
|
|
201
201
|
})
|
|
202
202
|
|
|
203
|
+
it("applies wrapIconTrigger through the chain", () => {
|
|
204
|
+
const input = `
|
|
205
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
206
|
+
|
|
207
|
+
export default function Test() {
|
|
208
|
+
return (
|
|
209
|
+
<Dropdown icon={{ name: "general.pencil" }} title="Edit options">
|
|
210
|
+
<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
|
|
211
|
+
</Dropdown>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
`.trim()
|
|
215
|
+
|
|
216
|
+
const result = applyTransform(input)
|
|
217
|
+
expect(result).toContain("<DropdownTrigger>")
|
|
218
|
+
expect(result).toContain("<DropdownIconButton")
|
|
219
|
+
expect(result).toContain('aria-label="Edit options"')
|
|
220
|
+
expect(result).toContain("#pencil")
|
|
221
|
+
expect(result).not.toContain("icon={{ name:")
|
|
222
|
+
expect(result).not.toContain('title="Edit options"')
|
|
223
|
+
expect(result).not.toContain('from "@planningcenter/tapestry-react"')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it("applies variantToKind through the chain (variant)", () => {
|
|
227
|
+
const input = `
|
|
228
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
229
|
+
|
|
230
|
+
export default function Test() {
|
|
231
|
+
return (
|
|
232
|
+
<Dropdown title="Actions" variant="outline">
|
|
233
|
+
<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
|
|
234
|
+
</Dropdown>
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
`.trim()
|
|
238
|
+
|
|
239
|
+
const result = applyTransform(input)
|
|
240
|
+
expect(result).toContain('kind="secondary"')
|
|
241
|
+
expect(result).not.toContain('variant="outline"')
|
|
242
|
+
const dropdownButtonLine = result
|
|
243
|
+
?.split("\n")
|
|
244
|
+
.find((l) => l.includes("<DropdownButton"))
|
|
245
|
+
expect(dropdownButtonLine).toContain("kind")
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it("applies variantToKind through the chain (theme)", () => {
|
|
249
|
+
const input = `
|
|
250
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
251
|
+
|
|
252
|
+
export default function Test() {
|
|
253
|
+
return (
|
|
254
|
+
<Dropdown title="Actions" theme="error">
|
|
255
|
+
<Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
|
|
256
|
+
</Dropdown>
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
`.trim()
|
|
260
|
+
|
|
261
|
+
const result = applyTransform(input)
|
|
262
|
+
expect(result).toContain('kind="delete"')
|
|
263
|
+
expect(result).not.toContain('theme="error"')
|
|
264
|
+
const dropdownButtonLine = result
|
|
265
|
+
?.split("\n")
|
|
266
|
+
.find((l) => l.includes("<DropdownButton"))
|
|
267
|
+
expect(dropdownButtonLine).toContain("kind")
|
|
268
|
+
})
|
|
269
|
+
|
|
203
270
|
it("applies moveDropdownImport through the chain", () => {
|
|
204
271
|
const input = `
|
|
205
272
|
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
@@ -10,6 +10,8 @@ import unsupportedProps from "./transforms/unsupportedProps"
|
|
|
10
10
|
import unsupportedPropsDivider from "./transforms/unsupportedPropsDivider"
|
|
11
11
|
import unsupportedPropsItem from "./transforms/unsupportedPropsItem"
|
|
12
12
|
import unsupportedPropsLink from "./transforms/unsupportedPropsLink"
|
|
13
|
+
import variantToKind from "./transforms/variantToKind"
|
|
14
|
+
import wrapIconTrigger from "./transforms/wrapIconTrigger"
|
|
13
15
|
import wrapMenu from "./transforms/wrapMenu"
|
|
14
16
|
import wrapTrigger from "./transforms/wrapTrigger"
|
|
15
17
|
|
|
@@ -25,7 +27,9 @@ const transform: Transform = (fileInfo, api, options) => {
|
|
|
25
27
|
linkToLink,
|
|
26
28
|
wrapMenu,
|
|
27
29
|
placementIdToMenu,
|
|
30
|
+
wrapIconTrigger,
|
|
28
31
|
wrapTrigger,
|
|
32
|
+
variantToKind,
|
|
29
33
|
dividerToSeparator,
|
|
30
34
|
auditSpreadProps,
|
|
31
35
|
unsupportedProps,
|
|
@@ -50,12 +50,17 @@ describe("placementIdToMenu transform", () => {
|
|
|
50
50
|
expect(result).toContain('<DropdownMenu placement={"bottom end"}')
|
|
51
51
|
})
|
|
52
52
|
|
|
53
|
-
it("moves dynamic placement expression and adds TODO comment", () => {
|
|
53
|
+
it("moves dynamic placement expression and adds TODO comment inside Dropdown before DropdownMenu", () => {
|
|
54
54
|
const result = applyTransform(withMenu("placement={myPlacement}"))
|
|
55
55
|
expect(result).toContain("<DropdownMenu placement={myPlacement}")
|
|
56
56
|
expect(result).toContain(
|
|
57
57
|
"TODO: tapestry-migration (placement): placement is a dynamic expression"
|
|
58
58
|
)
|
|
59
|
+
const todoIndex = result!.indexOf("TODO: tapestry-migration (placement)")
|
|
60
|
+
const dropdownOpenIndex = result!.indexOf("<Dropdown ")
|
|
61
|
+
const dropdownMenuIndex = result!.indexOf("<DropdownMenu")
|
|
62
|
+
expect(todoIndex).toBeGreaterThan(dropdownOpenIndex)
|
|
63
|
+
expect(todoIndex).toBeLessThan(dropdownMenuIndex)
|
|
59
64
|
})
|
|
60
65
|
})
|
|
61
66
|
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./variantToKind"
|
|
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
|
+
function wrappedInput(
|
|
18
|
+
attrs: string,
|
|
19
|
+
buttonElement = '<DropdownButton label="Actions" />'
|
|
20
|
+
) {
|
|
21
|
+
return `
|
|
22
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
23
|
+
import { DropdownButton, DropdownMenu, DropdownTrigger } from "@planningcenter/tapestry"
|
|
24
|
+
|
|
25
|
+
export default function Test() {
|
|
26
|
+
return (
|
|
27
|
+
<Dropdown ${attrs} onClose={fn}>
|
|
28
|
+
<DropdownTrigger>
|
|
29
|
+
${buttonElement}
|
|
30
|
+
</DropdownTrigger>
|
|
31
|
+
<DropdownMenu>
|
|
32
|
+
<DropdownAction id="edit" onAction={fn}>Edit</DropdownAction>
|
|
33
|
+
</DropdownMenu>
|
|
34
|
+
</Dropdown>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
`.trim()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("variantToKind transform", () => {
|
|
41
|
+
describe("variant-only mappings (default theme)", () => {
|
|
42
|
+
it('maps variant="outline" to kind="secondary"', () => {
|
|
43
|
+
const result = applyTransform(wrappedInput('variant="outline"'))
|
|
44
|
+
expect(result).toContain('kind="secondary"')
|
|
45
|
+
expect(result).not.toContain('variant="outline"')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('maps variant="fill" to kind="neutral"', () => {
|
|
49
|
+
const result = applyTransform(wrappedInput('variant="fill"'))
|
|
50
|
+
expect(result).toContain('kind="neutral"')
|
|
51
|
+
expect(result).not.toContain('variant="fill"')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('maps variant="naked" to kind="ghost"', () => {
|
|
55
|
+
const result = applyTransform(wrappedInput('variant="naked"'))
|
|
56
|
+
expect(result).toContain('kind="ghost"')
|
|
57
|
+
expect(result).not.toContain('variant="naked"')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('normalizes variant="ghost" to kind="ghost"', () => {
|
|
61
|
+
const result = applyTransform(wrappedInput('variant="ghost"'))
|
|
62
|
+
expect(result).toContain('kind="ghost"')
|
|
63
|
+
expect(result).not.toContain('variant="ghost"')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('normalizes variant="solid" to kind="neutral" (solid→fill→neutral)', () => {
|
|
67
|
+
const result = applyTransform(wrappedInput('variant="solid"'))
|
|
68
|
+
expect(result).toContain('kind="neutral"')
|
|
69
|
+
expect(result).not.toContain('variant="solid"')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe("theme-only mappings (default variant)", () => {
|
|
74
|
+
it('maps theme="error" to kind="delete"', () => {
|
|
75
|
+
const result = applyTransform(wrappedInput('theme="error"'))
|
|
76
|
+
expect(result).toContain('kind="delete"')
|
|
77
|
+
expect(result).not.toContain('theme="error"')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('maps theme="primary" to kind="primary"', () => {
|
|
81
|
+
const result = applyTransform(wrappedInput('theme="primary"'))
|
|
82
|
+
expect(result).toContain('kind="primary"')
|
|
83
|
+
expect(result).not.toContain('theme="primary"')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('normalizes theme="destroy" to kind="delete"', () => {
|
|
87
|
+
const result = applyTransform(wrappedInput('theme="destroy"'))
|
|
88
|
+
expect(result).toContain('kind="delete"')
|
|
89
|
+
expect(result).not.toContain('theme="destroy"')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('normalizes theme="interaction" to kind="primary"', () => {
|
|
93
|
+
const result = applyTransform(wrappedInput('theme="interaction"'))
|
|
94
|
+
expect(result).toContain('kind="primary"')
|
|
95
|
+
expect(result).not.toContain('theme="interaction"')
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe("theme + variant combined", () => {
|
|
100
|
+
it('maps theme="error" variant="outline" to kind="secondary-delete"', () => {
|
|
101
|
+
const result = applyTransform(
|
|
102
|
+
wrappedInput('theme="error" variant="outline"')
|
|
103
|
+
)
|
|
104
|
+
expect(result).toContain('kind="secondary-delete"')
|
|
105
|
+
expect(result).not.toContain('theme="error"')
|
|
106
|
+
expect(result).not.toContain('variant="outline"')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('maps theme="error" variant="naked" to kind="ghost-delete"', () => {
|
|
110
|
+
const result = applyTransform(
|
|
111
|
+
wrappedInput('theme="error" variant="naked"')
|
|
112
|
+
)
|
|
113
|
+
expect(result).toContain('kind="ghost-delete"')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('maps theme="primary" variant="outline" to kind="secondary-interaction"', () => {
|
|
117
|
+
const result = applyTransform(
|
|
118
|
+
wrappedInput('theme="primary" variant="outline"')
|
|
119
|
+
)
|
|
120
|
+
expect(result).toContain('kind="secondary-interaction"')
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe("kind placement", () => {
|
|
125
|
+
it("adds kind to the DropdownButton, not to Dropdown", () => {
|
|
126
|
+
const result = applyTransform(wrappedInput('variant="outline"'))
|
|
127
|
+
const dropdownButtonLine = result
|
|
128
|
+
?.split("\n")
|
|
129
|
+
.find((l) => l.includes("<DropdownButton"))
|
|
130
|
+
expect(dropdownButtonLine).toContain("kind")
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it("emits TODO and does not duplicate kind when button already has kind", () => {
|
|
134
|
+
const result = applyTransform(
|
|
135
|
+
wrappedInput(
|
|
136
|
+
'variant="outline"',
|
|
137
|
+
'<DropdownButton label="Actions" kind="primary" />'
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
expect(result).not.toContain('variant="outline"')
|
|
141
|
+
expect(result).toContain("TODO: tapestry-migration (variant):")
|
|
142
|
+
expect(result).toContain("secondary")
|
|
143
|
+
const buttonLine = result
|
|
144
|
+
?.split("\n")
|
|
145
|
+
.find((l) => l.includes("<DropdownButton"))
|
|
146
|
+
expect(buttonLine).toContain('kind="primary"')
|
|
147
|
+
expect(buttonLine).not.toContain('kind="secondary"')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('maps variant="outline" to kind="secondary" on DropdownIconButton', () => {
|
|
151
|
+
const result = applyTransform(
|
|
152
|
+
wrappedInput(
|
|
153
|
+
'variant="outline"',
|
|
154
|
+
'<DropdownIconButton aria-label="Edit" icon={icon} />'
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
expect(result).toContain('kind="secondary"')
|
|
158
|
+
expect(result).not.toContain('variant="outline"')
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe("complex/custom trigger child", () => {
|
|
163
|
+
it("removes variant and emits TODO for unknown trigger button", () => {
|
|
164
|
+
const input = `
|
|
165
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
166
|
+
import { DropdownMenu, DropdownTrigger } from "@planningcenter/tapestry"
|
|
167
|
+
|
|
168
|
+
export default function Test() {
|
|
169
|
+
return (
|
|
170
|
+
<Dropdown variant="outline" onClose={fn}>
|
|
171
|
+
<DropdownTrigger>
|
|
172
|
+
<MyCustomButton label="Actions" />
|
|
173
|
+
</DropdownTrigger>
|
|
174
|
+
<DropdownMenu />
|
|
175
|
+
</Dropdown>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
`.trim()
|
|
179
|
+
|
|
180
|
+
const result = applyTransform(input)
|
|
181
|
+
expect(result).not.toContain('variant="outline"')
|
|
182
|
+
expect(result).toContain("TODO: tapestry-migration (variant):")
|
|
183
|
+
expect(result).toContain("secondary")
|
|
184
|
+
const customButtonLine = result
|
|
185
|
+
?.split("\n")
|
|
186
|
+
.find((l) => l.includes("<MyCustomButton"))
|
|
187
|
+
expect(customButtonLine).not.toContain("kind")
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it("uses scope=theme when only theme is present on custom trigger", () => {
|
|
191
|
+
const input = `
|
|
192
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
193
|
+
import { DropdownMenu, DropdownTrigger } from "@planningcenter/tapestry"
|
|
194
|
+
|
|
195
|
+
export default function Test() {
|
|
196
|
+
return (
|
|
197
|
+
<Dropdown theme="error" onClose={fn}>
|
|
198
|
+
<DropdownTrigger>
|
|
199
|
+
<MyCustomButton label="Actions" />
|
|
200
|
+
</DropdownTrigger>
|
|
201
|
+
<DropdownMenu />
|
|
202
|
+
</Dropdown>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
`.trim()
|
|
206
|
+
|
|
207
|
+
const result = applyTransform(input)
|
|
208
|
+
expect(result).toContain("TODO: tapestry-migration (theme):")
|
|
209
|
+
expect(result).not.toContain("TODO: tapestry-migration (variant):")
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it("uses scope=variant/theme when both are present on custom trigger", () => {
|
|
213
|
+
const input = `
|
|
214
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
215
|
+
import { DropdownMenu, DropdownTrigger } from "@planningcenter/tapestry"
|
|
216
|
+
|
|
217
|
+
export default function Test() {
|
|
218
|
+
return (
|
|
219
|
+
<Dropdown theme="error" variant="outline" onClose={fn}>
|
|
220
|
+
<DropdownTrigger>
|
|
221
|
+
<MyCustomButton label="Actions" />
|
|
222
|
+
</DropdownTrigger>
|
|
223
|
+
<DropdownMenu />
|
|
224
|
+
</Dropdown>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
`.trim()
|
|
228
|
+
|
|
229
|
+
const result = applyTransform(input)
|
|
230
|
+
expect(result).toContain("TODO: tapestry-migration (variant/theme):")
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
describe("no changes scenarios", () => {
|
|
235
|
+
it("returns null for dynamic variant expression", () => {
|
|
236
|
+
expect(applyTransform(wrappedInput("variant={buttonVariant}"))).toBe(null)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it("returns null for dynamic theme expression", () => {
|
|
240
|
+
expect(applyTransform(wrappedInput("theme={buttonTheme}"))).toBe(null)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it("returns null for an unmapped variant value", () => {
|
|
244
|
+
expect(applyTransform(wrappedInput('variant="extra-large"'))).toBe(null)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it("returns null for an unmapped theme value", () => {
|
|
248
|
+
expect(applyTransform(wrappedInput('theme="unknown-theme"'))).toBe(null)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it("returns null when no DropdownTrigger child exists", () => {
|
|
252
|
+
const input = `
|
|
253
|
+
import { Dropdown } from "@planningcenter/tapestry-react"
|
|
254
|
+
|
|
255
|
+
export default function Test() {
|
|
256
|
+
return (
|
|
257
|
+
<Dropdown variant="outline" onClose={fn}>
|
|
258
|
+
<DropdownMenu />
|
|
259
|
+
</Dropdown>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
`.trim()
|
|
263
|
+
|
|
264
|
+
expect(applyTransform(input)).toBe(null)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it("returns null when neither variant nor theme is present", () => {
|
|
268
|
+
expect(applyTransform(wrappedInput(""))).toBe(null)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it("returns null when Dropdown is not imported from tapestry-react", () => {
|
|
272
|
+
const input = `
|
|
273
|
+
import { Dropdown } from "@planningcenter/tapestry"
|
|
274
|
+
|
|
275
|
+
export default function Test() {
|
|
276
|
+
return (
|
|
277
|
+
<Dropdown variant="outline">
|
|
278
|
+
<DropdownTrigger><DropdownButton label="Actions" /></DropdownTrigger>
|
|
279
|
+
<DropdownMenu />
|
|
280
|
+
</Dropdown>
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
`.trim()
|
|
284
|
+
|
|
285
|
+
expect(applyTransform(input)).toBe(null)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it("returns null for empty file", () => {
|
|
289
|
+
expect(applyTransform("")).toBe(null)
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { JSXAttribute, JSXElement, Transform } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { addAttribute } from "../../shared/actions/addAttribute"
|
|
4
|
+
import { addComment } from "../../shared/actions/addComment"
|
|
5
|
+
import { getAttribute } from "../../shared/actions/getAttribute"
|
|
6
|
+
import { removeAttribute } from "../../shared/actions/removeAttribute"
|
|
7
|
+
import { hasAttribute } from "../../shared/conditions/hasAttribute"
|
|
8
|
+
import { orConditions } from "../../shared/conditions/orConditions"
|
|
9
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
10
|
+
|
|
11
|
+
const THEME_KIND_MAP: Record<string, Record<string, string>> = {
|
|
12
|
+
default: {
|
|
13
|
+
default: "neutral",
|
|
14
|
+
fill: "neutral",
|
|
15
|
+
naked: "ghost",
|
|
16
|
+
outline: "secondary",
|
|
17
|
+
},
|
|
18
|
+
error: {
|
|
19
|
+
default: "delete",
|
|
20
|
+
fill: "delete",
|
|
21
|
+
naked: "ghost-delete",
|
|
22
|
+
outline: "secondary-delete",
|
|
23
|
+
},
|
|
24
|
+
neutral: {
|
|
25
|
+
default: "neutral",
|
|
26
|
+
fill: "neutral",
|
|
27
|
+
naked: "ghost",
|
|
28
|
+
outline: "secondary",
|
|
29
|
+
},
|
|
30
|
+
primary: {
|
|
31
|
+
default: "primary",
|
|
32
|
+
fill: "primary",
|
|
33
|
+
naked: "ghost-interaction",
|
|
34
|
+
outline: "secondary-interaction",
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const KNOWN_TRIGGER_BUTTONS = new Set(["DropdownButton", "DropdownIconButton"])
|
|
39
|
+
|
|
40
|
+
function normalizeTheme(value: string): string {
|
|
41
|
+
if (value === "destroy" || value === "delete") return "error"
|
|
42
|
+
if (value === "info" || value === "success" || value === "interaction")
|
|
43
|
+
return "primary"
|
|
44
|
+
return value
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeVariant(value: string): string {
|
|
48
|
+
if (value === "ghost") return "naked"
|
|
49
|
+
if (value === "solid") return "fill"
|
|
50
|
+
return value
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getStaticStringValue(
|
|
54
|
+
attr: JSXAttribute | undefined,
|
|
55
|
+
defaultValue: string
|
|
56
|
+
): string | null {
|
|
57
|
+
if (!attr || !attr.value) return defaultValue
|
|
58
|
+
if (attr.value.type === "StringLiteral") return attr.value.value
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function mapToKind(theme: string, variant: string): string | null {
|
|
63
|
+
return (
|
|
64
|
+
THEME_KIND_MAP[normalizeTheme(theme)]?.[normalizeVariant(variant)] ?? null
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findDropdownTrigger(element: JSXElement): JSXElement | undefined {
|
|
69
|
+
return (element.children ?? []).find(
|
|
70
|
+
(child): child is JSXElement =>
|
|
71
|
+
child.type === "JSXElement" &&
|
|
72
|
+
child.openingElement.name.type === "JSXIdentifier" &&
|
|
73
|
+
child.openingElement.name.name === "DropdownTrigger"
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function findFirstJSXElementChild(element: JSXElement): JSXElement | undefined {
|
|
78
|
+
return (element.children ?? []).find(
|
|
79
|
+
(child): child is JSXElement => child.type === "JSXElement"
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const transform: Transform = attributeTransformFactory({
|
|
84
|
+
condition: orConditions(hasAttribute("variant"), hasAttribute("theme")),
|
|
85
|
+
targetComponent: "Dropdown",
|
|
86
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
87
|
+
transform: (element, { j, source }) => {
|
|
88
|
+
const variantAttr = getAttribute({ element, name: "variant" })
|
|
89
|
+
const themeAttr = getAttribute({ element, name: "theme" })
|
|
90
|
+
|
|
91
|
+
const variantValue = getStaticStringValue(variantAttr, "fill")
|
|
92
|
+
const themeValue = getStaticStringValue(themeAttr, "default")
|
|
93
|
+
|
|
94
|
+
if (variantValue === null || themeValue === null) return false
|
|
95
|
+
|
|
96
|
+
const mappedKind = mapToKind(themeValue, variantValue)
|
|
97
|
+
if (!mappedKind) return false
|
|
98
|
+
|
|
99
|
+
const triggerChild = findDropdownTrigger(element)
|
|
100
|
+
if (!triggerChild) return false
|
|
101
|
+
|
|
102
|
+
const buttonChild = findFirstJSXElementChild(triggerChild)
|
|
103
|
+
if (!buttonChild) return false
|
|
104
|
+
|
|
105
|
+
const buttonName =
|
|
106
|
+
buttonChild.openingElement.name.type === "JSXIdentifier"
|
|
107
|
+
? buttonChild.openingElement.name.name
|
|
108
|
+
: null
|
|
109
|
+
|
|
110
|
+
if (variantAttr) removeAttribute("variant", { element, j, source })
|
|
111
|
+
if (themeAttr) removeAttribute("theme", { element, j, source })
|
|
112
|
+
|
|
113
|
+
const scope =
|
|
114
|
+
variantAttr && themeAttr
|
|
115
|
+
? "variant/theme"
|
|
116
|
+
: themeAttr
|
|
117
|
+
? "theme"
|
|
118
|
+
: "variant"
|
|
119
|
+
|
|
120
|
+
const existingKind = getAttribute({ element: buttonChild, name: "kind" })
|
|
121
|
+
|
|
122
|
+
if (buttonName && KNOWN_TRIGGER_BUTTONS.has(buttonName) && !existingKind) {
|
|
123
|
+
addAttribute({ element: buttonChild, j, name: "kind", value: mappedKind })
|
|
124
|
+
} else {
|
|
125
|
+
addComment({
|
|
126
|
+
element,
|
|
127
|
+
j,
|
|
128
|
+
scope,
|
|
129
|
+
source,
|
|
130
|
+
text: `add kind="${mappedKind}" to the trigger button manually`,
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return true
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
export default transform
|