@planningcenter/tapestry-migration-cli 3.4.1-rc.9 → 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.
Files changed (31) hide show
  1. package/package.json +3 -3
  2. package/src/components/dropdown/index.test.ts +177 -4
  3. package/src/components/dropdown/index.ts +24 -2
  4. package/src/components/dropdown/transforms/auditSpreadProps.test.ts +110 -0
  5. package/src/components/dropdown/transforms/auditSpreadProps.ts +10 -0
  6. package/src/components/dropdown/transforms/dividerToSeparator.test.ts +134 -0
  7. package/src/components/dropdown/transforms/dividerToSeparator.ts +67 -0
  8. package/src/components/dropdown/transforms/itemToAction.test.ts +19 -1
  9. package/src/components/dropdown/transforms/itemToAction.ts +14 -2
  10. package/src/components/dropdown/transforms/linkToLink.test.ts +19 -1
  11. package/src/components/dropdown/transforms/linkToLink.ts +13 -2
  12. package/src/components/dropdown/transforms/moveDropdownImport.test.ts +93 -0
  13. package/src/components/dropdown/transforms/moveDropdownImport.ts +13 -0
  14. package/src/components/dropdown/transforms/placementIdToMenu.test.ts +135 -0
  15. package/src/components/dropdown/transforms/placementIdToMenu.ts +96 -0
  16. package/src/components/dropdown/transforms/unsupportedProps.test.ts +145 -0
  17. package/src/components/dropdown/transforms/unsupportedProps.ts +12 -0
  18. package/src/components/dropdown/transforms/unsupportedPropsDivider.test.ts +143 -0
  19. package/src/components/dropdown/transforms/unsupportedPropsDivider.ts +26 -0
  20. package/src/components/dropdown/transforms/unsupportedPropsItem.test.ts +123 -0
  21. package/src/components/dropdown/transforms/unsupportedPropsItem.ts +12 -0
  22. package/src/components/dropdown/transforms/unsupportedPropsLink.test.ts +107 -0
  23. package/src/components/dropdown/transforms/unsupportedPropsLink.ts +12 -0
  24. package/src/components/dropdown/transforms/wrapMenu.test.ts +153 -0
  25. package/src/components/dropdown/transforms/wrapMenu.ts +54 -0
  26. package/src/components/dropdown/transforms/wrapTrigger.test.ts +283 -0
  27. package/src/components/dropdown/transforms/wrapTrigger.ts +98 -0
  28. package/src/components/input/transforms/unsupportedProps.test.ts +14 -13
  29. package/src/components/shared/conditions/isChildOf.test.ts +89 -0
  30. package/src/components/shared/conditions/isChildOf.ts +43 -0
  31. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +36 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/tapestry-migration-cli",
3
- "version": "3.4.1-rc.9",
3
+ "version": "3.4.1",
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.4.1-rc.9",
33
+ "@planningcenter/tapestry": "^3.4.1",
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": "8c98e7300610da3514ddea4ea4b2c262441c9912"
53
+ "gitHead": "f005d23b54d1a95f03e0823e6ab0d139af17cead"
54
54
  }
@@ -40,7 +40,7 @@ import { Dropdown } from "@planningcenter/tapestry-react"
40
40
 
41
41
  export default function Test() {
42
42
  return (
43
- <Dropdown title="Actions">
43
+ <Dropdown onClose={handleClose}>
44
44
  <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
45
45
  </Dropdown>
46
46
  )
@@ -53,12 +53,185 @@ export default function Test() {
53
53
  expect(result).not.toContain("Dropdown.Item")
54
54
  })
55
55
 
56
- it("returns null when there is nothing to transform", () => {
56
+ it("applies wrapMenu through the chain", () => {
57
+ const input = `
58
+ import { Dropdown } from "@planningcenter/tapestry-react"
59
+
60
+ export default function Test() {
61
+ return (
62
+ <Dropdown onClose={handleClose}>
63
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
64
+ </Dropdown>
65
+ )
66
+ }
67
+ `.trim()
68
+
69
+ const result = applyTransform(input)
70
+ expect(result).toContain("<DropdownMenu>")
71
+ })
72
+
73
+ it("applies placementIdToMenu through the chain", () => {
74
+ const input = `
75
+ import { Dropdown } from "@planningcenter/tapestry-react"
76
+
77
+ export default function Test() {
78
+ return (
79
+ <Dropdown placement="bottom-end">
80
+ <DropdownMenu>
81
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
82
+ </DropdownMenu>
83
+ </Dropdown>
84
+ )
85
+ }
86
+ `.trim()
87
+
88
+ const result = applyTransform(input)
89
+ expect(result).toContain('<DropdownMenu placement="bottom end"')
90
+ expect(result).not.toMatch(/<Dropdown\s[^>]*placement/)
91
+ })
92
+
93
+ it("applies wrapTrigger through the chain", () => {
94
+ const input = `
95
+ import { Dropdown } from "@planningcenter/tapestry-react"
96
+
97
+ export default function Test() {
98
+ return (
99
+ <Dropdown title="Actions">
100
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
101
+ </Dropdown>
102
+ )
103
+ }
104
+ `.trim()
105
+
106
+ const result = applyTransform(input)
107
+ expect(result).toContain("<DropdownTrigger>")
108
+ expect(result).toContain('<DropdownButton label="Actions"')
109
+ expect(result).not.toContain('title="Actions"')
110
+ })
111
+
112
+ it("applies unsupportedPropsDivider through the chain", () => {
113
+ const input = `
114
+ import { Dropdown, Divider } from "@planningcenter/tapestry-react"
115
+
116
+ export default function Test() {
117
+ return <Dropdown><Divider axis="vertical" /></Dropdown>
118
+ }
119
+ `.trim()
120
+
121
+ const result = applyTransform(input)
122
+ expect(result).toContain("TODO: tapestry-migration (axis)")
123
+ })
124
+
125
+ it("applies dividerToSeparator through the chain", () => {
126
+ const input = `
127
+ import { Dropdown, Divider } from "@planningcenter/tapestry-react"
128
+
129
+ export default function Test() {
130
+ return <Dropdown><Divider /></Dropdown>
131
+ }
132
+ `.trim()
133
+
134
+ const result = applyTransform(input)
135
+ expect(result).toContain("<DropdownSeparator")
136
+ expect(result).not.toContain("<Divider")
137
+ })
138
+
139
+ it("applies unsupportedPropsItem through the chain", () => {
57
140
  const input = `
58
141
  import { Dropdown } from "@planningcenter/tapestry-react"
59
142
 
60
- export default function Test({ children }) {
61
- return <Dropdown title="Actions">{children}</Dropdown>
143
+ export default function Test() {
144
+ return (
145
+ <Dropdown title="Actions">
146
+ <Dropdown.Item as={Link} onSelect={fn}>Edit</Dropdown.Item>
147
+ </Dropdown>
148
+ )
149
+ }
150
+ `.trim()
151
+
152
+ const result = applyTransform(input)
153
+ expect(result).toContain("TODO: tapestry-migration (as)")
154
+ })
155
+
156
+ it("applies unsupportedPropsLink through the chain", () => {
157
+ const input = `
158
+ import { Dropdown } from "@planningcenter/tapestry-react"
159
+
160
+ export default function Test() {
161
+ return (
162
+ <Dropdown title="Actions">
163
+ <Dropdown.Link to="/edit" disabled>Edit</Dropdown.Link>
164
+ </Dropdown>
165
+ )
166
+ }
167
+ `.trim()
168
+
169
+ const result = applyTransform(input)
170
+ expect(result).toContain("TODO: tapestry-migration (disabled)")
171
+ })
172
+
173
+ it("applies auditSpreadProps through the chain", () => {
174
+ const input = `
175
+ import { Dropdown } from "@planningcenter/tapestry-react"
176
+
177
+ export default function Test() {
178
+ return (
179
+ <Dropdown {...props} title="Actions">
180
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
181
+ </Dropdown>
182
+ )
183
+ }
184
+ `.trim()
185
+
186
+ const result = applyTransform(input)
187
+ expect(result).toContain("TODO: tapestry-migration (spreadAttribute)")
188
+ })
189
+
190
+ it("applies unsupportedProps through the chain", () => {
191
+ const input = `
192
+ import { Dropdown } from "@planningcenter/tapestry-react"
193
+
194
+ export default function Test() {
195
+ return <Dropdown variant="outline" onClose={fn}><Dropdown.Item onSelect={fn}>Edit</Dropdown.Item></Dropdown>
196
+ }
197
+ `.trim()
198
+
199
+ const result = applyTransform(input)
200
+ expect(result).toContain("TODO: tapestry-migration (variant)")
201
+ })
202
+
203
+ it("applies moveDropdownImport through the chain", () => {
204
+ const input = `
205
+ import { Dropdown } from "@planningcenter/tapestry-react"
206
+
207
+ export default function Test() {
208
+ return (
209
+ <Dropdown onClose={fn}>
210
+ <DropdownTrigger><DropdownButton label="Actions" /></DropdownTrigger>
211
+ <DropdownMenu><DropdownAction onAction={fn}>Edit</DropdownAction></DropdownMenu>
212
+ </Dropdown>
213
+ )
214
+ }
215
+ `.trim()
216
+
217
+ const result = applyTransform(input)
218
+ expect(result).toContain('from "@planningcenter/tapestry"')
219
+ expect(result).not.toContain('from "@planningcenter/tapestry-react"')
220
+ })
221
+
222
+ it("returns null when there is nothing to transform", () => {
223
+ const input = `
224
+ import { Dropdown } from "@planningcenter/tapestry"
225
+
226
+ export default function Test() {
227
+ return (
228
+ <Dropdown onClose={handleClose}>
229
+ <DropdownTrigger><DropdownButton label="Actions" /></DropdownTrigger>
230
+ <DropdownMenu>
231
+ <DropdownAction onAction={fn}>Edit</DropdownAction>
232
+ </DropdownMenu>
233
+ </Dropdown>
234
+ )
62
235
  }
63
236
  `.trim()
64
237
 
@@ -1,14 +1,36 @@
1
1
  import { Transform } from "jscodeshift"
2
2
 
3
+ import auditSpreadProps from "./transforms/auditSpreadProps"
4
+ import dividerToSeparator from "./transforms/dividerToSeparator"
3
5
  import itemToAction from "./transforms/itemToAction"
4
6
  import linkToLink from "./transforms/linkToLink"
7
+ import moveDropdownImport from "./transforms/moveDropdownImport"
8
+ import placementIdToMenu from "./transforms/placementIdToMenu"
9
+ import unsupportedProps from "./transforms/unsupportedProps"
10
+ import unsupportedPropsDivider from "./transforms/unsupportedPropsDivider"
11
+ import unsupportedPropsItem from "./transforms/unsupportedPropsItem"
12
+ import unsupportedPropsLink from "./transforms/unsupportedPropsLink"
13
+ import wrapMenu from "./transforms/wrapMenu"
14
+ import wrapTrigger from "./transforms/wrapTrigger"
5
15
 
6
- // When moveDropdownImport is added, it must remain last in the chain.
7
16
  const transform: Transform = (fileInfo, api, options) => {
8
17
  let currentSource = fileInfo.source
9
18
  let hasAnyChanges = false
10
19
 
11
- const transforms: Transform[] = [linkToLink, itemToAction]
20
+ const transforms: Transform[] = [
21
+ unsupportedPropsItem,
22
+ unsupportedPropsLink,
23
+ unsupportedPropsDivider,
24
+ itemToAction,
25
+ linkToLink,
26
+ wrapMenu,
27
+ placementIdToMenu,
28
+ wrapTrigger,
29
+ dividerToSeparator,
30
+ auditSpreadProps,
31
+ unsupportedProps,
32
+ moveDropdownImport,
33
+ ]
12
34
 
13
35
  for (const individualTransform of transforms) {
14
36
  const result = individualTransform(
@@ -0,0 +1,110 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./auditSpreadProps"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ const AUDIT_COMMENT =
9
+ "TODO: tapestry-migration (spreadAttribute): Spread props can contain unsupported props, please explore usages and migrate as needed."
10
+
11
+ function applyTransform(source: string): string | null {
12
+ const fileInfo = { path: "test.tsx", source }
13
+ return transform(
14
+ fileInfo,
15
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
16
+ {}
17
+ ) as string | null
18
+ }
19
+
20
+ describe("auditSpreadProps transform", () => {
21
+ it("adds a comment to Dropdown with spread props", () => {
22
+ const input = `
23
+ import { Dropdown } from "@planningcenter/tapestry-react"
24
+
25
+ export default function Test() {
26
+ const props = { onClose: handleClose }
27
+ return (
28
+ <Dropdown {...props} title="Actions">
29
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
30
+ </Dropdown>
31
+ )
32
+ }
33
+ `.trim()
34
+
35
+ const result = applyTransform(input)
36
+ expect(result).toContain(AUDIT_COMMENT)
37
+ expect(result).toContain("{...props}")
38
+ })
39
+
40
+ it("adds a comment for each spread", () => {
41
+ const input = `
42
+ import { Dropdown } from "@planningcenter/tapestry-react"
43
+
44
+ export default function Test() {
45
+ return (
46
+ <Dropdown {...baseProps} {...extraProps} title="Actions">
47
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
48
+ </Dropdown>
49
+ )
50
+ }
51
+ `.trim()
52
+
53
+ const result = applyTransform(input)
54
+ const commentCount = (result?.match(/spreadAttribute/g) || []).length
55
+ expect(commentCount).toBe(2)
56
+ })
57
+
58
+ it("preserves all other attributes", () => {
59
+ const input = `
60
+ import { Dropdown } from "@planningcenter/tapestry-react"
61
+
62
+ export default function Test() {
63
+ return (
64
+ <Dropdown title="Actions" placement="bottom-start" {...props}>
65
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
66
+ </Dropdown>
67
+ )
68
+ }
69
+ `.trim()
70
+
71
+ const result = applyTransform(input)
72
+ expect(result).toContain('title="Actions"')
73
+ expect(result).toContain('placement="bottom-start"')
74
+ expect(result).toContain("{...props}")
75
+ })
76
+
77
+ describe("no changes scenarios", () => {
78
+ it("returns null when no spread props", () => {
79
+ const input = `
80
+ import { Dropdown } from "@planningcenter/tapestry-react"
81
+
82
+ export default function Test() {
83
+ return (
84
+ <Dropdown title="Actions">
85
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
86
+ </Dropdown>
87
+ )
88
+ }
89
+ `.trim()
90
+
91
+ expect(applyTransform(input)).toBe(null)
92
+ })
93
+
94
+ it("returns null when not imported from tapestry-react", () => {
95
+ const input = `
96
+ import { Dropdown } from "some-other-library"
97
+
98
+ export default function Test() {
99
+ return <Dropdown {...props} title="Actions" />
100
+ }
101
+ `.trim()
102
+
103
+ expect(applyTransform(input)).toBe(null)
104
+ })
105
+
106
+ it("returns null for empty file", () => {
107
+ expect(applyTransform("")).toBe(null)
108
+ })
109
+ })
110
+ })
@@ -0,0 +1,10 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { commentOnSpreadPropsFactory } from "../../shared/transformFactories/commentOnSpreadPropsFactory"
4
+
5
+ const transform: Transform = commentOnSpreadPropsFactory({
6
+ targetComponent: "Dropdown",
7
+ targetPackage: "@planningcenter/tapestry-react",
8
+ })
9
+
10
+ export default transform
@@ -0,0 +1,134 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./dividerToSeparator"
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("dividerToSeparator transform", () => {
18
+ it("renames Divider to DropdownSeparator when inside a Dropdown", () => {
19
+ const input = `
20
+ import { Dropdown, Divider } from "@planningcenter/tapestry-react"
21
+
22
+ export default function Test() {
23
+ return <Dropdown><Divider /></Dropdown>
24
+ }
25
+ `.trim()
26
+
27
+ const result = applyTransform(input)
28
+ expect(result).toContain("<DropdownSeparator")
29
+ expect(result).not.toContain("<Divider")
30
+ })
31
+
32
+ it("handles non-self-closing Divider inside a Dropdown", () => {
33
+ const input = `
34
+ import { Dropdown, Divider } from "@planningcenter/tapestry-react"
35
+
36
+ export default function Test() {
37
+ return <Dropdown><Divider></Divider></Dropdown>
38
+ }
39
+ `.trim()
40
+
41
+ const result = applyTransform(input)
42
+ expect(result).toContain("<DropdownSeparator>")
43
+ expect(result).toContain("</DropdownSeparator>")
44
+ expect(result).not.toContain("<Divider")
45
+ })
46
+
47
+ it("renames multiple Dividers inside a Dropdown", () => {
48
+ const input = `
49
+ import { Dropdown, Divider } from "@planningcenter/tapestry-react"
50
+
51
+ export default function Test() {
52
+ return (
53
+ <Dropdown>
54
+ <Divider />
55
+ <Divider />
56
+ </Dropdown>
57
+ )
58
+ }
59
+ `.trim()
60
+
61
+ const result = applyTransform(input)
62
+ const count = (result?.match(/<DropdownSeparator/g) || []).length
63
+ expect(count).toBe(2)
64
+ expect(result).not.toContain("<Divider")
65
+ })
66
+
67
+ it("adds DropdownSeparator import from tapestry and removes Divider import when all Dividers are inside a Dropdown", () => {
68
+ const input = `
69
+ import { Dropdown, Divider } from "@planningcenter/tapestry-react"
70
+
71
+ export default function Test() {
72
+ return <Dropdown><Divider /></Dropdown>
73
+ }
74
+ `.trim()
75
+
76
+ const result = applyTransform(input)
77
+ expect(result).toContain('from "@planningcenter/tapestry"')
78
+ expect(result).toContain("DropdownSeparator")
79
+ expect(result).not.toContain(
80
+ 'import { Dropdown, Divider } from "@planningcenter/tapestry-react"'
81
+ )
82
+ })
83
+
84
+ it("keeps Divider import when some Dividers are outside a Dropdown", () => {
85
+ const input = `
86
+ import { Dropdown, Divider } from "@planningcenter/tapestry-react"
87
+
88
+ export default function Test() {
89
+ return (
90
+ <div>
91
+ <Divider />
92
+ <Dropdown><Divider /></Dropdown>
93
+ </div>
94
+ )
95
+ }
96
+ `.trim()
97
+
98
+ const result = applyTransform(input)
99
+ expect(result).toContain("DropdownSeparator")
100
+ expect(result).toContain(
101
+ 'import { Dropdown, Divider } from "@planningcenter/tapestry-react"'
102
+ )
103
+ })
104
+
105
+ describe("no changes scenarios", () => {
106
+ it("returns null when Divider is not inside a Dropdown", () => {
107
+ const input = `
108
+ import { Divider } from "@planningcenter/tapestry-react"
109
+
110
+ export default function Test() {
111
+ return <Divider />
112
+ }
113
+ `.trim()
114
+
115
+ expect(applyTransform(input)).toBe(null)
116
+ })
117
+
118
+ it("returns null when Divider is not imported from tapestry-react", () => {
119
+ const input = `
120
+ import { Divider } from "some-other-library"
121
+
122
+ export default function Test() {
123
+ return <Divider />
124
+ }
125
+ `.trim()
126
+
127
+ expect(applyTransform(input)).toBe(null)
128
+ })
129
+
130
+ it("returns null for empty file", () => {
131
+ expect(applyTransform("")).toBe(null)
132
+ })
133
+ })
134
+ })
@@ -0,0 +1,67 @@
1
+ import { Transform } from "jscodeshift"
2
+
3
+ import { transformElementName } from "../../shared/actions/transformElementName"
4
+ import { isChildOf } from "../../shared/conditions/isChildOf"
5
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
6
+ import {
7
+ addImport,
8
+ getImportName,
9
+ removeImportFromDeclaration,
10
+ } from "../../shared/transformFactories/helpers/manageImports"
11
+
12
+ const transform: Transform = (fileInfo, api, options) => {
13
+ const j = api.jscodeshift
14
+ const source = j(fileInfo.source)
15
+
16
+ const dropdownLocalName = getImportName(
17
+ "Dropdown",
18
+ "@planningcenter/tapestry-react",
19
+ { j, source }
20
+ )
21
+ if (!dropdownLocalName) return null
22
+
23
+ const result = attributeTransformFactory({
24
+ condition: isChildOf(dropdownLocalName, source, j),
25
+ targetComponent: "Divider",
26
+ targetPackage: "@planningcenter/tapestry-react",
27
+ transform: (element) =>
28
+ transformElementName({ element, name: "DropdownSeparator" }),
29
+ })(fileInfo, api, options)
30
+
31
+ if (!result) return null
32
+
33
+ const updatedSource = j(result as string)
34
+
35
+ addImport({
36
+ component: "DropdownSeparator",
37
+ fromPackage: "@planningcenter/tapestry-react",
38
+ j,
39
+ pkg: "@planningcenter/tapestry",
40
+ source: updatedSource,
41
+ })
42
+
43
+ const dividerLocalName = getImportName(
44
+ "Divider",
45
+ "@planningcenter/tapestry-react",
46
+ { j, source: updatedSource }
47
+ )
48
+ if (dividerLocalName) {
49
+ const dividerStillUsed =
50
+ updatedSource.find(j.JSXOpeningElement, {
51
+ name: { name: dividerLocalName },
52
+ }).length > 0
53
+
54
+ if (!dividerStillUsed) {
55
+ removeImportFromDeclaration(
56
+ updatedSource.find(j.ImportDeclaration, {
57
+ source: { value: "@planningcenter/tapestry-react" },
58
+ }),
59
+ "Divider"
60
+ )
61
+ }
62
+ }
63
+
64
+ return updatedSource.toSource()
65
+ }
66
+
67
+ export default transform
@@ -205,7 +205,7 @@ export default function Test() {
205
205
  expect(result).not.toContain("Dropdown.Item")
206
206
  })
207
207
 
208
- it("does not touch the import statement", () => {
208
+ it("does not touch the tapestry-react import statement", () => {
209
209
  const input = `
210
210
  import { Dropdown } from "@planningcenter/tapestry-react"
211
211
 
@@ -223,6 +223,24 @@ export default function Test() {
223
223
  'import { Dropdown } from "@planningcenter/tapestry-react"'
224
224
  )
225
225
  })
226
+
227
+ it("adds DropdownAction import from tapestry", () => {
228
+ const input = `
229
+ import { Dropdown } from "@planningcenter/tapestry-react"
230
+
231
+ export default function Test() {
232
+ return (
233
+ <Dropdown title="Actions">
234
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
235
+ </Dropdown>
236
+ )
237
+ }
238
+ `.trim()
239
+
240
+ const result = applyTransform(input)
241
+ expect(result).toContain('from "@planningcenter/tapestry"')
242
+ expect(result).toContain("DropdownAction")
243
+ })
226
244
  })
227
245
 
228
246
  describe("multiple items", () => {
@@ -3,11 +3,12 @@ import { Transform } from "jscodeshift"
3
3
  import { transformAttributeName } from "../../shared/actions/transformAttributeName"
4
4
  import { transformElementName } from "../../shared/actions/transformElementName"
5
5
  import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
6
+ import { addImport } from "../../shared/transformFactories/helpers/manageImports"
6
7
 
7
8
  const transform: Transform = attributeTransformFactory({
8
9
  targetComponent: "Dropdown.Item",
9
10
  targetPackage: "@planningcenter/tapestry-react",
10
- transform: (element, { j, options }) => {
11
+ transform: (element, { j, options, source }) => {
11
12
  const onSelectRenamed = transformAttributeName("onSelect", "onAction", {
12
13
  element,
13
14
  j,
@@ -27,7 +28,18 @@ const transform: Transform = attributeTransformFactory({
27
28
  element,
28
29
  name: "DropdownAction",
29
30
  })
30
- return onSelectRenamed || valueRenamed || textRenamed || elementRenamed
31
+ const changed =
32
+ onSelectRenamed || valueRenamed || textRenamed || elementRenamed
33
+ if (changed) {
34
+ addImport({
35
+ component: "DropdownAction",
36
+ fromPackage: "@planningcenter/tapestry-react",
37
+ j,
38
+ pkg: "@planningcenter/tapestry",
39
+ source,
40
+ })
41
+ }
42
+ return changed
31
43
  },
32
44
  })
33
45
 
@@ -186,7 +186,7 @@ export default function Test() {
186
186
  expect(result).toContain("<DropdownLink")
187
187
  })
188
188
 
189
- it("does not touch the import statement", () => {
189
+ it("does not touch the tapestry-react import statement", () => {
190
190
  const input = `
191
191
  import { Dropdown } from "@planningcenter/tapestry-react"
192
192
 
@@ -204,6 +204,24 @@ export default function Test() {
204
204
  'import { Dropdown } from "@planningcenter/tapestry-react"'
205
205
  )
206
206
  })
207
+
208
+ it("adds DropdownLink import from tapestry", () => {
209
+ const input = `
210
+ import { Dropdown } from "@planningcenter/tapestry-react"
211
+
212
+ export default function Test() {
213
+ return (
214
+ <Dropdown title="Nav">
215
+ <Dropdown.Link to="/docs">Docs</Dropdown.Link>
216
+ </Dropdown>
217
+ )
218
+ }
219
+ `.trim()
220
+
221
+ const result = applyTransform(input)
222
+ expect(result).toContain('from "@planningcenter/tapestry"')
223
+ expect(result).toContain("DropdownLink")
224
+ })
207
225
  })
208
226
 
209
227
  describe("no changes scenarios", () => {