@tanstack/devtools 0.6.23 → 0.7.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/dist/chunk/{XF4JFOLU.js → VZEY7HNC.js} +32 -9
- package/dist/dev.js +3 -3
- package/dist/devtools/{YRFZDV5N.js → 7NDEDZB7.js} +182 -54
- package/dist/devtools/{MBQPV7BO.js → JEZZ2PQE.js} +133 -29
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -3
- package/dist/server.js +2 -2
- package/package.json +3 -3
- package/src/context/devtools-context.test.ts +268 -1
- package/src/context/devtools-context.tsx +29 -9
- package/src/context/use-devtools-context.ts +2 -1
- package/src/styles/use-styles.ts +69 -0
- package/src/tabs/marketplace/plugin-section.tsx +52 -0
- package/src/tabs/marketplace/plugin-utils.test.ts +22 -14
- package/src/tabs/marketplace/plugin-utils.ts +14 -15
- package/src/tabs/plugin-registry.ts +29 -0
- package/src/utils/constants.ts +4 -0
- package/src/utils/get-default-active-plugins.test.ts +194 -0
- package/src/utils/get-default-active-plugins.ts +36 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createContext, createEffect } from 'solid-js'
|
|
2
2
|
import { createStore } from 'solid-js/store'
|
|
3
|
+
import { getDefaultActivePlugins } from '../utils/get-default-active-plugins'
|
|
3
4
|
import { tryParseJson } from '../utils/sanitize'
|
|
4
5
|
import {
|
|
5
6
|
TANSTACK_DEVTOOLS_SETTINGS,
|
|
@@ -49,6 +50,12 @@ export interface TanStackDevtoolsPlugin {
|
|
|
49
50
|
* If not provided, it will be generated based on the name.
|
|
50
51
|
*/
|
|
51
52
|
id?: string
|
|
53
|
+
/**
|
|
54
|
+
* Whether the plugin should be open by default when there are no active plugins.
|
|
55
|
+
* If true, this plugin will be added to activePlugins on initial load when activePlugins is empty.
|
|
56
|
+
* @default false
|
|
57
|
+
*/
|
|
58
|
+
defaultOpen?: boolean
|
|
52
59
|
/**
|
|
53
60
|
* Render the plugin UI by using the provided element. This function will be called
|
|
54
61
|
* when the plugin tab is clicked and expected to be mounted.
|
|
@@ -127,26 +134,39 @@ export function getStateFromLocalStorage(
|
|
|
127
134
|
return existingState
|
|
128
135
|
}
|
|
129
136
|
|
|
130
|
-
const getExistingStateFromStorage = (
|
|
137
|
+
export const getExistingStateFromStorage = (
|
|
131
138
|
config?: TanStackDevtoolsConfig,
|
|
132
139
|
plugins?: Array<TanStackDevtoolsPlugin>,
|
|
133
140
|
) => {
|
|
134
141
|
const existingState = getStateFromLocalStorage(plugins)
|
|
135
142
|
const settings = getSettings()
|
|
136
143
|
|
|
144
|
+
const pluginsWithIds =
|
|
145
|
+
plugins?.map((plugin, i) => {
|
|
146
|
+
const id = generatePluginId(plugin, i)
|
|
147
|
+
return {
|
|
148
|
+
...plugin,
|
|
149
|
+
id,
|
|
150
|
+
}
|
|
151
|
+
}) || []
|
|
152
|
+
|
|
153
|
+
// If no active plugins exist, add plugins with defaultOpen: true
|
|
154
|
+
// Or if there's only 1 plugin, activate it automatically
|
|
155
|
+
let activePlugins = existingState?.activePlugins || []
|
|
156
|
+
|
|
157
|
+
const shouldFillWithDefaultOpenPlugins =
|
|
158
|
+
activePlugins.length === 0 && pluginsWithIds.length > 0
|
|
159
|
+
if (shouldFillWithDefaultOpenPlugins) {
|
|
160
|
+
activePlugins = getDefaultActivePlugins(pluginsWithIds)
|
|
161
|
+
}
|
|
162
|
+
|
|
137
163
|
const state: DevtoolsStore = {
|
|
138
164
|
...initialState,
|
|
139
|
-
plugins:
|
|
140
|
-
plugins?.map((plugin, i) => {
|
|
141
|
-
const id = generatePluginId(plugin, i)
|
|
142
|
-
return {
|
|
143
|
-
...plugin,
|
|
144
|
-
id,
|
|
145
|
-
}
|
|
146
|
-
}) || [],
|
|
165
|
+
plugins: pluginsWithIds,
|
|
147
166
|
state: {
|
|
148
167
|
...initialState.state,
|
|
149
168
|
...existingState,
|
|
169
|
+
activePlugins,
|
|
150
170
|
},
|
|
151
171
|
settings: {
|
|
152
172
|
...initialState.settings,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createEffect, createMemo, useContext } from 'solid-js'
|
|
2
|
+
import { MAX_ACTIVE_PLUGINS } from '../utils/constants.js'
|
|
2
3
|
import { DevtoolsContext } from './devtools-context.jsx'
|
|
3
4
|
import { useDrawContext } from './draw-context.jsx'
|
|
4
5
|
|
|
@@ -50,7 +51,7 @@ export const usePlugins = () => {
|
|
|
50
51
|
const updatedPlugins = isActive
|
|
51
52
|
? prev.state.activePlugins.filter((id) => id !== pluginId)
|
|
52
53
|
: [...prev.state.activePlugins, pluginId]
|
|
53
|
-
if (updatedPlugins.length >
|
|
54
|
+
if (updatedPlugins.length > MAX_ACTIVE_PLUGINS) return prev
|
|
54
55
|
return {
|
|
55
56
|
...prev,
|
|
56
57
|
state: {
|
package/src/styles/use-styles.ts
CHANGED
|
@@ -1168,6 +1168,75 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => {
|
|
|
1168
1168
|
text-transform: uppercase;
|
|
1169
1169
|
letter-spacing: 0.05em;
|
|
1170
1170
|
`,
|
|
1171
|
+
pluginMarketplaceFeatureBanner: css`
|
|
1172
|
+
margin-top: 1rem;
|
|
1173
|
+
padding: 1.25rem 1.5rem;
|
|
1174
|
+
background: ${t(
|
|
1175
|
+
'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
|
|
1176
|
+
'linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%)',
|
|
1177
|
+
)};
|
|
1178
|
+
border-radius: 0.75rem;
|
|
1179
|
+
border: 1px solid ${t(colors.blue[400], colors.blue[800])};
|
|
1180
|
+
box-shadow:
|
|
1181
|
+
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
1182
|
+
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
1183
|
+
`,
|
|
1184
|
+
pluginMarketplaceFeatureBannerContent: css`
|
|
1185
|
+
display: flex;
|
|
1186
|
+
flex-direction: column;
|
|
1187
|
+
gap: 0.75rem;
|
|
1188
|
+
`,
|
|
1189
|
+
pluginMarketplaceFeatureBannerTitle: css`
|
|
1190
|
+
font-size: 1.125rem;
|
|
1191
|
+
font-weight: 700;
|
|
1192
|
+
color: white;
|
|
1193
|
+
margin: 0;
|
|
1194
|
+
display: flex;
|
|
1195
|
+
align-items: center;
|
|
1196
|
+
gap: 0.5rem;
|
|
1197
|
+
`,
|
|
1198
|
+
pluginMarketplaceFeatureBannerIcon: css`
|
|
1199
|
+
width: 24px;
|
|
1200
|
+
height: 24px;
|
|
1201
|
+
display: inline-flex;
|
|
1202
|
+
`,
|
|
1203
|
+
pluginMarketplaceFeatureBannerText: css`
|
|
1204
|
+
font-size: 0.95rem;
|
|
1205
|
+
color: ${t('rgba(255, 255, 255, 0.95)', 'rgba(255, 255, 255, 0.9)')};
|
|
1206
|
+
line-height: 1.5;
|
|
1207
|
+
margin: 0;
|
|
1208
|
+
`,
|
|
1209
|
+
pluginMarketplaceFeatureBannerButton: css`
|
|
1210
|
+
display: inline-flex;
|
|
1211
|
+
align-items: center;
|
|
1212
|
+
gap: 0.5rem;
|
|
1213
|
+
padding: 0.625rem 1.25rem;
|
|
1214
|
+
background: white;
|
|
1215
|
+
color: ${colors.blue[600]};
|
|
1216
|
+
font-weight: 600;
|
|
1217
|
+
font-size: 0.95rem;
|
|
1218
|
+
border-radius: 0.5rem;
|
|
1219
|
+
border: none;
|
|
1220
|
+
cursor: pointer;
|
|
1221
|
+
transition: all 0.2s ease;
|
|
1222
|
+
text-decoration: none;
|
|
1223
|
+
align-self: flex-start;
|
|
1224
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
1225
|
+
|
|
1226
|
+
&:hover {
|
|
1227
|
+
background: ${t(colors.gray[50], colors.gray[100])};
|
|
1228
|
+
transform: translateY(-1px);
|
|
1229
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
&:active {
|
|
1233
|
+
transform: translateY(0);
|
|
1234
|
+
}
|
|
1235
|
+
`,
|
|
1236
|
+
pluginMarketplaceFeatureBannerButtonIcon: css`
|
|
1237
|
+
width: 18px;
|
|
1238
|
+
height: 18px;
|
|
1239
|
+
`,
|
|
1171
1240
|
pluginMarketplaceCardDisabled: css`
|
|
1172
1241
|
opacity: 0.6;
|
|
1173
1242
|
filter: grayscale(0.3);
|
|
@@ -12,6 +12,31 @@ interface PluginSectionComponentProps {
|
|
|
12
12
|
onCardAction: (card: PluginCard) => void
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const StarIcon = () => (
|
|
16
|
+
<svg
|
|
17
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
18
|
+
viewBox="0 0 24 24"
|
|
19
|
+
fill="currentColor"
|
|
20
|
+
>
|
|
21
|
+
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
|
22
|
+
</svg>
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const MailIcon = () => (
|
|
26
|
+
<svg
|
|
27
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
28
|
+
viewBox="0 0 24 24"
|
|
29
|
+
fill="none"
|
|
30
|
+
stroke="currentColor"
|
|
31
|
+
stroke-width="2"
|
|
32
|
+
stroke-linecap="round"
|
|
33
|
+
stroke-linejoin="round"
|
|
34
|
+
>
|
|
35
|
+
<rect x="2" y="4" width="20" height="16" rx="2" />
|
|
36
|
+
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
|
37
|
+
</svg>
|
|
38
|
+
)
|
|
39
|
+
|
|
15
40
|
export const PluginSectionComponent = (props: PluginSectionComponentProps) => {
|
|
16
41
|
const styles = useStyles()
|
|
17
42
|
|
|
@@ -38,6 +63,33 @@ export const PluginSectionComponent = (props: PluginSectionComponentProps) => {
|
|
|
38
63
|
</div>
|
|
39
64
|
|
|
40
65
|
<Show when={!props.isCollapsed()}>
|
|
66
|
+
<Show when={props.section.id === 'featured'}>
|
|
67
|
+
<div class={styles().pluginMarketplaceFeatureBanner}>
|
|
68
|
+
<div class={styles().pluginMarketplaceFeatureBannerContent}>
|
|
69
|
+
<h4 class={styles().pluginMarketplaceFeatureBannerTitle}>
|
|
70
|
+
<span class={styles().pluginMarketplaceFeatureBannerIcon}>
|
|
71
|
+
<StarIcon />
|
|
72
|
+
</span>
|
|
73
|
+
Want to be featured here?
|
|
74
|
+
</h4>
|
|
75
|
+
<p class={styles().pluginMarketplaceFeatureBannerText}>
|
|
76
|
+
If you've built a plugin for TanStack Devtools and would like to
|
|
77
|
+
showcase it in the featured section, we'd love to hear from you!
|
|
78
|
+
Reach out to us to discuss partnership opportunities.
|
|
79
|
+
</p>
|
|
80
|
+
<a
|
|
81
|
+
href="mailto:partners+devtools@tanstack.com?subject=Featured%20Plugin%20Partnership%20Inquiry"
|
|
82
|
+
class={styles().pluginMarketplaceFeatureBannerButton}
|
|
83
|
+
>
|
|
84
|
+
<span class={styles().pluginMarketplaceFeatureBannerButtonIcon}>
|
|
85
|
+
<MailIcon />
|
|
86
|
+
</span>
|
|
87
|
+
Contact Us
|
|
88
|
+
</a>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</Show>
|
|
92
|
+
|
|
41
93
|
<div class={styles().pluginMarketplaceGrid}>
|
|
42
94
|
<For each={props.section.cards}>
|
|
43
95
|
{(card) => (
|
|
@@ -223,10 +223,11 @@ describe('groupIntoSections', () => {
|
|
|
223
223
|
|
|
224
224
|
const sections = groupIntoSections(cards)
|
|
225
225
|
|
|
226
|
-
expect(sections).toHaveLength(
|
|
227
|
-
expect(sections[0]?.id).toBe('
|
|
228
|
-
expect(sections[
|
|
229
|
-
expect(sections[
|
|
226
|
+
expect(sections).toHaveLength(2) // Featured (always present) + Active
|
|
227
|
+
expect(sections[0]?.id).toBe('featured')
|
|
228
|
+
expect(sections[1]?.id).toBe('active')
|
|
229
|
+
expect(sections[1]?.displayName).toBe('✓ Active Plugins')
|
|
230
|
+
expect(sections[1]?.cards).toHaveLength(1)
|
|
230
231
|
})
|
|
231
232
|
|
|
232
233
|
it('should group featured plugins', () => {
|
|
@@ -266,9 +267,11 @@ describe('groupIntoSections', () => {
|
|
|
266
267
|
|
|
267
268
|
const sections = groupIntoSections(cards)
|
|
268
269
|
|
|
269
|
-
expect(sections).toHaveLength(
|
|
270
|
-
expect(sections[0]?.id).toBe('
|
|
271
|
-
expect(sections.
|
|
270
|
+
expect(sections).toHaveLength(2) // Featured (always present) + Active
|
|
271
|
+
expect(sections[0]?.id).toBe('featured')
|
|
272
|
+
expect(sections[1]?.id).toBe('active')
|
|
273
|
+
expect(sections[0]?.cards).toHaveLength(0) // Featured section empty
|
|
274
|
+
expect(sections[1]?.cards).toHaveLength(1) // Active has the plugin
|
|
272
275
|
})
|
|
273
276
|
|
|
274
277
|
it('should group available plugins', () => {
|
|
@@ -286,9 +289,10 @@ describe('groupIntoSections', () => {
|
|
|
286
289
|
|
|
287
290
|
const sections = groupIntoSections(cards)
|
|
288
291
|
|
|
289
|
-
expect(sections).toHaveLength(
|
|
290
|
-
expect(sections[0]?.id).toBe('
|
|
291
|
-
expect(sections[
|
|
292
|
+
expect(sections).toHaveLength(2) // Featured (always present) + Available
|
|
293
|
+
expect(sections[0]?.id).toBe('featured')
|
|
294
|
+
expect(sections[1]?.id).toBe('available')
|
|
295
|
+
expect(sections[1]?.displayName).toBe('Available Plugins')
|
|
292
296
|
})
|
|
293
297
|
|
|
294
298
|
it('should not include featured plugins in available section', () => {
|
|
@@ -345,8 +349,8 @@ describe('groupIntoSections', () => {
|
|
|
345
349
|
const sections = groupIntoSections(cards)
|
|
346
350
|
|
|
347
351
|
expect(sections).toHaveLength(3)
|
|
348
|
-
expect(sections[0]?.id).toBe('
|
|
349
|
-
expect(sections[1]?.id).toBe('
|
|
352
|
+
expect(sections[0]?.id).toBe('featured')
|
|
353
|
+
expect(sections[1]?.id).toBe('active')
|
|
350
354
|
expect(sections[2]?.id).toBe('available')
|
|
351
355
|
})
|
|
352
356
|
|
|
@@ -365,12 +369,16 @@ describe('groupIntoSections', () => {
|
|
|
365
369
|
|
|
366
370
|
const sections = groupIntoSections(cards)
|
|
367
371
|
|
|
368
|
-
expect(sections).toHaveLength(
|
|
372
|
+
expect(sections).toHaveLength(1) // Only featured section (always present, empty)
|
|
373
|
+
expect(sections[0]?.id).toBe('featured')
|
|
374
|
+
expect(sections[0]?.cards).toHaveLength(0)
|
|
369
375
|
})
|
|
370
376
|
|
|
371
377
|
it('should handle empty card array', () => {
|
|
372
378
|
const sections = groupIntoSections([])
|
|
373
|
-
expect(sections).toHaveLength(
|
|
379
|
+
expect(sections).toHaveLength(1) // Featured section always present
|
|
380
|
+
expect(sections[0]?.id).toBe('featured')
|
|
381
|
+
expect(sections[0]?.cards).toHaveLength(0)
|
|
374
382
|
})
|
|
375
383
|
})
|
|
376
384
|
|
|
@@ -201,6 +201,20 @@ export const groupIntoSections = (
|
|
|
201
201
|
): Array<PluginSection> => {
|
|
202
202
|
const sections: Array<PluginSection> = []
|
|
203
203
|
|
|
204
|
+
// Add Featured section first - always show this section
|
|
205
|
+
const featuredCards = allCards.filter(
|
|
206
|
+
(c) =>
|
|
207
|
+
c.metadata?.featured &&
|
|
208
|
+
c.actionType !== 'already-installed' &&
|
|
209
|
+
c.isCurrentFramework, // Only show featured plugins for current framework
|
|
210
|
+
)
|
|
211
|
+
// Always add featured section, even if no cards to show the partner banner
|
|
212
|
+
sections.push({
|
|
213
|
+
id: 'featured',
|
|
214
|
+
displayName: '⭐ Featured',
|
|
215
|
+
cards: featuredCards,
|
|
216
|
+
})
|
|
217
|
+
|
|
204
218
|
// Add Active Plugins section
|
|
205
219
|
const activeCards = allCards.filter(
|
|
206
220
|
(c) => c.actionType === 'already-installed' && c.isRegistered,
|
|
@@ -213,21 +227,6 @@ export const groupIntoSections = (
|
|
|
213
227
|
})
|
|
214
228
|
}
|
|
215
229
|
|
|
216
|
-
// Add Featured section
|
|
217
|
-
const featuredCards = allCards.filter(
|
|
218
|
-
(c) =>
|
|
219
|
-
c.metadata?.featured &&
|
|
220
|
-
c.actionType !== 'already-installed' &&
|
|
221
|
-
c.isCurrentFramework, // Only show featured plugins for current framework
|
|
222
|
-
)
|
|
223
|
-
if (featuredCards.length > 0) {
|
|
224
|
-
sections.push({
|
|
225
|
-
id: 'featured',
|
|
226
|
-
displayName: '⭐ Featured',
|
|
227
|
-
cards: featuredCards,
|
|
228
|
-
})
|
|
229
|
-
}
|
|
230
|
-
|
|
231
230
|
// Add Available section - all plugins for current framework (TanStack + third-party)
|
|
232
231
|
const availableCards = allCards.filter(
|
|
233
232
|
(c) =>
|
|
@@ -212,6 +212,35 @@ const PLUGIN_REGISTRY: Record<string, PluginMetadata> = {
|
|
|
212
212
|
// THIRD-PARTY PLUGINS - Examples
|
|
213
213
|
// ==========================================
|
|
214
214
|
// External contributors can add their plugins below!
|
|
215
|
+
|
|
216
|
+
// Dimano — Prefetch Heatmap for TanStack Router
|
|
217
|
+
'@dimano/ts-devtools-plugin-prefetch-heatmap': {
|
|
218
|
+
packageName: '@dimano/ts-devtools-plugin-prefetch-heatmap',
|
|
219
|
+
title: 'Prefetch Heatmap',
|
|
220
|
+
description:
|
|
221
|
+
'Visualize TanStack Router prefetch intent, hits, and waste with a color overlay and a live metrics panel.',
|
|
222
|
+
requires: {
|
|
223
|
+
packageName: '@tanstack/react-router',
|
|
224
|
+
minVersion: '1.0.0',
|
|
225
|
+
},
|
|
226
|
+
// default export registers the plugin
|
|
227
|
+
pluginImport: {
|
|
228
|
+
importName: 'registerPrefetchHeatmapPlugin',
|
|
229
|
+
type: 'function',
|
|
230
|
+
},
|
|
231
|
+
// helps the host match your plugin deterministically
|
|
232
|
+
pluginId: 'prefetch-heatmap',
|
|
233
|
+
// show a nice card in the marketplace
|
|
234
|
+
logoUrl:
|
|
235
|
+
'https://raw.githubusercontent.com/dimitrianoudi/tanstack-prefetch-heatmap/main/assets/prefetch-heatmap-card.png',
|
|
236
|
+
docsUrl:
|
|
237
|
+
'https://github.com/dimitrianoudi/tanstack-prefetch-heatmap#prefetch-heatmap-devtools-plugin',
|
|
238
|
+
repoUrl: 'https://github.com/dimitrianoudi/tanstack-prefetch-heatmap',
|
|
239
|
+
author: 'Dimitris Anoudis (@dimitrianoudi)',
|
|
240
|
+
framework: 'react',
|
|
241
|
+
isNew: true,
|
|
242
|
+
tags: ['Router', 'Prefetch', 'Analytics', 'Overlay', 'TanStack'],
|
|
243
|
+
},
|
|
215
244
|
}
|
|
216
245
|
|
|
217
246
|
/**
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getDefaultActivePlugins } from './get-default-active-plugins'
|
|
3
|
+
import type { PluginWithId } from './get-default-active-plugins'
|
|
4
|
+
|
|
5
|
+
describe('getDefaultActivePlugins', () => {
|
|
6
|
+
it('should return empty array when no plugins provided', () => {
|
|
7
|
+
const result = getDefaultActivePlugins([])
|
|
8
|
+
expect(result).toEqual([])
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('should automatically activate a single plugin', () => {
|
|
12
|
+
const plugins: Array<PluginWithId> = [
|
|
13
|
+
{
|
|
14
|
+
id: 'only-plugin',
|
|
15
|
+
},
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const result = getDefaultActivePlugins(plugins)
|
|
19
|
+
expect(result).toEqual(['only-plugin'])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should automatically activate a single plugin even if defaultOpen is false', () => {
|
|
23
|
+
const plugins: Array<PluginWithId> = [
|
|
24
|
+
{
|
|
25
|
+
id: 'only-plugin',
|
|
26
|
+
defaultOpen: false,
|
|
27
|
+
},
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const result = getDefaultActivePlugins(plugins)
|
|
31
|
+
expect(result).toEqual(['only-plugin'])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should return empty array when multiple plugins without defaultOpen', () => {
|
|
35
|
+
const plugins: Array<PluginWithId> = [
|
|
36
|
+
{
|
|
37
|
+
id: 'plugin1',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'plugin2',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'plugin3',
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
const result = getDefaultActivePlugins(plugins)
|
|
48
|
+
expect(result).toEqual([])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should activate plugins with defaultOpen: true', () => {
|
|
52
|
+
const plugins: Array<PluginWithId> = [
|
|
53
|
+
{
|
|
54
|
+
id: 'plugin1',
|
|
55
|
+
defaultOpen: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'plugin2',
|
|
59
|
+
defaultOpen: false,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'plugin3',
|
|
63
|
+
defaultOpen: true,
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
const result = getDefaultActivePlugins(plugins)
|
|
68
|
+
expect(result).toEqual(['plugin1', 'plugin3'])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should limit defaultOpen plugins to MAX_ACTIVE_PLUGINS (3)', () => {
|
|
72
|
+
const plugins: Array<PluginWithId> = [
|
|
73
|
+
{
|
|
74
|
+
id: 'plugin1',
|
|
75
|
+
defaultOpen: true,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'plugin2',
|
|
79
|
+
defaultOpen: true,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'plugin3',
|
|
83
|
+
defaultOpen: true,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'plugin4',
|
|
87
|
+
defaultOpen: true,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'plugin5',
|
|
91
|
+
defaultOpen: true,
|
|
92
|
+
},
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
const result = getDefaultActivePlugins(plugins)
|
|
96
|
+
// Should only return first 3
|
|
97
|
+
expect(result).toEqual(['plugin1', 'plugin2', 'plugin3'])
|
|
98
|
+
expect(result.length).toBe(3)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should activate exactly MAX_ACTIVE_PLUGINS when that many have defaultOpen', () => {
|
|
102
|
+
const plugins: Array<PluginWithId> = [
|
|
103
|
+
{
|
|
104
|
+
id: 'plugin1',
|
|
105
|
+
defaultOpen: true,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'plugin2',
|
|
109
|
+
defaultOpen: true,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'plugin3',
|
|
113
|
+
defaultOpen: true,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'plugin4',
|
|
117
|
+
defaultOpen: false,
|
|
118
|
+
},
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
const result = getDefaultActivePlugins(plugins)
|
|
122
|
+
expect(result).toEqual(['plugin1', 'plugin2', 'plugin3'])
|
|
123
|
+
expect(result.length).toBe(3)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should handle mix of defaultOpen true/false/undefined', () => {
|
|
127
|
+
const plugins: Array<PluginWithId> = [
|
|
128
|
+
{
|
|
129
|
+
id: 'plugin1',
|
|
130
|
+
defaultOpen: true,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: 'plugin2',
|
|
134
|
+
// undefined defaultOpen
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'plugin3',
|
|
138
|
+
defaultOpen: false,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 'plugin4',
|
|
142
|
+
defaultOpen: true,
|
|
143
|
+
},
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
const result = getDefaultActivePlugins(plugins)
|
|
147
|
+
// Only plugin1 and plugin4 have defaultOpen: true
|
|
148
|
+
expect(result).toEqual(['plugin1', 'plugin4'])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should return single plugin even if it has defaultOpen: true', () => {
|
|
152
|
+
const plugins: Array<PluginWithId> = [
|
|
153
|
+
{
|
|
154
|
+
id: 'only-plugin',
|
|
155
|
+
defaultOpen: true,
|
|
156
|
+
},
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
const result = getDefaultActivePlugins(plugins)
|
|
160
|
+
expect(result).toEqual(['only-plugin'])
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('should stop at MAX_ACTIVE_PLUGINS limit when 5 plugins have defaultOpen: true', () => {
|
|
164
|
+
const plugins: Array<PluginWithId> = [
|
|
165
|
+
{
|
|
166
|
+
id: 'plugin1',
|
|
167
|
+
defaultOpen: true,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'plugin2',
|
|
171
|
+
defaultOpen: true,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: 'plugin3',
|
|
175
|
+
defaultOpen: true,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: 'plugin4',
|
|
179
|
+
defaultOpen: true,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: 'plugin5',
|
|
183
|
+
defaultOpen: true,
|
|
184
|
+
},
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
const result = getDefaultActivePlugins(plugins)
|
|
188
|
+
// Should only activate the first 3, plugin4 and plugin5 should be ignored
|
|
189
|
+
expect(result).toEqual(['plugin1', 'plugin2', 'plugin3'])
|
|
190
|
+
expect(result.length).toBe(3)
|
|
191
|
+
expect(result).not.toContain('plugin4')
|
|
192
|
+
expect(result).not.toContain('plugin5')
|
|
193
|
+
})
|
|
194
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { MAX_ACTIVE_PLUGINS } from './constants'
|
|
2
|
+
|
|
3
|
+
export interface PluginWithId {
|
|
4
|
+
id: string
|
|
5
|
+
defaultOpen?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Determines which plugins should be active by default when no plugins are currently active.
|
|
10
|
+
*
|
|
11
|
+
* Rules:
|
|
12
|
+
* 1. If there's only 1 plugin, activate it automatically
|
|
13
|
+
* 2. If there are multiple plugins, activate those with defaultOpen: true (up to MAX_ACTIVE_PLUGINS limit)
|
|
14
|
+
* 3. If no plugins have defaultOpen: true, return empty array
|
|
15
|
+
*
|
|
16
|
+
* @param plugins - Array of plugins with IDs
|
|
17
|
+
* @returns Array of plugin IDs that should be active by default
|
|
18
|
+
*/
|
|
19
|
+
export function getDefaultActivePlugins(
|
|
20
|
+
plugins: Array<PluginWithId>,
|
|
21
|
+
): Array<string> {
|
|
22
|
+
if (plugins.length === 0) {
|
|
23
|
+
return []
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// If there's only 1 plugin, activate it automatically
|
|
27
|
+
if (plugins.length === 1) {
|
|
28
|
+
return [plugins[0]!.id]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Otherwise, activate plugins with defaultOpen: true (up to the limit)
|
|
32
|
+
return plugins
|
|
33
|
+
.filter((plugin) => plugin.defaultOpen === true)
|
|
34
|
+
.slice(0, MAX_ACTIVE_PLUGINS)
|
|
35
|
+
.map((plugin) => plugin.id)
|
|
36
|
+
}
|