@visns-studio/visns-components 6.2.0 → 6.3.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/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.
|
|
94
|
+
"version": "6.3.0",
|
|
95
95
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
96
96
|
"main": "src/index.js",
|
|
97
97
|
"files": [
|
|
@@ -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';
|
|
@@ -166,6 +168,24 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
166
168
|
* tab itself has already replaced.
|
|
167
169
|
*/
|
|
168
170
|
const expectedDetailHashRef = useRef(null);
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The `{label, detail}` the server is believed to hold, as a comparable
|
|
174
|
+
* string.
|
|
175
|
+
*
|
|
176
|
+
* Every authoring action on this screen — adding a question, rewording an
|
|
177
|
+
* option, changing a size — only moves React state. Nothing reaches the
|
|
178
|
+
* server until the template itself is saved, and until this ref existed
|
|
179
|
+
* there was no way to tell "on screen" from "stored": a tab could be closed
|
|
180
|
+
* on an afternoon's work and say nothing about it.
|
|
181
|
+
*
|
|
182
|
+
* Null until the first load lands. Before that there is no baseline, and
|
|
183
|
+
* everything would compare as changed.
|
|
184
|
+
*/
|
|
185
|
+
const savedSnapshotRef = useRef(null);
|
|
186
|
+
const [unsavedChanges, setUnsavedChanges] = useState(false);
|
|
187
|
+
/** Bumped whenever the baseline moves, so the comparison below re-runs. */
|
|
188
|
+
const [savedBaselineTick, setSavedBaselineTick] = useState(0);
|
|
169
189
|
const lockEnabled = Boolean(optimisticLock);
|
|
170
190
|
const lockSchemaVersion =
|
|
171
191
|
optimisticLock?.schemaVersion ?? DEFAULT_LOCK_SCHEMA_VERSION;
|
|
@@ -1395,6 +1415,20 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
1395
1415
|
(!field.options || field.options.length === 0)
|
|
1396
1416
|
) {
|
|
1397
1417
|
errors.push('Please add options for the dropdown.');
|
|
1418
|
+
} else if (
|
|
1419
|
+
/**
|
|
1420
|
+
* A checkbox with no options renders as a bare label with nothing
|
|
1421
|
+
* to tick. Harmless-looking in the builder, and fatal in the field:
|
|
1422
|
+
* marked required, it cannot be answered, so the form it belongs to
|
|
1423
|
+
* can never be submitted. Dropdowns have always been held to this;
|
|
1424
|
+
* checkboxes were simply missed.
|
|
1425
|
+
*/
|
|
1426
|
+
field.type === 'checkbox' &&
|
|
1427
|
+
(!field.options || field.options.length === 0)
|
|
1428
|
+
) {
|
|
1429
|
+
errors.push(
|
|
1430
|
+
'Please add at least one option for the checkbox — a checkbox with no options cannot be answered.'
|
|
1431
|
+
);
|
|
1398
1432
|
} else if (
|
|
1399
1433
|
field.type === 'table_text' &&
|
|
1400
1434
|
(!field.options || field.options.length === 0)
|
|
@@ -1812,6 +1846,34 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
1812
1846
|
);
|
|
1813
1847
|
};
|
|
1814
1848
|
|
|
1849
|
+
/** The comparable form of the two things this screen actually stores. */
|
|
1850
|
+
const snapshotOf = (label, detail) =>
|
|
1851
|
+
JSON.stringify({ label: label ?? '', detail: detail ?? [] });
|
|
1852
|
+
|
|
1853
|
+
/**
|
|
1854
|
+
* Re-anchor the unsaved-changes baseline on what the server now holds.
|
|
1855
|
+
*
|
|
1856
|
+
* Takes a partial: the sort endpoint writes `detail` alone and leaves the
|
|
1857
|
+
* label as it was, so rebuilding the whole baseline from the current
|
|
1858
|
+
* on-screen `data` would quietly adopt an unsaved rename as "stored".
|
|
1859
|
+
*/
|
|
1860
|
+
const markSaved = ({ label, detail } = {}) => {
|
|
1861
|
+
const previous = savedSnapshotRef.current
|
|
1862
|
+
? JSON.parse(savedSnapshotRef.current)
|
|
1863
|
+
: {};
|
|
1864
|
+
|
|
1865
|
+
savedSnapshotRef.current = snapshotOf(
|
|
1866
|
+
label === undefined ? previous.label : label,
|
|
1867
|
+
detail === undefined ? previous.detail : detail
|
|
1868
|
+
);
|
|
1869
|
+
|
|
1870
|
+
// Never `setUnsavedChanges(false)` directly: a partial save can leave
|
|
1871
|
+
// something else still unsaved. Re-ordering writes `detail` and not the
|
|
1872
|
+
// label, so declaring the screen clean here would lose a pending
|
|
1873
|
+
// rename. Let the one comparison decide.
|
|
1874
|
+
setSavedBaselineTick((tick) => tick + 1);
|
|
1875
|
+
};
|
|
1876
|
+
|
|
1815
1877
|
/**
|
|
1816
1878
|
* "Somebody else got here first", in the only words that help.
|
|
1817
1879
|
*
|
|
@@ -1875,6 +1937,10 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
1875
1937
|
|
|
1876
1938
|
if (res.data.error === '') {
|
|
1877
1939
|
await rememberDetailHash(data.id, savedDetail);
|
|
1940
|
+
markSaved({
|
|
1941
|
+
label: data.label,
|
|
1942
|
+
detail: savedDetail,
|
|
1943
|
+
});
|
|
1878
1944
|
|
|
1879
1945
|
toast.success(
|
|
1880
1946
|
"You have successfully updated the form's detail."
|
|
@@ -1910,7 +1976,31 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
1910
1976
|
return;
|
|
1911
1977
|
}
|
|
1912
1978
|
|
|
1979
|
+
/**
|
|
1980
|
+
* Suppressing the shared handler's toast made this branch the
|
|
1981
|
+
* only voice the user has, so it must never be silent. It used
|
|
1982
|
+
* to `return` here, which turned an expired session (401
|
|
1983
|
+
* `Unauthenticated.`) — or any error body carrying neither
|
|
1984
|
+
* `errors` nor `message` — into a save that looked like it had
|
|
1985
|
+
* worked: nothing stored, and nothing said about it.
|
|
1986
|
+
*/
|
|
1913
1987
|
if (err?.response) {
|
|
1988
|
+
const status = err.response.status;
|
|
1989
|
+
const expired =
|
|
1990
|
+
status === 401 || message === 'Unauthenticated.';
|
|
1991
|
+
|
|
1992
|
+
toast.error(
|
|
1993
|
+
<div>
|
|
1994
|
+
<strong>Nothing was saved.</strong>{' '}
|
|
1995
|
+
{expired
|
|
1996
|
+
? 'Your session has expired. Sign in again in another tab, then come back and save — your changes are still on screen.'
|
|
1997
|
+
: `The server refused the save${
|
|
1998
|
+
status ? ` (error ${status})` : ''
|
|
1999
|
+
}. Your changes are still on screen — try again, and report this if it keeps happening.`}
|
|
2000
|
+
</div>,
|
|
2001
|
+
{ autoClose: false }
|
|
2002
|
+
);
|
|
2003
|
+
|
|
1914
2004
|
return;
|
|
1915
2005
|
}
|
|
1916
2006
|
}
|
|
@@ -1932,6 +2022,10 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
1932
2022
|
// The version this tab is editing from. Everything the guard does
|
|
1933
2023
|
// is measured against this moment.
|
|
1934
2024
|
await rememberDetailHash(res.data?.id, res.data?.detail);
|
|
2025
|
+
markSaved({
|
|
2026
|
+
label: res.data?.label,
|
|
2027
|
+
detail: res.data?.detail,
|
|
2028
|
+
});
|
|
1935
2029
|
} catch (err) {
|
|
1936
2030
|
toast.error(`Error: ${err}`);
|
|
1937
2031
|
}
|
|
@@ -1959,6 +2053,10 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
1959
2053
|
// here — but leaving the hash behind would make the next Save
|
|
1960
2054
|
// report a collision this tab caused itself.
|
|
1961
2055
|
await rememberDetailHash(data.id, savedDetail);
|
|
2056
|
+
|
|
2057
|
+
// Re-ordering is the one authoring action that reaches the server
|
|
2058
|
+
// on its own, so the unsaved-changes baseline has to follow it too.
|
|
2059
|
+
markSaved({ detail: savedDetail });
|
|
1962
2060
|
} catch (err) {
|
|
1963
2061
|
toast.error(`Error: ${err}`);
|
|
1964
2062
|
}
|
|
@@ -1990,6 +2088,297 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
1990
2088
|
fetchRoles();
|
|
1991
2089
|
}, []);
|
|
1992
2090
|
|
|
2091
|
+
/**
|
|
2092
|
+
* Is what is on screen different from what is stored?
|
|
2093
|
+
*
|
|
2094
|
+
* Compared against the baseline rather than tracked per-action, so it
|
|
2095
|
+
* cannot drift: undoing a change by hand correctly reports "all changes
|
|
2096
|
+
* saved" again, and no authoring path can forget to raise the flag.
|
|
2097
|
+
*/
|
|
2098
|
+
useEffect(() => {
|
|
2099
|
+
if (savedSnapshotRef.current === null) {
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
setUnsavedChanges(
|
|
2104
|
+
snapshotOf(data.label, data.detail) !== savedSnapshotRef.current
|
|
2105
|
+
);
|
|
2106
|
+
}, [data, savedBaselineTick]);
|
|
2107
|
+
|
|
2108
|
+
/**
|
|
2109
|
+
* The browser's own "leave site?" prompt, and only while there is something
|
|
2110
|
+
* to lose. Last line of defence for the failure this screen is prone to: a
|
|
2111
|
+
* question window's button reads as final, so a template can be closed in
|
|
2112
|
+
* the belief that its questions were filed.
|
|
2113
|
+
*/
|
|
2114
|
+
useEffect(() => {
|
|
2115
|
+
if (!unsavedChanges) {
|
|
2116
|
+
return undefined;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
const warnBeforeLeaving = (event) => {
|
|
2120
|
+
event.preventDefault();
|
|
2121
|
+
event.returnValue = '';
|
|
2122
|
+
|
|
2123
|
+
return '';
|
|
2124
|
+
};
|
|
2125
|
+
|
|
2126
|
+
window.addEventListener('beforeunload', warnBeforeLeaving);
|
|
2127
|
+
|
|
2128
|
+
return () =>
|
|
2129
|
+
window.removeEventListener('beforeunload', warnBeforeLeaving);
|
|
2130
|
+
}, [unsavedChanges]);
|
|
2131
|
+
|
|
2132
|
+
/* ---------------------------------------------------------------- */
|
|
2133
|
+
/* Guided help */
|
|
2134
|
+
/* ---------------------------------------------------------------- */
|
|
2135
|
+
|
|
2136
|
+
/**
|
|
2137
|
+
* Inline SVG for the guide's section headings. SweetAlert2 takes an HTML
|
|
2138
|
+
* string, so the lucide components used elsewhere in this file cannot be
|
|
2139
|
+
* rendered here — these mirror the same icons at the same weight.
|
|
2140
|
+
*/
|
|
2141
|
+
const helpIcon = (name, color) => {
|
|
2142
|
+
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;">`;
|
|
2143
|
+
const paths = {
|
|
2144
|
+
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"/>',
|
|
2145
|
+
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"/>',
|
|
2146
|
+
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"/>',
|
|
2147
|
+
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"/>',
|
|
2148
|
+
settings:
|
|
2149
|
+
'<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"/>',
|
|
2150
|
+
table: '<path d="M12 3v18"/><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/>',
|
|
2151
|
+
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"/>',
|
|
2152
|
+
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"/>',
|
|
2153
|
+
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"/>',
|
|
2154
|
+
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"/>',
|
|
2155
|
+
};
|
|
2156
|
+
return `${open}${paths[name] || paths.help}</svg>`;
|
|
2157
|
+
};
|
|
2158
|
+
|
|
2159
|
+
/** One heading style for every section of the guide. */
|
|
2160
|
+
const helpHeading = (icon, color, title) =>
|
|
2161
|
+
`<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(
|
|
2162
|
+
icon,
|
|
2163
|
+
color
|
|
2164
|
+
)}${title}</h3>`;
|
|
2165
|
+
|
|
2166
|
+
/** A tinted panel with a bulleted body, optionally titled. */
|
|
2167
|
+
const helpPanel = ({ bg, border, text, title, items }) => `
|
|
2168
|
+
<div style="background: ${bg}; border: 1px solid ${border}; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
|
|
2169
|
+
${
|
|
2170
|
+
title
|
|
2171
|
+
? `<h4 style="color: ${border}; margin: 0 0 8px 0; font-size: 14px;">${title}</h4>`
|
|
2172
|
+
: ''
|
|
2173
|
+
}
|
|
2174
|
+
<ul style="margin: 0; padding-left: 16px; color: ${text};">${items
|
|
2175
|
+
.map((item) => `<li>${item}</li>`)
|
|
2176
|
+
.join('')}</ul>
|
|
2177
|
+
</div>`;
|
|
2178
|
+
|
|
2179
|
+
/**
|
|
2180
|
+
* The end-user guide to building a template. Sections that describe an
|
|
2181
|
+
* opt-in feature are only included when this app actually switched that
|
|
2182
|
+
* feature on, so nobody is told about a control they will never see.
|
|
2183
|
+
*/
|
|
2184
|
+
const showGuidedHelp = () => {
|
|
2185
|
+
const sections = [
|
|
2186
|
+
helpHeading('rocket', '#3b82f6', 'What this screen is') +
|
|
2187
|
+
helpPanel({
|
|
2188
|
+
bg: '#f0f9ff',
|
|
2189
|
+
border: '#3b82f6',
|
|
2190
|
+
text: '#1e3a8a',
|
|
2191
|
+
items: [
|
|
2192
|
+
'This is the list of questions people will answer — for example a client review checklist',
|
|
2193
|
+
'<strong>Edit</strong> shows one compact row per question, with a summary of how it is set up',
|
|
2194
|
+
'<strong>Preview</strong> shows the form the way the person filling it in will see it',
|
|
2195
|
+
'<strong>Sections</strong> shows a strip of shortcuts, one per section; click one to jump to it',
|
|
2196
|
+
'Once there are more than five questions, a search box appears — matching questions stay bright and the rest dim',
|
|
2197
|
+
],
|
|
2198
|
+
}),
|
|
2199
|
+
|
|
2200
|
+
helpHeading('list', '#10b981', 'Adding and arranging questions') +
|
|
2201
|
+
helpPanel({
|
|
2202
|
+
bg: '#ecfdf5',
|
|
2203
|
+
border: '#10b981',
|
|
2204
|
+
text: '#065f46',
|
|
2205
|
+
items: dynamicFields
|
|
2206
|
+
? [
|
|
2207
|
+
'This template builds its own questions automatically, so there is no "Add Field" button',
|
|
2208
|
+
'Use the pencil to adjust a question, and the grip handle on the left to drag it into a new position',
|
|
2209
|
+
]
|
|
2210
|
+
: [
|
|
2211
|
+
'<strong>Add Field</strong> (bottom right) opens the question window',
|
|
2212
|
+
'The pencil edits a question, the copy icon duplicates it, the bin deletes it',
|
|
2213
|
+
'Duplicating a Section copies the section and every question inside it',
|
|
2214
|
+
'Drag the grip handle on the left to move a question; the up/down arrows let you type an exact position instead',
|
|
2215
|
+
'Deleting a question also clears any "show only when" rule that pointed at it, and tells you how many were cleared',
|
|
2216
|
+
],
|
|
2217
|
+
}),
|
|
2218
|
+
|
|
2219
|
+
helpHeading('layers', '#7c3aed', 'Sections and headings') +
|
|
2220
|
+
helpPanel({
|
|
2221
|
+
bg: '#f5f3ff',
|
|
2222
|
+
border: '#7c3aed',
|
|
2223
|
+
text: '#5b21b6',
|
|
2224
|
+
items: [
|
|
2225
|
+
'Add a question of type <strong>Section</strong> to start a new part of the form',
|
|
2226
|
+
'Everything after a Section belongs to it, until the next Section starts',
|
|
2227
|
+
'Questions before the first Section are listed under "Before first section"',
|
|
2228
|
+
'Click the arrow on a Section row to collapse or expand it while you work',
|
|
2229
|
+
'<strong>Heading</strong> is simpler — just a title on the page, with no grouping',
|
|
2230
|
+
'<strong>Plain Text</strong> is a block of formatted wording, for instructions or a declaration',
|
|
2231
|
+
],
|
|
2232
|
+
}),
|
|
2233
|
+
|
|
2234
|
+
helpHeading('type', '#0891b2', 'The kinds of question you can add') +
|
|
2235
|
+
helpPanel({
|
|
2236
|
+
bg: '#ecfeff',
|
|
2237
|
+
border: '#0891b2',
|
|
2238
|
+
text: '#155e75',
|
|
2239
|
+
items: [
|
|
2240
|
+
'<strong>Typed in:</strong> Text (one line), Textarea (several lines), Number',
|
|
2241
|
+
'<strong>Chosen from a list:</strong> Dropdown, Checkbox, Toggle (a yes/no switch)',
|
|
2242
|
+
'<strong>Dates and times:</strong> Date, Date & Time, Time',
|
|
2243
|
+
'<strong>Attached or drawn:</strong> File, Image, Video Upload, Signature, Canvas',
|
|
2244
|
+
'<strong>Grids:</strong> Table (Radio) and Table (Custom) — see below',
|
|
2245
|
+
'<strong>Layout only:</strong> Section, Heading, Plain Text',
|
|
2246
|
+
`<strong>Filled from your system:</strong> Dynamic Data pulls in a detail the system already holds${
|
|
2247
|
+
dynamicDropdowns && dynamicDropdowns.length > 0
|
|
2248
|
+
? ', and Dynamic Dropdown offers a list your system maintains'
|
|
2249
|
+
: ''
|
|
2250
|
+
}`,
|
|
2251
|
+
],
|
|
2252
|
+
}),
|
|
2253
|
+
|
|
2254
|
+
helpHeading('settings', '#f59e0b', 'Settings on each question') +
|
|
2255
|
+
helpPanel({
|
|
2256
|
+
bg: '#fffbeb',
|
|
2257
|
+
border: '#f59e0b',
|
|
2258
|
+
text: '#92400e',
|
|
2259
|
+
items: [
|
|
2260
|
+
'<strong>Label</strong> is the wording people read — write it as the question you want answered',
|
|
2261
|
+
'<strong>Required?</strong> set to Yes means the form cannot be submitted without an answer; required questions show an asterisk',
|
|
2262
|
+
'<strong>Size</strong> is how wide the question sits on the page: Full for its own row, Half or Quarter to share a row',
|
|
2263
|
+
'<strong>Height Size</strong> on a Textarea chooses a small, normal or large writing box',
|
|
2264
|
+
'<strong>Single or Bulk Image?</strong> decides whether one photo or several can be attached',
|
|
2265
|
+
'<strong>Associated Role?</strong> on a Signature says whose signature it is',
|
|
2266
|
+
'There is no help text, placeholder or preset answer — put any wording people need into the label, or a Plain Text block above',
|
|
2267
|
+
],
|
|
2268
|
+
}),
|
|
2269
|
+
|
|
2270
|
+
helpHeading('list', '#059669', 'Answers for Dropdown and Checkbox') +
|
|
2271
|
+
helpPanel({
|
|
2272
|
+
bg: '#f0fdf4',
|
|
2273
|
+
border: '#059669',
|
|
2274
|
+
text: '#166534',
|
|
2275
|
+
items: [
|
|
2276
|
+
'Type the answer in the box at the bottom of the Options list and press Enter, or click <strong>+ Add</strong>',
|
|
2277
|
+
'Click straight into an existing answer to reword it; the bin removes it',
|
|
2278
|
+
'Two answers cannot share the same wording',
|
|
2279
|
+
'Rewording an answer can unhook rules that were pointing at it — check any "show only when" rule that used it',
|
|
2280
|
+
],
|
|
2281
|
+
}),
|
|
2282
|
+
|
|
2283
|
+
helpHeading('table', '#be185d', 'Grid questions') +
|
|
2284
|
+
helpPanel({
|
|
2285
|
+
bg: '#fdf2f8',
|
|
2286
|
+
border: '#be185d',
|
|
2287
|
+
text: '#9d174d',
|
|
2288
|
+
items: [
|
|
2289
|
+
'<strong>Table (Radio)</strong> needs both Columns and Rows — the rows are the things being checked and the columns are the possible answers',
|
|
2290
|
+
'<strong>Table (Custom)</strong> needs Columns only, and gives one row of boxes to fill in',
|
|
2291
|
+
'Add a column or row by typing it in and pressing Enter, or clicking Add',
|
|
2292
|
+
'The rows are fixed when you build the template — the person filling in the form cannot add more',
|
|
2293
|
+
],
|
|
2294
|
+
}),
|
|
2295
|
+
|
|
2296
|
+
helpHeading(
|
|
2297
|
+
'branch',
|
|
2298
|
+
'#4f46e5',
|
|
2299
|
+
'Showing a question only when it applies'
|
|
2300
|
+
) +
|
|
2301
|
+
helpPanel({
|
|
2302
|
+
bg: '#eef2ff',
|
|
2303
|
+
border: '#4f46e5',
|
|
2304
|
+
text: '#3730a3',
|
|
2305
|
+
items: [
|
|
2306
|
+
'Every question window ends with <strong>Conditional Field Display Criteria</strong>',
|
|
2307
|
+
'Pick the earlier question it depends on, then either <strong>is</strong> with a value, or <strong>is filled in</strong>',
|
|
2308
|
+
'Only questions that come <em>before</em> this one can be used, and only ones that collect an answer',
|
|
2309
|
+
'A line at the bottom spells the rule out, e.g. "This field will only show when Advice type is Insurance"',
|
|
2310
|
+
'Leave it blank to always show the question, or use <strong>Clear rule</strong> to remove one',
|
|
2311
|
+
'Questions with a rule are marked "Conditional" in the list',
|
|
2312
|
+
],
|
|
2313
|
+
}),
|
|
2314
|
+
];
|
|
2315
|
+
|
|
2316
|
+
if (outstandingItems?.enabled) {
|
|
2317
|
+
sections.push(
|
|
2318
|
+
helpHeading('flag', '#d97706', 'Answers that raise a follow-up') +
|
|
2319
|
+
helpPanel({
|
|
2320
|
+
bg: '#fef3c7',
|
|
2321
|
+
border: '#d97706',
|
|
2322
|
+
text: '#92400e',
|
|
2323
|
+
items: [
|
|
2324
|
+
'On questions that offer answers, each answer has an <strong>Outstanding</strong> switch',
|
|
2325
|
+
'Turn it on for the answer that means "this needs following up", and the system raises an outstanding item when someone gives that answer',
|
|
2326
|
+
'Only one answer per question can be marked',
|
|
2327
|
+
'Leave every switch off to say this question never raises a follow-up',
|
|
2328
|
+
'If an amber warning appears, the answer the old rule pointed at no longer exists — set the switch again on the right answer',
|
|
2329
|
+
],
|
|
2330
|
+
})
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
sections.push(
|
|
2335
|
+
helpHeading('save', '#dc2626', 'Saving your work') +
|
|
2336
|
+
helpPanel({
|
|
2337
|
+
bg: '#fef2f2',
|
|
2338
|
+
border: '#dc2626',
|
|
2339
|
+
text: '#991b1b',
|
|
2340
|
+
items: [
|
|
2341
|
+
'<strong>Apply</strong> inside a question window only adds it to the list on screen — nothing is stored yet',
|
|
2342
|
+
'To store the template, click <strong>Save Template</strong> (bottom right)',
|
|
2343
|
+
'The label beside those buttons reads <strong>Unsaved changes</strong> until you do, and the browser warns you if you try to leave',
|
|
2344
|
+
'<strong>Edit Form</strong> is only for renaming the template',
|
|
2345
|
+
'Moving a question saves the whole template straight away, so the order is never lost',
|
|
2346
|
+
'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',
|
|
2347
|
+
],
|
|
2348
|
+
})
|
|
2349
|
+
);
|
|
2350
|
+
|
|
2351
|
+
Swal.fire({
|
|
2352
|
+
title: `<div style="display: flex; align-items: center; justify-content: center; gap: 8px;">${helpIcon(
|
|
2353
|
+
'help',
|
|
2354
|
+
'#3b82f6'
|
|
2355
|
+
)}Form Builder Guide</div>`,
|
|
2356
|
+
html: `<div style="text-align: left; font-size: 13px; line-height: 1.5; color: #374151; max-height: 70vh; overflow-y: auto;">${sections.join(
|
|
2357
|
+
'<div style="height: 12px;"></div>'
|
|
2358
|
+
)}</div>`,
|
|
2359
|
+
width: 800,
|
|
2360
|
+
confirmButtonText: 'Close',
|
|
2361
|
+
confirmButtonColor: '#6b7280',
|
|
2362
|
+
customClass: {
|
|
2363
|
+
popup: 'comprehensive-help-modal',
|
|
2364
|
+
htmlContainer: 'help-content-scrollable',
|
|
2365
|
+
},
|
|
2366
|
+
});
|
|
2367
|
+
};
|
|
2368
|
+
|
|
2369
|
+
// Show the guide once, the first time someone opens the builder.
|
|
2370
|
+
useEffect(() => {
|
|
2371
|
+
if (localStorage.getItem('formBuilder_hasSeenHelp')) return;
|
|
2372
|
+
|
|
2373
|
+
const timer = setTimeout(() => {
|
|
2374
|
+
showGuidedHelp();
|
|
2375
|
+
localStorage.setItem('formBuilder_hasSeenHelp', 'true');
|
|
2376
|
+
}, 500);
|
|
2377
|
+
|
|
2378
|
+
return () => clearTimeout(timer);
|
|
2379
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2380
|
+
}, []);
|
|
2381
|
+
|
|
1993
2382
|
return (
|
|
1994
2383
|
<div className={modalShow || modalFormShow ? styles.modalOpen : ''}>
|
|
1995
2384
|
<div className={styles.grid}>
|
|
@@ -2081,6 +2470,15 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
2081
2470
|
).length}{' '}
|
|
2082
2471
|
sections
|
|
2083
2472
|
</span>
|
|
2473
|
+
<button
|
|
2474
|
+
type="button"
|
|
2475
|
+
className={styles.toolBtn}
|
|
2476
|
+
onClick={showGuidedHelp}
|
|
2477
|
+
data-tooltip-id="action-tooltip"
|
|
2478
|
+
data-tooltip-content="How to build this form"
|
|
2479
|
+
>
|
|
2480
|
+
<HelpCircle size={15} /> Help
|
|
2481
|
+
</button>
|
|
2084
2482
|
<button
|
|
2085
2483
|
className={`${styles.toolBtn} ${
|
|
2086
2484
|
showOutline ? styles.toolBtnOn : ''
|
|
@@ -2307,6 +2705,20 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
2307
2705
|
)}
|
|
2308
2706
|
</div>
|
|
2309
2707
|
<div className={styles.polActions}>
|
|
2708
|
+
{/* The only honest answer to "have my changes been
|
|
2709
|
+
stored?". Everything else on this screen edits a
|
|
2710
|
+
copy held in the browser. */}
|
|
2711
|
+
<span
|
|
2712
|
+
className={`${styles.saveState} ${
|
|
2713
|
+
unsavedChanges
|
|
2714
|
+
? styles.saveStateDirty
|
|
2715
|
+
: styles.saveStateClean
|
|
2716
|
+
}`}
|
|
2717
|
+
>
|
|
2718
|
+
{unsavedChanges
|
|
2719
|
+
? 'Unsaved changes'
|
|
2720
|
+
: 'All changes saved'}
|
|
2721
|
+
</span>
|
|
2310
2722
|
{/* Nothing to author on a dynamic template, but
|
|
2311
2723
|
Edit Form stays so it can still be renamed. */}
|
|
2312
2724
|
{!dynamicFields && (
|
|
@@ -2323,6 +2735,19 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
2323
2735
|
>
|
|
2324
2736
|
Edit Form
|
|
2325
2737
|
</button>
|
|
2738
|
+
{/* Was reachable only through Edit Form — a window
|
|
2739
|
+
titled "Update Form Detail" showing nothing but
|
|
2740
|
+
the label, so the action that stores the
|
|
2741
|
+
questions looked like the one that renames the
|
|
2742
|
+
form. It belongs out here, beside the work. */}
|
|
2743
|
+
<button
|
|
2744
|
+
className={`${styles.btn} ${styles.btnPrimary}`}
|
|
2745
|
+
onClick={handleSubmit}
|
|
2746
|
+
data-tooltip-id="system-tooltip"
|
|
2747
|
+
data-tooltip-content="Store this template. Question windows only stage changes on screen."
|
|
2748
|
+
>
|
|
2749
|
+
Save Template
|
|
2750
|
+
</button>
|
|
2326
2751
|
</div>
|
|
2327
2752
|
</div>
|
|
2328
2753
|
</div>
|
|
@@ -3140,11 +3565,23 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
3140
3565
|
<div
|
|
3141
3566
|
className={`${styles.formItem} ${styles.fwItem} ${styles.lastItem}`}
|
|
3142
3567
|
>
|
|
3568
|
+
{/* This window has never stored
|
|
3569
|
+
anything — it edits the copy held in
|
|
3570
|
+
the browser. Calling its button
|
|
3571
|
+
"Save" was the whole reason people
|
|
3572
|
+
left the screen believing their work
|
|
3573
|
+
had been filed. */}
|
|
3574
|
+
<p className={styles.applyHint}>
|
|
3575
|
+
Adds this question to the list on
|
|
3576
|
+
screen. Use{' '}
|
|
3577
|
+
<strong>Save Template</strong> to
|
|
3578
|
+
store it.
|
|
3579
|
+
</p>
|
|
3143
3580
|
<button
|
|
3144
3581
|
className={`${styles.btn}`}
|
|
3145
3582
|
onClick={handleSaveField}
|
|
3146
3583
|
>
|
|
3147
|
-
|
|
3584
|
+
Apply
|
|
3148
3585
|
</button>
|
|
3149
3586
|
</div>
|
|
3150
3587
|
</form>
|
|
@@ -1752,224 +1752,56 @@ const GenericReportImproved = ({
|
|
|
1752
1752
|
return icons[iconName] || icons.chart;
|
|
1753
1753
|
};
|
|
1754
1754
|
|
|
1755
|
-
|
|
1756
|
-
|
|
1755
|
+
/* ---------------------------------------------------------------- */
|
|
1756
|
+
/* Guided help */
|
|
1757
|
+
/* ---------------------------------------------------------------- */
|
|
1758
|
+
|
|
1759
|
+
/**
|
|
1760
|
+
* One heading style for both guides, so the legacy and semantic help read
|
|
1761
|
+
* as the same help system. `icon` is a key of `getIcon`.
|
|
1762
|
+
*/
|
|
1763
|
+
const helpHeading = (icon, color, title) =>
|
|
1764
|
+
`<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;">${getIcon(
|
|
1765
|
+
icon,
|
|
1766
|
+
color,
|
|
1767
|
+
16
|
|
1768
|
+
)}${title}</h3>`;
|
|
1769
|
+
|
|
1770
|
+
/**
|
|
1771
|
+
* A tinted panel with a bulleted body. Passing `items` as a string renders
|
|
1772
|
+
* it as a paragraph instead, for the short explanatory panels.
|
|
1773
|
+
*/
|
|
1774
|
+
const helpPanel = ({ bg, border, text, title, icon, items }) => {
|
|
1775
|
+
const body = Array.isArray(items)
|
|
1776
|
+
? `<ul style="margin: 0; padding-left: 16px; color: ${text};">${items
|
|
1777
|
+
.map((item) => `<li>${item}</li>`)
|
|
1778
|
+
.join('')}</ul>`
|
|
1779
|
+
: `<p style="margin: 0; color: ${text};">${items}</p>`;
|
|
1780
|
+
|
|
1781
|
+
return `
|
|
1782
|
+
<div style="background: ${bg}; border: 1px solid ${border}; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
|
|
1783
|
+
${
|
|
1784
|
+
title
|
|
1785
|
+
? `<h4 style="color: ${border}; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">${
|
|
1786
|
+
icon ? getIcon(icon, border, 14) : ''
|
|
1787
|
+
}${title}</h4>`
|
|
1788
|
+
: ''
|
|
1789
|
+
}
|
|
1790
|
+
${body}
|
|
1791
|
+
</div>`;
|
|
1792
|
+
};
|
|
1793
|
+
|
|
1794
|
+
/** Shared SweetAlert2 options so both guides open the same way. */
|
|
1795
|
+
const showHelpModal = (titleIcon, titleText, sections) =>
|
|
1757
1796
|
Swal.fire({
|
|
1758
1797
|
title: `<div style="display: flex; align-items: center; justify-content: center; gap: 8px;">${getIcon(
|
|
1759
|
-
|
|
1798
|
+
titleIcon,
|
|
1760
1799
|
'#3b82f6',
|
|
1761
1800
|
24
|
|
1762
|
-
)}
|
|
1763
|
-
html:
|
|
1764
|
-
<div style="
|
|
1765
|
-
|
|
1766
|
-
<!-- Quick Start Section -->
|
|
1767
|
-
<div style="margin-bottom: 24px;">
|
|
1768
|
-
<h3 style="color: #1f2937; margin: 0 0 12px 0; font-size: 16px; border-bottom: 2px solid #3b82f6; padding-bottom: 6px; display: flex; align-items: center; gap: 8px;">
|
|
1769
|
-
${getIcon('rocket', '#3b82f6', 16)}
|
|
1770
|
-
Getting Started
|
|
1771
|
-
</h3>
|
|
1772
|
-
|
|
1773
|
-
<div style="background: #f0f9ff; border-left: 4px solid #3b82f6; padding: 12px; margin-bottom: 12px;">
|
|
1774
|
-
<h4 style="color: #1e40af; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
|
|
1775
|
-
${getIcon('file', '#1e40af', 14)}
|
|
1776
|
-
Option 1: Use Saved Reports (Recommended)
|
|
1777
|
-
</h4>
|
|
1778
|
-
<p style="margin: 0 0 8px 0;">Click the "Templates" button to access pre-built reports for common needs like client lists, recent orders, or financial summaries.</p>
|
|
1779
|
-
<p style="margin: 0; font-style: italic; color: #1e40af;">Perfect for beginners and quick results!</p>
|
|
1780
|
-
</div>
|
|
1781
|
-
|
|
1782
|
-
<div style="background: #f9fafb; border-left: 4px solid #6b7280; padding: 12px;">
|
|
1783
|
-
<h4 style="color: #374151; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
|
|
1784
|
-
${getIcon('cogs', '#374151', 14)}
|
|
1785
|
-
Option 2: Build Custom Report
|
|
1786
|
-
</h4>
|
|
1787
|
-
<p style="margin: 0;">Follow the 5-step wizard below to create exactly what you need.</p>
|
|
1788
|
-
</div>
|
|
1789
|
-
</div>
|
|
1790
|
-
|
|
1791
|
-
<!-- Step-by-Step Guide -->
|
|
1792
|
-
<div style="margin-bottom: 24px;">
|
|
1793
|
-
<h3 style="color: #1f2937; margin: 0 0 12px 0; font-size: 16px; border-bottom: 2px solid #10b981; padding-bottom: 6px; display: flex; align-items: center; gap: 8px;">
|
|
1794
|
-
${getIcon('list', '#10b981', 16)}
|
|
1795
|
-
5-Step Report Creation
|
|
1796
|
-
</h3>
|
|
1797
|
-
|
|
1798
|
-
<!-- Step 1 -->
|
|
1799
|
-
<div style="background: #ecfdf5; border: 1px solid #10b981; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
|
|
1800
|
-
<h4 style="color: #047857; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
|
|
1801
|
-
${getIcon('database', '#047857', 14)}
|
|
1802
|
-
Step 1: Choose Your Data Source
|
|
1803
|
-
</h4>
|
|
1804
|
-
<ul style="margin: 0; padding-left: 16px; color: #065f46;">
|
|
1805
|
-
<li>Select the main table (e.g., Clients, Orders, Invoices)</li>
|
|
1806
|
-
<li>This becomes the foundation of your report</li>
|
|
1807
|
-
<li>Look for the table icon to identify data types</li>
|
|
1808
|
-
</ul>
|
|
1809
|
-
</div>
|
|
1810
|
-
|
|
1811
|
-
<!-- Step 2 -->
|
|
1812
|
-
<div style="background: #eff6ff; border: 1px solid #3b82f6; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
|
|
1813
|
-
<h4 style="color: #1e40af; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
|
|
1814
|
-
${getIcon('columns', '#1e40af', 14)}
|
|
1815
|
-
Step 2: Select Columns
|
|
1816
|
-
</h4>
|
|
1817
|
-
<ul style="margin: 0; padding-left: 16px; color: #1e3a8a;">
|
|
1818
|
-
<li>Check the boxes for data you want to see</li>
|
|
1819
|
-
<li>Drag columns to reorder them</li>
|
|
1820
|
-
<li>Use "Select All" for comprehensive reports</li>
|
|
1821
|
-
<li>Preview shows how your data will look</li>
|
|
1822
|
-
</ul>
|
|
1823
|
-
</div>
|
|
1824
|
-
|
|
1825
|
-
<!-- Step 3 -->
|
|
1826
|
-
<div style="background: #fef3c7; border: 1px solid #f59e0b; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
|
|
1827
|
-
<h4 style="color: #92400e; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
|
|
1828
|
-
${getIcon('link', '#92400e', 14)}
|
|
1829
|
-
Step 3: Add Relationships (Optional)
|
|
1830
|
-
</h4>
|
|
1831
|
-
<ul style="margin: 0; padding-left: 16px; color: #92400e;">
|
|
1832
|
-
<li>Connect related data (e.g., add customer names to invoices)</li>
|
|
1833
|
-
<li>We suggest common relationships automatically</li>
|
|
1834
|
-
<li>Click "Add Relationship" to connect more tables</li>
|
|
1835
|
-
<li>This enriches your report with additional context</li>
|
|
1836
|
-
</ul>
|
|
1837
|
-
</div>
|
|
1838
|
-
|
|
1839
|
-
<!-- Step 4 -->
|
|
1840
|
-
<div style="background: #fdf2f8; border: 1px solid #ec4899; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
|
|
1841
|
-
<h4 style="color: #be185d; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
|
|
1842
|
-
${getIcon('filter', '#be185d', 14)}
|
|
1843
|
-
Step 4: Apply Filters (Optional)
|
|
1844
|
-
</h4>
|
|
1845
|
-
<ul style="margin: 0; padding-left: 16px; color: #be185d;">
|
|
1846
|
-
<li>Narrow down results (e.g., "Active clients only")</li>
|
|
1847
|
-
<li>Use AND/OR logic for complex conditions</li>
|
|
1848
|
-
<li>Group filters for advanced filtering</li>
|
|
1849
|
-
<li>Great for date ranges, status filters, etc.</li>
|
|
1850
|
-
</ul>
|
|
1851
|
-
</div>
|
|
1852
|
-
|
|
1853
|
-
<!-- Step 5 -->
|
|
1854
|
-
<div style="background: #f3e8ff; border: 1px solid #8b5cf6; border-radius: 8px; padding: 12px;">
|
|
1855
|
-
<h4 style="color: #7c3aed; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
|
|
1856
|
-
${getIcon('download', '#7c3aed', 14)}
|
|
1857
|
-
Step 5: Generate & Export
|
|
1858
|
-
</h4>
|
|
1859
|
-
<ul style="margin: 0; padding-left: 16px; color: #7c3aed;">
|
|
1860
|
-
<li>Click "Generate Report" to see your data</li>
|
|
1861
|
-
<li>Review and refine as needed</li>
|
|
1862
|
-
<li>Export as Excel, CSV, or PDF</li>
|
|
1863
|
-
<li>Save the report template for future use</li>
|
|
1864
|
-
</ul>
|
|
1865
|
-
</div>
|
|
1866
|
-
</div>
|
|
1867
|
-
|
|
1868
|
-
<!-- Pro Tips Section -->
|
|
1869
|
-
<div style="margin-bottom: 20px;">
|
|
1870
|
-
<h3 style="color: #1f2937; margin: 0 0 12px 0; font-size: 16px; border-bottom: 2px solid #f59e0b; padding-bottom: 6px; display: flex; align-items: center; gap: 8px;">
|
|
1871
|
-
${getIcon('star', '#f59e0b', 16)}
|
|
1872
|
-
Pro Tips
|
|
1873
|
-
</h3>
|
|
1874
|
-
|
|
1875
|
-
<div style="background: #fffbeb; border: 1px solid #f59e0b; border-radius: 8px; padding: 12px;">
|
|
1876
|
-
<ul style="margin: 0; padding-left: 16px; color: #92400e;">
|
|
1877
|
-
<li><strong>Start Simple:</strong> Begin with basic reports, then add complexity</li>
|
|
1878
|
-
<li><strong>Use Previews:</strong> Check the preview before generating large reports</li>
|
|
1879
|
-
<li><strong>Save Templates:</strong> Save frequently used reports as templates</li>
|
|
1880
|
-
<li><strong>Date Filters:</strong> Use date ranges for better performance</li>
|
|
1881
|
-
<li><strong>Column Limits:</strong> Too many columns can slow down reports</li>
|
|
1882
|
-
</ul>
|
|
1883
|
-
</div>
|
|
1884
|
-
</div>
|
|
1885
|
-
|
|
1886
|
-
<!-- Common Use Cases -->
|
|
1887
|
-
<div style="margin-bottom: 20px;">
|
|
1888
|
-
<h3 style="color: #1f2937; margin: 0 0 12px 0; font-size: 16px; border-bottom: 2px solid #10b981; padding-bottom: 6px; display: flex; align-items: center; gap: 8px;">
|
|
1889
|
-
${getIcon('lightbulb', '#10b981', 16)}
|
|
1890
|
-
Common Report Examples
|
|
1891
|
-
</h3>
|
|
1892
|
-
|
|
1893
|
-
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
|
|
1894
|
-
<div style="background: #f0fdf4; border: 1px solid #22c55e; border-radius: 6px; padding: 8px;">
|
|
1895
|
-
<strong style="color: #15803d;">${getIcon(
|
|
1896
|
-
'users',
|
|
1897
|
-
'#15803d',
|
|
1898
|
-
12
|
|
1899
|
-
)}Client List:</strong>
|
|
1900
|
-
<br><small style="color: #16a34a;">Clients table + Contact details</small>
|
|
1901
|
-
</div>
|
|
1902
|
-
<div style="background: #eff6ff; border: 1px solid #3b82f6; border-radius: 6px; padding: 8px;">
|
|
1903
|
-
<strong style="color: #1d4ed8;">${getIcon(
|
|
1904
|
-
'cart',
|
|
1905
|
-
'#1d4ed8',
|
|
1906
|
-
12
|
|
1907
|
-
)}Recent Orders:</strong>
|
|
1908
|
-
<br><small style="color: #2563eb;">Orders + Customer names + Date filter</small>
|
|
1909
|
-
</div>
|
|
1910
|
-
<div style="background: #fef3c7; border: 1px solid #f59e0b; border-radius: 6px; padding: 8px;">
|
|
1911
|
-
<strong style="color: #d97706;">${getIcon(
|
|
1912
|
-
'money',
|
|
1913
|
-
'#d97706',
|
|
1914
|
-
12
|
|
1915
|
-
)}Financial Summary:</strong>
|
|
1916
|
-
<br><small style="color: #f59e0b;">Invoices + Payments + Date grouping</small>
|
|
1917
|
-
</div>
|
|
1918
|
-
<div style="background: #fdf2f8; border: 1px solid #ec4899; border-radius: 6px; padding: 8px;">
|
|
1919
|
-
<strong style="color: #db2777;">${getIcon(
|
|
1920
|
-
'tasks',
|
|
1921
|
-
'#db2777',
|
|
1922
|
-
12
|
|
1923
|
-
)}Active Projects:</strong>
|
|
1924
|
-
<br><small style="color: #ec4899;">Projects + Status filter + Team members</small>
|
|
1925
|
-
</div>
|
|
1926
|
-
</div>
|
|
1927
|
-
</div>
|
|
1928
|
-
|
|
1929
|
-
<!-- Troubleshooting -->
|
|
1930
|
-
<div>
|
|
1931
|
-
<h3 style="color: #1f2937; margin: 0 0 12px 0; font-size: 16px; border-bottom: 2px solid #dc2626; padding-bottom: 6px; display: flex; align-items: center; gap: 8px;">
|
|
1932
|
-
${getIcon('settings', '#dc2626', 16)}
|
|
1933
|
-
Common Issues
|
|
1934
|
-
</h3>
|
|
1935
|
-
|
|
1936
|
-
<div style="background: #fef2f2; border: 1px solid #dc2626; border-radius: 8px; padding: 12px;">
|
|
1937
|
-
<div style="margin-bottom: 8px;">
|
|
1938
|
-
<strong style="color: #dc2626;">${getIcon(
|
|
1939
|
-
'clock',
|
|
1940
|
-
'#dc2626',
|
|
1941
|
-
12
|
|
1942
|
-
)}Report too slow?</strong>
|
|
1943
|
-
<span style="color: #991b1b;"> Add date filters or reduce columns</span>
|
|
1944
|
-
</div>
|
|
1945
|
-
<div style="margin-bottom: 8px;">
|
|
1946
|
-
<strong style="color: #dc2626;">${getIcon(
|
|
1947
|
-
'question',
|
|
1948
|
-
'#dc2626',
|
|
1949
|
-
12
|
|
1950
|
-
)}Missing data?</strong>
|
|
1951
|
-
<span style="color: #991b1b;"> Check relationships and join conditions</span>
|
|
1952
|
-
</div>
|
|
1953
|
-
<div style="margin-bottom: 8px;">
|
|
1954
|
-
<strong style="color: #dc2626;">${getIcon(
|
|
1955
|
-
'warning',
|
|
1956
|
-
'#dc2626',
|
|
1957
|
-
12
|
|
1958
|
-
)}Export failing?</strong>
|
|
1959
|
-
<span style="color: #991b1b;"> Try smaller date ranges or fewer rows</span>
|
|
1960
|
-
</div>
|
|
1961
|
-
<div>
|
|
1962
|
-
<strong style="color: #dc2626;">${getIcon(
|
|
1963
|
-
'eye',
|
|
1964
|
-
'#dc2626',
|
|
1965
|
-
12
|
|
1966
|
-
)}No data showing?</strong>
|
|
1967
|
-
<span style="color: #991b1b;"> Review your filter conditions</span>
|
|
1968
|
-
</div>
|
|
1969
|
-
</div>
|
|
1970
|
-
</div>
|
|
1971
|
-
</div>
|
|
1972
|
-
`,
|
|
1801
|
+
)}${titleText}</div>`,
|
|
1802
|
+
html: `<div style="text-align: left; font-size: 13px; line-height: 1.5; color: #374151; max-height: 70vh; overflow-y: auto;">${sections.join(
|
|
1803
|
+
'<div style="height: 12px;"></div>'
|
|
1804
|
+
)}</div>`,
|
|
1973
1805
|
width: 800,
|
|
1974
1806
|
confirmButtonText: 'Close',
|
|
1975
1807
|
confirmButtonColor: '#6b7280',
|
|
@@ -1980,7 +1812,324 @@ const GenericReportImproved = ({
|
|
|
1980
1812
|
htmlContainer: 'help-content-scrollable',
|
|
1981
1813
|
},
|
|
1982
1814
|
});
|
|
1983
|
-
|
|
1815
|
+
|
|
1816
|
+
/**
|
|
1817
|
+
* Help for the semantic wizard: the user picks subjects, related areas and
|
|
1818
|
+
* details by their business names, so the guide never mentions a table, a
|
|
1819
|
+
* column or a join.
|
|
1820
|
+
*/
|
|
1821
|
+
const showSemanticGuidedHelp = () =>
|
|
1822
|
+
showHelpModal('chart', 'Report Builder Guide', [
|
|
1823
|
+
helpHeading('rocket', '#3b82f6', 'Getting started') +
|
|
1824
|
+
helpPanel({
|
|
1825
|
+
bg: '#f0f9ff',
|
|
1826
|
+
border: '#3b82f6',
|
|
1827
|
+
text: '#1e3a8a',
|
|
1828
|
+
icon: 'file',
|
|
1829
|
+
title: 'Open a report someone already built',
|
|
1830
|
+
items: [
|
|
1831
|
+
'The builder opens on "Choose a Starting Point"',
|
|
1832
|
+
'Click any saved report to open it, change it and run it',
|
|
1833
|
+
'A padlock means only you can see it; a globe means everyone can',
|
|
1834
|
+
],
|
|
1835
|
+
}) +
|
|
1836
|
+
helpPanel({
|
|
1837
|
+
bg: '#f9fafb',
|
|
1838
|
+
border: '#6b7280',
|
|
1839
|
+
text: '#374151',
|
|
1840
|
+
icon: 'cogs',
|
|
1841
|
+
title: 'Build a new one',
|
|
1842
|
+
items: [
|
|
1843
|
+
'Click "Create New Report" and work through the six steps',
|
|
1844
|
+
'Only two things are compulsory: what the report is about, and at least one detail to show',
|
|
1845
|
+
'Related information, filters and grouping can all be skipped',
|
|
1846
|
+
],
|
|
1847
|
+
}),
|
|
1848
|
+
|
|
1849
|
+
helpHeading('list', '#10b981', 'The six steps') +
|
|
1850
|
+
helpPanel({
|
|
1851
|
+
bg: '#ecfdf5',
|
|
1852
|
+
border: '#10b981',
|
|
1853
|
+
text: '#065f46',
|
|
1854
|
+
icon: 'database',
|
|
1855
|
+
title: '1. Choose Your Subject',
|
|
1856
|
+
items: [
|
|
1857
|
+
'Each card is a kind of record your practice keeps, in plain English',
|
|
1858
|
+
'The card tells you how many details and related areas it has',
|
|
1859
|
+
'Pick the one you want one row per — for example one row per client',
|
|
1860
|
+
],
|
|
1861
|
+
}) +
|
|
1862
|
+
helpPanel({
|
|
1863
|
+
bg: '#eff6ff',
|
|
1864
|
+
border: '#3b82f6',
|
|
1865
|
+
text: '#1e3a8a',
|
|
1866
|
+
icon: 'link',
|
|
1867
|
+
title: '2. Add Related Data (optional)',
|
|
1868
|
+
items: [
|
|
1869
|
+
'Each card is an area already connected to your subject — nothing to set up',
|
|
1870
|
+
'Click one to make its details available at the next step',
|
|
1871
|
+
'Once added, areas connected to that one appear too, up to three steps out',
|
|
1872
|
+
'An area marked "many per record" can repeat a record across several rows',
|
|
1873
|
+
],
|
|
1874
|
+
}) +
|
|
1875
|
+
helpPanel({
|
|
1876
|
+
bg: '#f0fdf4',
|
|
1877
|
+
border: '#22c55e',
|
|
1878
|
+
text: '#166534',
|
|
1879
|
+
icon: 'columns',
|
|
1880
|
+
title: '3. Choose Information',
|
|
1881
|
+
items: [
|
|
1882
|
+
'Details are listed under the area they come from, then sorted into Details, Amounts & numbers, Dates, and Status & yes/no',
|
|
1883
|
+
'Tick anything you want as a column',
|
|
1884
|
+
'Each ticked detail gets a "Show as" picker — see Totals and counts below',
|
|
1885
|
+
'"Columns in your report" lists your choices left to right; use Up, Down and the X to reorder or remove them',
|
|
1886
|
+
],
|
|
1887
|
+
}) +
|
|
1888
|
+
helpPanel({
|
|
1889
|
+
bg: '#fdf2f8',
|
|
1890
|
+
border: '#ec4899',
|
|
1891
|
+
text: '#9d174d',
|
|
1892
|
+
icon: 'filter',
|
|
1893
|
+
title: '4. Filter Results (optional)',
|
|
1894
|
+
items: [
|
|
1895
|
+
'Add a condition, choose a detail, then a plain-English test: is, is not, contains, is between, is more than, is blank, is any of, and so on',
|
|
1896
|
+
'The value box matches the detail — a date picker for dates, a list of choices for a status',
|
|
1897
|
+
'Switch a group between "all of these" and "any of these"',
|
|
1898
|
+
'Add a nested group to mix the two, e.g. active AND (has email OR has phone)',
|
|
1899
|
+
'No conditions means every record is included',
|
|
1900
|
+
],
|
|
1901
|
+
}) +
|
|
1902
|
+
helpPanel({
|
|
1903
|
+
bg: '#fef3c7',
|
|
1904
|
+
border: '#f59e0b',
|
|
1905
|
+
text: '#92400e',
|
|
1906
|
+
icon: 'database',
|
|
1907
|
+
title: '5. Group & Order (optional)',
|
|
1908
|
+
items: [
|
|
1909
|
+
'Tick a column to break the results into sections by its value',
|
|
1910
|
+
'Only plain columns can be grouped — a summarised column is the answer, not the heading',
|
|
1911
|
+
'Add one or more sorts to control the row order, each A to Z / low to high or the reverse',
|
|
1912
|
+
],
|
|
1913
|
+
}) +
|
|
1914
|
+
helpPanel({
|
|
1915
|
+
bg: '#f3e8ff',
|
|
1916
|
+
border: '#8b5cf6',
|
|
1917
|
+
text: '#6b21a8',
|
|
1918
|
+
icon: 'eye',
|
|
1919
|
+
title: '6. Preview & Save',
|
|
1920
|
+
items: [
|
|
1921
|
+
'Answer any run-time questions, then click "Run report"',
|
|
1922
|
+
'Results appear 10, 25, 50 or 100 rows at a time',
|
|
1923
|
+
'Save the report, and export the results when they look right',
|
|
1924
|
+
],
|
|
1925
|
+
}),
|
|
1926
|
+
|
|
1927
|
+
helpHeading('money', '#0891b2', 'Totals, counts and averages') +
|
|
1928
|
+
helpPanel({
|
|
1929
|
+
bg: '#ecfeff',
|
|
1930
|
+
border: '#0891b2',
|
|
1931
|
+
text: '#155e75',
|
|
1932
|
+
items: [
|
|
1933
|
+
'"Show as" is set to <strong>Value</strong> by default, which simply lists the detail',
|
|
1934
|
+
'Change it to <strong>Total</strong>, <strong>Average</strong>, <strong>Lowest</strong> or <strong>Highest</strong> on amounts and numbers, <strong>Earliest</strong> or <strong>Latest</strong> on dates, and <strong>Count</strong> on anything',
|
|
1935
|
+
'As soon as one column is summarised, the report collapses to one row per combination of the plain columns you kept',
|
|
1936
|
+
'So for totals by adviser: choose Clients, show the adviser’s name as Value and the fee amount as Total — you get one row per adviser',
|
|
1937
|
+
'If you also tick grouping at step 5 while a column is summarised, tick <em>every</em> plain column, or the report will be refused',
|
|
1938
|
+
],
|
|
1939
|
+
}),
|
|
1940
|
+
|
|
1941
|
+
helpHeading('question', '#7c3aed', 'Reports that ask a question each time') +
|
|
1942
|
+
helpPanel({
|
|
1943
|
+
bg: '#f5f3ff',
|
|
1944
|
+
border: '#7c3aed',
|
|
1945
|
+
text: '#5b21b6',
|
|
1946
|
+
items: [
|
|
1947
|
+
'On any condition, tick "Ask me for this each time I run it"',
|
|
1948
|
+
'The value is left out of the saved report and asked for instead, under "Before we run this report"',
|
|
1949
|
+
'Give each question a clear name — that wording is what everyone else will see',
|
|
1950
|
+
'Untick "Must be answered" to make a question optional',
|
|
1951
|
+
'Ideal for anything that changes week to week, such as a date range',
|
|
1952
|
+
],
|
|
1953
|
+
}),
|
|
1954
|
+
|
|
1955
|
+
helpHeading('users', '#059669', 'Saving and sharing') +
|
|
1956
|
+
helpPanel({
|
|
1957
|
+
bg: '#ecfdf5',
|
|
1958
|
+
border: '#059669',
|
|
1959
|
+
text: '#065f46',
|
|
1960
|
+
items: [
|
|
1961
|
+
'"Save report" asks for a name and whether to make it public',
|
|
1962
|
+
'Private reports (padlock) are yours; public ones (globe) are visible to other users',
|
|
1963
|
+
'Opening a saved report and saving again updates it — use the copy option to keep the original',
|
|
1964
|
+
'A saved report stores the recipe, not the results, so it always runs against today’s data',
|
|
1965
|
+
],
|
|
1966
|
+
}),
|
|
1967
|
+
|
|
1968
|
+
helpHeading('download', '#b45309', 'Exporting') +
|
|
1969
|
+
helpPanel({
|
|
1970
|
+
bg: '#fffbeb',
|
|
1971
|
+
border: '#b45309',
|
|
1972
|
+
text: '#92400e',
|
|
1973
|
+
items: [
|
|
1974
|
+
'Run the report first, then choose Excel, CSV or PDF from "Export as"',
|
|
1975
|
+
'The export covers the whole report, not just the page on screen',
|
|
1976
|
+
'PDF suits short reports; use Excel or CSV for long ones',
|
|
1977
|
+
],
|
|
1978
|
+
}),
|
|
1979
|
+
|
|
1980
|
+
helpHeading('warning', '#dc2626', 'Good to know') +
|
|
1981
|
+
helpPanel({
|
|
1982
|
+
bg: '#fef2f2',
|
|
1983
|
+
border: '#dc2626',
|
|
1984
|
+
text: '#991b1b',
|
|
1985
|
+
items: [
|
|
1986
|
+
'Deleted records are left out of reports entirely — they are missing from the rows, and from every count and total',
|
|
1987
|
+
'Adding a "many per record" area can multiply your rows; summarise it instead to keep one row per record',
|
|
1988
|
+
'Nothing shown at all? Check your conditions at step 4 — one condition too many is the usual cause',
|
|
1989
|
+
'If the report is refused, the message on screen names the detail that caused it',
|
|
1990
|
+
],
|
|
1991
|
+
}),
|
|
1992
|
+
]);
|
|
1993
|
+
|
|
1994
|
+
/**
|
|
1995
|
+
* Help for the legacy table/column wizard, used when the newer report
|
|
1996
|
+
* engine is not available.
|
|
1997
|
+
*/
|
|
1998
|
+
const showLegacyGuidedHelp = () =>
|
|
1999
|
+
showHelpModal('chart', 'Complete Report Builder Guide', [
|
|
2000
|
+
helpHeading('rocket', '#3b82f6', 'Getting Started') +
|
|
2001
|
+
helpPanel({
|
|
2002
|
+
bg: '#f0f9ff',
|
|
2003
|
+
border: '#3b82f6',
|
|
2004
|
+
text: '#1e3a8a',
|
|
2005
|
+
icon: 'file',
|
|
2006
|
+
title: 'Option 1: Start from a saved report (recommended)',
|
|
2007
|
+
items: [
|
|
2008
|
+
'The builder opens on "Choose a Starting Point"',
|
|
2009
|
+
'Click any saved report to open it and adjust it',
|
|
2010
|
+
'A padlock means only you can see it; a globe means everyone can',
|
|
2011
|
+
],
|
|
2012
|
+
}) +
|
|
2013
|
+
helpPanel({
|
|
2014
|
+
bg: '#f9fafb',
|
|
2015
|
+
border: '#6b7280',
|
|
2016
|
+
text: '#374151',
|
|
2017
|
+
icon: 'cogs',
|
|
2018
|
+
title: 'Option 2: Build a custom report',
|
|
2019
|
+
items: [
|
|
2020
|
+
'Click "Create New Report" and follow the six steps below',
|
|
2021
|
+
],
|
|
2022
|
+
}),
|
|
2023
|
+
|
|
2024
|
+
helpHeading('list', '#10b981', 'Six steps to a report') +
|
|
2025
|
+
helpPanel({
|
|
2026
|
+
bg: '#ecfdf5',
|
|
2027
|
+
border: '#10b981',
|
|
2028
|
+
text: '#065f46',
|
|
2029
|
+
icon: 'database',
|
|
2030
|
+
title: 'Step 1: Select Your Data',
|
|
2031
|
+
items: [
|
|
2032
|
+
'Choose the main data source your report is about',
|
|
2033
|
+
'This becomes the foundation of the report',
|
|
2034
|
+
'You can bring in related data at the next step',
|
|
2035
|
+
],
|
|
2036
|
+
}) +
|
|
2037
|
+
helpPanel({
|
|
2038
|
+
bg: '#fef3c7',
|
|
2039
|
+
border: '#f59e0b',
|
|
2040
|
+
text: '#92400e',
|
|
2041
|
+
icon: 'link',
|
|
2042
|
+
title: 'Step 2: Add Related Data (optional)',
|
|
2043
|
+
items: [
|
|
2044
|
+
'Review the connections suggested for you',
|
|
2045
|
+
'Connect the ones that add value, e.g. customer names on invoices',
|
|
2046
|
+
'Connected data provides extra fields at the next step',
|
|
2047
|
+
'Skip this step if the main data source has everything you need',
|
|
2048
|
+
],
|
|
2049
|
+
}) +
|
|
2050
|
+
helpPanel({
|
|
2051
|
+
bg: '#eff6ff',
|
|
2052
|
+
border: '#3b82f6',
|
|
2053
|
+
text: '#1e3a8a',
|
|
2054
|
+
icon: 'columns',
|
|
2055
|
+
title: 'Step 3: Choose Information',
|
|
2056
|
+
items: [
|
|
2057
|
+
'Tick the fields you want to see, from your main data source and anything you connected',
|
|
2058
|
+
'Fields are grouped by category to make them easier to find',
|
|
2059
|
+
'Use "Select All Visible" or "Clear All" to work quickly',
|
|
2060
|
+
],
|
|
2061
|
+
}) +
|
|
2062
|
+
helpPanel({
|
|
2063
|
+
bg: '#fdf2f8',
|
|
2064
|
+
border: '#ec4899',
|
|
2065
|
+
text: '#9d174d',
|
|
2066
|
+
icon: 'filter',
|
|
2067
|
+
title: 'Step 4: Filter Results (optional)',
|
|
2068
|
+
items: [
|
|
2069
|
+
'Narrow the results down, e.g. active clients only',
|
|
2070
|
+
'Combine conditions with AND/OR logic',
|
|
2071
|
+
'Group conditions together for more involved rules',
|
|
2072
|
+
'Great for date ranges and status filters',
|
|
2073
|
+
],
|
|
2074
|
+
}) +
|
|
2075
|
+
helpPanel({
|
|
2076
|
+
bg: '#f0fdf4',
|
|
2077
|
+
border: '#22c55e',
|
|
2078
|
+
text: '#166534',
|
|
2079
|
+
icon: 'database',
|
|
2080
|
+
title: 'Step 5: Group Configuration (optional)',
|
|
2081
|
+
items: [
|
|
2082
|
+
'Turn on grouping to split the report into sections',
|
|
2083
|
+
'Choose the field to group by, e.g. status or category',
|
|
2084
|
+
'Set the group display and styling options',
|
|
2085
|
+
'Skip this step for a plain table',
|
|
2086
|
+
],
|
|
2087
|
+
}) +
|
|
2088
|
+
helpPanel({
|
|
2089
|
+
bg: '#f3e8ff',
|
|
2090
|
+
border: '#8b5cf6',
|
|
2091
|
+
text: '#6b21a8',
|
|
2092
|
+
icon: 'download',
|
|
2093
|
+
title: 'Step 6: Preview & Save',
|
|
2094
|
+
items: [
|
|
2095
|
+
'Click "Generate Report" to see your data',
|
|
2096
|
+
'Review the results and refine as needed',
|
|
2097
|
+
'Save the report so you can run it again later',
|
|
2098
|
+
'Click "Export" to download the results as an Excel file',
|
|
2099
|
+
],
|
|
2100
|
+
}),
|
|
2101
|
+
|
|
2102
|
+
helpHeading('star', '#f59e0b', 'Pro Tips') +
|
|
2103
|
+
helpPanel({
|
|
2104
|
+
bg: '#fffbeb',
|
|
2105
|
+
border: '#f59e0b',
|
|
2106
|
+
text: '#92400e',
|
|
2107
|
+
items: [
|
|
2108
|
+
'<strong>Start simple:</strong> begin with a basic report, then add to it',
|
|
2109
|
+
'<strong>Save before exporting:</strong> an export needs the report to have a name',
|
|
2110
|
+
'<strong>Save your work:</strong> a saved report can be run again any time against current data',
|
|
2111
|
+
'<strong>Date filters:</strong> narrowing the dates keeps big reports fast',
|
|
2112
|
+
'<strong>Field limits:</strong> too many fields can slow a report down',
|
|
2113
|
+
],
|
|
2114
|
+
}),
|
|
2115
|
+
|
|
2116
|
+
helpHeading('settings', '#dc2626', 'Common Issues') +
|
|
2117
|
+
helpPanel({
|
|
2118
|
+
bg: '#fef2f2',
|
|
2119
|
+
border: '#dc2626',
|
|
2120
|
+
text: '#991b1b',
|
|
2121
|
+
items: [
|
|
2122
|
+
'<strong>Report too slow?</strong> Add date filters or reduce the number of fields',
|
|
2123
|
+
'<strong>Missing data?</strong> Check the connections you added at step 2',
|
|
2124
|
+
'<strong>Export failing?</strong> Try a smaller date range or fewer rows',
|
|
2125
|
+
'<strong>No data showing?</strong> Review your filter conditions',
|
|
2126
|
+
],
|
|
2127
|
+
}),
|
|
2128
|
+
]);
|
|
2129
|
+
|
|
2130
|
+
/** The guide matches whichever wizard the user is actually looking at. */
|
|
2131
|
+
const showGuidedHelp = () =>
|
|
2132
|
+
isSemantic ? showSemanticGuidedHelp() : showLegacyGuidedHelp();
|
|
1984
2133
|
|
|
1985
2134
|
// Initialize component
|
|
1986
2135
|
useEffect(() => {
|
|
@@ -1994,12 +2143,17 @@ const GenericReportImproved = ({
|
|
|
1994
2143
|
|
|
1995
2144
|
fetchSavedReports();
|
|
1996
2145
|
|
|
1997
|
-
// Show help on first load
|
|
1998
|
-
|
|
2146
|
+
// Show help on first load. The two wizards keep separate flags: users
|
|
2147
|
+
// who already dismissed the legacy guide have never seen the semantic
|
|
2148
|
+
// one, and it describes a different set of steps.
|
|
2149
|
+
const helpSeenKey = isSemantic
|
|
2150
|
+
? 'reportBuilder_hasSeenSemanticHelp'
|
|
2151
|
+
: 'reportBuilder_hasSeenHelp';
|
|
2152
|
+
const hasSeenHelp = localStorage.getItem(helpSeenKey);
|
|
1999
2153
|
if (!hasSeenHelp) {
|
|
2000
2154
|
setTimeout(() => {
|
|
2001
2155
|
showGuidedHelp();
|
|
2002
|
-
localStorage.setItem(
|
|
2156
|
+
localStorage.setItem(helpSeenKey, 'true');
|
|
2003
2157
|
}, 500);
|
|
2004
2158
|
}
|
|
2005
2159
|
}, [tableUrl, reportsUrl, semanticStatus]); // Re-fetch when URLs change
|
|
@@ -643,7 +643,8 @@ select:not(:placeholder-shown) + .fi__span {
|
|
|
643
643
|
overflow: visible;
|
|
644
644
|
position: relative;
|
|
645
645
|
z-index: 100;
|
|
646
|
-
box-shadow:
|
|
646
|
+
box-shadow:
|
|
647
|
+
0 20px 60px rgba(0, 0, 0, 0.15),
|
|
647
648
|
0 0 0 1px rgba(var(--primary-rgb), 0.06);
|
|
648
649
|
}
|
|
649
650
|
|
|
@@ -786,9 +787,6 @@ select:not(:placeholder-shown) + .fi__span {
|
|
|
786
787
|
|
|
787
788
|
/* Adding btn class */
|
|
788
789
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
790
|
/* Floating action buttons */
|
|
793
791
|
.polActions {
|
|
794
792
|
position: fixed;
|
|
@@ -801,9 +799,11 @@ select:not(:placeholder-shown) + .fi__span {
|
|
|
801
799
|
background: rgba(255, 255, 255, 0.95);
|
|
802
800
|
padding: 10px 14px;
|
|
803
801
|
border-radius: 12px;
|
|
804
|
-
box-shadow:
|
|
802
|
+
box-shadow:
|
|
803
|
+
0 4px 20px rgba(0, 0, 0, 0.12),
|
|
805
804
|
0 0 0 1px rgba(var(--primary-rgb), 0.08);
|
|
806
805
|
backdrop-filter: blur(8px);
|
|
806
|
+
align-items: center;
|
|
807
807
|
|
|
808
808
|
button {
|
|
809
809
|
display: block;
|
|
@@ -814,6 +814,50 @@ select:not(:placeholder-shown) + .fi__span {
|
|
|
814
814
|
}
|
|
815
815
|
}
|
|
816
816
|
|
|
817
|
+
/* Stored or not — the one thing this screen never used to say out loud. */
|
|
818
|
+
.saveState {
|
|
819
|
+
display: inline-flex;
|
|
820
|
+
align-items: center;
|
|
821
|
+
gap: 6px;
|
|
822
|
+
padding: 0 6px;
|
|
823
|
+
font-size: 0.8rem;
|
|
824
|
+
font-weight: 600;
|
|
825
|
+
white-space: nowrap;
|
|
826
|
+
|
|
827
|
+
&::before {
|
|
828
|
+
content: '';
|
|
829
|
+
width: 8px;
|
|
830
|
+
height: 8px;
|
|
831
|
+
border-radius: 50%;
|
|
832
|
+
background: currentColor;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
.saveStateDirty {
|
|
837
|
+
color: #b45309;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
.saveStateClean {
|
|
841
|
+
color: #15803d;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/* The save that actually writes, weighted so it reads as the end of the job. */
|
|
845
|
+
.btnPrimary {
|
|
846
|
+
box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.35);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/* Sits under the question window's Apply, where the misunderstanding happens. */
|
|
850
|
+
.applyHint {
|
|
851
|
+
flex: 1;
|
|
852
|
+
align-self: center;
|
|
853
|
+
margin: 0;
|
|
854
|
+
padding-right: 1rem;
|
|
855
|
+
font-size: 0.78rem;
|
|
856
|
+
line-height: 1.35;
|
|
857
|
+
color: var(--text-color-light, #6b7280);
|
|
858
|
+
text-align: left;
|
|
859
|
+
}
|
|
860
|
+
|
|
817
861
|
.fwItem {
|
|
818
862
|
flex-basis: 100%;
|
|
819
863
|
}
|
|
@@ -1337,6 +1381,12 @@ select:not(:placeholder-shown) + .fi__span {
|
|
|
1337
1381
|
.dragHandle {
|
|
1338
1382
|
color: var(--primary-color) !important;
|
|
1339
1383
|
cursor: grab;
|
|
1384
|
+
/* Same reason as .cGrip: on iPadOS the browser treats a press-and-move on
|
|
1385
|
+
this handle as a scroll unless the element opts out, so the drag never
|
|
1386
|
+
activates and reordering is silently unavailable. */
|
|
1387
|
+
touch-action: none;
|
|
1388
|
+
-webkit-user-select: none;
|
|
1389
|
+
user-select: none;
|
|
1340
1390
|
padding: 4px;
|
|
1341
1391
|
border-radius: 6px;
|
|
1342
1392
|
background: white;
|
|
@@ -2047,7 +2097,8 @@ select:not(:placeholder-shown) + .fi__span {
|
|
|
2047
2097
|
border-radius: 12px;
|
|
2048
2098
|
padding: 1.25rem 1.5rem;
|
|
2049
2099
|
width: 280px;
|
|
2050
|
-
box-shadow:
|
|
2100
|
+
box-shadow:
|
|
2101
|
+
0 16px 48px rgba(0, 0, 0, 0.18),
|
|
2051
2102
|
0 0 0 1px rgba(var(--primary-rgb), 0.08);
|
|
2052
2103
|
|
|
2053
2104
|
h3 {
|
|
@@ -2472,7 +2523,6 @@ select:not(:placeholder-shown) + .fi__span {
|
|
|
2472
2523
|
margin-right: -1.15rem !important;
|
|
2473
2524
|
}
|
|
2474
2525
|
|
|
2475
|
-
|
|
2476
2526
|
/* The header and search span both columns; only the rail and canvas sit
|
|
2477
2527
|
side by side. */
|
|
2478
2528
|
|
|
@@ -2622,9 +2672,6 @@ select:not(:placeholder-shown) + .fi__span {
|
|
|
2622
2672
|
|
|
2623
2673
|
/* ---- compact rows ---- */
|
|
2624
2674
|
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
2675
|
.duplicateBtn {
|
|
2629
2676
|
display: flex;
|
|
2630
2677
|
align-items: center;
|
|
@@ -2705,6 +2752,12 @@ select:not(:placeholder-shown) + .fi__span {
|
|
|
2705
2752
|
align-items: center;
|
|
2706
2753
|
color: rgba(var(--paragraph-rgb), 0.4);
|
|
2707
2754
|
cursor: grab;
|
|
2755
|
+
/* dnd-kit's PointerSensor cannot start a drag from an element the browser
|
|
2756
|
+
has already claimed for scrolling. Without this the grip does nothing at
|
|
2757
|
+
all on a touch screen — the page just scrolls under the finger. */
|
|
2758
|
+
touch-action: none;
|
|
2759
|
+
-webkit-user-select: none;
|
|
2760
|
+
user-select: none;
|
|
2708
2761
|
|
|
2709
2762
|
&:active {
|
|
2710
2763
|
cursor: grabbing;
|