@metaobjectsdev/codegen-ts-tanstack 0.24.4 → 0.25.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/README.md +9 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/reference-templates.d.ts +5 -0
- package/dist/reference-templates.d.ts.map +1 -0
- package/dist/reference-templates.js +12 -0
- package/dist/reference-templates.js.map +1 -0
- package/dist/tanstack-grid-hook.d.ts +28 -2
- package/dist/tanstack-grid-hook.d.ts.map +1 -1
- package/dist/tanstack-grid-hook.js +35 -18
- package/dist/tanstack-grid-hook.js.map +1 -1
- package/dist/tanstack-grid.d.ts +27 -3
- package/dist/tanstack-grid.d.ts.map +1 -1
- package/dist/tanstack-grid.js +20 -12
- package/dist/tanstack-grid.js.map +1 -1
- package/dist/tanstack-query.d.ts +6 -2
- package/dist/tanstack-query.d.ts.map +1 -1
- package/dist/tanstack-query.js +25 -10
- package/dist/tanstack-query.js.map +1 -1
- package/dist/templates/columns-file.d.ts.map +1 -1
- package/dist/templates/columns-file.js +40 -9
- package/dist/templates/columns-file.js.map +1 -1
- package/dist/templates/grid-hook-file.js +1 -1
- package/dist/templates/grid-hook-file.js.map +1 -1
- package/dist/templates/hooks-file.js +16 -16
- package/dist/templates/hooks-file.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +19 -0
- package/src/reference/grid-hook.ts +154 -0
- package/src/reference/grid.ts +117 -0
- package/src/reference/hooks.ts +112 -0
- package/src/reference-templates.ts +16 -0
- package/src/tanstack-grid-hook.ts +61 -18
- package/src/tanstack-grid.ts +42 -12
- package/src/tanstack-query.ts +34 -10
- package/src/templates/columns-file.ts +39 -6
- package/src/templates/grid-hook-file.ts +1 -1
- package/src/templates/hooks-file.ts +16 -16
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
2
|
-
import { perEntity, type Generator, type GeneratorFactory, formatTs, entityOutputPath, entityMetaFileName, renderEntityMetaFile, servesReadApi, isTphSubtype,
|
|
2
|
+
import { perEntity, type Generator, type GeneratorFactory, formatTs, entityOutputPath, entityMetaFileName, renderEntityMetaFile, servesReadApi, isTphSubtype,
|
|
3
|
+
withClientDirective, namesRef, namesConstArg,
|
|
4
|
+
} from "@metaobjectsdev/codegen-ts";
|
|
3
5
|
import { hasDataGridLayout, warnMissingDataGridLayout } from "./data-grid-gate.js";
|
|
4
6
|
import { renderGridHookFile } from "./templates/grid-hook-file.js";
|
|
5
7
|
|
|
6
8
|
export interface TanstackGridHookOpts {
|
|
7
9
|
filter?: (entity: MetaObject) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Opt a TPH subtype IN to its own per-subtype grid. Default `() => false`: a
|
|
12
|
+
* discriminator base emits ONE polymorphic grid and its subtypes emit none, which is
|
|
13
|
+
* the single-source-of-truth arrangement almost every TPH model wants.
|
|
14
|
+
*
|
|
15
|
+
* This is an OPTION rather than a `filter` clause because it WIDENS. A `filter` is
|
|
16
|
+
* ANDed with the built-in gates, so it can only ever narrow — an opt-in is not
|
|
17
|
+
* expressible through it. (It is not a metadata attribute either: `@emitGrid` was
|
|
18
|
+
* never registered vocabulary, so authoring it failed `meta verify`.)
|
|
19
|
+
*
|
|
20
|
+
* Pass the SAME predicate to `tanstackGrid()` and `tanstackGridHook()`. If they
|
|
21
|
+
* disagree you reproduce #287 exactly: the hook emits a `<Sub>.grid.ts` whose sibling
|
|
22
|
+
* `<Sub>.columns.tsx` is never emitted — a dangling `use<Sub>DefaultGrid()`, and an
|
|
23
|
+
* outright TS2307 when the inherited layout carries an `@filter` preset, since the hook
|
|
24
|
+
* then imports `<sub>DefaultFilter` from the missing columns module.
|
|
25
|
+
*/
|
|
26
|
+
tphSubtypeGrids?: (entity: MetaObject) => boolean;
|
|
8
27
|
target?: string;
|
|
9
28
|
}
|
|
10
29
|
|
|
@@ -12,30 +31,40 @@ export interface TanstackGridHookOpts {
|
|
|
12
31
|
* Per-entity generator that emits <Entity>.grid.ts — one
|
|
13
32
|
* use<Entity><Grid>Grid() hook per layout[dataGrid] declared on the entity.
|
|
14
33
|
*
|
|
15
|
-
* Per-entity opt-
|
|
16
|
-
*
|
|
34
|
+
* Per-entity opt-IN: presence of at least one dataGrid layout on the object
|
|
35
|
+
* (mirrors tanstackGrid).
|
|
36
|
+
*
|
|
37
|
+
* A TPH subtype additionally needs `tphSubtypeGrids` — pass the SAME predicate to
|
|
38
|
+
* tanstackGrid(), or this generator emits a `<Sub>.grid.ts` with no `<Sub>.columns.tsx`
|
|
39
|
+
* beside it.
|
|
40
|
+
*
|
|
41
|
+
* Decide per generator what you consume: wire only the generators whose output you
|
|
42
|
+
* actually import, and narrow this one with its `filter` option. There is no `@emit*`
|
|
43
|
+
* metadata attribute — those were never registered vocabulary, so `meta verify` rejects
|
|
44
|
+
* them (ERR_UNKNOWN_ATTR).
|
|
17
45
|
*/
|
|
18
46
|
export const tanstackGridHook = function tanstackGridHook(opts?: TanstackGridHookOpts): Generator {
|
|
19
47
|
const userFilter = opts?.filter ?? (() => true);
|
|
48
|
+
// Default OFF — byte-identical to the behaviour every project that never declared a
|
|
49
|
+
// per-subtype grid already had.
|
|
50
|
+
const tphSubtypeGrids = opts?.tphSubtypeGrids ?? (() => false);
|
|
20
51
|
// Every gate EXCEPT the dataGrid-layout opt-in: the framework instance-artifact
|
|
21
|
-
// guard (skips abstract types)
|
|
22
|
-
//
|
|
23
|
-
// alone held back (#287).
|
|
52
|
+
// guard (skips abstract types) and the user filter. Split out so the discoverability
|
|
53
|
+
// note can name exactly the entities the LAYOUT gate alone held back (#287).
|
|
24
54
|
//
|
|
25
|
-
// The TPH clause must MATCH tanstackGrid's exactly
|
|
55
|
+
// The TPH clause must MATCH tanstackGrid's exactly, which means the SAME
|
|
56
|
+
// `tphSubtypeGrids` predicate must be passed to both. A TPH subtype inherits its base's
|
|
26
57
|
// dataGrid layout via extends, so `hasDataGridLayout` is true for it — but tanstackGrid
|
|
27
|
-
// deliberately emits no per-subtype columns
|
|
28
|
-
// polymorphic grid is the single source of truth).
|
|
29
|
-
// subtype
|
|
30
|
-
// a dangling `use<Sub>DefaultGrid()` with nothing to pair it with, and an
|
|
31
|
-
// TS2307 when the inherited layout carries an `@filter` preset (the hook then
|
|
32
|
-
// `<sub>DefaultFilter` from the missing columns module).
|
|
58
|
+
// deliberately emits no per-subtype columns unless `tphSubtypeGrids` opts it in (the
|
|
59
|
+
// base's polymorphic grid is the single source of truth). Where the two predicates
|
|
60
|
+
// disagree, a TPH subtype gets a `<Sub>.grid.ts` whose sibling `<Sub>.columns.tsx` is
|
|
61
|
+
// never emitted: a dangling `use<Sub>DefaultGrid()` with nothing to pair it with, and an
|
|
62
|
+
// outright TS2307 when the inherited layout carries an `@filter` preset (the hook then
|
|
63
|
+
// imports `<sub>DefaultFilter` from the missing columns module).
|
|
33
64
|
const passesOtherGates = (e: MetaObject): boolean =>
|
|
34
65
|
servesReadApi(e)
|
|
35
|
-
// ADR-0039: resolving — a concrete entity may inherit its @emit* opt-out flag via extends.
|
|
36
|
-
&& e.attr(CODEGEN_ATTR_EMIT_TANSTACK) !== false
|
|
37
66
|
&& userFilter(e)
|
|
38
|
-
&& (!isTphSubtype(e) || e
|
|
67
|
+
&& (!isTphSubtype(e) || tphSubtypeGrids(e));
|
|
39
68
|
const emit = perEntity(async (entity: MetaObject, ctx) => {
|
|
40
69
|
if (!ctx.renderContext) {
|
|
41
70
|
throw new Error("tanstack-grid-hook: renderContext is required (provided by runGen)");
|
|
@@ -45,17 +74,31 @@ export const tanstackGridHook = function tanstackGridHook(opts?: TanstackGridHoo
|
|
|
45
74
|
// the entity generator is scaffold-and-own (ADR-0034), so it cannot be changed
|
|
46
75
|
// from the package. Emissions are byte-identical between generators, which the
|
|
47
76
|
// runner collapses (#266).
|
|
77
|
+
//
|
|
78
|
+
// §A6/§B2 — same `namesRef` pair every other §A6 site builds, scoped to THIS
|
|
79
|
+
// generator's own render context (see tanstack-query.ts's matching comment for why
|
|
80
|
+
// `.meta.ts` needs its own target-scoped check rather than reusing the entity
|
|
81
|
+
// module's).
|
|
82
|
+
const rc = ctx.renderContext;
|
|
83
|
+
const metaNames = namesRef(entity, rc);
|
|
48
84
|
return [{
|
|
49
85
|
path: entityOutputPath(ctx.renderContext.outputLayout, entity.package,
|
|
50
86
|
entityMetaFileName(entity.name)),
|
|
51
|
-
content: await formatTs(renderEntityMetaFile(
|
|
87
|
+
content: await formatTs(renderEntityMetaFile(
|
|
88
|
+
entity,
|
|
89
|
+
ctx.renderContext.apiPrefix,
|
|
90
|
+
namesConstArg(metaNames),
|
|
91
|
+
)),
|
|
52
92
|
}, {
|
|
53
93
|
path: entityOutputPath(
|
|
54
94
|
ctx.renderContext.outputLayout,
|
|
55
95
|
entity.package,
|
|
56
96
|
`${entity.name}.grid.ts`,
|
|
57
97
|
),
|
|
58
|
-
content:
|
|
98
|
+
content: withClientDirective(
|
|
99
|
+
await formatTs(renderGridHookFile(entity, ctx.renderContext)),
|
|
100
|
+
ctx.renderContext.clientDirective,
|
|
101
|
+
),
|
|
59
102
|
}];
|
|
60
103
|
});
|
|
61
104
|
const generator: Generator = {
|
package/src/tanstack-grid.ts
CHANGED
|
@@ -1,41 +1,71 @@
|
|
|
1
1
|
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
2
|
-
import { perEntity, type Generator, type GeneratorFactory, formatTs, entityOutputPath, servesReadApi, isTphSubtype,
|
|
2
|
+
import { perEntity, type Generator, type GeneratorFactory, formatTs, entityOutputPath, servesReadApi, isTphSubtype,
|
|
3
|
+
withClientDirective,
|
|
4
|
+
} from "@metaobjectsdev/codegen-ts";
|
|
3
5
|
import { hasDataGridLayout, warnMissingDataGridLayout } from "./data-grid-gate.js";
|
|
4
6
|
import { renderColumnsFile } from "./templates/columns-file.js";
|
|
5
7
|
|
|
6
8
|
export interface TanstackGridOpts {
|
|
7
9
|
filter?: (entity: MetaObject) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Opt a TPH subtype IN to its own per-subtype grid. Default `() => false`: a
|
|
12
|
+
* discriminator base emits ONE polymorphic grid and its subtypes emit none, which is
|
|
13
|
+
* the single-source-of-truth arrangement almost every TPH model wants.
|
|
14
|
+
*
|
|
15
|
+
* This is an OPTION rather than a `filter` clause because it WIDENS. A `filter` is
|
|
16
|
+
* ANDed with the built-in gates, so it can only ever narrow — an opt-in is not
|
|
17
|
+
* expressible through it. (It is not a metadata attribute either: `@emitGrid` was
|
|
18
|
+
* never registered vocabulary, so authoring it failed `meta verify`.)
|
|
19
|
+
*
|
|
20
|
+
* Pass the SAME predicate to `tanstackGrid()` and `tanstackGridHook()`. If they
|
|
21
|
+
* disagree you reproduce #287 exactly: the hook emits a `<Sub>.grid.ts` whose sibling
|
|
22
|
+
* `<Sub>.columns.tsx` is never emitted — a dangling `use<Sub>DefaultGrid()`, and an
|
|
23
|
+
* outright TS2307 when the inherited layout carries an `@filter` preset, since the hook
|
|
24
|
+
* then imports `<sub>DefaultFilter` from the missing columns module.
|
|
25
|
+
*/
|
|
26
|
+
tphSubtypeGrids?: (entity: MetaObject) => boolean;
|
|
8
27
|
target?: string;
|
|
9
28
|
}
|
|
10
29
|
|
|
11
30
|
/**
|
|
12
|
-
* Per-entity opt-
|
|
13
|
-
*
|
|
14
|
-
*
|
|
31
|
+
* Per-entity opt-IN: presence of at least one `dataGrid` layout on the object. If that
|
|
32
|
+
* passes and the user-supplied filter passes, the generator emits.
|
|
33
|
+
*
|
|
34
|
+
* A TPH subtype additionally needs `tphSubtypeGrids` — pass the SAME predicate to
|
|
35
|
+
* tanstackGridHook(), or the emitted `<Sub>.grid.ts` has no `<Sub>.columns.tsx`.
|
|
36
|
+
*
|
|
37
|
+
* Decide per generator what you consume: wire only the generators whose output you
|
|
38
|
+
* actually import, and narrow this one with its `filter` option. There is no `@emit*`
|
|
39
|
+
* metadata attribute — those were never registered vocabulary, so `meta verify` rejects
|
|
40
|
+
* them (ERR_UNKNOWN_ATTR).
|
|
15
41
|
*/
|
|
16
42
|
export const tanstackGrid = function tanstackGrid(opts?: TanstackGridOpts): Generator {
|
|
17
43
|
const userFilter = opts?.filter ?? (() => true);
|
|
44
|
+
// Default OFF — byte-identical to the behaviour every project that never declared a
|
|
45
|
+
// per-subtype grid already had.
|
|
46
|
+
const tphSubtypeGrids = opts?.tphSubtypeGrids ?? (() => false);
|
|
18
47
|
// Every gate EXCEPT the dataGrid-layout opt-in: the framework instance-artifact
|
|
19
|
-
// guard (skips abstract types)
|
|
48
|
+
// guard (skips abstract types) and the user filter.
|
|
20
49
|
// FR-017 Tier 3: a TPH discriminator base emits ONE polymorphic grid. Its
|
|
21
50
|
// subtypes inherit the base's dataGrid layout via extends, but per-subtype grids
|
|
22
|
-
// are opt-IN only
|
|
23
|
-
// single source of truth.
|
|
51
|
+
// are opt-IN only, via the `tphSubtypeGrids` option — otherwise the polymorphic
|
|
52
|
+
// grid is the single source of truth. Pass the SAME predicate to tanstackGridHook().
|
|
24
53
|
// Split out so the discoverability note can name exactly the entities the LAYOUT
|
|
25
|
-
// gate alone held back (#287) — an
|
|
54
|
+
// gate alone held back (#287) — an abstract type is not a surprise.
|
|
26
55
|
const passesOtherGates = (e: MetaObject): boolean =>
|
|
27
56
|
servesReadApi(e)
|
|
28
|
-
// ADR-0039: resolving — a concrete entity may inherit its @emit* opt-out flag via extends.
|
|
29
|
-
&& e.attr(CODEGEN_ATTR_EMIT_TANSTACK) !== false
|
|
30
57
|
&& userFilter(e)
|
|
31
|
-
&& (!isTphSubtype(e) || e
|
|
58
|
+
&& (!isTphSubtype(e) || tphSubtypeGrids(e));
|
|
32
59
|
const emit = perEntity(async (entity: MetaObject, ctx) => {
|
|
33
60
|
if (!ctx.renderContext) {
|
|
34
61
|
throw new Error("tanstack-grid: renderContext is required (provided by runGen)");
|
|
35
62
|
}
|
|
36
63
|
return {
|
|
37
64
|
path: entityOutputPath(ctx.renderContext.outputLayout, entity.package, `${entity.name}.columns.tsx`),
|
|
38
|
-
content:
|
|
65
|
+
content: withClientDirective(
|
|
66
|
+
await formatTs(renderColumnsFile(entity, ctx.renderContext)),
|
|
67
|
+
ctx.renderContext.clientDirective,
|
|
68
|
+
),
|
|
39
69
|
};
|
|
40
70
|
});
|
|
41
71
|
const generator: Generator = {
|
package/src/tanstack-query.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
2
|
-
import { perEntity, type Generator, type GeneratorFactory, formatTs, entityOutputPath, entityMetaFileName, renderEntityMetaFile, servesReadApi, isTphSubtype,
|
|
2
|
+
import { perEntity, type Generator, type GeneratorFactory, formatTs, entityOutputPath, entityMetaFileName, renderEntityMetaFile, servesReadApi, isTphSubtype,
|
|
3
|
+
withClientDirective, namesRef, namesConstArg,
|
|
4
|
+
} from "@metaobjectsdev/codegen-ts";
|
|
3
5
|
import { renderHooksFile } from "./templates/hooks-file.js";
|
|
4
6
|
|
|
5
7
|
export interface TanstackQueryOpts {
|
|
@@ -11,21 +13,24 @@ export interface TanstackQueryOpts {
|
|
|
11
13
|
* Per-entity generator that emits <Entity>.hooks.ts — a query-key factory
|
|
12
14
|
* plus 2 query hooks and 3 mutation hooks backed by useEntityFetcher().
|
|
13
15
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
+
* If the user supplies their own filter, it AND-composes with the built-in gates.
|
|
17
|
+
*
|
|
18
|
+
* Decide per generator what you consume: wire only the generators whose output you
|
|
19
|
+
* actually import, and narrow this one with its `filter` option. There is no `@emit*`
|
|
20
|
+
* metadata attribute — those were never registered vocabulary, so `meta verify` rejects
|
|
21
|
+
* them (ERR_UNKNOWN_ATTR).
|
|
16
22
|
*/
|
|
17
23
|
export const tanstackQuery = function tanstackQuery(opts?: TanstackQueryOpts): Generator {
|
|
18
24
|
const userFilter = opts?.filter ?? (() => true);
|
|
19
25
|
const generator: Generator = {
|
|
20
26
|
name: "tanstack-query",
|
|
21
27
|
// AND-composes the framework instance-artifact guard (skips abstract types —
|
|
22
|
-
// they contribute shape via inheritance only and have no instance to query)
|
|
23
|
-
//
|
|
24
|
-
//
|
|
28
|
+
// they contribute shape via inheritance only and have no instance to query)
|
|
29
|
+
// with the optional user filter. Projections still pass here and get read-only
|
|
30
|
+
// hooks via renderHooksFile's isProjection branch.
|
|
25
31
|
// FR-017 Tier 3: TPH subtypes get no standalone hooks file — their per-subtype
|
|
26
32
|
// hooks live in the discriminator base's hooks file (polymorphic + per-subtype).
|
|
27
|
-
|
|
28
|
-
filter: (e: MetaObject) => servesReadApi(e) && e.attr(CODEGEN_ATTR_EMIT_TANSTACK) !== false && !isTphSubtype(e) && userFilter(e),
|
|
33
|
+
filter: (e: MetaObject) => servesReadApi(e) && !isTphSubtype(e) && userFilter(e),
|
|
29
34
|
generate: perEntity(async (entity, ctx) => {
|
|
30
35
|
if (!ctx.renderContext) {
|
|
31
36
|
throw new Error(
|
|
@@ -36,14 +41,33 @@ export const tanstackQuery = function tanstackQuery(opts?: TanstackQueryOpts): G
|
|
|
36
41
|
// generator: the entity generator is scaffold-and-own (ADR-0034), so it cannot
|
|
37
42
|
// be changed from the package. Emissions are byte-identical between generators,
|
|
38
43
|
// which the runner collapses (#266).
|
|
44
|
+
//
|
|
45
|
+
// §A6/§B2 — same `namesRef` pair every other §A6 site builds, scoped to THIS
|
|
46
|
+
// generator's own render context: `.meta.ts` is a UI-generator artifact that can
|
|
47
|
+
// sit on a DIFFERENT target from `namesFile()` (unlike the entity module, which
|
|
48
|
+
// shares a target with names by construction — see names-file.ts).
|
|
49
|
+
// `ctx.renderContext.includeNames` is already computed per-target by the runner,
|
|
50
|
+
// so it is false here whenever the names artifact does not land in THIS
|
|
51
|
+
// generator's own target — no separate check needed.
|
|
52
|
+
const rc = ctx.renderContext;
|
|
53
|
+
const metaNames = namesRef(entity, rc);
|
|
39
54
|
const metaFile = {
|
|
40
55
|
path: entityOutputPath(ctx.renderContext.outputLayout, entity.package,
|
|
41
56
|
entityMetaFileName(entity.name)),
|
|
42
|
-
content: await formatTs(renderEntityMetaFile(
|
|
57
|
+
content: await formatTs(renderEntityMetaFile(
|
|
58
|
+
entity,
|
|
59
|
+
ctx.renderContext.apiPrefix,
|
|
60
|
+
namesConstArg(metaNames),
|
|
61
|
+
)),
|
|
43
62
|
};
|
|
44
63
|
return [metaFile, {
|
|
45
64
|
path: entityOutputPath(ctx.renderContext.outputLayout, entity.package, `${entity.name}.hooks.ts`),
|
|
46
|
-
|
|
65
|
+
// Outside formatTs deliberately: the directive must stay the module's first
|
|
66
|
+
// token, and a formatter is entitled to move a leading string expression.
|
|
67
|
+
content: withClientDirective(
|
|
68
|
+
await formatTs(renderHooksFile(entity, ctx.renderContext)),
|
|
69
|
+
ctx.renderContext.clientDirective,
|
|
70
|
+
),
|
|
47
71
|
}];
|
|
48
72
|
}),
|
|
49
73
|
};
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
LAYOUT_DATA_GRID_ATTR_FILTER,
|
|
10
10
|
LAYOUT_DATA_GRID_ATTR_COLUMNS,
|
|
11
11
|
OBJECT_ATTR_DISCRIMINATOR,
|
|
12
|
+
VIEW_SUBTYPE_HIDDEN,
|
|
12
13
|
} from "@metaobjectsdev/metadata";
|
|
13
14
|
import type { RenderContext } from "@metaobjectsdev/codegen-ts";
|
|
14
15
|
import {
|
|
@@ -16,6 +17,9 @@ import {
|
|
|
16
17
|
entityModuleSpecifier,
|
|
17
18
|
isTphDiscriminatorBase,
|
|
18
19
|
collectTphSubtypeFields,
|
|
20
|
+
isSortableField,
|
|
21
|
+
viewForContext,
|
|
22
|
+
VIEW_CONTEXT_GRID,
|
|
19
23
|
} from "@metaobjectsdev/codegen-ts";
|
|
20
24
|
|
|
21
25
|
/** FR-017 TPH grid context, threaded into extractGrids when the entity is a
|
|
@@ -52,16 +56,27 @@ function humanize(s: string): string {
|
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
function fieldViewKind(field: MetaField): string {
|
|
59
|
+
// #356: the view declared for the GRID, never whichever view came first — a
|
|
60
|
+
// field declaring a form control and a grid cell must render both.
|
|
55
61
|
// ADR-0039: resolving — a field's view may be inherited via extends.
|
|
56
|
-
const view = field
|
|
62
|
+
const view = viewForContext(field, VIEW_CONTEXT_GRID);
|
|
57
63
|
return view?.subType ?? "text";
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
function fieldLabel(field: MetaField): string {
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
// #353 — this read `@label`, which NO provider registers, so the override branch was
|
|
68
|
+
// unreachable: authoring it fails the strict load `meta verify` runs (ERR_UNKNOWN_ATTR)
|
|
69
|
+
// and every header fell back to humanize(). `title` is already a registered common attr
|
|
70
|
+
// on every node and already means "a noun phrase", so nothing was registered for this
|
|
71
|
+
// — ADR-0037 step 0: the vocabulary existed, only the read was wrong.
|
|
72
|
+
// View first, then the field itself: a view-level title is the more specific override
|
|
73
|
+
// (this rendering of the field); a field-level one names the field wherever it appears.
|
|
74
|
+
// ADR-0039: resolving — a field's view (and its @title) may be inherited via extends.
|
|
75
|
+
// #356: the GRID's own view — a form control's @title must not retitle a column.
|
|
76
|
+
const viewTitle = viewForContext(field, VIEW_CONTEXT_GRID)?.attr("title");
|
|
77
|
+
if (typeof viewTitle === "string" && viewTitle.length > 0) return viewTitle;
|
|
78
|
+
const fieldTitle = field.attr("title");
|
|
79
|
+
if (typeof fieldTitle === "string" && fieldTitle.length > 0) return fieldTitle;
|
|
65
80
|
return humanize(field.name);
|
|
66
81
|
}
|
|
67
82
|
|
|
@@ -106,10 +121,28 @@ function extractGrids(entity: MetaObject, tph?: TphGridInfo): GridSpec[] {
|
|
|
106
121
|
const columns: ColumnSpec[] = columnNames.flatMap((name) => {
|
|
107
122
|
const field = fieldsByName.get(name);
|
|
108
123
|
if (!field) return []; // columns ref that doesn't exist on entity; defensive skip
|
|
124
|
+
const viewKind = fieldViewKind(field);
|
|
125
|
+
// #355 — `view.hidden` is registered as "Not rendered; carried but not shown", and
|
|
126
|
+
// the generated FORM has always honoured that (an <input type="hidden">). The grid
|
|
127
|
+
// did not: no renderer is keyed `hidden`, so EntityGrid's `if (!renderer) return col`
|
|
128
|
+
// fell through to TanStack's default cell and PRINTED the value — the opposite of
|
|
129
|
+
// what the subtype says. Emitting a blank cell would not fix it either: the column
|
|
130
|
+
// would still hold a header and a sort target, so the value would be hidden while
|
|
131
|
+
// the column was not. The column is dropped instead, which is what "not rendered"
|
|
132
|
+
// means. This applies even when @columns names the field: a declaration that the
|
|
133
|
+
// field is a column and a declaration that it is not rendered contradict each other,
|
|
134
|
+
// and the one about RENDERING is the specific answer to the question a grid asks.
|
|
135
|
+
if (viewKind === VIEW_SUBTYPE_HIDDEN) return [];
|
|
109
136
|
const spec: ColumnSpec = {
|
|
110
137
|
id: name,
|
|
111
138
|
header: fieldLabel(field),
|
|
112
|
-
viewKind
|
|
139
|
+
viewKind,
|
|
140
|
+
// #352 — ALWAYS emitted, never left absent. EntityGrid gates on
|
|
141
|
+
// `meta?.sortable !== false`, so an omitted flag reads as "offer it" and the
|
|
142
|
+
// header renders clickable for a field the server's SortAllowlist rejects
|
|
143
|
+
// (400 `sort.unknown_field`). isSortableField is the SAME predicate that builds
|
|
144
|
+
// that allowlist — imported (#354), never reimplemented, so the two cannot drift.
|
|
145
|
+
sortable: isSortableField(field),
|
|
113
146
|
};
|
|
114
147
|
// FR-017: the discriminator column renders as a subtype badge.
|
|
115
148
|
if (tph && name === tph.discField) spec.renderer = "badge";
|
|
@@ -138,7 +138,7 @@ export function ${hookName}() {
|
|
|
138
138
|
const query = ${useQuerySym}<{ rows: ${entityName}Row[]; total: number }>({
|
|
139
139
|
queryKey: [${JSON.stringify(lcEntity)}, "grid", ${JSON.stringify(grid.name)}, qs],
|
|
140
140
|
queryFn: () => fetcher<{ rows: ${entityName}Row[]; total: number }>(
|
|
141
|
-
\`\${${entityName}.$
|
|
141
|
+
\`\${${entityName}.$path}?\${qs}\`,
|
|
142
142
|
),
|
|
143
143
|
});
|
|
144
144
|
|
|
@@ -144,7 +144,7 @@ export function ${hookName}(
|
|
|
144
144
|
const fetcher = ${useEntityFetcherSym}();
|
|
145
145
|
return ${useQuerySym}<${targetSym}[]>({
|
|
146
146
|
queryKey: ${keysVar}.relation(${relLit}, sourceId),
|
|
147
|
-
queryFn: () => fetcher<${targetSym}[]>(\`\${${source}.$
|
|
147
|
+
queryFn: () => fetcher<${targetSym}[]>(\`\${${source}.$path}/\${sourceId}/${e.name}\`),
|
|
148
148
|
enabled: sourceId != null && (opts?.enabled ?? true),
|
|
149
149
|
...opts,
|
|
150
150
|
});
|
|
@@ -206,7 +206,7 @@ export function use${entityName}(
|
|
|
206
206
|
const fetcher = ${useEntityFetcherSym}();
|
|
207
207
|
return ${useQuerySym}<${entityName}Row>({
|
|
208
208
|
queryKey: ${keysVar}.detail(id),
|
|
209
|
-
queryFn: () => fetcher<${entityName}Row>(\`\${${entityName}.$
|
|
209
|
+
queryFn: () => fetcher<${entityName}Row>(\`\${${entityName}.$path}/\${id}\`),
|
|
210
210
|
...opts,
|
|
211
211
|
});
|
|
212
212
|
}
|
|
@@ -219,7 +219,7 @@ export function use${entityNamePlural}(
|
|
|
219
219
|
const qs = filter ? "?" + ${buildFilterQsSym}(filter as Record<string, unknown>) : "";
|
|
220
220
|
return ${useQuerySym}<${entityName}Row[]>({
|
|
221
221
|
queryKey: ${keysVar}.list(filter),
|
|
222
|
-
queryFn: () => fetcher<${entityName}Row[]>(\`\${${entityName}.$
|
|
222
|
+
queryFn: () => fetcher<${entityName}Row[]>(\`\${${entityName}.$path}\${qs}\`),
|
|
223
223
|
...opts,
|
|
224
224
|
});
|
|
225
225
|
}
|
|
@@ -287,7 +287,7 @@ export function use${entityName}(
|
|
|
287
287
|
const fetcher = ${useEntityFetcherSym}();
|
|
288
288
|
return ${useQuerySym}<${entityName}Row>({
|
|
289
289
|
queryKey: ${keysVar}.detail(id),
|
|
290
|
-
queryFn: () => fetcher<${entityName}Row>(\`\${${entityName}.$
|
|
290
|
+
queryFn: () => fetcher<${entityName}Row>(\`\${${entityName}.$path}/\${id}\`),
|
|
291
291
|
...opts,
|
|
292
292
|
});
|
|
293
293
|
}
|
|
@@ -300,7 +300,7 @@ export function use${entityNamePlural}(
|
|
|
300
300
|
const qs = filter ? "?" + ${buildFilterQsSym}(filter as Record<string, unknown>) : "";
|
|
301
301
|
return ${useQuerySym}<${entityName}Row[]>({
|
|
302
302
|
queryKey: ${keysVar}.list(filter),
|
|
303
|
-
queryFn: () => fetcher<${entityName}Row[]>(\`\${${entityName}.$
|
|
303
|
+
queryFn: () => fetcher<${entityName}Row[]>(\`\${${entityName}.$path}\${qs}\`),
|
|
304
304
|
...opts,
|
|
305
305
|
});
|
|
306
306
|
}
|
|
@@ -315,7 +315,7 @@ export function useCreate${entityName}(
|
|
|
315
315
|
const fetcher = ${useEntityFetcherSym}();
|
|
316
316
|
const qc = ${useQueryClientSym}();
|
|
317
317
|
return ${useMutationSym}<${entityName}Row, Error, ${entityName}Insert>({
|
|
318
|
-
mutationFn: (input) => fetcher<${entityName}Row>(\`\${${entityName}.$
|
|
318
|
+
mutationFn: (input) => fetcher<${entityName}Row>(\`\${${entityName}.$path}\`, {
|
|
319
319
|
method: "POST",
|
|
320
320
|
headers: { "Content-Type": "application/json" },
|
|
321
321
|
body: JSON.stringify(input),
|
|
@@ -334,7 +334,7 @@ export function useUpdate${entityName}(
|
|
|
334
334
|
const fetcher = ${useEntityFetcherSym}();
|
|
335
335
|
const qc = ${useQueryClientSym}();
|
|
336
336
|
return ${useMutationSym}({
|
|
337
|
-
mutationFn: ({ id, input }) => fetcher<${entityName}Row>(\`\${${entityName}.$
|
|
337
|
+
mutationFn: ({ id, input }) => fetcher<${entityName}Row>(\`\${${entityName}.$path}/\${id}\`, {
|
|
338
338
|
method: "PATCH",
|
|
339
339
|
headers: { "Content-Type": "application/json" },
|
|
340
340
|
body: JSON.stringify(input),
|
|
@@ -353,7 +353,7 @@ export function useDelete${entityName}(
|
|
|
353
353
|
const fetcher = ${useEntityFetcherSym}();
|
|
354
354
|
const qc = ${useQueryClientSym}();
|
|
355
355
|
return ${useMutationSym}({
|
|
356
|
-
mutationFn: (id) => fetcher<void>(\`\${${entityName}.$
|
|
356
|
+
mutationFn: (id) => fetcher<void>(\`\${${entityName}.$path}/\${id}\`, { method: "DELETE" }),
|
|
357
357
|
...opts,
|
|
358
358
|
onSuccess: (...args) => {
|
|
359
359
|
qc.invalidateQueries({ queryKey: ${keysVar}.all() });
|
|
@@ -401,7 +401,7 @@ function renderTphHooksFile(base: MetaObject, ctx: RenderContext, baseModule: st
|
|
|
401
401
|
|
|
402
402
|
const subtypes = plan.subtypes;
|
|
403
403
|
|
|
404
|
-
// `${baseName}` imports BOTH the constants value (for $path
|
|
404
|
+
// `${baseName}` imports BOTH the constants value (for $path) and the
|
|
405
405
|
// discriminated-union type (declaration merge). Each subtype contributes its
|
|
406
406
|
// interface type AND its own filter type (discriminator-excluded — the route
|
|
407
407
|
// pins it), so per-subtype hooks filter on the fields the per-subtype
|
|
@@ -442,7 +442,7 @@ export function use${baseName}(
|
|
|
442
442
|
const fetcher = ${useEntityFetcherSym}();
|
|
443
443
|
return ${useQuerySym}<${baseName}>({
|
|
444
444
|
queryKey: ${keysVar}.detail(id),
|
|
445
|
-
queryFn: () => fetcher<${baseName}>(\`\${${baseName}.$
|
|
445
|
+
queryFn: () => fetcher<${baseName}>(\`\${${baseName}.$path}/\${id}\`),
|
|
446
446
|
...opts,
|
|
447
447
|
});
|
|
448
448
|
}
|
|
@@ -455,7 +455,7 @@ export function use${pluralize(baseName)}(
|
|
|
455
455
|
const qs = filter ? "?" + ${buildFilterQsSym}(filter as Record<string, unknown>) : "";
|
|
456
456
|
return ${useQuerySym}<${baseName}[]>({
|
|
457
457
|
queryKey: ${keysVar}.list(filter),
|
|
458
|
-
queryFn: () => fetcher<${baseName}[]>(\`\${${baseName}.$
|
|
458
|
+
queryFn: () => fetcher<${baseName}[]>(\`\${${baseName}.$path}\${qs}\`),
|
|
459
459
|
...opts,
|
|
460
460
|
});
|
|
461
461
|
}
|
|
@@ -467,7 +467,7 @@ export function use${pluralize(baseName)}(
|
|
|
467
467
|
const valueLit = JSON.stringify(value);
|
|
468
468
|
const createInput = `Omit<${subName}, ${JSON.stringify(discField)}>`;
|
|
469
469
|
const updateInput = `Partial<${createInput}>`;
|
|
470
|
-
const subPath = `\`\${${baseName}.$
|
|
470
|
+
const subPath = `\`\${${baseName}.$path}/${seg}\``;
|
|
471
471
|
return code`
|
|
472
472
|
export function use${pluralize(subName)}(
|
|
473
473
|
filter?: ${subName}Filter,
|
|
@@ -477,7 +477,7 @@ export function use${pluralize(subName)}(
|
|
|
477
477
|
const qs = filter ? "?" + ${buildFilterQsSym}(filter as Record<string, unknown>) : "";
|
|
478
478
|
return ${useQuerySym}<${subName}[]>({
|
|
479
479
|
queryKey: ${keysVar}.subtypeList(${valueLit}, filter),
|
|
480
|
-
queryFn: () => fetcher<${subName}[]>(\`\${${baseName}.$
|
|
480
|
+
queryFn: () => fetcher<${subName}[]>(\`\${${baseName}.$path}/${seg}\${qs}\`),
|
|
481
481
|
...opts,
|
|
482
482
|
});
|
|
483
483
|
}
|
|
@@ -489,7 +489,7 @@ export function use${subName}(
|
|
|
489
489
|
const fetcher = ${useEntityFetcherSym}();
|
|
490
490
|
return ${useQuerySym}<${subName}>({
|
|
491
491
|
queryKey: ${keysVar}.subtypeDetail(${valueLit}, id),
|
|
492
|
-
queryFn: () => fetcher<${subName}>(\`\${${baseName}.$
|
|
492
|
+
queryFn: () => fetcher<${subName}>(\`\${${baseName}.$path}/${seg}/\${id}\`),
|
|
493
493
|
...opts,
|
|
494
494
|
});
|
|
495
495
|
}
|
|
@@ -519,7 +519,7 @@ export function useUpdate${subName}(
|
|
|
519
519
|
const fetcher = ${useEntityFetcherSym}();
|
|
520
520
|
const qc = ${useQueryClientSym}();
|
|
521
521
|
return ${useMutationSym}({
|
|
522
|
-
mutationFn: ({ id, input }) => fetcher<${subName}>(\`\${${baseName}.$
|
|
522
|
+
mutationFn: ({ id, input }) => fetcher<${subName}>(\`\${${baseName}.$path}/${seg}/\${id}\`, {
|
|
523
523
|
method: "PATCH",
|
|
524
524
|
headers: { "Content-Type": "application/json" },
|
|
525
525
|
body: JSON.stringify(input),
|
|
@@ -538,7 +538,7 @@ export function useDelete${subName}(
|
|
|
538
538
|
const fetcher = ${useEntityFetcherSym}();
|
|
539
539
|
const qc = ${useQueryClientSym}();
|
|
540
540
|
return ${useMutationSym}({
|
|
541
|
-
mutationFn: (id) => fetcher<void>(\`\${${baseName}.$
|
|
541
|
+
mutationFn: (id) => fetcher<void>(\`\${${baseName}.$path}/${seg}/\${id}\`, { method: "DELETE" }),
|
|
542
542
|
...opts,
|
|
543
543
|
onSuccess: (...args) => {
|
|
544
544
|
qc.invalidateQueries({ queryKey: ${keysVar}.all() });
|