sdc_client 0.158.9 → 0.160.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/CHANGELOG.md +58 -0
- package/LICENSE +21 -0
- package/dist/index.js +23 -13
- package/dist/ugly.index.js +2 -1
- package/dist/ugly.index.js.LICENSE.txt +1 -8
- package/gulp/gulp.jsx +11 -36
- package/package.json +23 -12
- package/src/index.js +31 -7
- package/src/simpleDomControl/AbstractSDC.js +15 -11
- package/src/simpleDomControl/sdc_controller.js +3 -3
- package/src/simpleDomControl/sdc_dom_events.js +5 -10
- package/src/simpleDomControl/sdc_events.js +3 -3
- package/src/simpleDomControl/sdc_main.js +34 -13
- package/src/simpleDomControl/sdc_model.js +225 -92
- package/src/simpleDomControl/sdc_params.js +0 -3
- package/src/simpleDomControl/sdc_server_call.js +21 -4
- package/src/simpleDomControl/sdc_test_utils.js +0 -1
- package/src/simpleDomControl/sdc_utils.js +59 -7
- package/src/simpleDomControl/sdc_view.js +16 -1
- package/.github/workflows/node.js.yml +0 -38
- package/.idea/inspectionProfiles/Project_Default.xml +0 -17
- package/.idea/jsLibraryMappings.xml +0 -6
- package/.idea/misc.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -6
- package/.idea/workspace.xml +0 -403
- package/.readthedocs.yaml +0 -13
- package/.tool-versions +0 -1
- package/SimpleDomControlClient.iml +0 -12
- package/docs/api-reference.rst +0 -225
- package/docs/conf.py +0 -11
- package/docs/controllers.rst +0 -228
- package/docs/events-and-dom.rst +0 -120
- package/docs/getting-started.rst +0 -143
- package/docs/index.rst +0 -22
- package/docs/models.rst +0 -351
- package/docs/overview.rst +0 -66
- package/docs/requirements.txt +0 -1
- package/eslint.config.js +0 -43
- package/gulpfile.cjs +0 -30
- package/jest.config.mjs +0 -197
- package/test/controller.test.js +0 -93
- package/test/dist.test.js +0 -127
- package/test/model.test.js +0 -330
- package/test/models/Author.js +0 -145
- package/test/models/Book.js +0 -112
- package/test/models/BookContent.js +0 -114
- package/test/models/SdcUser.js +0 -75
- package/test/models/src.js +0 -8
- package/test/sdc_model_dates.ai.test.js +0 -57
- package/test/sdc_server_call.ai.test.js +0 -267
- package/test/utils.js +0 -88
- package/test/view.test.js +0 -159
- package/test-setup.js +0 -4
- package/webpack.config/webpack.default.config.cjs +0 -14
- package/webpack.config/webpack.development.config.cjs +0 -14
- package/webpack.config/webpack.production.config.cjs +0 -11
|
@@ -47,7 +47,10 @@ let sdcDomFragment = function (element, props) {
|
|
|
47
47
|
if (props) {
|
|
48
48
|
Object.entries(props).forEach(([k, v]) => {
|
|
49
49
|
if (k.startsWith("on")) {
|
|
50
|
-
|
|
50
|
+
const type = k.substring(2).toLowerCase();
|
|
51
|
+
$new_elem[0].addEventListener(type, v);
|
|
52
|
+
// Remembered so reconcile() can move the listeners to a node it keeps.
|
|
53
|
+
($new_elem[0]._sdcListeners ??= []).push([type, v]);
|
|
51
54
|
} else {
|
|
52
55
|
if (PROPERTIES_UPDATE[k.toLowerCase()]) {
|
|
53
56
|
k = PROPERTIES_UPDATE[k.toLowerCase()];
|
|
@@ -75,6 +78,14 @@ window.sdcDom = function (tagName, props, ...children) {
|
|
|
75
78
|
return $new_elem;
|
|
76
79
|
};
|
|
77
80
|
|
|
81
|
+
const redirector = (a) => {
|
|
82
|
+
if (a["url-link"]) {
|
|
83
|
+
trigger("onNavLink", a["url-link"]);
|
|
84
|
+
} else {
|
|
85
|
+
window.location.href = a["url"];
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
78
89
|
export let app = {
|
|
79
90
|
CSRF_TOKEN: window.CSRF_TOKEN || "",
|
|
80
91
|
LANGUAGE_CODE: window.LANGUAGE_CODE || "en",
|
|
@@ -87,6 +98,7 @@ export let app = {
|
|
|
87
98
|
_isInit: false,
|
|
88
99
|
_origin_trigger: null,
|
|
89
100
|
_globalControllerClasses: [],
|
|
101
|
+
_createdGlobals: [],
|
|
90
102
|
|
|
91
103
|
init_sdc: () => {
|
|
92
104
|
if (!app._isInit) {
|
|
@@ -119,8 +131,12 @@ export let app = {
|
|
|
119
131
|
|
|
120
132
|
app.tagNames = tagList();
|
|
121
133
|
|
|
134
|
+
// Global controllers are created only once, even if init_sdc() is called again.
|
|
135
|
+
const newGlobals = Global.filter((tagName) => !app._createdGlobals.includes(tagName));
|
|
136
|
+
app._createdGlobals.push(...newGlobals);
|
|
137
|
+
|
|
122
138
|
const $globalDiv = $("<div></div>");
|
|
123
|
-
|
|
139
|
+
newGlobals.forEach((tagName) => {
|
|
124
140
|
const ControllerClass = controllerList[tagName][0];
|
|
125
141
|
$globalDiv.append(
|
|
126
142
|
`<${ControllerClass.prototype._tagName}></${ControllerClass.prototype._tagName}>`,
|
|
@@ -169,9 +185,12 @@ export let app = {
|
|
|
169
185
|
* @param {AbstractSDC} Controller
|
|
170
186
|
*/
|
|
171
187
|
registerGlobal: (Controller) => {
|
|
172
|
-
app.register(Controller);
|
|
188
|
+
const registration = app.register(Controller);
|
|
173
189
|
let tagName = Controller.prototype._tagName;
|
|
174
|
-
Global.
|
|
190
|
+
if (!Global.includes(tagName)) {
|
|
191
|
+
Global.push(tagName);
|
|
192
|
+
}
|
|
193
|
+
return registration;
|
|
175
194
|
},
|
|
176
195
|
|
|
177
196
|
cleanCache: () => {
|
|
@@ -260,14 +279,23 @@ export let app = {
|
|
|
260
279
|
.then((a, b, c) => {
|
|
261
280
|
resolve(a, b, c);
|
|
262
281
|
if (a.status === "redirect") {
|
|
263
|
-
|
|
282
|
+
redirector(a);
|
|
264
283
|
} else {
|
|
265
284
|
p.then(() => {
|
|
266
285
|
app.refresh(controller.$container);
|
|
267
286
|
});
|
|
268
287
|
}
|
|
269
288
|
})
|
|
270
|
-
.catch(
|
|
289
|
+
.catch((a, b, c) => {
|
|
290
|
+
// send_redirect() answers with HTTP 301 and the redirect in the JSON body.
|
|
291
|
+
if (a.status === 301 && a.responseJSON) {
|
|
292
|
+
a = a.responseJSON;
|
|
293
|
+
redirector(a);
|
|
294
|
+
resolve(a, b, c);
|
|
295
|
+
} else {
|
|
296
|
+
reject(a, b, c);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
271
299
|
});
|
|
272
300
|
|
|
273
301
|
return p;
|
|
@@ -275,13 +303,6 @@ export let app = {
|
|
|
275
303
|
|
|
276
304
|
submitFormAndUpdateView: (controller, form, url, method) => {
|
|
277
305
|
let formData = new FormData(form);
|
|
278
|
-
const redirector = (a) => {
|
|
279
|
-
if (a["url-link"]) {
|
|
280
|
-
trigger("onNavLink", a["url-link"]);
|
|
281
|
-
} else {
|
|
282
|
-
window.location.href = a["url"];
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
306
|
|
|
286
307
|
const p = new Promise((resolve, reject) => {
|
|
287
308
|
uploadFileFormData(formData, url || form.action, method || form.method)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getValueFromField, setValueInField, uuidv4 } from "./sdc_utils.js";
|
|
1
|
+
import { formatDate, getValueFromField, setValueInField, toDate, uuidv4 } from "./sdc_utils.js";
|
|
2
2
|
import { getModel } from "./sdc_socket.js"
|
|
3
3
|
import { app } from "./sdc_main.js";
|
|
4
4
|
import { trigger } from "./sdc_events.js";
|
|
@@ -6,11 +6,15 @@ import { trigger } from "./sdc_events.js";
|
|
|
6
6
|
const MAX_FILE_UPLOAD = 25000;
|
|
7
7
|
const CONNECTING_REQUEST_ID = "_connecting_process";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Copy a model instance for another queryset. A new instance of the same class is
|
|
11
|
+
* created from the serialized field values (structuredClone cannot copy the
|
|
12
|
+
* WeakRef/Proxy members of a model).
|
|
13
|
+
*/
|
|
14
|
+
const cloneModel = (original) => {
|
|
15
|
+
const copy = new original.constructor({});
|
|
16
|
+
copy.setValues(original.serialize());
|
|
17
|
+
return copy;
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
class SdcModelError extends Error {
|
|
@@ -82,6 +86,29 @@ function normalizePk(pk) {
|
|
|
82
86
|
return Number.isNaN(normalizedPk) ? -1 : normalizedPk;
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Decide whether an argument of `create()` is an options object
|
|
91
|
+
* (`{elem, data}`) or the data itself. It is treated as options if it is
|
|
92
|
+
* a non-empty plain object whose keys are only `elem` and/or `data`, and
|
|
93
|
+
* `elem` (if present) is an SdcModel.
|
|
94
|
+
*
|
|
95
|
+
* @param {*} arg
|
|
96
|
+
* @returns {boolean}
|
|
97
|
+
*/
|
|
98
|
+
function isCreateOptions(arg) {
|
|
99
|
+
if (!arg || typeof arg !== "object" || arg instanceof SdcModel) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const keys = Object.keys(arg);
|
|
103
|
+
if (keys.length === 0 || !keys.every((k) => k === "elem" || k === "data")) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if ("elem" in arg && arg.elem !== null && !(arg.elem instanceof SdcModel)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return !("data" in arg) || arg.data === null || typeof arg.data === "object";
|
|
110
|
+
}
|
|
111
|
+
|
|
85
112
|
export class SdcQuerySet {
|
|
86
113
|
/**
|
|
87
114
|
* Lightweight client-side collection wrapper around the SDC model websocket.
|
|
@@ -109,6 +136,9 @@ export class SdcQuerySet {
|
|
|
109
136
|
};
|
|
110
137
|
this.onCreate = () => {
|
|
111
138
|
};
|
|
139
|
+
// Called with the removed models when rows of this queryset are deleted on the
|
|
140
|
+
// server. If not set, onUpdate is called instead.
|
|
141
|
+
this.onDelete = null;
|
|
112
142
|
|
|
113
143
|
return new Proxy(this, {
|
|
114
144
|
get(target, prop) {
|
|
@@ -150,6 +180,22 @@ export class SdcQuerySet {
|
|
|
150
180
|
this.onUpdate = handler;
|
|
151
181
|
}
|
|
152
182
|
|
|
183
|
+
set on_delete(handler) {
|
|
184
|
+
this.onDelete = handler;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Remove the models with the given ids from the local list.
|
|
189
|
+
*
|
|
190
|
+
* @param {Array<number>} ids
|
|
191
|
+
* @returns {Array<SdcModel>} the removed models
|
|
192
|
+
*/
|
|
193
|
+
_removeIds(ids) {
|
|
194
|
+
const removed = this.valuesList.filter((item) => ids.includes(item.id));
|
|
195
|
+
this.valuesList = this.valuesList.filter((item) => !ids.includes(item.id));
|
|
196
|
+
return removed;
|
|
197
|
+
}
|
|
198
|
+
|
|
153
199
|
/**
|
|
154
200
|
*
|
|
155
201
|
*
|
|
@@ -169,19 +215,25 @@ export class SdcQuerySet {
|
|
|
169
215
|
*
|
|
170
216
|
*
|
|
171
217
|
* @param {Array<integr|string>|integer|SdcModel|SdcQuerySet|string} ids
|
|
172
|
-
*/
|
|
218
|
+
*/
|
|
173
219
|
_setIds(ids) {
|
|
174
220
|
|
|
175
221
|
if (ids === null || ids === "" || Array.isArray(ids) && ids.length === 0) {
|
|
176
222
|
this.valuesList = [];
|
|
177
223
|
return this.valuesList;
|
|
178
|
-
} else if (ids instanceof SdcQuerySet) {
|
|
179
|
-
this
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
224
|
+
} else if (ids instanceof SdcQuerySet || ids instanceof SdcModel) {
|
|
225
|
+
// Models already in this queryset are kept (so references to them stay valid);
|
|
226
|
+
// other models are copied into it.
|
|
227
|
+
const sources = ids instanceof SdcModel ? [ids] : ids.valuesList;
|
|
228
|
+
this.valuesList = sources.map((source) => {
|
|
229
|
+
const existing = source.id !== null ? this.byId(source.id) : null;
|
|
230
|
+
if (existing) {
|
|
231
|
+
return existing;
|
|
232
|
+
}
|
|
233
|
+
const copy = cloneModel(source);
|
|
234
|
+
copy._setQuerySet(this, true);
|
|
235
|
+
return copy;
|
|
236
|
+
});
|
|
185
237
|
return this.valuesList;
|
|
186
238
|
}
|
|
187
239
|
|
|
@@ -277,12 +329,14 @@ export class SdcQuerySet {
|
|
|
277
329
|
* Load model instances matching the current query into the queryset cache.
|
|
278
330
|
*
|
|
279
331
|
* @param {?object} modelQuery
|
|
332
|
+
* @param {object} [options]
|
|
333
|
+
* @param {?number} [options.limit] load at most this many instances (the server slices the query)
|
|
280
334
|
* @returns {Promise<SdcQuerySet>}
|
|
281
335
|
*/
|
|
282
|
-
load(modelQuery = null) {
|
|
336
|
+
load(modelQuery = null, { limit = null } = {}) {
|
|
283
337
|
this.valuesList = [];
|
|
284
338
|
this.modelQuery = modelQuery ?? this.modelQuery;
|
|
285
|
-
return this._sendLoad();
|
|
339
|
+
return this._sendLoad(null, limit);
|
|
286
340
|
}
|
|
287
341
|
|
|
288
342
|
/**
|
|
@@ -327,7 +381,10 @@ export class SdcQuerySet {
|
|
|
327
381
|
}),
|
|
328
382
|
);
|
|
329
383
|
|
|
330
|
-
this.openRequest[event_id] = [
|
|
384
|
+
this.openRequest[event_id] = [(res) => {
|
|
385
|
+
this._removeIds([normalizePk(pk)]);
|
|
386
|
+
resolve(res);
|
|
387
|
+
}, reject];
|
|
331
388
|
});
|
|
332
389
|
});
|
|
333
390
|
}
|
|
@@ -337,19 +394,23 @@ export class SdcQuerySet {
|
|
|
337
394
|
*
|
|
338
395
|
* @returns {Promise<*>}
|
|
339
396
|
*/
|
|
340
|
-
_sendLoad(loadQuery = null) {
|
|
397
|
+
_sendLoad(loadQuery = null, limit = null) {
|
|
341
398
|
return this.isConnected().then(() => {
|
|
342
399
|
const event_id = uuidv4();
|
|
400
|
+
const args = {
|
|
401
|
+
model_name: this.modelName,
|
|
402
|
+
model_query: loadQuery ?? this.modelQuery,
|
|
403
|
+
};
|
|
404
|
+
if (limit !== null) {
|
|
405
|
+
args.limit = limit;
|
|
406
|
+
}
|
|
343
407
|
return new Promise((resolve, reject) => {
|
|
344
408
|
this.socket.send(
|
|
345
409
|
JSON.stringify({
|
|
346
410
|
event: "model",
|
|
347
411
|
event_type: "load",
|
|
348
412
|
event_id,
|
|
349
|
-
args
|
|
350
|
-
model_name: this.modelName,
|
|
351
|
-
model_query: loadQuery ?? this.modelQuery,
|
|
352
|
-
},
|
|
413
|
+
args,
|
|
353
414
|
}),
|
|
354
415
|
);
|
|
355
416
|
|
|
@@ -520,7 +581,9 @@ export class SdcQuerySet {
|
|
|
520
581
|
)
|
|
521
582
|
.data("model", modelObj)
|
|
522
583
|
.data("model_pk", pk)
|
|
523
|
-
.data("form_name", formName)
|
|
584
|
+
.data("form_name", formName)
|
|
585
|
+
.attr('SdcModelName', this.modelName)
|
|
586
|
+
.attr('SdcModelPK', pk);
|
|
524
587
|
modelObj.addForm($form);
|
|
525
588
|
if ($form.length > 0 && !$form[0].hasAttribute("sdc_submit")) {
|
|
526
589
|
$form.attr("sdc_submit", "submitModelFormDistributor");
|
|
@@ -638,12 +701,24 @@ export class SdcQuerySet {
|
|
|
638
701
|
}
|
|
639
702
|
|
|
640
703
|
/**
|
|
704
|
+
* Create a new instance in the DB.
|
|
641
705
|
*
|
|
642
|
-
* @param elem
|
|
643
|
-
*
|
|
706
|
+
* @param {{elem?: SdcModel, data?: object} | object} arg
|
|
707
|
+
* Either an options object containing `elem` and/or `data`,
|
|
708
|
+
* or `data` directly.
|
|
709
|
+
*
|
|
710
|
+
* @example
|
|
711
|
+
* create({ elem, data });
|
|
712
|
+
* create({ elem });
|
|
713
|
+
* create(data);
|
|
644
714
|
* @returns {Promise<unknown>}
|
|
645
715
|
*/
|
|
646
|
-
create(
|
|
716
|
+
create(arg = null) {
|
|
717
|
+
let { elem = null, data = null } = isCreateOptions(arg) ? arg : { data: arg };
|
|
718
|
+
if (data && typeof data === "object" && Object.keys(data).length === 0) {
|
|
719
|
+
data = null;
|
|
720
|
+
}
|
|
721
|
+
|
|
647
722
|
const event_id = uuidv4();
|
|
648
723
|
if (!elem) {
|
|
649
724
|
elem = this.new(data);
|
|
@@ -758,43 +833,47 @@ export class SdcQuerySet {
|
|
|
758
833
|
const value = elem[key];
|
|
759
834
|
if (value instanceof File) {
|
|
760
835
|
toSolve.push(
|
|
761
|
-
new Promise(
|
|
836
|
+
new Promise((resolve, reject) => {
|
|
762
837
|
const event_id = uuidv4();
|
|
763
838
|
this.openRequest[event_id] = [resolve, reject];
|
|
764
839
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
840
|
+
value.arrayBuffer().then((buffer) => {
|
|
841
|
+
let result = new Uint8Array(buffer);
|
|
842
|
+
let numberOfChunks = Math.ceil(result.length / MAX_FILE_UPLOAD);
|
|
843
|
+
files[key] = {
|
|
844
|
+
id: event_id,
|
|
845
|
+
file_name: value.name,
|
|
846
|
+
field_name: key,
|
|
847
|
+
content_length: value.size,
|
|
848
|
+
};
|
|
849
|
+
for (let i = 0; i < numberOfChunks; ++i) {
|
|
850
|
+
const chunk = Array.from(result.slice(
|
|
851
|
+
MAX_FILE_UPLOAD * i,
|
|
852
|
+
MAX_FILE_UPLOAD * (i + 1),
|
|
853
|
+
));
|
|
854
|
+
this.socket.send(
|
|
855
|
+
JSON.stringify({
|
|
856
|
+
event: "model",
|
|
857
|
+
event_type: "upload",
|
|
858
|
+
event_id,
|
|
859
|
+
args: {
|
|
860
|
+
chunk,
|
|
861
|
+
idx: i,
|
|
862
|
+
number_of_chunks: numberOfChunks,
|
|
863
|
+
file_name: value.name,
|
|
864
|
+
field_name: key,
|
|
865
|
+
content_length: value.size,
|
|
866
|
+
content_type: value.type,
|
|
867
|
+
model_name: this.modelName,
|
|
868
|
+
model_query: this.modelQuery,
|
|
869
|
+
},
|
|
870
|
+
}),
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
}).catch((err) => {
|
|
874
|
+
this._closeOpenRequest(event_id);
|
|
875
|
+
reject(err);
|
|
876
|
+
});
|
|
798
877
|
}),
|
|
799
878
|
);
|
|
800
879
|
}
|
|
@@ -814,7 +893,7 @@ export class SdcQuerySet {
|
|
|
814
893
|
async _onMessage(e) {
|
|
815
894
|
let data = JSON.parse(e.data);
|
|
816
895
|
if (data.is_error) {
|
|
817
|
-
if (this.openRequest
|
|
896
|
+
if (Object.hasOwn(this.openRequest, data.event_id)) {
|
|
818
897
|
this.openRequest[data.event_id][1](new SdcModelError(data));
|
|
819
898
|
this._closeOpenRequest(data.event_id);
|
|
820
899
|
}
|
|
@@ -838,7 +917,7 @@ export class SdcQuerySet {
|
|
|
838
917
|
this._isConnectingProcess = false;
|
|
839
918
|
this.openRequest[CONNECTING_REQUEST_ID][0](data);
|
|
840
919
|
this._closeOpenRequest(CONNECTING_REQUEST_ID);
|
|
841
|
-
} else if (["load", "named_view", "detail_view"].includes(data.type)) {
|
|
920
|
+
} else if (["load", "list_view", "named_view", "detail_view"].includes(data.type)) {
|
|
842
921
|
const jsonRes = JSON.parse(data.args.data);
|
|
843
922
|
data.args.data = await this._parseServerRes(jsonRes);
|
|
844
923
|
} else if (data.type === "on_update" || data.type === "on_create") {
|
|
@@ -855,6 +934,11 @@ export class SdcQuerySet {
|
|
|
855
934
|
|
|
856
935
|
cb(obj);
|
|
857
936
|
data.args.data = obj;
|
|
937
|
+
} else if (data.type === "on_delete") {
|
|
938
|
+
const ids = JSON.parse(data.args.data).map((x) => normalizePk(x.id ?? x.pk));
|
|
939
|
+
const removed = this._removeIds(ids);
|
|
940
|
+
(this.onDelete ?? this.onUpdate)(removed);
|
|
941
|
+
data.args.data = removed;
|
|
858
942
|
}
|
|
859
943
|
|
|
860
944
|
let instance = data.data?.instance;
|
|
@@ -862,7 +946,7 @@ export class SdcQuerySet {
|
|
|
862
946
|
data.data.instance = JSON.parse(data.data.instance);
|
|
863
947
|
}
|
|
864
948
|
|
|
865
|
-
if (this.openRequest
|
|
949
|
+
if (Object.hasOwn(this.openRequest, data.event_id)) {
|
|
866
950
|
this.openRequest[data.event_id][0](data);
|
|
867
951
|
this._closeOpenRequest(data.event_id);
|
|
868
952
|
}
|
|
@@ -924,14 +1008,17 @@ export class SdcQuerySet {
|
|
|
924
1008
|
`SDC Model (${this.modelName}, ${this.modelId}) Socket closed unexpectedly`,
|
|
925
1009
|
);
|
|
926
1010
|
this._isConnected = false;
|
|
927
|
-
for (const
|
|
1011
|
+
for (const value of Object.values(this.openRequest)) {
|
|
928
1012
|
value[1](e);
|
|
929
1013
|
}
|
|
930
1014
|
this.openRequest = {};
|
|
931
1015
|
|
|
1016
|
+
this._isConnectingProcess = false;
|
|
932
1017
|
setTimeout(() => {
|
|
933
|
-
if (this._autoReconnect) {
|
|
934
|
-
|
|
1018
|
+
if (this._autoReconnect && !this._isConnected) {
|
|
1019
|
+
// isConnected() opens the socket and sends the connect handshake, so
|
|
1020
|
+
// live updates work again and later requests reuse this connection.
|
|
1021
|
+
this.isConnected().catch(() => {
|
|
935
1022
|
});
|
|
936
1023
|
}
|
|
937
1024
|
}, 1000);
|
|
@@ -1028,11 +1115,12 @@ export default class SdcModel {
|
|
|
1028
1115
|
* @returns {SdcQuerySet}
|
|
1029
1116
|
*/
|
|
1030
1117
|
static querySet(modelQuerySet = null, parent = null) {
|
|
1118
|
+
const { modelName } = new this({});
|
|
1031
1119
|
if (!parent) {
|
|
1032
|
-
return new SdcQuerySet(
|
|
1120
|
+
return new SdcQuerySet(modelName, modelQuerySet);
|
|
1033
1121
|
}
|
|
1034
1122
|
|
|
1035
|
-
return parent.querySet(
|
|
1123
|
+
return parent.querySet(modelName, modelQuerySet);
|
|
1036
1124
|
}
|
|
1037
1125
|
|
|
1038
1126
|
addForm($form) {
|
|
@@ -1054,15 +1142,25 @@ export default class SdcModel {
|
|
|
1054
1142
|
_onChange(event) {
|
|
1055
1143
|
const { name } = event.target;
|
|
1056
1144
|
if (this.constructor.fields[name]) {
|
|
1057
|
-
|
|
1145
|
+
try {
|
|
1146
|
+
this[`set${name}`](getValueFromField(event.target));
|
|
1147
|
+
} catch {
|
|
1148
|
+
// Invalid input (e.g. an empty required field) is kept in the form and
|
|
1149
|
+
// validated by the server on submit.
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
// Show the new value in the other forms of this model, not in the input being edited.
|
|
1153
|
+
this._updateForm(name, event.target);
|
|
1058
1154
|
}
|
|
1059
1155
|
}
|
|
1060
1156
|
|
|
1061
|
-
_updateForm(fieldName) {
|
|
1157
|
+
_updateForm(fieldName, skipElement = null) {
|
|
1062
1158
|
const self = this;
|
|
1063
1159
|
this._forms.forEach(($form) => {
|
|
1064
1160
|
$form.find(`[name="${fieldName}"]`).each(function () {
|
|
1065
|
-
|
|
1161
|
+
if (this !== skipElement) {
|
|
1162
|
+
setValueInField(this, self[fieldName]);
|
|
1163
|
+
}
|
|
1066
1164
|
});
|
|
1067
1165
|
});
|
|
1068
1166
|
}
|
|
@@ -1090,7 +1188,15 @@ export default class SdcModel {
|
|
|
1090
1188
|
return this._querySet.deref().save({ pk: this.id, formName, data });
|
|
1091
1189
|
}
|
|
1092
1190
|
|
|
1093
|
-
|
|
1191
|
+
/**
|
|
1192
|
+
* Create this instance in the DB.
|
|
1193
|
+
*
|
|
1194
|
+
* @param {{data?: object} | object | null} values
|
|
1195
|
+
* Either `{ data }` or `data` directly.
|
|
1196
|
+
* @returns {Promise<unknown>}
|
|
1197
|
+
*/
|
|
1198
|
+
create(values = null) {
|
|
1199
|
+
const data = isCreateOptions(values) ? (values.data ?? null) : values;
|
|
1094
1200
|
return this._querySet.deref().create({ elem: this, data });
|
|
1095
1201
|
}
|
|
1096
1202
|
|
|
@@ -1159,6 +1265,9 @@ export default class SdcModel {
|
|
|
1159
1265
|
}
|
|
1160
1266
|
} else if (val.is_relation && value instanceof SdcModel) {
|
|
1161
1267
|
acc[key] = value.id ?? null;
|
|
1268
|
+
} else if (value instanceof Date) {
|
|
1269
|
+
// Django's DateField does not accept a full ISO datetime.
|
|
1270
|
+
acc[key] = val.type === "DateField" ? formatDate(value) : value.toISOString();
|
|
1162
1271
|
} else {
|
|
1163
1272
|
acc[key] = value;
|
|
1164
1273
|
}
|
|
@@ -1219,15 +1328,16 @@ export default class SdcModel {
|
|
|
1219
1328
|
$forms = this._resolveForms($forms);
|
|
1220
1329
|
|
|
1221
1330
|
const self = this;
|
|
1222
|
-
const fields = this.constructor
|
|
1331
|
+
const { fields } = this.constructor;
|
|
1223
1332
|
$forms.each(function () {
|
|
1224
1333
|
const pk = normalizePk($(this).data("model_pk"));
|
|
1225
|
-
|
|
1334
|
+
// Create forms carry model_pk -1 while the new model's id is still null.
|
|
1335
|
+
if (normalizePk(self.id) !== pk) {
|
|
1226
1336
|
return;
|
|
1227
1337
|
}
|
|
1228
1338
|
|
|
1229
1339
|
for (let formItem of this.elements) {
|
|
1230
|
-
let name = formItem
|
|
1340
|
+
let { name } = formItem;
|
|
1231
1341
|
if (name && name !== "" && !!fields[name]) {
|
|
1232
1342
|
setValueInField(formItem, self[name])
|
|
1233
1343
|
}
|
|
@@ -1247,12 +1357,17 @@ export default class SdcModel {
|
|
|
1247
1357
|
$forms = this._resolveForms($forms);
|
|
1248
1358
|
|
|
1249
1359
|
const self = this;
|
|
1250
|
-
const fields = this.constructor
|
|
1360
|
+
const { fields } = this.constructor;
|
|
1251
1361
|
const returnValue = {}
|
|
1252
1362
|
|
|
1253
1363
|
function setValueInForm(name, value) {
|
|
1254
|
-
if (
|
|
1255
|
-
|
|
1364
|
+
if (fields[name]) {
|
|
1365
|
+
try {
|
|
1366
|
+
self[name] = value;
|
|
1367
|
+
} catch {
|
|
1368
|
+
// Invalid values are still sent to the server, which returns the form
|
|
1369
|
+
// errors; the model keeps its last valid value.
|
|
1370
|
+
}
|
|
1256
1371
|
}
|
|
1257
1372
|
returnValue[name] = value;
|
|
1258
1373
|
}
|
|
@@ -1387,14 +1502,21 @@ export default class SdcModel {
|
|
|
1387
1502
|
|
|
1388
1503
|
switch (type) {
|
|
1389
1504
|
case "CharField":
|
|
1390
|
-
case "
|
|
1505
|
+
case "TextField":
|
|
1506
|
+
case "SlugField":
|
|
1391
1507
|
case "UUIDField":
|
|
1392
1508
|
case "EmailField":
|
|
1393
1509
|
return `${value}`;
|
|
1394
1510
|
|
|
1395
1511
|
case "IntegerField":
|
|
1396
|
-
case "
|
|
1512
|
+
case "SmallIntegerField":
|
|
1397
1513
|
case "BigIntegerField":
|
|
1514
|
+
case "PositiveIntegerField":
|
|
1515
|
+
case "PositiveSmallIntegerField":
|
|
1516
|
+
case "PositiveBigIntegerField":
|
|
1517
|
+
case "AutoField":
|
|
1518
|
+
case "SmallAutoField":
|
|
1519
|
+
case "BigAutoField":
|
|
1398
1520
|
return parseInt(value, 10);
|
|
1399
1521
|
|
|
1400
1522
|
case "FloatField":
|
|
@@ -1406,7 +1528,7 @@ export default class SdcModel {
|
|
|
1406
1528
|
|
|
1407
1529
|
case "DateField":
|
|
1408
1530
|
case "DateTimeField":
|
|
1409
|
-
return
|
|
1531
|
+
return toDate(value);
|
|
1410
1532
|
|
|
1411
1533
|
case "URLField":
|
|
1412
1534
|
return new URL(value);
|
|
@@ -1429,14 +1551,7 @@ export default class SdcModel {
|
|
|
1429
1551
|
}
|
|
1430
1552
|
|
|
1431
1553
|
if (typeof File !== "undefined" && value instanceof File) {
|
|
1432
|
-
|
|
1433
|
-
return `File too large (max ${config.max_size} bytes)`;
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
if (config.allowed_types && !config.allowed_types.includes(value.type)) {
|
|
1437
|
-
return `Invalid file type (${value.type})`;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1554
|
+
// Size and type are checked in validate().
|
|
1440
1555
|
return value;
|
|
1441
1556
|
}
|
|
1442
1557
|
break;
|
|
@@ -1518,8 +1633,14 @@ function validateField(value, config) {
|
|
|
1518
1633
|
break;
|
|
1519
1634
|
|
|
1520
1635
|
case "IntegerField":
|
|
1521
|
-
case "
|
|
1636
|
+
case "SmallIntegerField":
|
|
1522
1637
|
case "BigIntegerField":
|
|
1638
|
+
case "PositiveIntegerField":
|
|
1639
|
+
case "PositiveSmallIntegerField":
|
|
1640
|
+
case "PositiveBigIntegerField":
|
|
1641
|
+
case "AutoField":
|
|
1642
|
+
case "SmallAutoField":
|
|
1643
|
+
case "BigAutoField":
|
|
1523
1644
|
if (!isFinite(parseInt(value))) {
|
|
1524
1645
|
return "Must be an integer";
|
|
1525
1646
|
}
|
|
@@ -1566,14 +1687,26 @@ function validateField(value, config) {
|
|
|
1566
1687
|
break;
|
|
1567
1688
|
|
|
1568
1689
|
case "JSONField":
|
|
1569
|
-
if (typeof value
|
|
1570
|
-
|
|
1690
|
+
if (typeof value === "string") {
|
|
1691
|
+
try {
|
|
1692
|
+
JSON.parse(value);
|
|
1693
|
+
} catch {
|
|
1694
|
+
return "Must be valid JSON";
|
|
1695
|
+
}
|
|
1571
1696
|
}
|
|
1572
1697
|
break;
|
|
1573
1698
|
case "FileField":
|
|
1574
1699
|
if (!FileLoaded.isValid(value) && !(value instanceof File) && !(value instanceof FileLoaded)) {
|
|
1575
1700
|
return "Must be a valid file";
|
|
1576
1701
|
}
|
|
1702
|
+
if (typeof File !== "undefined" && value instanceof File) {
|
|
1703
|
+
if (config.max_size && value.size > config.max_size) {
|
|
1704
|
+
return `File too large (max ${config.max_size} bytes)`;
|
|
1705
|
+
}
|
|
1706
|
+
if (config.allowed_types && !config.allowed_types.includes(value.type)) {
|
|
1707
|
+
return `Invalid file type (${value.type})`;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1577
1710
|
break;
|
|
1578
1711
|
default:
|
|
1579
1712
|
break;
|
|
@@ -21,7 +21,6 @@ export function prepareData(data, controller = null) {
|
|
|
21
21
|
{
|
|
22
22
|
...fields,
|
|
23
23
|
id: pk,
|
|
24
|
-
id: pk,
|
|
25
24
|
},
|
|
26
25
|
];
|
|
27
26
|
} catch {
|
|
@@ -47,8 +46,6 @@ function getDomTagParamsWithList($element, controller = null) {
|
|
|
47
46
|
function reg_runOnInitWithParameter(controller, $element, applyController) {
|
|
48
47
|
if (!controller) {
|
|
49
48
|
return false;
|
|
50
|
-
} else if (typeof controller.onInit !== "function") {
|
|
51
|
-
return false;
|
|
52
49
|
}
|
|
53
50
|
|
|
54
51
|
controller.params = getDomTagParamsWithList(
|