@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.
- package/dist/builder_bundle.js +7 -7
- package/package.json +1 -1
- package/src/components/elements/RelationBadges.js +97 -0
- package/src/components/elements/RelationPicker.js +251 -0
- package/src/components/elements/View.js +209 -183
- package/src/components/elements/ViewLink.js +33 -20
- package/src/components/elements/utils.js +94 -5
- package/src/components/storage.js +2 -0
package/package.json
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React, { Fragment, useContext, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
parseRelationPath,
|
|
4
|
+
parseLegacyRelation,
|
|
5
|
+
removeWhitespaces,
|
|
6
|
+
} from "./utils";
|
|
7
|
+
|
|
8
|
+
const buildBadgeCfgs = (parsed, parentTbl) => {
|
|
9
|
+
const result = [];
|
|
10
|
+
let currentCfg = null;
|
|
11
|
+
for (const { type, table, key } of parsed) {
|
|
12
|
+
if (type === "Inbound") {
|
|
13
|
+
if (currentCfg) result.push(currentCfg);
|
|
14
|
+
currentCfg = { up: key, table };
|
|
15
|
+
} else {
|
|
16
|
+
if (!currentCfg) result.push({ down: key, table: parentTbl });
|
|
17
|
+
else {
|
|
18
|
+
currentCfg.down = key;
|
|
19
|
+
result.push(currentCfg);
|
|
20
|
+
}
|
|
21
|
+
currentCfg = { table };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (
|
|
25
|
+
currentCfg &&
|
|
26
|
+
!result.find(
|
|
27
|
+
({ down, table, up }) =>
|
|
28
|
+
down === currentCfg.down &&
|
|
29
|
+
table === currentCfg.table &&
|
|
30
|
+
up === currentCfg.up
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
result.push(currentCfg);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const buildBadge = ({ up, table, down }, index) => {
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
key={removeWhitespaces(`badge_${table}_${index}`)}
|
|
41
|
+
className="my-1 d-flex"
|
|
42
|
+
>
|
|
43
|
+
<div className="m-auto badge bg-primary">
|
|
44
|
+
{up ? (
|
|
45
|
+
<div className="mt-1 d-flex justify-content-center">
|
|
46
|
+
<span className="pe-2">{up}</span>
|
|
47
|
+
<i className="fas fa-arrow-up"></i>
|
|
48
|
+
</div>
|
|
49
|
+
) : (
|
|
50
|
+
""
|
|
51
|
+
)}
|
|
52
|
+
<div className="m-1 fw-bolder">{table}</div>
|
|
53
|
+
{down ? (
|
|
54
|
+
<div className="mb-1 d-flex justify-content-center">
|
|
55
|
+
<span className="pe-2">{down}</span>
|
|
56
|
+
<i className="fas fa-arrow-down" style={{ marginTop: "-1px" }}></i>
|
|
57
|
+
</div>
|
|
58
|
+
) : (
|
|
59
|
+
""
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const RelationBadges = ({ view, relation, parentTbl, fk_options }) => {
|
|
67
|
+
if (relation) {
|
|
68
|
+
const parsed = parseRelationPath(relation, fk_options);
|
|
69
|
+
return (
|
|
70
|
+
<div className="overflow-scroll">
|
|
71
|
+
{parsed.length > 0
|
|
72
|
+
? buildBadgeCfgs(parsed, parentTbl).map(buildBadge)
|
|
73
|
+
: buildBadge({ table: "invalid relation" }, 0)}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
} else {
|
|
77
|
+
const [prefix, rest] = view.split(":");
|
|
78
|
+
const parsed = parseLegacyRelation(prefix, rest, parentTbl);
|
|
79
|
+
if (parsed.length === 0)
|
|
80
|
+
return buildBadge({ table: "invalid relation" }, 0);
|
|
81
|
+
else if (
|
|
82
|
+
parsed.length === 1 &&
|
|
83
|
+
(parsed[0].type === "Independent" || parsed[0].type === "Own")
|
|
84
|
+
)
|
|
85
|
+
return (
|
|
86
|
+
<div className="overflow-scroll">
|
|
87
|
+
{buildBadge({ table: parsed[0].table }, 0)}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
else
|
|
91
|
+
return (
|
|
92
|
+
<div className="overflow-scroll">
|
|
93
|
+
{buildBadgeCfgs(parsed, parentTbl).map(buildBadge)}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/*global $ */
|
|
2
|
+
import React, { Fragment, useContext, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
parseRelationPath,
|
|
5
|
+
parseLegacyRelation,
|
|
6
|
+
removeWhitespaces,
|
|
7
|
+
} from "./utils";
|
|
8
|
+
|
|
9
|
+
const parseRelations = (allOptions, viewname, parentTbl) => {
|
|
10
|
+
const viewtable = allOptions.view_name_opts
|
|
11
|
+
? allOptions.view_name_opts.find((opt) => opt.name === viewname)?.table
|
|
12
|
+
: undefined;
|
|
13
|
+
const options = allOptions.view_relation_opts[viewname] || [];
|
|
14
|
+
const result = { table: parentTbl, inboundKeys: [], fkeys: [] };
|
|
15
|
+
if (!viewtable) return result;
|
|
16
|
+
for (const { value } of options) {
|
|
17
|
+
let path = null;
|
|
18
|
+
if (value.startsWith(".")) {
|
|
19
|
+
path = parseRelationPath(value, allOptions.fk_options);
|
|
20
|
+
} else {
|
|
21
|
+
const [prefix, rest] = value.split(":");
|
|
22
|
+
path = parseLegacyRelation(prefix, rest, parentTbl);
|
|
23
|
+
}
|
|
24
|
+
if (path.length > 0) buildLevels(value, path, result, parentTbl);
|
|
25
|
+
else console.log(`The relation path '${value}' is invalid`);
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const buildLevels = (path, parsed, result, parentTbl) => {
|
|
31
|
+
let currentLevel = result;
|
|
32
|
+
for (const relation of parsed) {
|
|
33
|
+
if (relation.type === "Inbound") {
|
|
34
|
+
const existing = currentLevel.inboundKeys.find(
|
|
35
|
+
(key) => key.name === relation.key && key.table === relation.table
|
|
36
|
+
);
|
|
37
|
+
if (existing) {
|
|
38
|
+
currentLevel = existing;
|
|
39
|
+
} else {
|
|
40
|
+
const nextLevel = {
|
|
41
|
+
name: relation.key,
|
|
42
|
+
table: relation.table,
|
|
43
|
+
inboundKeys: [],
|
|
44
|
+
fkeys: [],
|
|
45
|
+
};
|
|
46
|
+
currentLevel.inboundKeys.push(nextLevel);
|
|
47
|
+
currentLevel = nextLevel;
|
|
48
|
+
}
|
|
49
|
+
} else if (relation.type === "Foreign") {
|
|
50
|
+
const existing = currentLevel.fkeys.find(
|
|
51
|
+
(key) => key.name === relation.key
|
|
52
|
+
);
|
|
53
|
+
if (existing) {
|
|
54
|
+
currentLevel = existing;
|
|
55
|
+
} else {
|
|
56
|
+
const nextLevel = {
|
|
57
|
+
name: relation.key,
|
|
58
|
+
table: relation.table,
|
|
59
|
+
inboundKeys: [],
|
|
60
|
+
fkeys: [],
|
|
61
|
+
};
|
|
62
|
+
currentLevel.fkeys.push(nextLevel);
|
|
63
|
+
currentLevel = nextLevel;
|
|
64
|
+
}
|
|
65
|
+
} else if (relation.type === "Independent") {
|
|
66
|
+
result.fkeys.push({
|
|
67
|
+
name: "None (no relation)",
|
|
68
|
+
table: "",
|
|
69
|
+
inboundKeys: [],
|
|
70
|
+
fkeys: [],
|
|
71
|
+
relPath: path,
|
|
72
|
+
});
|
|
73
|
+
} else if (relation.type === "Own") {
|
|
74
|
+
result.fkeys.push({
|
|
75
|
+
name: `${parentTbl} (same table)`,
|
|
76
|
+
table: "",
|
|
77
|
+
inboundKeys: [],
|
|
78
|
+
fkeys: [],
|
|
79
|
+
relPath: path,
|
|
80
|
+
});
|
|
81
|
+
} else if (relation.type === "OneToOneShow") {
|
|
82
|
+
result.inboundKeys.push({
|
|
83
|
+
name: relation.key,
|
|
84
|
+
table: relation.table,
|
|
85
|
+
inboundKeys: [],
|
|
86
|
+
fkeys: [],
|
|
87
|
+
relPath: path,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
currentLevel.relPath = path;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const RelationPicker = ({ options, viewname, update }) => {
|
|
95
|
+
const maxRecLevelDefault = 10;
|
|
96
|
+
const [maxRecLevel, setMaxRecLevel] = useState(maxRecLevelDefault);
|
|
97
|
+
const relationLevels = parseRelations(options, viewname, options.tableName);
|
|
98
|
+
|
|
99
|
+
const levelClasses = (level) => {
|
|
100
|
+
const classes = [];
|
|
101
|
+
for (let i = level; i < maxRecLevel; i++) {
|
|
102
|
+
classes.push(`.dropdown_level_${i}.show`);
|
|
103
|
+
}
|
|
104
|
+
return classes.join(",");
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const activeClasses = (level) => {
|
|
108
|
+
const classes = [];
|
|
109
|
+
for (let i = level; i < maxRecLevel; i++) {
|
|
110
|
+
classes.push(`.item_level_${i}.active`);
|
|
111
|
+
}
|
|
112
|
+
return classes.join(",");
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const buildPicker = (level, reclevel) => {
|
|
116
|
+
return (
|
|
117
|
+
<div className="dropdown-menu">
|
|
118
|
+
<h5 className="join-table-header text-center">{level.table}</h5>
|
|
119
|
+
<ul className="ps-0 mb-0">
|
|
120
|
+
{
|
|
121
|
+
// foreign keys
|
|
122
|
+
level.fkeys.map((fkey) => {
|
|
123
|
+
const hasSubLevels =
|
|
124
|
+
fkey.fkeys.length > 0 || fkey.inboundKeys.length > 0;
|
|
125
|
+
const identifier = removeWhitespaces(
|
|
126
|
+
`${fkey.name}_${fkey.table}_${level.table}_${reclevel}`
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return hasSubLevels ? (
|
|
130
|
+
<li
|
|
131
|
+
key={`${identifier}_fkey_key`}
|
|
132
|
+
className={`dropdown-item ${identifier}_fkey_item item_level_${reclevel} ${
|
|
133
|
+
reclevel < 5 ? "dropstart" : "dropdown"
|
|
134
|
+
}`}
|
|
135
|
+
role="button"
|
|
136
|
+
>
|
|
137
|
+
<div
|
|
138
|
+
className={`dropdown-toggle ${identifier}_fkey_toggle dropdown_level_${reclevel}`}
|
|
139
|
+
role="button"
|
|
140
|
+
aria-expanded="false"
|
|
141
|
+
onClick={() => {
|
|
142
|
+
$(
|
|
143
|
+
`.${identifier}_fkey_toggle,${levelClasses(reclevel)}`
|
|
144
|
+
).dropdown("toggle");
|
|
145
|
+
if (reclevel > maxRecLevel) setMaxRecLevel(reclevel);
|
|
146
|
+
$(activeClasses(reclevel)).removeClass("active");
|
|
147
|
+
$(`.${identifier}_fkey_item`).addClass(() => "active");
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
{fkey.name}
|
|
151
|
+
</div>
|
|
152
|
+
{buildPicker(fkey, reclevel + 1)}
|
|
153
|
+
</li>
|
|
154
|
+
) : (
|
|
155
|
+
<li
|
|
156
|
+
key={`${identifier}_fkey_key`}
|
|
157
|
+
className="dropdown-item"
|
|
158
|
+
role="button"
|
|
159
|
+
onClick={() => {
|
|
160
|
+
update(fkey.relPath);
|
|
161
|
+
$(".dropdown-item.active").removeClass("active");
|
|
162
|
+
$(`${levelClasses(0)}`).dropdown("toggle");
|
|
163
|
+
setMaxRecLevel(maxRecLevelDefault);
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
{fkey.name}
|
|
167
|
+
</li>
|
|
168
|
+
);
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
{
|
|
172
|
+
// inbound keys
|
|
173
|
+
level.inboundKeys.map((inboundKey) => {
|
|
174
|
+
const hasSubLevels =
|
|
175
|
+
inboundKey.fkeys.length > 0 ||
|
|
176
|
+
inboundKey.inboundKeys.length > 0;
|
|
177
|
+
const identifier = removeWhitespaces(
|
|
178
|
+
`${inboundKey.name}_${inboundKey.table}_${level.table}_${reclevel}`
|
|
179
|
+
);
|
|
180
|
+
return hasSubLevels ? (
|
|
181
|
+
<li
|
|
182
|
+
key={`${identifier}_inboundkey_key`}
|
|
183
|
+
className={`dropdown-item ${identifier}_inbound_item item_level_${reclevel} ${
|
|
184
|
+
reclevel < 5 ? "dropstart" : "dropdown"
|
|
185
|
+
}`}
|
|
186
|
+
role="button"
|
|
187
|
+
>
|
|
188
|
+
<div
|
|
189
|
+
className={`dropdown-toggle ${identifier}_inbound_toggle dropdown_level_${reclevel}`}
|
|
190
|
+
role="button"
|
|
191
|
+
aria-expanded="false"
|
|
192
|
+
onClick={() => {
|
|
193
|
+
$(
|
|
194
|
+
`.${identifier}_inbound_toggle,${levelClasses(
|
|
195
|
+
reclevel
|
|
196
|
+
)}`
|
|
197
|
+
).dropdown("toggle");
|
|
198
|
+
if (reclevel > maxRecLevel) setMaxRecLevel(reclevel);
|
|
199
|
+
$(activeClasses(reclevel)).removeClass("active");
|
|
200
|
+
$(`.${identifier}_inbound_item`).addClass(() => "active");
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
{inboundKey.name} (from {inboundKey.table})
|
|
204
|
+
</div>
|
|
205
|
+
{buildPicker(inboundKey, reclevel + 1)}
|
|
206
|
+
</li>
|
|
207
|
+
) : (
|
|
208
|
+
<li
|
|
209
|
+
key={`${identifier}_inboundkey_key`}
|
|
210
|
+
className="dropdown-item"
|
|
211
|
+
role="button"
|
|
212
|
+
onClick={() => {
|
|
213
|
+
update(inboundKey.relPath);
|
|
214
|
+
$(".dropdown-item.active").removeClass("active");
|
|
215
|
+
$(`${levelClasses(0)}`).dropdown("toggle");
|
|
216
|
+
setMaxRecLevel(maxRecLevelDefault);
|
|
217
|
+
}}
|
|
218
|
+
>
|
|
219
|
+
{inboundKey.name} (from {inboundKey.table})
|
|
220
|
+
</li>
|
|
221
|
+
);
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
</ul>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<div>
|
|
231
|
+
<label>Relation</label>
|
|
232
|
+
<div style={{ zIndex: 10000 }} className="dropstart">
|
|
233
|
+
<button
|
|
234
|
+
id="_relation_picker_toggle_"
|
|
235
|
+
className="btn btn-outline-primary dropdown-toggle dropdown_level_0 mb-1"
|
|
236
|
+
aria-expanded="false"
|
|
237
|
+
onClick={() => {
|
|
238
|
+
$(".dropdown-item.active").removeClass("active");
|
|
239
|
+
$(`#_relation_picker_toggle_,${levelClasses(0)}`).dropdown(
|
|
240
|
+
"toggle"
|
|
241
|
+
);
|
|
242
|
+
setMaxRecLevel(maxRecLevelDefault);
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
Select
|
|
246
|
+
</button>
|
|
247
|
+
{buildPicker(relationLevels, 1)}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
};
|