@saltcorn/builder 0.9.4-beta.8 → 0.9.4-beta.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/builder",
3
- "version": "0.9.4-beta.8",
3
+ "version": "0.9.4-beta.9",
4
4
  "description": "Drag and drop view builder for Saltcorn, open-source no-code platform",
5
5
  "main": "index.js",
6
6
  "homepage": "https://saltcorn.com",
@@ -20,6 +20,7 @@
20
20
  "@babel/preset-react": "7.9.4",
21
21
  "@craftjs/core": "0.1.0-beta.20",
22
22
  "@craftjs/utils": "0.1.0-beta.20",
23
+ "@saltcorn/common-code": "0.9.4-beta.8",
23
24
  "saltcorn-craft-layers-noeye": "0.1.0-beta.22",
24
25
  "@fonticonpicker/react-fonticonpicker": "1.2.0",
25
26
  "@fortawesome/fontawesome-svg-core": "1.2.34",
@@ -52,6 +52,7 @@ import {
52
52
  faRedo,
53
53
  faTrashAlt,
54
54
  faSave,
55
+ faExclamationTriangle,
55
56
  } from "@fortawesome/free-solid-svg-icons";
56
57
  import {
57
58
  faCaretSquareLeft,
@@ -394,7 +395,7 @@ const Builder = ({ options, layout, mode }) => {
394
395
  const [previews, setPreviews] = useState({});
395
396
  const [uploadedFiles, setUploadedFiles] = useState([]);
396
397
  const nodekeys = useRef([]);
397
- const [isSaving, setIsSaving] = useState(false);
398
+ const [savingState, setSavingState] = useState({ isSaving: false });
398
399
  const [isEnlarged, setIsEnlarged] = useState(false);
399
400
  const [isLeftEnlarged, setIsLeftEnlarged] = useState(false);
400
401
  const [relationsCache, setRelationsCache] = useState({});
@@ -422,7 +423,8 @@ const Builder = ({ options, layout, mode }) => {
422
423
  <div className="componets-and-library-accordion toolbox-card">
423
424
  <InitNewElement
424
425
  nodekeys={nodekeys}
425
- setIsSaving={setIsSaving}
426
+ setSavingState={setSavingState}
427
+ savingState={savingState}
426
428
  />
427
429
  <Accordion>
428
430
  <div className="card mt-1" accordiontitle="Components">
@@ -503,7 +505,12 @@ const Builder = ({ options, layout, mode }) => {
503
505
  <HistoryPanel />
504
506
  <FontAwesomeIcon
505
507
  icon={faSave}
506
- className={isSaving ? "d-inline" : "d-none"}
508
+ className={savingState.isSaving ? "d-inline" : "d-none"}
509
+ />
510
+ <FontAwesomeIcon
511
+ icon={faExclamationTriangle}
512
+ color="#ff0033"
513
+ className={savingState.error ? "d-inline" : "d-none"}
507
514
  />
508
515
  <FontAwesomeIcon
509
516
  icon={isEnlarged ? faCaretSquareRight : faCaretSquareLeft}
@@ -511,7 +518,13 @@ const Builder = ({ options, layout, mode }) => {
511
518
  onClick={() => setIsEnlarged(!isEnlarged)}
512
519
  title={isEnlarged ? "Shrink" : "Enlarge"}
513
520
  />
514
-
521
+ <div
522
+ className={` ${
523
+ savingState.error ? "d-block" : "d-none"
524
+ } my-2 fw-bold`}
525
+ >
526
+ your work is not being saved
527
+ </div>
515
528
  <SettingsPanel />
516
529
  </div>
517
530
  </div>
@@ -83,7 +83,7 @@ export /**
83
83
  * @subcategory components
84
84
  * @namespace
85
85
  */
86
- const InitNewElement = ({ nodekeys, setIsSaving }) => {
86
+ const InitNewElement = ({ nodekeys, savingState, setSavingState }) => {
87
87
  const [saveTimeout, setSaveTimeout] = useState(false);
88
88
  const savedData = useRef(false);
89
89
  const { actions, query, connectors } = useEditor((state, query) => {
@@ -103,7 +103,7 @@ const InitNewElement = ({ nodekeys, setIsSaving }) => {
103
103
  }
104
104
  if (isEqual(savedData.current, JSON.stringify(data.layout))) return;
105
105
  savedData.current = JSON.stringify(data.layout);
106
- setIsSaving(true);
106
+ setSavingState({ isSaving: true });
107
107
 
108
108
  fetch(`/${urlroot}/savebuilder/${options.page_id || options.view_id}`, {
109
109
  method: "POST", // or 'PUT'
@@ -112,9 +112,29 @@ const InitNewElement = ({ nodekeys, setIsSaving }) => {
112
112
  "CSRF-Token": options.csrfToken,
113
113
  },
114
114
  body: JSON.stringify(data),
115
- }).then(() => {
116
- setIsSaving(false);
117
- });
115
+ })
116
+ .then((response) => {
117
+ response.json().then((data) => {
118
+ if (typeof data?.error === "string") {
119
+ // don't log duplicates
120
+ if (!savingState.error)
121
+ window.notifyAlert({ type: "danger", text: data.error });
122
+ setSavingState({ isSaving: false, error: data.error });
123
+ } else setSavingState({ isSaving: false });
124
+ });
125
+ })
126
+ .catch((e) => {
127
+ const text = e || "Unable to save";
128
+ // don't log duplicates
129
+ if (savingState.error) setSavingState({ isSaving: false, error: text });
130
+ else {
131
+ window.notifyAlert({ type: "danger", text: text });
132
+ setSavingState({
133
+ isSaving: false,
134
+ error: text,
135
+ });
136
+ }
137
+ });
118
138
  };
119
139
  const throttledSave = useThrottle(() => {
120
140
  doSave(query);
@@ -1,33 +1,49 @@
1
1
  import React from "react";
2
2
  import { removeWhitespaces } from "./utils";
3
+ import { parseLegacyRelation, RelationType } from "@saltcorn/common-code";
3
4
 
4
- const buildBadgeCfgs = (parsed, parentTbl) => {
5
- const result = [];
6
- let currentCfg = null;
7
- for (const { type, table, key } of parsed) {
8
- if (type === "Inbound") {
9
- if (currentCfg) result.push(currentCfg);
10
- currentCfg = { up: key, table };
11
- } else {
12
- if (!currentCfg && key) result.push({ down: key, table: parentTbl });
13
- else if (currentCfg) {
14
- currentCfg.down = key;
15
- result.push(currentCfg);
5
+ const buildBadgeCfgs = (sourceTblName, type, path, caches) => {
6
+ if (type === RelationType.OWN)
7
+ return [{ table: `${sourceTblName} (same table)` }];
8
+ else if (type === RelationType.INDEPENDENT)
9
+ return [{ table: "None (no relation)" }];
10
+ else if (path.length === 0) return [{ table: "invalid relation" }];
11
+ else {
12
+ const result = [];
13
+ let currentCfg = null;
14
+ let currentTbl = sourceTblName;
15
+ for (const pathElement of path) {
16
+ if (pathElement.inboundKey) {
17
+ if (currentCfg) result.push(currentCfg);
18
+ currentTbl = pathElement.table;
19
+ currentCfg = { up: pathElement.inboundKey, table: currentTbl };
20
+ } else if (pathElement.fkey) {
21
+ if (!currentCfg)
22
+ result.push({ down: pathElement.fkey, table: currentTbl });
23
+ else {
24
+ currentCfg.down = pathElement.fkey;
25
+ result.push(currentCfg);
26
+ }
27
+ const tblObj = caches.tableNameCache[currentTbl];
28
+ const fkey = tblObj.foreign_keys.find(
29
+ (key) => key.name === pathElement.fkey
30
+ );
31
+ currentTbl = fkey.reftable_name;
32
+ currentCfg = { table: currentTbl };
16
33
  }
17
- currentCfg = { table };
18
34
  }
19
- }
20
- if (
21
- currentCfg &&
22
- !result.find(
23
- ({ down, table, up }) =>
24
- down === currentCfg.down &&
25
- table === currentCfg.table &&
26
- up === currentCfg.up
35
+ if (
36
+ currentCfg &&
37
+ !result.find(
38
+ ({ down, table, up }) =>
39
+ down === currentCfg.down &&
40
+ table === currentCfg.table &&
41
+ up === currentCfg.up
42
+ )
27
43
  )
28
- )
29
- result.push(currentCfg);
30
- return result;
44
+ result.push(currentCfg);
45
+ return result;
46
+ }
31
47
  };
32
48
 
33
49
  const buildBadge = ({ up, table, down }, index) => {
@@ -59,41 +75,34 @@ const buildBadge = ({ up, table, down }, index) => {
59
75
  );
60
76
  };
61
77
 
62
- export const RelationBadges = ({
63
- view,
64
- relation,
65
- parentTbl,
66
- tableNameCache,
67
- }) => {
78
+ export const RelationBadges = ({ view, relation, parentTbl, caches }) => {
68
79
  if (relation) {
69
- const parsed = relationHelpers.parseRelationPath(relation, tableNameCache);
70
-
71
80
  return (
72
81
  <div className="overflow-scroll">
73
- {parsed.length > 0
74
- ? buildBadgeCfgs(parsed, parentTbl).map(buildBadge)
75
- : buildBadge({ table: "invalid relation" }, 0)}
82
+ {buildBadgeCfgs(
83
+ relation.sourceTblName,
84
+ relation.type,
85
+ relation.path,
86
+ caches
87
+ ).map(buildBadge)}
76
88
  </div>
77
89
  );
78
90
  } else {
79
91
  if (!view) return buildBadge({ table: "invalid relation" }, 0);
80
92
  const [prefix, rest] = view.split(":");
81
- const parsed = relationHelpers.parseLegacyRelation(prefix, rest, parentTbl);
82
- if (parsed.length === 0)
83
- return buildBadge({ table: "invalid relation" }, 0);
84
- else if (
85
- parsed.length === 1 &&
86
- (parsed[0].type === "Independent" || parsed[0].type === "Own")
87
- )
93
+ if (!rest) return buildBadge({ table: "invalid relation" }, 0);
94
+ const { type, path } = parseLegacyRelation(prefix, rest, parentTbl);
95
+ if (path.length === 0) return buildBadge({ table: "invalid relation" }, 0);
96
+ else if (path.length === 1 && (type === "Independent" || type === "Own"))
88
97
  return (
89
98
  <div className="overflow-scroll">
90
- {buildBadge({ table: parsed[0].table }, 0)}
99
+ {buildBadge({ table: path[0].table }, 0)}
91
100
  </div>
92
101
  );
93
102
  else
94
103
  return (
95
104
  <div className="overflow-scroll">
96
- {buildBadgeCfgs(parsed, parentTbl).map(buildBadge)}
105
+ {buildBadgeCfgs(parentTbl, type, path, caches).map(buildBadge)}
97
106
  </div>
98
107
  );
99
108
  }
@@ -424,7 +424,9 @@ const TabsSettings = () => {
424
424
  />
425
425
  </td>
426
426
  </tr>
427
- {options.mode === "show" || options.mode === "edit" ? (
427
+ {options.mode === "show" ||
428
+ options.mode === "edit" ||
429
+ options.mode === "filter" ? (
428
430
  <Fragment>
429
431
  <tr>
430
432
  <th colSpan="2">Show if formula</th>
@@ -17,13 +17,18 @@ import {
17
17
  buildOptions,
18
18
  HelpTopicLink,
19
19
  initialRelation,
20
- prepCacheAndFinder,
21
- updateRelationsCache,
20
+ buildLayers,
22
21
  } from "./utils";
23
22
 
24
23
  import { RelationBadges } from "./RelationBadges";
25
24
  import { RelationOnDemandPicker } from "./RelationOnDemandPicker";
26
25
 
26
+ import {
27
+ RelationsFinder,
28
+ Relation,
29
+ buildTableCaches,
30
+ } from "@saltcorn/common-code";
31
+
27
32
  export /**
28
33
  * @param {object} props
29
34
  * @param {*} props.name
@@ -48,7 +53,6 @@ const View = ({ name, view, configuration, state }) => {
48
53
  if (rest.startsWith(".")) viewname = prefix;
49
54
  else viewname = rest;
50
55
  }
51
-
52
56
  const theview = options.views.find((v) => v.name === viewname);
53
57
  const label = theview ? theview.label : view;
54
58
  const { previews, setPreviews } = React.useContext(previewCtx);
@@ -109,10 +113,23 @@ const ViewSettings = () => {
109
113
  extra_state_fml,
110
114
  } = node;
111
115
  const options = React.useContext(optionsCtx);
112
- const { caches, finder } = useMemo(
113
- () => prepCacheAndFinder(options),
114
- [undefined]
115
- );
116
+ const {
117
+ tables,
118
+ views,
119
+ max_relations_layer_depth,
120
+ tableName,
121
+ excluded_subview_templates,
122
+ } = options;
123
+ // not needed in page editor
124
+ let finder = null;
125
+ let tableCaches = null;
126
+ if (tables && views) {
127
+ finder = useMemo(
128
+ () => new RelationsFinder(tables, views, max_relations_layer_depth),
129
+ [undefined]
130
+ );
131
+ tableCaches = useMemo(() => buildTableCaches(tables), [undefined]);
132
+ }
116
133
  const fixed_state_fields =
117
134
  options.fixed_state_fields && options.fixed_state_fields[view];
118
135
  const { setPreviews } = React.useContext(previewCtx);
@@ -135,27 +152,48 @@ const ViewSettings = () => {
135
152
  else viewname = rest;
136
153
  }
137
154
  if (viewname.includes(".")) viewname = viewname.split(".")[0];
138
- if (finder)
139
- updateRelationsCache(
140
- relationsCache,
141
- setRelationsCache,
142
- options,
143
- finder,
144
- viewname
155
+
156
+ if (
157
+ finder &&
158
+ !(relationsCache[tableName] && relationsCache[tableName][viewname])
159
+ ) {
160
+ const relations = finder.findRelations(
161
+ tableName,
162
+ viewname,
163
+ excluded_subview_templates
145
164
  );
146
- const [relations, setRelations] = finder
147
- ? React.useState(relationsCache[options.tableName][viewname])
165
+ const layers = buildLayers(
166
+ relations,
167
+ tableName,
168
+ tableCaches.tableNameCache
169
+ );
170
+ relationsCache[tableName] = relationsCache[tableName] || {};
171
+ relationsCache[tableName][viewname] = { relations, layers };
172
+ setRelationsCache({ ...relationsCache });
173
+ }
174
+ const [relationsData, setRelationsData] = finder
175
+ ? React.useState(relationsCache[tableName][viewname])
148
176
  : [undefined, undefined];
149
- let safeRelation = relation;
177
+ let safeRelation = null;
178
+ const subView = views.find((view) => view.name === viewname);
179
+ if (relation) {
180
+ const subTbl = tables.find((tbl) => tbl.id === subView.table_id);
181
+ safeRelation = new Relation(
182
+ relation,
183
+ subTbl ? subTbl.name : "",
184
+ subView.display_type
185
+ );
186
+ }
150
187
  if (
151
188
  options.mode !== "filter" &&
189
+ subView.table_id &&
152
190
  !safeRelation &&
153
191
  !hasLegacyRelation &&
154
- relations?.paths.length > 0
192
+ relationsData?.relations.length > 0
155
193
  ) {
156
- safeRelation = initialRelation(relations.paths, options.tableName);
194
+ safeRelation = initialRelation(relationsData.relations);
157
195
  setProp((prop) => {
158
- prop.relation = safeRelation;
196
+ prop.relation = safeRelation.relationString;
159
197
  });
160
198
  }
161
199
  const helpContext = { view_name: viewname };
@@ -169,24 +207,32 @@ const ViewSettings = () => {
169
207
  prop.view = target_value;
170
208
  });
171
209
  } else {
172
- updateRelationsCache(
173
- relationsCache,
174
- setRelationsCache,
175
- options,
176
- finder,
177
- target_value
210
+ const newRelations = finder.findRelations(
211
+ tableName,
212
+ target_value,
213
+ excluded_subview_templates
178
214
  );
179
- const newRelations = relationsCache[options.tableName][target_value];
180
- if (newRelations.paths.length > 0) {
215
+ const layers = buildLayers(
216
+ newRelations,
217
+ tableName,
218
+ tableCaches.tableNameCache
219
+ );
220
+ relationsCache[tableName] = relationsCache[tableName] || {};
221
+ relationsCache[tableName][target_value] = {
222
+ relations: newRelations,
223
+ layers,
224
+ };
225
+ if (newRelations.length > 0) {
181
226
  setProp((prop) => {
182
227
  prop.view = target_value;
183
- prop.relation = initialRelation(
184
- newRelations.paths,
185
- options.tableName
186
- );
228
+ prop.relation = initialRelation(newRelations).relationString;
229
+ });
230
+ setRelationsData({ relations: newRelations, layers });
231
+ } else
232
+ window.notifyAlert({
233
+ type: "warning",
234
+ text: `${target_value} has no relations`,
187
235
  });
188
- setRelations(newRelations);
189
- }
190
236
  }
191
237
  }
192
238
  }
@@ -194,7 +240,7 @@ const ViewSettings = () => {
194
240
 
195
241
  return (
196
242
  <div>
197
- {relations ? (
243
+ {relationsData ? (
198
244
  <Fragment>
199
245
  <div>
200
246
  <label>View to {options.mode === "show" ? "embed" : "show"}</label>
@@ -214,7 +260,7 @@ const ViewSettings = () => {
214
260
  {options.mode !== "filter" && (
215
261
  <div>
216
262
  <RelationOnDemandPicker
217
- relations={relations.layers}
263
+ relations={relationsData.layers}
218
264
  update={(relPath) => {
219
265
  if (relPath.startsWith(".")) {
220
266
  setProp((prop) => {
@@ -232,8 +278,8 @@ const ViewSettings = () => {
232
278
  <RelationBadges
233
279
  view={view}
234
280
  relation={safeRelation}
235
- parentTbl={options.tableName}
236
- tableNameCache={caches.tableNameCache}
281
+ parentTbl={tableName}
282
+ caches={tableCaches}
237
283
  />
238
284
  </div>
239
285
  )}
@@ -18,14 +18,19 @@ import {
18
18
  setAPropGen,
19
19
  FormulaTooltip,
20
20
  HelpTopicLink,
21
- prepCacheAndFinder,
22
21
  initialRelation,
23
- updateRelationsCache,
22
+ buildLayers,
24
23
  } from "./utils";
25
24
 
26
25
  import { RelationBadges } from "./RelationBadges";
27
26
  import { RelationOnDemandPicker } from "./RelationOnDemandPicker";
28
27
 
28
+ import {
29
+ RelationsFinder,
30
+ Relation,
31
+ buildTableCaches,
32
+ } from "@saltcorn/common-code";
33
+
29
34
  export /**
30
35
  * @param {object} props
31
36
  * @param {string} props.name
@@ -127,10 +132,18 @@ const ViewLinkSettings = () => {
127
132
  link_target_blank,
128
133
  } = node;
129
134
  const options = React.useContext(optionsCtx);
130
- const { caches, finder } = useMemo(
131
- () => prepCacheAndFinder(options),
135
+ const {
136
+ tables,
137
+ views,
138
+ tableName,
139
+ excluded_subview_templates,
140
+ max_relations_layer_depth,
141
+ } = options;
142
+ const finder = useMemo(
143
+ () => new RelationsFinder(tables, views, max_relations_layer_depth),
132
144
  [undefined]
133
145
  );
146
+ const tableCaches = useMemo(() => buildTableCaches(tables), [undefined]);
134
147
  const { relationsCache, setRelationsCache } = React.useContext(relationsCtx);
135
148
  let errorString = false;
136
149
  try {
@@ -147,50 +160,82 @@ const ViewLinkSettings = () => {
147
160
  const safeViewName = use_view_name?.includes(".")
148
161
  ? use_view_name.split(".")[0]
149
162
  : use_view_name;
150
- updateRelationsCache(
151
- relationsCache,
152
- setRelationsCache,
153
- options,
154
- finder,
155
- safeViewName
156
- );
157
- const [relations, setRelations] = React.useState(
163
+ const subView = views.find((view) => view.name === safeViewName);
164
+ const hasTableId = subView?.table_id !== undefined;
165
+ if (!(relationsCache[tableName] && relationsCache[tableName][safeViewName])) {
166
+ const relations = finder.findRelations(
167
+ tableName,
168
+ safeViewName,
169
+ excluded_subview_templates
170
+ );
171
+ const layers = buildLayers(
172
+ relations,
173
+ tableName,
174
+ tableCaches.tableNameCache
175
+ );
176
+ relationsCache[tableName] = relationsCache[tableName] || {};
177
+ relationsCache[tableName][safeViewName] = { relations, layers };
178
+ setRelationsCache({ ...relationsCache });
179
+ }
180
+ const [relationsData, setRelationsData] = React.useState(
158
181
  relationsCache[options.tableName][safeViewName]
159
182
  );
160
- let safeRelation = relation;
161
- if (!safeRelation && !hasLegacyRelation && relations?.paths.length > 0) {
162
- safeRelation = initialRelation(relations.paths, options.tableName);
183
+ let safeRelation = null;
184
+ if (relation) {
185
+ const subView = views.find((view) => view.name === safeViewName);
186
+ const subTbl = tables.find((tbl) => tbl.id === subView.table_id);
187
+ safeRelation = new Relation(
188
+ relation,
189
+ subTbl ? subTbl.name : "",
190
+ subView.display_type
191
+ );
192
+ }
193
+ if (
194
+ !safeRelation &&
195
+ !hasLegacyRelation &&
196
+ relationsData?.relations.length > 0
197
+ ) {
198
+ safeRelation = initialRelation(relationsData.relations);
163
199
  setProp((prop) => {
164
- prop.relation = safeRelation;
200
+ prop.relation = safeRelation.relationString;
165
201
  });
166
202
  }
167
203
  const set_view_name = (e) => {
168
204
  if (e.target) {
169
205
  const target_value = e.target.value;
170
206
  if (target_value !== use_view_name) {
171
- updateRelationsCache(
172
- relationsCache,
173
- setRelationsCache,
174
- options,
175
- finder,
176
- target_value
207
+ const newRelations = finder.findRelations(
208
+ tableName,
209
+ target_value,
210
+ excluded_subview_templates
211
+ );
212
+ const layers = buildLayers(
213
+ newRelations,
214
+ tableName,
215
+ tableCaches.tableNameCache
177
216
  );
178
- const newRelations = relationsCache[options.tableName][target_value];
179
- if (newRelations.paths.length > 0) {
217
+
218
+ relationsCache[tableName] = relationsCache[tableName] || {};
219
+ relationsCache[tableName][target_value] = {
220
+ relations: newRelations,
221
+ layers,
222
+ };
223
+ if (newRelations.length > 0) {
180
224
  setProp((prop) => {
181
225
  prop.name = target_value;
182
- prop.relation = initialRelation(
183
- newRelations.paths,
184
- options.tableName
185
- );
226
+ prop.relation = initialRelation(newRelations).relationString;
227
+ });
228
+ setRelationsData({ relations: newRelations, layers });
229
+ } else
230
+ window.notifyAlert({
231
+ type: "warning",
232
+ text: `${target_value} has no relations`,
186
233
  });
187
- setRelations(newRelations);
188
- }
189
234
  }
190
235
  }
191
236
  };
192
237
  const helpContext = { view_name: use_view_name };
193
- if (options.tableName) helpContext.srcTable = options.tableName;
238
+ if (tableName) helpContext.srcTable = tableName;
194
239
  return (
195
240
  <div>
196
241
  <table className="w-100">
@@ -215,7 +260,7 @@ const ViewLinkSettings = () => {
215
260
  <tr>
216
261
  <td colSpan="2">
217
262
  <RelationOnDemandPicker
218
- relations={relations.layers}
263
+ relations={relationsData.layers}
219
264
  update={(relPath) => {
220
265
  if (relPath.startsWith(".")) {
221
266
  setProp((prop) => {
@@ -233,8 +278,8 @@ const ViewLinkSettings = () => {
233
278
  <RelationBadges
234
279
  view={name}
235
280
  relation={safeRelation}
236
- parentTbl={options.tableName}
237
- tableNameCache={caches.tableNameCache}
281
+ parentTbl={tableName}
282
+ caches={tableCaches}
238
283
  />
239
284
  </td>
240
285
  </tr>