@saltcorn/builder 0.9.2 → 0.9.3-beta.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/babel.config.js +6 -0
- package/dist/builder_bundle.js +10 -10
- package/package.json +10 -2
- package/src/components/Toolbox.js +4 -10
- package/src/components/elements/RelationBadges.js +14 -11
- package/src/components/elements/RelationOnDemandPicker.js +212 -0
- package/src/components/elements/View.js +82 -49
- package/src/components/elements/ViewLink.js +47 -24
- package/src/components/elements/utils.js +35 -84
- package/tests/relations_finder.test.js +275 -0
- package/tests/test_data.js +163 -0
- package/src/components/elements/RelationPicker.js +0 -273
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
export const fixturesData = () => {
|
|
5
|
+
const { tables, views } = JSON.parse(
|
|
6
|
+
readFileSync(join(__dirname, "schema_data.json"))
|
|
7
|
+
);
|
|
8
|
+
return { tables, views };
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
let nextFieldId = 3000;
|
|
12
|
+
|
|
13
|
+
export const withAnotherUserField = () => {
|
|
14
|
+
const { tables, views } = JSON.parse(JSON.stringify(fixturesData()));
|
|
15
|
+
const uiit = tables.find(({ name }) => name === "user_interested_in_topic");
|
|
16
|
+
uiit.foreign_keys.push({
|
|
17
|
+
name: "another_user",
|
|
18
|
+
id: nextFieldId++,
|
|
19
|
+
table_id: uiit.id,
|
|
20
|
+
reftable_name: "users",
|
|
21
|
+
is_unique: false,
|
|
22
|
+
});
|
|
23
|
+
return { tables, views };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const withSecondTopicField = () => {
|
|
27
|
+
const { tables, views } = JSON.parse(JSON.stringify(withAnotherUserField()));
|
|
28
|
+
const bit = tables.find(({ name }) => name === "blog_in_topic");
|
|
29
|
+
bit.foreign_keys.push({
|
|
30
|
+
name: "second_topic",
|
|
31
|
+
id: nextFieldId++,
|
|
32
|
+
table_id: bit.id,
|
|
33
|
+
reftable_name: "topics",
|
|
34
|
+
is_unique: false,
|
|
35
|
+
});
|
|
36
|
+
return { tables, views };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const withMultipleInbounds = () => {
|
|
40
|
+
const { tables, views } = JSON.parse(JSON.stringify(withSecondTopicField()));
|
|
41
|
+
const nextTableId = tables.length + 1;
|
|
42
|
+
tables.push({
|
|
43
|
+
name: "second_inbound",
|
|
44
|
+
id: nextTableId,
|
|
45
|
+
foreign_keys: [
|
|
46
|
+
{
|
|
47
|
+
name: "topic",
|
|
48
|
+
id: nextFieldId++,
|
|
49
|
+
table_id: nextTableId,
|
|
50
|
+
reftable_name: "topics",
|
|
51
|
+
is_unique: false,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "user",
|
|
55
|
+
id: nextFieldId++,
|
|
56
|
+
table_id: nextTableId,
|
|
57
|
+
reftable_name: "users",
|
|
58
|
+
is_unique: false,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
return { tables, views };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const withKeyFromLayerTwo = () => {
|
|
66
|
+
const { tables, views } = JSON.parse(JSON.stringify(withMultipleInbounds()));
|
|
67
|
+
const inboundInbound = tables.find(({ name }) => name === "inbound_inbound");
|
|
68
|
+
inboundInbound.foreign_keys.push({
|
|
69
|
+
name: "post_from_layer_two",
|
|
70
|
+
id: nextFieldId++,
|
|
71
|
+
table_id: inboundInbound.id,
|
|
72
|
+
reftable_name: "blog_posts",
|
|
73
|
+
is_unique: false,
|
|
74
|
+
});
|
|
75
|
+
return { tables, views };
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const withKeyFromLayerThree = () => {
|
|
79
|
+
const { tables, views } = JSON.parse(JSON.stringify(withKeyFromLayerTwo()));
|
|
80
|
+
const nextTableId = tables.length + 1;
|
|
81
|
+
tables.push({
|
|
82
|
+
name: "inbound_level_three",
|
|
83
|
+
id: nextTableId,
|
|
84
|
+
foreign_keys: [
|
|
85
|
+
{
|
|
86
|
+
name: "inbound_level_two",
|
|
87
|
+
id: nextFieldId++,
|
|
88
|
+
table_id: nextTableId,
|
|
89
|
+
reftable_name: "inbound_inbound",
|
|
90
|
+
is_unique: false,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "topic",
|
|
94
|
+
id: nextFieldId++,
|
|
95
|
+
table_id: nextTableId,
|
|
96
|
+
reftable_name: "topics",
|
|
97
|
+
is_unique: false,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
return { tables, views };
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const withSimplePostTopicrelation = () => {
|
|
105
|
+
const { tables, views } = JSON.parse(JSON.stringify(fixturesData()));
|
|
106
|
+
const simpleTopicsId = tables.length + 1;
|
|
107
|
+
const simplePostsId = simpleTopicsId + 1;
|
|
108
|
+
const simplePostInboundId = simplePostsId + 1;
|
|
109
|
+
tables.push({
|
|
110
|
+
name: "simple_posts",
|
|
111
|
+
id: simplePostsId,
|
|
112
|
+
foreign_keys: [
|
|
113
|
+
{
|
|
114
|
+
name: "topic",
|
|
115
|
+
table_id: simplePostsId,
|
|
116
|
+
id: nextFieldId++,
|
|
117
|
+
reftable_name: "simple_topics",
|
|
118
|
+
is_unique: false,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
tables.push({
|
|
123
|
+
name: "simple_topics",
|
|
124
|
+
id: simpleTopicsId,
|
|
125
|
+
foreign_keys: [],
|
|
126
|
+
});
|
|
127
|
+
tables.push({
|
|
128
|
+
name: "simple_post_inbound",
|
|
129
|
+
id: simplePostInboundId,
|
|
130
|
+
foreign_keys: [
|
|
131
|
+
{
|
|
132
|
+
name: "post",
|
|
133
|
+
table_id: simplePostInboundId,
|
|
134
|
+
id: nextFieldId++,
|
|
135
|
+
reftable_name: "simple_posts",
|
|
136
|
+
is_unique: false,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "topic",
|
|
140
|
+
table_id: simplePostInboundId,
|
|
141
|
+
id: nextFieldId++,
|
|
142
|
+
reftable_name: "simple_topics",
|
|
143
|
+
is_unique: false,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
const users = tables.find(({ name }) => name === "users");
|
|
148
|
+
users.foreign_keys.push({
|
|
149
|
+
name: "favsimpletopic",
|
|
150
|
+
table_id: users.id,
|
|
151
|
+
id: nextFieldId++,
|
|
152
|
+
reftable_name: "simple_topics",
|
|
153
|
+
is_unique: false,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
views.push({
|
|
157
|
+
name: "simple_posts_list",
|
|
158
|
+
table_id: simplePostsId,
|
|
159
|
+
display_type: "NO_ROW_LIMIT",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return { tables, views };
|
|
163
|
+
};
|
|
@@ -1,273 +0,0 @@
|
|
|
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
|
-
<div key={`${identifier}_inboundkey_key_div`}>
|
|
182
|
-
<li
|
|
183
|
-
key={`${identifier}_inboundkey_key`}
|
|
184
|
-
className={`dropdown-item ${identifier}_inbound_item item_level_${reclevel} ${
|
|
185
|
-
reclevel < 5 ? "dropstart" : "dropdown"
|
|
186
|
-
}`}
|
|
187
|
-
role="button"
|
|
188
|
-
>
|
|
189
|
-
<div
|
|
190
|
-
className={`dropdown-toggle ${identifier}_inbound_toggle dropdown_level_${reclevel}`}
|
|
191
|
-
role="button"
|
|
192
|
-
aria-expanded="false"
|
|
193
|
-
onClick={() => {
|
|
194
|
-
$(
|
|
195
|
-
`.${identifier}_inbound_toggle,${levelClasses(
|
|
196
|
-
reclevel
|
|
197
|
-
)}`
|
|
198
|
-
).dropdown("toggle");
|
|
199
|
-
if (reclevel > maxRecLevel) setMaxRecLevel(reclevel);
|
|
200
|
-
$(activeClasses(reclevel)).removeClass("active");
|
|
201
|
-
$(`.${identifier}_inbound_item`).addClass(
|
|
202
|
-
() => "active"
|
|
203
|
-
);
|
|
204
|
-
}}
|
|
205
|
-
>
|
|
206
|
-
{inboundKey.name} (from {inboundKey.table})
|
|
207
|
-
</div>
|
|
208
|
-
{buildPicker(inboundKey, reclevel + 1)}
|
|
209
|
-
</li>
|
|
210
|
-
{/* it's a level and a direct link */}
|
|
211
|
-
{inboundKey.relPath ? (
|
|
212
|
-
<li
|
|
213
|
-
key={`${identifier}_inboundkey_key_direct`}
|
|
214
|
-
className="dropdown-item"
|
|
215
|
-
role="button"
|
|
216
|
-
onClick={() => {
|
|
217
|
-
update(inboundKey.relPath);
|
|
218
|
-
$(".dropdown-item.active").removeClass("active");
|
|
219
|
-
$(`${levelClasses(0)}`).dropdown("toggle");
|
|
220
|
-
setMaxRecLevel(maxRecLevelDefault);
|
|
221
|
-
}}
|
|
222
|
-
>
|
|
223
|
-
{inboundKey.name} (from {inboundKey.table})
|
|
224
|
-
</li>
|
|
225
|
-
) : (
|
|
226
|
-
""
|
|
227
|
-
)}
|
|
228
|
-
</div>
|
|
229
|
-
) : (
|
|
230
|
-
<li
|
|
231
|
-
key={`${identifier}_inboundkey_key`}
|
|
232
|
-
className="dropdown-item"
|
|
233
|
-
role="button"
|
|
234
|
-
onClick={() => {
|
|
235
|
-
update(inboundKey.relPath);
|
|
236
|
-
$(".dropdown-item.active").removeClass("active");
|
|
237
|
-
$(`${levelClasses(0)}`).dropdown("toggle");
|
|
238
|
-
setMaxRecLevel(maxRecLevelDefault);
|
|
239
|
-
}}
|
|
240
|
-
>
|
|
241
|
-
{inboundKey.name} (from {inboundKey.table})
|
|
242
|
-
</li>
|
|
243
|
-
);
|
|
244
|
-
})
|
|
245
|
-
}
|
|
246
|
-
</ul>
|
|
247
|
-
</div>
|
|
248
|
-
);
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
return (
|
|
252
|
-
<div>
|
|
253
|
-
<label>Relation</label>
|
|
254
|
-
<div style={{ zIndex: 10000 }} className="dropstart">
|
|
255
|
-
<button
|
|
256
|
-
id="_relation_picker_toggle_"
|
|
257
|
-
className="btn btn-outline-primary dropdown-toggle dropdown_level_0 mb-1"
|
|
258
|
-
aria-expanded="false"
|
|
259
|
-
onClick={() => {
|
|
260
|
-
$(".dropdown-item.active").removeClass("active");
|
|
261
|
-
$(`#_relation_picker_toggle_,${levelClasses(0)}`).dropdown(
|
|
262
|
-
"toggle"
|
|
263
|
-
);
|
|
264
|
-
setMaxRecLevel(maxRecLevelDefault);
|
|
265
|
-
}}
|
|
266
|
-
>
|
|
267
|
-
Select
|
|
268
|
-
</button>
|
|
269
|
-
{buildPicker(relationLevels, 1)}
|
|
270
|
-
</div>
|
|
271
|
-
</div>
|
|
272
|
-
);
|
|
273
|
-
};
|