@aglyn/plugins-data 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +47 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/data-console-page.d.ts +13 -0
- package/src/lib/components/data-console-page.js +63 -0
- package/src/lib/components/data-console-page.js.map +1 -0
- package/src/lib/components/dataset-record-dialog.component.d.ts +52 -0
- package/src/lib/components/dataset-record-dialog.component.js +249 -0
- package/src/lib/components/dataset-record-dialog.component.js.map +1 -0
- package/src/lib/components/dataset-schema-dialog.component.d.ts +51 -0
- package/src/lib/components/dataset-schema-dialog.component.js +979 -0
- package/src/lib/components/dataset-schema-dialog.component.js.map +1 -0
- package/src/lib/components/host-datasets-card.component.d.ts +39 -0
- package/src/lib/components/host-datasets-card.component.js +1831 -0
- package/src/lib/components/host-datasets-card.component.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +9 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/model/dataset-io.d.ts +43 -0
- package/src/lib/model/dataset-io.js +84 -0
- package/src/lib/model/dataset-io.js.map +1 -0
- package/src/lib/model/dataset-record-view.d.ts +157 -0
- package/src/lib/model/dataset-record-view.js +293 -0
- package/src/lib/model/dataset-record-view.js.map +1 -0
- package/src/lib/model/index.d.ts +22 -0
- package/src/lib/model/index.js +22 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/plugin.d.ts +35 -0
- package/src/lib/plugin.js +78 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/repeat/dataset-repeat-rows.d.ts +45 -0
- package/src/lib/repeat/dataset-repeat-rows.js +108 -0
- package/src/lib/repeat/dataset-repeat-rows.js.map +1 -0
- package/src/lib/repeat/dataset-repeat-source.d.ts +39 -0
- package/src/lib/repeat/dataset-repeat-source.js +95 -0
- package/src/lib/repeat/dataset-repeat-source.js.map +1 -0
|
@@ -0,0 +1,979 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
|
+
import { DATASET_FIELD_TYPE_LABELS, DATASET_FIELD_TYPES, defaultDatasetFieldId, getCustomFieldType, listCustomFieldTypes, validateDatasetFieldId, describeScope, effectiveDatasetModel, HOST_ACCESS_ROLES, hostScopeToken, narrowsScope, scopeCovers, ORG_SCOPE_TOKEN, scopeToStore, storedScope, visibleToHost } from "@aglyn/aglyn";
|
|
20
|
+
import { useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
21
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
22
|
+
import { Button, Checkbox, Alert, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, IconButton, MenuItem, Select, Stack, TextField, Typography } from "@mui/material";
|
|
23
|
+
import { collection, doc, limit, query, updateDoc, where } from "firebase/firestore";
|
|
24
|
+
import { useCallback, useEffect, useState } from "react";
|
|
25
|
+
import { useFirestore, useFirestoreCollection, useOrgDataScope, useScopeTokens, useUser, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
|
|
26
|
+
/** Types surfaced in the picker; the rest exist for compat, not authoring. */ const AUTHORABLE_TYPES = [
|
|
27
|
+
'text',
|
|
28
|
+
'bool',
|
|
29
|
+
'int32',
|
|
30
|
+
'float',
|
|
31
|
+
'timestamp',
|
|
32
|
+
'coordinates',
|
|
33
|
+
'sorted',
|
|
34
|
+
'reference'
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Model builder (AGL-178): edits the AGL-177 typed model on a dataset —
|
|
38
|
+
* singular/plural display names (dod.ts Schema.Name), add/edit/remove/
|
|
39
|
+
* reorder fields, per-field type/required/default/description/validation.
|
|
40
|
+
* Schema-change policies (documented per the issue): type changes never
|
|
41
|
+
* rewrite documents — non-conforming values surface in the editor when it
|
|
42
|
+
* validates (AGL-179); removed fields leave orphaned values that strip
|
|
43
|
+
* lazily on next document save; fieldIds are stable keys — only display
|
|
44
|
+
* names rename.
|
|
45
|
+
*/ export function DatasetSchemaDialog(props) {
|
|
46
|
+
var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _find, _ref8, _ref9, _ref10, _ref11;
|
|
47
|
+
var _editorDefinition_reference, _editorDefinition_reference1, _editorDefinition_reference2, _editorDefinition_reference3, _editorDefinition_validation;
|
|
48
|
+
const { hostId, dataset, datasets, recordCount, seedFromCache, seedUnreadable, onClose } = props;
|
|
49
|
+
// Org-shared data root (AGL-237/239). Null until the async org lookup
|
|
50
|
+
// settles (AGL-1061), and for a host with no owning org. Saving is held
|
|
51
|
+
// on it rather than redirected: the host path this used to fall back to
|
|
52
|
+
// is now denied by the rules outright (AGL-1050), so a save there would
|
|
53
|
+
// fail loudly instead of quietly — but it should not be attempted at all.
|
|
54
|
+
const { scope: dataScope, orgId } = useOrgDataScope({
|
|
55
|
+
hostId,
|
|
56
|
+
orgId: props.orgId
|
|
57
|
+
});
|
|
58
|
+
const firestore = useFirestore();
|
|
59
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
60
|
+
const { confirm } = useConfirmationContext();
|
|
61
|
+
// Sharing scope (AGL-1044) — org datasets only; the legacy host path is
|
|
62
|
+
// site-private by construction.
|
|
63
|
+
// `orgWide` reads TRUE while the member doc loads (AGL-1047), which would
|
|
64
|
+
// offer a scoped collaborator the scope Select for a beat before replacing
|
|
65
|
+
// it with the read-only summary — and offering a control the AGL-1041
|
|
66
|
+
// rules will reject is the thing the branch below exists to avoid. Hold
|
|
67
|
+
// the whole block until `loaded` rather than flash the wrong one.
|
|
68
|
+
const { orgWide: viewerOrgWide, loaded: scopeLoaded } = useScopeTokens(orgId != null ? orgId : undefined);
|
|
69
|
+
// Sites in this org, for naming what a narrowing would cost. Queried
|
|
70
|
+
// here rather than via the console's useOrgHosts — this component lives
|
|
71
|
+
// in a lib and cannot import from an app — so it must repeat that hook's
|
|
72
|
+
// membership filter, which is the part this was missing (AGL-1145).
|
|
73
|
+
//
|
|
74
|
+
// `/hosts/{hostId}` is gated per document on `memberRoles.{uid}`, and
|
|
75
|
+
// Firestore evaluates a LIST against the QUERY rather than its results: it
|
|
76
|
+
// refuses to run one that COULD return a denied document, whatever today's
|
|
77
|
+
// data happens to hold. So filtering on `orgId` alone was not a partial
|
|
78
|
+
// result, it was a flat denial for every non-staff caller — including one
|
|
79
|
+
// who is a member of every site in the org. Proved in the emulator
|
|
80
|
+
// (cloud/hosts-list-constraint.spec.mjs); the reason nobody hit it is that
|
|
81
|
+
// staff match the rule's other branch, and staff are who tested it.
|
|
82
|
+
const { data: user } = useUser();
|
|
83
|
+
const uid = user == null ? void 0 : user.uid;
|
|
84
|
+
const { data: orgHostDocs } = useFirestoreCollection(// Hold rather than substitute a placeholder while the uid resolves. The
|
|
85
|
+
// old `orgId ?? '-none-'` fired a real query on every mount before the
|
|
86
|
+
// scope was known; a sentinel is still a question asked of the server,
|
|
87
|
+
// and here it is one that gets denied.
|
|
88
|
+
()=>uid && orgId ? query(collection(firestore, 'hosts'), where(`memberRoles.${uid}`, 'in', HOST_ACCESS_ROLES), where('orgId', '==', orgId), limit(200)) : null, [
|
|
89
|
+
firestore,
|
|
90
|
+
orgId,
|
|
91
|
+
uid
|
|
92
|
+
], {
|
|
93
|
+
idField: '$id'
|
|
94
|
+
});
|
|
95
|
+
const orgHostList = orgHostDocs != null ? orgHostDocs : [];
|
|
96
|
+
const [visibleTo, setVisibleTo] = useState([]);
|
|
97
|
+
/**
|
|
98
|
+
* The dataset stores NO scope (AGL-1484) — as opposed to storing `['org']`.
|
|
99
|
+
*
|
|
100
|
+
* The dialog used to render the two identically, which for a dataset is a
|
|
101
|
+
* worse lie than it was for a media file: `orgs/{orgId}/datasets` is read
|
|
102
|
+
* with `array-contains-any` in `resolve-dataset.ts` and in the console's
|
|
103
|
+
* own `organizations.ts`, so an unstamped dataset is matched by no reader
|
|
104
|
+
* in the product. It renders on no site and is missing from the reference
|
|
105
|
+
* cards even for an org-wide member — while this control said "All sites".
|
|
106
|
+
*/ const [scopeUnset, setScopeUnset] = useState(false);
|
|
107
|
+
const [model, setModel] = useState({
|
|
108
|
+
fields: {},
|
|
109
|
+
order: []
|
|
110
|
+
});
|
|
111
|
+
const [names, setNames] = useState({
|
|
112
|
+
singular: '',
|
|
113
|
+
plural: ''
|
|
114
|
+
});
|
|
115
|
+
useEffect(()=>{
|
|
116
|
+
var _ref, _ref1, _ref2;
|
|
117
|
+
var _dataset_names, _dataset_names1;
|
|
118
|
+
if (!dataset) return;
|
|
119
|
+
// The fourth copy of the AGL-1466 substitution (AGL-1484), and the seed
|
|
120
|
+
// half of it. Persisting the default on open was the other option and
|
|
121
|
+
// AGL-1466 ruled it out: opening a dialog would write, the AGL-1042
|
|
122
|
+
// rules refuse that write from anyone who is not org-wide, and it
|
|
123
|
+
// repairs only what somebody happens to look at — a repair mechanism
|
|
124
|
+
// wearing a UI. `previousScope` in `handleSave` reads the SAME helper;
|
|
125
|
+
// see the note there for why the pair is the safety property.
|
|
126
|
+
const storedDatasetScope = storedScope(dataset.visibleTo);
|
|
127
|
+
setVisibleTo(storedDatasetScope != null ? storedDatasetScope : []);
|
|
128
|
+
setScopeUnset(!storedDatasetScope);
|
|
129
|
+
const effective = effectiveDatasetModel(dataset);
|
|
130
|
+
setModel({
|
|
131
|
+
fields: JSON.parse(JSON.stringify(effective.fields)),
|
|
132
|
+
order: [
|
|
133
|
+
...effective.order
|
|
134
|
+
]
|
|
135
|
+
});
|
|
136
|
+
setNames({
|
|
137
|
+
singular: (_ref = (_dataset_names = dataset.names) == null ? void 0 : _dataset_names.singular) != null ? _ref : '',
|
|
138
|
+
plural: (_ref1 = (_ref2 = (_dataset_names1 = dataset.names) == null ? void 0 : _dataset_names1.plural) != null ? _ref2 : dataset.displayName) != null ? _ref1 : ''
|
|
139
|
+
});
|
|
140
|
+
}, [
|
|
141
|
+
dataset
|
|
142
|
+
]);
|
|
143
|
+
// Per-field editor (null fieldId = new field). `idDraft`/`idTouched` back
|
|
144
|
+
// the editable Reference ID (AGL-578): for a new field the draft follows
|
|
145
|
+
// the display name until the user edits it; existing fields keep the id
|
|
146
|
+
// frozen (idTouched=true, input disabled).
|
|
147
|
+
const [fieldEditor, setFieldEditor] = useState(null);
|
|
148
|
+
const openFieldEditor = useCallback((fieldId)=>()=>{
|
|
149
|
+
var _ref;
|
|
150
|
+
var _definition_validation;
|
|
151
|
+
const definition = fieldId ? JSON.parse(JSON.stringify(model.fields[fieldId])) : {
|
|
152
|
+
name: '',
|
|
153
|
+
type: 'text'
|
|
154
|
+
};
|
|
155
|
+
setFieldEditor({
|
|
156
|
+
fieldId,
|
|
157
|
+
definition,
|
|
158
|
+
optionsText: ((_ref = (_definition_validation = definition.validation) == null ? void 0 : _definition_validation.options) != null ? _ref : []).join(', '),
|
|
159
|
+
idDraft: fieldId != null ? fieldId : '',
|
|
160
|
+
idTouched: fieldId != null
|
|
161
|
+
});
|
|
162
|
+
}, [
|
|
163
|
+
model
|
|
164
|
+
]);
|
|
165
|
+
const handleFieldSave = useCallback(async ()=>{
|
|
166
|
+
var _definition_validation;
|
|
167
|
+
var _definition_description;
|
|
168
|
+
if (!fieldEditor) return;
|
|
169
|
+
const definition = _extends({}, fieldEditor.definition);
|
|
170
|
+
if (!definition.name.trim()) return;
|
|
171
|
+
definition.name = definition.name.trim();
|
|
172
|
+
// Descriptions (AGL-560) surface as hints wherever the field appears;
|
|
173
|
+
// blank ones are dropped so the model doesn't accrete empty strings.
|
|
174
|
+
const description = (_definition_description = definition.description) == null ? void 0 : _definition_description.trim();
|
|
175
|
+
if (description) definition.description = description;
|
|
176
|
+
else delete definition.description;
|
|
177
|
+
const options = fieldEditor.optionsText.split(',').map((option)=>option.trim()).filter(Boolean);
|
|
178
|
+
definition.validation = _extends({}, (_definition_validation = definition.validation) != null ? _definition_validation : {}, options.length ? {
|
|
179
|
+
options
|
|
180
|
+
} : {});
|
|
181
|
+
if (!options.length) delete definition.validation.options;
|
|
182
|
+
if (!Object.keys(definition.validation).length) delete definition.validation;
|
|
183
|
+
let fieldId = fieldEditor.fieldId;
|
|
184
|
+
if (!fieldId) {
|
|
185
|
+
// New field: the reference id comes from the (editable) draft, which
|
|
186
|
+
// defaulted from the display name. Validate before it becomes a key.
|
|
187
|
+
const id = fieldEditor.idDraft.trim();
|
|
188
|
+
const idError = validateDatasetFieldId(id, new Set(model.order.map((existing)=>existing.toLowerCase())));
|
|
189
|
+
if (idError) {
|
|
190
|
+
return void enqueueSnackbar(idError, {
|
|
191
|
+
variant: 'warning',
|
|
192
|
+
persist: false
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
fieldId = id;
|
|
196
|
+
} else if (recordCount > 0 && model.fields[fieldId] && model.fields[fieldId].type !== definition.type) {
|
|
197
|
+
// Documented policy: changing a type never rewrites documents;
|
|
198
|
+
// existing values that no longer conform get flagged when edited.
|
|
199
|
+
const confirmed = await confirm({
|
|
200
|
+
title: 'Change field type?',
|
|
201
|
+
description: `${recordCount} document${recordCount === 1 ? '' : 's'} exist. ` + 'Stored values are not rewritten — ones that no longer match ' + `"${DATASET_FIELD_TYPE_LABELS[definition.type]}" will be flagged ` + 'when documents are next edited.',
|
|
202
|
+
confirmationText: 'Change type'
|
|
203
|
+
}).then(()=>true).catch(()=>false);
|
|
204
|
+
if (!confirmed) return;
|
|
205
|
+
}
|
|
206
|
+
setModel((prev)=>({
|
|
207
|
+
fields: _extends({}, prev.fields, {
|
|
208
|
+
[fieldId]: definition
|
|
209
|
+
}),
|
|
210
|
+
order: prev.order.includes(fieldId) ? prev.order : [
|
|
211
|
+
...prev.order,
|
|
212
|
+
fieldId
|
|
213
|
+
]
|
|
214
|
+
}));
|
|
215
|
+
setFieldEditor(null);
|
|
216
|
+
}, [
|
|
217
|
+
fieldEditor,
|
|
218
|
+
model,
|
|
219
|
+
recordCount,
|
|
220
|
+
confirm,
|
|
221
|
+
enqueueSnackbar
|
|
222
|
+
]);
|
|
223
|
+
const handleFieldRemove = useCallback((fieldId)=>async ()=>{
|
|
224
|
+
var _ref;
|
|
225
|
+
var _model_fields_fieldId;
|
|
226
|
+
const confirmed = await confirm({
|
|
227
|
+
title: `Remove field "${(_ref = (_model_fields_fieldId = model.fields[fieldId]) == null ? void 0 : _model_fields_fieldId.name) != null ? _ref : fieldId}"?`,
|
|
228
|
+
description: 'Existing documents keep their stored value until they are next ' + 'saved (it is stripped then). Bindings using this field stop ' + 'resolving.',
|
|
229
|
+
confirmationText: 'Remove',
|
|
230
|
+
confirmationButtonProps: {
|
|
231
|
+
color: 'error'
|
|
232
|
+
}
|
|
233
|
+
}).then(()=>true).catch(()=>false);
|
|
234
|
+
if (!confirmed) return;
|
|
235
|
+
setModel((prev)=>{
|
|
236
|
+
const fields = _extends({}, prev.fields);
|
|
237
|
+
delete fields[fieldId];
|
|
238
|
+
return {
|
|
239
|
+
fields,
|
|
240
|
+
order: prev.order.filter((id)=>id !== fieldId)
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
}, [
|
|
244
|
+
model,
|
|
245
|
+
confirm
|
|
246
|
+
]);
|
|
247
|
+
const moveField = useCallback((fieldId, delta)=>()=>setModel((prev)=>{
|
|
248
|
+
const index = prev.order.indexOf(fieldId);
|
|
249
|
+
const target = index + delta;
|
|
250
|
+
if (index < 0 || target < 0 || target >= prev.order.length) return prev;
|
|
251
|
+
const order = [
|
|
252
|
+
...prev.order
|
|
253
|
+
];
|
|
254
|
+
order.splice(index, 1);
|
|
255
|
+
order.splice(target, 0, fieldId);
|
|
256
|
+
return _extends({}, prev, {
|
|
257
|
+
order
|
|
258
|
+
});
|
|
259
|
+
}), []);
|
|
260
|
+
const handleSave = useCallback(async ()=>{
|
|
261
|
+
var _storedScope;
|
|
262
|
+
if (!dataset || !dataScope) return;
|
|
263
|
+
/**
|
|
264
|
+
* What the document actually carries, `[]` when it carries nothing
|
|
265
|
+
* (AGL-1484) — matching the seed above, through the same helper.
|
|
266
|
+
*
|
|
267
|
+
* The pairing is the whole safety property, not tidiness. Both sides
|
|
268
|
+
* used to substitute `[ORG_SCOPE_TOKEN]`, so an unset dataset compared
|
|
269
|
+
* equal to itself and merely OPENING the dialog wrote nothing; the
|
|
270
|
+
* damage was confined to the screen. Correcting the seed alone would
|
|
271
|
+
* make `scopeChanged` true for every unset dataset the moment its
|
|
272
|
+
* dialog opened, turning a lie on screen into a write nobody asked for
|
|
273
|
+
* — one the AGL-1041 rules then reject for any member who is not
|
|
274
|
+
* org-wide, failing the WHOLE schema save with it.
|
|
275
|
+
*
|
|
276
|
+
* `[]` is also the honest previous value in the other direction: a
|
|
277
|
+
* deliberate save of exactly `['org']` on an unset dataset used to
|
|
278
|
+
* compare equal to the default and be dropped, and that is the one save
|
|
279
|
+
* an unstamped dataset most needs to land.
|
|
280
|
+
*/ const previousScope = (_storedScope = storedScope(dataset.visibleTo)) != null ? _storedScope : [];
|
|
281
|
+
const scopeChanged = JSON.stringify([
|
|
282
|
+
...previousScope
|
|
283
|
+
].sort()) !== JSON.stringify([
|
|
284
|
+
...visibleTo
|
|
285
|
+
].sort());
|
|
286
|
+
/**
|
|
287
|
+
* The scope this save writes, or null when it writes none.
|
|
288
|
+
*
|
|
289
|
+
* Only an org-wide member may change the scope — the AGL-1041 rules deny
|
|
290
|
+
* anyone else, and a rejected field fails the WHOLE write.
|
|
291
|
+
*
|
|
292
|
+
* A selection `scopeToStore` cannot store — "Selected sites…" with no site
|
|
293
|
+
* picked, or more sites than one query can match — refuses the save rather
|
|
294
|
+
* than substituting a scope: the org token would share the collection with
|
|
295
|
+
* every site, and anything narrower would drop sites somebody picked.
|
|
296
|
+
*/ const scopeWrite = orgId && viewerOrgWide && scopeChanged ? scopeToStore(visibleTo) : null;
|
|
297
|
+
if (scopeWrite == null ? void 0 : scopeWrite.problem) {
|
|
298
|
+
return void enqueueSnackbar(scopeWrite.problem, {
|
|
299
|
+
variant: 'warning',
|
|
300
|
+
persist: false
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (scopeChanged && narrowsScope(previousScope, visibleTo)) {
|
|
304
|
+
// Narrowing takes a dataset away from sites whose pages may bind it,
|
|
305
|
+
// and a repeatable bound to a dataset it can no longer see renders
|
|
306
|
+
// nothing (AGL-1039). Name the cost before paying it.
|
|
307
|
+
const losing = orgHostList.filter((host)=>visibleToHost(previousScope, host.$id)).filter((host)=>!visibleToHost(visibleTo, host.$id)).map((host)=>{
|
|
308
|
+
var _ref, _host_name;
|
|
309
|
+
return (_ref = (_host_name = host.name) != null ? _host_name : host.subdomain) != null ? _ref : host.$id;
|
|
310
|
+
});
|
|
311
|
+
const proceed = await confirm({
|
|
312
|
+
title: 'Limit which sites can use this collection?',
|
|
313
|
+
description: losing.length ? `${losing.join(', ')} will stop seeing this collection. Pages ` + 'that repeat over it will render nothing. No data is deleted.' : 'No site loses access, so nothing breaks today.',
|
|
314
|
+
confirmationText: 'Limit access'
|
|
315
|
+
}).then(()=>true).catch(()=>false);
|
|
316
|
+
if (!proceed) return;
|
|
317
|
+
}
|
|
318
|
+
if (!model.order.length) {
|
|
319
|
+
return void enqueueSnackbar('A collection needs at least one field', {
|
|
320
|
+
variant: 'warning',
|
|
321
|
+
persist: false
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Refuse the write when the seeding listener is unconfirmed (AGL-1358).
|
|
326
|
+
*
|
|
327
|
+
* Everything below is seeded from `dataset` and written back whole, so
|
|
328
|
+
* `updateDoc` field-level merging protects nothing — the untouched
|
|
329
|
+
* fields are all in the payload. Two of them make a stale seed worse
|
|
330
|
+
* than an inconvenience:
|
|
331
|
+
*
|
|
332
|
+
* - `visibleTo` is the AGL-1041/1042 scoping PREDICATE. Rewriting it
|
|
333
|
+
* from a cached seed re-exposes a collection that was narrowed, or
|
|
334
|
+
* hides one that was widened, and the confirmation above is computed
|
|
335
|
+
* against `previousScope` read from that same stale seed — so it does
|
|
336
|
+
* not even warn about the sites it is really taking access from.
|
|
337
|
+
* - `model` is the entire schema. A cached seed reinstates fields that
|
|
338
|
+
* were deleted and drops fields that were added.
|
|
339
|
+
*/ await writeGuardedBySeed({
|
|
340
|
+
subject: 'collection schema',
|
|
341
|
+
unreadable: seedUnreadable,
|
|
342
|
+
fromCache: seedFromCache
|
|
343
|
+
}, async ()=>void await updateDoc(doc(firestore, dataScope[0], dataScope[1], 'datasets', dataset.$id), _extends({
|
|
344
|
+
model,
|
|
345
|
+
names: {
|
|
346
|
+
singular: names.singular.trim(),
|
|
347
|
+
plural: names.plural.trim()
|
|
348
|
+
}
|
|
349
|
+
}, names.plural.trim() ? {
|
|
350
|
+
displayName: names.plural.trim()
|
|
351
|
+
} : {}, {
|
|
352
|
+
// v1 compat: keep the flat column list mirroring the model order
|
|
353
|
+
// so unmigrated consumers (AGL-103 bindings) keep working.
|
|
354
|
+
fields: model.order
|
|
355
|
+
}, (scopeWrite == null ? void 0 : scopeWrite.scope) ? {
|
|
356
|
+
visibleTo: scopeWrite.scope
|
|
357
|
+
} : {}))).then((result)=>{
|
|
358
|
+
// A refusal leaves the dialog open with the edited schema intact.
|
|
359
|
+
if (!result.ok) {
|
|
360
|
+
return void enqueueSnackbar(result.message, {
|
|
361
|
+
variant: 'warning',
|
|
362
|
+
persist: false
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
enqueueSnackbar('Schema saved', {
|
|
366
|
+
variant: 'success',
|
|
367
|
+
persist: false
|
|
368
|
+
});
|
|
369
|
+
onClose();
|
|
370
|
+
}).catch(()=>enqueueSnackbar('An error has occurred', {
|
|
371
|
+
variant: 'error'
|
|
372
|
+
}));
|
|
373
|
+
}, [
|
|
374
|
+
dataset,
|
|
375
|
+
model,
|
|
376
|
+
names,
|
|
377
|
+
visibleTo,
|
|
378
|
+
viewerOrgWide,
|
|
379
|
+
orgHostList,
|
|
380
|
+
confirm,
|
|
381
|
+
firestore,
|
|
382
|
+
dataScope,
|
|
383
|
+
orgId,
|
|
384
|
+
seedFromCache,
|
|
385
|
+
seedUnreadable,
|
|
386
|
+
enqueueSnackbar,
|
|
387
|
+
onClose
|
|
388
|
+
]);
|
|
389
|
+
const editorDefinition = fieldEditor == null ? void 0 : fieldEditor.definition;
|
|
390
|
+
// Reference ID editing (AGL-578): the id is editable only while creating a
|
|
391
|
+
// field; live-validate the draft so the input and Save button can react.
|
|
392
|
+
const isNewField = fieldEditor != null && fieldEditor.fieldId == null;
|
|
393
|
+
const fieldIdError = fieldEditor && isNewField ? validateDatasetFieldId(fieldEditor.idDraft, new Set(model.order.map((id)=>id.toLowerCase()))) : null;
|
|
394
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
395
|
+
children: [
|
|
396
|
+
/*#__PURE__*/ _jsxs(Dialog, {
|
|
397
|
+
open: Boolean(dataset),
|
|
398
|
+
onClose: onClose,
|
|
399
|
+
maxWidth: "sm",
|
|
400
|
+
fullWidth: true,
|
|
401
|
+
children: [
|
|
402
|
+
/*#__PURE__*/ _jsx(DialogTitle, {
|
|
403
|
+
children: `Schema — ${(_ref = dataset == null ? void 0 : dataset.displayName) != null ? _ref : ''}`
|
|
404
|
+
}),
|
|
405
|
+
/*#__PURE__*/ _jsxs(DialogContent, {
|
|
406
|
+
sx: {
|
|
407
|
+
display: 'flex',
|
|
408
|
+
flexDirection: 'column',
|
|
409
|
+
gap: 2
|
|
410
|
+
},
|
|
411
|
+
children: [
|
|
412
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
413
|
+
direction: "row",
|
|
414
|
+
spacing: 1,
|
|
415
|
+
sx: {
|
|
416
|
+
mt: 1
|
|
417
|
+
},
|
|
418
|
+
children: [
|
|
419
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
420
|
+
size: "small",
|
|
421
|
+
label: "Singular name",
|
|
422
|
+
value: names.singular,
|
|
423
|
+
onChange: (event)=>setNames((prev)=>_extends({}, prev, {
|
|
424
|
+
singular: event.target.value
|
|
425
|
+
})),
|
|
426
|
+
helperText: "e.g. Product"
|
|
427
|
+
}),
|
|
428
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
429
|
+
size: "small",
|
|
430
|
+
label: "Plural name",
|
|
431
|
+
value: names.plural,
|
|
432
|
+
onChange: (event)=>setNames((prev)=>_extends({}, prev, {
|
|
433
|
+
plural: event.target.value
|
|
434
|
+
})),
|
|
435
|
+
helperText: "e.g. Products (shown as the title)"
|
|
436
|
+
})
|
|
437
|
+
]
|
|
438
|
+
}),
|
|
439
|
+
orgId ? /*#__PURE__*/ _jsxs(Stack, {
|
|
440
|
+
spacing: 0.5,
|
|
441
|
+
children: [
|
|
442
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
443
|
+
variant: "caption",
|
|
444
|
+
color: "text.secondary",
|
|
445
|
+
children: 'Shared with'
|
|
446
|
+
}),
|
|
447
|
+
!scopeLoaded ? /*#__PURE__*/ _jsx(Typography, {
|
|
448
|
+
variant: "body2",
|
|
449
|
+
color: "text.secondary",
|
|
450
|
+
children: 'Checking your access…'
|
|
451
|
+
}) : viewerOrgWide ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
452
|
+
children: [
|
|
453
|
+
scopeUnset ? /*#__PURE__*/ _jsx(Alert, {
|
|
454
|
+
severity: "warning",
|
|
455
|
+
children: 'This collection has never been shared. Nothing is ' + 'stored, so it is hidden from every site and any ' + 'page repeating over it renders nothing. Choose who ' + 'it is shared with to fix that.'
|
|
456
|
+
}) : null,
|
|
457
|
+
/*#__PURE__*/ _jsxs(Select, {
|
|
458
|
+
size: "small",
|
|
459
|
+
value: visibleTo.includes(ORG_SCOPE_TOKEN) ? 'org' : scopeUnset ? 'unset' : 'hosts',
|
|
460
|
+
onChange: (event)=>{
|
|
461
|
+
// Any choice ends the unset state — from here the
|
|
462
|
+
// control shows what was picked, not what was (not)
|
|
463
|
+
// stored.
|
|
464
|
+
setScopeUnset(false);
|
|
465
|
+
setVisibleTo(event.target.value === 'org' ? [
|
|
466
|
+
ORG_SCOPE_TOKEN
|
|
467
|
+
] : []);
|
|
468
|
+
},
|
|
469
|
+
children: [
|
|
470
|
+
scopeUnset ? /*#__PURE__*/ _jsx(MenuItem, {
|
|
471
|
+
value: "unset",
|
|
472
|
+
disabled: true,
|
|
473
|
+
children: 'Not shared with any site'
|
|
474
|
+
}) : null,
|
|
475
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
476
|
+
value: "org",
|
|
477
|
+
children: 'All sites'
|
|
478
|
+
}),
|
|
479
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
480
|
+
value: "hosts",
|
|
481
|
+
children: 'Selected sites…'
|
|
482
|
+
})
|
|
483
|
+
]
|
|
484
|
+
}),
|
|
485
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
486
|
+
variant: "caption",
|
|
487
|
+
color: "text.secondary",
|
|
488
|
+
children: 'Sites you exclude cannot see or use this dataset — in the console or on their pages.'
|
|
489
|
+
}),
|
|
490
|
+
!scopeUnset && !visibleTo.includes(ORG_SCOPE_TOKEN) ? /*#__PURE__*/ _jsx(Stack, {
|
|
491
|
+
direction: "row",
|
|
492
|
+
spacing: 0.5,
|
|
493
|
+
useFlexGap: true,
|
|
494
|
+
sx: {
|
|
495
|
+
flexWrap: 'wrap'
|
|
496
|
+
},
|
|
497
|
+
children: orgHostList.map((host)=>{
|
|
498
|
+
var _ref, _host_name;
|
|
499
|
+
const token = hostScopeToken(host.$id);
|
|
500
|
+
const on = visibleTo.includes(token);
|
|
501
|
+
return /*#__PURE__*/ _jsx(Chip, {
|
|
502
|
+
size: "small",
|
|
503
|
+
label: (_ref = (_host_name = host.name) != null ? _host_name : host.subdomain) != null ? _ref : host.$id,
|
|
504
|
+
color: on ? 'primary' : 'default',
|
|
505
|
+
variant: on ? 'filled' : 'outlined',
|
|
506
|
+
onClick: ()=>setVisibleTo((prev)=>on ? prev.filter((t)=>t !== token) : [
|
|
507
|
+
...prev,
|
|
508
|
+
token
|
|
509
|
+
])
|
|
510
|
+
}, host.$id);
|
|
511
|
+
})
|
|
512
|
+
}) : null,
|
|
513
|
+
!scopeUnset && scopeToStore(visibleTo).problem ? /*#__PURE__*/ _jsx(Typography, {
|
|
514
|
+
variant: "caption",
|
|
515
|
+
color: "error",
|
|
516
|
+
children: scopeToStore(visibleTo).problem
|
|
517
|
+
}) : null
|
|
518
|
+
]
|
|
519
|
+
}) : /*#__PURE__*/ _jsx(Typography, {
|
|
520
|
+
variant: "body2",
|
|
521
|
+
children: scopeUnset ? 'Never shared — hidden from every site' : describeScope(visibleTo, Object.fromEntries(orgHostList.map((host)=>{
|
|
522
|
+
var _ref, _host_name;
|
|
523
|
+
return [
|
|
524
|
+
host.$id,
|
|
525
|
+
(_ref = (_host_name = host.name) != null ? _host_name : host.subdomain) != null ? _ref : host.$id
|
|
526
|
+
];
|
|
527
|
+
})))
|
|
528
|
+
})
|
|
529
|
+
]
|
|
530
|
+
}) : null,
|
|
531
|
+
/*#__PURE__*/ _jsx(Stack, {
|
|
532
|
+
spacing: 0.5,
|
|
533
|
+
children: model.order.map((fieldId, index)=>{
|
|
534
|
+
var _DATASET_FIELD_TYPE_LABELS_field_type;
|
|
535
|
+
var _field_validation;
|
|
536
|
+
const field = model.fields[fieldId];
|
|
537
|
+
if (!field) return null;
|
|
538
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
539
|
+
direction: "row",
|
|
540
|
+
spacing: 1,
|
|
541
|
+
sx: {
|
|
542
|
+
alignItems: 'center',
|
|
543
|
+
px: 1,
|
|
544
|
+
py: 0.5,
|
|
545
|
+
border: '1px solid',
|
|
546
|
+
borderColor: 'divider',
|
|
547
|
+
borderRadius: 1
|
|
548
|
+
},
|
|
549
|
+
children: [
|
|
550
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
551
|
+
sx: {
|
|
552
|
+
flex: 1,
|
|
553
|
+
minWidth: 0
|
|
554
|
+
},
|
|
555
|
+
children: [
|
|
556
|
+
/*#__PURE__*/ _jsxs(Typography, {
|
|
557
|
+
variant: "body2",
|
|
558
|
+
noWrap: true,
|
|
559
|
+
children: [
|
|
560
|
+
field.name,
|
|
561
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
562
|
+
component: "span",
|
|
563
|
+
variant: "caption",
|
|
564
|
+
color: "text.secondary",
|
|
565
|
+
children: ` · ${fieldId}`
|
|
566
|
+
})
|
|
567
|
+
]
|
|
568
|
+
}),
|
|
569
|
+
field.description ? /*#__PURE__*/ _jsx(Typography, {
|
|
570
|
+
variant: "caption",
|
|
571
|
+
color: "text.secondary",
|
|
572
|
+
noWrap: true,
|
|
573
|
+
children: field.description
|
|
574
|
+
}) : null
|
|
575
|
+
]
|
|
576
|
+
}),
|
|
577
|
+
/*#__PURE__*/ _jsx(Chip, {
|
|
578
|
+
size: "small",
|
|
579
|
+
label: (_DATASET_FIELD_TYPE_LABELS_field_type = DATASET_FIELD_TYPE_LABELS[field.type]) != null ? _DATASET_FIELD_TYPE_LABELS_field_type : field.type
|
|
580
|
+
}),
|
|
581
|
+
field.required || ((_field_validation = field.validation) == null ? void 0 : _field_validation.required) ? /*#__PURE__*/ _jsx(Chip, {
|
|
582
|
+
size: "small",
|
|
583
|
+
color: "primary",
|
|
584
|
+
label: "required"
|
|
585
|
+
}) : null,
|
|
586
|
+
/*#__PURE__*/ _jsx(IconButton, {
|
|
587
|
+
size: "small",
|
|
588
|
+
disabled: index === 0,
|
|
589
|
+
onClick: moveField(fieldId, -1),
|
|
590
|
+
"aria-label": "Move up",
|
|
591
|
+
children: '▲'
|
|
592
|
+
}),
|
|
593
|
+
/*#__PURE__*/ _jsx(IconButton, {
|
|
594
|
+
size: "small",
|
|
595
|
+
disabled: index === model.order.length - 1,
|
|
596
|
+
onClick: moveField(fieldId, 1),
|
|
597
|
+
"aria-label": "Move down",
|
|
598
|
+
children: '▼'
|
|
599
|
+
}),
|
|
600
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
601
|
+
size: "small",
|
|
602
|
+
onClick: openFieldEditor(fieldId),
|
|
603
|
+
children: 'Edit'
|
|
604
|
+
}),
|
|
605
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
606
|
+
size: "small",
|
|
607
|
+
color: "error",
|
|
608
|
+
onClick: handleFieldRemove(fieldId),
|
|
609
|
+
children: 'Remove'
|
|
610
|
+
})
|
|
611
|
+
]
|
|
612
|
+
}, fieldId);
|
|
613
|
+
})
|
|
614
|
+
}),
|
|
615
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
616
|
+
size: "small",
|
|
617
|
+
variant: "outlined",
|
|
618
|
+
color: "primary",
|
|
619
|
+
onClick: openFieldEditor(null),
|
|
620
|
+
sx: {
|
|
621
|
+
alignSelf: 'flex-start'
|
|
622
|
+
},
|
|
623
|
+
children: 'Add field'
|
|
624
|
+
})
|
|
625
|
+
]
|
|
626
|
+
}),
|
|
627
|
+
/*#__PURE__*/ _jsxs(DialogActions, {
|
|
628
|
+
children: [
|
|
629
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
630
|
+
onClick: onClose,
|
|
631
|
+
children: 'Cancel'
|
|
632
|
+
}),
|
|
633
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
634
|
+
variant: "contained",
|
|
635
|
+
color: "primary",
|
|
636
|
+
disabled: !dataScope,
|
|
637
|
+
onClick: handleSave,
|
|
638
|
+
children: 'Save schema'
|
|
639
|
+
})
|
|
640
|
+
]
|
|
641
|
+
})
|
|
642
|
+
]
|
|
643
|
+
}),
|
|
644
|
+
/*#__PURE__*/ _jsxs(Dialog, {
|
|
645
|
+
open: Boolean(fieldEditor),
|
|
646
|
+
onClose: ()=>setFieldEditor(null),
|
|
647
|
+
maxWidth: "xs",
|
|
648
|
+
fullWidth: true,
|
|
649
|
+
children: [
|
|
650
|
+
/*#__PURE__*/ _jsx(DialogTitle, {
|
|
651
|
+
children: (fieldEditor == null ? void 0 : fieldEditor.fieldId) ? 'Edit field' : 'New field'
|
|
652
|
+
}),
|
|
653
|
+
/*#__PURE__*/ _jsxs(DialogContent, {
|
|
654
|
+
sx: {
|
|
655
|
+
display: 'flex',
|
|
656
|
+
flexDirection: 'column',
|
|
657
|
+
gap: 2
|
|
658
|
+
},
|
|
659
|
+
children: [
|
|
660
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
661
|
+
size: "small",
|
|
662
|
+
label: "Display name",
|
|
663
|
+
value: (_ref1 = editorDefinition == null ? void 0 : editorDefinition.name) != null ? _ref1 : '',
|
|
664
|
+
autoFocus: true,
|
|
665
|
+
onChange: (event)=>{
|
|
666
|
+
const name = event.target.value;
|
|
667
|
+
setFieldEditor((prev)=>{
|
|
668
|
+
if (!prev) return prev;
|
|
669
|
+
const next = _extends({}, prev, {
|
|
670
|
+
definition: _extends({}, prev.definition, {
|
|
671
|
+
name
|
|
672
|
+
})
|
|
673
|
+
});
|
|
674
|
+
// The reference id follows the display name until the user
|
|
675
|
+
// edits it (new fields only) — AGL-578.
|
|
676
|
+
if (prev.fieldId == null && !prev.idTouched) {
|
|
677
|
+
next.idDraft = defaultDatasetFieldId(name, new Set(model.order.map((id)=>id.toLowerCase())));
|
|
678
|
+
}
|
|
679
|
+
return next;
|
|
680
|
+
});
|
|
681
|
+
},
|
|
682
|
+
sx: {
|
|
683
|
+
mt: 1
|
|
684
|
+
},
|
|
685
|
+
helperText: "Shown to people editing records and wherever the field appears"
|
|
686
|
+
}),
|
|
687
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
688
|
+
size: "small",
|
|
689
|
+
label: "Reference ID",
|
|
690
|
+
value: isNewField ? (_ref2 = fieldEditor == null ? void 0 : fieldEditor.idDraft) != null ? _ref2 : '' : (_ref3 = fieldEditor == null ? void 0 : fieldEditor.fieldId) != null ? _ref3 : '',
|
|
691
|
+
disabled: !isNewField,
|
|
692
|
+
onChange: (event)=>setFieldEditor((prev)=>prev ? _extends({}, prev, {
|
|
693
|
+
idDraft: event.target.value,
|
|
694
|
+
idTouched: true
|
|
695
|
+
}) : prev),
|
|
696
|
+
error: Boolean(fieldIdError),
|
|
697
|
+
helperText: isNewField ? fieldIdError != null ? fieldIdError : 'Stable key used in bindings ({{item.id}}), forms, and imports. Auto-filled from the display name — edit to override. Fixed once the field is created.' : 'Reference IDs are stable keys and cannot change after creation'
|
|
698
|
+
}),
|
|
699
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
700
|
+
size: "small",
|
|
701
|
+
label: "Description",
|
|
702
|
+
multiline: true,
|
|
703
|
+
minRows: 2,
|
|
704
|
+
value: (_ref4 = editorDefinition == null ? void 0 : editorDefinition.description) != null ? _ref4 : '',
|
|
705
|
+
onChange: (event)=>setFieldEditor((prev)=>prev ? _extends({}, prev, {
|
|
706
|
+
definition: _extends({}, prev.definition, {
|
|
707
|
+
description: event.target.value
|
|
708
|
+
})
|
|
709
|
+
}) : prev),
|
|
710
|
+
helperText: "What this field is for — shown as a hint wherever the field appears"
|
|
711
|
+
}),
|
|
712
|
+
/*#__PURE__*/ _jsxs(TextField, {
|
|
713
|
+
select: true,
|
|
714
|
+
size: "small",
|
|
715
|
+
label: "Type",
|
|
716
|
+
value: (editorDefinition == null ? void 0 : editorDefinition.customType) ? `custom:${editorDefinition.customType}` : (_ref5 = editorDefinition == null ? void 0 : editorDefinition.type) != null ? _ref5 : 'text',
|
|
717
|
+
onChange: (event)=>setFieldEditor((prev)=>{
|
|
718
|
+
if (!prev) return prev;
|
|
719
|
+
const selected = event.target.value;
|
|
720
|
+
// Custom field types (AGL-434): ride a base storage type
|
|
721
|
+
// and stamp customType so the record editor + validators
|
|
722
|
+
// pick up the plugin behavior.
|
|
723
|
+
if (selected.startsWith('custom:')) {
|
|
724
|
+
const fieldType = getCustomFieldType(selected.slice(7));
|
|
725
|
+
if (!fieldType) return prev;
|
|
726
|
+
return _extends({}, prev, {
|
|
727
|
+
definition: _extends({}, prev.definition, {
|
|
728
|
+
type: fieldType.baseType,
|
|
729
|
+
customType: fieldType.name
|
|
730
|
+
})
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
const definition = _extends({}, prev.definition, {
|
|
734
|
+
type: selected
|
|
735
|
+
});
|
|
736
|
+
delete definition.customType;
|
|
737
|
+
return _extends({}, prev, {
|
|
738
|
+
definition
|
|
739
|
+
});
|
|
740
|
+
}),
|
|
741
|
+
children: [
|
|
742
|
+
((fieldEditor == null ? void 0 : fieldEditor.fieldId) && editorDefinition && !AUTHORABLE_TYPES.includes(editorDefinition.type) ? DATASET_FIELD_TYPES : AUTHORABLE_TYPES).map((type)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
743
|
+
value: type,
|
|
744
|
+
children: DATASET_FIELD_TYPE_LABELS[type]
|
|
745
|
+
}, type)),
|
|
746
|
+
listCustomFieldTypes().map((fieldType)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
747
|
+
value: `custom:${fieldType.name}`,
|
|
748
|
+
children: `${fieldType.label} (${fieldType.pluginId})`
|
|
749
|
+
}, `custom:${fieldType.name}`))
|
|
750
|
+
]
|
|
751
|
+
}),
|
|
752
|
+
(editorDefinition == null ? void 0 : editorDefinition.type) === 'reference' ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
753
|
+
children: [
|
|
754
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
755
|
+
select: true,
|
|
756
|
+
size: "small",
|
|
757
|
+
label: "Target collection",
|
|
758
|
+
value: (_ref6 = editorDefinition == null ? void 0 : (_editorDefinition_reference = editorDefinition.reference) == null ? void 0 : _editorDefinition_reference.datasetId) != null ? _ref6 : '',
|
|
759
|
+
onChange: (event)=>setFieldEditor((prev)=>{
|
|
760
|
+
var _prev_definition_reference;
|
|
761
|
+
return prev ? _extends({}, prev, {
|
|
762
|
+
definition: _extends({}, prev.definition, {
|
|
763
|
+
reference: _extends({}, (_prev_definition_reference = prev.definition.reference) != null ? _prev_definition_reference : {
|
|
764
|
+
datasetId: ''
|
|
765
|
+
}, {
|
|
766
|
+
datasetId: event.target.value
|
|
767
|
+
})
|
|
768
|
+
})
|
|
769
|
+
}) : prev;
|
|
770
|
+
}),
|
|
771
|
+
children: (datasets != null ? datasets : []).filter((target)=>target.$id !== (dataset == null ? void 0 : dataset.$id))// Only targets this dataset's sites can actually resolve
|
|
772
|
+
// (AGL-1044). A reference to a dataset a site cannot see
|
|
773
|
+
// renders blank with nothing to say why, so an
|
|
774
|
+
// unresolvable target is not offered. The currently
|
|
775
|
+
// selected one always stays listed — otherwise changing
|
|
776
|
+
// scope would silently blank an existing field.
|
|
777
|
+
.filter((target)=>{
|
|
778
|
+
var _editorDefinition_reference;
|
|
779
|
+
return target.$id === (editorDefinition == null ? void 0 : (_editorDefinition_reference = editorDefinition.reference) == null ? void 0 : _editorDefinition_reference.datasetId) || scopeCovers(target.visibleTo, visibleTo);
|
|
780
|
+
}).map((target)=>{
|
|
781
|
+
var _target_displayName;
|
|
782
|
+
return /*#__PURE__*/ _jsx(MenuItem, {
|
|
783
|
+
value: target.$id,
|
|
784
|
+
children: (_target_displayName = target.displayName) != null ? _target_displayName : target.$id
|
|
785
|
+
}, target.$id);
|
|
786
|
+
})
|
|
787
|
+
}),
|
|
788
|
+
(()=>{
|
|
789
|
+
var _target_displayName;
|
|
790
|
+
var _editorDefinition_reference;
|
|
791
|
+
// Flag an EXISTING reference that this dataset's scope has
|
|
792
|
+
// outgrown — the field was valid when created and quietly
|
|
793
|
+
// is not any more.
|
|
794
|
+
const targetId = editorDefinition == null ? void 0 : (_editorDefinition_reference = editorDefinition.reference) == null ? void 0 : _editorDefinition_reference.datasetId;
|
|
795
|
+
const target = (datasets != null ? datasets : []).find((d)=>d.$id === targetId);
|
|
796
|
+
if (!target || scopeCovers(target.visibleTo, visibleTo)) {
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
return /*#__PURE__*/ _jsx(Alert, {
|
|
800
|
+
severity: "warning",
|
|
801
|
+
children: `"${(_target_displayName = target.displayName) != null ? _target_displayName : target.$id}" is not shared with ` + 'every site this collection is. Those sites will show ' + 'this field blank. Widen its sharing, or narrow this ' + "collection's."
|
|
802
|
+
});
|
|
803
|
+
})(),
|
|
804
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
805
|
+
select: true,
|
|
806
|
+
size: "small",
|
|
807
|
+
label: "Display field",
|
|
808
|
+
value: (_ref7 = editorDefinition == null ? void 0 : (_editorDefinition_reference1 = editorDefinition.reference) == null ? void 0 : _editorDefinition_reference1.displayFieldId) != null ? _ref7 : '',
|
|
809
|
+
onChange: (event)=>setFieldEditor((prev)=>(prev == null ? void 0 : prev.definition.reference) ? _extends({}, prev, {
|
|
810
|
+
definition: _extends({}, prev.definition, {
|
|
811
|
+
reference: _extends({}, prev.definition.reference, {
|
|
812
|
+
displayFieldId: event.target.value
|
|
813
|
+
})
|
|
814
|
+
})
|
|
815
|
+
}) : prev),
|
|
816
|
+
helperText: "Shown in pickers and grid cells",
|
|
817
|
+
children: effectiveDatasetModel((_find = (datasets != null ? datasets : []).find((target)=>{
|
|
818
|
+
var _editorDefinition_reference;
|
|
819
|
+
return target.$id === (editorDefinition == null ? void 0 : (_editorDefinition_reference = editorDefinition.reference) == null ? void 0 : _editorDefinition_reference.datasetId);
|
|
820
|
+
})) != null ? _find : {}).order.map((targetFieldId)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
821
|
+
value: targetFieldId,
|
|
822
|
+
children: targetFieldId
|
|
823
|
+
}, targetFieldId))
|
|
824
|
+
}),
|
|
825
|
+
/*#__PURE__*/ _jsxs(TextField, {
|
|
826
|
+
select: true,
|
|
827
|
+
size: "small",
|
|
828
|
+
label: "When a referenced document is deleted",
|
|
829
|
+
value: (_ref8 = editorDefinition == null ? void 0 : (_editorDefinition_reference2 = editorDefinition.reference) == null ? void 0 : _editorDefinition_reference2.onDelete) != null ? _ref8 : 'setNull',
|
|
830
|
+
onChange: (event)=>setFieldEditor((prev)=>(prev == null ? void 0 : prev.definition.reference) ? _extends({}, prev, {
|
|
831
|
+
definition: _extends({}, prev.definition, {
|
|
832
|
+
reference: _extends({}, prev.definition.reference, {
|
|
833
|
+
onDelete: event.target.value
|
|
834
|
+
})
|
|
835
|
+
})
|
|
836
|
+
}) : prev),
|
|
837
|
+
children: [
|
|
838
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
839
|
+
value: "setNull",
|
|
840
|
+
children: 'Clear the reference'
|
|
841
|
+
}),
|
|
842
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
843
|
+
value: "restrict",
|
|
844
|
+
children: 'Block the delete'
|
|
845
|
+
})
|
|
846
|
+
]
|
|
847
|
+
}),
|
|
848
|
+
/*#__PURE__*/ _jsx(FormControlLabel, {
|
|
849
|
+
control: /*#__PURE__*/ _jsx(Checkbox, {
|
|
850
|
+
size: "small",
|
|
851
|
+
checked: Boolean(editorDefinition == null ? void 0 : (_editorDefinition_reference3 = editorDefinition.reference) == null ? void 0 : _editorDefinition_reference3.multiple),
|
|
852
|
+
onChange: (event)=>setFieldEditor((prev)=>(prev == null ? void 0 : prev.definition.reference) ? _extends({}, prev, {
|
|
853
|
+
definition: _extends({}, prev.definition, {
|
|
854
|
+
reference: _extends({}, prev.definition.reference, {
|
|
855
|
+
multiple: event.target.checked
|
|
856
|
+
})
|
|
857
|
+
})
|
|
858
|
+
}) : prev)
|
|
859
|
+
}),
|
|
860
|
+
label: "Allow multiple (many-to-many)"
|
|
861
|
+
})
|
|
862
|
+
]
|
|
863
|
+
}) : null,
|
|
864
|
+
/*#__PURE__*/ _jsx(FormControlLabel, {
|
|
865
|
+
control: /*#__PURE__*/ _jsx(Checkbox, {
|
|
866
|
+
size: "small",
|
|
867
|
+
checked: Boolean(editorDefinition == null ? void 0 : editorDefinition.required),
|
|
868
|
+
onChange: (event)=>setFieldEditor((prev)=>prev ? _extends({}, prev, {
|
|
869
|
+
definition: _extends({}, prev.definition, {
|
|
870
|
+
required: event.target.checked
|
|
871
|
+
})
|
|
872
|
+
}) : prev)
|
|
873
|
+
}),
|
|
874
|
+
label: "Required"
|
|
875
|
+
}),
|
|
876
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
877
|
+
size: "small",
|
|
878
|
+
label: "Default value",
|
|
879
|
+
value: String((_ref9 = editorDefinition == null ? void 0 : editorDefinition.default) != null ? _ref9 : ''),
|
|
880
|
+
onChange: (event)=>setFieldEditor((prev)=>prev ? _extends({}, prev, {
|
|
881
|
+
definition: _extends({}, prev.definition, event.target.value ? {
|
|
882
|
+
default: event.target.value
|
|
883
|
+
} : {
|
|
884
|
+
default: undefined
|
|
885
|
+
})
|
|
886
|
+
}) : prev),
|
|
887
|
+
helperText: "Pre-filled when creating documents"
|
|
888
|
+
}),
|
|
889
|
+
(editorDefinition == null ? void 0 : editorDefinition.type) === 'text' ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
890
|
+
children: [
|
|
891
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
892
|
+
size: "small",
|
|
893
|
+
label: "Pattern (regex)",
|
|
894
|
+
value: (_ref10 = editorDefinition == null ? void 0 : (_editorDefinition_validation = editorDefinition.validation) == null ? void 0 : _editorDefinition_validation.regex) != null ? _ref10 : '',
|
|
895
|
+
onChange: (event)=>setFieldEditor((prev)=>{
|
|
896
|
+
var _prev_definition_validation;
|
|
897
|
+
return prev ? _extends({}, prev, {
|
|
898
|
+
definition: _extends({}, prev.definition, {
|
|
899
|
+
validation: _extends({}, (_prev_definition_validation = prev.definition.validation) != null ? _prev_definition_validation : {}, event.target.value ? {
|
|
900
|
+
regex: event.target.value
|
|
901
|
+
} : {
|
|
902
|
+
regex: undefined
|
|
903
|
+
})
|
|
904
|
+
})
|
|
905
|
+
}) : prev;
|
|
906
|
+
})
|
|
907
|
+
}),
|
|
908
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
909
|
+
size: "small",
|
|
910
|
+
label: "Options",
|
|
911
|
+
value: (_ref11 = fieldEditor == null ? void 0 : fieldEditor.optionsText) != null ? _ref11 : '',
|
|
912
|
+
onChange: (event)=>setFieldEditor((prev)=>prev ? _extends({}, prev, {
|
|
913
|
+
optionsText: event.target.value
|
|
914
|
+
}) : prev),
|
|
915
|
+
helperText: "Comma-separated enum, e.g. basic, plus"
|
|
916
|
+
})
|
|
917
|
+
]
|
|
918
|
+
}) : null,
|
|
919
|
+
editorDefinition && [
|
|
920
|
+
'int32',
|
|
921
|
+
'int64',
|
|
922
|
+
'float',
|
|
923
|
+
'timestamp',
|
|
924
|
+
'text'
|
|
925
|
+
].includes(editorDefinition.type) ? /*#__PURE__*/ _jsx(Stack, {
|
|
926
|
+
direction: "row",
|
|
927
|
+
spacing: 1,
|
|
928
|
+
children: [
|
|
929
|
+
'min',
|
|
930
|
+
'max'
|
|
931
|
+
].map((bound)=>{
|
|
932
|
+
var _ref;
|
|
933
|
+
var _editorDefinition_validation;
|
|
934
|
+
return /*#__PURE__*/ _jsx(TextField, {
|
|
935
|
+
size: "small",
|
|
936
|
+
type: "number",
|
|
937
|
+
label: editorDefinition.type === 'text' ? `${bound === 'min' ? 'Min' : 'Max'} length` : bound === 'min' ? 'Min' : 'Max',
|
|
938
|
+
value: (_ref = (_editorDefinition_validation = editorDefinition.validation) == null ? void 0 : _editorDefinition_validation[bound]) != null ? _ref : '',
|
|
939
|
+
onChange: (event)=>setFieldEditor((prev)=>{
|
|
940
|
+
var _prev_definition_validation;
|
|
941
|
+
return prev ? _extends({}, prev, {
|
|
942
|
+
definition: _extends({}, prev.definition, {
|
|
943
|
+
validation: _extends({}, (_prev_definition_validation = prev.definition.validation) != null ? _prev_definition_validation : {}, event.target.value === '' ? {
|
|
944
|
+
[bound]: undefined
|
|
945
|
+
} : {
|
|
946
|
+
[bound]: Number(event.target.value)
|
|
947
|
+
})
|
|
948
|
+
})
|
|
949
|
+
}) : prev;
|
|
950
|
+
})
|
|
951
|
+
}, bound);
|
|
952
|
+
})
|
|
953
|
+
}) : null
|
|
954
|
+
]
|
|
955
|
+
}),
|
|
956
|
+
/*#__PURE__*/ _jsxs(DialogActions, {
|
|
957
|
+
children: [
|
|
958
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
959
|
+
onClick: ()=>setFieldEditor(null),
|
|
960
|
+
children: 'Cancel'
|
|
961
|
+
}),
|
|
962
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
963
|
+
variant: "contained",
|
|
964
|
+
color: "primary",
|
|
965
|
+
disabled: !(editorDefinition == null ? void 0 : editorDefinition.name.trim()) || Boolean(fieldIdError),
|
|
966
|
+
onClick: handleFieldSave,
|
|
967
|
+
children: (fieldEditor == null ? void 0 : fieldEditor.fieldId) ? 'Save field' : 'Add field'
|
|
968
|
+
})
|
|
969
|
+
]
|
|
970
|
+
})
|
|
971
|
+
]
|
|
972
|
+
})
|
|
973
|
+
]
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
DatasetSchemaDialog.displayName = 'DatasetSchemaDialog';
|
|
977
|
+
export default DatasetSchemaDialog;
|
|
978
|
+
|
|
979
|
+
//# sourceMappingURL=dataset-schema-dialog.component.js.map
|