@visns-studio/visns-components 6.1.8 → 6.2.1
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/README.md +161 -1
- package/package.json +1 -1
- package/src/components/Fetch.jsx +49 -19
- package/src/components/generic/GenericFormBuilder.jsx +259 -0
- package/src/components/generic/GenericReport.jsx +1207 -286
- package/src/components/generic/SectionGroupedReport.jsx +199 -226
- package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +81 -0
- package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +337 -0
- package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +483 -0
- package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +202 -0
- package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +157 -0
- package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +352 -0
- package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +154 -0
- package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +153 -0
- package/src/components/generic/reportSemantics.js +971 -0
- package/src/components/generic/useSemanticModel.js +99 -0
- package/src/components/styles/ReportSemantic.module.scss +444 -0
- package/src/index.js +4 -0
package/README.md
CHANGED
|
@@ -3300,13 +3300,173 @@ A powerful and enhanced report building component that provides an intuitive wiz
|
|
|
3300
3300
|
columnUrl: '/ajax/reportBuilder/getTableColumns',
|
|
3301
3301
|
reportsUrl: '/ajax/reportBuilder/reports',
|
|
3302
3302
|
executeUrl: '/ajax/reportBuilder/execute',
|
|
3303
|
-
|
|
3303
|
+
detectedJoinsUrl: '/ajax/reportBuilder/getSuggestedJoins',
|
|
3304
|
+
exportUrl: '/ajax/reportBuilder/export',
|
|
3305
|
+
semanticModelUrl: '/ajax/reportBuilder/semanticModel',
|
|
3304
3306
|
}}
|
|
3305
3307
|
userProfile={userProfile}
|
|
3306
3308
|
businessTemplates={businessTemplates} // Optional pre-built templates
|
|
3307
3309
|
/>
|
|
3308
3310
|
```
|
|
3309
3311
|
|
|
3312
|
+
**Semantic Mode (report definition v2)**
|
|
3313
|
+
|
|
3314
|
+
On mount the component POSTs to `setting.semanticModelUrl` (default
|
|
3315
|
+
`/ajax/reportBuilder/semanticModel`). If it responds with
|
|
3316
|
+
`{success: true, data: {entities: {...}}}` the wizard switches to **semantic
|
|
3317
|
+
mode**, where the user picks business entities, labelled fields and labelled
|
|
3318
|
+
relations — no table or column names are ever shown. If the endpoint 404s or
|
|
3319
|
+
errors, the wizard silently falls back to **legacy mode** (the existing
|
|
3320
|
+
table/column/join behaviour), so this is fully backwards compatible.
|
|
3321
|
+
|
|
3322
|
+
Semantic model response shape:
|
|
3323
|
+
|
|
3324
|
+
```json
|
|
3325
|
+
{
|
|
3326
|
+
"success": true,
|
|
3327
|
+
"data": {
|
|
3328
|
+
"entities": {
|
|
3329
|
+
"clients": {
|
|
3330
|
+
"label": "Clients",
|
|
3331
|
+
"plural": "Clients",
|
|
3332
|
+
"description": "People the practice advises",
|
|
3333
|
+
"fields": {
|
|
3334
|
+
"firstname": { "label": "First name", "type": "text" },
|
|
3335
|
+
"fee_amount": {
|
|
3336
|
+
"label": "Fee amount",
|
|
3337
|
+
"type": "money",
|
|
3338
|
+
"summable": true
|
|
3339
|
+
},
|
|
3340
|
+
"fds_due_date": { "label": "FDS due date", "type": "date" },
|
|
3341
|
+
"status": {
|
|
3342
|
+
"label": "Status",
|
|
3343
|
+
"type": "enum",
|
|
3344
|
+
"values": { "1": "Active", "0": "Inactive" }
|
|
3345
|
+
}
|
|
3346
|
+
},
|
|
3347
|
+
"relations": {
|
|
3348
|
+
"adviser": {
|
|
3349
|
+
"label": "Their adviser",
|
|
3350
|
+
"entity": "users",
|
|
3351
|
+
"cardinality": "one"
|
|
3352
|
+
},
|
|
3353
|
+
"notes": {
|
|
3354
|
+
"label": "Their notes",
|
|
3355
|
+
"entity": "client_notes",
|
|
3356
|
+
"cardinality": "many"
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
```
|
|
3364
|
+
|
|
3365
|
+
Field types are `text | number | money | percent | date | datetime | boolean | enum`.
|
|
3366
|
+
Field ids are opaque handles — the server maps them to storage, so the client
|
|
3367
|
+
never needs to know the underlying table or column.
|
|
3368
|
+
|
|
3369
|
+
The wizard builds a **report definition v2** document, saves it in `detail`, and
|
|
3370
|
+
POSTs it when running or exporting the report:
|
|
3371
|
+
|
|
3372
|
+
```json
|
|
3373
|
+
{
|
|
3374
|
+
"schema_version": 2,
|
|
3375
|
+
"entity": "clients",
|
|
3376
|
+
"fields": [
|
|
3377
|
+
{ "field": "firstname" },
|
|
3378
|
+
{ "field": "adviser.name" },
|
|
3379
|
+
{ "agg": "sum", "field": "fee_amount", "label": "Total fees" }
|
|
3380
|
+
],
|
|
3381
|
+
"filters": {
|
|
3382
|
+
"op": "and",
|
|
3383
|
+
"items": [
|
|
3384
|
+
{ "field": "status", "operator": "equals", "value": "1" },
|
|
3385
|
+
{
|
|
3386
|
+
"op": "or",
|
|
3387
|
+
"items": [
|
|
3388
|
+
{ "field": "home_email", "operator": "not_empty" },
|
|
3389
|
+
{ "field": "work_email", "operator": "not_empty" }
|
|
3390
|
+
]
|
|
3391
|
+
},
|
|
3392
|
+
{
|
|
3393
|
+
"field": "fds_due_date",
|
|
3394
|
+
"operator": "between",
|
|
3395
|
+
"param": "due_range"
|
|
3396
|
+
}
|
|
3397
|
+
]
|
|
3398
|
+
},
|
|
3399
|
+
"parameters": [
|
|
3400
|
+
{
|
|
3401
|
+
"id": "due_range",
|
|
3402
|
+
"label": "Due date range",
|
|
3403
|
+
"type": "date_range",
|
|
3404
|
+
"required": true
|
|
3405
|
+
}
|
|
3406
|
+
],
|
|
3407
|
+
"groupBy": ["adviser.name"],
|
|
3408
|
+
"sort": [{ "field": "surname", "dir": "asc" }]
|
|
3409
|
+
}
|
|
3410
|
+
```
|
|
3411
|
+
|
|
3412
|
+
- **Relation dot-paths**: a `relation.field` path (e.g. `adviser.name`) reaches
|
|
3413
|
+
a field through a declared relation. Only relations declared in the semantic
|
|
3414
|
+
model can be traversed.
|
|
3415
|
+
- **Aggregates**: `sum | count | avg | min | max`. `sum` and `avg` are offered
|
|
3416
|
+
only on fields marked `summable: true` or typed `number`, `money` or
|
|
3417
|
+
`percent`; `count` is available on anything.
|
|
3418
|
+
- **Runtime parameters**: a filter carrying `param` instead of `value` is a
|
|
3419
|
+
runtime parameter — the run screen prompts for it using the matching entry
|
|
3420
|
+
in `parameters` before executing.
|
|
3421
|
+
|
|
3422
|
+
**Operators by field type:**
|
|
3423
|
+
|
|
3424
|
+
| Field type | Operators |
|
|
3425
|
+
| ----------------------- | --------------------------------------------------------------------------- |
|
|
3426
|
+
| `text` | `equals`, `not_equals`, `contains`, `not_contains`, `is_empty`, `not_empty` |
|
|
3427
|
+
| `number`/`money`/`percent` | `equals`, `not_equals`, `gt`, `gte`, `lt`, `lte`, `between`, `is_empty`, `not_empty` |
|
|
3428
|
+
| `date`/`datetime` | `equals`, `before`, `after`, `between`, `is_empty`, `not_empty` |
|
|
3429
|
+
| `boolean` | `is_true`, `is_false` |
|
|
3430
|
+
| `enum` | `equals`, `not_equals`, `in`, `not_in` |
|
|
3431
|
+
|
|
3432
|
+
**Execute contract:**
|
|
3433
|
+
|
|
3434
|
+
- `POST executeUrl` with `{definition, parameters, limit, offset}` →
|
|
3435
|
+
`{success, data: [rows], total}`. Each row is keyed by the field path or the
|
|
3436
|
+
aggregate label exactly as given in `fields` — e.g. `"firstname"`,
|
|
3437
|
+
`"adviser.name"`, `"Total fees"`.
|
|
3438
|
+
- `POST exportUrl` with `{definition, parameters, format}` → a file blob.
|
|
3439
|
+
|
|
3440
|
+
Save/load endpoints are unchanged; `detail` simply holds the v2 document. A
|
|
3441
|
+
legacy v1 document (one that has `mainTable`) keeps the current legacy
|
|
3442
|
+
behaviour.
|
|
3443
|
+
|
|
3444
|
+
The pure helpers behind all of this live in
|
|
3445
|
+
`src/components/generic/reportSemantics.js` (model lookup, operator menus,
|
|
3446
|
+
definition serialisation) with the endpoint probe in
|
|
3447
|
+
`src/components/generic/useSemanticModel.js`. They are covered by:
|
|
3448
|
+
|
|
3449
|
+
```bash
|
|
3450
|
+
node --test tests/reportSemantics.test.mjs
|
|
3451
|
+
```
|
|
3452
|
+
|
|
3453
|
+
**Embeddable grouped report viewers**
|
|
3454
|
+
|
|
3455
|
+
`GroupedReportRenderer` and `SectionGroupedReport` are exported so consuming apps
|
|
3456
|
+
can embed a read-only grouped report view without the wizard. Columns accept an
|
|
3457
|
+
explicit `key` (the exact row key returned by the execute contract) and a
|
|
3458
|
+
`header` label:
|
|
3459
|
+
|
|
3460
|
+
```jsx
|
|
3461
|
+
import { GroupedReportRenderer } from '@visns-studio/visns-components';
|
|
3462
|
+
|
|
3463
|
+
<GroupedReportRenderer
|
|
3464
|
+
data={{ grouped: true, groups, totalGroups, totalRecords }}
|
|
3465
|
+
config={{ type: 'sections', groupDisplayName: 'Adviser' }}
|
|
3466
|
+
columns={[{ key: 'adviser.name', header: 'Adviser' }]}
|
|
3467
|
+
/>;
|
|
3468
|
+
```
|
|
3469
|
+
|
|
3310
3470
|
**Major Enhancements:**
|
|
3311
3471
|
|
|
3312
3472
|
- **6-Step Wizard Interface**: Guided workflow with visual progress indicators
|
package/package.json
CHANGED
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
92
92
|
},
|
|
93
93
|
"name": "@visns-studio/visns-components",
|
|
94
|
-
"version": "6.1
|
|
94
|
+
"version": "6.2.1",
|
|
95
95
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
96
96
|
"main": "src/index.js",
|
|
97
97
|
"files": [
|
package/src/components/Fetch.jsx
CHANGED
|
@@ -124,6 +124,41 @@ const invalidateRelatedCaches = (url, method) => {
|
|
|
124
124
|
}
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Turn an API error bag into displayable HTML, whatever shape it arrived in.
|
|
129
|
+
*
|
|
130
|
+
* Laravel validation sends `{field: ["msg", ...]}`; the report-semantics
|
|
131
|
+
* endpoints send `[{path, message}, ...]` (see report-semantics.md §6). A bare
|
|
132
|
+
* list of strings is accepted too. Anything unreadable yields '' so the caller
|
|
133
|
+
* can fall back to `data.message` — never a thrown TypeError, which used to
|
|
134
|
+
* leave the request promise permanently unsettled.
|
|
135
|
+
*/
|
|
136
|
+
const flattenErrorBag = (errors) => {
|
|
137
|
+
const lines = [];
|
|
138
|
+
|
|
139
|
+
const collect = (entry) => {
|
|
140
|
+
if (entry === null || entry === undefined) return;
|
|
141
|
+
if (Array.isArray(entry)) {
|
|
142
|
+
entry.forEach(collect);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (typeof entry === 'object') {
|
|
146
|
+
// `{path, message}` from the semantic compiler.
|
|
147
|
+
if (typeof entry.message === 'string') {
|
|
148
|
+
lines.push(entry.message);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
Object.values(entry).forEach(collect);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
lines.push(String(entry));
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
collect(errors);
|
|
158
|
+
|
|
159
|
+
return lines.filter(Boolean).map((line) => `${line}<br />`).join('');
|
|
160
|
+
};
|
|
161
|
+
|
|
127
162
|
// Original CustomFetch logic extracted for reuse
|
|
128
163
|
const originalCustomFetch = (
|
|
129
164
|
url,
|
|
@@ -275,13 +310,12 @@ const originalCustomFetch = (
|
|
|
275
310
|
const { data } = error.response;
|
|
276
311
|
|
|
277
312
|
if (data.errors) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
});
|
|
313
|
+
errorMessage = flattenErrorBag(data.errors);
|
|
314
|
+
// A bag we could not read (or an empty one) must still
|
|
315
|
+
// surface something the user can act on.
|
|
316
|
+
if (!errorMessage && data.message) {
|
|
317
|
+
errorMessage = data.message;
|
|
318
|
+
}
|
|
285
319
|
} else if (data.message) {
|
|
286
320
|
if (data.message === 'CSRF token mismatch.') {
|
|
287
321
|
window.location.replace('/login');
|
|
@@ -345,12 +379,10 @@ const CustomFetch = (
|
|
|
345
379
|
if (error.response && error.response.data) {
|
|
346
380
|
const { data } = error.response;
|
|
347
381
|
if (data.errors) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
});
|
|
353
|
-
});
|
|
382
|
+
errorMessage =
|
|
383
|
+
flattenErrorBag(data.errors) ||
|
|
384
|
+
data.message ||
|
|
385
|
+
'';
|
|
354
386
|
} else if (data.message) {
|
|
355
387
|
errorMessage = data.message;
|
|
356
388
|
}
|
|
@@ -364,12 +396,10 @@ const CustomFetch = (
|
|
|
364
396
|
if (error.response && error.response.data) {
|
|
365
397
|
const { data } = error.response;
|
|
366
398
|
if (data.errors) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
});
|
|
372
|
-
});
|
|
399
|
+
errorMessage =
|
|
400
|
+
flattenErrorBag(data.errors) ||
|
|
401
|
+
data.message ||
|
|
402
|
+
'';
|
|
373
403
|
} else if (data.message) {
|
|
374
404
|
if (data.message === 'CSRF token mismatch.') {
|
|
375
405
|
window.location.replace('/login');
|
|
@@ -40,7 +40,9 @@ import {
|
|
|
40
40
|
ChevronRight,
|
|
41
41
|
Copy,
|
|
42
42
|
List,
|
|
43
|
+
HelpCircle,
|
|
43
44
|
} from 'lucide-react';
|
|
45
|
+
import Swal from 'sweetalert2';
|
|
44
46
|
import { confirmDialog } from '../utils/ConfirmDialog';
|
|
45
47
|
|
|
46
48
|
import 'react-toggle/style.css';
|
|
@@ -1990,6 +1992,254 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
1990
1992
|
fetchRoles();
|
|
1991
1993
|
}, []);
|
|
1992
1994
|
|
|
1995
|
+
/* ---------------------------------------------------------------- */
|
|
1996
|
+
/* Guided help */
|
|
1997
|
+
/* ---------------------------------------------------------------- */
|
|
1998
|
+
|
|
1999
|
+
/**
|
|
2000
|
+
* Inline SVG for the guide's section headings. SweetAlert2 takes an HTML
|
|
2001
|
+
* string, so the lucide components used elsewhere in this file cannot be
|
|
2002
|
+
* rendered here — these mirror the same icons at the same weight.
|
|
2003
|
+
*/
|
|
2004
|
+
const helpIcon = (name, color) => {
|
|
2005
|
+
const open = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; display: inline-block;">`;
|
|
2006
|
+
const paths = {
|
|
2007
|
+
rocket: '<path d="M12 2c5 3 8 5 8 9a6 6 0 1 1-12 0c0-4 3-6 8-9Z"/><path d="m16 6-4 14-4-14"/><circle cx="12" cy="12" r="2"/>',
|
|
2008
|
+
list: '<line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/>',
|
|
2009
|
+
layers: '<path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 12.5-9.17 4.16a2 2 0 0 1-1.66 0L2 12.5"/>',
|
|
2010
|
+
type: '<polyline points="4 7 4 4 20 4 20 7"/><line x1="9" x2="15" y1="20" y2="20"/><line x1="12" x2="12" y1="4" y2="20"/>',
|
|
2011
|
+
settings:
|
|
2012
|
+
'<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
|
|
2013
|
+
table: '<path d="M12 3v18"/><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/>',
|
|
2014
|
+
branch: '<line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
|
|
2015
|
+
flag: '<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" x2="4" y1="22" y2="15"/>',
|
|
2016
|
+
save: '<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/>',
|
|
2017
|
+
help: '<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>',
|
|
2018
|
+
};
|
|
2019
|
+
return `${open}${paths[name] || paths.help}</svg>`;
|
|
2020
|
+
};
|
|
2021
|
+
|
|
2022
|
+
/** One heading style for every section of the guide. */
|
|
2023
|
+
const helpHeading = (icon, color, title) =>
|
|
2024
|
+
`<h3 style="color: #1f2937; margin: 0 0 12px 0; font-size: 16px; border-bottom: 2px solid ${color}; padding-bottom: 6px; display: flex; align-items: center; gap: 8px;">${helpIcon(
|
|
2025
|
+
icon,
|
|
2026
|
+
color
|
|
2027
|
+
)}${title}</h3>`;
|
|
2028
|
+
|
|
2029
|
+
/** A tinted panel with a bulleted body, optionally titled. */
|
|
2030
|
+
const helpPanel = ({ bg, border, text, title, items }) => `
|
|
2031
|
+
<div style="background: ${bg}; border: 1px solid ${border}; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
|
|
2032
|
+
${
|
|
2033
|
+
title
|
|
2034
|
+
? `<h4 style="color: ${border}; margin: 0 0 8px 0; font-size: 14px;">${title}</h4>`
|
|
2035
|
+
: ''
|
|
2036
|
+
}
|
|
2037
|
+
<ul style="margin: 0; padding-left: 16px; color: ${text};">${items
|
|
2038
|
+
.map((item) => `<li>${item}</li>`)
|
|
2039
|
+
.join('')}</ul>
|
|
2040
|
+
</div>`;
|
|
2041
|
+
|
|
2042
|
+
/**
|
|
2043
|
+
* The end-user guide to building a template. Sections that describe an
|
|
2044
|
+
* opt-in feature are only included when this app actually switched that
|
|
2045
|
+
* feature on, so nobody is told about a control they will never see.
|
|
2046
|
+
*/
|
|
2047
|
+
const showGuidedHelp = () => {
|
|
2048
|
+
const sections = [
|
|
2049
|
+
helpHeading('rocket', '#3b82f6', 'What this screen is') +
|
|
2050
|
+
helpPanel({
|
|
2051
|
+
bg: '#f0f9ff',
|
|
2052
|
+
border: '#3b82f6',
|
|
2053
|
+
text: '#1e3a8a',
|
|
2054
|
+
items: [
|
|
2055
|
+
'This is the list of questions people will answer — for example a client review checklist',
|
|
2056
|
+
'<strong>Edit</strong> shows one compact row per question, with a summary of how it is set up',
|
|
2057
|
+
'<strong>Preview</strong> shows the form the way the person filling it in will see it',
|
|
2058
|
+
'<strong>Sections</strong> shows a strip of shortcuts, one per section; click one to jump to it',
|
|
2059
|
+
'Once there are more than five questions, a search box appears — matching questions stay bright and the rest dim',
|
|
2060
|
+
],
|
|
2061
|
+
}),
|
|
2062
|
+
|
|
2063
|
+
helpHeading('list', '#10b981', 'Adding and arranging questions') +
|
|
2064
|
+
helpPanel({
|
|
2065
|
+
bg: '#ecfdf5',
|
|
2066
|
+
border: '#10b981',
|
|
2067
|
+
text: '#065f46',
|
|
2068
|
+
items: dynamicFields
|
|
2069
|
+
? [
|
|
2070
|
+
'This template builds its own questions automatically, so there is no "Add Field" button',
|
|
2071
|
+
'Use the pencil to adjust a question, and the grip handle on the left to drag it into a new position',
|
|
2072
|
+
]
|
|
2073
|
+
: [
|
|
2074
|
+
'<strong>Add Field</strong> (bottom right) opens the question window',
|
|
2075
|
+
'The pencil edits a question, the copy icon duplicates it, the bin deletes it',
|
|
2076
|
+
'Duplicating a Section copies the section and every question inside it',
|
|
2077
|
+
'Drag the grip handle on the left to move a question; the up/down arrows let you type an exact position instead',
|
|
2078
|
+
'Deleting a question also clears any "show only when" rule that pointed at it, and tells you how many were cleared',
|
|
2079
|
+
],
|
|
2080
|
+
}),
|
|
2081
|
+
|
|
2082
|
+
helpHeading('layers', '#7c3aed', 'Sections and headings') +
|
|
2083
|
+
helpPanel({
|
|
2084
|
+
bg: '#f5f3ff',
|
|
2085
|
+
border: '#7c3aed',
|
|
2086
|
+
text: '#5b21b6',
|
|
2087
|
+
items: [
|
|
2088
|
+
'Add a question of type <strong>Section</strong> to start a new part of the form',
|
|
2089
|
+
'Everything after a Section belongs to it, until the next Section starts',
|
|
2090
|
+
'Questions before the first Section are listed under "Before first section"',
|
|
2091
|
+
'Click the arrow on a Section row to collapse or expand it while you work',
|
|
2092
|
+
'<strong>Heading</strong> is simpler — just a title on the page, with no grouping',
|
|
2093
|
+
'<strong>Plain Text</strong> is a block of formatted wording, for instructions or a declaration',
|
|
2094
|
+
],
|
|
2095
|
+
}),
|
|
2096
|
+
|
|
2097
|
+
helpHeading('type', '#0891b2', 'The kinds of question you can add') +
|
|
2098
|
+
helpPanel({
|
|
2099
|
+
bg: '#ecfeff',
|
|
2100
|
+
border: '#0891b2',
|
|
2101
|
+
text: '#155e75',
|
|
2102
|
+
items: [
|
|
2103
|
+
'<strong>Typed in:</strong> Text (one line), Textarea (several lines), Number',
|
|
2104
|
+
'<strong>Chosen from a list:</strong> Dropdown, Checkbox, Toggle (a yes/no switch)',
|
|
2105
|
+
'<strong>Dates and times:</strong> Date, Date & Time, Time',
|
|
2106
|
+
'<strong>Attached or drawn:</strong> File, Image, Video Upload, Signature, Canvas',
|
|
2107
|
+
'<strong>Grids:</strong> Table (Radio) and Table (Custom) — see below',
|
|
2108
|
+
'<strong>Layout only:</strong> Section, Heading, Plain Text',
|
|
2109
|
+
`<strong>Filled from your system:</strong> Dynamic Data pulls in a detail the system already holds${
|
|
2110
|
+
dynamicDropdowns && dynamicDropdowns.length > 0
|
|
2111
|
+
? ', and Dynamic Dropdown offers a list your system maintains'
|
|
2112
|
+
: ''
|
|
2113
|
+
}`,
|
|
2114
|
+
],
|
|
2115
|
+
}),
|
|
2116
|
+
|
|
2117
|
+
helpHeading('settings', '#f59e0b', 'Settings on each question') +
|
|
2118
|
+
helpPanel({
|
|
2119
|
+
bg: '#fffbeb',
|
|
2120
|
+
border: '#f59e0b',
|
|
2121
|
+
text: '#92400e',
|
|
2122
|
+
items: [
|
|
2123
|
+
'<strong>Label</strong> is the wording people read — write it as the question you want answered',
|
|
2124
|
+
'<strong>Required?</strong> set to Yes means the form cannot be submitted without an answer; required questions show an asterisk',
|
|
2125
|
+
'<strong>Size</strong> is how wide the question sits on the page: Full for its own row, Half or Quarter to share a row',
|
|
2126
|
+
'<strong>Height Size</strong> on a Textarea chooses a small, normal or large writing box',
|
|
2127
|
+
'<strong>Single or Bulk Image?</strong> decides whether one photo or several can be attached',
|
|
2128
|
+
'<strong>Associated Role?</strong> on a Signature says whose signature it is',
|
|
2129
|
+
'There is no help text, placeholder or preset answer — put any wording people need into the label, or a Plain Text block above',
|
|
2130
|
+
],
|
|
2131
|
+
}),
|
|
2132
|
+
|
|
2133
|
+
helpHeading('list', '#059669', 'Answers for Dropdown and Checkbox') +
|
|
2134
|
+
helpPanel({
|
|
2135
|
+
bg: '#f0fdf4',
|
|
2136
|
+
border: '#059669',
|
|
2137
|
+
text: '#166534',
|
|
2138
|
+
items: [
|
|
2139
|
+
'Type the answer in the box at the bottom of the Options list and press Enter, or click <strong>+ Add</strong>',
|
|
2140
|
+
'Click straight into an existing answer to reword it; the bin removes it',
|
|
2141
|
+
'Two answers cannot share the same wording',
|
|
2142
|
+
'Rewording an answer can unhook rules that were pointing at it — check any "show only when" rule that used it',
|
|
2143
|
+
],
|
|
2144
|
+
}),
|
|
2145
|
+
|
|
2146
|
+
helpHeading('table', '#be185d', 'Grid questions') +
|
|
2147
|
+
helpPanel({
|
|
2148
|
+
bg: '#fdf2f8',
|
|
2149
|
+
border: '#be185d',
|
|
2150
|
+
text: '#9d174d',
|
|
2151
|
+
items: [
|
|
2152
|
+
'<strong>Table (Radio)</strong> needs both Columns and Rows — the rows are the things being checked and the columns are the possible answers',
|
|
2153
|
+
'<strong>Table (Custom)</strong> needs Columns only, and gives one row of boxes to fill in',
|
|
2154
|
+
'Add a column or row by typing it in and pressing Enter, or clicking Add',
|
|
2155
|
+
'The rows are fixed when you build the template — the person filling in the form cannot add more',
|
|
2156
|
+
],
|
|
2157
|
+
}),
|
|
2158
|
+
|
|
2159
|
+
helpHeading(
|
|
2160
|
+
'branch',
|
|
2161
|
+
'#4f46e5',
|
|
2162
|
+
'Showing a question only when it applies'
|
|
2163
|
+
) +
|
|
2164
|
+
helpPanel({
|
|
2165
|
+
bg: '#eef2ff',
|
|
2166
|
+
border: '#4f46e5',
|
|
2167
|
+
text: '#3730a3',
|
|
2168
|
+
items: [
|
|
2169
|
+
'Every question window ends with <strong>Conditional Field Display Criteria</strong>',
|
|
2170
|
+
'Pick the earlier question it depends on, then either <strong>is</strong> with a value, or <strong>is filled in</strong>',
|
|
2171
|
+
'Only questions that come <em>before</em> this one can be used, and only ones that collect an answer',
|
|
2172
|
+
'A line at the bottom spells the rule out, e.g. "This field will only show when Advice type is Insurance"',
|
|
2173
|
+
'Leave it blank to always show the question, or use <strong>Clear rule</strong> to remove one',
|
|
2174
|
+
'Questions with a rule are marked "Conditional" in the list',
|
|
2175
|
+
],
|
|
2176
|
+
}),
|
|
2177
|
+
];
|
|
2178
|
+
|
|
2179
|
+
if (outstandingItems?.enabled) {
|
|
2180
|
+
sections.push(
|
|
2181
|
+
helpHeading('flag', '#d97706', 'Answers that raise a follow-up') +
|
|
2182
|
+
helpPanel({
|
|
2183
|
+
bg: '#fef3c7',
|
|
2184
|
+
border: '#d97706',
|
|
2185
|
+
text: '#92400e',
|
|
2186
|
+
items: [
|
|
2187
|
+
'On questions that offer answers, each answer has an <strong>Outstanding</strong> switch',
|
|
2188
|
+
'Turn it on for the answer that means "this needs following up", and the system raises an outstanding item when someone gives that answer',
|
|
2189
|
+
'Only one answer per question can be marked',
|
|
2190
|
+
'Leave every switch off to say this question never raises a follow-up',
|
|
2191
|
+
'If an amber warning appears, the answer the old rule pointed at no longer exists — set the switch again on the right answer',
|
|
2192
|
+
],
|
|
2193
|
+
})
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
sections.push(
|
|
2198
|
+
helpHeading('save', '#dc2626', 'Saving your work') +
|
|
2199
|
+
helpPanel({
|
|
2200
|
+
bg: '#fef2f2',
|
|
2201
|
+
border: '#dc2626',
|
|
2202
|
+
text: '#991b1b',
|
|
2203
|
+
items: [
|
|
2204
|
+
'<strong>Save</strong> inside a question window only adds it to the list on screen — nothing is stored yet',
|
|
2205
|
+
'To store the template, click <strong>Edit Form</strong> (bottom right) and then <strong>Save</strong>',
|
|
2206
|
+
'Moving a question saves the whole template straight away, so the order is never lost',
|
|
2207
|
+
'If someone else changed this template while you had it open, you are told nothing was saved — reload the page before saving again, or your changes would undo theirs',
|
|
2208
|
+
],
|
|
2209
|
+
})
|
|
2210
|
+
);
|
|
2211
|
+
|
|
2212
|
+
Swal.fire({
|
|
2213
|
+
title: `<div style="display: flex; align-items: center; justify-content: center; gap: 8px;">${helpIcon(
|
|
2214
|
+
'help',
|
|
2215
|
+
'#3b82f6'
|
|
2216
|
+
)}Form Builder Guide</div>`,
|
|
2217
|
+
html: `<div style="text-align: left; font-size: 13px; line-height: 1.5; color: #374151; max-height: 70vh; overflow-y: auto;">${sections.join(
|
|
2218
|
+
'<div style="height: 12px;"></div>'
|
|
2219
|
+
)}</div>`,
|
|
2220
|
+
width: 800,
|
|
2221
|
+
confirmButtonText: 'Close',
|
|
2222
|
+
confirmButtonColor: '#6b7280',
|
|
2223
|
+
customClass: {
|
|
2224
|
+
popup: 'comprehensive-help-modal',
|
|
2225
|
+
htmlContainer: 'help-content-scrollable',
|
|
2226
|
+
},
|
|
2227
|
+
});
|
|
2228
|
+
};
|
|
2229
|
+
|
|
2230
|
+
// Show the guide once, the first time someone opens the builder.
|
|
2231
|
+
useEffect(() => {
|
|
2232
|
+
if (localStorage.getItem('formBuilder_hasSeenHelp')) return;
|
|
2233
|
+
|
|
2234
|
+
const timer = setTimeout(() => {
|
|
2235
|
+
showGuidedHelp();
|
|
2236
|
+
localStorage.setItem('formBuilder_hasSeenHelp', 'true');
|
|
2237
|
+
}, 500);
|
|
2238
|
+
|
|
2239
|
+
return () => clearTimeout(timer);
|
|
2240
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2241
|
+
}, []);
|
|
2242
|
+
|
|
1993
2243
|
return (
|
|
1994
2244
|
<div className={modalShow || modalFormShow ? styles.modalOpen : ''}>
|
|
1995
2245
|
<div className={styles.grid}>
|
|
@@ -2081,6 +2331,15 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
2081
2331
|
).length}{' '}
|
|
2082
2332
|
sections
|
|
2083
2333
|
</span>
|
|
2334
|
+
<button
|
|
2335
|
+
type="button"
|
|
2336
|
+
className={styles.toolBtn}
|
|
2337
|
+
onClick={showGuidedHelp}
|
|
2338
|
+
data-tooltip-id="action-tooltip"
|
|
2339
|
+
data-tooltip-content="How to build this form"
|
|
2340
|
+
>
|
|
2341
|
+
<HelpCircle size={15} /> Help
|
|
2342
|
+
</button>
|
|
2084
2343
|
<button
|
|
2085
2344
|
className={`${styles.toolBtn} ${
|
|
2086
2345
|
showOutline ? styles.toolBtnOn : ''
|