@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.
Files changed (83) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/CHANGELOG.md +213 -0
  3. package/dist/Pilotiq.d.ts +55 -0
  4. package/dist/Pilotiq.d.ts.map +1 -1
  5. package/dist/Pilotiq.js +21 -0
  6. package/dist/Pilotiq.js.map +1 -1
  7. package/dist/Resource.d.ts +39 -0
  8. package/dist/Resource.d.ts.map +1 -1
  9. package/dist/Resource.js +30 -0
  10. package/dist/Resource.js.map +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/pageData/helpers.d.ts +19 -1
  15. package/dist/pageData/helpers.d.ts.map +1 -1
  16. package/dist/pageData/helpers.js +33 -0
  17. package/dist/pageData/helpers.js.map +1 -1
  18. package/dist/pageData/navigation.d.ts +17 -1
  19. package/dist/pageData/navigation.d.ts.map +1 -1
  20. package/dist/pageData/navigation.js +14 -0
  21. package/dist/pageData/navigation.js.map +1 -1
  22. package/dist/pageData/resourcePages.d.ts.map +1 -1
  23. package/dist/pageData/resourcePages.js +17 -2
  24. package/dist/pageData/resourcePages.js.map +1 -1
  25. package/dist/pageData.d.ts +1 -1
  26. package/dist/pageData.d.ts.map +1 -1
  27. package/dist/pageData.js +1 -1
  28. package/dist/pageData.js.map +1 -1
  29. package/dist/react/AppShell.d.ts +5 -0
  30. package/dist/react/AppShell.d.ts.map +1 -1
  31. package/dist/react/AppShell.js +1 -1
  32. package/dist/react/AppShell.js.map +1 -1
  33. package/dist/react/FormCollabBindingRegistry.d.ts +71 -1
  34. package/dist/react/FormCollabBindingRegistry.d.ts.map +1 -1
  35. package/dist/react/FormCollabBindingRegistry.js.map +1 -1
  36. package/dist/react/FormStateContext.d.ts +17 -0
  37. package/dist/react/FormStateContext.d.ts.map +1 -1
  38. package/dist/react/FormStateContext.js +44 -3
  39. package/dist/react/FormStateContext.js.map +1 -1
  40. package/dist/react/RecordWrapperGate.d.ts +19 -6
  41. package/dist/react/RecordWrapperGate.d.ts.map +1 -1
  42. package/dist/react/RecordWrapperGate.js +18 -8
  43. package/dist/react/RecordWrapperGate.js.map +1 -1
  44. package/dist/react/fields/MarkdownInput.d.ts.map +1 -1
  45. package/dist/react/fields/MarkdownInput.js +105 -3
  46. package/dist/react/fields/MarkdownInput.js.map +1 -1
  47. package/dist/react/fields/TextLikeInput.d.ts +10 -0
  48. package/dist/react/fields/TextLikeInput.d.ts.map +1 -1
  49. package/dist/react/fields/TextLikeInput.js +179 -0
  50. package/dist/react/fields/TextLikeInput.js.map +1 -1
  51. package/dist/react/fields/textDelta.d.ts +44 -0
  52. package/dist/react/fields/textDelta.d.ts.map +1 -0
  53. package/dist/react/fields/textDelta.js +80 -0
  54. package/dist/react/fields/textDelta.js.map +1 -0
  55. package/dist/react/index.d.ts +2 -2
  56. package/dist/react/index.d.ts.map +1 -1
  57. package/dist/react/index.js +1 -1
  58. package/dist/react/index.js.map +1 -1
  59. package/dist/react/parseRecordEditUrl.d.ts +33 -9
  60. package/dist/react/parseRecordEditUrl.d.ts.map +1 -1
  61. package/dist/react/parseRecordEditUrl.js +40 -2
  62. package/dist/react/parseRecordEditUrl.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/Pilotiq.ts +64 -0
  65. package/src/Resource.test.ts +44 -0
  66. package/src/Resource.ts +58 -0
  67. package/src/index.ts +2 -0
  68. package/src/pageData/helpers.ts +40 -1
  69. package/src/pageData/navigation.ts +32 -1
  70. package/src/pageData/resourcePages.ts +17 -1
  71. package/src/pageData.test.ts +137 -0
  72. package/src/pageData.ts +1 -0
  73. package/src/react/AppShell.tsx +6 -1
  74. package/src/react/FormCollabBindingRegistry.ts +63 -1
  75. package/src/react/FormStateContext.tsx +62 -3
  76. package/src/react/RecordWrapperGate.tsx +26 -8
  77. package/src/react/fields/MarkdownInput.tsx +100 -3
  78. package/src/react/fields/TextLikeInput.tsx +203 -1
  79. package/src/react/fields/textDelta.test.ts +141 -0
  80. package/src/react/fields/textDelta.ts +86 -0
  81. package/src/react/index.ts +9 -1
  82. package/src/react/parseRecordEditUrl.test.ts +48 -1
  83. package/src/react/parseRecordEditUrl.ts +52 -13
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
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
- * Parses a pilotiq URL into a record-edit identity, or returns `null`
3
- * for any URL that isn't a record-bound edit page.
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, `edit`)
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` → { resourceSlug: 'articles', recordId: '123' }
18
- * `/admin/blog/articles/123/edit` → { resourceSlug: 'blog/articles', recordId: '123' }
19
- * `/admin/articles/123/comments/456/edit` → { resourceSlug: 'articles/123/comments', recordId: '456' }
20
- * `/admin/articles/123/comments` null (no trailing /edit)
21
- * `/admin/articles/123/comments/create` → null (no record id)
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;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,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,CAuB3B"}
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
- export function parseRecordEditUrl(currentPath, basePath) {
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
- if (parts[parts.length - 1] !== 'edit')
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":"AA4BA,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,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM;QAAE,OAAO,IAAI,CAAA;IAEnD,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;KACT,CAAA;AACH,CAAC"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pilotiq/pilotiq",
3
- "version": "0.8.2",
3
+ "version": "0.10.0",
4
4
  "description": "View-based admin panel for RudderJS",
5
5
  "license": "MIT",
6
6
  "repository": {
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
@@ -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
@@ -6,6 +6,8 @@ export {
6
6
  type UserResolver,
7
7
  type UploadConfig,
8
8
  type SignOutConfig,
9
+ type EditPageHydrator,
10
+ type EditPageHydratorContext,
9
11
  } from './Pilotiq.js'
10
12
  export {
11
13
  UserMenuItem,
@@ -1,4 +1,9 @@
1
- import type { Pilotiq, PilotiqConfig } from '../Pilotiq.js'
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
- form.withValues(withBuilders)
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
  }