docusaurus-plugin-generate-schema-docs 1.6.0 → 1.7.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.
- package/README.md +17 -0
- package/__tests__/__fixtures__/static/schemas/battle-test-event.json +771 -0
- package/__tests__/__fixtures__/static/schemas/conditional-event.json +52 -0
- package/__tests__/__fixtures__/static/schemas/nested-conditional-event.json +50 -0
- package/__tests__/components/ConditionalRows.test.js +150 -0
- package/__tests__/components/ConnectorLines.visualRegression.test.js +93 -0
- package/__tests__/components/FoldableRows.test.js +7 -4
- package/__tests__/components/SchemaRows.test.js +31 -0
- package/__tests__/components/__snapshots__/ConnectorLines.visualRegression.test.js.snap +7 -0
- package/__tests__/generateEventDocs.partials.test.js +134 -0
- package/__tests__/helpers/buildExampleFromSchema.test.js +49 -0
- package/__tests__/helpers/schemaToExamples.test.js +75 -0
- package/__tests__/helpers/schemaToTableData.battleTest.test.js +704 -0
- package/__tests__/helpers/schemaToTableData.hierarchicalLines.test.js +190 -7
- package/__tests__/helpers/schemaToTableData.test.js +263 -2
- package/__tests__/helpers/validator.test.js +6 -6
- package/__tests__/syncGtm.test.js +346 -0
- package/components/ConditionalRows.js +156 -0
- package/components/FoldableRows.js +88 -61
- package/components/PropertiesTable.js +1 -1
- package/components/PropertyRow.js +24 -8
- package/components/SchemaRows.css +115 -0
- package/components/SchemaRows.js +31 -4
- package/generateEventDocs.js +41 -34
- package/helpers/buildExampleFromSchema.js +11 -0
- package/helpers/continuingLinesStyle.js +169 -0
- package/helpers/schema-doc-template.js +2 -5
- package/helpers/schemaToExamples.js +75 -2
- package/helpers/schemaToTableData.js +252 -26
- package/helpers/update-schema-ids.js +3 -3
- package/helpers/validator.js +7 -19
- package/index.js +34 -0
- package/package.json +1 -1
- package/scripts/sync-gtm.js +397 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import React, { useState, useId } from 'react';
|
|
2
|
+
import SchemaRows from './SchemaRows';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import {
|
|
5
|
+
getContinuingLinesStyle,
|
|
6
|
+
getBracketLinesStyle,
|
|
7
|
+
mergeBackgroundStyles,
|
|
8
|
+
} from '../helpers/continuingLinesStyle';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Renders 'if/then/else' conditionals as a set of `<tr>` elements
|
|
12
|
+
* that integrate directly into the main table body.
|
|
13
|
+
*
|
|
14
|
+
* Structure:
|
|
15
|
+
* 1. Condition (if) rows: always visible, info-styled
|
|
16
|
+
* 2. Branch toggles (then/else): radio-style, foldable
|
|
17
|
+
*/
|
|
18
|
+
export default function ConditionalRows({
|
|
19
|
+
row,
|
|
20
|
+
bracketEnds: parentBracketEnds,
|
|
21
|
+
}) {
|
|
22
|
+
const {
|
|
23
|
+
condition,
|
|
24
|
+
branches,
|
|
25
|
+
level = 0,
|
|
26
|
+
continuingLevels = [],
|
|
27
|
+
groupBrackets = [],
|
|
28
|
+
} = row;
|
|
29
|
+
const [activeBranch, setActiveBranch] = useState(0);
|
|
30
|
+
const radioGroupId = useId();
|
|
31
|
+
|
|
32
|
+
// Compute this group's own bracket and combine with any parent brackets.
|
|
33
|
+
// bracketIndex = total number of existing brackets, so each group gets a unique position.
|
|
34
|
+
const ownBracket = {
|
|
35
|
+
level,
|
|
36
|
+
bracketIndex: groupBrackets.length,
|
|
37
|
+
};
|
|
38
|
+
const allBrackets = [...groupBrackets, ownBracket];
|
|
39
|
+
|
|
40
|
+
// colSpan=5 rows need continuing ancestor lines to pass through (for visual
|
|
41
|
+
// continuity). They don't have ::before/::after connectors, so we draw all
|
|
42
|
+
// ancestor lines via background gradients. We always include the immediate
|
|
43
|
+
// parent level (level-1) so the parent-to-child connector passes through.
|
|
44
|
+
// Only draw ancestor lines where the next level up is also continuing;
|
|
45
|
+
// this matches PropertyRow's filter to avoid stray lines at a parent's
|
|
46
|
+
// corner connector when that parent is the last in its group.
|
|
47
|
+
const ancestorLevels = continuingLevels.filter(
|
|
48
|
+
(lvl) => lvl < level && continuingLevels.includes(lvl + 1),
|
|
49
|
+
);
|
|
50
|
+
// Always include the immediate parent level so the parent-to-child
|
|
51
|
+
// connector passes through these full-width rows.
|
|
52
|
+
if (level > 0 && !ancestorLevels.includes(level - 1)) {
|
|
53
|
+
ancestorLevels.push(level - 1);
|
|
54
|
+
}
|
|
55
|
+
const treePassthrough = getContinuingLinesStyle(ancestorLevels, 0);
|
|
56
|
+
const indent = { paddingLeft: `${level * 1.25 + 0.5}rem` };
|
|
57
|
+
|
|
58
|
+
// If header: top cap on ownBracket
|
|
59
|
+
const headerBracketStyle = getBracketLinesStyle(allBrackets, {
|
|
60
|
+
starting: [ownBracket],
|
|
61
|
+
});
|
|
62
|
+
const headerMerged = mergeBackgroundStyles(
|
|
63
|
+
treePassthrough,
|
|
64
|
+
headerBracketStyle,
|
|
65
|
+
);
|
|
66
|
+
const headerStyle = { ...headerMerged, ...indent, paddingTop: '0.5rem' };
|
|
67
|
+
|
|
68
|
+
// Middle rows (branch toggles): no caps
|
|
69
|
+
const middleBracketStyle = getBracketLinesStyle(allBrackets);
|
|
70
|
+
const middleMerged = mergeBackgroundStyles(
|
|
71
|
+
treePassthrough,
|
|
72
|
+
middleBracketStyle,
|
|
73
|
+
);
|
|
74
|
+
const middleStyle = { ...middleMerged, ...indent };
|
|
75
|
+
|
|
76
|
+
// Last branch toggle when content is NOT shown: bottom cap (+ parent ends)
|
|
77
|
+
const allEndings = [ownBracket, ...(parentBracketEnds || [])];
|
|
78
|
+
const lastToggleBracketStyle = getBracketLinesStyle(allBrackets, {
|
|
79
|
+
ending: allEndings,
|
|
80
|
+
});
|
|
81
|
+
const lastToggleMerged = mergeBackgroundStyles(
|
|
82
|
+
treePassthrough,
|
|
83
|
+
lastToggleBracketStyle,
|
|
84
|
+
);
|
|
85
|
+
const lastToggleStyle = {
|
|
86
|
+
...lastToggleMerged,
|
|
87
|
+
...indent,
|
|
88
|
+
paddingBottom: '0.5rem',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
{/* Condition (if) section - always visible */}
|
|
94
|
+
<tr className="conditional-condition-header">
|
|
95
|
+
<td colSpan={5} style={headerStyle}>
|
|
96
|
+
<span className="conditional-condition-label">
|
|
97
|
+
<span className="conditional-info-icon-wrapper">
|
|
98
|
+
<span className="conditional-info-icon">i</span>
|
|
99
|
+
<span className="conditional-info-tooltip">
|
|
100
|
+
The properties below define the condition. When the condition is
|
|
101
|
+
met, the “Then” branch applies. Otherwise, the
|
|
102
|
+
“Else” branch applies.
|
|
103
|
+
</span>
|
|
104
|
+
</span>
|
|
105
|
+
<strong>If</strong>
|
|
106
|
+
</span>
|
|
107
|
+
{condition.description && (
|
|
108
|
+
<p className="conditional-condition-description">
|
|
109
|
+
{condition.description}
|
|
110
|
+
</p>
|
|
111
|
+
)}
|
|
112
|
+
</td>
|
|
113
|
+
</tr>
|
|
114
|
+
<SchemaRows tableData={condition.rows} />
|
|
115
|
+
|
|
116
|
+
{/* Branch toggles (then / else) */}
|
|
117
|
+
{branches.map((branch, index) => {
|
|
118
|
+
const isActive = activeBranch === index;
|
|
119
|
+
const isLastBranch = index === branches.length - 1;
|
|
120
|
+
const toggleStyle =
|
|
121
|
+
isLastBranch && !isActive ? lastToggleStyle : middleStyle;
|
|
122
|
+
return (
|
|
123
|
+
<React.Fragment key={branch.title}>
|
|
124
|
+
<tr className="choice-row">
|
|
125
|
+
<td colSpan={5} style={toggleStyle}>
|
|
126
|
+
<label className="choice-row-header">
|
|
127
|
+
<input
|
|
128
|
+
type="radio"
|
|
129
|
+
name={`conditional-${radioGroupId}`}
|
|
130
|
+
checked={isActive}
|
|
131
|
+
onChange={() => setActiveBranch(index)}
|
|
132
|
+
/>
|
|
133
|
+
<span className={clsx('choice-row-toggle', 'radio')} />
|
|
134
|
+
<strong>{branch.title}</strong>
|
|
135
|
+
</label>
|
|
136
|
+
{branch.description && (
|
|
137
|
+
<p className="choice-row-description">{branch.description}</p>
|
|
138
|
+
)}
|
|
139
|
+
</td>
|
|
140
|
+
</tr>
|
|
141
|
+
{isActive && (
|
|
142
|
+
<SchemaRows
|
|
143
|
+
tableData={branch.rows}
|
|
144
|
+
bracketEnds={
|
|
145
|
+
isLastBranch
|
|
146
|
+
? [ownBracket, ...(parentBracketEnds || [])]
|
|
147
|
+
: undefined
|
|
148
|
+
}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
</React.Fragment>
|
|
152
|
+
);
|
|
153
|
+
})}
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -1,60 +1,12 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useState, useId } from 'react';
|
|
2
2
|
import SchemaRows from './SchemaRows';
|
|
3
3
|
import Heading from '@theme/Heading';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* @returns {object} Style object with background gradients
|
|
11
|
-
*/
|
|
12
|
-
const getContinuingLinesStyle = (continuingLevels = [], level = 0) => {
|
|
13
|
-
const getLevelPosition = (lvl) => lvl * 1.25 + 0.5;
|
|
14
|
-
|
|
15
|
-
const allGradients = [];
|
|
16
|
-
const allSizes = [];
|
|
17
|
-
const allPositions = [];
|
|
18
|
-
|
|
19
|
-
// Draw continuing lines for all ancestor levels
|
|
20
|
-
continuingLevels.forEach((lvl) => {
|
|
21
|
-
const pos = getLevelPosition(lvl);
|
|
22
|
-
allGradients.push(
|
|
23
|
-
'linear-gradient(var(--ifm-table-border-color), var(--ifm-table-border-color))',
|
|
24
|
-
);
|
|
25
|
-
allSizes.push('1px 100%');
|
|
26
|
-
allPositions.push(`${pos}rem top`);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// Also draw the line for the immediate parent level (level - 1) if level > 0
|
|
30
|
-
// This connects the choice rows to their parent property
|
|
31
|
-
if (level > 0) {
|
|
32
|
-
const parentPos = getLevelPosition(level - 1);
|
|
33
|
-
// Check if this position is not already in continuing levels
|
|
34
|
-
if (!continuingLevels.includes(level - 1)) {
|
|
35
|
-
allGradients.push(
|
|
36
|
-
'linear-gradient(var(--ifm-table-border-color), var(--ifm-table-border-color))',
|
|
37
|
-
);
|
|
38
|
-
allSizes.push('1px 100%');
|
|
39
|
-
allPositions.push(`${parentPos}rem top`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Calculate indentation based on level
|
|
44
|
-
const paddingLeft = `${level * 1.25 + 0.5}rem`;
|
|
45
|
-
|
|
46
|
-
if (allGradients.length === 0) {
|
|
47
|
-
return { paddingLeft };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
paddingLeft,
|
|
52
|
-
backgroundImage: allGradients.join(', '),
|
|
53
|
-
backgroundSize: allSizes.join(', '),
|
|
54
|
-
backgroundPosition: allPositions.join(', '),
|
|
55
|
-
backgroundRepeat: 'no-repeat',
|
|
56
|
-
};
|
|
57
|
-
};
|
|
5
|
+
import {
|
|
6
|
+
getContinuingLinesStyle,
|
|
7
|
+
getBracketLinesStyle,
|
|
8
|
+
mergeBackgroundStyles,
|
|
9
|
+
} from '../helpers/continuingLinesStyle';
|
|
58
10
|
|
|
59
11
|
// A clickable row that acts as a header/summary for a foldable choice
|
|
60
12
|
const ChoiceRow = ({
|
|
@@ -89,7 +41,7 @@ const ChoiceRow = ({
|
|
|
89
41
|
* Renders 'oneOf' and 'anyOf' choices as a set of foldable `<tr>` elements
|
|
90
42
|
* that integrate directly into the main table body.
|
|
91
43
|
*/
|
|
92
|
-
export default function FoldableRows({ row }) {
|
|
44
|
+
export default function FoldableRows({ row, bracketEnds: parentBracketEnds }) {
|
|
93
45
|
const {
|
|
94
46
|
choiceType,
|
|
95
47
|
options,
|
|
@@ -98,7 +50,9 @@ export default function FoldableRows({ row }) {
|
|
|
98
50
|
required,
|
|
99
51
|
level = 0,
|
|
100
52
|
continuingLevels = [],
|
|
53
|
+
groupBrackets = [],
|
|
101
54
|
} = row;
|
|
55
|
+
const radioGroupId = useId();
|
|
102
56
|
const [openOneOf, setOpenOneOf] = useState(0); // For oneOf, track the single open index
|
|
103
57
|
const [openAnyOf, setOpenAnyOf] = useState({}); // For anyOf, track each option's state
|
|
104
58
|
|
|
@@ -120,14 +74,70 @@ export default function FoldableRows({ row }) {
|
|
|
120
74
|
selectTitle
|
|
121
75
|
);
|
|
122
76
|
|
|
123
|
-
//
|
|
124
|
-
|
|
77
|
+
// Compute this group's own bracket and combine with any parent brackets.
|
|
78
|
+
// bracketIndex = total number of existing brackets, so each group gets a unique position.
|
|
79
|
+
const ownBracket = {
|
|
80
|
+
level,
|
|
81
|
+
bracketIndex: groupBrackets.length,
|
|
82
|
+
};
|
|
83
|
+
const allBrackets = [...groupBrackets, ownBracket];
|
|
84
|
+
|
|
85
|
+
// colSpan=5 rows need continuing ancestor lines to pass through (for visual
|
|
86
|
+
// continuity). They don't have ::before/::after connectors, so we draw all
|
|
87
|
+
// ancestor lines via background gradients. We always include the immediate
|
|
88
|
+
// parent level (level-1) so the parent-to-child connector passes through.
|
|
89
|
+
// Only draw ancestor lines where the next level up is also continuing;
|
|
90
|
+
// this matches PropertyRow's filter to avoid stray lines at a parent's
|
|
91
|
+
// corner connector when that parent is the last in its group.
|
|
92
|
+
const ancestorLevels = continuingLevels.filter(
|
|
93
|
+
(lvl) => lvl < level && continuingLevels.includes(lvl + 1),
|
|
94
|
+
);
|
|
95
|
+
// Always include the immediate parent level so the parent-to-child
|
|
96
|
+
// connector passes through these full-width rows.
|
|
97
|
+
if (level > 0 && !ancestorLevels.includes(level - 1)) {
|
|
98
|
+
ancestorLevels.push(level - 1);
|
|
99
|
+
}
|
|
100
|
+
const treePassthrough = getContinuingLinesStyle(ancestorLevels, 0);
|
|
101
|
+
const indent = { paddingLeft: `${level * 1.25 + 0.5}rem` };
|
|
102
|
+
|
|
103
|
+
// Header row: top cap on the ownBracket
|
|
104
|
+
const headerBracketStyle = getBracketLinesStyle(allBrackets, {
|
|
105
|
+
starting: [ownBracket],
|
|
106
|
+
});
|
|
107
|
+
const headerMerged = mergeBackgroundStyles(
|
|
108
|
+
treePassthrough,
|
|
109
|
+
headerBracketStyle,
|
|
110
|
+
);
|
|
111
|
+
const headerStyle = { ...headerMerged, ...indent, paddingTop: '0.5rem' };
|
|
112
|
+
|
|
113
|
+
// Middle rows (option toggles): no caps
|
|
114
|
+
const middleBracketStyle = getBracketLinesStyle(allBrackets);
|
|
115
|
+
const middleMerged = mergeBackgroundStyles(
|
|
116
|
+
treePassthrough,
|
|
117
|
+
middleBracketStyle,
|
|
118
|
+
);
|
|
119
|
+
const middleStyle = { ...middleMerged, ...indent };
|
|
120
|
+
|
|
121
|
+
// Last toggle when its content is NOT shown: bottom cap on ownBracket (+ parent ends)
|
|
122
|
+
const allEndings = [ownBracket, ...(parentBracketEnds || [])];
|
|
123
|
+
const lastToggleBracketStyle = getBracketLinesStyle(allBrackets, {
|
|
124
|
+
ending: allEndings,
|
|
125
|
+
});
|
|
126
|
+
const lastToggleMerged = mergeBackgroundStyles(
|
|
127
|
+
treePassthrough,
|
|
128
|
+
lastToggleBracketStyle,
|
|
129
|
+
);
|
|
130
|
+
const lastToggleStyle = {
|
|
131
|
+
...lastToggleMerged,
|
|
132
|
+
...indent,
|
|
133
|
+
paddingBottom: '0.5rem',
|
|
134
|
+
};
|
|
125
135
|
|
|
126
136
|
return (
|
|
127
137
|
<>
|
|
128
138
|
{/* A header row for the entire choice block */}
|
|
129
139
|
<tr>
|
|
130
|
-
<td colSpan={5} style={
|
|
140
|
+
<td colSpan={5} style={headerStyle}>
|
|
131
141
|
<Heading as="h4" className="choice-row-header-headline">
|
|
132
142
|
{header}
|
|
133
143
|
</Heading>
|
|
@@ -139,6 +149,10 @@ export default function FoldableRows({ row }) {
|
|
|
139
149
|
{options.map((option, index) => {
|
|
140
150
|
const isActive =
|
|
141
151
|
choiceType === 'oneOf' ? openOneOf === index : !!openAnyOf[index];
|
|
152
|
+
const isLastOption = index === options.length - 1;
|
|
153
|
+
// Last toggle needs bottom cap when its content is hidden
|
|
154
|
+
const toggleStyle =
|
|
155
|
+
isLastOption && !isActive ? lastToggleStyle : middleStyle;
|
|
142
156
|
return (
|
|
143
157
|
<React.Fragment key={option.title}>
|
|
144
158
|
<ChoiceRow
|
|
@@ -151,11 +165,24 @@ export default function FoldableRows({ row }) {
|
|
|
151
165
|
}
|
|
152
166
|
isActive={isActive}
|
|
153
167
|
isRadio={choiceType === 'oneOf'}
|
|
154
|
-
name={
|
|
155
|
-
|
|
168
|
+
name={
|
|
169
|
+
choiceType === 'oneOf'
|
|
170
|
+
? `oneOf-${radioGroupId}`
|
|
171
|
+
: `anyOf-${option.title}-${radioGroupId}`
|
|
172
|
+
}
|
|
173
|
+
continuingLinesStyle={toggleStyle}
|
|
156
174
|
/>
|
|
157
175
|
{/* If the option is active, render its rows directly into the table body */}
|
|
158
|
-
{isActive &&
|
|
176
|
+
{isActive && (
|
|
177
|
+
<SchemaRows
|
|
178
|
+
tableData={option.rows}
|
|
179
|
+
bracketEnds={
|
|
180
|
+
isLastOption
|
|
181
|
+
? [ownBracket, ...(parentBracketEnds || [])]
|
|
182
|
+
: undefined
|
|
183
|
+
}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
159
186
|
</React.Fragment>
|
|
160
187
|
);
|
|
161
188
|
})}
|