@xeplr/ui-factory 1.0.1 → 1.0.3
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/AUTHORING.md +101 -12
- package/README.md +73 -22
- package/bin/xeplr-factory.js +72 -1
- package/package.json +6 -2
- package/src/controls.js +142 -45
- package/src/designs/BuilderSample.jsx +80 -36
- package/src/designs/ControlView.jsx +43 -36
- package/src/designs/ListView.jsx +95 -0
- package/src/designs/PropertyPanel.jsx +187 -13
- package/src/designs/ScreenModal.jsx +42 -0
- package/src/designs/ScreenSample.jsx +49 -9
- package/src/designs/factory.css +72 -20
- package/src/designs/index.js +2 -0
- package/src/designs/styles.js +59 -0
- package/src/document.js +31 -4
- package/src/generate.js +136 -25
- package/src/index.js +2 -2
- package/src/model.js +12 -5
- package/src/pages.jsx +62 -10
- package/src/remote.js +66 -0
- package/src/scaffold.js +116 -0
- package/src/tableSchema.js +329 -0
- package/src/useFactoryBuilder.js +102 -17
- package/src/useFactoryScreen.js +290 -78
- package/src/validateDocument.js +73 -5
- package/src/values.js +61 -0
package/src/controls.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// THE CONTROLS a screen is made of — one entry per type, and nothing about any
|
|
2
2
|
// control lives anywhere else. The palette lists these, the property panel is
|
|
3
|
-
// generated from `properties`, the document checker reads `props`,
|
|
4
|
-
// form schema reads `valueType`. Adding a control is
|
|
3
|
+
// generated from `properties`, the document checker reads `props`, `validation`
|
|
4
|
+
// and `styles`, and the form schema reads `valueType`. Adding a control is
|
|
5
|
+
// adding an entry.
|
|
5
6
|
//
|
|
6
7
|
// Pure data: no React. The renderers are in designs/ControlView.jsx, keyed by
|
|
7
8
|
// the same `type`, so this file also runs in node — the CLI and the tests read
|
|
@@ -14,6 +15,73 @@
|
|
|
14
15
|
export const OPTION_ID = 'id'
|
|
15
16
|
export const OPTION_LABEL = 'name'
|
|
16
17
|
|
|
18
|
+
// ── styles ───────────────────────────────────────────────────────────────
|
|
19
|
+
// Every look a control can have is a key under `props.style`, described once
|
|
20
|
+
// here: its editor, and the values it accepts. Sizes are PIXELS at the screen's
|
|
21
|
+
// design width; a screen shown narrower shrinks them in proportion, and never
|
|
22
|
+
// enlarges them.
|
|
23
|
+
|
|
24
|
+
export const FONT_FAMILIES = [
|
|
25
|
+
{ value: 'system-ui, -apple-system, "Segoe UI", sans-serif', label: 'System' },
|
|
26
|
+
{ value: 'Arial, Helvetica, sans-serif', label: 'Arial' },
|
|
27
|
+
{ value: 'Verdana, Geneva, sans-serif', label: 'Verdana' },
|
|
28
|
+
{ value: 'Tahoma, Geneva, sans-serif', label: 'Tahoma' },
|
|
29
|
+
{ value: '"Trebuchet MS", sans-serif', label: 'Trebuchet MS' },
|
|
30
|
+
{ value: 'Georgia, serif', label: 'Georgia' },
|
|
31
|
+
{ value: '"Times New Roman", Times, serif', label: 'Times New Roman' },
|
|
32
|
+
{ value: '"Courier New", Courier, monospace', label: 'Courier New' },
|
|
33
|
+
{ value: 'ui-monospace, SFMono-Regular, Menlo, monospace', label: 'Monospace' }
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const WEIGHTS = [
|
|
37
|
+
{ value: 300, label: 'Light' },
|
|
38
|
+
{ value: 400, label: 'Regular' },
|
|
39
|
+
{ value: 500, label: 'Medium' },
|
|
40
|
+
{ value: 600, label: 'Semibold' },
|
|
41
|
+
{ value: 700, label: 'Bold' },
|
|
42
|
+
{ value: 800, label: 'Extra bold' }
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* key → { label, type (editor), check }. `check` is what the document checker
|
|
47
|
+
* accepts; `hint` is what it says when a value is refused.
|
|
48
|
+
*/
|
|
49
|
+
export const STYLE_KEYS = {
|
|
50
|
+
fontFamily: { label: 'Font', type: 'font', hint: 'a CSS font-family, e.g. "Georgia, serif"' },
|
|
51
|
+
fontSize: { label: 'Font size (px)', type: 'number', min: 8, max: 96, hint: 'a number of pixels from 8 to 96' },
|
|
52
|
+
fontWeight: { label: 'Weight', type: 'select', options: WEIGHTS, hint: 'one of 300, 400, 500, 600, 700, 800' },
|
|
53
|
+
fontStyle: { label: 'Italic', type: 'toggleValue', on: 'italic', hint: '"normal" or "italic"' },
|
|
54
|
+
textAlign: { label: 'Align', type: 'select', options: [
|
|
55
|
+
{ value: 'left', label: 'Left' }, { value: 'center', label: 'Centre' }, { value: 'right', label: 'Right' }
|
|
56
|
+
], hint: '"left", "center" or "right"' },
|
|
57
|
+
color: { label: 'Text colour', type: 'color', hint: 'a colour as #rgb or #rrggbb' },
|
|
58
|
+
background: { label: 'Background', type: 'color', hint: 'a colour as #rgb or #rrggbb, or "transparent"' },
|
|
59
|
+
borderColor: { label: 'Border colour', type: 'color', hint: 'a colour as #rgb or #rrggbb, or "transparent"' },
|
|
60
|
+
borderWidth: { label: 'Border width (px)', type: 'number', min: 0, max: 10, hint: 'a number of pixels from 0 to 10' },
|
|
61
|
+
borderRadius: { label: 'Corner radius (px)', type: 'number', min: 0, max: 40, hint: 'a number of pixels from 0 to 40' },
|
|
62
|
+
labelFontSize: { label: 'Label size (px)', type: 'number', min: 8, max: 48, hint: 'a number of pixels from 8 to 48' },
|
|
63
|
+
labelFontWeight: { label: 'Label weight', type: 'select', options: WEIGHTS, hint: 'one of 300, 400, 500, 600, 700, 800' },
|
|
64
|
+
labelColor: { label: 'Label colour', type: 'color', hint: 'a colour as #rgb or #rrggbb' }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The screen-wide defaults every control inherits. */
|
|
68
|
+
export const SCREEN_STYLE_KEYS = ['fontFamily', 'fontSize', 'color', 'background']
|
|
69
|
+
|
|
70
|
+
const INPUT_STYLES = ['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'textAlign', 'color', 'background', 'borderColor', 'borderWidth', 'borderRadius', 'labelFontSize', 'labelFontWeight', 'labelColor']
|
|
71
|
+
|
|
72
|
+
/** Property-panel groups for a control's styles, split the way people look for them. */
|
|
73
|
+
function styleGroups(keys) {
|
|
74
|
+
const field = (k) => ({ path: `props.style.${k}`, label: STYLE_KEYS[k].label, type: STYLE_KEYS[k].type, options: STYLE_KEYS[k].options, on: STYLE_KEYS[k].on, min: STYLE_KEYS[k].min, max: STYLE_KEYS[k].max })
|
|
75
|
+
const groups = [
|
|
76
|
+
{ key: 'text', title: 'Text', keys: ['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'textAlign', 'color'] },
|
|
77
|
+
{ key: 'box', title: 'Box', keys: ['background', 'borderColor', 'borderWidth', 'borderRadius'] },
|
|
78
|
+
{ key: 'labelStyle', title: 'Label', keys: ['labelFontSize', 'labelFontWeight', 'labelColor'] }
|
|
79
|
+
]
|
|
80
|
+
return groups
|
|
81
|
+
.map((g) => ({ key: g.key, title: g.title, fields: g.keys.filter((k) => keys.includes(k)).map(field) }))
|
|
82
|
+
.filter((g) => g.fields.length)
|
|
83
|
+
}
|
|
84
|
+
|
|
17
85
|
// ── property panel contract ──────────────────────────────────────────────
|
|
18
86
|
// { key, title, fields: [{ path, label, type, options?, help? }] }
|
|
19
87
|
// The same shape as the BI dashboard's widgetProperties.js, so the two panels
|
|
@@ -42,8 +110,9 @@ export const CONTROLS = {
|
|
|
42
110
|
valueType: 'string',
|
|
43
111
|
defaultSize: { w: 0.44, h: 0.08 },
|
|
44
112
|
defaults: { label: 'Text' },
|
|
45
|
-
props: ['name', 'label', 'placeholder', 'required', 'default', 'validation'],
|
|
113
|
+
props: ['name', 'label', 'placeholder', 'required', 'default', 'validation', 'style'],
|
|
46
114
|
validation: ['minLength', 'maxLength', 'pattern', 'patternMessage'],
|
|
115
|
+
styles: INPUT_STYLES,
|
|
47
116
|
properties: [
|
|
48
117
|
FIELD_BASICS([PLACEHOLDER, { path: 'props.default', label: 'Default value', type: 'text' }]),
|
|
49
118
|
{
|
|
@@ -55,7 +124,8 @@ export const CONTROLS = {
|
|
|
55
124
|
{ path: 'props.validation.pattern', label: 'Pattern (regex)', type: 'text' },
|
|
56
125
|
{ path: 'props.validation.patternMessage', label: 'Pattern message', type: 'text', help: 'Shown when the pattern does not match' }
|
|
57
126
|
]
|
|
58
|
-
}
|
|
127
|
+
},
|
|
128
|
+
...styleGroups(INPUT_STYLES)
|
|
59
129
|
]
|
|
60
130
|
},
|
|
61
131
|
|
|
@@ -67,8 +137,9 @@ export const CONTROLS = {
|
|
|
67
137
|
valueType: 'string',
|
|
68
138
|
defaultSize: { w: 0.92, h: 0.18 },
|
|
69
139
|
defaults: { label: 'Notes' },
|
|
70
|
-
props: ['name', 'label', 'placeholder', 'required', 'default', 'validation'],
|
|
140
|
+
props: ['name', 'label', 'placeholder', 'required', 'default', 'validation', 'style'],
|
|
71
141
|
validation: ['minLength', 'maxLength'],
|
|
142
|
+
styles: INPUT_STYLES,
|
|
72
143
|
properties: [
|
|
73
144
|
FIELD_BASICS([PLACEHOLDER, { path: 'props.default', label: 'Default value', type: 'textarea' }]),
|
|
74
145
|
{
|
|
@@ -78,7 +149,8 @@ export const CONTROLS = {
|
|
|
78
149
|
{ path: 'props.validation.minLength', label: 'Min length', type: 'number' },
|
|
79
150
|
{ path: 'props.validation.maxLength', label: 'Max length', type: 'number' }
|
|
80
151
|
]
|
|
81
|
-
}
|
|
152
|
+
},
|
|
153
|
+
...styleGroups(INPUT_STYLES)
|
|
82
154
|
]
|
|
83
155
|
},
|
|
84
156
|
|
|
@@ -90,8 +162,9 @@ export const CONTROLS = {
|
|
|
90
162
|
valueType: 'number',
|
|
91
163
|
defaultSize: { w: 0.44, h: 0.08 },
|
|
92
164
|
defaults: { label: 'Number' },
|
|
93
|
-
props: ['name', 'label', 'placeholder', 'required', 'default', 'validation'],
|
|
165
|
+
props: ['name', 'label', 'placeholder', 'required', 'default', 'validation', 'style'],
|
|
94
166
|
validation: ['min', 'max', 'integer'],
|
|
167
|
+
styles: INPUT_STYLES,
|
|
95
168
|
properties: [
|
|
96
169
|
FIELD_BASICS([PLACEHOLDER, { path: 'props.default', label: 'Default value', type: 'number' }]),
|
|
97
170
|
{
|
|
@@ -102,7 +175,8 @@ export const CONTROLS = {
|
|
|
102
175
|
{ path: 'props.validation.max', label: 'Maximum', type: 'number' },
|
|
103
176
|
{ path: 'props.validation.integer', label: 'Whole numbers only', type: 'toggle' }
|
|
104
177
|
]
|
|
105
|
-
}
|
|
178
|
+
},
|
|
179
|
+
...styleGroups(INPUT_STYLES)
|
|
106
180
|
]
|
|
107
181
|
},
|
|
108
182
|
|
|
@@ -114,8 +188,9 @@ export const CONTROLS = {
|
|
|
114
188
|
valueType: 'date',
|
|
115
189
|
defaultSize: { w: 0.44, h: 0.08 },
|
|
116
190
|
defaults: { label: 'Date' },
|
|
117
|
-
props: ['name', 'label', 'required', 'default', 'validation'],
|
|
191
|
+
props: ['name', 'label', 'required', 'default', 'validation', 'style'],
|
|
118
192
|
validation: ['min', 'max'],
|
|
193
|
+
styles: INPUT_STYLES,
|
|
119
194
|
properties: [
|
|
120
195
|
FIELD_BASICS([{ path: 'props.default', label: 'Default value', type: 'date' }]),
|
|
121
196
|
{
|
|
@@ -125,7 +200,8 @@ export const CONTROLS = {
|
|
|
125
200
|
{ path: 'props.validation.min', label: 'Earliest', type: 'date' },
|
|
126
201
|
{ path: 'props.validation.max', label: 'Latest', type: 'date' }
|
|
127
202
|
]
|
|
128
|
-
}
|
|
203
|
+
},
|
|
204
|
+
...styleGroups(INPUT_STYLES)
|
|
129
205
|
]
|
|
130
206
|
},
|
|
131
207
|
|
|
@@ -135,13 +211,15 @@ export const CONTROLS = {
|
|
|
135
211
|
group: 'Inputs',
|
|
136
212
|
input: true,
|
|
137
213
|
valueType: 'boolean',
|
|
138
|
-
defaultSize: { w: 0.44, h: 0.
|
|
214
|
+
defaultSize: { w: 0.44, h: 0.05 },
|
|
139
215
|
defaults: { label: 'Checkbox', default: false },
|
|
140
216
|
// `required` on a checkbox means it must be ticked — "I accept".
|
|
141
|
-
props: ['name', 'label', 'required', 'default'],
|
|
217
|
+
props: ['name', 'label', 'required', 'default', 'style'],
|
|
142
218
|
validation: [],
|
|
219
|
+
styles: ['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'color'],
|
|
143
220
|
properties: [
|
|
144
|
-
FIELD_BASICS([{ path: 'props.default', label: 'Ticked by default', type: 'toggle' }])
|
|
221
|
+
FIELD_BASICS([{ path: 'props.default', label: 'Ticked by default', type: 'toggle' }]),
|
|
222
|
+
...styleGroups(['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'color'])
|
|
145
223
|
]
|
|
146
224
|
},
|
|
147
225
|
|
|
@@ -154,8 +232,9 @@ export const CONTROLS = {
|
|
|
154
232
|
valueType: null,
|
|
155
233
|
defaultSize: { w: 0.44, h: 0.08 },
|
|
156
234
|
defaults: { label: 'Dropdown', placeholder: 'Select…', data: { source: 'static', options: [] } },
|
|
157
|
-
props: ['name', 'label', 'placeholder', 'required', 'default', 'data'],
|
|
235
|
+
props: ['name', 'label', 'placeholder', 'required', 'default', 'data', 'style'],
|
|
158
236
|
validation: [],
|
|
237
|
+
styles: INPUT_STYLES.filter((k) => k !== 'textAlign'),
|
|
159
238
|
properties: [
|
|
160
239
|
FIELD_BASICS([PLACEHOLDER]),
|
|
161
240
|
{
|
|
@@ -164,7 +243,8 @@ export const CONTROLS = {
|
|
|
164
243
|
fields: [
|
|
165
244
|
{ path: 'props.data', label: 'Options come from', type: 'dataSource' }
|
|
166
245
|
]
|
|
167
|
-
}
|
|
246
|
+
},
|
|
247
|
+
...styleGroups(INPUT_STYLES.filter((k) => k !== 'textAlign'))
|
|
168
248
|
]
|
|
169
249
|
},
|
|
170
250
|
|
|
@@ -173,53 +253,70 @@ export const CONTROLS = {
|
|
|
173
253
|
label: 'Label',
|
|
174
254
|
group: 'Content',
|
|
175
255
|
input: false,
|
|
176
|
-
defaultSize: { w: 0.92, h: 0.
|
|
256
|
+
defaultSize: { w: 0.92, h: 0.05 },
|
|
177
257
|
defaults: { text: 'Label', variant: 'text' },
|
|
178
|
-
props: ['text', 'variant'],
|
|
258
|
+
props: ['text', 'variant', 'style'],
|
|
179
259
|
validation: [],
|
|
260
|
+
styles: ['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'textAlign', 'color', 'background', 'borderColor', 'borderWidth', 'borderRadius'],
|
|
180
261
|
properties: [
|
|
181
262
|
{
|
|
182
263
|
key: 'content',
|
|
183
264
|
title: 'Content',
|
|
184
265
|
fields: [
|
|
185
266
|
{ path: 'props.text', label: 'Text', type: 'textarea' },
|
|
186
|
-
{ path: 'props.variant', label: '
|
|
267
|
+
{ path: 'props.variant', label: 'Preset', type: 'select', options: [
|
|
187
268
|
{ value: 'heading', label: 'Heading' },
|
|
188
269
|
{ value: 'subheading', label: 'Subheading' },
|
|
189
270
|
{ value: 'text', label: 'Text' }
|
|
190
|
-
] }
|
|
191
|
-
]
|
|
192
|
-
}
|
|
193
|
-
]
|
|
194
|
-
},
|
|
195
|
-
|
|
196
|
-
button: {
|
|
197
|
-
type: 'button',
|
|
198
|
-
label: 'Button',
|
|
199
|
-
group: 'Actions',
|
|
200
|
-
input: false,
|
|
201
|
-
defaultSize: { w: 0.18, h: 0.07 },
|
|
202
|
-
defaults: { label: 'Save', action: 'submit' },
|
|
203
|
-
props: ['label', 'action'],
|
|
204
|
-
validation: [],
|
|
205
|
-
properties: [
|
|
206
|
-
{
|
|
207
|
-
key: 'button',
|
|
208
|
-
title: 'Button',
|
|
209
|
-
fields: [
|
|
210
|
-
{ path: 'props.label', label: 'Label', type: 'text' },
|
|
211
|
-
{ path: 'props.action', label: 'Does', type: 'select', options: [
|
|
212
|
-
{ value: 'submit', label: 'Submit the form' },
|
|
213
|
-
{ value: 'reset', label: 'Reset the form' }
|
|
214
|
-
] }
|
|
271
|
+
], help: 'A starting size and weight — the Text settings below override it' }
|
|
215
272
|
]
|
|
216
|
-
}
|
|
273
|
+
},
|
|
274
|
+
...styleGroups(['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'textAlign', 'color', 'background', 'borderColor', 'borderWidth', 'borderRadius'])
|
|
217
275
|
]
|
|
218
276
|
}
|
|
219
277
|
}
|
|
220
278
|
|
|
279
|
+
/** What a list's rows can offer. */
|
|
280
|
+
export const LIST_ACTIONS = ['new', 'edit', 'delete']
|
|
281
|
+
|
|
282
|
+
CONTROLS.list = {
|
|
283
|
+
type: 'list',
|
|
284
|
+
label: 'List',
|
|
285
|
+
group: 'Data',
|
|
286
|
+
input: false,
|
|
287
|
+
defaultSize: { w: 0.92, h: 0.42 },
|
|
288
|
+
// No columns → the screen's own fields, in reading order.
|
|
289
|
+
defaults: { title: 'Saved records', pageSize: 10, actions: ['new', 'edit', 'delete'] },
|
|
290
|
+
// editScreen: the id of the screen Edit and New open in a POPUP. Without one,
|
|
291
|
+
// Edit opens the row in this screen's own fields (a list on a form).
|
|
292
|
+
props: ['title', 'source', 'editScreen', 'columns', 'pageSize', 'actions', 'style'],
|
|
293
|
+
validation: [],
|
|
294
|
+
styles: ['fontFamily', 'fontSize', 'color', 'background', 'borderColor', 'borderWidth', 'borderRadius'],
|
|
295
|
+
properties: [
|
|
296
|
+
{
|
|
297
|
+
key: 'list',
|
|
298
|
+
title: 'List',
|
|
299
|
+
fields: [
|
|
300
|
+
{ path: 'props.title', label: 'Title', type: 'text' },
|
|
301
|
+
{ path: 'props.source', label: 'Records from', type: 'table', help: 'Blank → the table this screen saves to' },
|
|
302
|
+
{ path: 'props.editScreen', label: 'Edit in', type: 'screen', help: 'The screen Edit and New open in a popup' },
|
|
303
|
+
{ path: 'props.pageSize', label: 'Rows per page', type: 'number' }
|
|
304
|
+
]
|
|
305
|
+
},
|
|
306
|
+
{ key: 'columns', title: 'Columns', fields: [{ path: 'props.columns', label: 'Columns', type: 'columns' }] },
|
|
307
|
+
{ key: 'actions', title: 'Actions', fields: [{ path: 'props.actions', label: 'Actions', type: 'actions' }] },
|
|
308
|
+
...styleGroups(['fontFamily', 'fontSize', 'color', 'background', 'borderColor', 'borderWidth', 'borderRadius'])
|
|
309
|
+
]
|
|
310
|
+
}
|
|
311
|
+
|
|
221
312
|
export const LABEL_VARIANTS = ['heading', 'subheading', 'text']
|
|
222
|
-
|
|
313
|
+
|
|
314
|
+
/** The sizes a label preset starts from, in px at the design width. */
|
|
315
|
+
export const LABEL_PRESETS = {
|
|
316
|
+
heading: { fontSize: 26, fontWeight: 700 },
|
|
317
|
+
subheading: { fontSize: 18, fontWeight: 600 },
|
|
318
|
+
text: { fontSize: 14, fontWeight: 400 }
|
|
319
|
+
}
|
|
223
320
|
|
|
224
321
|
/** Palette order: grouped, in declaration order. */
|
|
225
322
|
export function controlGroups(controls = CONTROLS) {
|
|
@@ -3,14 +3,25 @@ import { XeplrCanvas } from '@xeplr/ui-canvas'
|
|
|
3
3
|
import ControlView from './ControlView.jsx'
|
|
4
4
|
import Palette, { DRAG_TYPE } from './Palette.jsx'
|
|
5
5
|
import PropertyPanel from './PropertyPanel.jsx'
|
|
6
|
+
import { screenStyle } from './styles.js'
|
|
6
7
|
|
|
7
|
-
// The builder, drawn: name and
|
|
8
|
-
// on the canvas in the middle,
|
|
9
|
-
//
|
|
8
|
+
// The builder, drawn: name and save status on top, controls on the left, the
|
|
9
|
+
// screen on the canvas in the middle, properties on the right — the selected
|
|
10
|
+
// control's, or the screen's own when nothing is selected. Presentation only —
|
|
11
|
+
// everything comes from useFactoryBuilder.
|
|
10
12
|
//
|
|
11
|
-
// The canvas is
|
|
12
|
-
// screen's aspect
|
|
13
|
-
// the builder shows the screen at its real
|
|
13
|
+
// The canvas is exactly the screen's design width (never wider), in fraction
|
|
14
|
+
// units with pageAspect = the screen's aspect: the same geometry ScreenSample
|
|
15
|
+
// renders with, so the builder shows the screen at its real size.
|
|
16
|
+
|
|
17
|
+
const STATUS = {
|
|
18
|
+
idle: '',
|
|
19
|
+
pending: 'Unsaved changes…',
|
|
20
|
+
saving: 'Saving…',
|
|
21
|
+
saved: 'All changes saved',
|
|
22
|
+
invalid: 'Not saved — fix the problems',
|
|
23
|
+
error: 'Not saved'
|
|
24
|
+
}
|
|
14
25
|
|
|
15
26
|
export default function BuilderSample({ ctrl, renderPreview, className, style }) {
|
|
16
27
|
const { document: doc } = ctrl
|
|
@@ -44,28 +55,54 @@ export default function BuilderSample({ ctrl, renderPreview, className, style })
|
|
|
44
55
|
placeholder="Screen name, e.g. New employee"
|
|
45
56
|
aria-label="Screen name"
|
|
46
57
|
/>
|
|
47
|
-
<div className=
|
|
48
|
-
{ctrl.
|
|
58
|
+
<div className={`xeplr-factory-bar-status xeplr-factory-bar-status--${ctrl.status}`} role="status" aria-live="polite">
|
|
59
|
+
{ctrl.status === 'error' ? `Not saved — ${ctrl.saveError}` : STATUS[ctrl.status]}
|
|
49
60
|
</div>
|
|
50
61
|
{problemCount > 0 && (
|
|
51
|
-
<span className=
|
|
62
|
+
<span className="xeplr-factory-problems" title={ctrl.validation.errors.map((x) => `${x.path}: ${x.message}`).join('\n')}>
|
|
52
63
|
{problemCount} problem{problemCount === 1 ? '' : 's'}
|
|
53
64
|
</span>
|
|
54
65
|
)}
|
|
66
|
+
{ctrl.status === 'error' && (
|
|
67
|
+
<button type="button" className="xeplr-factory-secondary" onClick={ctrl.save}>Retry</button>
|
|
68
|
+
)}
|
|
55
69
|
{renderPreview && (
|
|
56
70
|
<button type="button" className="xeplr-factory-secondary" aria-pressed={preview} onClick={() => setPreview((p) => !p)}>
|
|
57
71
|
{preview ? 'Back to design' : 'Preview'}
|
|
58
72
|
</button>
|
|
59
73
|
)}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
{ctrl.canPublish && (
|
|
75
|
+
<button type="button" className="xeplr-factory-primary" onClick={() => ctrl.publish()} disabled={ctrl.publishing}>
|
|
76
|
+
{ctrl.publishing ? 'Publishing…' : 'Publish'}
|
|
77
|
+
</button>
|
|
78
|
+
)}
|
|
63
79
|
</header>
|
|
64
80
|
|
|
65
|
-
{ctrl.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
{ctrl.publishResult && ctrl.publishResult.confirm && (
|
|
82
|
+
<div className="xeplr-factory-publish is-confirm" role="alertdialog" aria-label="Confirm removing columns">
|
|
83
|
+
<div className="xeplr-factory-confirm-text">
|
|
84
|
+
<strong>Publishing removes {ctrl.publishResult.confirm.length === 1 ? 'a column' : `${ctrl.publishResult.confirm.length} columns`} and all the data in {ctrl.publishResult.confirm.length === 1 ? 'it' : 'them'}.</strong>
|
|
85
|
+
<ul>
|
|
86
|
+
{ctrl.publishResult.confirm.map((c) => (
|
|
87
|
+
<li key={c.column}><code>{c.column}</code> — {c.records} saved value{c.records === 1 ? '' : 's'}, for every company using this table</li>
|
|
88
|
+
))}
|
|
89
|
+
</ul>
|
|
90
|
+
This cannot be undone. To keep the data, put the field back before publishing.
|
|
91
|
+
</div>
|
|
92
|
+
<div className="xeplr-factory-confirm-actions">
|
|
93
|
+
<button type="button" className="xeplr-factory-danger" disabled={ctrl.publishing}
|
|
94
|
+
onClick={() => ctrl.publish(ctrl.publishResult.confirm.map((c) => c.column))}>
|
|
95
|
+
{ctrl.publishing ? 'Publishing…' : 'Remove and publish'}
|
|
96
|
+
</button>
|
|
97
|
+
<button type="button" className="xeplr-factory-secondary" onClick={ctrl.clearPublishResult}>Cancel</button>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
{ctrl.publishResult && !ctrl.publishResult.confirm && (
|
|
102
|
+
<div className={`xeplr-factory-publish${ctrl.publishResult.ok ? ' is-ok' : ' is-error'}`} role="status">
|
|
103
|
+
<span>{ctrl.publishResult.message}</span>
|
|
104
|
+
{ctrl.publishResult.detail && <pre className="xeplr-factory-publish-detail">{ctrl.publishResult.detail}</pre>}
|
|
105
|
+
<button type="button" className="xeplr-factory-icon-button" aria-label="Dismiss" onClick={ctrl.clearPublishResult}>×</button>
|
|
69
106
|
</div>
|
|
70
107
|
)}
|
|
71
108
|
|
|
@@ -80,34 +117,41 @@ export default function BuilderSample({ ctrl, renderPreview, className, style })
|
|
|
80
117
|
onDragOver={(e) => { if ([...e.dataTransfer.types].includes(DRAG_TYPE)) { e.preventDefault(); e.dataTransfer.dropEffect = 'copy' } }}
|
|
81
118
|
onDrop={onDrop}
|
|
82
119
|
>
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
120
|
+
<div className="xeplr-factory-frame" style={{ maxWidth: doc.width }}>
|
|
121
|
+
<XeplrCanvas
|
|
122
|
+
className="xeplr-factory-canvas"
|
|
123
|
+
style={screenStyle(doc)}
|
|
124
|
+
items={doc.nodes}
|
|
125
|
+
units="fraction"
|
|
126
|
+
pageAspect={doc.aspect}
|
|
127
|
+
features={{ resize: true, marquee: true }}
|
|
128
|
+
selection={ctrl.selected}
|
|
129
|
+
onSelectionChange={ctrl.setSelected}
|
|
130
|
+
onItemChange={ctrl.moveControl}
|
|
131
|
+
minSizeFor={() => ({ minWidth: 16, minHeight: 12 })}
|
|
132
|
+
renderItem={(node, { selected }) => (
|
|
133
|
+
<div className={`xeplr-factory-design-node${ctrl.errorsByNode[node.id] ? ' has-error' : ''}${selected ? ' is-selected' : ''}`}>
|
|
134
|
+
<ControlView node={node} mode="design" list={node.type === 'list' ? { doc } : undefined} />
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
underlay={doc.nodes.length === 0 && (
|
|
138
|
+
<div className="xeplr-factory-empty">Drag controls here from the left</div>
|
|
139
|
+
)}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
102
142
|
</div>
|
|
103
143
|
|
|
104
144
|
<PropertyPanel
|
|
145
|
+
doc={doc}
|
|
105
146
|
node={ctrl.selectedNode}
|
|
106
147
|
control={ctrl.selectedControl}
|
|
107
148
|
selectionCount={ctrl.selected.size}
|
|
108
|
-
errors={ctrl.selectedNode ? ctrl.errorsByNode[ctrl.selectedNode.id] :
|
|
149
|
+
errors={ctrl.selectedNode ? ctrl.errorsByNode[ctrl.selectedNode.id] : ctrl.errorsByNode._document}
|
|
109
150
|
tables={ctrl.tables}
|
|
151
|
+
screens={ctrl.screens}
|
|
152
|
+
lockedNames={ctrl.lockedNames}
|
|
110
153
|
onChange={(path, value) => ctrl.setProperty(ctrl.selectedNode.id, path, value)}
|
|
154
|
+
onScreenChange={ctrl.setScreen}
|
|
111
155
|
onRemove={ctrl.removeSelected}
|
|
112
156
|
/>
|
|
113
157
|
</div>
|