-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcommon.js
More file actions
278 lines (251 loc) · 8.95 KB
/
common.js
File metadata and controls
278 lines (251 loc) · 8.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
const View = require("@saltcorn/data/models/view");
const { getState } = require("@saltcorn/data/db/state");
const db = require("@saltcorn/data/db");
const { spawn } = require("child_process");
const path = require("path");
const fs = require("fs").promises;
const buildViewBundle = async (buildMode, viewName, timestamp) => {
const tenant = db.getTenantSchema() || "public";
return new Promise((resolve, reject) => {
const child = spawn(
"npm",
[
"run",
buildMode === "development" ? "build_view_dev" : "build_view",
"--",
"--env",
`view_name=${viewName}`,
"--env",
`tenant_name=${tenant}`,
"--env",
`timestamp=${timestamp}`,
"--env",
`bundle_name=${viewName}.bundle.js`,
],
{
cwd: __dirname,
}
);
child.stdout.on("data", (data) => {
getState().log(5, data.toString());
});
child.stderr?.on("data", (data) => {
getState().log(2, data.toString());
});
child.on("exit", function (code, signal) {
getState().log(5, `child process exited with code ${code}`);
resolve(code);
});
child.on("error", (msg) => {
getState().log(2, `child process failed: ${msg.code}`);
reject(msg.code);
});
});
};
const buildSafeViewName = (viewName) => viewName.replace(/[^a-zA-Z0-9]/g, "_");
const handleUserCode = async (
userCode,
buildMode,
viewName,
oldTimestamp,
newTimestamp
) => {
const tenant = db.getTenantSchema() || "public";
const userCodeDir = path.join(__dirname, "user-code", tenant);
const codeDirExists = await fs
.access(userCodeDir)
.then(() => true)
.catch(() => false);
if (!codeDirExists) await fs.mkdir(userCodeDir, { recursive: true });
const safeViewName = buildSafeViewName(viewName);
await fs.writeFile(
path.join(userCodeDir, `${safeViewName}.js`),
userCode,
"utf8"
);
if ((await buildViewBundle(buildMode, safeViewName, newTimestamp)) !== 0) {
throw new Error("Build failed please check your server logs");
}
try {
await fs.rm(
path.join(__dirname, "public", tenant, `${safeViewName}_${oldTimestamp}`),
{ recursive: true, force: true }
);
} catch (err) {
getState().log(
2,
"Error removing old directory: " + err.message || "Unknown error"
);
}
};
const reactViewSystemPrompt = `Use the generate_react_view tool to generate a react view. Here is an example of a
valid generated react code:
\`\`\`import React from "react";
export default function App({}) {
return <h1>Hello world</h1>;
}
\`\`\`
The generated code must include the react import at the top, and your generated code should export default the component.
A react view can be tableless or table-based. A tableless react view could for example show the current time, a table based view could show the data of one or multiple persons.
When a react view is tabless, it gets this properties:
\`\`\`import React from "react";
export default function App({viewName, query}) {...}
\`\`\`
When a react view is table based, it gets this properties:
\`\`\`import React from "react";
export default function App({viewName, query, tableName, rows, state}) {...}
\`\`\`
- viewName: the name of the view
- query: the query parameters of the view
- tableName: the name of the Saltcorn table
- rows: the rows of the table, this is an array of objects, each object is a row of the table
- state: the state of the view, this is an object with the state of the view
A react-view has access to bootstrap 5 styles. react-bootstrap is not available please use the normal bootstrap classes.
A react-view can use the function set_state_field(key, value, e) to change the current query of the browser window.
By changing the query you can act as a filter. For example if you show a list of persons, you can filter by name, age, etc.
Key is the name of the field, value is the value you want to set, and e is the event that triggered the change.
A react-filter-view can exist independent of other views, it only has to call set_state_field.
A react-view hast accees to the react-lib npm package. This is a module with hooks and functions to interact with the Saltcorn system. You can import it like this:
\`\`\`
import { useFetchOneRow, useFetchRows } from "@saltcorn/react-lib/hooks";
import { fetchOneRow, fetchRows, insertRow, updateRow, deleteRow } from "@saltcorn/react-lib/api";
\`\`\`
Please note that hooks are in the hooks submodule and functions are in the api submodule.
useFetchOneRow and useFetchRows are hooks to fetch rows from a Saltcorn table. Table-based views receive the initial rows in the rows property,
useFetchOneRow and useFetchRows can load data at runtime. This can be useful when the initial data changes,
or for loading data from another table than the table of the view, or for tableless views.
Parameters of useFetchOneRow:
- tableName: the name of the table
- query: the query to fetch the row as an object
- dependencies: an array of dependencies, when one of the dependencies changes, useFetchOneRow will fetch the row again
Returns an object with:
- isLoading: boolean indicating if the data is loading
- error: string describing the error or null
- row: an object with the data of the row
useFetchRows has the same signature, but returns an array of rows instead of a single row.
Example of useFetchRows:
\`\`\`
import React, { useState } from "react";
import { useFetchRows } from "@saltcorn/react-lib/hooks";
export default function App({query, tableName}) {
const { rows, isLoading, error } = useFetchRows("persons", query || {});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{rows.map((row) => (
<div key={row.id}>
{row.name} - {row.age}
</div>
))}
</div>
);
}
When you do not want to use the hooks, you can use fetchOneRow and fetchRows. fetchRows example:
\`\`\`
import React, { useState, useEffect } from "react";
import { fetchOneRow, fetchRows } from "@saltcorn/react-lib/api";
export default function App({query, tableName}) {
// load rows once
const [rows, setRows] = useState([]);
useEffect(() => {
fetchRows(tableName, query).then((rows) => setRows(rows));
}, [tableName, query]);
}
\`\`\`
fetchRows returns an empty array if nothing was found and throws an error if something goes wrong.
fetchOneRow example:
\`\`\`
import React, { useState, useEffect } from "react";
import { fetchOneRow } from "@saltcorn/react-lib/api";
export default function App({query, tableName}) {
// load row once
const [row, setRow] = useState(null);
useEffect(() => {
fetchOneRow(tableName, query).then((row) => setRow(row));
}, [tableName, query]);
}
\`\`\`
fetchOneRow returns null if nothing was found and throws an error if something goes wrong.
Under "@saltcorn/react-lib/api" you also find the functions insertRow, updateRow and deleteRow.
They all throw an error if something goes wrong.
Parameters of insertRow:
- tableName: the name of the table
- row: the row to insert as an object
Example:
\`\`\`
import React, { useState } from "react";
import { insertRow } from "@saltcorn/react-lib/api";
export default function App({query, tableName}) {
const [row, setRow] = useState(null);
const handleInsert = () => {
insertRow(tableName, row).then((row) => setRow(row));
}
}
\`\`\`
Parameters of updateRow:
- tableName: the name of the table
- id: the id of the row to update
- row: the row to update as an object
\`\`\`
import React, { useState } from "react";
import { updateRow } from "@saltcorn/react-lib/api";
export default function App({query, tableName}) {
const [row, setRow] = useState(null);
const handleUpdate = () => {
updateRow(tableName, row).then((row) => setRow(row));
}
}
\`\`\`
Parameters of deleteRow:
- tableName: the name of the table
- id: the id of the row to delete
Example:
\`\`\`
import React, { useState } from "react";
import { deleteRow } from "@saltcorn/react-lib/api";
export default function App({query, tableName}) {
const [row, setRow] = useState(null);
const handleDelete = () => {
deleteRow(tableName, row.id).then(() => setRow(null));
}
}
\`\`\`
`;
const escapeHtml = (unsafe) =>
unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
const buildAndUpdateView = async (
user_code,
build_mode,
viewname,
oldTimestamp
) => {
const newTimestamp = new Date().valueOf();
await handleUserCode(
user_code,
build_mode,
viewname,
oldTimestamp,
newTimestamp
);
const view = View.findOne({ name: viewname });
await View.update(
{
configuration: { ...(view.configuration || {}), timestamp: newTimestamp },
},
view.id
);
await getState().refresh_views();
};
module.exports = {
buildSafeViewName,
handleUserCode,
buildAndUpdateView,
escapeHtml,
reactViewSystemPrompt,
};