@visns-studio/visns-components 6.0.3 → 6.0.5
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/package.json +1 -1
- package/src/components/DataGrid.jsx +30 -12
- package/src/components/auth/Login.jsx +203 -175
- package/src/components/columns/ColumnRenderers.jsx +34 -0
- package/src/components/generic/GenericFormBuilder.jsx +550 -25
- package/src/components/generic/GenericIndex.jsx +15 -5
- package/src/components/styles/DataGrid.module.scss +38 -22
- package/src/components/styles/Form.module.scss +32 -33
- package/src/components/styles/GenericDetail.module.scss +28 -30
- package/src/components/styles/GenericDynamic.module.scss +7 -21
- package/src/components/styles/GenericEditableTable.module.scss +5 -22
- package/src/components/styles/GenericFormBuilder.module.scss +545 -30
- package/src/components/styles/GenericIndex.module.scss +17 -32
- package/src/components/styles/GenericMain.module.scss +18 -6
- package/src/components/styles/GenericQuote.module.scss +7 -22
- package/src/components/styles/GenericReport.module.scss +5 -37
- package/src/components/styles/Login.module.scss +415 -239
- package/src/components/styles/Navigation.module.scss +124 -35
- package/src/components/styles/Profile.module.scss +5 -26
- package/src/components/styles/QuickAction.module.scss +34 -29
- package/src/components/styles/_controls.scss +139 -0
- package/src/components/styles/global.css +54 -15
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import '../styles/global.css';
|
|
2
2
|
|
|
3
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { useParams } from 'react-router-dom';
|
|
5
5
|
import StandardModal from './StandardModal';
|
|
6
6
|
import { arrayMove } from '@dnd-kit/sortable';
|
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
Search,
|
|
39
39
|
ChevronDown,
|
|
40
40
|
ChevronRight,
|
|
41
|
+
Copy,
|
|
42
|
+
List,
|
|
41
43
|
} from 'lucide-react';
|
|
42
44
|
import { confirmDialog } from '../utils/ConfirmDialog';
|
|
43
45
|
|
|
@@ -124,10 +126,28 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
124
126
|
});
|
|
125
127
|
const [moveToPosition, setMoveToPosition] = useState('');
|
|
126
128
|
const [collapsedSections, setCollapsedSections] = useState(new Set());
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Compact mode renders each field as a one-line summary instead of a live
|
|
132
|
+
* control. Site Start Inspection has 87 fields; previewing them all means
|
|
133
|
+
* two dozen full-height textareas and file pickers, so arranging the form
|
|
134
|
+
* turns into a scroll expedition through mostly empty boxes. Preview is
|
|
135
|
+
* still a click away when you want to see the real thing.
|
|
136
|
+
*/
|
|
137
|
+
const [compactMode, setCompactMode] = useState(true);
|
|
138
|
+
const [showOutline, setShowOutline] = useState(true);
|
|
127
139
|
const sortTriggeredRef = useRef(false);
|
|
128
140
|
|
|
129
141
|
const canvasTypes = SketchConfig.canvasTypes;
|
|
130
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Templates whose fields are composed per form rather than authored here
|
|
145
|
+
* (outstanding items build theirs from the linked inspection). Offering
|
|
146
|
+
* "add a field" on one of these would be offering something that has no
|
|
147
|
+
* effect.
|
|
148
|
+
*/
|
|
149
|
+
const dynamicFields = data.field_source === 'dynamic';
|
|
150
|
+
|
|
131
151
|
// Generate compact field info for display in the center top
|
|
132
152
|
const getFieldInfo = (field) => {
|
|
133
153
|
const fieldTypeLabel =
|
|
@@ -236,6 +256,181 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
236
256
|
return collapsed;
|
|
237
257
|
};
|
|
238
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Sections are implicit: `type: 'section'` acts as a divider in a flat
|
|
261
|
+
* array, and everything after it belongs to it until the next one. This
|
|
262
|
+
* turns that into groups for the outline and the section headers.
|
|
263
|
+
*/
|
|
264
|
+
const sectionGroups = useMemo(() => {
|
|
265
|
+
const groups = [];
|
|
266
|
+
let current = null;
|
|
267
|
+
|
|
268
|
+
(data.detail || []).forEach((field, index) => {
|
|
269
|
+
if (field.type === 'section') {
|
|
270
|
+
current = {
|
|
271
|
+
id: field.id,
|
|
272
|
+
label: field.label || 'Untitled section',
|
|
273
|
+
index,
|
|
274
|
+
fields: [],
|
|
275
|
+
};
|
|
276
|
+
groups.push(current);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Fields before the first section still need somewhere to live.
|
|
281
|
+
if (!current) {
|
|
282
|
+
current = {
|
|
283
|
+
id: '__ungrouped',
|
|
284
|
+
label: 'Before first section',
|
|
285
|
+
index: -1,
|
|
286
|
+
fields: [],
|
|
287
|
+
};
|
|
288
|
+
groups.push(current);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
current.fields.push({ field, index });
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
return groups;
|
|
295
|
+
}, [data.detail]);
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* A duplicate needs its own id or React keys collide and the sortable
|
|
299
|
+
* list starts swapping the wrong rows. Ids are "<slug>::<uuid>", so the
|
|
300
|
+
* slug is kept for readability and the uuid regenerated.
|
|
301
|
+
*/
|
|
302
|
+
const freshFieldId = (existingId) => {
|
|
303
|
+
const slug = String(existingId || 'field').split('::')[0];
|
|
304
|
+
return `${slug}::${uuidv4()}`;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const handleDuplicateField = (id) => {
|
|
308
|
+
const index = (data.detail || []).findIndex((item) => item.id === id);
|
|
309
|
+
if (index < 0) return;
|
|
310
|
+
|
|
311
|
+
const original = data.detail[index];
|
|
312
|
+
const copy = {
|
|
313
|
+
...original,
|
|
314
|
+
id: freshFieldId(original.id),
|
|
315
|
+
label: `${original.label} (copy)`,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const newDetail = [...data.detail];
|
|
319
|
+
newDetail.splice(index + 1, 0, copy);
|
|
320
|
+
setData((items) => ({ ...items, detail: newDetail }));
|
|
321
|
+
toast.success(`Duplicated "${original.label}".`);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Duplicating a section takes its fields with it — the whole point is the
|
|
326
|
+
* repeated checkbox / photo / comments block, which is tedious to rebuild
|
|
327
|
+
* one field at a time.
|
|
328
|
+
*/
|
|
329
|
+
const handleDuplicateSection = (sectionId) => {
|
|
330
|
+
const group = sectionGroups.find((entry) => entry.id === sectionId);
|
|
331
|
+
if (!group || group.index < 0) return;
|
|
332
|
+
|
|
333
|
+
const section = data.detail[group.index];
|
|
334
|
+
const block = [section, ...group.fields.map((entry) => entry.field)];
|
|
335
|
+
const copies = block.map((field, offset) => ({
|
|
336
|
+
...field,
|
|
337
|
+
id: freshFieldId(field.id),
|
|
338
|
+
label: offset === 0 ? `${field.label} (copy)` : field.label,
|
|
339
|
+
// A copied field must not keep pointing at the original's
|
|
340
|
+
// conditional source, or editing one would silently drive both.
|
|
341
|
+
conditional_field: '',
|
|
342
|
+
conditional_value: '',
|
|
343
|
+
}));
|
|
344
|
+
|
|
345
|
+
const insertAt = group.index + block.length;
|
|
346
|
+
const newDetail = [...data.detail];
|
|
347
|
+
newDetail.splice(insertAt, 0, ...copies);
|
|
348
|
+
setData((items) => ({ ...items, detail: newDetail }));
|
|
349
|
+
toast.success(
|
|
350
|
+
`Duplicated "${section.label}" and ${group.fields.length} field${
|
|
351
|
+
group.fields.length === 1 ? '' : 's'
|
|
352
|
+
}.`
|
|
353
|
+
);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Which section is currently on screen, so the outline reflects where you
|
|
359
|
+
* are rather than just listing what exists. Without it the rail is a
|
|
360
|
+
* static menu and you still lose your place in 87 fields.
|
|
361
|
+
*/
|
|
362
|
+
const [activeSection, setActiveSection] = useState(null);
|
|
363
|
+
|
|
364
|
+
useEffect(() => {
|
|
365
|
+
if (!showOutline || sectionGroups.length === 0) return undefined;
|
|
366
|
+
|
|
367
|
+
const onScroll = () => {
|
|
368
|
+
let current = null;
|
|
369
|
+
for (const group of sectionGroups) {
|
|
370
|
+
if (group.index < 0) continue;
|
|
371
|
+
const node = document.getElementById(`fb-field-${group.id}`);
|
|
372
|
+
if (!node) continue;
|
|
373
|
+
// A section counts as current once its header passes the
|
|
374
|
+
// upper third of the viewport.
|
|
375
|
+
if (node.getBoundingClientRect().top <= window.innerHeight / 3) {
|
|
376
|
+
current = group.id;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
setActiveSection(current);
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
onScroll();
|
|
383
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
384
|
+
|
|
385
|
+
return () => window.removeEventListener('scroll', onScroll);
|
|
386
|
+
}, [showOutline, sectionGroups]);
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The app header is `position: fixed` with a content-driven height, so a
|
|
391
|
+
* hard-coded sticky offset puts the builder toolbar underneath it.
|
|
392
|
+
* Measure it and expose it as a custom property instead.
|
|
393
|
+
*/
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
const measure = () => {
|
|
396
|
+
// The <header> TAG, not a class: GenericMain renders it with a
|
|
397
|
+
// CSS-module class, so the DOM name is hashed and a `.header`
|
|
398
|
+
// selector never matches.
|
|
399
|
+
const header = document.querySelector('header');
|
|
400
|
+
|
|
401
|
+
// Only a fixed or sticky header overlaps the page; a static one
|
|
402
|
+
// scrolls away and must not add an offset.
|
|
403
|
+
let height = 0;
|
|
404
|
+
if (header) {
|
|
405
|
+
const pos = window.getComputedStyle(header).position;
|
|
406
|
+
if (pos === 'fixed' || pos === 'sticky') {
|
|
407
|
+
height = Math.round(header.getBoundingClientRect().height);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
document.documentElement.style.setProperty(
|
|
412
|
+
'--fb-sticky-top',
|
|
413
|
+
`${height}px`
|
|
414
|
+
);
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
measure();
|
|
418
|
+
window.addEventListener('resize', measure);
|
|
419
|
+
// Fonts and logo images can land after first paint and change the
|
|
420
|
+
// header height, so re-measure once things settle.
|
|
421
|
+
const settle = setTimeout(measure, 400);
|
|
422
|
+
|
|
423
|
+
return () => {
|
|
424
|
+
window.removeEventListener('resize', measure);
|
|
425
|
+
clearTimeout(settle);
|
|
426
|
+
};
|
|
427
|
+
}, []);
|
|
428
|
+
|
|
429
|
+
const scrollToField = (fieldId) => {
|
|
430
|
+
const node = document.getElementById(`fb-field-${fieldId}`);
|
|
431
|
+
if (node) node.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
432
|
+
};
|
|
433
|
+
|
|
239
434
|
const toggleSectionCollapse = (sectionId) => {
|
|
240
435
|
setCollapsedSections((prev) => {
|
|
241
436
|
const newSet = new Set(prev);
|
|
@@ -273,6 +468,155 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
273
468
|
const collapsedFieldIds = getCollapsedFieldIds();
|
|
274
469
|
|
|
275
470
|
// Create sortable field item using @dnd-kit
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Compact edit row.
|
|
474
|
+
*
|
|
475
|
+
* Deliberately shares NO markup or styles with SortableFieldItem. That
|
|
476
|
+
* component renders a tall preview card — absolutely positioned control
|
|
477
|
+
* bar, fixed heights, a two-column grid — and every attempt to override
|
|
478
|
+
* those from the outside produced a different broken layout. Two modes,
|
|
479
|
+
* two renderers, no shared stylesheet.
|
|
480
|
+
*
|
|
481
|
+
* The behaviour is identical: same sortable id, same handlers, same
|
|
482
|
+
* DndContext. Only the presentation differs.
|
|
483
|
+
*/
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Preview row — the form as it will actually appear.
|
|
487
|
+
*
|
|
488
|
+
* No drag handle, index badge, action icons or hover chrome: editing all
|
|
489
|
+
* happens in the compact list, so carrying the authoring furniture in
|
|
490
|
+
* here only obscures what the preview is for. Not sortable either, which
|
|
491
|
+
* is why it needs no DndContext.
|
|
492
|
+
*/
|
|
493
|
+
const PreviewFieldItem = ({ field }) => (
|
|
494
|
+
<div
|
|
495
|
+
className={`${renderClassName(field)} ${styles.pvItem}`}
|
|
496
|
+
data-field-size={field.size}
|
|
497
|
+
>
|
|
498
|
+
{renderField(field)}
|
|
499
|
+
</div>
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
const CompactFieldRow = ({ field, index: counter }) => {
|
|
503
|
+
const {
|
|
504
|
+
attributes,
|
|
505
|
+
listeners,
|
|
506
|
+
setNodeRef,
|
|
507
|
+
transform,
|
|
508
|
+
transition,
|
|
509
|
+
isDragging,
|
|
510
|
+
} = useSortable({
|
|
511
|
+
id: field.id,
|
|
512
|
+
data: { type: 'field', size: field.size, index: counter },
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const isSection = field.type === 'section';
|
|
516
|
+
const group = isSection
|
|
517
|
+
? sectionGroups.find((entry) => entry.id === field.id)
|
|
518
|
+
: null;
|
|
519
|
+
const isSectionCollapsed = isSection && collapsedSections.has(field.id);
|
|
520
|
+
|
|
521
|
+
const isDimmed =
|
|
522
|
+
searchQuery &&
|
|
523
|
+
!(
|
|
524
|
+
field.label?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
525
|
+
field.type?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
526
|
+
field.id?.toLowerCase().includes(searchQuery.toLowerCase())
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
return (
|
|
530
|
+
<div
|
|
531
|
+
ref={setNodeRef}
|
|
532
|
+
id={`fb-field-${field.id}`}
|
|
533
|
+
className={`${styles.cRow} ${isSection ? styles.cRowSection : ''} ${
|
|
534
|
+
isDragging ? styles.cRowDragging : ''
|
|
535
|
+
}`}
|
|
536
|
+
style={{
|
|
537
|
+
transform: CSS.Transform.toString(transform),
|
|
538
|
+
transition: isDragging ? 'none' : transition,
|
|
539
|
+
opacity: isDimmed ? 0.3 : 1,
|
|
540
|
+
}}
|
|
541
|
+
>
|
|
542
|
+
<span
|
|
543
|
+
{...attributes}
|
|
544
|
+
{...listeners}
|
|
545
|
+
className={styles.cGrip}
|
|
546
|
+
title="Drag to reorder"
|
|
547
|
+
>
|
|
548
|
+
<ChevronVertical size={15} strokeWidth={2.5} />
|
|
549
|
+
</span>
|
|
550
|
+
|
|
551
|
+
{isSection ? (
|
|
552
|
+
<button
|
|
553
|
+
className={styles.cCollapse}
|
|
554
|
+
onClick={() => toggleSectionCollapse(field.id)}
|
|
555
|
+
title={isSectionCollapsed ? 'Expand' : 'Collapse'}
|
|
556
|
+
>
|
|
557
|
+
{isSectionCollapsed ? (
|
|
558
|
+
<ChevronRight size={15} />
|
|
559
|
+
) : (
|
|
560
|
+
<ChevronDown size={15} />
|
|
561
|
+
)}
|
|
562
|
+
</button>
|
|
563
|
+
) : (
|
|
564
|
+
<span className={styles.cNum}>{counter + 1}</span>
|
|
565
|
+
)}
|
|
566
|
+
|
|
567
|
+
<span className={styles.cLabel} title={field.label}>
|
|
568
|
+
{field.label || <em>Untitled</em>}
|
|
569
|
+
</span>
|
|
570
|
+
|
|
571
|
+
<span className={styles.cMeta}>
|
|
572
|
+
{isSection
|
|
573
|
+
? `${group ? group.fields.length : 0} fields`
|
|
574
|
+
: getFieldInfo(field)}
|
|
575
|
+
</span>
|
|
576
|
+
|
|
577
|
+
<span className={styles.cActions}>
|
|
578
|
+
<button
|
|
579
|
+
title={
|
|
580
|
+
isSection
|
|
581
|
+
? 'Duplicate section and its fields'
|
|
582
|
+
: 'Duplicate field'
|
|
583
|
+
}
|
|
584
|
+
onClick={() =>
|
|
585
|
+
isSection
|
|
586
|
+
? handleDuplicateSection(field.id)
|
|
587
|
+
: handleDuplicateField(field.id)
|
|
588
|
+
}
|
|
589
|
+
>
|
|
590
|
+
<Copy size={15} />
|
|
591
|
+
</button>
|
|
592
|
+
<button
|
|
593
|
+
title="Move to position"
|
|
594
|
+
onClick={() => {
|
|
595
|
+
setMoveToDialog({
|
|
596
|
+
show: true,
|
|
597
|
+
fieldId: field.id,
|
|
598
|
+
currentPos: counter + 1,
|
|
599
|
+
});
|
|
600
|
+
setMoveToPosition(String(counter + 1));
|
|
601
|
+
}}
|
|
602
|
+
>
|
|
603
|
+
<ArrowUpDown size={15} />
|
|
604
|
+
</button>
|
|
605
|
+
<button title="Edit" onClick={() => handleEdit(counter)}>
|
|
606
|
+
<Pencil size={15} />
|
|
607
|
+
</button>
|
|
608
|
+
<button
|
|
609
|
+
className={styles.cDelete}
|
|
610
|
+
title="Delete"
|
|
611
|
+
onClick={() => handleDeleteField(field.id, field.label)}
|
|
612
|
+
>
|
|
613
|
+
<TrashCan size={15} />
|
|
614
|
+
</button>
|
|
615
|
+
</span>
|
|
616
|
+
</div>
|
|
617
|
+
);
|
|
618
|
+
};
|
|
619
|
+
|
|
276
620
|
const SortableFieldItem = ({ field, index: counter }) => {
|
|
277
621
|
const {
|
|
278
622
|
attributes,
|
|
@@ -413,6 +757,27 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
413
757
|
)}
|
|
414
758
|
</button>
|
|
415
759
|
)}
|
|
760
|
+
<button
|
|
761
|
+
className={styles.duplicateBtn}
|
|
762
|
+
data-tooltip-id="action-tooltip"
|
|
763
|
+
data-tooltip-content={
|
|
764
|
+
isSection
|
|
765
|
+
? 'Duplicate section and its fields'
|
|
766
|
+
: 'Duplicate field'
|
|
767
|
+
}
|
|
768
|
+
data-tooltip-place="top"
|
|
769
|
+
onClick={(e) => {
|
|
770
|
+
e.preventDefault();
|
|
771
|
+
e.stopPropagation();
|
|
772
|
+
if (isSection) {
|
|
773
|
+
handleDuplicateSection(field.id);
|
|
774
|
+
} else {
|
|
775
|
+
handleDuplicateField(field.id);
|
|
776
|
+
}
|
|
777
|
+
}}
|
|
778
|
+
>
|
|
779
|
+
<Copy strokeWidth={2.5} size={17} />
|
|
780
|
+
</button>
|
|
416
781
|
<ArrowUpDown
|
|
417
782
|
className={styles.moveIcon}
|
|
418
783
|
strokeWidth={3}
|
|
@@ -464,7 +829,7 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
464
829
|
/>
|
|
465
830
|
</div>
|
|
466
831
|
</div>
|
|
467
|
-
<div className={styles.fieldContent}>
|
|
832
|
+
<div className={styles.fieldContent} id={`fb-field-${field.id}`}>
|
|
468
833
|
{renderField(field)}
|
|
469
834
|
</div>
|
|
470
835
|
</div>
|
|
@@ -1348,16 +1713,23 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
1348
1713
|
<div className={styles.grid}>
|
|
1349
1714
|
<div className={styles.grid__row}>
|
|
1350
1715
|
<div className={styles.grid__fullwidth}>
|
|
1351
|
-
<div
|
|
1352
|
-
|
|
1716
|
+
<div
|
|
1717
|
+
className={styles.formSplit}
|
|
1718
|
+
>
|
|
1719
|
+
<div className={styles.stickyHead}>
|
|
1720
|
+
<div
|
|
1721
|
+
className={`${styles.gridtxt__header} ${styles.builderBar}`}
|
|
1722
|
+
>
|
|
1353
1723
|
<span>
|
|
1354
1724
|
{formTitle
|
|
1355
1725
|
? formTitle
|
|
1356
1726
|
: 'Form Template Preview'}
|
|
1357
1727
|
</span>
|
|
1358
|
-
|
|
1728
|
+
<div className={styles.builderTools}>
|
|
1359
1729
|
{data.detail && data.detail.length > 5 && (
|
|
1360
|
-
<div
|
|
1730
|
+
<div
|
|
1731
|
+
className={`${styles.searchBar} ${styles.searchBarTop}`}
|
|
1732
|
+
>
|
|
1361
1733
|
<Search
|
|
1362
1734
|
size={16}
|
|
1363
1735
|
className={styles.searchIcon}
|
|
@@ -1406,6 +1778,108 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
1406
1778
|
)}
|
|
1407
1779
|
</div>
|
|
1408
1780
|
)}
|
|
1781
|
+
<span className={styles.fieldCount}>
|
|
1782
|
+
{(data.detail || []).length} fields ·{' '}
|
|
1783
|
+
{sectionGroups.filter(
|
|
1784
|
+
(group) => group.index >= 0
|
|
1785
|
+
).length}{' '}
|
|
1786
|
+
sections
|
|
1787
|
+
</span>
|
|
1788
|
+
<button
|
|
1789
|
+
className={`${styles.toolBtn} ${
|
|
1790
|
+
showOutline ? styles.toolBtnOn : ''
|
|
1791
|
+
}`}
|
|
1792
|
+
onClick={() =>
|
|
1793
|
+
setShowOutline((open) => !open)
|
|
1794
|
+
}
|
|
1795
|
+
data-tooltip-id="action-tooltip"
|
|
1796
|
+
data-tooltip-content="Toggle the section outline"
|
|
1797
|
+
>
|
|
1798
|
+
<List size={15} /> Sections
|
|
1799
|
+
</button>
|
|
1800
|
+
<div className={styles.modeSwitch}>
|
|
1801
|
+
<button
|
|
1802
|
+
className={
|
|
1803
|
+
compactMode
|
|
1804
|
+
? styles.modeOn
|
|
1805
|
+
: undefined
|
|
1806
|
+
}
|
|
1807
|
+
onClick={() => setCompactMode(true)}
|
|
1808
|
+
>
|
|
1809
|
+
Edit
|
|
1810
|
+
</button>
|
|
1811
|
+
<button
|
|
1812
|
+
className={
|
|
1813
|
+
compactMode
|
|
1814
|
+
? undefined
|
|
1815
|
+
: styles.modeOn
|
|
1816
|
+
}
|
|
1817
|
+
onClick={() =>
|
|
1818
|
+
setCompactMode(false)
|
|
1819
|
+
}
|
|
1820
|
+
>
|
|
1821
|
+
Preview
|
|
1822
|
+
</button>
|
|
1823
|
+
</div>
|
|
1824
|
+
</div>
|
|
1825
|
+
</div>
|
|
1826
|
+
{showOutline && sectionGroups.length > 0 && (
|
|
1827
|
+
<nav className={styles.sectionNav}>
|
|
1828
|
+
{sectionGroups.map((group) => {
|
|
1829
|
+
const collapsed = collapsedSections.has(
|
|
1830
|
+
group.id
|
|
1831
|
+
);
|
|
1832
|
+
|
|
1833
|
+
return (
|
|
1834
|
+
<button
|
|
1835
|
+
key={group.id}
|
|
1836
|
+
className={`${styles.sectionChip} ${
|
|
1837
|
+
activeSection === group.id
|
|
1838
|
+
? styles.sectionChipActive
|
|
1839
|
+
: ''
|
|
1840
|
+
} ${
|
|
1841
|
+
collapsed
|
|
1842
|
+
? styles.sectionChipCollapsed
|
|
1843
|
+
: ''
|
|
1844
|
+
}`}
|
|
1845
|
+
title={
|
|
1846
|
+
collapsed
|
|
1847
|
+
? `${group.label} (collapsed)`
|
|
1848
|
+
: group.label
|
|
1849
|
+
}
|
|
1850
|
+
onClick={() => {
|
|
1851
|
+
// Jumping into a collapsed
|
|
1852
|
+
// section opens it first,
|
|
1853
|
+
// otherwise the scroll
|
|
1854
|
+
// lands on nothing.
|
|
1855
|
+
if (collapsed) {
|
|
1856
|
+
toggleSectionCollapse(
|
|
1857
|
+
group.id
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
scrollToField(group.id);
|
|
1861
|
+
}}
|
|
1862
|
+
>
|
|
1863
|
+
<span
|
|
1864
|
+
className={
|
|
1865
|
+
styles.sectionChip__label
|
|
1866
|
+
}
|
|
1867
|
+
>
|
|
1868
|
+
{group.label}
|
|
1869
|
+
</span>
|
|
1870
|
+
<span
|
|
1871
|
+
className={
|
|
1872
|
+
styles.sectionChip__count
|
|
1873
|
+
}
|
|
1874
|
+
>
|
|
1875
|
+
{group.fields.length}
|
|
1876
|
+
</span>
|
|
1877
|
+
</button>
|
|
1878
|
+
);
|
|
1879
|
+
})}
|
|
1880
|
+
</nav>
|
|
1881
|
+
)}
|
|
1882
|
+
</div>
|
|
1409
1883
|
{data.detail && data.detail.length > 0 ? (
|
|
1410
1884
|
<div className={styles.modal__content}>
|
|
1411
1885
|
<div className={styles.formcontainer}>
|
|
@@ -1431,16 +1905,30 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
1431
1905
|
strategy={rectSortingStrategy}
|
|
1432
1906
|
>
|
|
1433
1907
|
<div
|
|
1434
|
-
className={
|
|
1908
|
+
className={`${
|
|
1909
|
+
compactMode
|
|
1910
|
+
? styles.cList
|
|
1911
|
+
: styles.modalForm
|
|
1912
|
+
} ${
|
|
1913
|
+
compactMode
|
|
1914
|
+
? ''
|
|
1915
|
+
: styles.pvForm
|
|
1916
|
+
}`}
|
|
1435
1917
|
>
|
|
1436
1918
|
{data.detail.map(
|
|
1437
|
-
(field, index) =>
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1919
|
+
(field, index) =>
|
|
1920
|
+
compactMode ? (
|
|
1921
|
+
<CompactFieldRow
|
|
1922
|
+
key={field.id}
|
|
1923
|
+
field={field}
|
|
1924
|
+
index={index}
|
|
1925
|
+
/>
|
|
1926
|
+
) : (
|
|
1927
|
+
<PreviewFieldItem
|
|
1928
|
+
key={field.id}
|
|
1929
|
+
field={field}
|
|
1930
|
+
/>
|
|
1931
|
+
)
|
|
1444
1932
|
)}
|
|
1445
1933
|
</div>
|
|
1446
1934
|
</SortableContext>
|
|
@@ -1481,21 +1969,58 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
1481
1969
|
</div>
|
|
1482
1970
|
</div>
|
|
1483
1971
|
) : (
|
|
1484
|
-
<div
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1972
|
+
<div
|
|
1973
|
+
className={`${styles.emptyState} ${
|
|
1974
|
+
dynamicFields
|
|
1975
|
+
? styles.emptyStateDynamic
|
|
1976
|
+
: ''
|
|
1977
|
+
}`}
|
|
1978
|
+
>
|
|
1979
|
+
<span className={styles.emptyState__icon}>
|
|
1980
|
+
<List size={26} strokeWidth={1.75} />
|
|
1981
|
+
</span>
|
|
1982
|
+
{dynamicFields ? (
|
|
1983
|
+
<>
|
|
1984
|
+
<h3>Fields are added automatically</h3>
|
|
1985
|
+
<p>
|
|
1986
|
+
This form builds its own fields
|
|
1987
|
+
each time it is created, from
|
|
1988
|
+
the outstanding items on the
|
|
1989
|
+
linked inspection. Anything
|
|
1990
|
+
added here would not appear on
|
|
1991
|
+
the form.
|
|
1992
|
+
</p>
|
|
1993
|
+
</>
|
|
1994
|
+
) : (
|
|
1995
|
+
<>
|
|
1996
|
+
<h3>This form has no fields yet</h3>
|
|
1997
|
+
<p>
|
|
1998
|
+
Add a section to group related
|
|
1999
|
+
questions, then add the fields
|
|
2000
|
+
that go inside it.
|
|
2001
|
+
</p>
|
|
2002
|
+
<button
|
|
2003
|
+
className={styles.btn}
|
|
2004
|
+
onClick={handleOpenModal}
|
|
2005
|
+
>
|
|
2006
|
+
Add the first field
|
|
2007
|
+
</button>
|
|
2008
|
+
</>
|
|
2009
|
+
)}
|
|
1489
2010
|
</div>
|
|
1490
2011
|
)}
|
|
1491
2012
|
</div>
|
|
1492
2013
|
<div className={styles.polActions}>
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
2014
|
+
{/* Nothing to author on a dynamic template, but
|
|
2015
|
+
Edit Form stays so it can still be renamed. */}
|
|
2016
|
+
{!dynamicFields && (
|
|
2017
|
+
<button
|
|
2018
|
+
className={styles.btn}
|
|
2019
|
+
onClick={handleOpenModal}
|
|
2020
|
+
>
|
|
2021
|
+
Add Field
|
|
2022
|
+
</button>
|
|
2023
|
+
)}
|
|
1499
2024
|
<button
|
|
1500
2025
|
className={styles.btn}
|
|
1501
2026
|
onClick={handleOpenModalForm}
|