@table-bro/mcp 0.0.0-stage → 1.2.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/LICENSE +21 -0
- package/README.md +97 -2
- package/dist/docs/docs.d.ts +21 -0
- package/dist/docs/docs.js +103 -0
- package/dist/docs/pages.d.ts +28 -0
- package/dist/docs/pages.js +2734 -0
- package/dist/docs/topics.d.ts +11 -0
- package/dist/docs/topics.js +410 -0
- package/dist/extension.d.ts +23 -0
- package/dist/extension.js +1 -0
- package/dist/generate/angular.d.ts +26 -0
- package/dist/generate/angular.js +241 -0
- package/dist/generate/cells.d.ts +22 -0
- package/dist/generate/cells.js +157 -0
- package/dist/generate/code.d.ts +48 -0
- package/dist/generate/code.js +153 -0
- package/dist/generate/config.d.ts +16 -0
- package/dist/generate/config.js +95 -0
- package/dist/generate/fillings.d.ts +49 -0
- package/dist/generate/fillings.js +130 -0
- package/dist/generate/filters.d.ts +36 -0
- package/dist/generate/filters.js +254 -0
- package/dist/generate/generate-code.d.ts +34 -0
- package/dist/generate/generate-code.js +59 -0
- package/dist/generate/names.d.ts +52 -0
- package/dist/generate/names.js +93 -0
- package/dist/generate/rows.d.ts +5 -0
- package/dist/generate/rows.js +22 -0
- package/dist/generate/scaffold.d.ts +35 -0
- package/dist/generate/scaffold.js +62 -0
- package/dist/generate/service.d.ts +16 -0
- package/dist/generate/service.js +320 -0
- package/dist/generate/setup.d.ts +34 -0
- package/dist/generate/setup.js +158 -0
- package/dist/generate/vanilla.d.ts +10 -0
- package/dist/generate/vanilla.js +160 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +10 -0
- package/dist/project/inspect-project.d.ts +53 -0
- package/dist/project/inspect-project.js +223 -0
- package/dist/server/main.d.ts +2 -0
- package/dist/server/main.js +8 -0
- package/dist/server/server.d.ts +17 -0
- package/dist/server/server.js +212 -0
- package/dist/spec/builtin-specs.d.ts +13 -0
- package/dist/spec/builtin-specs.js +167 -0
- package/dist/spec/css-variables.d.ts +22 -0
- package/dist/spec/css-variables.js +54 -0
- package/dist/spec/schema.d.ts +19 -0
- package/dist/spec/schema.js +278 -0
- package/dist/spec/skin-config.d.ts +30 -0
- package/dist/spec/skin-config.js +67 -0
- package/dist/spec/spec.d.ts +157 -0
- package/dist/spec/spec.js +2 -0
- package/dist/spec/validate-spec.d.ts +54 -0
- package/dist/spec/validate-spec.js +979 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +71 -4
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { imports, literal, objectKey, quote } from './code.js';
|
|
2
|
+
import { BroNames } from './names.js';
|
|
3
|
+
/**
|
|
4
|
+
* The service of a server table, as BroService of @table-bro/angular: loadPage sends the request of a query of the table
|
|
5
|
+
* and gives the rows of its page with the number of all rows; mapServerQuery turns the query into the request of the API,
|
|
6
|
+
* and mapServerResponse the answer of the API into the rows. The page only delivers the rows to the table. The simulated
|
|
7
|
+
* server of the site is a REST API with names of its own, so its service maps as the service of a real API does.
|
|
8
|
+
*/
|
|
9
|
+
export function serviceCode(spec, source, filtersCode) {
|
|
10
|
+
// The spec as the service sees its API: the simulated server has the names and the parameters of a REST API
|
|
11
|
+
const view = (source === 'simulated') ? simulatedSpec(spec, filtersCode) : spec;
|
|
12
|
+
const names = BroNames.of(spec);
|
|
13
|
+
const row = spec.rowType;
|
|
14
|
+
const api = view.api;
|
|
15
|
+
const pages = (source === 'api-pages' || source === 'simulated');
|
|
16
|
+
const filters = Boolean(filtersCode) && pages && Boolean(api?.filters);
|
|
17
|
+
// The simulated server and an API without pages filter the rows as filtersTransform of the local mode does
|
|
18
|
+
const localFilters = Boolean(filtersCode) && (source === 'simulated' || source === 'api');
|
|
19
|
+
const renamed = renamedSources(view);
|
|
20
|
+
const http = (source !== 'simulated');
|
|
21
|
+
// The parts of each source: only the one of the source runs, as only an API has the paths of its answer
|
|
22
|
+
const types = {
|
|
23
|
+
project: () => [
|
|
24
|
+
'/** A request of your API: its query parameters. */',
|
|
25
|
+
`export type ${names.apiQueryType} = Record<string, string | number | boolean>;`,
|
|
26
|
+
'',
|
|
27
|
+
'/** An answer of your API: the rows of a page and the number of all rows. Replace it with the answer of your API. */',
|
|
28
|
+
`export interface ${names.apiPageType} {`,
|
|
29
|
+
` rows: ${row}[];`,
|
|
30
|
+
' total: number;',
|
|
31
|
+
'}'
|
|
32
|
+
],
|
|
33
|
+
simulated: () => apiTypes(view, names, 'A request of the simulated server: the query parameters of a page, its search, its sort and its filters. Your API has its own.'),
|
|
34
|
+
'api-pages': () => apiTypes(view, names, 'A request of the API: the query parameters of a page, its search, its sort and its filters.'),
|
|
35
|
+
api: () => apiTypes(view, names, 'A request of the API: it gives all its rows at once and takes no parameters.')
|
|
36
|
+
}[source]();
|
|
37
|
+
const loadPage = (source === 'simulated')
|
|
38
|
+
? [
|
|
39
|
+
' /** A page of the query: the request of the simulated server, and its answer as the rows of the table. */',
|
|
40
|
+
` loadPage(query: BroQuery<${row}>): Promise<BroServerPage<${row}>> {`,
|
|
41
|
+
` return simulatedServer(this.mapServerQuery(query)).then((response: ${names.apiPageType}) => this.mapServerResponse(response));`,
|
|
42
|
+
' }'
|
|
43
|
+
]
|
|
44
|
+
: (source === 'api')
|
|
45
|
+
? [
|
|
46
|
+
' /** A page of the query from all the rows of the API, filtered by the search and the filters and sorted. */',
|
|
47
|
+
` loadPage(query: BroQuery<${row}>): Promise<BroServerPage<${row}>> {`,
|
|
48
|
+
` return this.loadRows().then((rows: ${row}[]) => queryPage(rows, query));`,
|
|
49
|
+
' }',
|
|
50
|
+
'',
|
|
51
|
+
' /** All the rows of the API: its answer comes once, and the service turns it into rows. */',
|
|
52
|
+
` private loadRows(): Promise<${row}[]> {`,
|
|
53
|
+
` return this.apiRows ??= ${request(names, quote(spec.api.url), '')}`,
|
|
54
|
+
` .then((response: ${names.apiPageType}) => this.mapServerResponse(response).rows);`,
|
|
55
|
+
' }'
|
|
56
|
+
]
|
|
57
|
+
: [
|
|
58
|
+
` /** A page of the query: the request of ${(source === 'project') ? 'your API' : 'the API'}, and its answer as the rows of the table. */`,
|
|
59
|
+
` loadPage(query: BroQuery<${row}>): Promise<BroServerPage<${row}>> {`,
|
|
60
|
+
...((source === 'project') ? [' // TODO: the address of your API'] : []),
|
|
61
|
+
` return ${request(names, quote((source === 'project') ? `/api/${names.kebab}s` : spec.api.url))}`,
|
|
62
|
+
` .then((response: ${names.apiPageType}) => this.mapServerResponse(response));`,
|
|
63
|
+
' }'
|
|
64
|
+
];
|
|
65
|
+
const mapQuery = {
|
|
66
|
+
project: () => [
|
|
67
|
+
` mapServerQuery(query: BroQuery<${row}>): ${names.apiQueryType} {`,
|
|
68
|
+
' // TODO: the parameters of your API for the sort (query.sort), the search and the filters (query.filter)',
|
|
69
|
+
' return {page: query.page, pageSize: query.pageSize};',
|
|
70
|
+
' }'
|
|
71
|
+
],
|
|
72
|
+
simulated: () => pagesQuery(view, names, filters, renamed.length > 0),
|
|
73
|
+
'api-pages': () => pagesQuery(view, names, filters, renamed.length > 0),
|
|
74
|
+
api: () => [
|
|
75
|
+
' /** The API takes no parameters: the service answers the queries of the table from all its rows. */',
|
|
76
|
+
` mapServerQuery(): ${names.apiQueryType} {`,
|
|
77
|
+
' return {};',
|
|
78
|
+
' }'
|
|
79
|
+
]
|
|
80
|
+
}[source]();
|
|
81
|
+
const mapResponse = (source === 'project')
|
|
82
|
+
? [
|
|
83
|
+
` mapServerResponse(response: ${names.apiPageType}): BroServerPage<${row}> {`,
|
|
84
|
+
' return {rows: response.rows, total: response.total};',
|
|
85
|
+
' }'
|
|
86
|
+
]
|
|
87
|
+
: [
|
|
88
|
+
` mapServerResponse(response: ${names.apiPageType}): BroServerPage<${row}> {`,
|
|
89
|
+
` const rows: ${row}[] = ${access('response', api.rows)}.map((item: ${names.apiItemType}) => ({`,
|
|
90
|
+
view.fields.map(field => ` ${objectKey(field.name)}: ${access('item', field.source ?? field.name)}`).join(',\n'),
|
|
91
|
+
' }));',
|
|
92
|
+
'',
|
|
93
|
+
` return {rows, total: ${pages ? access('response', api.total) : 'rows.length'}};`,
|
|
94
|
+
' }'
|
|
95
|
+
];
|
|
96
|
+
const members = [
|
|
97
|
+
...(http ? [' private http = inject(HttpClient);'] : []),
|
|
98
|
+
...((source === 'api') ? [' /** All the rows of the API, loaded once: the service answers the queries of the table from them. */', ` private apiRows?: Promise<${row}[]>;`] : []),
|
|
99
|
+
...(http ? [''] : []),
|
|
100
|
+
...loadPage,
|
|
101
|
+
'',
|
|
102
|
+
...mapQuery,
|
|
103
|
+
'',
|
|
104
|
+
...mapResponse
|
|
105
|
+
];
|
|
106
|
+
// The simulated server and an API without pages answer the queries from all the rows, after the service
|
|
107
|
+
const local = [
|
|
108
|
+
...(localFilters ? ['', `/** The filters of the drawer on ${(source === 'api') ? 'the rows of the API' : 'the records of the simulated server'}. */`, `const filterRows = ${filtersCode.transformCode()};`] : []),
|
|
109
|
+
...((source === 'api') ? ['', queryPageCode(spec, localFilters ? `filterRows(rows, (query.filter ?? {}) as ${names.filtersType})` : 'rows')] : []),
|
|
110
|
+
...((source === 'simulated') ? ['', ...simulatedServerCode(spec, view, names, localFilters)] : [])
|
|
111
|
+
];
|
|
112
|
+
return [
|
|
113
|
+
imports({
|
|
114
|
+
'@angular/common/http': http ? ['HttpClient'] : [],
|
|
115
|
+
'@angular/core': ['Injectable', ...(http ? ['inject'] : [])],
|
|
116
|
+
rxjs: http ? ['firstValueFrom'] : [],
|
|
117
|
+
'@table-bro/core': ['type BroQuery', ...(localFilters ? ['matchesFilterCondition'] : [])],
|
|
118
|
+
'@table-bro/angular': ['type BroServerPage', 'type BroService'],
|
|
119
|
+
[`./${names.kebab}`]: [`type ${row}`, ...((filters || localFilters) ? [`type ${names.filtersType}`] : [])]
|
|
120
|
+
}),
|
|
121
|
+
'',
|
|
122
|
+
...types,
|
|
123
|
+
'',
|
|
124
|
+
...((pages && renamed.length)
|
|
125
|
+
? [
|
|
126
|
+
'/** The names of the API for the fields of a row: the API sorts by them. */',
|
|
127
|
+
`const apiFields: Partial<Record<keyof ${row}, string>> = ${literal(Object.fromEntries(renamed), '')};`,
|
|
128
|
+
''
|
|
129
|
+
]
|
|
130
|
+
: []),
|
|
131
|
+
'/**',
|
|
132
|
+
` * The service of the table: it sends the request of a query of the table to ${(source === 'simulated') ? 'the simulated server' : (source === 'project') ? 'your API' : 'the API'}, and turns its answer`,
|
|
133
|
+
' * into the rows of a page with the number of all rows. The page delivers them to the table.',
|
|
134
|
+
' */',
|
|
135
|
+
'@Injectable({providedIn: \'root\'})',
|
|
136
|
+
`export class ${names.serviceClass} implements BroService<${row}, ${names.apiQueryType}, ${names.apiPageType}> {`,
|
|
137
|
+
...members,
|
|
138
|
+
'}',
|
|
139
|
+
...local,
|
|
140
|
+
''
|
|
141
|
+
].join('\n');
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The request of a query to an API by HttpClient: its parameters come from mapServerQuery.
|
|
145
|
+
* @param names - the names of the table
|
|
146
|
+
* @param url - the address of the API, as a string literal
|
|
147
|
+
* @param query - the argument of mapServerQuery: none for an API without parameters
|
|
148
|
+
*/
|
|
149
|
+
function request(names, url, query = 'query') {
|
|
150
|
+
return `firstValueFrom(this.http.get<${names.apiPageType}>(${url}, {params: this.mapServerQuery(${query})}))`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The spec as the simulated server of the site gives it: a REST API with the parameters page, per_page, q and sort,
|
|
154
|
+
* the filters in snake case, and the answer {data, meta: {total_count}} with the fields in its own names.
|
|
155
|
+
*/
|
|
156
|
+
function simulatedSpec(spec, filters) {
|
|
157
|
+
return {
|
|
158
|
+
...spec,
|
|
159
|
+
fields: spec.fields.map(field => ({ ...field, source: simulatedName(field.name, field.type === 'boolean') })),
|
|
160
|
+
api: {
|
|
161
|
+
url: '',
|
|
162
|
+
rows: 'data',
|
|
163
|
+
total: 'meta.total_count',
|
|
164
|
+
paging: { size: 'per_page', page: 'page' },
|
|
165
|
+
search: 'q',
|
|
166
|
+
sort: { field: 'sort' },
|
|
167
|
+
filters: Object.fromEntries((filters?.keys() ?? []).map(({ key, type }) => [key, simulatedName(key, type === 'boolean')]))
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/** A name of the simulated API: snake case, and a yes or no field starts with is_ when it has no such start. */
|
|
172
|
+
function simulatedName(name, flag) {
|
|
173
|
+
const snake = name.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
|
174
|
+
return (flag && !/^(is|has|can)_/.test(snake)) ? `is_${snake}` : snake;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The simulated server of the site: its records are the rows of the spec. It reads its query parameters as a server
|
|
178
|
+
* does, and answers in half a second with a page of its records in the names of its API.
|
|
179
|
+
*/
|
|
180
|
+
function simulatedServerCode(spec, view, names, filters) {
|
|
181
|
+
const row = spec.rowType;
|
|
182
|
+
const api = view.api;
|
|
183
|
+
const filterValues = Object.entries(api.filters ?? {}).map(([key, parameter]) => ` ${objectKey(key)}: request[${quote(parameter)}]`);
|
|
184
|
+
return [
|
|
185
|
+
'/** The records of the simulated server: the rows of the spec. */',
|
|
186
|
+
`const serverRows: ${row}[] = ${literal(spec.rows ?? [], '')};`,
|
|
187
|
+
'',
|
|
188
|
+
'/**',
|
|
189
|
+
' * The simulated server: it reads its query parameters as a server does, and answers in half a second with a page of',
|
|
190
|
+
' * its records in the names of its API. Your API takes its place.',
|
|
191
|
+
' */',
|
|
192
|
+
`function simulatedServer(request: ${names.apiQueryType}): Promise<${names.apiPageType}> {`,
|
|
193
|
+
` const page: number = Number(request[${quote(api.paging.page)}]);`,
|
|
194
|
+
` const perPage: number = Number(request[${quote(api.paging.size)}]);`,
|
|
195
|
+
` const search: string = String(request[${quote(api.search)}] ?? '').toLowerCase();`,
|
|
196
|
+
` const sort: string = String(request[${quote(api.sort.field)}] ?? '');`,
|
|
197
|
+
` const key = sort.replace(/^-/, '') as keyof ${names.apiItemType};`,
|
|
198
|
+
' const direction: number = sort.startsWith(\'-\') ? -1 : 1;',
|
|
199
|
+
...(filters ? [' const filters = {', filterValues.join(',\n'), ` } as ${names.filtersType};`] : []),
|
|
200
|
+
` const items: ${names.apiItemType}[] = ${filters ? 'filterRows(serverRows, filters)' : 'serverRows'}`,
|
|
201
|
+
` .map((row: ${row}) => ({`,
|
|
202
|
+
view.fields.map(field => ` ${objectKey(field.source ?? field.name)}: row.${field.name}`).join(',\n'),
|
|
203
|
+
' }))',
|
|
204
|
+
' .filter(item => !search || Object.values(item).some(value => String(value).toLowerCase().includes(search)))',
|
|
205
|
+
' .sort((a, b) => sort ? String(a[key]).localeCompare(String(b[key]), undefined, {numeric: true}) * direction : 0);',
|
|
206
|
+
' const start: number = (page - 1) * perPage;',
|
|
207
|
+
'',
|
|
208
|
+
` return new Promise(resolve => setTimeout(() => resolve({${api.rows}: items.slice(start, start + perPage), meta: {'total_count': items.length}}), 500));`,
|
|
209
|
+
'}'
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
/** The page of a query from all the rows of a server: filtered by the search and the filters, sorted, and cut to the page. */
|
|
213
|
+
function queryPageCode(spec, filtered) {
|
|
214
|
+
const row = spec.rowType;
|
|
215
|
+
return [
|
|
216
|
+
'/** The page of a query from all the rows: filtered by the search and the filters, sorted, and cut to the page. */',
|
|
217
|
+
`function queryPage(rows: ${row}[], query: BroQuery<${row}>): BroServerPage<${row}> {`,
|
|
218
|
+
' const search: string = (query.filter?.search ?? \'\').toLowerCase();',
|
|
219
|
+
` const [key] = (query.sort?.order ?? []) as (keyof ${row})[];`,
|
|
220
|
+
' const direction: number = (key && query.sort?.keys[key]?.direction === \'desc\') ? -1 : 1;',
|
|
221
|
+
` const found: ${row}[] = ${filtered}`,
|
|
222
|
+
' .filter(row => !search || Object.values(row).some(value => String(value).toLowerCase().includes(search)))',
|
|
223
|
+
' .sort((a, b) => key ? String(a[key]).localeCompare(String(b[key]), undefined, {numeric: true}) * direction : 0);',
|
|
224
|
+
' const start: number = (query.page - 1) * query.pageSize;',
|
|
225
|
+
'',
|
|
226
|
+
' return {rows: found.slice(start, start + query.pageSize), total: found.length};',
|
|
227
|
+
'}'
|
|
228
|
+
].join('\n');
|
|
229
|
+
}
|
|
230
|
+
/** The types of the request, of an item and of the answer of the API of the spec, as far as the table reads them. */
|
|
231
|
+
function apiTypes(spec, names, request) {
|
|
232
|
+
const api = spec.api;
|
|
233
|
+
const item = new Map();
|
|
234
|
+
const page = new Map();
|
|
235
|
+
spec.fields.forEach(field => addPath(item, field.source ?? field.name, field.type));
|
|
236
|
+
if (api.rows) {
|
|
237
|
+
addPath(page, api.rows, `${names.apiItemType}[]`);
|
|
238
|
+
}
|
|
239
|
+
if (api.total && api.paging) {
|
|
240
|
+
addPath(page, api.total, 'number');
|
|
241
|
+
}
|
|
242
|
+
return [
|
|
243
|
+
`/** ${request} */`,
|
|
244
|
+
`export type ${names.apiQueryType} = Record<string, string | number | boolean>;`,
|
|
245
|
+
'',
|
|
246
|
+
'/** An item of the answer of the API: the values that the table shows. */',
|
|
247
|
+
`export interface ${names.apiItemType} ${typeText(item, '')}`,
|
|
248
|
+
'',
|
|
249
|
+
...(api.rows
|
|
250
|
+
? [
|
|
251
|
+
`/** An answer of the API: ${api.paging ? 'a page of its items with the number of all items' : 'all its items'}. */`,
|
|
252
|
+
`export interface ${names.apiPageType} ${typeText(page, '')}`
|
|
253
|
+
]
|
|
254
|
+
: [
|
|
255
|
+
'/** An answer of the API: all its items. */',
|
|
256
|
+
`export type ${names.apiPageType} = ${names.apiItemType}[];`
|
|
257
|
+
])
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
/** The query parameters of a page of the API: its size and place, its search, its sort and its filters. */
|
|
261
|
+
function pagesQuery(spec, names, filters, renamed) {
|
|
262
|
+
const api = spec.api;
|
|
263
|
+
const paging = api.paging;
|
|
264
|
+
const field = renamed ? 'apiFields[key] ?? key' : 'key';
|
|
265
|
+
const params = [
|
|
266
|
+
` ${objectKey(paging.size)}: query.pageSize`,
|
|
267
|
+
paging.offset ? ` ${objectKey(paging.offset)}: (query.page - 1) * query.pageSize` : ` ${objectKey(paging.page)}: query.page`,
|
|
268
|
+
...(api.search ? [` ${objectKey(api.search)}: query.filter?.search`] : []),
|
|
269
|
+
...(!api.sort
|
|
270
|
+
? []
|
|
271
|
+
: api.sort.order
|
|
272
|
+
? [
|
|
273
|
+
` ${objectKey(api.sort.field)}: key && (${field})`,
|
|
274
|
+
` ${objectKey(api.sort.order)}: key && (query.sort?.keys[key]?.direction ?? 'asc')`
|
|
275
|
+
]
|
|
276
|
+
: [
|
|
277
|
+
` ${objectKey(api.sort.field)}: key && \`\${(query.sort?.keys[key]?.direction === 'desc') ? '-' : ''}\${${field}}\``
|
|
278
|
+
]),
|
|
279
|
+
...Object.entries(api.filters ?? {}).map(([key, parameter]) => ` ${objectKey(parameter)}: filters.${key}`)
|
|
280
|
+
];
|
|
281
|
+
return [
|
|
282
|
+
' /** The page, the search, the sort and the filters as the query parameters of the API; an empty value filters nothing. */',
|
|
283
|
+
` mapServerQuery(query: BroQuery<${spec.rowType}>): ${names.apiQueryType} {`,
|
|
284
|
+
...(api.sort ? [` const [key] = (query.sort?.order ?? []) as (keyof ${spec.rowType})[];`] : []),
|
|
285
|
+
...(filters ? [` const filters = (query.filter ?? {}) as ${names.filtersType};`] : []),
|
|
286
|
+
' const params: Record<string, string | number | boolean | null | undefined> = {',
|
|
287
|
+
params.join(',\n'),
|
|
288
|
+
' };',
|
|
289
|
+
'',
|
|
290
|
+
` return Object.fromEntries(Object.entries(params).filter(([, value]) => (value ?? '') !== '' && value !== false)) as ${names.apiQueryType};`,
|
|
291
|
+
' }'
|
|
292
|
+
];
|
|
293
|
+
}
|
|
294
|
+
/** The fields of a row whose values come from other names of the API: the API sorts by its own names. */
|
|
295
|
+
export function renamedSources(spec) {
|
|
296
|
+
return spec.api?.sort ? spec.fields.filter(field => field.source && field.source !== field.name).map(field => [field.name, field.source]) : [];
|
|
297
|
+
}
|
|
298
|
+
function addPath(tree, path, type) {
|
|
299
|
+
const [key, ...rest] = path.split('.');
|
|
300
|
+
if (!rest.length) {
|
|
301
|
+
tree.set(key, type);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const child = tree.get(key);
|
|
305
|
+
const node = (child instanceof Map) ? child : new Map();
|
|
306
|
+
tree.set(key, node);
|
|
307
|
+
addPath(node, rest.join('.'), type);
|
|
308
|
+
}
|
|
309
|
+
function typeText(tree, indent) {
|
|
310
|
+
const inner = indent + ' ';
|
|
311
|
+
const fields = [...tree].map(([key, value]) => `${inner}${property(key)}: ${(value instanceof Map) ? typeText(value, inner) : value};`);
|
|
312
|
+
return `{\n${fields.join('\n')}\n${indent}}`;
|
|
313
|
+
}
|
|
314
|
+
/** A value at a path, as item.user.name or item['news-site']. */
|
|
315
|
+
function access(object, path) {
|
|
316
|
+
return path ? `${object}${path.split('.').map(key => /^[A-Za-z_$][\w$]*$/.test(key) ? `.${key}` : `[${quote(key)}]`).join('')}` : object;
|
|
317
|
+
}
|
|
318
|
+
function property(key) {
|
|
319
|
+
return /^[A-Za-z_$][\w$]*$/.test(key) ? key : quote(key);
|
|
320
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { BroSpecSkin, BroTableSpec } from '../spec/spec.js';
|
|
2
|
+
export type BroTarget = 'angular' | 'vanilla';
|
|
3
|
+
/** The functions of the optional elements of a skin: provideTableBroMatSkin() takes them, a vanilla skin calls them after its import. */
|
|
4
|
+
export type BroSkinFeature = 'withTags' | 'withSwitch' | 'withToolbarMenu';
|
|
5
|
+
/**
|
|
6
|
+
* The optional elements of the skin that a table of a spec uses: its ready elements tags and switch, and the menu of
|
|
7
|
+
* the toolbar, an element of its own only in the skin mat.
|
|
8
|
+
* @param spec - the spec of the table
|
|
9
|
+
* @param skin - the skin of the project
|
|
10
|
+
*/
|
|
11
|
+
export declare function skinFeatures(spec: BroTableSpec, skin: BroSpecSkin): BroSkinFeature[];
|
|
12
|
+
/** A step of the setup of a project, as the docs of Bootstrap show such steps: a title, a line about it, its code. */
|
|
13
|
+
export interface BroSetupStep {
|
|
14
|
+
title: string;
|
|
15
|
+
/** The line under the title: a name in backticks is code. */
|
|
16
|
+
text?: string;
|
|
17
|
+
/** The points of the step, a list before its code: a name in backticks is code. */
|
|
18
|
+
items?: string[];
|
|
19
|
+
code?: string;
|
|
20
|
+
language?: 'bash' | 'html' | 'json' | 'scss' | 'typescript';
|
|
21
|
+
/** The npm package of the paid module that the step sets up: the site shows such steps apart, after the code. */
|
|
22
|
+
module?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The setup of a project, once for all its tables: the packages, the CSS and the registration of the skin with its
|
|
26
|
+
* optional elements. The generator gives it apart from the files of a table.
|
|
27
|
+
* @param target - an Angular project or Vanilla JS
|
|
28
|
+
* @param skin - the skin of the project
|
|
29
|
+
* @param extraPackages - the packages that the tables need besides the ones of the skin
|
|
30
|
+
* @param features - the optional elements of the skin that the tables use
|
|
31
|
+
*/
|
|
32
|
+
export declare function setupSteps(target: BroTarget, skin: BroSpecSkin, extraPackages?: string[], features?: BroSkinFeature[]): BroSetupStep[];
|
|
33
|
+
/** The steps of a setup as Markdown, for an agent. */
|
|
34
|
+
export declare function setupText(steps: BroSetupStep[]): string;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { imports } from './code.js';
|
|
2
|
+
/** The CSS of the core and of each skin, each in its package. */
|
|
3
|
+
const css = {
|
|
4
|
+
core: 'node_modules/@table-bro/core/dist/table-bro.min.css',
|
|
5
|
+
mat: 'node_modules/@table-bro/angular-material/dist/table-bro.material.min.css',
|
|
6
|
+
bs: 'node_modules/@table-bro/bootstrap/dist/table-bro.bootstrap.min.css',
|
|
7
|
+
nv: 'node_modules/@table-bro/native/dist/table-bro.native.min.css'
|
|
8
|
+
};
|
|
9
|
+
/** What each package gives the project: the step that installs the packages names them before the command. */
|
|
10
|
+
const packageRoles = {
|
|
11
|
+
'@table-bro/core': '`@table-bro/core`: the table engine itself, the Custom Element <table-bro> with its rows, pages, sort, search and row details, and the CSS of the core',
|
|
12
|
+
'@table-bro/angular': '`@table-bro/angular`: `<table-bro-ng>` for the templates of Angular, with signal inputs and outputs, and the contracts of a page and of its service',
|
|
13
|
+
'@table-bro/angular-material': '`@table-bro/angular-material`: the skin mat, pixel-perfect Material data tables on the components of Angular Material, in the theme of the application, with its Filter Drawer',
|
|
14
|
+
'@angular/elements': '`@angular/elements`: turns the Angular components of the cells into Custom Elements, which the table creates by their tags',
|
|
15
|
+
'@table-bro/bootstrap': '`@table-bro/bootstrap`: the skin bs, the table on the components and the classes of Bootstrap 5: its buttons, dropdowns and tooltips',
|
|
16
|
+
bootstrap: '`bootstrap`: Bootstrap itself, its CSS and the Dropdown and Tooltip that the skin uses',
|
|
17
|
+
'@types/bootstrap': '`@types/bootstrap`: the types of Bootstrap, which the import of Dropdown and Tooltip needs in a strict TypeScript project',
|
|
18
|
+
'@table-bro/native': '`@table-bro/native`: the skin nv, plain HTML elements, no UI library'
|
|
19
|
+
};
|
|
20
|
+
/** What each optional element is: the text of a setup names the functions with them. */
|
|
21
|
+
const featureRoles = {
|
|
22
|
+
withTags: '`withTags()` for tags',
|
|
23
|
+
withSwitch: '`withSwitch()` for switches',
|
|
24
|
+
withToolbarMenu: '`withToolbarMenu()` for the menu of the toolbar'
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The optional elements of the skin that a table of a spec uses: its ready elements tags and switch, and the menu of
|
|
28
|
+
* the toolbar, an element of its own only in the skin mat.
|
|
29
|
+
* @param spec - the spec of the table
|
|
30
|
+
* @param skin - the skin of the project
|
|
31
|
+
*/
|
|
32
|
+
export function skinFeatures(spec, skin) {
|
|
33
|
+
const elements = (spec.cols ?? []).map(col => (col.component && 'element' in col.component) ? col.component.element : undefined);
|
|
34
|
+
return [
|
|
35
|
+
['withTags', elements.includes('tags')],
|
|
36
|
+
['withSwitch', elements.includes('switch')],
|
|
37
|
+
['withToolbarMenu', skin === 'mat' && Boolean(spec.toolbar?.menu)]
|
|
38
|
+
].filter(([, used]) => used).map(([feature]) => feature);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The setup of a project, once for all its tables: the packages, the CSS and the registration of the skin with its
|
|
42
|
+
* optional elements. The generator gives it apart from the files of a table.
|
|
43
|
+
* @param target - an Angular project or Vanilla JS
|
|
44
|
+
* @param skin - the skin of the project
|
|
45
|
+
* @param extraPackages - the packages that the tables need besides the ones of the skin
|
|
46
|
+
* @param features - the optional elements of the skin that the tables use
|
|
47
|
+
*/
|
|
48
|
+
export function setupSteps(target, skin, extraPackages = [], features = []) {
|
|
49
|
+
const angular = (target === 'angular');
|
|
50
|
+
const calls = features.map(feature => `${feature}()`).join(', ');
|
|
51
|
+
// The elements that only some tables use: a project that registers the skin already adds the ones of a new table
|
|
52
|
+
const optional = `The elements that only some tables use come with their functions: ${['withTags', 'withSwitch', ...((skin === 'mat') ? ['withToolbarMenu'] : [])].map(feature => featureRoles[feature]).join(', ')}; a project that registers the skin already adds the ones of a new table${(skin === 'mat') ? ' to the call' : `. A page may define them itself, as \`customElements.define('bro-${skin}-tags', ${(skin === 'bs') ? 'BroBootstrapTagsComponent' : 'BroNativeTagsComponent'})\``}.`;
|
|
53
|
+
// Angular server-side rendering renders a vanilla skin only when the server bundle imports its package
|
|
54
|
+
const ssrImport = (pkg) => ` With Angular server-side rendering, \`app.config.ts\` imports the package as well, \`import '${pkg}';\`, so that the server renders the skin too: [Prerendering](https://table-bro.com/docs/ssr/prerendering).`;
|
|
55
|
+
// A vanilla skin registers itself on its import, and its optional elements come with calls after it
|
|
56
|
+
const vanillaCalls = features.map(feature => `${feature}();`);
|
|
57
|
+
const packages = ['@table-bro/core', ...(angular ? ['@table-bro/angular'] : []), ...{
|
|
58
|
+
mat: ['@table-bro/angular-material', '@angular/elements'],
|
|
59
|
+
bs: ['@table-bro/bootstrap', 'bootstrap', '@types/bootstrap'],
|
|
60
|
+
nv: ['@table-bro/native']
|
|
61
|
+
}[skin], ...extraPackages];
|
|
62
|
+
const styles = [...(skin === 'bs' ? ['node_modules/bootstrap/dist/css/bootstrap.min.css'] : []), css.core, css[skin]];
|
|
63
|
+
const steps = [
|
|
64
|
+
{
|
|
65
|
+
title: 'Install the packages',
|
|
66
|
+
text: (skin === 'mat') ? 'The table and the parts it needs; a project without Angular Material adds it first with `ng add @angular/material`.' : 'The table and the parts it needs:',
|
|
67
|
+
items: packages.map(name => packageRoles[name] ?? `\`${name}\``),
|
|
68
|
+
code: `npm install ${packages.join(' ')}`,
|
|
69
|
+
language: 'bash'
|
|
70
|
+
},
|
|
71
|
+
angular
|
|
72
|
+
? {
|
|
73
|
+
title: 'Add the CSS',
|
|
74
|
+
text: 'To `"styles"` of `angular.json`:',
|
|
75
|
+
code: `"styles": [\n${styles.map(style => ` "${style}"`).join(',\n')}\n]`,
|
|
76
|
+
language: 'json'
|
|
77
|
+
}
|
|
78
|
+
: {
|
|
79
|
+
title: 'Link the CSS',
|
|
80
|
+
text: 'On the page:',
|
|
81
|
+
code: styles.map(style => `<link rel="stylesheet" href="${style}">`).join('\n'),
|
|
82
|
+
language: 'html'
|
|
83
|
+
}
|
|
84
|
+
];
|
|
85
|
+
if (skin === 'mat') {
|
|
86
|
+
steps.push({
|
|
87
|
+
title: 'Register the skin and its elements',
|
|
88
|
+
text: `In the providers of \`app.config.ts\`. ${optional}`,
|
|
89
|
+
code: [
|
|
90
|
+
imports({
|
|
91
|
+
'@angular/core': ['type ApplicationConfig'],
|
|
92
|
+
'@table-bro/angular-material': ['provideTableBroMatSkin', ...features]
|
|
93
|
+
}),
|
|
94
|
+
'',
|
|
95
|
+
'export const appConfig: ApplicationConfig = {',
|
|
96
|
+
' providers: [',
|
|
97
|
+
` provideTableBroMatSkin(${calls})`,
|
|
98
|
+
' ]',
|
|
99
|
+
'};'
|
|
100
|
+
].join('\n'),
|
|
101
|
+
language: 'typescript'
|
|
102
|
+
}, {
|
|
103
|
+
title: 'Keep the theme of Angular Material',
|
|
104
|
+
text: 'The mat skin takes the theme of the application, as the components of Angular Material do: the table looks as a `mat-table` of the project, and a table in the dark theme takes the dark scheme of the theme. `ng add @angular/material` writes the theme into `styles.scss`; a project without it adds it:',
|
|
105
|
+
code: [
|
|
106
|
+
'@use \'@angular/material\' as mat;',
|
|
107
|
+
'',
|
|
108
|
+
'html {',
|
|
109
|
+
' @include mat.theme((',
|
|
110
|
+
' color: (',
|
|
111
|
+
' primary: mat.$azure-palette,',
|
|
112
|
+
' tertiary: mat.$blue-palette,',
|
|
113
|
+
' ),',
|
|
114
|
+
' typography: Roboto,',
|
|
115
|
+
' density: 0,',
|
|
116
|
+
' ));',
|
|
117
|
+
'}'
|
|
118
|
+
].join('\n'),
|
|
119
|
+
language: 'scss'
|
|
120
|
+
}, {
|
|
121
|
+
title: 'Load the fonts of the icons',
|
|
122
|
+
text: 'The mat skin shows the ligature icons of Material, as Angular Material asks; the cells and the dialogs of the page take the newer Material Symbols. In `index.html`:',
|
|
123
|
+
code: '<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">\n<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,300,0,0" rel="stylesheet">',
|
|
124
|
+
language: 'html'
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
else if (skin === 'bs') {
|
|
128
|
+
steps.push({
|
|
129
|
+
title: 'Register the skin',
|
|
130
|
+
text: `Once, ${angular ? 'in `main.ts`' : 'before the tables'}: the import registers the skin, which takes the classes of Bootstrap. ${optional}${angular ? ssrImport('@table-bro/bootstrap') : ''}`,
|
|
131
|
+
code: [
|
|
132
|
+
imports({ bootstrap: ['Dropdown', 'Tooltip'], '@table-bro/bootstrap': ['useBootstrap', ...features] }),
|
|
133
|
+
'',
|
|
134
|
+
'useBootstrap({Dropdown, Tooltip});',
|
|
135
|
+
...vanillaCalls
|
|
136
|
+
].join('\n'),
|
|
137
|
+
language: 'typescript'
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
steps.push({
|
|
142
|
+
title: 'Register the skin',
|
|
143
|
+
text: `Once, ${angular ? 'in `main.ts`' : 'before the tables'}: the import registers the skin. ${optional}${angular ? ssrImport('@table-bro/native') : ''}`,
|
|
144
|
+
code: features.length ? [imports({ '@table-bro/native': features }), '', ...vanillaCalls].join('\n') : 'import \'@table-bro/native\';',
|
|
145
|
+
language: 'typescript'
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return steps;
|
|
149
|
+
}
|
|
150
|
+
/** The steps of a setup as Markdown, for an agent. */
|
|
151
|
+
export function setupText(steps) {
|
|
152
|
+
return steps.map((step, index) => [
|
|
153
|
+
`**${index + 1}. ${step.title}**`,
|
|
154
|
+
...(step.text ? [step.text] : []),
|
|
155
|
+
...(step.items ? [step.items.map(item => `- ${item}`).join('\n')] : []),
|
|
156
|
+
...(step.code ? [`\`\`\`${step.language ?? ''}\n${step.code}\n\`\`\``] : [])
|
|
157
|
+
].join('\n\n')).join('\n\n');
|
|
158
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BroMcpExtension } from '../extension.js';
|
|
2
|
+
import type { BroSpecSkin, BroTableSpec } from '../spec/spec.js';
|
|
3
|
+
import { type BroTableFiles } from './angular.js';
|
|
4
|
+
import type { BroSpecFillings } from './fillings.js';
|
|
5
|
+
/**
|
|
6
|
+
* The table for a page without a framework, in the skins bs and nv: an ES module with a function that renders
|
|
7
|
+
* <table-bro> into a container, with its cells, its filter drawer and its CSS variables. The module imports the config
|
|
8
|
+
* of the skin from its package; the setup of the project registers the skin with the optional elements of the table.
|
|
9
|
+
*/
|
|
10
|
+
export declare function vanillaTable(spec: BroTableSpec, skin: BroSpecSkin, fillings: BroSpecFillings, tagSuffix?: string, extensions?: BroMcpExtension[]): BroTableFiles;
|