sdc_client 0.158.8 → 0.159.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 +53 -0
- package/LICENSE +21 -0
- package/dist/index.js +49 -24
- package/dist/ugly.index.js +2 -1
- package/dist/ugly.index.js.LICENSE.txt +1 -8
- package/gulp/gulp.jsx +12 -42
- package/package.json +39 -31
- package/src/index.js +27 -7
- package/src/simpleDomControl/AbstractSDC.js +24 -11
- package/src/simpleDomControl/sdc_controller.js +7 -4
- 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 +209 -84
- 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 +21 -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 -60
- package/gulpfile.jsx +0 -25
- 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 -22
- package/webpack.config/webpack.development.config.cjs +0 -14
- package/webpack.config/webpack.production.config.cjs +0 -11
|
@@ -15,7 +15,6 @@ function checkIfEventFits(ev_type, e, target) {
|
|
|
15
15
|
target !== elementUnderMouse && !target.contains(elementUnderMouse)
|
|
16
16
|
);
|
|
17
17
|
case "mouseenter":
|
|
18
|
-
case "mousein":
|
|
19
18
|
elementUnderMouse = document.elementFromPoint(e.clientX, e.clientY);
|
|
20
19
|
return (
|
|
21
20
|
target !== elementUnderMouse && !target.contains(elementUnderMouse)
|
|
@@ -28,7 +27,7 @@ function checkIfEventFits(ev_type, e, target) {
|
|
|
28
27
|
export function windowEventHandler(event) {
|
|
29
28
|
let ev_type = event.type;
|
|
30
29
|
if (
|
|
31
|
-
|
|
30
|
+
Object.hasOwn(event, "namespace") &&
|
|
32
31
|
event.namespace &&
|
|
33
32
|
event.namespace.length
|
|
34
33
|
)
|
|
@@ -97,16 +96,12 @@ export function initEvents() {
|
|
|
97
96
|
* @param {AbstractSDC} controller
|
|
98
97
|
*/
|
|
99
98
|
export function setControllerEvents(controller) {
|
|
100
|
-
if (controller._isEventsSet) {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
99
|
const events = controller.getEvents();
|
|
105
100
|
for (let ev_type in events) {
|
|
106
|
-
if (
|
|
101
|
+
if (Object.hasOwn(events, ev_type)) {
|
|
107
102
|
let eventList = events[ev_type];
|
|
108
103
|
for (let domSelector in eventList) {
|
|
109
|
-
if (
|
|
104
|
+
if (Object.hasOwn(eventList, domSelector)) {
|
|
110
105
|
controller.find(domSelector).each(function () {
|
|
111
106
|
let $elements = $(this);
|
|
112
107
|
let event_list = $elements.attr(`sdc_${ev_type}`) || null;
|
|
@@ -122,6 +117,6 @@ export function setControllerEvents(controller) {
|
|
|
122
117
|
}
|
|
123
118
|
}
|
|
124
119
|
}
|
|
125
|
-
//
|
|
126
|
-
//
|
|
120
|
+
// Not cached on purpose: a refresh can add new elements that match the selectors,
|
|
121
|
+
// so the attributes are set again on every refresh (existing entries are skipped).
|
|
127
122
|
}
|
|
@@ -23,7 +23,7 @@ let eventList = {};
|
|
|
23
23
|
*/
|
|
24
24
|
export function on(name, controller) {
|
|
25
25
|
setEvent(name);
|
|
26
|
-
if (!
|
|
26
|
+
if (!Object.hasOwn(eventList, name)) {
|
|
27
27
|
return console.log("No event: " + name, controller);
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -61,7 +61,7 @@ export function setEvent(name, functionName) {
|
|
|
61
61
|
*/
|
|
62
62
|
export function allOff(controller) {
|
|
63
63
|
for (let eventName in handlerList) {
|
|
64
|
-
if (
|
|
64
|
+
if (Object.hasOwn(handlerList, eventName)) {
|
|
65
65
|
for (let i = handlerList[eventName].length - 1; i >= 0; i--) {
|
|
66
66
|
if (controller === handlerList[eventName][i]) {
|
|
67
67
|
handlerList[eventName].splice(i, 1);
|
|
@@ -82,7 +82,7 @@ export function allOff(controller) {
|
|
|
82
82
|
export function trigger(name) {
|
|
83
83
|
let args = Array.apply(null, arguments);
|
|
84
84
|
name = args.shift();
|
|
85
|
-
if (!
|
|
85
|
+
if (!Object.hasOwn(handlerList, name) || !Object.hasOwn(eventList, name)) {
|
|
86
86
|
return promiseDummyFactory();
|
|
87
87
|
}
|
|
88
88
|
let handler = handlerList[name];
|
|
@@ -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
|
|
|
@@ -327,7 +379,10 @@ export class SdcQuerySet {
|
|
|
327
379
|
}),
|
|
328
380
|
);
|
|
329
381
|
|
|
330
|
-
this.openRequest[event_id] = [
|
|
382
|
+
this.openRequest[event_id] = [(res) => {
|
|
383
|
+
this._removeIds([normalizePk(pk)]);
|
|
384
|
+
resolve(res);
|
|
385
|
+
}, reject];
|
|
331
386
|
});
|
|
332
387
|
});
|
|
333
388
|
}
|
|
@@ -638,12 +693,24 @@ export class SdcQuerySet {
|
|
|
638
693
|
}
|
|
639
694
|
|
|
640
695
|
/**
|
|
696
|
+
* Create a new instance in the DB.
|
|
641
697
|
*
|
|
642
|
-
* @param elem
|
|
643
|
-
*
|
|
698
|
+
* @param {{elem?: SdcModel, data?: object} | object} arg
|
|
699
|
+
* Either an options object containing `elem` and/or `data`,
|
|
700
|
+
* or `data` directly.
|
|
701
|
+
*
|
|
702
|
+
* @example
|
|
703
|
+
* create({ elem, data });
|
|
704
|
+
* create({ elem });
|
|
705
|
+
* create(data);
|
|
644
706
|
* @returns {Promise<unknown>}
|
|
645
707
|
*/
|
|
646
|
-
create(
|
|
708
|
+
create(arg = null) {
|
|
709
|
+
let { elem = null, data = null } = isCreateOptions(arg) ? arg : { data: arg };
|
|
710
|
+
if (data && typeof data === "object" && Object.keys(data).length === 0) {
|
|
711
|
+
data = null;
|
|
712
|
+
}
|
|
713
|
+
|
|
647
714
|
const event_id = uuidv4();
|
|
648
715
|
if (!elem) {
|
|
649
716
|
elem = this.new(data);
|
|
@@ -758,43 +825,47 @@ export class SdcQuerySet {
|
|
|
758
825
|
const value = elem[key];
|
|
759
826
|
if (value instanceof File) {
|
|
760
827
|
toSolve.push(
|
|
761
|
-
new Promise(
|
|
828
|
+
new Promise((resolve, reject) => {
|
|
762
829
|
const event_id = uuidv4();
|
|
763
830
|
this.openRequest[event_id] = [resolve, reject];
|
|
764
831
|
|
|
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
|
-
|
|
832
|
+
value.arrayBuffer().then((buffer) => {
|
|
833
|
+
let result = new Uint8Array(buffer);
|
|
834
|
+
let numberOfChunks = Math.ceil(result.length / MAX_FILE_UPLOAD);
|
|
835
|
+
files[key] = {
|
|
836
|
+
id: event_id,
|
|
837
|
+
file_name: value.name,
|
|
838
|
+
field_name: key,
|
|
839
|
+
content_length: value.size,
|
|
840
|
+
};
|
|
841
|
+
for (let i = 0; i < numberOfChunks; ++i) {
|
|
842
|
+
const chunk = Array.from(result.slice(
|
|
843
|
+
MAX_FILE_UPLOAD * i,
|
|
844
|
+
MAX_FILE_UPLOAD * (i + 1),
|
|
845
|
+
));
|
|
846
|
+
this.socket.send(
|
|
847
|
+
JSON.stringify({
|
|
848
|
+
event: "model",
|
|
849
|
+
event_type: "upload",
|
|
850
|
+
event_id,
|
|
851
|
+
args: {
|
|
852
|
+
chunk,
|
|
853
|
+
idx: i,
|
|
854
|
+
number_of_chunks: numberOfChunks,
|
|
855
|
+
file_name: value.name,
|
|
856
|
+
field_name: key,
|
|
857
|
+
content_length: value.size,
|
|
858
|
+
content_type: value.type,
|
|
859
|
+
model_name: this.modelName,
|
|
860
|
+
model_query: this.modelQuery,
|
|
861
|
+
},
|
|
862
|
+
}),
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
}).catch((err) => {
|
|
866
|
+
this._closeOpenRequest(event_id);
|
|
867
|
+
reject(err);
|
|
868
|
+
});
|
|
798
869
|
}),
|
|
799
870
|
);
|
|
800
871
|
}
|
|
@@ -814,7 +885,7 @@ export class SdcQuerySet {
|
|
|
814
885
|
async _onMessage(e) {
|
|
815
886
|
let data = JSON.parse(e.data);
|
|
816
887
|
if (data.is_error) {
|
|
817
|
-
if (this.openRequest
|
|
888
|
+
if (Object.hasOwn(this.openRequest, data.event_id)) {
|
|
818
889
|
this.openRequest[data.event_id][1](new SdcModelError(data));
|
|
819
890
|
this._closeOpenRequest(data.event_id);
|
|
820
891
|
}
|
|
@@ -838,7 +909,7 @@ export class SdcQuerySet {
|
|
|
838
909
|
this._isConnectingProcess = false;
|
|
839
910
|
this.openRequest[CONNECTING_REQUEST_ID][0](data);
|
|
840
911
|
this._closeOpenRequest(CONNECTING_REQUEST_ID);
|
|
841
|
-
} else if (["load", "named_view", "detail_view"].includes(data.type)) {
|
|
912
|
+
} else if (["load", "list_view", "named_view", "detail_view"].includes(data.type)) {
|
|
842
913
|
const jsonRes = JSON.parse(data.args.data);
|
|
843
914
|
data.args.data = await this._parseServerRes(jsonRes);
|
|
844
915
|
} else if (data.type === "on_update" || data.type === "on_create") {
|
|
@@ -855,6 +926,11 @@ export class SdcQuerySet {
|
|
|
855
926
|
|
|
856
927
|
cb(obj);
|
|
857
928
|
data.args.data = obj;
|
|
929
|
+
} else if (data.type === "on_delete") {
|
|
930
|
+
const ids = JSON.parse(data.args.data).map((x) => normalizePk(x.id ?? x.pk));
|
|
931
|
+
const removed = this._removeIds(ids);
|
|
932
|
+
(this.onDelete ?? this.onUpdate)(removed);
|
|
933
|
+
data.args.data = removed;
|
|
858
934
|
}
|
|
859
935
|
|
|
860
936
|
let instance = data.data?.instance;
|
|
@@ -862,7 +938,7 @@ export class SdcQuerySet {
|
|
|
862
938
|
data.data.instance = JSON.parse(data.data.instance);
|
|
863
939
|
}
|
|
864
940
|
|
|
865
|
-
if (this.openRequest
|
|
941
|
+
if (Object.hasOwn(this.openRequest, data.event_id)) {
|
|
866
942
|
this.openRequest[data.event_id][0](data);
|
|
867
943
|
this._closeOpenRequest(data.event_id);
|
|
868
944
|
}
|
|
@@ -924,14 +1000,17 @@ export class SdcQuerySet {
|
|
|
924
1000
|
`SDC Model (${this.modelName}, ${this.modelId}) Socket closed unexpectedly`,
|
|
925
1001
|
);
|
|
926
1002
|
this._isConnected = false;
|
|
927
|
-
for (const
|
|
1003
|
+
for (const value of Object.values(this.openRequest)) {
|
|
928
1004
|
value[1](e);
|
|
929
1005
|
}
|
|
930
1006
|
this.openRequest = {};
|
|
931
1007
|
|
|
1008
|
+
this._isConnectingProcess = false;
|
|
932
1009
|
setTimeout(() => {
|
|
933
|
-
if (this._autoReconnect) {
|
|
934
|
-
|
|
1010
|
+
if (this._autoReconnect && !this._isConnected) {
|
|
1011
|
+
// isConnected() opens the socket and sends the connect handshake, so
|
|
1012
|
+
// live updates work again and later requests reuse this connection.
|
|
1013
|
+
this.isConnected().catch(() => {
|
|
935
1014
|
});
|
|
936
1015
|
}
|
|
937
1016
|
}, 1000);
|
|
@@ -1028,11 +1107,12 @@ export default class SdcModel {
|
|
|
1028
1107
|
* @returns {SdcQuerySet}
|
|
1029
1108
|
*/
|
|
1030
1109
|
static querySet(modelQuerySet = null, parent = null) {
|
|
1110
|
+
const { modelName } = new this({});
|
|
1031
1111
|
if (!parent) {
|
|
1032
|
-
return new SdcQuerySet(
|
|
1112
|
+
return new SdcQuerySet(modelName, modelQuerySet);
|
|
1033
1113
|
}
|
|
1034
1114
|
|
|
1035
|
-
return parent.querySet(
|
|
1115
|
+
return parent.querySet(modelName, modelQuerySet);
|
|
1036
1116
|
}
|
|
1037
1117
|
|
|
1038
1118
|
addForm($form) {
|
|
@@ -1054,15 +1134,25 @@ export default class SdcModel {
|
|
|
1054
1134
|
_onChange(event) {
|
|
1055
1135
|
const { name } = event.target;
|
|
1056
1136
|
if (this.constructor.fields[name]) {
|
|
1057
|
-
|
|
1137
|
+
try {
|
|
1138
|
+
this[`set${name}`](getValueFromField(event.target));
|
|
1139
|
+
} catch {
|
|
1140
|
+
// Invalid input (e.g. an empty required field) is kept in the form and
|
|
1141
|
+
// validated by the server on submit.
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
// Show the new value in the other forms of this model, not in the input being edited.
|
|
1145
|
+
this._updateForm(name, event.target);
|
|
1058
1146
|
}
|
|
1059
1147
|
}
|
|
1060
1148
|
|
|
1061
|
-
_updateForm(fieldName) {
|
|
1149
|
+
_updateForm(fieldName, skipElement = null) {
|
|
1062
1150
|
const self = this;
|
|
1063
1151
|
this._forms.forEach(($form) => {
|
|
1064
1152
|
$form.find(`[name="${fieldName}"]`).each(function () {
|
|
1065
|
-
|
|
1153
|
+
if (this !== skipElement) {
|
|
1154
|
+
setValueInField(this, self[fieldName]);
|
|
1155
|
+
}
|
|
1066
1156
|
});
|
|
1067
1157
|
});
|
|
1068
1158
|
}
|
|
@@ -1090,7 +1180,15 @@ export default class SdcModel {
|
|
|
1090
1180
|
return this._querySet.deref().save({ pk: this.id, formName, data });
|
|
1091
1181
|
}
|
|
1092
1182
|
|
|
1093
|
-
|
|
1183
|
+
/**
|
|
1184
|
+
* Create this instance in the DB.
|
|
1185
|
+
*
|
|
1186
|
+
* @param {{data?: object} | object | null} values
|
|
1187
|
+
* Either `{ data }` or `data` directly.
|
|
1188
|
+
* @returns {Promise<unknown>}
|
|
1189
|
+
*/
|
|
1190
|
+
create(values = null) {
|
|
1191
|
+
const data = isCreateOptions(values) ? (values.data ?? null) : values;
|
|
1094
1192
|
return this._querySet.deref().create({ elem: this, data });
|
|
1095
1193
|
}
|
|
1096
1194
|
|
|
@@ -1159,6 +1257,9 @@ export default class SdcModel {
|
|
|
1159
1257
|
}
|
|
1160
1258
|
} else if (val.is_relation && value instanceof SdcModel) {
|
|
1161
1259
|
acc[key] = value.id ?? null;
|
|
1260
|
+
} else if (value instanceof Date) {
|
|
1261
|
+
// Django's DateField does not accept a full ISO datetime.
|
|
1262
|
+
acc[key] = val.type === "DateField" ? formatDate(value) : value.toISOString();
|
|
1162
1263
|
} else {
|
|
1163
1264
|
acc[key] = value;
|
|
1164
1265
|
}
|
|
@@ -1219,15 +1320,16 @@ export default class SdcModel {
|
|
|
1219
1320
|
$forms = this._resolveForms($forms);
|
|
1220
1321
|
|
|
1221
1322
|
const self = this;
|
|
1222
|
-
const fields = this.constructor
|
|
1323
|
+
const { fields } = this.constructor;
|
|
1223
1324
|
$forms.each(function () {
|
|
1224
1325
|
const pk = normalizePk($(this).data("model_pk"));
|
|
1225
|
-
|
|
1326
|
+
// Create forms carry model_pk -1 while the new model's id is still null.
|
|
1327
|
+
if (normalizePk(self.id) !== pk) {
|
|
1226
1328
|
return;
|
|
1227
1329
|
}
|
|
1228
1330
|
|
|
1229
1331
|
for (let formItem of this.elements) {
|
|
1230
|
-
let name = formItem
|
|
1332
|
+
let { name } = formItem;
|
|
1231
1333
|
if (name && name !== "" && !!fields[name]) {
|
|
1232
1334
|
setValueInField(formItem, self[name])
|
|
1233
1335
|
}
|
|
@@ -1247,12 +1349,17 @@ export default class SdcModel {
|
|
|
1247
1349
|
$forms = this._resolveForms($forms);
|
|
1248
1350
|
|
|
1249
1351
|
const self = this;
|
|
1250
|
-
const fields = this.constructor
|
|
1352
|
+
const { fields } = this.constructor;
|
|
1251
1353
|
const returnValue = {}
|
|
1252
1354
|
|
|
1253
1355
|
function setValueInForm(name, value) {
|
|
1254
|
-
if (
|
|
1255
|
-
|
|
1356
|
+
if (fields[name]) {
|
|
1357
|
+
try {
|
|
1358
|
+
self[name] = value;
|
|
1359
|
+
} catch {
|
|
1360
|
+
// Invalid values are still sent to the server, which returns the form
|
|
1361
|
+
// errors; the model keeps its last valid value.
|
|
1362
|
+
}
|
|
1256
1363
|
}
|
|
1257
1364
|
returnValue[name] = value;
|
|
1258
1365
|
}
|
|
@@ -1387,14 +1494,21 @@ export default class SdcModel {
|
|
|
1387
1494
|
|
|
1388
1495
|
switch (type) {
|
|
1389
1496
|
case "CharField":
|
|
1390
|
-
case "
|
|
1497
|
+
case "TextField":
|
|
1498
|
+
case "SlugField":
|
|
1391
1499
|
case "UUIDField":
|
|
1392
1500
|
case "EmailField":
|
|
1393
1501
|
return `${value}`;
|
|
1394
1502
|
|
|
1395
1503
|
case "IntegerField":
|
|
1396
|
-
case "
|
|
1504
|
+
case "SmallIntegerField":
|
|
1397
1505
|
case "BigIntegerField":
|
|
1506
|
+
case "PositiveIntegerField":
|
|
1507
|
+
case "PositiveSmallIntegerField":
|
|
1508
|
+
case "PositiveBigIntegerField":
|
|
1509
|
+
case "AutoField":
|
|
1510
|
+
case "SmallAutoField":
|
|
1511
|
+
case "BigAutoField":
|
|
1398
1512
|
return parseInt(value, 10);
|
|
1399
1513
|
|
|
1400
1514
|
case "FloatField":
|
|
@@ -1406,7 +1520,7 @@ export default class SdcModel {
|
|
|
1406
1520
|
|
|
1407
1521
|
case "DateField":
|
|
1408
1522
|
case "DateTimeField":
|
|
1409
|
-
return
|
|
1523
|
+
return toDate(value);
|
|
1410
1524
|
|
|
1411
1525
|
case "URLField":
|
|
1412
1526
|
return new URL(value);
|
|
@@ -1429,14 +1543,7 @@ export default class SdcModel {
|
|
|
1429
1543
|
}
|
|
1430
1544
|
|
|
1431
1545
|
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
|
-
|
|
1546
|
+
// Size and type are checked in validate().
|
|
1440
1547
|
return value;
|
|
1441
1548
|
}
|
|
1442
1549
|
break;
|
|
@@ -1518,8 +1625,14 @@ function validateField(value, config) {
|
|
|
1518
1625
|
break;
|
|
1519
1626
|
|
|
1520
1627
|
case "IntegerField":
|
|
1521
|
-
case "
|
|
1628
|
+
case "SmallIntegerField":
|
|
1522
1629
|
case "BigIntegerField":
|
|
1630
|
+
case "PositiveIntegerField":
|
|
1631
|
+
case "PositiveSmallIntegerField":
|
|
1632
|
+
case "PositiveBigIntegerField":
|
|
1633
|
+
case "AutoField":
|
|
1634
|
+
case "SmallAutoField":
|
|
1635
|
+
case "BigAutoField":
|
|
1523
1636
|
if (!isFinite(parseInt(value))) {
|
|
1524
1637
|
return "Must be an integer";
|
|
1525
1638
|
}
|
|
@@ -1566,14 +1679,26 @@ function validateField(value, config) {
|
|
|
1566
1679
|
break;
|
|
1567
1680
|
|
|
1568
1681
|
case "JSONField":
|
|
1569
|
-
if (typeof value
|
|
1570
|
-
|
|
1682
|
+
if (typeof value === "string") {
|
|
1683
|
+
try {
|
|
1684
|
+
JSON.parse(value);
|
|
1685
|
+
} catch {
|
|
1686
|
+
return "Must be valid JSON";
|
|
1687
|
+
}
|
|
1571
1688
|
}
|
|
1572
1689
|
break;
|
|
1573
1690
|
case "FileField":
|
|
1574
1691
|
if (!FileLoaded.isValid(value) && !(value instanceof File) && !(value instanceof FileLoaded)) {
|
|
1575
1692
|
return "Must be a valid file";
|
|
1576
1693
|
}
|
|
1694
|
+
if (typeof File !== "undefined" && value instanceof File) {
|
|
1695
|
+
if (config.max_size && value.size > config.max_size) {
|
|
1696
|
+
return `File too large (max ${config.max_size} bytes)`;
|
|
1697
|
+
}
|
|
1698
|
+
if (config.allowed_types && !config.allowed_types.includes(value.type)) {
|
|
1699
|
+
return `Invalid file type (${value.type})`;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1577
1702
|
break;
|
|
1578
1703
|
default:
|
|
1579
1704
|
break;
|