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 +1 -1
- package/source/services/RecordsSet-MetaController.js +37 -0
- package/source/views/create/RecordSet-Create.js +13 -2
- package/source/views/dashboard/RecordSet-Dashboard.js +2 -0
- package/source/views/list/RecordSet-List-Title.js +8 -0
- package/source/views/list/RecordSet-List.js +22 -0
- package/source/views/read/RecordSet-Read.js +85 -35
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
203
|
+
{~T:PRSP-Read-Header-Template~}
|
|
178
204
|
<!--
|
|
179
205
|
{~DJ:Record~}
|
|
180
206
|
-->
|
|
181
207
|
<style>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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.
|
|
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
|
}
|