docusaurus-plugin-generate-schema-docs 1.7.0 → 1.8.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.
@@ -0,0 +1,346 @@
1
+ /* eslint-env jest */
2
+ /**
3
+ * @jest-environment node
4
+ */
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const gtmScript = require('../scripts/sync-gtm');
8
+ const { execSync } = require('child_process');
9
+ const RefParser = require('@apidevtools/json-schema-ref-parser');
10
+
11
+ jest.mock('fs');
12
+ jest.mock('child_process');
13
+ jest.mock('@apidevtools/json-schema-ref-parser');
14
+
15
+ describe('parseArgs', () => {
16
+ it('should parse --quiet, --json, and --skip-array-sub-properties flags', () => {
17
+ const argv = [
18
+ 'node',
19
+ 'script.js',
20
+ '--quiet',
21
+ '--json',
22
+ '--skip-array-sub-properties',
23
+ ];
24
+ const { isQuiet, isJson, skipArraySubProperties } =
25
+ gtmScript.parseArgs(argv);
26
+ expect(isQuiet).toBe(true);
27
+ expect(isJson).toBe(true);
28
+ expect(skipArraySubProperties).toBe(true);
29
+ });
30
+
31
+ it('should parse --path argument', () => {
32
+ const argv = ['node', 'script.js', '--path=./my-demo'];
33
+ const { siteDir } = gtmScript.parseArgs(argv);
34
+ expect(siteDir).toBe('./my-demo');
35
+ });
36
+
37
+ it('should default siteDir to .', () => {
38
+ const argv = ['node', 'script.js'];
39
+ const { siteDir } = gtmScript.parseArgs(argv);
40
+ expect(siteDir).toBe('.');
41
+ });
42
+ });
43
+
44
+ describe('getLatestSchemaPath', () => {
45
+ const SITE_DIR = '/fake/site';
46
+ const SCHEMAS_BASE_PATH = path.join(SITE_DIR, 'static', 'schemas');
47
+
48
+ beforeEach(() => {
49
+ jest.clearAllMocks();
50
+ });
51
+
52
+ it('should return path to latest version when versions.json exists', () => {
53
+ fs.existsSync.mockImplementation(
54
+ (p) => p === path.join(SITE_DIR, 'versions.json'),
55
+ );
56
+ fs.readFileSync.mockReturnValue('["1.2.0", "1.1.1"]');
57
+ const result = gtmScript.getLatestSchemaPath(SITE_DIR);
58
+ expect(result).toBe(path.join(SCHEMAS_BASE_PATH, '1.2.0'));
59
+ });
60
+
61
+ it('should return path to "next" when versions.json does not exist', () => {
62
+ const nextPath = path.join(SCHEMAS_BASE_PATH, 'next');
63
+ fs.existsSync.mockImplementation((p) => p === nextPath);
64
+ const result = gtmScript.getLatestSchemaPath(SITE_DIR);
65
+ expect(result).toBe(nextPath);
66
+ });
67
+
68
+ it('should return base schemas path as fallback', () => {
69
+ fs.existsSync.mockReturnValue(false);
70
+ const result = gtmScript.getLatestSchemaPath(SITE_DIR);
71
+ expect(result).toBe(SCHEMAS_BASE_PATH);
72
+ });
73
+ });
74
+
75
+ describe('getVariablesFromSchemas', () => {
76
+ afterEach(() => {
77
+ jest.resetAllMocks();
78
+ });
79
+
80
+ const SCHEMA_PATH = '/fake/schemas';
81
+ const mockFiles = {
82
+ [SCHEMA_PATH]: ['complex-event.json', 'components'],
83
+ [path.join(SCHEMA_PATH, 'components')]: ['address.json'],
84
+ };
85
+ const addressSchema = {
86
+ title: 'Address',
87
+ type: 'object',
88
+ properties: {
89
+ street: { type: 'string', description: 'Street name.' },
90
+ city: { type: 'string', description: 'City name.' },
91
+ },
92
+ };
93
+ const complexEventSchema = {
94
+ title: 'Complex Event',
95
+ type: 'object',
96
+ properties: {
97
+ $schema: { type: 'string', description: 'Should now be included.' },
98
+ event: { type: 'string', const: 'user_update' },
99
+ user_data: {
100
+ type: 'object',
101
+ properties: {
102
+ user_id: { type: 'string', description: 'The user ID.' },
103
+ addresses: {
104
+ type: 'array',
105
+ description: 'List of addresses.',
106
+ items: { $ref: './components/address.json' },
107
+ },
108
+ },
109
+ },
110
+ timestamp: { type: 'number', description: 'Event timestamp.' },
111
+ },
112
+ };
113
+ const mockFileContents = {
114
+ [path.join(SCHEMA_PATH, 'complex-event.json')]:
115
+ JSON.stringify(complexEventSchema),
116
+ [path.join(SCHEMA_PATH, 'components', 'address.json')]:
117
+ JSON.stringify(addressSchema),
118
+ };
119
+
120
+ beforeEach(() => {
121
+ fs.readdirSync.mockImplementation((p) => mockFiles[p] || []);
122
+ fs.statSync.mockImplementation((p) => ({
123
+ isDirectory: () => !!mockFiles[p],
124
+ }));
125
+ fs.readFileSync.mockImplementation((p) => mockFileContents[p] || '');
126
+ fs.existsSync.mockImplementation(
127
+ (p) => !!mockFiles[p] || !!mockFileContents[p],
128
+ );
129
+ RefParser.bundle.mockClear();
130
+ });
131
+
132
+ it('should find all top-level variables, including $schema', async () => {
133
+ const bundledSchema = JSON.parse(JSON.stringify(complexEventSchema));
134
+ bundledSchema.properties.user_data.properties.addresses.items =
135
+ addressSchema;
136
+ RefParser.bundle.mockResolvedValue(bundledSchema);
137
+
138
+ const result = await gtmScript.getVariablesFromSchemas(SCHEMA_PATH, {});
139
+
140
+ const expected = expect.arrayContaining([
141
+ expect.objectContaining({ name: '$schema' }),
142
+ expect.objectContaining({ name: 'event' }),
143
+ expect.objectContaining({ name: 'user_data' }),
144
+ expect.objectContaining({ name: 'user_data.user_id' }),
145
+ expect.objectContaining({ name: 'user_data.addresses' }),
146
+ expect.objectContaining({ name: 'user_data.addresses.0.street' }),
147
+ expect.objectContaining({ name: 'user_data.addresses.0.city' }),
148
+ expect.objectContaining({ name: 'timestamp' }),
149
+ ]);
150
+
151
+ expect(result.length).toBe(8);
152
+ expect(result).toEqual(expected);
153
+ });
154
+
155
+ it('should skip array sub-properties when skipArraySubProperties is true', async () => {
156
+ const bundledSchema = JSON.parse(JSON.stringify(complexEventSchema));
157
+ bundledSchema.properties.user_data.properties.addresses.items =
158
+ addressSchema;
159
+ RefParser.bundle.mockResolvedValue(bundledSchema);
160
+
161
+ const result = await gtmScript.getVariablesFromSchemas(SCHEMA_PATH, {
162
+ skipArraySubProperties: true,
163
+ });
164
+
165
+ const expected = [
166
+ '$schema',
167
+ 'event',
168
+ 'user_data',
169
+ 'user_data.user_id',
170
+ 'user_data.addresses',
171
+ 'timestamp',
172
+ ];
173
+
174
+ expect(result.map((r) => r.name)).toEqual(expect.arrayContaining(expected));
175
+ expect(result.length).toBe(expected.length);
176
+ });
177
+ });
178
+
179
+ describe('GTM Synchronization Logic', () => {
180
+ const schemaVariables = [
181
+ { name: '$schema', description: 'The schema version.' },
182
+ { name: 'event', description: 'The event name.' },
183
+ { name: 'user_id', description: 'The user ID.' },
184
+ ];
185
+ const gtmVariables = [
186
+ {
187
+ name: 'DLV - event',
188
+ variableId: '1',
189
+ type: 'v',
190
+ parameter: [{ key: 'name', value: 'event' }],
191
+ },
192
+ {
193
+ name: 'DLV - old_variable',
194
+ variableId: '123',
195
+ type: 'v',
196
+ parameter: [{ key: 'name', value: 'old_variable' }],
197
+ },
198
+ ];
199
+
200
+ beforeEach(() => {
201
+ jest.clearAllMocks();
202
+ });
203
+
204
+ describe('getVariablesToCreate', () => {
205
+ it('should return variables that are in schema but not in GTM', () => {
206
+ const toCreate = gtmScript.getVariablesToCreate(
207
+ schemaVariables,
208
+ gtmVariables,
209
+ );
210
+ expect(toCreate).toEqual([
211
+ { name: '$schema', description: 'The schema version.' },
212
+ { name: 'user_id', description: 'The user ID.' },
213
+ ]);
214
+ });
215
+ });
216
+
217
+ describe('getVariablesToDelete', () => {
218
+ it('should return variables that are in GTM but not in schema', () => {
219
+ const toDelete = gtmScript.getVariablesToDelete(
220
+ schemaVariables,
221
+ gtmVariables,
222
+ );
223
+ expect(toDelete).toEqual([
224
+ {
225
+ name: 'DLV - old_variable',
226
+ variableId: '123',
227
+ type: 'v',
228
+ parameter: [{ key: 'name', value: 'old_variable' }],
229
+ },
230
+ ]);
231
+ });
232
+ });
233
+
234
+ describe('createGtmVariables', () => {
235
+ it('should call execSync to create variables and return their names', () => {
236
+ const toCreate = gtmScript.getVariablesToCreate(
237
+ schemaVariables,
238
+ gtmVariables,
239
+ );
240
+ const created = gtmScript.createGtmVariables(toCreate);
241
+ expect(execSync).toHaveBeenCalledTimes(2);
242
+
243
+ const schemaVarConfig = JSON.stringify({
244
+ notes: "References the '$schema' property. The schema version.",
245
+ parameter: [
246
+ { type: 'INTEGER', key: 'dataLayerVersion', value: '2' },
247
+ { type: 'BOOLEAN', key: 'setDefaultValue', value: 'false' },
248
+ { type: 'TEMPLATE', key: 'name', value: '$schema' },
249
+ ],
250
+ });
251
+ expect(execSync).toHaveBeenCalledWith(
252
+ `gtm variables create --name "DLV - \\$schema" --type v --config '${schemaVarConfig}' --quiet`,
253
+ { stdio: 'inherit' },
254
+ );
255
+
256
+ const userIdVarConfig = JSON.stringify({
257
+ notes: 'The user ID.',
258
+ parameter: [
259
+ { type: 'INTEGER', key: 'dataLayerVersion', value: '2' },
260
+ { type: 'BOOLEAN', key: 'setDefaultValue', value: 'false' },
261
+ { type: 'TEMPLATE', key: 'name', value: 'user_id' },
262
+ ],
263
+ });
264
+ expect(execSync).toHaveBeenCalledWith(
265
+ `gtm variables create --name "DLV - user_id" --type v --config '${userIdVarConfig}' --quiet`,
266
+ { stdio: 'inherit' },
267
+ );
268
+ expect(created).toEqual(['$schema', 'user_id']);
269
+ });
270
+ });
271
+
272
+ describe('deleteGtmVariables', () => {
273
+ it('should call execSync to delete variables and return their names', () => {
274
+ const toDelete = gtmScript.getVariablesToDelete(
275
+ schemaVariables,
276
+ gtmVariables,
277
+ );
278
+ const deleted = gtmScript.deleteGtmVariables(toDelete);
279
+ expect(execSync).toHaveBeenCalledTimes(1);
280
+ expect(execSync).toHaveBeenCalledWith(
281
+ 'gtm variables delete --variable-id 123 --force --quiet',
282
+ { stdio: 'inherit' },
283
+ );
284
+ expect(deleted).toEqual(['old_variable']);
285
+ });
286
+ });
287
+ });
288
+
289
+ describe('main function', () => {
290
+ let mockDeps;
291
+ let logSpy;
292
+
293
+ beforeEach(() => {
294
+ logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
295
+ fs.existsSync.mockReturnValue(true);
296
+ mockDeps = {
297
+ setupGtmWorkspace: jest.fn().mockResolvedValue({
298
+ workspaceName: 'test-workspace',
299
+ workspaceId: '123',
300
+ }),
301
+ syncGtmVariables: jest.fn().mockResolvedValue({
302
+ created: ['var1'],
303
+ deleted: ['var2'],
304
+ inSync: ['var3'],
305
+ }),
306
+ getVariablesFromSchemas: jest
307
+ .fn()
308
+ .mockResolvedValue([{ name: 'event', description: 'Event name' }]),
309
+ getLatestSchemaPath: jest.fn().mockReturnValue('/fake/schemas'),
310
+ assertGtmCliAvailable: jest.fn(),
311
+ logger: gtmScript.logger,
312
+ parseArgs: gtmScript.parseArgs,
313
+ process: {
314
+ exit: jest.fn(),
315
+ },
316
+ };
317
+ });
318
+
319
+ afterEach(() => {
320
+ jest.restoreAllMocks();
321
+ });
322
+
323
+ it('should run quietly when --quiet is passed', async () => {
324
+ const argv = ['node', 'script.js', '--quiet'];
325
+ await gtmScript.main(argv, mockDeps);
326
+ expect(logSpy).not.toHaveBeenCalled();
327
+ });
328
+
329
+ it('should output JSON when --json is passed', async () => {
330
+ const argv = ['node', 'script.js', '--json'];
331
+ await gtmScript.main(argv, mockDeps);
332
+ expect(logSpy).toHaveBeenCalledTimes(1);
333
+ expect(JSON.parse(logSpy.mock.calls[0][0])).toEqual({
334
+ workspace: { workspaceName: 'test-workspace', workspaceId: '123' },
335
+ created: ['var1'],
336
+ deleted: ['var2'],
337
+ inSync: ['var3'],
338
+ });
339
+ });
340
+
341
+ it('should use the path from --path argument', async () => {
342
+ const argv = ['node', 'script.js', '--path=./my-demo'];
343
+ await gtmScript.main(argv, mockDeps);
344
+ expect(mockDeps.getLatestSchemaPath).toHaveBeenCalledWith('./my-demo');
345
+ });
346
+ });
@@ -10,13 +10,16 @@ import validateSchemas from '../validateSchemas';
10
10
  describe('validateSchemas', () => {
11
11
  let tmpDir;
12
12
  let consoleErrorSpy;
13
- let consoleLogSpy;
13
+ let consoleWarnSpy;
14
14
 
15
15
  beforeEach(() => {
16
16
  tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'schema-test-'));
17
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
18
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
17
19
  });
18
20
 
19
21
  afterEach(() => {
22
+ jest.restoreAllMocks();
20
23
  fs.rmSync(tmpDir, { recursive: true, force: true });
21
24
  });
22
25
 
@@ -50,4 +53,53 @@ describe('validateSchemas', () => {
50
53
  const result = await validateSchemas(schemaDir);
51
54
  expect(result).toBe(false);
52
55
  });
56
+
57
+ it('should warn and fallback to default target when x-tracking-targets is missing', async () => {
58
+ const schemaDir = path.join(tmpDir, 'schemas');
59
+ fs.mkdirSync(schemaDir, { recursive: true });
60
+ fs.writeFileSync(
61
+ path.join(schemaDir, 'event.json'),
62
+ JSON.stringify({
63
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
64
+ type: 'object',
65
+ properties: {
66
+ event: {
67
+ type: 'string',
68
+ const: 'test_event',
69
+ },
70
+ },
71
+ required: ['event'],
72
+ }),
73
+ );
74
+
75
+ const result = await validateSchemas(schemaDir);
76
+ expect(result).toBe(true);
77
+ expect(consoleWarnSpy).toHaveBeenCalled();
78
+ expect(consoleWarnSpy.mock.calls[0][0]).toContain('web-datalayer-js');
79
+ });
80
+
81
+ it('should fail when x-tracking-targets has an unsupported target', async () => {
82
+ const schemaDir = path.join(tmpDir, 'schemas');
83
+ fs.mkdirSync(schemaDir, { recursive: true });
84
+ fs.writeFileSync(
85
+ path.join(schemaDir, 'event.json'),
86
+ JSON.stringify({
87
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
88
+ type: 'object',
89
+ 'x-tracking-targets': ['web-not-supported-js'],
90
+ properties: {
91
+ event: {
92
+ type: 'string',
93
+ const: 'test_event',
94
+ },
95
+ },
96
+ required: ['event'],
97
+ }),
98
+ );
99
+
100
+ const result = await validateSchemas(schemaDir);
101
+ expect(result).toBe(false);
102
+ expect(consoleErrorSpy).toHaveBeenCalled();
103
+ expect(consoleErrorSpy.mock.calls[0][0]).toContain('x-tracking-targets');
104
+ });
53
105
  });
@@ -1,80 +1,215 @@
1
- import React from 'react';
1
+ import React, { useEffect, useMemo } from 'react';
2
2
  import CodeBlock from '@theme/CodeBlock';
3
3
  import Tabs from '@theme/Tabs';
4
4
  import TabItem from '@theme/TabItem';
5
5
  import Heading from '@theme/Heading';
6
- import { schemaToExamples } from '../helpers/schemaToExamples';
6
+ import {
7
+ buildExampleModel,
8
+ findClearableProperties,
9
+ } from '../helpers/exampleModel';
7
10
 
8
- const generateCodeSnippet = (example, schema, dataLayerName = 'dataLayer') => {
9
- const clearableProperties = findClearableProperties(schema || {});
10
- let codeSnippet = '';
11
- const propertiesToClear = clearableProperties.filter(
12
- (prop) => prop in example,
13
- );
11
+ const TARGET_HASH_KEY = 'target';
12
+ const TARGET_HASH_PREFIX = `${TARGET_HASH_KEY}-`;
13
+ const TARGET_STORAGE_KEY = 'tracking-docs-selected-target';
14
14
 
15
- if (propertiesToClear.length > 0) {
16
- const resetObject = {};
17
- propertiesToClear.forEach((prop) => {
18
- resetObject[prop] = null;
19
- });
20
- codeSnippet += `window.${dataLayerName}.push(${JSON.stringify(
21
- resetObject,
22
- null,
23
- 2,
24
- )});\n`;
15
+ function parseHashTarget(rawHash = '') {
16
+ const raw = rawHash.startsWith('#') ? rawHash.substring(1) : rawHash;
17
+ if (!raw) return null;
18
+ if (raw.startsWith(TARGET_HASH_PREFIX)) {
19
+ return raw.substring(TARGET_HASH_PREFIX.length) || null;
25
20
  }
21
+ return null;
22
+ }
23
+
24
+ function readHashTarget() {
25
+ if (typeof window === 'undefined') return null;
26
+ return parseHashTarget(window.location.hash || '');
27
+ }
28
+
29
+ function readSearchTarget(search = '') {
30
+ const query = search.startsWith('?') ? search.substring(1) : search;
31
+ if (!query) return null;
32
+ const params = new URLSearchParams(query);
33
+ return params.get(TARGET_HASH_KEY);
34
+ }
26
35
 
27
- codeSnippet += `window.${dataLayerName}.push(${JSON.stringify(
28
- example,
36
+ function persistTarget(targetId) {
37
+ if (typeof window === 'undefined') return;
38
+ window.localStorage.setItem(TARGET_STORAGE_KEY, targetId);
39
+ window.history.replaceState(
29
40
  null,
30
- 2,
31
- )});`;
32
- return codeSnippet;
33
- };
41
+ '',
42
+ `${window.location.pathname}${window.location.search}#${TARGET_HASH_PREFIX}${targetId}`,
43
+ );
44
+ }
45
+
46
+ function resolveInitialTargetId(targets) {
47
+ const safeTargets = Array.isArray(targets)
48
+ ? targets.filter((t) => t && typeof t.id === 'string' && t.id.length > 0)
49
+ : [];
50
+ if (safeTargets.length === 0) return null;
51
+ const validTargetIds = new Set(safeTargets.map((t) => t.id));
52
+
53
+ const fromHash = readHashTarget();
54
+ if (fromHash && validTargetIds.has(fromHash)) {
55
+ return fromHash;
56
+ }
57
+
58
+ if (typeof window !== 'undefined') {
59
+ const fromStorage = window.localStorage.getItem(TARGET_STORAGE_KEY);
60
+ if (fromStorage && validTargetIds.has(fromStorage)) {
61
+ return fromStorage;
62
+ }
63
+ }
64
+
65
+ return safeTargets[0].id;
66
+ }
34
67
 
35
68
  export default function ExampleDataLayer({ schema, dataLayerName }) {
36
- const exampleGroups = schemaToExamples(schema);
69
+ const model = useMemo(
70
+ () => buildExampleModel(schema, { dataLayerName }),
71
+ [schema, dataLayerName],
72
+ );
73
+ const safeTargets = useMemo(
74
+ () =>
75
+ (Array.isArray(model.targets) ? model.targets : []).filter(
76
+ (target) =>
77
+ target && typeof target.id === 'string' && target.id.length > 0,
78
+ ),
79
+ [model.targets],
80
+ );
81
+ const exampleGroups = model.variantGroups;
82
+ const targetId = resolveInitialTargetId(safeTargets);
83
+ const showTargetTabs = safeTargets.length > 1;
84
+ const validTargetIds = useMemo(
85
+ () => new Set(safeTargets.map((target) => target.id)),
86
+ [safeTargets],
87
+ );
88
+
89
+ useEffect(() => {
90
+ if (typeof window === 'undefined') return undefined;
91
+ let isSyncing = false;
92
+ const originalReplaceState = window.history.replaceState;
93
+
94
+ const syncFromSearch = () => {
95
+ if (isSyncing) return;
96
+ const targetFromSearch = readSearchTarget(window.location.search);
97
+ if (!targetFromSearch || !validTargetIds.has(targetFromSearch)) return;
98
+
99
+ isSyncing = true;
100
+ try {
101
+ window.localStorage.setItem(TARGET_STORAGE_KEY, targetFromSearch);
102
+
103
+ const searchParams = new URLSearchParams(window.location.search);
104
+ searchParams.delete(TARGET_HASH_KEY);
105
+ const remainingSearch = searchParams.toString();
106
+ const searchPart = remainingSearch ? `?${remainingSearch}` : '';
107
+
108
+ originalReplaceState.call(
109
+ window.history,
110
+ null,
111
+ '',
112
+ `${window.location.pathname}${searchPart}#${TARGET_HASH_PREFIX}${targetFromSearch}`,
113
+ );
114
+ } finally {
115
+ isSyncing = false;
116
+ }
117
+ };
118
+
119
+ syncFromSearch();
120
+ window.history.replaceState = function patchedReplaceState(...args) {
121
+ const result = originalReplaceState.apply(window.history, args);
122
+ syncFromSearch();
123
+ return result;
124
+ };
125
+
126
+ return () => {
127
+ window.history.replaceState = originalReplaceState;
128
+ };
129
+ }, [validTargetIds]);
37
130
 
38
131
  if (!exampleGroups || exampleGroups.length === 0) {
39
132
  return null;
40
133
  }
41
134
 
42
- // Handle the simple case of a single default example with no choices
43
- if (exampleGroups.length === 1 && exampleGroups[0].property === 'default') {
44
- const codeSnippet = generateCodeSnippet(
45
- exampleGroups[0].options[0].example,
46
- schema,
47
- dataLayerName,
135
+ const getLanguageForTarget = (targetIdForSnippet) =>
136
+ safeTargets.find((target) => target.id === targetIdForSnippet)?.language ||
137
+ 'javascript';
138
+
139
+ const renderVariantGroups = (currentTargetId) => (
140
+ <>
141
+ {exampleGroups.map((group) => {
142
+ const showVariantTabs = group.options.length > 1;
143
+
144
+ if (!showVariantTabs) {
145
+ return (
146
+ <div key={group.property} style={{ marginTop: '20px' }}>
147
+ {!model.isSimpleDefault && (
148
+ <Heading as="h4">
149
+ <code>{group.property}</code> options:
150
+ </Heading>
151
+ )}
152
+ <CodeBlock language={getLanguageForTarget(currentTargetId)}>
153
+ {group.options[0].snippets[currentTargetId]}
154
+ </CodeBlock>
155
+ </div>
156
+ );
157
+ }
158
+
159
+ return (
160
+ <div key={group.property} style={{ marginTop: '20px' }}>
161
+ <Heading as="h4">
162
+ <code>{group.property}</code> options:
163
+ </Heading>
164
+ <Tabs>
165
+ {group.options.map(({ id, title, snippets }) => (
166
+ <TabItem value={id} label={title} key={id}>
167
+ <CodeBlock language={getLanguageForTarget(currentTargetId)}>
168
+ {snippets[currentTargetId]}
169
+ </CodeBlock>
170
+ </TabItem>
171
+ ))}
172
+ </Tabs>
173
+ </div>
174
+ );
175
+ })}
176
+ </>
177
+ );
178
+
179
+ // Single target + single default variant => keep old layout
180
+ if (!showTargetTabs && model.isSimpleDefault) {
181
+ const snippets = exampleGroups[0].options[0].snippets || {};
182
+ const codeSnippet =
183
+ (targetId && snippets[targetId]) || Object.values(snippets)[0] || '';
184
+ return (
185
+ <CodeBlock language={getLanguageForTarget(targetId)}>
186
+ {codeSnippet}
187
+ </CodeBlock>
48
188
  );
49
- return <CodeBlock language="javascript">{codeSnippet}</CodeBlock>;
189
+ }
190
+
191
+ if (!showTargetTabs) {
192
+ return renderVariantGroups(targetId);
50
193
  }
51
194
 
52
195
  return (
53
- <>
54
- {exampleGroups.map((group) => (
55
- <div key={group.property} style={{ marginTop: '20px' }}>
56
- <Heading as="h4">
57
- <code>{group.property}</code> options:
58
- </Heading>
59
- <Tabs>
60
- {group.options.map(({ title, example }, index) => (
61
- <TabItem value={index} label={title} key={index}>
62
- <CodeBlock language="javascript">
63
- {generateCodeSnippet(example, schema, dataLayerName)}
64
- </CodeBlock>
65
- </TabItem>
66
- ))}
67
- </Tabs>
68
- </div>
69
- ))}
70
- </>
196
+ <div data-testid="target-tabs">
197
+ <Tabs defaultValue={targetId} queryString={TARGET_HASH_KEY}>
198
+ {safeTargets.map((target) => (
199
+ <TabItem value={target.id} label={target.label} key={target.id}>
200
+ <span id={`${TARGET_HASH_PREFIX}${target.id}`} />
201
+ {renderVariantGroups(target.id)}
202
+ </TabItem>
203
+ ))}
204
+ </Tabs>
205
+ </div>
71
206
  );
72
207
  }
73
208
 
74
- export const findClearableProperties = (schema) => {
75
- if (!schema || !schema.properties) return [];
76
-
77
- return Object.entries(schema.properties)
78
- .filter(([, definition]) => definition['x-gtm-clear'] === true)
79
- .map(([key]) => key);
209
+ export {
210
+ findClearableProperties,
211
+ resolveInitialTargetId,
212
+ readHashTarget,
213
+ readSearchTarget,
214
+ TARGET_STORAGE_KEY,
80
215
  };