@saltcorn/builder 0.8.6-beta.2 → 0.8.6-beta.4

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.
@@ -10,15 +10,15 @@ import optionsCtx from "../context";
10
10
  import previewCtx from "../preview_context";
11
11
 
12
12
  import {
13
- blockProps,
14
- BlockSetting,
15
- MinRoleSetting,
16
13
  fetchViewPreview,
17
14
  ConfigForm,
18
15
  setAPropGen,
19
16
  FormulaTooltip,
20
17
  } from "./utils";
21
18
 
19
+ import { RelationPicker } from "./RelationPicker";
20
+ import { RelationBadges } from "./RelationBadges";
21
+
22
22
  export /**
23
23
  * @param {object} props
24
24
  * @param {*} props.name
@@ -29,45 +29,46 @@ export /**
29
29
  * @subcategory components
30
30
  * @namespace
31
31
  */
32
- const View = ({ name, view, configuration, state }) => {
33
- const {
34
- selected,
35
- node_id,
36
- connectors: { connect, drag },
37
- } = useNode((node) => ({ selected: node.events.selected, node_id: node.id }));
38
- const options = useContext(optionsCtx);
32
+ const View = ({ name, view, configuration, state }) => {
33
+ const {
34
+ selected,
35
+ node_id,
36
+ connectors: { connect, drag },
37
+ } = useNode((node) => ({ selected: node.events.selected, node_id: node.id }));
38
+ const options = useContext(optionsCtx);
39
39
 
40
- const views = options.views;
41
- const theview = views.find((v) => v.name === view);
42
- const label = theview ? theview.label : view;
43
- const { previews, setPreviews } = useContext(previewCtx);
44
- const myPreview = previews[node_id];
45
- useEffect(() => {
46
- fetchViewPreview({
47
- options,
48
- view,
49
- setPreviews,
50
- configuration,
51
- node_id,
52
- })();
53
- }, [view, configuration, state]);
54
- return (
55
- <div
56
- ref={(dom) => connect(drag(dom))}
57
- className={`${myPreview ? "" : "builder-embed-view"} ${selected ? "selected-node" : ""
58
- }`}
59
- >
60
- {myPreview ? (
61
- <div
62
- className="d-inline"
63
- dangerouslySetInnerHTML={{ __html: myPreview }}
64
- ></div>
65
- ) : (
66
- `View: ${label}`
67
- )}
68
- </div>
69
- );
70
- };
40
+ const views = options.views;
41
+ const theview = views.find((v) => v.name === view);
42
+ const label = theview ? theview.label : view;
43
+ const { previews, setPreviews } = useContext(previewCtx);
44
+ const myPreview = previews[node_id];
45
+ useEffect(() => {
46
+ fetchViewPreview({
47
+ options,
48
+ view,
49
+ setPreviews,
50
+ configuration,
51
+ node_id,
52
+ })();
53
+ }, [view, configuration, state]);
54
+ return (
55
+ <div
56
+ ref={(dom) => connect(drag(dom))}
57
+ className={`${myPreview ? "" : "builder-embed-view"} ${
58
+ selected ? "selected-node" : ""
59
+ }`}
60
+ >
61
+ {myPreview ? (
62
+ <div
63
+ className="d-inline"
64
+ dangerouslySetInnerHTML={{ __html: myPreview }}
65
+ ></div>
66
+ ) : (
67
+ `View: ${label}`
68
+ )}
69
+ </div>
70
+ );
71
+ };
71
72
 
72
73
  export /**
73
74
  * @returns {div}
@@ -75,161 +76,186 @@ export /**
75
76
  * @subcategory components
76
77
  * @namespace
77
78
  */
78
- const ViewSettings = () => {
79
- const node = useNode((node) => ({
80
- view_name: node.data.props.view_name,
81
- name: node.data.props.name,
82
- view: node.data.props.view,
83
- state: node.data.props.state,
84
- extra_state_fml: node.data.props.extra_state_fml,
85
- configuration: node.data.props.configuration, // fixed states
86
- node_id: node.id,
87
- }));
79
+ const ViewSettings = () => {
80
+ const node = useNode((node) => ({
81
+ view_name: node.data.props.view_name,
82
+ name: node.data.props.name,
83
+ view: node.data.props.view,
84
+ relation: node.data.props.relation,
85
+ state: node.data.props.state,
86
+ extra_state_fml: node.data.props.extra_state_fml,
87
+ configuration: node.data.props.configuration, // fixed states
88
+ node_id: node.id,
89
+ }));
88
90
 
89
- const {
90
- actions: { setProp },
91
- name,
92
- view,
93
- state,
94
- node_id,
95
- configuration,
96
- extra_state_fml,
97
- view_name,
98
- } = node;
99
- const options = useContext(optionsCtx);
100
- const views = options.views;
101
- const fixed_state_fields =
102
- options.fixed_state_fields && options.fixed_state_fields[view];
103
- const { setPreviews } = useContext(previewCtx);
91
+ const {
92
+ actions: { setProp },
93
+ name,
94
+ view,
95
+ relation,
96
+ state,
97
+ node_id,
98
+ configuration,
99
+ extra_state_fml,
100
+ view_name,
101
+ } = node;
102
+ const options = useContext(optionsCtx);
103
+ const views = options.views;
104
+ const fixed_state_fields =
105
+ options.fixed_state_fields && options.fixed_state_fields[view];
106
+ const { setPreviews } = useContext(previewCtx);
104
107
 
105
- const setAProp = setAPropGen(setProp);
106
- let errorString = false;
107
- try {
108
- Function("return " + extra_state_fml);
109
- } catch (error) {
110
- errorString = error.message;
111
- }
112
- let viewname = view_name || view;
113
- if (viewname && viewname.includes(":")) viewname = viewname.split(":")[1];
114
- if (viewname && viewname.includes(".")) viewname = viewname.split(".")[0];
115
- const set_view_name = (e) => {
116
- if (e.target) {
117
- const target_value = e.target.value;
118
- setProp((prop) => (prop.view_name = target_value));
119
- if (target_value !== viewname) {
120
- setProp((prop) => (prop.view = options.view_relation_opts[target_value][0].value));
108
+ const setAProp = setAPropGen(setProp);
109
+ let errorString = false;
110
+ try {
111
+ Function("return " + extra_state_fml);
112
+ } catch (error) {
113
+ errorString = error.message;
114
+ }
121
115
 
122
- }
116
+ let viewname = view_name || view;
117
+ if (viewname && viewname.includes(":")) {
118
+ const [prefix, rest] = viewname.split(":");
119
+ if (rest.startsWith(".")) viewname = prefix;
120
+ else viewname = rest;
121
+ }
122
+ if (viewname.includes(".")) viewname = viewname.split(".")[0];
123
+
124
+ const set_view_name = (e) => {
125
+ if (e.target) {
126
+ const target_value = e.target.value;
127
+ setProp((prop) => (prop.view_name = target_value));
128
+ if (target_value !== viewname) {
129
+ setProp((prop) => {
130
+ if (options.view_relation_opts[target_value]) {
131
+ prop.view = options.view_relation_opts[target_value][0].value;
132
+ prop.relation = undefined;
133
+ }
134
+ });
123
135
  }
124
- };
125
- return (
126
- <div>
127
- {options.view_name_opts
128
- ? <Fragment>
129
- <div>
130
- <label>View to {options.mode === "show" ? "embed" : "show"}</label>
131
- <select
132
- value={viewname}
133
- className="form-control form-select"
134
- onChange={set_view_name}
135
- onBlur={set_view_name}
136
- >
137
- {options.view_name_opts.map((f, ix) => (
138
- <option key={ix} value={f.name}>
139
- {f.label}
140
- </option>
141
- ))}
142
- </select>
143
- </div>
144
- <div>
145
- <label>Relation</label>
146
- <select
147
- value={view}
148
- className="form-control form-select"
149
- onChange={setAProp("view")}
150
- onBlur={setAProp("view")}
151
- >
152
- {(options.view_relation_opts[viewname] || []).map((f, ix) => (
153
- <option key={ix} value={f.value}>
154
- {f.label}
155
- </option>
156
- ))}
157
- </select>
158
- </div>
159
- </Fragment>
160
- : <div>
136
+ }
137
+ };
138
+
139
+ return (
140
+ <div>
141
+ {options.view_name_opts ? (
142
+ <Fragment>
143
+ <div>
161
144
  <label>View to {options.mode === "show" ? "embed" : "show"}</label>
162
145
  <select
163
- value={view}
146
+ value={viewname}
164
147
  className="form-control form-select"
165
- onChange={setAProp("view")}
166
- onBlur={setAProp("view")}
148
+ onChange={set_view_name}
149
+ onBlur={set_view_name}
167
150
  >
168
- {views.map((f, ix) => (
151
+ {options.view_name_opts.map((f, ix) => (
169
152
  <option key={ix} value={f.name}>
170
- {f.label || f.name}
153
+ {f.label}
171
154
  </option>
172
155
  ))}
173
156
  </select>
174
- </div>}
175
- {options.mode === "page" && (
176
- <Fragment>
177
- <div>
178
- <label>State</label>
179
- <select
180
- value={state}
181
- className="form-control form-select"
182
- onChange={setAProp("state")}
183
- onBlur={setAProp("state")}
184
- >
185
- <option value="shared">Shared</option>
186
- <option value="fixed">Fixed</option>
187
- </select>
188
- </div>
189
- {state === "fixed" &&
190
- fixed_state_fields &&
191
- fixed_state_fields.length > 0 && (
192
- <Fragment>
193
- <h6>View state fields</h6>
194
- <ConfigForm
195
- fields={fixed_state_fields}
196
- configuration={configuration || {}}
197
- setProp={setProp}
198
- node={node}
199
- />
200
- </Fragment>
201
- )}
202
- </Fragment>
203
- )}
204
- {(state === "shared" || options.mode === "page") && (
205
- <Fragment>
206
- {" "}
207
- <label>Extra state Formula <FormulaTooltip /></label>
208
- <input
209
- type="text"
210
- className="viewlink-label form-control"
211
- value={extra_state_fml}
212
- onChange={setAProp("extra_state_fml")}
213
- />
214
- {errorString ? (
215
- <small className="text-danger font-monospace d-block">
216
- {errorString}
217
- </small>
218
- ) : null}
219
- </Fragment>
220
- )}
221
- {view ? (
222
- <a
223
- className="d-block mt-2"
224
- target="_blank"
225
- href={`/viewedit/config/${viewname}`}
157
+ </div>
158
+ <RelationPicker
159
+ options={options}
160
+ viewname={viewname}
161
+ update={(relPath) => {
162
+ if (relPath.startsWith(".")) {
163
+ setProp((prop) => {
164
+ prop.view = viewname;
165
+ prop.relation = relPath;
166
+ });
167
+ } else {
168
+ setProp((prop) => {
169
+ prop.view = relPath;
170
+ prop.relation = undefined;
171
+ });
172
+ }
173
+ }}
174
+ />
175
+ <RelationBadges
176
+ view={view}
177
+ relation={relation}
178
+ parentTbl={options.tableName}
179
+ fk_options={options.fk_options}
180
+ />
181
+ </Fragment>
182
+ ) : (
183
+ <div>
184
+ <label>View to {options.mode === "show" ? "embed" : "show"}</label>
185
+ <select
186
+ value={view}
187
+ className="form-control form-select"
188
+ onChange={setAProp("view")}
189
+ onBlur={setAProp("view")}
226
190
  >
227
- Configure this view
228
- </a>
229
- ) : null}
230
- </div>
231
- );
232
- };
191
+ {views.map((f, ix) => (
192
+ <option key={ix} value={f.name}>
193
+ {f.label || f.name}
194
+ </option>
195
+ ))}
196
+ </select>
197
+ </div>
198
+ )}
199
+ {options.mode === "page" && (
200
+ <Fragment>
201
+ <div>
202
+ <label>State</label>
203
+ <select
204
+ value={state}
205
+ className="form-control form-select"
206
+ onChange={setAProp("state")}
207
+ onBlur={setAProp("state")}
208
+ >
209
+ <option value="shared">Shared</option>
210
+ <option value="fixed">Fixed</option>
211
+ </select>
212
+ </div>
213
+ {state === "fixed" &&
214
+ fixed_state_fields &&
215
+ fixed_state_fields.length > 0 && (
216
+ <Fragment>
217
+ <h6>View state fields</h6>
218
+ <ConfigForm
219
+ fields={fixed_state_fields}
220
+ configuration={configuration || {}}
221
+ setProp={setProp}
222
+ node={node}
223
+ />
224
+ </Fragment>
225
+ )}
226
+ </Fragment>
227
+ )}
228
+ {(state === "shared" || options.mode === "page") && (
229
+ <Fragment>
230
+ {" "}
231
+ <label>
232
+ Extra state Formula <FormulaTooltip />
233
+ </label>
234
+ <input
235
+ type="text"
236
+ className="viewlink-label form-control"
237
+ value={extra_state_fml}
238
+ onChange={setAProp("extra_state_fml")}
239
+ />
240
+ {errorString ? (
241
+ <small className="text-danger font-monospace d-block">
242
+ {errorString}
243
+ </small>
244
+ ) : null}
245
+ </Fragment>
246
+ )}
247
+ {view ? (
248
+ <a
249
+ className="d-block mt-2"
250
+ target="_blank"
251
+ href={`/viewedit/config/${viewname}`}
252
+ >
253
+ Configure this view
254
+ </a>
255
+ ) : null}
256
+ </div>
257
+ );
258
+ };
233
259
 
234
260
  /**
235
261
  * @type {object}
@@ -8,7 +8,6 @@ import React, { useContext } from "react";
8
8
  import { useNode } from "@craftjs/core";
9
9
  import optionsCtx from "../context";
10
10
  import {
11
- blockProps,
12
11
  BlockSetting,
13
12
  MinRoleSettingRow,
14
13
  OrFormula,
@@ -18,6 +17,9 @@ import {
18
17
  FormulaTooltip,
19
18
  } from "./utils";
20
19
 
20
+ import { RelationPicker } from "./RelationPicker";
21
+ import { RelationBadges } from "./RelationBadges";
22
+
21
23
  export /**
22
24
  * @param {object} props
23
25
  * @param {string} props.name
@@ -91,6 +93,7 @@ export /**
91
93
  const ViewLinkSettings = () => {
92
94
  const node = useNode((node) => ({
93
95
  name: node.data.props.name,
96
+ relation: node.data.props.relation,
94
97
  block: node.data.props.block,
95
98
  minRole: node.data.props.minRole,
96
99
  isFormula: node.data.props.isFormula,
@@ -110,6 +113,7 @@ const ViewLinkSettings = () => {
110
113
  const {
111
114
  actions: { setProp },
112
115
  name,
116
+ relation,
113
117
  block,
114
118
  minRole,
115
119
  label,
@@ -138,10 +142,10 @@ const ViewLinkSettings = () => {
138
142
  const target_value = e.target.value;
139
143
  setProp((prop) => (prop.view_name = target_value));
140
144
  if (target_value !== use_view_name) {
141
- setProp(
142
- (prop) =>
143
- (prop.name = options.view_relation_opts[target_value][0].value)
144
- );
145
+ setProp((prop) => {
146
+ prop.name = options.view_relation_opts[target_value][0].value;
147
+ prop.relation = undefined;
148
+ });
145
149
  }
146
150
  }
147
151
  };
@@ -168,21 +172,29 @@ const ViewLinkSettings = () => {
168
172
  </tr>
169
173
  <tr>
170
174
  <td colSpan="2">
171
- <label>Relation</label>
172
- <select
173
- value={name}
174
- className="form-control form-select"
175
- onChange={setAProp("name")}
176
- onBlur={setAProp("name")}
177
- >
178
- {(options.view_relation_opts[use_view_name] || []).map(
179
- (f, ix) => (
180
- <option key={ix} value={f.value}>
181
- {f.label}
182
- </option>
183
- )
184
- )}
185
- </select>
175
+ <RelationPicker
176
+ options={options}
177
+ viewname={use_view_name}
178
+ update={(relPath) => {
179
+ if (relPath.startsWith(".")) {
180
+ setProp((prop) => {
181
+ prop.name = use_view_name;
182
+ prop.relation = relPath;
183
+ });
184
+ } else {
185
+ setProp((prop) => {
186
+ prop.name = relPath;
187
+ prop.relation = undefined;
188
+ });
189
+ }
190
+ }}
191
+ />
192
+ <RelationBadges
193
+ view={name}
194
+ relation={relation}
195
+ parentTbl={options.tableName}
196
+ fk_options={options.fk_options}
197
+ />
186
198
  </td>
187
199
  </tr>
188
200
  <tr>
@@ -272,6 +284,7 @@ ViewLink.craft = {
272
284
  column_type: "ViewLink",
273
285
  fields: [
274
286
  { name: "name", segment_name: "view", column_name: "view" },
287
+ "relation",
275
288
  { name: "label", segment_name: "view_label", canBeFormula: true },
276
289
  "block",
277
290
  "textStyle",
@@ -471,11 +471,15 @@ export const fetchViewPreview =
471
471
  let viewname,
472
472
  body = configuration ? { ...configuration } : {};
473
473
  if (view.includes(":")) {
474
- const [reltype, rest] = view.split(":");
475
- const [vnm] = rest.split(".");
476
- viewname = vnm;
477
- body.reltype = reltype;
478
- body.path = rest;
474
+ const [prefix, rest] = view.split(":");
475
+ const tokens = rest.split(".");
476
+ if (rest.startsWith(".")) {
477
+ viewname = prefix;
478
+ } else {
479
+ viewname = tokens[0];
480
+ body.reltype = prefix;
481
+ body.path = rest;
482
+ }
479
483
  } else viewname = view;
480
484
 
481
485
  fetchPreview({
@@ -1319,3 +1323,88 @@ const Tooltip = ({ children }) => {
1319
1323
  </Tippy>
1320
1324
  );
1321
1325
  };
1326
+
1327
+ const getFkTarget = (field, options) => {
1328
+ const option = options.find((fk) => fk.name === field);
1329
+ return option ? option.reftable_name : null;
1330
+ };
1331
+
1332
+ export const parseRelationPath = (path, fk_options) => {
1333
+ const result = [];
1334
+ const tokens = path.split(".");
1335
+ if (tokens.length >= 3) {
1336
+ let currentTbl = tokens[1];
1337
+ for (const relation of tokens.slice(2)) {
1338
+ if (relation.indexOf("$") > 0) {
1339
+ const [inboundTbl, inboundKey] = relation.split("$");
1340
+ result.push({ type: "Inbound", table: inboundTbl, key: inboundKey });
1341
+ currentTbl = inboundTbl;
1342
+ } else {
1343
+ const targetTbl = getFkTarget(relation, fk_options[currentTbl]);
1344
+ if (!targetTbl) {
1345
+ console.log(`The foreign key '${relation}' is invalid`);
1346
+ return [];
1347
+ }
1348
+ result.push({ type: "Foreign", table: targetTbl, key: relation });
1349
+ currentTbl = targetTbl;
1350
+ }
1351
+ }
1352
+ }
1353
+ return result;
1354
+ };
1355
+
1356
+ export const parseLegacyRelation = (type, rest, parentTbl) => {
1357
+ switch (type) {
1358
+ case "ChildList": {
1359
+ const path = rest ? rest.split(".") : [];
1360
+ if (path.length === 3) {
1361
+ const [viewName, table, key] = path;
1362
+ return [
1363
+ {
1364
+ type: "Inbound",
1365
+ table,
1366
+ key,
1367
+ },
1368
+ ];
1369
+ } else if (path.length === 5) {
1370
+ const [viewName, thrTbl, thrTblFkey, fromTbl, fromTblFkey] = path;
1371
+ return [
1372
+ {
1373
+ type: "Inbound",
1374
+ table: thrTbl,
1375
+ key: thrTblFkey,
1376
+ },
1377
+ {
1378
+ type: "Inbound",
1379
+ table: fromTbl,
1380
+ key: fromTblFkey,
1381
+ },
1382
+ ];
1383
+ }
1384
+ break;
1385
+ }
1386
+ case "Independent": {
1387
+ return [{ type: "Independent", table: "None (no relation)" }];
1388
+ }
1389
+ case "Own": {
1390
+ return [{ type: "Own", table: `${parentTbl} (same table)` }];
1391
+ }
1392
+ case "OneToOneShow": {
1393
+ const tokens = rest ? rest.split(".") : [];
1394
+ if (tokens.length !== 3) break;
1395
+ const [viewname, relatedTbl, fkey] = tokens;
1396
+ return [{ type: "Inbound", table: relatedTbl, key: fkey }];
1397
+ }
1398
+ case "ParentShow": {
1399
+ const tokens = rest ? rest.split(".") : [];
1400
+ if (tokens.length !== 3) break;
1401
+ const [viewname, parentTbl, fkey] = tokens;
1402
+ return [{ type: "Foreign", table: parentTbl, key: fkey }];
1403
+ }
1404
+ }
1405
+ return [];
1406
+ };
1407
+
1408
+ export const removeWhitespaces = (str) => {
1409
+ return str.replace(/\s/g, "X");
1410
+ };
@@ -142,6 +142,7 @@ export /**
142
142
  <View
143
143
  key={ix}
144
144
  view={segment.view}
145
+ relation={segment.relation}
145
146
  view_name={segment.view_name}
146
147
  name={segment.name}
147
148
  state={segment.state}
@@ -457,6 +458,7 @@ export /**
457
458
  return {
458
459
  type: "view",
459
460
  view: node.props.view,
461
+ relation: node.props.relation,
460
462
  name:
461
463
  node.props.name === "not_assigned" ? rand_ident() : node.props.name,
462
464
  state: node.props.state,