@planningcenter/tapestry-migration-cli 2.3.0-rc.6 → 2.3.0-rc.8
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 +5064 -0
- package/package.json +9 -5
- package/src/components/button/index.ts +45 -3
- package/src/components/button/transforms/auditSpreadProps.test.ts +352 -0
- package/src/components/button/transforms/auditSpreadProps.ts +24 -0
- package/src/components/button/transforms/childrenToLabel.test.ts +363 -0
- package/src/components/button/transforms/childrenToLabel.ts +84 -0
- package/src/components/button/transforms/commentOnVisualKindDifference.ts +24 -0
- package/src/components/button/transforms/convertStyleProps.test.ts +464 -0
- package/src/components/button/transforms/convertStyleProps.ts +16 -0
- package/src/components/button/transforms/iconToIconButton.test.ts +377 -0
- package/src/components/button/transforms/iconToIconButton.ts +53 -0
- package/src/components/button/transforms/removeAsButton.ts +15 -0
- package/src/components/button/transforms/removeDuplicateKeys.test.ts +302 -0
- package/src/components/button/transforms/removeDuplicateKeys.ts +8 -0
- package/src/components/button/transforms/reviewStyles.ts +17 -0
- package/src/components/button/transforms/spinnerToLoadingButton.test.ts +165 -0
- package/src/components/button/transforms/spinnerToLoadingButton.ts +14 -0
- package/src/components/button/transforms/unsupportedProps.ts +73 -0
- package/src/components/shared/actions/addCommentToAttribute.test.ts +45 -0
- package/src/components/shared/actions/addCommentToAttribute.ts +28 -0
- package/src/components/shared/actions/addCommentToUnsupportedProps.ts +29 -0
- package/src/components/shared/actions/getSpreadProps.ts +7 -0
- package/src/components/shared/actions/hasSpreadProps.ts +7 -0
- package/src/components/shared/actions/removeChildren.ts +7 -0
- package/src/components/shared/actions/removeDuplicateKeys.test.ts +280 -0
- package/src/components/shared/actions/removeDuplicateKeys.ts +45 -0
- package/src/components/shared/actions/removeUnusedImport.test.ts +302 -0
- package/src/components/shared/actions/removeUnusedImport.ts +81 -0
- package/src/components/shared/actions/transformElementName.test.ts +9 -9
- package/src/components/shared/actions/transformElementName.ts +13 -16
- package/src/components/shared/conditions/hasChildren.ts +5 -0
- package/src/components/shared/getJavaScriptTheme.ts +68 -0
- package/src/components/shared/jsThemeLoader.ts +85 -0
- package/src/components/shared/transformFactories/attributeTransformFactory.ts +14 -6
- package/src/components/shared/transformFactories/componentTransformFactory.ts +1 -1
- package/src/components/shared/transformFactories/stylePropTransformFactory.ts +362 -0
- package/src/index.ts +4 -0
- package/src/stubs/stackViewPlugin.ts +33 -0
- package/src/stubs/tapestry-stub.ts +16 -0
- package/src/tapestry-react-shim.ts +7 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import jscodeshift from "jscodeshift"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import transform from "./childrenToLabel"
|
|
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("childrenToLabel transform", () => {
|
|
18
|
+
describe("simple text conversion", () => {
|
|
19
|
+
it("should convert simple text children to label prop", () => {
|
|
20
|
+
const input = `
|
|
21
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
22
|
+
|
|
23
|
+
function Test() {
|
|
24
|
+
return <Button>Save</Button>
|
|
25
|
+
}
|
|
26
|
+
`.trim()
|
|
27
|
+
|
|
28
|
+
const result = applyTransform(input)
|
|
29
|
+
expect(result).toContain('<Button label="Save" />')
|
|
30
|
+
expect(result).not.toContain("Save</Button>")
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("should handle text with whitespace", () => {
|
|
34
|
+
const input = `
|
|
35
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
36
|
+
|
|
37
|
+
function Test() {
|
|
38
|
+
return <Button> Save Changes </Button>
|
|
39
|
+
}
|
|
40
|
+
`.trim()
|
|
41
|
+
|
|
42
|
+
const result = applyTransform(input)
|
|
43
|
+
expect(result).toContain('<Button label="Save Changes" />')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("should convert string literal expressions", () => {
|
|
47
|
+
const input = `
|
|
48
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
49
|
+
|
|
50
|
+
function Test() {
|
|
51
|
+
return <Button>{"Delete"}</Button>
|
|
52
|
+
}
|
|
53
|
+
`.trim()
|
|
54
|
+
|
|
55
|
+
const result = applyTransform(input)
|
|
56
|
+
expect(result).toContain('<Button label="Delete" />')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("should convert simple template literals without expressions", () => {
|
|
60
|
+
const input = `
|
|
61
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
62
|
+
|
|
63
|
+
function Test() {
|
|
64
|
+
return <Button>{\`Submit Form\`}</Button>
|
|
65
|
+
}
|
|
66
|
+
`.trim()
|
|
67
|
+
|
|
68
|
+
const result = applyTransform(input)
|
|
69
|
+
expect(result).toContain('<Button label="Submit Form" />')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it("should combine multiple text nodes", () => {
|
|
73
|
+
const input = `
|
|
74
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
75
|
+
|
|
76
|
+
function Test() {
|
|
77
|
+
return <Button>Save{""}{"File"}</Button>
|
|
78
|
+
}
|
|
79
|
+
`.trim()
|
|
80
|
+
|
|
81
|
+
const result = applyTransform(input)
|
|
82
|
+
expect(result).toContain('<Button label="SaveFile" />')
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe("complex children scenarios", () => {
|
|
87
|
+
it("should add comment for complex JSX children", () => {
|
|
88
|
+
const input = `
|
|
89
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
90
|
+
|
|
91
|
+
function Test() {
|
|
92
|
+
return (
|
|
93
|
+
<div>
|
|
94
|
+
<Button>
|
|
95
|
+
<Icon name="save" />
|
|
96
|
+
Save
|
|
97
|
+
</Button>
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
`.trim()
|
|
102
|
+
|
|
103
|
+
const result = applyTransform(input)
|
|
104
|
+
expect(result).toContain(
|
|
105
|
+
"{/* TODO: tapestry-migration (children): complex children cannot be converted to label prop"
|
|
106
|
+
)
|
|
107
|
+
expect(result).toContain(
|
|
108
|
+
"take time to find the right text for the button"
|
|
109
|
+
)
|
|
110
|
+
expect(result).toContain(
|
|
111
|
+
"prefix and suffix to correctly display those icons"
|
|
112
|
+
)
|
|
113
|
+
expect(result).toContain('<Icon name="save" />')
|
|
114
|
+
expect(result).toContain("Save")
|
|
115
|
+
expect(result).toContain("</Button>")
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("should add comment for expression children", () => {
|
|
119
|
+
const input = `
|
|
120
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
121
|
+
|
|
122
|
+
function Test() {
|
|
123
|
+
return (
|
|
124
|
+
<div>
|
|
125
|
+
<Button>{isLoading ? "Loading..." : "Submit"}</Button>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
`.trim()
|
|
130
|
+
|
|
131
|
+
const result = applyTransform(input)
|
|
132
|
+
expect(result).toContain(
|
|
133
|
+
"{/* TODO: tapestry-migration (children): complex children cannot be converted to label prop"
|
|
134
|
+
)
|
|
135
|
+
expect(result).toContain('{isLoading ? "Loading..." : "Submit"}')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it("should add comment for template literals with expressions", () => {
|
|
139
|
+
const input = `
|
|
140
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
141
|
+
|
|
142
|
+
function Test() {
|
|
143
|
+
return (
|
|
144
|
+
<div>
|
|
145
|
+
<Button>{\`Save \${count} items\`}</Button>
|
|
146
|
+
</div>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
`.trim()
|
|
150
|
+
|
|
151
|
+
const result = applyTransform(input)
|
|
152
|
+
expect(result).toContain(
|
|
153
|
+
"{/* TODO: tapestry-migration (children): complex children cannot be converted to label prop"
|
|
154
|
+
)
|
|
155
|
+
expect(result).toContain("{`Save ${count} items`}")
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe("existing label prop scenarios", () => {
|
|
160
|
+
it("should add comment when Button has both label and children", () => {
|
|
161
|
+
const input = `
|
|
162
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
163
|
+
|
|
164
|
+
function Test() {
|
|
165
|
+
return (
|
|
166
|
+
<div>
|
|
167
|
+
<Button label="Existing Label">
|
|
168
|
+
Child Content
|
|
169
|
+
</Button>
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
`.trim()
|
|
174
|
+
|
|
175
|
+
const result = applyTransform(input)
|
|
176
|
+
expect(result).toContain(
|
|
177
|
+
"{/* TODO: tapestry-migration (label): Button has both label prop and children"
|
|
178
|
+
)
|
|
179
|
+
expect(result).toContain(
|
|
180
|
+
"take time to find the right text for the button"
|
|
181
|
+
)
|
|
182
|
+
expect(result).toContain('label="Existing Label"')
|
|
183
|
+
expect(result).toContain("Child Content")
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it("should handle Button with label and simple text children", () => {
|
|
187
|
+
const input = `
|
|
188
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
189
|
+
|
|
190
|
+
function Test() {
|
|
191
|
+
return (
|
|
192
|
+
<div>
|
|
193
|
+
<Button label="Save">Click Me</Button>
|
|
194
|
+
</div>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
`.trim()
|
|
198
|
+
|
|
199
|
+
const result = applyTransform(input)
|
|
200
|
+
expect(result).toContain(
|
|
201
|
+
"{/* TODO: tapestry-migration (label): Button has both label prop and children"
|
|
202
|
+
)
|
|
203
|
+
expect(result).toContain('label="Save"')
|
|
204
|
+
expect(result).toContain("Click Me")
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
describe("edge cases", () => {
|
|
209
|
+
it("should skip Buttons with no children", () => {
|
|
210
|
+
const input = `
|
|
211
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
212
|
+
|
|
213
|
+
function Test() {
|
|
214
|
+
return <Button onClick={handleClick} />
|
|
215
|
+
}
|
|
216
|
+
`.trim()
|
|
217
|
+
|
|
218
|
+
const result = applyTransform(input)
|
|
219
|
+
expect(result).toBe(null) // No changes
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it("should skip Buttons with only whitespace children", () => {
|
|
223
|
+
const input = `
|
|
224
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
225
|
+
|
|
226
|
+
function Test() {
|
|
227
|
+
return <Button> </Button>
|
|
228
|
+
}
|
|
229
|
+
`.trim()
|
|
230
|
+
|
|
231
|
+
const result = applyTransform(input)
|
|
232
|
+
expect(result).toBe(null) // No changes since whitespace is trimmed to empty
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it("should handle mixed whitespace and text", () => {
|
|
236
|
+
const input = `
|
|
237
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
238
|
+
|
|
239
|
+
function Test() {
|
|
240
|
+
return <Button> Submit Form </Button>
|
|
241
|
+
}
|
|
242
|
+
`.trim()
|
|
243
|
+
|
|
244
|
+
const result = applyTransform(input)
|
|
245
|
+
expect(result).toContain('<Button label="Submit Form" />')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it("should not transform Button from other packages", () => {
|
|
249
|
+
const input = `
|
|
250
|
+
import { Button } from "@other/package"
|
|
251
|
+
import { Button as TapestryButton } from "@planningcenter/tapestry-react"
|
|
252
|
+
|
|
253
|
+
function Test() {
|
|
254
|
+
return (
|
|
255
|
+
<div>
|
|
256
|
+
<Button>Should not change</Button>
|
|
257
|
+
<TapestryButton>Should change</TapestryButton>
|
|
258
|
+
</div>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
`.trim()
|
|
262
|
+
|
|
263
|
+
const result = applyTransform(input)
|
|
264
|
+
expect(result).toContain("<Button>Should not change</Button>")
|
|
265
|
+
expect(result).toContain('<TapestryButton label="Should change" />')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it("should handle aliased imports", () => {
|
|
269
|
+
const input = `
|
|
270
|
+
import { Button as TapestryButton } from "@planningcenter/tapestry-react"
|
|
271
|
+
|
|
272
|
+
function Test() {
|
|
273
|
+
return <TapestryButton>Save</TapestryButton>
|
|
274
|
+
}
|
|
275
|
+
`.trim()
|
|
276
|
+
|
|
277
|
+
const result = applyTransform(input)
|
|
278
|
+
expect(result).toContain('<TapestryButton label="Save" />')
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it("should return null for files without tapestry-react Button", () => {
|
|
282
|
+
const input = `
|
|
283
|
+
import { Button } from "react-bootstrap"
|
|
284
|
+
|
|
285
|
+
function Test() {
|
|
286
|
+
return <Button>Save</Button>
|
|
287
|
+
}
|
|
288
|
+
`.trim()
|
|
289
|
+
|
|
290
|
+
const result = applyTransform(input)
|
|
291
|
+
expect(result).toBe(null)
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
describe("multiple buttons", () => {
|
|
296
|
+
it("should handle multiple buttons with different scenarios", () => {
|
|
297
|
+
const input = `
|
|
298
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
299
|
+
|
|
300
|
+
function Test() {
|
|
301
|
+
return (
|
|
302
|
+
<div>
|
|
303
|
+
<Button>Simple Text</Button>
|
|
304
|
+
<Button label="existing">Has Label</Button>
|
|
305
|
+
<Button>
|
|
306
|
+
<Icon name="save" />
|
|
307
|
+
Complex
|
|
308
|
+
</Button>
|
|
309
|
+
<Button>{"String Expression"}</Button>
|
|
310
|
+
</div>
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
`.trim()
|
|
314
|
+
|
|
315
|
+
const result = applyTransform(input)
|
|
316
|
+
|
|
317
|
+
// Simple text should be converted
|
|
318
|
+
expect(result).toContain('<Button label="Simple Text" />')
|
|
319
|
+
|
|
320
|
+
// Existing label should get comment
|
|
321
|
+
expect(result).toContain(
|
|
322
|
+
"{/* TODO: tapestry-migration (label): Button has both label prop and children"
|
|
323
|
+
)
|
|
324
|
+
expect(result).toContain('label="existing"')
|
|
325
|
+
|
|
326
|
+
// Complex JSX should get comment
|
|
327
|
+
expect(result).toContain(
|
|
328
|
+
"{/* TODO: tapestry-migration (children): complex children cannot be converted to label prop"
|
|
329
|
+
)
|
|
330
|
+
expect(result).toContain('<Icon name="save" />')
|
|
331
|
+
|
|
332
|
+
// String expression should be converted
|
|
333
|
+
expect(result).toContain('<Button label="String Expression" />')
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe("comment formatting", () => {
|
|
338
|
+
it("should properly format JSX comments in JSX context", () => {
|
|
339
|
+
const input = `
|
|
340
|
+
import { Button } from "@planningcenter/tapestry-react"
|
|
341
|
+
|
|
342
|
+
function Test() {
|
|
343
|
+
return (
|
|
344
|
+
<div>
|
|
345
|
+
<Button>
|
|
346
|
+
<span>Complex</span>
|
|
347
|
+
</Button>
|
|
348
|
+
</div>
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
`.trim()
|
|
352
|
+
|
|
353
|
+
const result = applyTransform(input)
|
|
354
|
+
|
|
355
|
+
// Should have proper JSX comment syntax
|
|
356
|
+
expect(result).toMatch(/\{\s*\/\*\s*TODO: tapestry-migration/)
|
|
357
|
+
expect(result).toMatch(/\*\/\s*\}/)
|
|
358
|
+
expect(result).toContain(
|
|
359
|
+
"take time to find the right text for the button"
|
|
360
|
+
)
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { JSXElement } from "jscodeshift"
|
|
2
|
+
|
|
3
|
+
import { addAttribute } from "../../shared/actions/addAttribute"
|
|
4
|
+
import { addComment } from "../../shared/actions/addComment"
|
|
5
|
+
import { removeChildren } from "../../shared/actions/removeChildren"
|
|
6
|
+
import { hasAttribute } from "../../shared/conditions/hasAttribute"
|
|
7
|
+
import { hasChildren } from "../../shared/conditions/hasChildren"
|
|
8
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
9
|
+
|
|
10
|
+
function extractTextContent(children: NonNullable<JSXElement["children"]>): {
|
|
11
|
+
isSimpleText: boolean
|
|
12
|
+
textContent: string
|
|
13
|
+
} {
|
|
14
|
+
let textContent = ""
|
|
15
|
+
|
|
16
|
+
for (const child of children) {
|
|
17
|
+
if (child.type === "JSXText") {
|
|
18
|
+
const text = child.value.trim()
|
|
19
|
+
if (text) textContent += text
|
|
20
|
+
} else if (
|
|
21
|
+
child.type === "JSXExpressionContainer" &&
|
|
22
|
+
child.expression.type === "StringLiteral"
|
|
23
|
+
) {
|
|
24
|
+
textContent += child.expression.value
|
|
25
|
+
} else if (
|
|
26
|
+
child.type === "JSXExpressionContainer" &&
|
|
27
|
+
child.expression.type === "TemplateLiteral" &&
|
|
28
|
+
child.expression.expressions.length === 0
|
|
29
|
+
) {
|
|
30
|
+
// Simple template literal with no expressions like `hello`
|
|
31
|
+
textContent += child.expression.quasis[0].value.raw
|
|
32
|
+
} else {
|
|
33
|
+
// Complex content (JSX elements, expressions, etc.)
|
|
34
|
+
return { isSimpleText: false, textContent: "" }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { isSimpleText: true, textContent }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildComment(message: string): string {
|
|
42
|
+
return `${message} - take time to find the right text for the button. If icons are used in the Button, you can use prefix and suffix to correctly display those icons.`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const transform = attributeTransformFactory({
|
|
46
|
+
condition: hasChildren,
|
|
47
|
+
targetComponent: "Button",
|
|
48
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
49
|
+
transform: (element, { j, source }) => {
|
|
50
|
+
if (hasAttribute("label")(element)) {
|
|
51
|
+
addComment({
|
|
52
|
+
element,
|
|
53
|
+
j,
|
|
54
|
+
scope: "label",
|
|
55
|
+
source,
|
|
56
|
+
text: buildComment("Button has both label prop and children"),
|
|
57
|
+
})
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { isSimpleText, textContent } = extractTextContent(element.children!)
|
|
62
|
+
|
|
63
|
+
if (isSimpleText && textContent) {
|
|
64
|
+
addAttribute({ element, j, name: "label", value: textContent })
|
|
65
|
+
removeChildren(element)
|
|
66
|
+
return true
|
|
67
|
+
} else if (!isSimpleText) {
|
|
68
|
+
addComment({
|
|
69
|
+
element,
|
|
70
|
+
j,
|
|
71
|
+
scope: "children",
|
|
72
|
+
source,
|
|
73
|
+
text: buildComment(
|
|
74
|
+
"complex children cannot be converted to label prop"
|
|
75
|
+
),
|
|
76
|
+
})
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return false
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
export default transform
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { addComment } from "../../shared/actions/addComment"
|
|
2
|
+
import { hasAttributeValue } from "../../shared/conditions/hasAttributeValue"
|
|
3
|
+
import { orConditions } from "../../shared/conditions/orConditions"
|
|
4
|
+
import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
|
|
5
|
+
|
|
6
|
+
export default attributeTransformFactory({
|
|
7
|
+
condition: orConditions(
|
|
8
|
+
hasAttributeValue("theme", "info"),
|
|
9
|
+
hasAttributeValue("theme", "success")
|
|
10
|
+
),
|
|
11
|
+
targetComponent: "Button",
|
|
12
|
+
targetPackage: "@planningcenter/tapestry-react",
|
|
13
|
+
transform: (element, { j, source }) => {
|
|
14
|
+
addComment({
|
|
15
|
+
element,
|
|
16
|
+
j,
|
|
17
|
+
scope: "theme",
|
|
18
|
+
source,
|
|
19
|
+
text: "Your button's theme is 'info' or 'success', which both map to the same 'primary' kind. This will create a visual change, but it is the recommended adjustment.",
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
return true
|
|
23
|
+
},
|
|
24
|
+
})
|