@pilotiq/pilotiq 0.8.2 → 0.10.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/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +213 -0
- package/dist/Pilotiq.d.ts +55 -0
- package/dist/Pilotiq.d.ts.map +1 -1
- package/dist/Pilotiq.js +21 -0
- package/dist/Pilotiq.js.map +1 -1
- package/dist/Resource.d.ts +39 -0
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +30 -0
- package/dist/Resource.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pageData/helpers.d.ts +19 -1
- package/dist/pageData/helpers.d.ts.map +1 -1
- package/dist/pageData/helpers.js +33 -0
- package/dist/pageData/helpers.js.map +1 -1
- package/dist/pageData/navigation.d.ts +17 -1
- package/dist/pageData/navigation.d.ts.map +1 -1
- package/dist/pageData/navigation.js +14 -0
- package/dist/pageData/navigation.js.map +1 -1
- package/dist/pageData/resourcePages.d.ts.map +1 -1
- package/dist/pageData/resourcePages.js +17 -2
- package/dist/pageData/resourcePages.js.map +1 -1
- package/dist/pageData.d.ts +1 -1
- package/dist/pageData.d.ts.map +1 -1
- package/dist/pageData.js +1 -1
- package/dist/pageData.js.map +1 -1
- package/dist/react/AppShell.d.ts +5 -0
- package/dist/react/AppShell.d.ts.map +1 -1
- package/dist/react/AppShell.js +1 -1
- package/dist/react/AppShell.js.map +1 -1
- package/dist/react/FormCollabBindingRegistry.d.ts +71 -1
- package/dist/react/FormCollabBindingRegistry.d.ts.map +1 -1
- package/dist/react/FormCollabBindingRegistry.js.map +1 -1
- package/dist/react/FormStateContext.d.ts +17 -0
- package/dist/react/FormStateContext.d.ts.map +1 -1
- package/dist/react/FormStateContext.js +44 -3
- package/dist/react/FormStateContext.js.map +1 -1
- package/dist/react/RecordWrapperGate.d.ts +19 -6
- package/dist/react/RecordWrapperGate.d.ts.map +1 -1
- package/dist/react/RecordWrapperGate.js +18 -8
- package/dist/react/RecordWrapperGate.js.map +1 -1
- package/dist/react/fields/MarkdownInput.d.ts.map +1 -1
- package/dist/react/fields/MarkdownInput.js +105 -3
- package/dist/react/fields/MarkdownInput.js.map +1 -1
- package/dist/react/fields/TextLikeInput.d.ts +10 -0
- package/dist/react/fields/TextLikeInput.d.ts.map +1 -1
- package/dist/react/fields/TextLikeInput.js +179 -0
- package/dist/react/fields/TextLikeInput.js.map +1 -1
- package/dist/react/fields/textDelta.d.ts +44 -0
- package/dist/react/fields/textDelta.d.ts.map +1 -0
- package/dist/react/fields/textDelta.js +80 -0
- package/dist/react/fields/textDelta.js.map +1 -0
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/parseRecordEditUrl.d.ts +33 -9
- package/dist/react/parseRecordEditUrl.d.ts.map +1 -1
- package/dist/react/parseRecordEditUrl.js +40 -2
- package/dist/react/parseRecordEditUrl.js.map +1 -1
- package/package.json +1 -1
- package/src/Pilotiq.ts +64 -0
- package/src/Resource.test.ts +44 -0
- package/src/Resource.ts +58 -0
- package/src/index.ts +2 -0
- package/src/pageData/helpers.ts +40 -1
- package/src/pageData/navigation.ts +32 -1
- package/src/pageData/resourcePages.ts +17 -1
- package/src/pageData.test.ts +137 -0
- package/src/pageData.ts +1 -0
- package/src/react/AppShell.tsx +6 -1
- package/src/react/FormCollabBindingRegistry.ts +63 -1
- package/src/react/FormStateContext.tsx +62 -3
- package/src/react/RecordWrapperGate.tsx +26 -8
- package/src/react/fields/MarkdownInput.tsx +100 -3
- package/src/react/fields/TextLikeInput.tsx +203 -1
- package/src/react/fields/textDelta.test.ts +141 -0
- package/src/react/fields/textDelta.ts +86 -0
- package/src/react/index.ts +9 -1
- package/src/react/parseRecordEditUrl.test.ts +48 -1
- package/src/react/parseRecordEditUrl.ts +52 -13
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAsB,MAAM,eAAe,CAAA;AAC5D,OAAO,EACL,yBAAyB,EACzB,oBAAoB,EACpB,UAAU,GAEX,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EACL,cAAc,EAEd,UAAU,GAEX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAA2B,MAAM,eAAe,CAAA;AAChG,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAA4B,MAAM,6BAA6B,CAAA;AACjH,OAAO,EACL,gCAAgC,EAChC,2BAA2B,GAE5B,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,6BAA6B,GAI9B,MAAM,gCAAgC,CAAA;AACvC,OAAO,EACL,gCAAgC,EAChC,2BAA2B,GAE5B,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EACL,iBAAiB,EACjB,aAAa,GAEd,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACL,wBAAwB,EACxB,mBAAmB,GAGpB,MAAM,qCAAqC,CAAA;AAC5C,OAAO,EACL,yBAAyB,EACzB,oBAAoB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* URL → record-page identity parser. Used by `RecordWrapperGate` (and any
|
|
3
|
+
* plugin that wants to reason about record-bound URLs) to decide whether
|
|
4
|
+
* the current page is a record-edit or record-view route.
|
|
4
5
|
*
|
|
5
6
|
* A URL matches when:
|
|
6
7
|
* 1. it starts with the panel's `basePath`
|
|
7
|
-
* 2. after stripping the prefix it ends with `/edit`
|
|
8
|
+
* 2. after stripping the prefix it ends with `/edit` or `/view`
|
|
8
9
|
* 3. there are at least three remaining segments (resource slug,
|
|
9
|
-
* record id,
|
|
10
|
+
* record id, terminal token)
|
|
10
11
|
*
|
|
11
12
|
* The `resourceSlug` is the slash-joined chain of every segment up to
|
|
12
13
|
* the record id — this gives clustered resources (`${base}/blog/articles/123/edit`)
|
|
@@ -14,13 +15,36 @@
|
|
|
14
15
|
* distinct slugs so two URLs that target different records always
|
|
15
16
|
* produce different room names downstream.
|
|
16
17
|
*
|
|
17
|
-
* `/admin/articles/123/edit` → {
|
|
18
|
-
* `/admin/
|
|
19
|
-
* `/admin/articles/123/
|
|
20
|
-
* `/admin/articles/123/comments`
|
|
21
|
-
* `/admin/articles/123/comments
|
|
18
|
+
* `/admin/articles/123/edit` → { slug: 'articles', id: '123', role: 'edit' }
|
|
19
|
+
* `/admin/articles/123/view` → { slug: 'articles', id: '123', role: 'view' }
|
|
20
|
+
* `/admin/blog/articles/123/edit` → { slug: 'blog/articles', id: '123', role: 'edit' }
|
|
21
|
+
* `/admin/articles/123/comments/456/edit` → { slug: 'articles/123/comments', id: '456', role: 'edit' }
|
|
22
|
+
* `/admin/articles/123/comments` → null (no trailing /edit or /view)
|
|
23
|
+
* `/admin/articles/123/comments/create` → null (terminal token isn't edit|view)
|
|
22
24
|
* `/site/articles/123/edit` → null (basePath mismatch when basePath='/admin')
|
|
23
25
|
*/
|
|
26
|
+
/** Page roles `parseRecordPageUrl` recognizes. `'edit'` and `'view'`
|
|
27
|
+
* are the two record-scoped page roles where collab and other
|
|
28
|
+
* record-bound plugins want to mount their per-record wrapper. */
|
|
29
|
+
export type RecordPageRole = 'edit' | 'view';
|
|
30
|
+
export interface RecordPageIdentity {
|
|
31
|
+
resourceSlug: string;
|
|
32
|
+
recordId: string;
|
|
33
|
+
/** Which terminal URL segment matched — `'edit'` for the writable edit
|
|
34
|
+
* page, `'view'` for the read-only view page. Lets the gate decide per
|
|
35
|
+
* resource whether collab activates on this role (defaults to `'edit'`
|
|
36
|
+
* only; resources opt into `'view'` for presence-only experiences). */
|
|
37
|
+
role: RecordPageRole;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parse a pilotiq URL into a `{ slug, id, role }` identity. Returns
|
|
41
|
+
* `null` for any URL that isn't a record-edit or record-view page.
|
|
42
|
+
*/
|
|
43
|
+
export declare function parseRecordPageUrl(currentPath: string, basePath: string): RecordPageIdentity | null;
|
|
44
|
+
/** Legacy alias: parse a URL into an edit-only identity. Returns `null`
|
|
45
|
+
* for view URLs (and any non-edit URL). Kept for back-compat with the
|
|
46
|
+
* pre-`parseRecordPageUrl` public API; new code should call
|
|
47
|
+
* `parseRecordPageUrl` and branch on `role`. */
|
|
24
48
|
export interface RecordEditIdentity {
|
|
25
49
|
resourceSlug: string;
|
|
26
50
|
recordId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parseRecordEditUrl.d.ts","sourceRoot":"","sources":["../../src/react/parseRecordEditUrl.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"parseRecordEditUrl.d.ts","sourceRoot":"","sources":["../../src/react/parseRecordEditUrl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;kEAEkE;AAClE,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,CAAA;AAE5C,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAM,MAAM,CAAA;IACpB;;;2EAGuE;IACvE,IAAI,EAAU,cAAc,CAAA;CAC7B;AAID;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAK,MAAM,GAClB,kBAAkB,GAAG,IAAI,CAyB3B;AAED;;;gDAGgD;AAChD,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAM,MAAM,CAAA;CACrB;AAED,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAK,MAAM,GAClB,kBAAkB,GAAG,IAAI,CAI3B"}
|
|
@@ -1,4 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* URL → record-page identity parser. Used by `RecordWrapperGate` (and any
|
|
3
|
+
* plugin that wants to reason about record-bound URLs) to decide whether
|
|
4
|
+
* the current page is a record-edit or record-view route.
|
|
5
|
+
*
|
|
6
|
+
* A URL matches when:
|
|
7
|
+
* 1. it starts with the panel's `basePath`
|
|
8
|
+
* 2. after stripping the prefix it ends with `/edit` or `/view`
|
|
9
|
+
* 3. there are at least three remaining segments (resource slug,
|
|
10
|
+
* record id, terminal token)
|
|
11
|
+
*
|
|
12
|
+
* The `resourceSlug` is the slash-joined chain of every segment up to
|
|
13
|
+
* the record id — this gives clustered resources (`${base}/blog/articles/123/edit`)
|
|
14
|
+
* and nested-relation edits (`${base}/articles/123/comments/456/edit`)
|
|
15
|
+
* distinct slugs so two URLs that target different records always
|
|
16
|
+
* produce different room names downstream.
|
|
17
|
+
*
|
|
18
|
+
* `/admin/articles/123/edit` → { slug: 'articles', id: '123', role: 'edit' }
|
|
19
|
+
* `/admin/articles/123/view` → { slug: 'articles', id: '123', role: 'view' }
|
|
20
|
+
* `/admin/blog/articles/123/edit` → { slug: 'blog/articles', id: '123', role: 'edit' }
|
|
21
|
+
* `/admin/articles/123/comments/456/edit` → { slug: 'articles/123/comments', id: '456', role: 'edit' }
|
|
22
|
+
* `/admin/articles/123/comments` → null (no trailing /edit or /view)
|
|
23
|
+
* `/admin/articles/123/comments/create` → null (terminal token isn't edit|view)
|
|
24
|
+
* `/site/articles/123/edit` → null (basePath mismatch when basePath='/admin')
|
|
25
|
+
*/
|
|
26
|
+
const RECOGNIZED_ROLES = ['edit', 'view'];
|
|
27
|
+
/**
|
|
28
|
+
* Parse a pilotiq URL into a `{ slug, id, role }` identity. Returns
|
|
29
|
+
* `null` for any URL that isn't a record-edit or record-view page.
|
|
30
|
+
*/
|
|
31
|
+
export function parseRecordPageUrl(currentPath, basePath) {
|
|
2
32
|
if (!currentPath)
|
|
3
33
|
return null;
|
|
4
34
|
// Normalise — trailing slashes on the URL or trailing slashes on
|
|
@@ -11,7 +41,8 @@ export function parseRecordEditUrl(currentPath, basePath) {
|
|
|
11
41
|
const parts = tail.split('/').filter(Boolean);
|
|
12
42
|
if (parts.length < 3)
|
|
13
43
|
return null;
|
|
14
|
-
|
|
44
|
+
const terminal = parts[parts.length - 1];
|
|
45
|
+
if (!RECOGNIZED_ROLES.includes(terminal))
|
|
15
46
|
return null;
|
|
16
47
|
const recordId = parts[parts.length - 2];
|
|
17
48
|
const slugParts = parts.slice(0, parts.length - 2);
|
|
@@ -20,6 +51,13 @@ export function parseRecordEditUrl(currentPath, basePath) {
|
|
|
20
51
|
return {
|
|
21
52
|
resourceSlug: slugParts.join('/'),
|
|
22
53
|
recordId,
|
|
54
|
+
role: terminal,
|
|
23
55
|
};
|
|
24
56
|
}
|
|
57
|
+
export function parseRecordEditUrl(currentPath, basePath) {
|
|
58
|
+
const identity = parseRecordPageUrl(currentPath, basePath);
|
|
59
|
+
if (!identity || identity.role !== 'edit')
|
|
60
|
+
return null;
|
|
61
|
+
return { resourceSlug: identity.resourceSlug, recordId: identity.recordId };
|
|
62
|
+
}
|
|
25
63
|
//# sourceMappingURL=parseRecordEditUrl.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parseRecordEditUrl.js","sourceRoot":"","sources":["../../src/react/parseRecordEditUrl.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"parseRecordEditUrl.js","sourceRoot":"","sources":["../../src/react/parseRecordEditUrl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAiBH,MAAM,gBAAgB,GAAkC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAExE;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,WAAmB,EACnB,QAAmB;IAEnB,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAA;IAC7B,iEAAiE;IACjE,2DAA2D;IAC3D,MAAM,WAAW,GAAG,WAAW,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IACnD,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IAEhD,IAAI,WAAW,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,WAAW,CAAC;QAAE,OAAO,IAAI,CAAA;IAE3E,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IACtE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAE7C,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IACjC,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAE,CAAA;IACzC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,QAA0B,CAAC;QAAE,OAAO,IAAI,CAAA;IAEvE,MAAM,QAAQ,GAAM,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAE,CAAA;IAC5C,MAAM,SAAS,GAAK,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IACpD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAEvC,OAAO;QACL,YAAY,EAAE,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;QACjC,QAAQ;QACR,IAAI,EAAU,QAA0B;KACzC,CAAA;AACH,CAAC;AAWD,MAAM,UAAU,kBAAkB,CAChC,WAAmB,EACnB,QAAmB;IAEnB,MAAM,QAAQ,GAAG,kBAAkB,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAA;IAC1D,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,IAAI,CAAA;IACtD,OAAO,EAAE,YAAY,EAAE,QAAQ,CAAC,YAAY,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,CAAA;AAC7E,CAAC"}
|
package/package.json
CHANGED
package/src/Pilotiq.ts
CHANGED
|
@@ -67,6 +67,44 @@ export interface PilotiqPlugin {
|
|
|
67
67
|
*/
|
|
68
68
|
export type UserResolver = (req: unknown) => unknown | null | Promise<unknown | null>
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Server-side hook handed to `Pilotiq.editPageHydrator(fn)`. The
|
|
72
|
+
* resource-edit data builder calls every registered hydrator after the
|
|
73
|
+
* standard fill pipeline (`loadRecord` → `mutateFormDataBeforeFill` →
|
|
74
|
+
* `fillFromRecord` → `mutateFormDataAfterFill` →
|
|
75
|
+
* `applyRelationshipRepeaterFill` → `applyRelationshipBuilderFill`) and
|
|
76
|
+
* merges each non-null return onto the form's default values.
|
|
77
|
+
*
|
|
78
|
+
* Multiple hydrators are walked in registration order; later hydrators
|
|
79
|
+
* override keys produced by earlier ones. A hydrator returning `null`
|
|
80
|
+
* (or throwing) is a pass-through — the DB row values survive intact.
|
|
81
|
+
*
|
|
82
|
+
* Designed for the SSR-from-Y.Doc consumer in `@pilotiq-pro/collab`:
|
|
83
|
+
* read persisted Y.Text + Y.Map values for the record and override the
|
|
84
|
+
* matching field defaults, killing the DB → Y.Doc value flicker on
|
|
85
|
+
* hydration. Other consumers (per-tenant overrides, A/B experiments,
|
|
86
|
+
* draft-snapshot resume) compose the same way.
|
|
87
|
+
*
|
|
88
|
+
* Pilotiq core stays Yjs-free — the hook's `Record<string, unknown>`
|
|
89
|
+
* return is opaque, so the collab consumer's Yjs imports stay confined
|
|
90
|
+
* to its own `/server` subpath.
|
|
91
|
+
*/
|
|
92
|
+
export interface EditPageHydratorContext {
|
|
93
|
+
/** The Resource class being edited (server-side reference, not the
|
|
94
|
+
* serialized meta — gives access to `model`, `getSlug()`, etc.). */
|
|
95
|
+
resource: ResourceClass
|
|
96
|
+
/** Record id from the URL, stringified. */
|
|
97
|
+
recordId: string
|
|
98
|
+
/** Values produced by the fill pipeline so far — DB row → hooks →
|
|
99
|
+
* relationship fills. Hydrators read this to make merge decisions
|
|
100
|
+
* (e.g. "only override fields that have content in Y.Doc"). */
|
|
101
|
+
currentValues: Record<string, unknown>
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type EditPageHydrator = (
|
|
105
|
+
ctx: EditPageHydratorContext,
|
|
106
|
+
) => Record<string, unknown> | null | Promise<Record<string, unknown> | null>
|
|
107
|
+
|
|
70
108
|
/**
|
|
71
109
|
* Upload configuration. Apps register an adapter via `Pilotiq.uploads({
|
|
72
110
|
* adapter })`; the `_uploads` route hands every incoming file to it.
|
|
@@ -203,6 +241,11 @@ export interface PilotiqConfig {
|
|
|
203
241
|
* separator + Sign-out entry. Without this, the menu shows custom
|
|
204
242
|
* items (if any) and the user identity, but no sign-out affordance. */
|
|
205
243
|
signOut?: SignOutConfig
|
|
244
|
+
/** Server-side hydrators applied after the fill pipeline on every
|
|
245
|
+
* resource edit page. Empty / unset → behaviour unchanged. See
|
|
246
|
+
* `EditPageHydrator` for the contract; plugins register via
|
|
247
|
+
* `panel.editPageHydrator(fn)`. */
|
|
248
|
+
editPageHydrators?: EditPageHydrator[]
|
|
206
249
|
/** Database notifications — opt-in. Mounts the bell + 4 endpoints
|
|
207
250
|
* (`_notifications` list / `:id/read` / `:id/unread` / `read-all`).
|
|
208
251
|
* Reads rows from `@rudderjs/notification`'s `notification` table via
|
|
@@ -521,6 +564,27 @@ export class Pilotiq {
|
|
|
521
564
|
return this
|
|
522
565
|
}
|
|
523
566
|
|
|
567
|
+
/**
|
|
568
|
+
* Register a server-side hydrator that runs after the fill pipeline
|
|
569
|
+
* on every resource edit page. Each registered hydrator's non-null
|
|
570
|
+
* return merges onto the form's default values; multiple registrations
|
|
571
|
+
* are walked in registration order (later wins on key conflicts).
|
|
572
|
+
*
|
|
573
|
+
* panel.editPageHydrator(async ({ resource, recordId }) => {
|
|
574
|
+
* // override defaults for this record from your alt source
|
|
575
|
+
* return { title: await draftStore.read(resource.getSlug(), recordId) }
|
|
576
|
+
* })
|
|
577
|
+
*
|
|
578
|
+
* Plugins typically register from inside their `register(panel)` hook.
|
|
579
|
+
* Throwing or returning `null` is a pass-through — the DB row values
|
|
580
|
+
* survive. See `EditPageHydrator` for the full contract.
|
|
581
|
+
*/
|
|
582
|
+
editPageHydrator(fn: EditPageHydrator): this {
|
|
583
|
+
if (!this.config.editPageHydrators) this.config.editPageHydrators = []
|
|
584
|
+
this.config.editPageHydrators.push(fn)
|
|
585
|
+
return this
|
|
586
|
+
}
|
|
587
|
+
|
|
524
588
|
/**
|
|
525
589
|
* Enable persistent database-backed notifications. Mounts the bell in
|
|
526
590
|
* the panel chrome + a small set of `_notifications` endpoints. The
|
package/src/Resource.test.ts
CHANGED
|
@@ -237,4 +237,48 @@ describe('Resource (static API)', () => {
|
|
|
237
237
|
assert.equal(await R.canRestore(null, { ownedBy: 'other' }), false)
|
|
238
238
|
})
|
|
239
239
|
})
|
|
240
|
+
|
|
241
|
+
describe('collab opt-in', () => {
|
|
242
|
+
it('omitted static collab → getResolvedCollabConfig returns null', () => {
|
|
243
|
+
class R extends Resource {}
|
|
244
|
+
assert.equal(R.getResolvedCollabConfig(), null)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('static collab = false → null (explicit opt-out)', () => {
|
|
248
|
+
class R extends Resource {
|
|
249
|
+
static override collab = false as const
|
|
250
|
+
}
|
|
251
|
+
assert.equal(R.getResolvedCollabConfig(), null)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('static collab = true → defaults to { pages: [edit], presence: true }', () => {
|
|
255
|
+
class R extends Resource {
|
|
256
|
+
static override collab = true as const
|
|
257
|
+
}
|
|
258
|
+
assert.deepEqual(R.getResolvedCollabConfig(), {
|
|
259
|
+
pages: ['edit'],
|
|
260
|
+
presence: true,
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('object form merges with defaults', () => {
|
|
265
|
+
class R extends Resource {
|
|
266
|
+
static override collab = { pages: ['edit', 'view'] as const }
|
|
267
|
+
}
|
|
268
|
+
assert.deepEqual(R.getResolvedCollabConfig(), {
|
|
269
|
+
pages: ['edit', 'view'],
|
|
270
|
+
presence: true, // default preserved
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('object form can suppress presence', () => {
|
|
275
|
+
class R extends Resource {
|
|
276
|
+
static override collab = { presence: false }
|
|
277
|
+
}
|
|
278
|
+
assert.deepEqual(R.getResolvedCollabConfig(), {
|
|
279
|
+
pages: ['edit'], // default preserved
|
|
280
|
+
presence: false,
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
})
|
|
240
284
|
})
|
package/src/Resource.ts
CHANGED
|
@@ -50,6 +50,33 @@ export type NavigationBadgeColor =
|
|
|
50
50
|
export type NavigationBadgeHandler =
|
|
51
51
|
() => string | number | undefined | Promise<string | number | undefined>
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Per-resource collab configuration. Set via `static collab = true` (the
|
|
55
|
+
* 90% case — opts the edit page in with presence) or via the object form
|
|
56
|
+
* for finer control. Omitting `static collab` entirely keeps the resource
|
|
57
|
+
* collab-free even when the `@pilotiq-pro/collab` plugin is registered.
|
|
58
|
+
*
|
|
59
|
+
* pages — page roles where collab activates. `'edit'` syncs values +
|
|
60
|
+
* presence; `'view'` is presence-only (the page is read-only,
|
|
61
|
+
* value-sync would be moot). Defaults to `['edit']`.
|
|
62
|
+
* presence — when false, suppress the awareness layer (focus chips,
|
|
63
|
+
* cursor positions) while keeping value-sync. Defaults to true.
|
|
64
|
+
*
|
|
65
|
+
* Field-level `.collab(false)` always wins over this setting — opting the
|
|
66
|
+
* resource in then opting individual fields out is the supported shape.
|
|
67
|
+
*/
|
|
68
|
+
export interface ResourceCollabConfig {
|
|
69
|
+
pages: ReadonlyArray<'edit' | 'view'>
|
|
70
|
+
presence: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Raw shape accepted by `static collab` before normalization. `true` is a
|
|
74
|
+
* shorthand for `{ pages: ['edit'], presence: true }`. */
|
|
75
|
+
export type ResourceCollabInput = boolean | {
|
|
76
|
+
pages?: ReadonlyArray<'edit' | 'view'>
|
|
77
|
+
presence?: boolean
|
|
78
|
+
}
|
|
79
|
+
|
|
53
80
|
/**
|
|
54
81
|
* Abstract Resource base class. **All methods are static** — resources are
|
|
55
82
|
* registered by class, not by instance. Routes look up the class and call
|
|
@@ -297,6 +324,37 @@ export abstract class Resource {
|
|
|
297
324
|
return undefined
|
|
298
325
|
}
|
|
299
326
|
|
|
327
|
+
// ─── Realtime collab opt-in ────────────────────────────────
|
|
328
|
+
// Opt-in per resource. The `@pilotiq-pro/collab` plugin registers the
|
|
329
|
+
// global singletons (transport, Tiptap extension factory, RecordWrapper
|
|
330
|
+
// factory); resources with `static collab` unset stay collab-free even
|
|
331
|
+
// when the plugin is installed. Field-level `.collab(false)` overrides
|
|
332
|
+
// this setting per field.
|
|
333
|
+
|
|
334
|
+
/** Enable collab on this resource. `true` is the 90% case — shorthand
|
|
335
|
+
* for `{ pages: ['edit'], presence: true }`. Use the object form to
|
|
336
|
+
* narrow which page roles activate or to suppress presence. Default
|
|
337
|
+
* `false` (collab off — the WS room is never opened for this resource). */
|
|
338
|
+
static collab: ResourceCollabInput = false
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Normalize `static collab` into the canonical wire shape, or return
|
|
342
|
+
* `null` when the resource has opted out. Centralizes the `true` →
|
|
343
|
+
* defaults expansion and the `{ pages?: … }` merge.
|
|
344
|
+
*
|
|
345
|
+
* Result lands on `panelInfo().recordCollab[slug]`; the gate reads it
|
|
346
|
+
* to decide whether to mount the record wrapper.
|
|
347
|
+
*/
|
|
348
|
+
static getResolvedCollabConfig(): ResourceCollabConfig | null {
|
|
349
|
+
const raw = this.collab
|
|
350
|
+
if (raw === false || raw == null) return null
|
|
351
|
+
if (raw === true) return { pages: ['edit'], presence: true }
|
|
352
|
+
return {
|
|
353
|
+
pages: raw.pages ?? ['edit'],
|
|
354
|
+
presence: raw.presence ?? true,
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
300
358
|
// ─── Plan #10: authorization predicates ────────────────────
|
|
301
359
|
// All async, all default `true`. Routes call them with the resolved
|
|
302
360
|
// user (from `Pilotiq.user(fn)`); the renderer threads the same user
|
package/src/index.ts
CHANGED
package/src/pageData/helpers.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Pilotiq,
|
|
3
|
+
PilotiqConfig,
|
|
4
|
+
EditPageHydrator,
|
|
5
|
+
EditPageHydratorContext,
|
|
6
|
+
} from '../Pilotiq.js'
|
|
2
7
|
import type { Page } from '../Page.js'
|
|
3
8
|
import { Element } from '../schema/Element.js'
|
|
4
9
|
import { Field } from '../fields/Field.js'
|
|
@@ -386,6 +391,40 @@ export async function applyFillPipeline<R>(
|
|
|
386
391
|
return values
|
|
387
392
|
}
|
|
388
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Walk every hydrator registered via `Pilotiq.editPageHydrator(fn)` and
|
|
396
|
+
* merge non-null returns onto `currentValues`. Hydrators run sequentially
|
|
397
|
+
* in registration order; later returns override keys from earlier ones.
|
|
398
|
+
*
|
|
399
|
+
* Failure mode is permissive: a hydrator that throws or returns `null`
|
|
400
|
+
* contributes nothing; the page still renders against the fill-pipeline
|
|
401
|
+
* values it received. Errors emit a `console.warn` so the page isn't
|
|
402
|
+
* silently relying on missing data.
|
|
403
|
+
*
|
|
404
|
+
* Returns the merged overlay (NOT the final values). The caller composes
|
|
405
|
+
* the overlay onto the form via `form.withValues({ ...current, ...overlay })`
|
|
406
|
+
* — keeps the merge order explicit at the call site (overlay wins).
|
|
407
|
+
*
|
|
408
|
+
* Empty hydrators array / no overrides → returns `{}` so callers can
|
|
409
|
+
* skip the `withValues` call cheaply.
|
|
410
|
+
*/
|
|
411
|
+
export async function applyEditPageHydrators(
|
|
412
|
+
hydrators: ReadonlyArray<EditPageHydrator>,
|
|
413
|
+
ctx: EditPageHydratorContext,
|
|
414
|
+
): Promise<Record<string, unknown>> {
|
|
415
|
+
if (hydrators.length === 0) return {}
|
|
416
|
+
let overlay: Record<string, unknown> = {}
|
|
417
|
+
for (const fn of hydrators) {
|
|
418
|
+
try {
|
|
419
|
+
const result = await fn(ctx)
|
|
420
|
+
if (result && typeof result === 'object') overlay = { ...overlay, ...result }
|
|
421
|
+
} catch (err) {
|
|
422
|
+
console.warn('[pilotiq] editPageHydrator threw — falling back to fill-pipeline values', err)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return overlay
|
|
426
|
+
}
|
|
427
|
+
|
|
389
428
|
/**
|
|
390
429
|
* Walk the form's top-level Repeaters and replace `values[fieldName]`
|
|
391
430
|
* with rows fetched from `parent.related(name)` for any
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Pilotiq, PilotiqConfig } from '../Pilotiq.js'
|
|
2
2
|
import type { Page } from '../Page.js'
|
|
3
|
-
import type { ResourceClass, NavigationBadgeColor } from '../Resource.js'
|
|
3
|
+
import type { ResourceClass, NavigationBadgeColor, ResourceCollabConfig } from '../Resource.js'
|
|
4
4
|
import type { GlobalClass } from '../Global.js'
|
|
5
5
|
import type { ClusterClass } from '../Cluster.js'
|
|
6
6
|
import { resourceBasePath, globalBasePath, pageBasePath } from '../clusterPaths.js'
|
|
@@ -194,6 +194,35 @@ export interface PanelInfoRoute {
|
|
|
194
194
|
url?: string
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Per-resource collab opt-in map, keyed by the URL slug
|
|
199
|
+
* `parseRecordPageUrl` produces. Non-clustered resource → `getSlug()`;
|
|
200
|
+
* clustered resource → `${cluster.getSlug()}/${R.getSlug()}`.
|
|
201
|
+
*
|
|
202
|
+
* `RecordWrapperGate` reads this map to decide whether the page tree
|
|
203
|
+
* needs the plugin-registered RecordWrapper (collab room, audit, …)
|
|
204
|
+
* mounted around the record view/edit content area.
|
|
205
|
+
*
|
|
206
|
+
* Nested-relation edit URLs (`/articles/123/comments/456/edit`) have a
|
|
207
|
+
* dynamic-id segment in the gate's URL slug and don't match here in v1.
|
|
208
|
+
* Collab on nested-relation edits is a follow-up — top-level resource
|
|
209
|
+
* edits are the common case and ship now.
|
|
210
|
+
*/
|
|
211
|
+
export type RecordCollabMap = Record<string, ResourceCollabConfig>
|
|
212
|
+
|
|
213
|
+
function resourceSlugForGate(R: ResourceClass): string {
|
|
214
|
+
return R.cluster ? `${R.cluster.getSlug()}/${R.getSlug()}` : R.getSlug()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildRecordCollabMap(cfg: Readonly<PilotiqConfig>): RecordCollabMap | undefined {
|
|
218
|
+
const map: RecordCollabMap = {}
|
|
219
|
+
for (const R of cfg.resources) {
|
|
220
|
+
const collab = R.getResolvedCollabConfig()
|
|
221
|
+
if (collab) map[resourceSlugForGate(R)] = collab
|
|
222
|
+
}
|
|
223
|
+
return Object.keys(map).length > 0 ? map : undefined
|
|
224
|
+
}
|
|
225
|
+
|
|
197
226
|
export async function panelInfo(
|
|
198
227
|
pilotiq: Pilotiq,
|
|
199
228
|
req?: unknown,
|
|
@@ -210,6 +239,7 @@ export async function panelInfo(
|
|
|
210
239
|
buildRightSidebarMeta(cfg, user),
|
|
211
240
|
])
|
|
212
241
|
const databaseNotifications = buildDatabaseNotificationsMeta(cfg, user)
|
|
242
|
+
const recordCollab = buildRecordCollabMap(cfg)
|
|
213
243
|
// AI suggestion mode — sparse: omit when 'auto' (the default) so the
|
|
214
244
|
// wire shape stays minimal for panels that don't opt into review mode.
|
|
215
245
|
// Plugin clients (e.g. @pilotiq-pro/ai's `AiClientToolBindings`) read
|
|
@@ -225,6 +255,7 @@ export async function panelInfo(
|
|
|
225
255
|
...(userMenu ? { userMenu } : {}),
|
|
226
256
|
...(databaseNotifications ? { databaseNotifications } : {}),
|
|
227
257
|
...(rightSidebar ? { rightSidebar } : {}),
|
|
258
|
+
...(recordCollab ? { recordCollab } : {}),
|
|
228
259
|
...(Object.keys(renderHooks).length > 0 ? { renderHooks } : {}),
|
|
229
260
|
...(aiSuggestionsMode !== 'auto' ? { aiSuggestionsMode } : {}),
|
|
230
261
|
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
resourceViewBreadcrumbs,
|
|
26
26
|
} from './breadcrumbs.js'
|
|
27
27
|
import {
|
|
28
|
+
applyEditPageHydrators,
|
|
28
29
|
applyFillPipeline,
|
|
29
30
|
applyRelationshipBuilderFill,
|
|
30
31
|
applyRelationshipRepeaterFill,
|
|
@@ -400,7 +401,22 @@ export async function resourceEditData(
|
|
|
400
401
|
const values = await applyFillPipeline(form, record)
|
|
401
402
|
const withRelations = await applyRelationshipRepeaterFill(form, values, record, R.model)
|
|
402
403
|
const withBuilders = await applyRelationshipBuilderFill(form, withRelations, record, R.model)
|
|
403
|
-
|
|
404
|
+
// Hydrators run AFTER the standard fill pipeline so they overlay
|
|
405
|
+
// on top of DB-row + relationship-row values. Skipped on the
|
|
406
|
+
// prefill branch (validation-error round-trip) — overlaying there
|
|
407
|
+
// would clobber the user's just-submitted input that the page is
|
|
408
|
+
// re-displaying for them to fix.
|
|
409
|
+
const hydrators = cfg.editPageHydrators ?? []
|
|
410
|
+
const overlay = hydrators.length > 0
|
|
411
|
+
? await applyEditPageHydrators(hydrators, {
|
|
412
|
+
resource: R,
|
|
413
|
+
recordId,
|
|
414
|
+
currentValues: withBuilders,
|
|
415
|
+
})
|
|
416
|
+
: {}
|
|
417
|
+
form.withValues(Object.keys(overlay).length > 0
|
|
418
|
+
? { ...withBuilders, ...overlay }
|
|
419
|
+
: withBuilders)
|
|
404
420
|
} else if (prefill?.values) {
|
|
405
421
|
form.withValues(prefill.values)
|
|
406
422
|
}
|