pict-section-recordset 1.21.0 → 1.22.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pict-section-recordset",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "description": "Pict dynamic record set management views",
5
5
  "main": "source/Pict-Section-RecordSet.js",
6
6
  "files": [
@@ -186,6 +186,43 @@ class RecordSetMetacontroller extends libFableServiceProviderBase
186
186
  return this.recordSetProviderConfigurations[pRecordSet];
187
187
  }
188
188
 
189
+ /**
190
+ * Resolve a column name to the record-set it references, so a form can mount an entity Picker and the
191
+ * read view can resolve the name. Two sources, in order:
192
+ *
193
+ * 1. The record-set config's explicit `ForeignKeyEntities` map ({ ColumnName: EntityName }). This is
194
+ * how non-conventional references are declared — prefixed/suffixed columns (ParentIDSample,
195
+ * ItemIDTestSpecificationSet, LinkedIDUser, IDMaterialParent, …) and any override. Mapping a
196
+ * column to a falsy value explicitly opts it OUT (keeps it a plain input).
197
+ * 2. The plain `ID<Entity>` convention — a column named exactly `ID` + an entity that is a
198
+ * registered record-set (IDOrganization → Organization). No prefix heuristics; anything that
199
+ * isn't a clean `ID<Entity>` must be declared in the map above.
200
+ *
201
+ * Returns false for the row's own id, IDCustomer, non-references, and references this app doesn't
202
+ * manage as a record-set (which then stay plain inputs).
203
+ *
204
+ * @param {string} pFieldName - the schema column name.
205
+ * @param {Record<string, any>} [pRecordSetConfiguration] - the owning record-set's configuration.
206
+ * @param {string} [pOwnIDField] - the row's own id field, excluded (e.g. 'IDTestSpecification').
207
+ * @return {string|false} the referenced record-set name, or false.
208
+ */
209
+ resolveForeignEntity(pFieldName, pRecordSetConfiguration, pOwnIDField)
210
+ {
211
+ if (!pFieldName || (typeof pFieldName !== 'string')) { return false; }
212
+ // 1. Explicit per-record-set mapping wins (declares exceptions; a falsy value opts a column out).
213
+ const tmpMap = pRecordSetConfiguration && pRecordSetConfiguration.ForeignKeyEntities;
214
+ if (tmpMap && Object.prototype.hasOwnProperty.call(tmpMap, pFieldName))
215
+ {
216
+ return tmpMap[pFieldName] || false;
217
+ }
218
+ // 2. Plain ID<Entity> convention, validated against the registered record-sets.
219
+ if ((pFieldName === pOwnIDField) || (pFieldName === 'IDCustomer')) { return false; }
220
+ if (!pFieldName.startsWith('ID')) { return false; }
221
+ const tmpRemote = pFieldName.slice(2);
222
+ if (!tmpRemote) { return false; }
223
+ return this.recordSetProviderConfigurations[tmpRemote] ? tmpRemote : false;
224
+ }
225
+
189
226
  /**
190
227
  * @param {Record<string, any>} pRecordSetConfiguration - The RecordSet configuration to load.
191
228
  */
@@ -210,14 +210,14 @@ class viewRecordSetCreate extends libPictRecordSetRecordView
210
210
  let rowCounter = 1;
211
211
  for (const p of Object.keys(tmpRecordCreateData.RecordSchema.properties))
212
212
  {
213
- const exclusionSet = [this.pict.providers[this.providerHash].getIDField(), this.pict.providers[this.providerHash].getGUIDField(), 'CreatingIDUser', 'UpdatingIDUser', 'DeletingIDUser', 'Deleted', 'CreateDate', 'UpdateDate', 'DeleteDate', 'Deleted'];
213
+ const exclusionSet = [this.pict.providers[this.providerHash].getIDField(), this.pict.providers[this.providerHash].getGUIDField(), 'CreatingIDUser', 'UpdatingIDUser', 'DeletingIDUser', 'Deleted', 'CreateDate', 'UpdateDate', 'DeleteDate', 'Deleted', 'IDCustomer', 'ExternalSyncDate', 'ExternalSyncGUID']; // IDCustomer: meadow-endpoints/retold tenancy discriminator — server-managed, never settable via the API
214
214
  if (exclusionSet.includes(p))
215
215
  {
216
216
  continue;
217
217
  }
218
218
  const tmpDescriptor =
219
219
  {
220
- "Name": `${ this.pict.providers[pProviderHash].getHumanReadableFieldName?.() || p }`,
220
+ "Name": `${ this.pict.providers[pProviderHash].getHumanReadableFieldName?.(p) || p }`,
221
221
  "Hash": `${ tmpRecordCreateData.RecordSet }-${ p }`,
222
222
  "DataType": "String",
223
223
  "PictForm":
@@ -256,6 +256,17 @@ class viewRecordSetCreate extends libPictRecordSetRecordView
256
256
  tmpDescriptor.DataType = 'String';
257
257
  }
258
258
 
259
+ const tmpForeignEntity = this.pict.PictSectionRecordSet.resolveForeignEntity(p, pRecordConfiguration, this.pict.providers[this.providerHash].getIDField());
260
+ if (tmpForeignEntity && this.pict.providers['Pict-Section-Picker'])
261
+ {
262
+ // Foreign-key column → searchable entity Picker (edit) + resolved name (read).
263
+ tmpDescriptor.PictForm.InputType = 'Picker';
264
+ tmpDescriptor.PictForm.Entity = tmpForeignEntity;
265
+ const tmpForeignConfig = this.pict.PictSectionRecordSet.recordSetProviderConfigurations[tmpForeignEntity];
266
+ if (tmpForeignConfig && tmpForeignConfig.SearchFields) { tmpDescriptor.PictForm.SearchFields = tmpForeignConfig.SearchFields; }
267
+ }
268
+ // Per-record-set label override (e.g. Contract/Project IDOrganization → "Prime Contractor").
269
+ if (pRecordConfiguration.FieldLabels && pRecordConfiguration.FieldLabels[p]) { tmpDescriptor.Name = pRecordConfiguration.FieldLabels[p]; }
259
270
  this.defaultManifest.Descriptors[`${ tmpRecordCreateData.RecordSet }Details.${ p }`] = tmpDescriptor;
260
271
  }
261
272
  this.pict.TemplateProvider.addTemplate(`PRSP-Create-RecordCreate-Template`, /*html*/`
@@ -218,6 +218,8 @@ class viewRecordSetDashboard extends libPictRecordSetRecordView
218
218
  'DeletingIDUser',
219
219
  'UpdateDate',
220
220
  'UpdatingIDUser',
221
+ 'IDCustomer', // meadow-endpoints/retold tenancy discriminator — server-managed, hidden from every record view
222
+ 'ExternalSyncDate', 'ExternalSyncGUID', // external-sync auditing stamps (Headlight integration sync)
221
223
  ];
222
224
 
223
225
  const tmpSchema = pRecordListData.RecordSchema;
@@ -34,8 +34,16 @@ const _DEFAULT_CONFIGURATION_List_Title =
34
34
  <header id="PRSP_Title_Container">
35
35
  <h1 id="PRSP_Title">{~D:Record.Title~}</h1>
36
36
  <h2 id="PRSP_Subtitle">{~D:Record.Subtitle~}</h2>
37
+ {~TIfAbs:PRSP-List-Title-CreateButton-Template:Record:Record.RecordSetConfiguration.RecordSetListShowCreateButton^TRUE^~}
37
38
  </header>
38
39
  <!-- DefaultPackage end view template: [PRSP-List-Title-Template] -->
40
+ `
41
+ },
42
+ {
43
+ Hash: 'PRSP-List-Title-CreateButton-Template',
44
+ Template: /*html*/`
45
+ <!-- Optional list "New" action; opt in per record set via RecordSetConfiguration.RecordSetListShowCreateButton. -->
46
+ <button type="button" class="prsp-list-title-create" title="Create a new record" onclick="_Pict.views['RSP-RecordSet-List'].createNew('{~D:Record.RecordSet~}')">{~I:Plus~} New</button>
39
47
  `
40
48
  },
41
49
  ],
@@ -189,6 +189,26 @@ class viewRecordSetList extends libPictRecordSetRecordView
189
189
  return true;
190
190
  }
191
191
 
192
+ /**
193
+ * Navigate to the Create form for a record set. Backs the optional list title-bar "New" button
194
+ * (opt in per record set via RecordSetConfiguration.RecordSetListShowCreateButton). Routes through
195
+ * the section router so it works in both hash and memory router modes; falls back to the active
196
+ * route's record set when called without an argument.
197
+ * @param {string} [pRecordSet] - The record set to create a new record in.
198
+ * @return {boolean} True when navigation was issued.
199
+ */
200
+ createNew(pRecordSet)
201
+ {
202
+ const tmpRecordSet = pRecordSet || this.fable?.providers?.RecordSetRouter?.pictRouter?.router?.current?.[0]?.data?.RecordSet;
203
+ if (!tmpRecordSet)
204
+ {
205
+ this.log.warn(`RecordSetList: createNew called but no record set could be resolved.`);
206
+ return false;
207
+ }
208
+ this.fable.providers.RecordSetRouter.pictRouter.navigate(`/PSRS/${ tmpRecordSet }/Create`);
209
+ return true;
210
+ }
211
+
192
212
  onBeforeRenderList(pRecordListData)
193
213
  {
194
214
  return pRecordListData;
@@ -269,6 +289,8 @@ class viewRecordSetList extends libPictRecordSetRecordView
269
289
  'DeletingIDUser',
270
290
  'UpdateDate',
271
291
  'UpdatingIDUser',
292
+ 'IDCustomer', // meadow-endpoints/retold tenancy discriminator — server-managed, hidden from every record view
293
+ 'ExternalSyncDate', 'ExternalSyncGUID', // external-sync auditing stamps (Headlight integration sync)
272
294
  ];
273
295
  }
274
296
 
@@ -5,7 +5,9 @@ const libViewAssociationEditor = require('../associate/RecordSet-AssociationEdit
5
5
  // surfaced through the record audit header (the first-class activity line + the Details
6
6
  // modal), not inline in the record body. The entity's own ID/GUID field names are added
7
7
  // at suppression time via the provider's getIDField()/getGUIDField().
8
- const _AUDIT_FIELD_NAMES = ['CreatingIDUser', 'UpdatingIDUser', 'DeletingIDUser', 'Deleted', 'CreateDate', 'UpdateDate', 'DeleteDate'];
8
+ // Audit / sync-bookkeeping fields suppressed from the record body and surfaced in the Details modal.
9
+ // ExternalSyncDate / ExternalSyncGUID are the external-sync auditing stamps (Headlight integration sync).
10
+ const _AUDIT_FIELD_NAMES = ['CreatingIDUser', 'UpdatingIDUser', 'DeletingIDUser', 'Deleted', 'CreateDate', 'UpdateDate', 'DeleteDate', 'ExternalSyncDate', 'ExternalSyncGUID'];
9
11
 
10
12
  /** @type {Record<string, any>} */
11
13
  const _DEFAULT_CONFIGURATION__Read = (
@@ -43,9 +45,10 @@ const _DEFAULT_CONFIGURATION__Read = (
43
45
  .prsp-audit-button:hover { border-color: var(--theme-color-brand-primary, #156dd1); color: var(--theme-color-brand-primary, #156dd1); }
44
46
  .prsp-record-related:empty { display: none; }
45
47
  .prsp-audit-anchor { position: relative; }
46
- .prsp-audit-popover { position: absolute; top: calc(100% + 8px); right: 0; min-width: 320px; max-width: 90vw; background: var(--theme-color-background-panel, #fff); border: 1px solid var(--theme-color-border-default, #d7dce3); border-radius: 10px; box-shadow: 0 14px 36px rgba(15, 23, 42, 0.18); padding: 1rem 1.1rem; z-index: 40; display: none; }
48
+ .prsp-audit-popover { position: absolute; top: calc(100% + 8px); right: 0; min-width: 380px; width: max-content; max-width: 90vw; background: var(--theme-color-background-panel, #fff); border: 1px solid var(--theme-color-border-default, #d7dce3); border-radius: 10px; box-shadow: 0 14px 36px rgba(15, 23, 42, 0.18); padding: 1rem 1.1rem; z-index: 40; display: none; }
47
49
  .prsp-audit-popover.is-open { display: block; }
48
- .prsp-audit-dl { display: grid; grid-template-columns: auto 1fr; gap: 0.6rem 1rem; align-items: baseline; margin: 0; }
50
+ .prsp-audit-dl { display: grid; grid-template-columns: auto auto; gap: 0.6rem 1rem; align-items: baseline; margin: 0; }
51
+ .prsp-audit-dl dd { white-space: nowrap; }
49
52
  .prsp-audit-dl dt { color: var(--theme-color-text-muted, #6b7686); font-size: 0.74rem; text-transform: uppercase; letter-spacing: 0.03em; white-space: nowrap; }
50
53
  .prsp-audit-dl dd { margin: 0; color: var(--theme-color-text-primary, #1f2733); font-size: 0.9rem; }
51
54
  .prsp-audit-dl dd small { color: var(--theme-color-text-muted, #6b7686); }
@@ -59,6 +62,18 @@ const _DEFAULT_CONFIGURATION__Read = (
59
62
  .prsp-record-read input.input[readonly], .prsp-record-read input.input[disabled], .prsp-record-read textarea[readonly], .prsp-record-read textarea[disabled], .prsp-record-read select[disabled] { background: transparent !important; border-color: transparent !important; box-shadow: none !important; padding-left: 0 !important; padding-right: 0 !important; height: auto !important; min-height: 0 !important; opacity: 1 !important; cursor: default !important; color: var(--theme-color-text-primary, #1f2733) !important; -webkit-text-fill-color: var(--theme-color-text-primary, #1f2733) !important; font-weight: 600 !important; font-size: 1.1rem !important; }
60
63
  .prsp-record-read .label { font-size: 0.68rem !important; font-weight: 600 !important; text-transform: uppercase !important; letter-spacing: 0.05em !important; color: var(--theme-color-text-muted, #6b7686) !important; margin-bottom: 0.05rem !important; }
61
64
  .prsp-record-read .section-header { background: var(--theme-color-background-selected, #e3edfb) !important; color: var(--theme-color-brand-primary, #156dd1) !important; font-size: 0.74rem !important; font-weight: 700 !important; text-transform: uppercase !important; letter-spacing: 0.06em !important; padding: 0.4rem 0.8rem !important; border-radius: 6px !important; border-bottom: 0 !important; margin: 1.1rem 0 0.7rem !important; }
65
+ /* Record header — a readable title (the record's own name, via a heuristic) with the entity
66
+ type as an eyebrow, instead of a raw "RecordSet GUIDField [guid]" string. */
67
+ .prsp-read-head { margin: 0 0 1.1rem; }
68
+ .prsp-read-eyebrow { font-size: 0.72rem; font-weight: 650; text-transform: uppercase; letter-spacing: 0.06em; color: var(--theme-color-text-muted, #6b7686); }
69
+ .prsp-read-title { font-size: 1.5rem; font-weight: 700; line-height: 1.2; margin: 0.1rem 0 0; color: var(--theme-color-text-primary, #1f2733); }
70
+ /* Read-view association tabs — pill nav, shared by the Tab and Split layouts (single source;
71
+ layouts style only their own nav container). */
72
+ .psrs-tab { padding: 0.4rem 0.85rem; border: 1px solid var(--theme-color-border-default, #d7dce3); border-radius: 8px; cursor: pointer; font-size: 0.88rem; color: var(--theme-color-text-secondary, #45505f); background: var(--theme-color-background-panel, #fff); user-select: none; transition: all 0.15s ease; }
73
+ .psrs-tab:hover { background: var(--theme-color-background-tertiary, #eceef2); color: var(--theme-color-text-primary, #1f2733); }
74
+ .psrs-tab.is-active { border-color: var(--theme-color-brand-primary, #156dd1); background: var(--theme-color-background-selected, #e3edfb); color: var(--theme-color-brand-primary, #156dd1); font-weight: 600; }
75
+ .psrs-tab-body { display: none; }
76
+ .psrs-tab-body.is-active { display: block; }
62
77
  `,
63
78
  CSSPriority: 500,
64
79
 
@@ -119,11 +134,22 @@ const _DEFAULT_CONFIGURATION__Read = (
119
134
  Hash: 'PRSP-Read-RecordTabNav-Template',
120
135
  Template: /*html*/`<!-- Placeholder for tabs, something has gone wrong if this comment is rendered. -->`
121
136
  },
137
+ {
138
+ // Readable record header, shared by all three layouts: the record's display name (heuristic)
139
+ // over a small entity-type eyebrow, falling back to the GUID field + value when the record
140
+ // has no obvious name. DisplayTitle / TitleEyebrow are computed in renderRead().
141
+ Hash: 'PRSP-Read-Header-Template',
142
+ Template: /*html*/`
143
+ <div class="prsp-read-head">
144
+ <div class="prsp-read-eyebrow">{~D:Record.TitleEyebrow~}</div>
145
+ <h1 class="prsp-read-title">{~D:Record.DisplayTitle~}</h1>
146
+ </div>`
147
+ },
122
148
  {
123
149
  Hash: 'PRSP-Read-Basic-Template',
124
150
  Template: /*html*/`
125
151
  <!-- DefaultPackage pict view template: [PRSP-Read-Basic-Template] -->
126
- <h1>{~D:Record.RecordSet~} {~D:Record.GUIDAddress~} [{~D:Record.RecordConfiguration.GUIDRecord~}]</h1>
152
+ {~T:PRSP-Read-Header-Template~}
127
153
  <!--
128
154
  {~DJ:Record~}
129
155
  -->
@@ -156,7 +182,7 @@ const _DEFAULT_CONFIGURATION__Read = (
156
182
  .psrs-split-view.psrs-collapsed #psrs-resize { display: none; }
157
183
  .psrs-split-view.psrs-collapsed .psrs-left-panel { min-width: 100% !important; width: 100%; }
158
184
  </style>
159
- <h1>{~D:Record.RecordSet~} {~D:Record.GUIDAddress~} [{~D:Record.RecordConfiguration.GUIDRecord~}]</h1>
185
+ {~T:PRSP-Read-Header-Template~}
160
186
  <div class="psrs-split-tabnav">{~T:PRSP-Read-RecordTabNav-Template~}</div>
161
187
  <div class="psrs-split-view psrs-collapsed">
162
188
  <div class="psrs-left-panel" style="min-width: {~D:Record.SplitLeftWidth~};">
@@ -174,36 +200,14 @@ const _DEFAULT_CONFIGURATION__Read = (
174
200
  Hash: 'PRSP-Read-Tab-Template',
175
201
  Template: /*html*/`
176
202
  <!-- DefaultPackage pict view template: [PRSP-Read-Tab-Template] -->
177
- <h1>{~D:Record.RecordSet~} {~D:Record.GUIDAddress~} [{~D:Record.RecordConfiguration.GUIDRecord~}]</h1>
203
+ {~T:PRSP-Read-Header-Template~}
178
204
  <!--
179
205
  {~DJ:Record~}
180
206
  -->
181
207
  <style>
182
- #PRSP-Read-Tab-Nav
183
- {
184
- display: flex;
185
- border-bottom: 1px solid rgba(0,0,0,0.5);
186
- margin-bottom: 20px;
187
- width: 100%;
188
- }
189
- .psrs-tab.is-active
190
- {
191
- border: 1px solid rgba(0,0,0,0.5);
192
- }
193
- .psrs-tab
194
- {
195
- padding: 10px;
196
- border-right: 1px solid rgba(0,0,0,0.5);
197
- border-left: 1px solid rgba(0,0,0,0.5);
198
- }
199
- .psrs-tab-body
200
- {
201
- display: none;
202
- }
203
- .psrs-tab-body.is-active
204
- {
205
- display: inherit;
206
- }
208
+ /* Tab layout owns only its nav container; the pill styling for .psrs-tab /
209
+ .psrs-tab-body is shared from the view's persistent CSS block. */
210
+ #PRSP-Read-Tab-Nav { display: flex; flex-wrap: wrap; gap: 0.35rem; width: 100%; margin: 0 0 1.25rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--theme-color-border-light, #e8ebf0); }
207
211
  </style>
208
212
  <div class="psrs-tab-view">
209
213
  <div id="PRSP-Read-Tabs-Container">
@@ -241,6 +245,7 @@ const _DEFAULT_CONFIGURATION__Read = (
241
245
  {~TS:PRSP-Read-RecordAudit-Created-Template:AppData.PRSP_RecordAudit.CreatedSlot~}
242
246
  {~TS:PRSP-Read-RecordAudit-Updated-Template:AppData.PRSP_RecordAudit.UpdatedSlot~}
243
247
  {~TS:PRSP-Read-RecordAudit-Deleted-Template:AppData.PRSP_RecordAudit.DeletedSlot~}
248
+ {~TS:PRSP-Read-RecordAudit-ExternalSync-Template:AppData.PRSP_RecordAudit.ExternalSyncSlot~}
244
249
  </dl>
245
250
  </div>
246
251
  </div>
@@ -267,6 +272,14 @@ const _DEFAULT_CONFIGURATION__Read = (
267
272
  Hash: 'PRSP-Read-RecordAudit-Deleted-Template',
268
273
  Template: /*html*/`<dt>Deleted</dt><dd class="is-deleted">{~DateFormat:Record.Date^MMM D, YYYY - h:mm A~} <small>by {~E:User^Record.UserID^PRSP-Read-RecordAudit-UserName-Template~}</small></dd>`
269
274
  },
275
+ {
276
+ Hash: 'PRSP-Read-RecordAudit-ExternalSync-Template',
277
+ Template: /*html*/`<dt>External sync</dt><dd>{~TIfAbs:PRSP-Read-RecordAudit-ExternalSyncDate-Template:Record:Record.HasDate^TRUE^~}{~NE:Record.NeverSynced^Never synced~}</dd>`
278
+ },
279
+ {
280
+ Hash: 'PRSP-Read-RecordAudit-ExternalSyncDate-Template',
281
+ Template: /*html*/`{~DateFormat:Record.Date^MMM D, YYYY - h:mm A~}`
282
+ },
270
283
  {
271
284
  // Soft-deleted record banner (the ViewDeleted route, or a record deleted out from
272
285
  // under a normal View). The one-or-zero-element DeletedBannerSlot drives it.
@@ -517,7 +530,9 @@ class viewRecordSetRead extends libPictRecordSetRecordView
517
530
  DeletedSlot: tmpDeleted ? [{ Date: pRecord.DeleteDate, UserID: pRecord.DeletingIDUser }] : [],
518
531
  // The ViewDeleted route's banner: present whenever the record is soft-deleted (whether the
519
532
  // user arrived via ViewDeleted or the record was deleted out from under a normal View).
520
- DeletedBannerSlot: (!!pRecord.Deleted) ? [{ Date: pRecord.DeleteDate, UserID: pRecord.DeletingIDUser, HasDate: this._validAuditDate(pRecord.DeleteDate) }] : []
533
+ DeletedBannerSlot: (!!pRecord.Deleted) ? [{ Date: pRecord.DeleteDate, UserID: pRecord.DeletingIDUser, HasDate: this._validAuditDate(pRecord.DeleteDate) }] : [],
534
+ // External-sync auditing stamp — surfaced in the Details modal for entities that carry the field.
535
+ ExternalSyncSlot: ('ExternalSyncDate' in pRecord) ? [{ Date: pRecord.ExternalSyncDate, HasDate: this._validAuditDate(pRecord.ExternalSyncDate), NeverSynced: !this._validAuditDate(pRecord.ExternalSyncDate) }] : []
521
536
  };
522
537
  }
523
538
 
@@ -656,7 +671,7 @@ class viewRecordSetRead extends libPictRecordSetRecordView
656
671
  return;
657
672
  }
658
673
  const tmpProvider = this.pict.providers[this.providerHash];
659
- const tmpSuppress = _AUDIT_FIELD_NAMES.slice();
674
+ const tmpSuppress = _AUDIT_FIELD_NAMES.concat('IDCustomer'); // IDCustomer: meadow-endpoints/retold tenancy discriminator — server-managed
660
675
  if (tmpProvider)
661
676
  {
662
677
  tmpSuppress.push(tmpProvider.getIDField());
@@ -821,6 +836,14 @@ class viewRecordSetRead extends libPictRecordSetRecordView
821
836
  `);
822
837
  }
823
838
 
839
+ // Readable record header: the record's own name (heuristic — Name / Title / DisplayName / …),
840
+ // with the entity type (camelCase split) as an eyebrow. Falls back to the GUID field + value
841
+ // when the record has no obvious display field. Computed before onBeforeRenderRead so apps can
842
+ // still override DisplayTitle / TitleEyebrow there.
843
+ const tmpReadableName = this._computeDisplayName(tmpRecordReadData.Record);
844
+ tmpRecordReadData.DisplayTitle = tmpReadableName || `${ tmpRecordReadData.GUIDAddress } [${ pRecordConfiguration.GUIDRecord }]`;
845
+ tmpRecordReadData.TitleEyebrow = String(pRecordConfiguration.RecordSet || '').replace(/([a-z0-9])([A-Z])/g, '$1 $2');
846
+
824
847
  tmpRecordReadData = this.onBeforeRenderRead(tmpRecordReadData);
825
848
 
826
849
  this.renderAsync(`PRSP_Renderable_Read_${ this.layoutType }`, tmpRecordReadData.RenderDestination, tmpRecordReadData,
@@ -954,14 +977,14 @@ class viewRecordSetRead extends libPictRecordSetRecordView
954
977
  const schema = await this.pict.providers[providerHash].getRecordSchema();
955
978
  for (const p of Object.keys(schema.properties))
956
979
  {
957
- const exclusionSet = [this.pict.providers[this.providerHash].getIDField(), this.pict.providers[this.providerHash].getGUIDField()].concat(_AUDIT_FIELD_NAMES);
980
+ const exclusionSet = [this.pict.providers[this.providerHash].getIDField(), this.pict.providers[this.providerHash].getGUIDField()].concat(_AUDIT_FIELD_NAMES, 'IDCustomer');
958
981
  if (exclusionSet.includes(p))
959
982
  {
960
983
  continue;
961
984
  }
962
985
  const tmpDescriptor =
963
986
  {
964
- "Name": `${ this.pict.providers[providerHash].getHumanReadableFieldName?.() || p }`,
987
+ "Name": `${ this.pict.providers[providerHash].getHumanReadableFieldName?.(p) || p }`,
965
988
  "Hash": `${ this.pict.providers[this.providerHash].options.Entity }-${ p }`,
966
989
  "DataType": "String",
967
990
  "PictForm":
@@ -1000,6 +1023,28 @@ class viewRecordSetRead extends libPictRecordSetRecordView
1000
1023
  tmpDescriptor.DataType = 'String';
1001
1024
  }
1002
1025
 
1026
+ const tmpForeignEntity = this.pict.PictSectionRecordSet.resolveForeignEntity(p, this.pict.PictSectionRecordSet.recordSetProviderConfigurations[recordSet], this.pict.providers[this.providerHash].getIDField());
1027
+ if (tmpForeignEntity && this.pict.providers['Pict-Section-Picker'])
1028
+ {
1029
+ // Foreign-key column → searchable entity Picker (edit) + resolved name (read, see the view-mode override).
1030
+ tmpDescriptor.PictForm.InputType = 'Picker';
1031
+ tmpDescriptor.PictForm.Entity = tmpForeignEntity;
1032
+ const tmpForeignConfig = this.pict.PictSectionRecordSet.recordSetProviderConfigurations[tmpForeignEntity];
1033
+ if (tmpForeignConfig && tmpForeignConfig.SearchFields) { tmpDescriptor.PictForm.SearchFields = tmpForeignConfig.SearchFields; }
1034
+ }
1035
+ // Per-record-set label override (e.g. Contract/Project IDOrganization → "Prime Contractor").
1036
+ const tmpReadRSConfig = this.pict.PictSectionRecordSet.recordSetProviderConfigurations[recordSet];
1037
+ if (tmpReadRSConfig && tmpReadRSConfig.FieldLabels && tmpReadRSConfig.FieldLabels[p]) { tmpDescriptor.Name = tmpReadRSConfig.FieldLabels[p]; }
1038
+ // JSON/object columns → embedded ObjectEditor (read-only tree in View, editable in Edit). Driven by the
1039
+ // schema column type, with a per-record-set ObjectEditorFields list to opt in string-typed JSON blobs.
1040
+ // Gated on the pict-section-form ObjectEditor input being registered (graceful on older form versions).
1041
+ const tmpObjectEditorFields = (tmpReadRSConfig && Array.isArray(tmpReadRSConfig.ObjectEditorFields)) ? tmpReadRSConfig.ObjectEditorFields : [];
1042
+ if (tmpDescriptor.PictForm.InputType !== 'Picker' && this.pict.providers['Pict-Input-ObjectEditor'] &&
1043
+ (schema.properties[p].type === 'object' || schema.properties[p].type === 'json' || tmpObjectEditorFields.includes(p)))
1044
+ {
1045
+ tmpDescriptor.DataType = 'Object';
1046
+ tmpDescriptor.PictForm.InputType = 'ObjectEditor';
1047
+ }
1003
1048
  defaultManifest.Descriptors[`${ recordSet }Details.${ p }`] = tmpDescriptor;
1004
1049
  }
1005
1050
  return defaultManifest;
@@ -1030,6 +1075,11 @@ class viewRecordSetRead extends libPictRecordSetRecordView
1030
1075
  {
1031
1076
  tmpManifest.Descriptors[x].PictForm = {};
1032
1077
  }
1078
+ // Entity-reference (Picker) fields stay a Picker but render read-only (the resolved entity
1079
+ // name as plain text, no dropdown); everything else becomes plain read-only text.
1080
+ if (tmpManifest.Descriptors[x].PictForm.InputType === 'Picker') { tmpManifest.Descriptors[x].PictForm.ReadOnly = true; continue; }
1081
+ // ObjectEditor stays an ObjectEditor but renders its read-only tree (not a plain text field).
1082
+ if (tmpManifest.Descriptors[x].PictForm.InputType === 'ObjectEditor') { tmpManifest.Descriptors[x].PictForm.ReadOnly = true; continue; }
1033
1083
  tmpManifest.Descriptors[x].PictForm.InputType = 'ReadOnly';
1034
1084
  }
1035
1085
  }