1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
|
import { $mobx, action, observable, runInAction, trace, values } from 'mobx';
import { computedFn } from 'mobx-utils';
import { returnZero } from '../Utils';
import { DocServer } from '../client/DocServer';
import { LinkManager } from '../client/util/LinkManager';
import { SerializationHelper } from '../client/util/SerializationHelper';
import { UndoManager } from '../client/util/UndoManager';
import { Doc, DocListCast, Field, FieldResult, HierarchyMapping, ReverseHierarchyMap, StrListCast, aclLevel, updateCachedAcls } from './Doc';
import { AclAdmin, AclAugment, AclEdit, AclPrivate, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, UpdatingFromServer, Width } from './DocSymbols';
import { FieldChanged, Id, Parent, ToValue } from './FieldSymbols';
import { List } from './List';
import { ObjectField } from './ObjectField';
import { PrefetchProxy, ProxyField } from './Proxy';
import { RefField } from './RefField';
import { RichTextField } from './RichTextField';
import { SchemaHeaderField } from './SchemaHeaderField';
import { ComputedField } from './ScriptField';
import { DocCast, ScriptCast, StrCast } from './Types';
import { BaseException } from 'pdfjs-dist/types/src/shared/util';
function _readOnlySetter(): never {
throw new Error("Documents can't be modified in read-only mode");
}
var tracing = false;
export function TraceMobx() {
tracing && trace();
}
const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
if (SerializationHelper.IsSerializing() || typeof prop === 'symbol') {
target[prop] = value;
return true;
}
value = value?.[SelfProxy] ?? value; // convert any Doc type values to Proxy's
const curValue = target.__fieldTuples[prop];
if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) {
// TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically
// curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way
return true;
}
if (value instanceof RefField) {
value = new ProxyField(value);
}
if (value instanceof ObjectField) {
if (value[Parent] && value[Parent] !== receiver && !(value instanceof PrefetchProxy)) {
throw new Error("Can't put the same object in multiple documents at the same time");
}
value[Parent] = receiver;
value[FieldChanged] = containedFieldChangedHandler(receiver, prop, value);
}
if (curValue instanceof ObjectField) {
delete curValue[Parent];
delete curValue[FieldChanged];
}
if (typeof prop === 'string' && _propSetterCB.has(prop)) _propSetterCB.get(prop)!(target[SelfProxy], value);
const effectiveAcl = GetEffectiveAcl(target);
const writeMode = DocServer.getFieldWriteMode(prop as string);
const fromServer = target[UpdatingFromServer];
const sameAuthor = fromServer || receiver.author === Doc.CurrentUserEmail;
const writeToDoc =
sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Playground || writeMode === DocServer.WriteMode.LivePlayground || (effectiveAcl === AclAugment && value instanceof RichTextField);
const writeToServer =
!DocServer.Control.isReadOnly() && //
(sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (effectiveAcl === AclAugment && value instanceof RichTextField));
if (writeToDoc) {
if (value === undefined) {
target[FieldKeys] && delete target[FieldKeys][prop]; // Lists don't have a FieldKeys field
delete target.__fieldTuples[prop];
} else {
// bcz: uncomment to see if server is being updated
// console.log(prop + ' = ' + value + '(' + curValue + ')');
target[FieldKeys] && (target[FieldKeys][prop] = true); // Lists don't have a FieldKeys field
target.__fieldTuples[prop] = value;
}
if (writeToServer) {
// prettier-ignore
if (value === undefined)
(target as Doc|ObjectField)[FieldChanged]?.(undefined, { $unset: { ['fields.' + prop]: '' } });
else (target as Doc|ObjectField)[FieldChanged]?.(undefined, { $set: { ['fields.' + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) :value}});
if (prop === 'author' || prop.toString().startsWith('acl')) updateCachedAcls(target);
} else {
DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue);
}
!receiver[Initializing] &&
!StrListCast(receiver.undoIgnoreFields).includes(prop.toString()) &&
(!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) &&
UndoManager.AddEvent(
{
redo: () => (receiver[prop] = value),
undo: () => {
const wasUpdate = receiver[UpdatingFromServer];
const wasForce = receiver[ForceServerWrite];
receiver[ForceServerWrite] = true; // needed since writes aren't propagated to server if UpdatingFromServerIsSet
receiver[UpdatingFromServer] = true; // needed if the event caused ACL's to change such that the doc is otherwise no longer editable.
receiver[prop] = curValue;
receiver[ForceServerWrite] = wasForce;
receiver[UpdatingFromServer] = wasUpdate;
},
prop: prop?.toString(),
},
value
);
return true;
}
return true;
});
let _setter: (target: any, prop: string | symbol | number, value: any, receiver: any) => boolean = _setterImpl;
export function makeReadOnly() {
_setter = _readOnlySetter;
}
export function makeEditable() {
_setter = _setterImpl;
}
export function normalizeEmail(email: string) {
return email.replace(/\./g, '__');
}
export function denormalizeEmail(email: string) {
return email.replace(/__/g, '.');
}
/**
* Copies parent's acl fields to the child
*/
export function inheritParentAcls(parent: Doc, child: Doc, layoutOnly: boolean) {
[...Object.keys(parent), ...(Doc.CurrentUserEmail !== parent.author ? ['acl-Owner'] : [])]
.filter(key => key.startsWith('acl'))
.forEach(key => {
// if the default acl mode is private, then don't inherit the acl-guest permission, but set it to private.
// const permission: string = key === 'acl-guest' && Doc.defaultAclPrivate ? AclPrivate : parent[key];
const parAcl = ReverseHierarchyMap.get(StrCast(key === 'acl-Owner' ? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Edit) : parent[key]))?.acl;
if (parAcl) {
const sharePermission = HierarchyMapping.get(parAcl)?.name;
sharePermission && distributeAcls(key === 'acl-Owner' ? `acl-${normalizeEmail(StrCast(parent.author))}` : key, sharePermission, child, undefined, false, layoutOnly);
}
});
}
/**
* These are the various levels of access a user can have to a document.
*
* Admin: a user with admin access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), as well as change others' access rights to that document.
*
* Edit: a user with edit access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), but not change any access rights to that document.
*
* Add: a user with add access to a document can augment documents/annotations to that document but cannot edit or delete anything.
*
* View: a user with view access to a document can only view it - they cannot add/remove/edit anything.
*
* None: the document is not shared with that user.
*
* Unset: Remove a sharing permission (eg., used )
*/
export enum SharingPermissions {
Admin = 'Admin',
Edit = 'Edit',
Augment = 'Augment',
View = 'View',
None = 'Not-Shared',
}
// return acl from cache or cache the acl and return.
const getEffectiveAclCache = computedFn(function (target: any, user?: string) {
return getEffectiveAcl(target, user);
}, true);
/**
* Calculates the effective access right to a document for the current user.
*/
export function GetEffectiveAcl(target: any, user?: string): symbol {
if (!target) return AclPrivate;
if (target[UpdatingFromServer] || Doc.CurrentUserEmail === 'guest') return AclAdmin;
return getEffectiveAclCache(target, user); // all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable)
}
export function GetPropAcl(target: any, prop: string | symbol | number) {
if (typeof prop === 'symbol' || target[UpdatingFromServer]) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent
if (prop && DocServer.IsPlaygroundField(prop.toString())) return AclEdit; // playground props are always editable
return GetEffectiveAcl(target);
}
let cachedGroups = observable([] as string[]);
const getCachedGroupByNameCache = computedFn(function (name: string) {
return cachedGroups.includes(name);
}, true);
export function GetCachedGroupByName(name: string) {
return getCachedGroupByNameCache(name);
}
export function SetCachedGroups(groups: string[]) {
runInAction(() => cachedGroups.push(...groups));
}
function getEffectiveAcl(target: any, user?: string): symbol {
const targetAcls = target[DocAcl];
if (targetAcls?.['acl-Me'] === AclAdmin || GetCachedGroupByName('Admin')) return AclAdmin;
const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group
if (targetAcls && Object.keys(targetAcls).length) {
let effectiveAcl = AclPrivate;
for (const [key, value] of Object.entries(targetAcls)) {
// there are issues with storing fields with . in the name, so they are replaced with _ during creation
// as a result we need to restore them again during this comparison.
const entity = denormalizeEmail(key.substring(4)); // an individual or a group
if (GetCachedGroupByName(entity) || userChecked === entity || entity === 'Me') {
if (HierarchyMapping.get(value as symbol)!.level > HierarchyMapping.get(effectiveAcl)!.level) {
effectiveAcl = value as symbol;
}
}
}
return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)!.level < aclLevel.editable ? AclEdit : effectiveAcl;
}
// authored documents are private until an ACL is set.
const targetAuthor = target.__fieldTuples?.author || target.author; // target may be a Doc of Proxy, so check __fieldTuples.author and .author
if (targetAuthor && targetAuthor !== userChecked) return AclPrivate;
return AclAdmin;
}
/**
* Recursively distributes the access right for a user across the children of a document and its annotations.
* @param key the key storing the access right (e.g. acl-groupname)
* @param acl the access right being stored (e.g. "Can Edit")
* @param target the document on which this access right is being set
* @param visited list of Doc's already distributed to.
* @param allowUpgrade whether permissions can be made less restrictive
* @param layoutOnly just sets the layout doc's ACL (unless the data doc has no entry for the ACL, in which case it will be set as well)
*/
export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, visited?: Doc[], allowUpgrade?: boolean, layoutOnly = false) {
const selfKey = `acl-${normalizeEmail(Doc.CurrentUserEmail)}`;
if (!visited) visited = [] as Doc[];
if (!target || visited.includes(target) || key === selfKey) return;
visited.push(target);
let dataDocChanged = false;
const dataDoc = target[DocData];
const curVal = ReverseHierarchyMap.get(StrCast(dataDoc[key]))?.level ?? 0;
const aclVal = ReverseHierarchyMap.get(acl)?.level ?? 0;
if (!layoutOnly && dataDoc && (allowUpgrade !== false || !dataDoc[key] || curVal > aclVal)) {
// propagate ACLs to links, children, and annotations
LinkManager.Links(dataDoc).forEach(link => distributeAcls(key, acl, link, visited, allowUpgrade ? true : false));
DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).forEach(d => {
distributeAcls(key, acl, d, visited, allowUpgrade ? true : false);
d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, allowUpgrade ? true : false);
});
DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '_annotations']).forEach(d => {
distributeAcls(key, acl, d, visited, allowUpgrade ? true : false);
d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, allowUpgrade ? true : false);
});
Object.keys(target) // share expanded layout templates (eg, for presElementBox'es )
.filter(lkey => lkey.includes('layout[') && DocCast(target[lkey]))
.map(lkey => {
distributeAcls(key, acl, DocCast(target[lkey]), visited, allowUpgrade ? true : false);
});
if (GetEffectiveAcl(dataDoc) === AclAdmin) {
dataDoc[key] = acl;
dataDocChanged = true;
}
}
let layoutDocChanged = false; // determines whether fetchProto should be called or not (i.e. is there a change that should be reflected in target[AclSym])
// if it is inheriting from a collection, it only inherits if A) allowUpgrade is set B) the key doesn't already exist or c) the right being inherited is more restrictive
if (GetEffectiveAcl(target) === AclAdmin && (allowUpgrade || !Doc.GetT(target, key, 'boolean', true) || ReverseHierarchyMap.get(StrCast(target[key]))!.level > aclVal)) {
target[key] = acl;
layoutDocChanged = true;
if (dataDoc[key] === undefined) dataDoc[key] = acl;
}
layoutDocChanged && updateCachedAcls(target); // updates target[AclSym] when changes to acls have been made
dataDocChanged && updateCachedAcls(dataDoc);
}
export var _propSetterCB = new Map<string, ((target: any, value: any) => void) | undefined>();
/**
* sets a callback function to be called whenever a value is assigned to the specified field key.
* For example, this is used to "publish" documents with titles that start with '@'
* @param prop
* @param setter
*/
export function SetPropSetterCb(prop: string, setter: ((target: any, value: any) => void) | undefined) {
_propSetterCB.set(prop, setter);
}
//
// target should be either a Doc or ListImpl. receiver should be a Proxy<Doc> Or List.
//
export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
if (!in_prop) {
console.log('WARNING: trying to set an empty property. This should be fixed. ');
return false;
}
let prop = in_prop;
const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : GetPropAcl(target, prop);
if (effectiveAcl !== AclEdit && effectiveAcl !== AclAugment && effectiveAcl !== AclAdmin) return true;
// if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't
if (typeof prop === 'string' && prop.startsWith('acl') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true;
if (typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('_')) {
if (!prop.startsWith('__')) prop = prop.substring(1);
if (target.__LAYOUT__) {
target.__LAYOUT__[prop] = value;
return true;
}
}
if (target.__fieldTuples[prop] instanceof ComputedField) {
if (target.__fieldTuples[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) {
return ScriptCast(target.__fieldTuples[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success ? true : false;
}
}
return _setter(target, prop, value, receiver);
}
export function getter(target: any, prop: string | symbol, proxy: any): any {
// prettier-ignore
switch (prop) {
case 'then' : return undefined;
case 'constructor': case 'toString': case 'valueOf':
case 'serializeInfo': case 'factory':
return target[prop];
case DocAcl : return target[DocAcl];
case $mobx: return target.__fieldTuples[prop];
case DocLayout: return target.__LAYOUT__;
case Height: case Width: if (GetEffectiveAcl(target) === AclPrivate) return returnZero;
default :
if (typeof prop === 'symbol') return target[prop];
if (prop.startsWith('isMobX')) return target[prop];
if (prop.startsWith('__')) return target[prop];
if (GetEffectiveAcl(target) === AclPrivate && prop !== 'author') return undefined;
}
const layout_prop = prop.startsWith('_') ? prop.substring(1) : undefined;
if (layout_prop && target.__LAYOUT__) return target.__LAYOUT__[layout_prop];
return getFieldImpl(target, layout_prop ?? prop, proxy);
}
function getFieldImpl(target: any, prop: string | number, proxy: any, ignoreProto: boolean = false): any {
const field = target.__fieldTuples[prop];
const value = field?.[ToValue]?.(proxy); // converts ComputedFields to values, or unpacks ProxyFields into Proxys
if (value) return value.value;
if (field === undefined && !ignoreProto && prop !== 'proto') {
const proto = getFieldImpl(target, 'proto', proxy, true); //TODO tfs: instead of proxy we could use target[SelfProxy]... I don't which semantics we want or if it really matters
if (proto instanceof Doc && GetEffectiveAcl(proto) !== AclPrivate) {
return getFieldImpl(proto, prop, proxy, ignoreProto);
}
}
return field;
}
export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any {
return getFieldImpl(target, prop, target[SelfProxy], ignoreProto);
}
export function deleteProperty(target: any, prop: string | number | symbol) {
if (typeof prop === 'symbol') {
delete target[prop];
} else {
target[SelfProxy][prop] = undefined;
}
return true;
}
// this function creates a function that can be used to setup Undo for whenever an ObjectField changes.
// the idea is that the Doc field setter can only setup undo at the granularity of an entire field and won't even be called if
// just a part of a field (eg. field within an ObjectField) changes. This function returns a function that can be called
// whenever an internal ObjectField field changes. It should be passed a 'diff' specification describing the change. Currently,
// List's are the only true ObjectFields that can be partially modified (ignoring SchemaHeaderFields which should go away).
// The 'diff' specification that a list can send is limited to indicating that something was added, removed, or that the list contents
// were replaced. Based on this specification, an Undo event is setup that will save enough information about the ObjectField to be
// able to undo and redo the partial change.
//
export function containedFieldChangedHandler(container: List<Field> | Doc, prop: string | number, liveContainedField: ObjectField) {
let lastValue: FieldResult = liveContainedField instanceof ObjectField ? ObjectField.MakeCopy(liveContainedField) : liveContainedField;
return (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; items: Field[] | undefined; length: number | undefined; hint?: any }, dummyServerOp?: any) => {
const serializeItems = () => ({ __type: 'list', fields: diff?.items?.map((item: Field) => SerializationHelper.Serialize(item)) });
// prettier-ignore
const serverOp = diff?.op === '$addToSet'
? { $addToSet: { ['fields.' + prop]: serializeItems() }, length: diff.length }
: diff?.op === '$remFromSet'
? { $remFromSet: { ['fields.' + prop]: serializeItems(), hint: diff.hint}, length: diff.length }
: { $set: { ['fields.' + prop]: liveContainedField ? SerializationHelper.Serialize(liveContainedField) : undefined } };
if (!(container instanceof Doc) || !container[UpdatingFromServer]) {
const prevValue = ObjectField.MakeCopy(lastValue as List<any>);
lastValue = ObjectField.MakeCopy(liveContainedField);
const newValue = ObjectField.MakeCopy(liveContainedField);
if (diff?.op === '$addToSet') {
UndoManager.AddEvent(
{
redo: () => {
//console.log('redo $add: ' + prop, diff.items); // bcz: uncomment to log undo
(container as any)[prop as any]?.push(...(diff.items || [])?.map((item: any) => item.value ?? item));
lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
},
undo: action(() => {
// console.log('undo $add: ' + prop, diff.items); // bcz: uncomment to log undo
diff.items?.forEach((item: any) => {
const ind =
item instanceof SchemaHeaderField //
? (container as any)[prop as any]?.findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading)
: (container as any)[prop as any]?.indexOf(item.value ?? item);
ind !== undefined && ind !== -1 && (container as any)[prop as any]?.splice(ind, 1);
});
lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
}),
prop: 'add ' + diff.items?.length + ' items to list',
},
diff?.items
);
} else if (diff?.op === '$remFromSet') {
UndoManager.AddEvent(
{
redo: action(() => {
// console.log('redo $rem: ' + prop, diff.items); // bcz: uncomment to log undo
diff.items?.forEach((item: any) => {
const ind =
item instanceof SchemaHeaderField //
? (container as any)[prop as any]?.findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading)
: (container as any)[prop as any]?.indexOf(item.value ?? item);
ind !== undefined && ind !== -1 && (container as any)[prop as any]?.splice(ind, 1);
});
lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
}),
undo: () => {
// console.log('undo $rem: ' + prop, diff.items); // bcz: uncomment to log undo
diff.items?.forEach((item: any) => {
if (item instanceof SchemaHeaderField) {
const ind = (prevValue as List<any>).findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading);
ind !== -1 && (container as any)[prop as any].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) === -1 && (container as any)[prop as any].splice(ind, 0, item);
} else {
const ind = (prevValue as List<any>).indexOf(item.value ?? item);
ind !== -1 && (container as any)[prop as any].indexOf(item.value ?? item) === -1 && (container as any)[prop as any].splice(ind, 0, item);
}
});
lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
},
prop: 'remove ' + diff.items?.length + ' items from list',
},
diff?.items
);
} else {
const setFieldVal = (val: Field | undefined) => (container instanceof Doc ? (container[prop as string] = val) : (container[prop as number] = val as Field));
UndoManager.AddEvent(
{
redo: () => {
// console.log('redo list: ' + prop, fieldVal()); // bcz: uncomment to log undo
setFieldVal(newValue instanceof ObjectField ? ObjectField.MakeCopy(newValue) : undefined);
lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
},
undo: () => {
// console.log('undo list: ' + prop, fieldVal()); // bcz: uncomment to log undo
setFieldVal(prevValue instanceof ObjectField ? ObjectField.MakeCopy(prevValue) : undefined);
lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
},
prop: 'set list field',
},
diff?.items
);
}
}
container[FieldChanged]?.(undefined, serverOp);
};
}
|