@qoretechnologies/reqraft 0.10.2 → 0.10.4
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/design/COMPACT_ENGINE_REDESIGN.md +156 -0
- package/design/FORM_ENGINE_COMPACT_UX_PLAN.md +353 -0
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +153 -94
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
- package/dist/components/form/engine/CompactToolbar.js +130 -94
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +181 -45
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts +6 -3
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +70 -48
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
- package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts +19 -0
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +22 -1
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/variants/VariantCalmTable.d.ts +6 -0
- package/dist/components/form/engine/variants/VariantCalmTable.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantCalmTable.js +94 -0
- package/dist/components/form/engine/variants/VariantCalmTable.js.map +1 -0
- package/dist/components/form/engine/variants/VariantCards.d.ts +6 -0
- package/dist/components/form/engine/variants/VariantCards.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantCards.js +80 -0
- package/dist/components/form/engine/variants/VariantCards.js.map +1 -0
- package/dist/components/form/engine/variants/VariantFocus.d.ts +7 -0
- package/dist/components/form/engine/variants/VariantFocus.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantFocus.js +138 -0
- package/dist/components/form/engine/variants/VariantFocus.js.map +1 -0
- package/dist/components/form/engine/variants/VariantMinimal.d.ts +6 -0
- package/dist/components/form/engine/variants/VariantMinimal.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantMinimal.js +73 -0
- package/dist/components/form/engine/variants/VariantMinimal.js.map +1 -0
- package/dist/components/form/engine/variants/focusDemo.d.ts +13 -0
- package/dist/components/form/engine/variants/focusDemo.d.ts.map +1 -0
- package/dist/components/form/engine/variants/focusDemo.js +139 -0
- package/dist/components/form/engine/variants/focusDemo.js.map +1 -0
- package/dist/components/form/engine/variants/variantModel.d.ts +70 -0
- package/dist/components/form/engine/variants/variantModel.d.ts.map +1 -0
- package/dist/components/form/engine/variants/variantModel.js +133 -0
- package/dist/components/form/engine/variants/variantModel.js.map +1 -0
- package/dist/components/form/engine/variants/variantParts.d.ts +79 -0
- package/dist/components/form/engine/variants/variantParts.d.ts.map +1 -0
- package/dist/components/form/engine/variants/variantParts.js +191 -0
- package/dist/components/form/engine/variants/variantParts.js.map +1 -0
- package/dist/components/form/fields/auto/AutoFormField.d.ts +3 -0
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +2 -2
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactRow.tsx +256 -234
- package/src/components/form/engine/CompactToolbar.tsx +108 -68
- package/src/components/form/engine/FormEngine.stories.tsx +127 -110
- package/src/components/form/engine/FormEngine.tsx +248 -67
- package/src/components/form/engine/compactRowStyles.ts +207 -134
- package/src/components/form/engine/compactToolbarContext.ts +1 -0
- package/src/components/form/engine/readFirst.ts +35 -0
- package/src/components/form/engine/variants/FormEngineVariants.stories.tsx +119 -0
- package/src/components/form/engine/variants/VariantCalmTable.tsx +242 -0
- package/src/components/form/engine/variants/VariantCards.tsx +212 -0
- package/src/components/form/engine/variants/VariantFocus.tsx +382 -0
- package/src/components/form/engine/variants/VariantMinimal.tsx +170 -0
- package/src/components/form/engine/variants/focusDemo.ts +145 -0
- package/src/components/form/engine/variants/variantModel.ts +216 -0
- package/src/components/form/engine/variants/variantParts.tsx +313 -0
- package/src/components/form/fields/auto/AutoFormField.tsx +5 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VARIANT 3 — "Focus" (production-leaning)
|
|
3
|
+
*
|
|
4
|
+
* The original Focus structure — three expandable boxes (Needs attention / Set /
|
|
5
|
+
* Optional) — with the engine's real functionality layered in:
|
|
6
|
+
* • REQUIRED (one-of) GROUPS highlighted as a "pick one" cluster in the box,
|
|
7
|
+
* • schema GROUPS as thin Minimal-style labels INSIDE the Set / Optional boxes,
|
|
8
|
+
* • a "Descriptions" toggle (short_desc under every field),
|
|
9
|
+
* • COMPLEX fields showing their nested value inline with Show-more,
|
|
10
|
+
* • real inline editing (AutoFormField) on click.
|
|
11
|
+
*
|
|
12
|
+
* Attention box and the rest are EXCLUSIVE: a field needing attention sits in the
|
|
13
|
+
* box until resolved, then drops into Set (or Optional) below.
|
|
14
|
+
*/
|
|
15
|
+
import { ReqoreIcon, ReqoreP } from '@qoretechnologies/reqore';
|
|
16
|
+
import { IQorusFormField, IQorusFormSchema } from '@qoretechnologies/ts-toolkit';
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import styled from 'styled-components';
|
|
19
|
+
import { IVariantGroup, IVariantRow, TVariantStatus } from './variantModel';
|
|
20
|
+
import {
|
|
21
|
+
ComplexPreview,
|
|
22
|
+
InlineEdit,
|
|
23
|
+
STATUS_COLOR,
|
|
24
|
+
StatusDot,
|
|
25
|
+
TVariantForm,
|
|
26
|
+
ValueView,
|
|
27
|
+
VariantToolbar,
|
|
28
|
+
useVariantColors,
|
|
29
|
+
useVariantForm,
|
|
30
|
+
} from './variantParts';
|
|
31
|
+
|
|
32
|
+
const Wrap = styled.div<{ $hover: string; $faint: string }>`
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-flow: column;
|
|
35
|
+
gap: 18px;
|
|
36
|
+
font-size: 13px;
|
|
37
|
+
|
|
38
|
+
.vf-row {
|
|
39
|
+
display: grid;
|
|
40
|
+
grid-template-columns: minmax(190px, 320px) minmax(0, 1fr) auto;
|
|
41
|
+
column-gap: 16px;
|
|
42
|
+
row-gap: 2px;
|
|
43
|
+
align-items: center;
|
|
44
|
+
/* Single-line rows: centre the content within the min-height. */
|
|
45
|
+
align-content: center;
|
|
46
|
+
min-height: 38px;
|
|
47
|
+
padding: 6px 12px;
|
|
48
|
+
border-radius: 8px;
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
}
|
|
51
|
+
/* Rows with a description / preview / editor below: pin the name+value to the
|
|
52
|
+
top instead of centring the whole block. */
|
|
53
|
+
.vf-row.vf-tall {
|
|
54
|
+
align-content: start;
|
|
55
|
+
}
|
|
56
|
+
.vf-row:hover,
|
|
57
|
+
.vf-row[aria-expanded='true'] {
|
|
58
|
+
background: ${({ $hover }) => $hover};
|
|
59
|
+
}
|
|
60
|
+
.vf-section {
|
|
61
|
+
border-radius: 12px;
|
|
62
|
+
padding: 6px;
|
|
63
|
+
}
|
|
64
|
+
.vf-sechead {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: 8px;
|
|
68
|
+
padding: 8px 12px;
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
user-select: none;
|
|
71
|
+
}
|
|
72
|
+
.vf-grouplabel {
|
|
73
|
+
font-size: 10px;
|
|
74
|
+
letter-spacing: 1px;
|
|
75
|
+
text-transform: uppercase;
|
|
76
|
+
padding: 10px 0 2px 12px;
|
|
77
|
+
}
|
|
78
|
+
.vf-cluster {
|
|
79
|
+
border-radius: 10px;
|
|
80
|
+
padding: 4px 4px 6px;
|
|
81
|
+
margin: 2px 0;
|
|
82
|
+
}
|
|
83
|
+
.vf-desc {
|
|
84
|
+
grid-column: 1 / -1;
|
|
85
|
+
padding: 0 0 2px;
|
|
86
|
+
font-size: 12px;
|
|
87
|
+
line-height: 1.45;
|
|
88
|
+
}
|
|
89
|
+
.vf-preview {
|
|
90
|
+
grid-column: 2 / -1;
|
|
91
|
+
padding: 2px 0 4px;
|
|
92
|
+
}
|
|
93
|
+
@media (max-width: 640px) {
|
|
94
|
+
.vf-row {
|
|
95
|
+
grid-template-columns: 1fr auto;
|
|
96
|
+
grid-template-areas: 'label dot' 'value value';
|
|
97
|
+
row-gap: 2px;
|
|
98
|
+
}
|
|
99
|
+
.vf-label {
|
|
100
|
+
grid-area: label;
|
|
101
|
+
}
|
|
102
|
+
.vf-value {
|
|
103
|
+
grid-area: value;
|
|
104
|
+
}
|
|
105
|
+
.vf-dot {
|
|
106
|
+
grid-area: dot;
|
|
107
|
+
}
|
|
108
|
+
.vf-preview {
|
|
109
|
+
grid-column: 1 / -1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
`;
|
|
113
|
+
|
|
114
|
+
const Row = ({ r, form, showDesc }: { r: IVariantRow; form: TVariantForm; showDesc: boolean }) => {
|
|
115
|
+
const c = useVariantColors();
|
|
116
|
+
const editing = form.editing === r.name;
|
|
117
|
+
const isHash = r.value.kind === 'hash';
|
|
118
|
+
// A row is "tall" (has something below the name/value line) when editing, when
|
|
119
|
+
// it's a complex preview, or when descriptions are shown for it.
|
|
120
|
+
const tall = editing || isHash || (showDesc && !!r.shortDesc);
|
|
121
|
+
return (
|
|
122
|
+
<div
|
|
123
|
+
className={tall ? 'vf-row vf-tall' : 'vf-row'}
|
|
124
|
+
role='button'
|
|
125
|
+
tabIndex={0}
|
|
126
|
+
aria-expanded={editing}
|
|
127
|
+
onClick={() => !r.readOnly && form.startEdit(r.name)}
|
|
128
|
+
>
|
|
129
|
+
<span
|
|
130
|
+
className='vf-label'
|
|
131
|
+
style={{ fontWeight: 600, color: c.text, display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}
|
|
132
|
+
>
|
|
133
|
+
{r.label}
|
|
134
|
+
{r.required ?
|
|
135
|
+
<ReqoreIcon icon='Asterisk' size='9px' style={{ color: c.danger }} />
|
|
136
|
+
: null}
|
|
137
|
+
</span>
|
|
138
|
+
<span className='vf-value' style={{ color: c.muted, minWidth: 0 }}>
|
|
139
|
+
<ValueView value={r.value} />
|
|
140
|
+
{r.reason && r.status !== 'set' ?
|
|
141
|
+
<span style={{ color: STATUS_COLOR(r.status, c), fontSize: 12, marginLeft: 8 }}>
|
|
142
|
+
{r.reason}
|
|
143
|
+
</span>
|
|
144
|
+
: null}
|
|
145
|
+
</span>
|
|
146
|
+
<span className='vf-dot' style={{ display: 'inline-flex', justifyContent: 'flex-end' }}>
|
|
147
|
+
<StatusDot status={r.status} />
|
|
148
|
+
</span>
|
|
149
|
+
{showDesc && r.shortDesc && !editing ?
|
|
150
|
+
<div className='vf-desc' style={{ color: c.faint }}>
|
|
151
|
+
{r.shortDesc}
|
|
152
|
+
</div>
|
|
153
|
+
: null}
|
|
154
|
+
{isHash && !editing ?
|
|
155
|
+
<div className='vf-preview'>
|
|
156
|
+
<ComplexPreview value={r.field?.value} onOpen={() => !r.readOnly && form.startEdit(r.name)} />
|
|
157
|
+
</div>
|
|
158
|
+
: null}
|
|
159
|
+
{editing ?
|
|
160
|
+
<>
|
|
161
|
+
{r.shortDesc ? <div className='vf-desc' style={{ color: c.faint }}>{r.shortDesc}</div> : null}
|
|
162
|
+
<InlineEdit row={r} onDone={form.stopEdit} />
|
|
163
|
+
</>
|
|
164
|
+
: null}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/** A one-of required group, highlighted: "pick one of these". */
|
|
170
|
+
const RequiredCluster = ({
|
|
171
|
+
rows,
|
|
172
|
+
form,
|
|
173
|
+
showDesc,
|
|
174
|
+
}: {
|
|
175
|
+
rows: IVariantRow[];
|
|
176
|
+
form: TVariantForm;
|
|
177
|
+
showDesc: boolean;
|
|
178
|
+
}) => {
|
|
179
|
+
const c = useVariantColors();
|
|
180
|
+
const tint = c.warning;
|
|
181
|
+
return (
|
|
182
|
+
<div className='vf-cluster' style={{ background: `${tint}10`, border: `1px solid ${tint}33` }}>
|
|
183
|
+
{/* Matches the thin group-label style (.vf-grouplabel) for compactness. */}
|
|
184
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 0 2px 12px' }}>
|
|
185
|
+
<ReqoreIcon icon='LinkM' size='11px' style={{ color: tint }} />
|
|
186
|
+
<span style={{ fontSize: 10, letterSpacing: 1, textTransform: 'uppercase', color: tint }}>
|
|
187
|
+
One of the below is required
|
|
188
|
+
</span>
|
|
189
|
+
</div>
|
|
190
|
+
{rows.map((r) => <Row key={r.name} r={r} form={form} showDesc={showDesc} />)}
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const Section = ({
|
|
196
|
+
title,
|
|
197
|
+
intent,
|
|
198
|
+
count,
|
|
199
|
+
children,
|
|
200
|
+
defaultOpen = true,
|
|
201
|
+
}: {
|
|
202
|
+
title: string;
|
|
203
|
+
intent: string;
|
|
204
|
+
count: number;
|
|
205
|
+
children: React.ReactNode;
|
|
206
|
+
defaultOpen?: boolean;
|
|
207
|
+
}) => {
|
|
208
|
+
const [open, setOpen] = React.useState(defaultOpen);
|
|
209
|
+
if (!count) return null;
|
|
210
|
+
return (
|
|
211
|
+
<div className='vf-section' style={{ background: `${intent}0e`, border: `1px solid ${intent}22` }}>
|
|
212
|
+
<div className='vf-sechead' onClick={() => setOpen((o) => !o)}>
|
|
213
|
+
<ReqoreIcon icon={open ? 'ArrowDownSLine' : 'ArrowRightSLine'} size='15px' />
|
|
214
|
+
<ReqoreP effect={{ weight: 'bold' }}>{title}</ReqoreP>
|
|
215
|
+
<span
|
|
216
|
+
style={{
|
|
217
|
+
background: `${intent}22`,
|
|
218
|
+
color: intent,
|
|
219
|
+
borderRadius: 20,
|
|
220
|
+
padding: '1px 9px',
|
|
221
|
+
fontSize: 12,
|
|
222
|
+
fontWeight: 700,
|
|
223
|
+
}}
|
|
224
|
+
>
|
|
225
|
+
{count}
|
|
226
|
+
</span>
|
|
227
|
+
</div>
|
|
228
|
+
{open ? <div>{children}</div> : null}
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/** Rows of a given status, grouped by schema group with thin Minimal-style
|
|
234
|
+
* labels. Used inside the Set and Optional boxes. */
|
|
235
|
+
const GroupedRows = ({
|
|
236
|
+
groups,
|
|
237
|
+
status,
|
|
238
|
+
skip,
|
|
239
|
+
form,
|
|
240
|
+
showDesc,
|
|
241
|
+
}: {
|
|
242
|
+
groups: IVariantGroup[];
|
|
243
|
+
status: TVariantStatus;
|
|
244
|
+
skip: Set<string>;
|
|
245
|
+
form: TVariantForm;
|
|
246
|
+
showDesc: boolean;
|
|
247
|
+
}) => {
|
|
248
|
+
const c = useVariantColors();
|
|
249
|
+
const sections = groups
|
|
250
|
+
.map((g) => ({ g, rows: g.rows.filter((r) => r.status === status && !skip.has(r.name)) }))
|
|
251
|
+
.filter((s) => s.rows.length);
|
|
252
|
+
return (
|
|
253
|
+
<>
|
|
254
|
+
{sections.map(({ g, rows }) => (
|
|
255
|
+
<div key={g.name}>
|
|
256
|
+
{/* only label when there's more than one group's worth, else it's noise */}
|
|
257
|
+
{sections.length > 1 ?
|
|
258
|
+
<div className='vf-grouplabel' style={{ color: c.faint }}>
|
|
259
|
+
{g.label}
|
|
260
|
+
</div>
|
|
261
|
+
: null}
|
|
262
|
+
{rows.map((r) => <Row key={r.name} r={r} form={form} showDesc={showDesc} />)}
|
|
263
|
+
</div>
|
|
264
|
+
))}
|
|
265
|
+
</>
|
|
266
|
+
);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export const VariantFocus = ({
|
|
270
|
+
options,
|
|
271
|
+
values,
|
|
272
|
+
config,
|
|
273
|
+
}: {
|
|
274
|
+
options: IQorusFormSchema;
|
|
275
|
+
values: Record<string, IQorusFormField>;
|
|
276
|
+
config?: any;
|
|
277
|
+
}) => {
|
|
278
|
+
const c = useVariantColors();
|
|
279
|
+
const form = useVariantForm(options, values, config);
|
|
280
|
+
const s = form.summary;
|
|
281
|
+
const showDesc = form.showDescriptions;
|
|
282
|
+
|
|
283
|
+
const allRows = form.visibleGroups.flatMap((g) => g.rows);
|
|
284
|
+
const unmetGroups = Object.values(form.requiredGroups).filter((g) => !g.satisfied);
|
|
285
|
+
const unmetMembers = new Set<string>();
|
|
286
|
+
unmetGroups.forEach((g) => g.members.forEach((m) => unmetMembers.add(m)));
|
|
287
|
+
|
|
288
|
+
// Attention = invalid + individually-required-todo (NOT one-of members, which
|
|
289
|
+
// show as their cluster) + the unmet one-of clusters.
|
|
290
|
+
const attentionRows = allRows.filter(
|
|
291
|
+
(r) => (r.status === 'invalid' || r.status === 'todo') && !unmetMembers.has(r.name)
|
|
292
|
+
);
|
|
293
|
+
const attentionCount = attentionRows.length + unmetGroups.length;
|
|
294
|
+
const skip = new Set<string>(attentionRows.map((r) => r.name));
|
|
295
|
+
unmetMembers.forEach((n) => skip.add(n));
|
|
296
|
+
|
|
297
|
+
const setCount = allRows.filter((r) => r.status === 'set' && !skip.has(r.name)).length;
|
|
298
|
+
const optionalCount = allRows.filter((r) => r.status === 'unset' && !skip.has(r.name)).length;
|
|
299
|
+
|
|
300
|
+
// Group the attention items by schema group (placing each one-of cluster under
|
|
301
|
+
// its members' group) so the attention box reads like the Set / Optional boxes.
|
|
302
|
+
const clusterGroupName = (members: string[]) =>
|
|
303
|
+
form.visibleGroups.find((g) => g.rows.some((r) => r.name === members[0]))?.name;
|
|
304
|
+
const attnGroups = form.visibleGroups
|
|
305
|
+
.map((g) => ({
|
|
306
|
+
g,
|
|
307
|
+
rows: g.rows.filter(
|
|
308
|
+
(r) => (r.status === 'invalid' || r.status === 'todo') && !unmetMembers.has(r.name)
|
|
309
|
+
),
|
|
310
|
+
clusters: unmetGroups.filter((rg) => clusterGroupName(rg.members) === g.name),
|
|
311
|
+
}))
|
|
312
|
+
.filter((x) => x.rows.length || x.clusters.length);
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<Wrap $hover={c.hover} $faint={c.faint}>
|
|
316
|
+
{/* Cards-style header */}
|
|
317
|
+
<div style={{ display: 'flex', flexFlow: 'column', gap: 10 }}>
|
|
318
|
+
<div style={{ display: 'flex', alignItems: 'baseline', gap: 12, flexWrap: 'wrap' }}>
|
|
319
|
+
<ReqoreP size='big' effect={{ weight: 'bold' }}>
|
|
320
|
+
{s.pct}% complete
|
|
321
|
+
</ReqoreP>
|
|
322
|
+
<span style={{ color: c.muted, fontSize: 12 }}>
|
|
323
|
+
{s.set}/{s.total} set
|
|
324
|
+
</span>
|
|
325
|
+
{s.attention ?
|
|
326
|
+
<button
|
|
327
|
+
type='button'
|
|
328
|
+
onClick={form.toggleAttention}
|
|
329
|
+
style={{
|
|
330
|
+
background: 'none',
|
|
331
|
+
border: 'none',
|
|
332
|
+
cursor: 'pointer',
|
|
333
|
+
color: c.warning,
|
|
334
|
+
fontSize: 12,
|
|
335
|
+
textDecoration: 'underline',
|
|
336
|
+
textUnderlineOffset: 3,
|
|
337
|
+
}}
|
|
338
|
+
>
|
|
339
|
+
{form.filter === 'attention' ? '← show all' : `${s.attention} need attention →`}
|
|
340
|
+
</button>
|
|
341
|
+
: null}
|
|
342
|
+
</div>
|
|
343
|
+
<div style={{ height: 4, borderRadius: 2, background: c.line, overflow: 'hidden', display: 'flex' }}>
|
|
344
|
+
<div style={{ width: `${(s.set / s.total) * 100}%`, background: c.success }} />
|
|
345
|
+
<div style={{ width: `${(s.attention / s.total) * 100}%`, background: c.warning }} />
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
<VariantToolbar form={form} />
|
|
349
|
+
|
|
350
|
+
{/* The signature: Needs attention / Set / Optional expandable boxes —
|
|
351
|
+
attention items grouped by schema group, like the other boxes. */}
|
|
352
|
+
<Section title='Needs attention' intent={c.warning} count={attentionCount}>
|
|
353
|
+
{attnGroups.map(({ g, rows, clusters }) => (
|
|
354
|
+
<div key={g.name}>
|
|
355
|
+
{attnGroups.length > 1 ?
|
|
356
|
+
<div className='vf-grouplabel' style={{ color: c.faint }}>
|
|
357
|
+
{g.label}
|
|
358
|
+
</div>
|
|
359
|
+
: null}
|
|
360
|
+
{clusters.map((cl) => (
|
|
361
|
+
<RequiredCluster
|
|
362
|
+
key={cl.key}
|
|
363
|
+
rows={allRows.filter((r) => cl.members.includes(r.name))}
|
|
364
|
+
form={form}
|
|
365
|
+
showDesc={showDesc}
|
|
366
|
+
/>
|
|
367
|
+
))}
|
|
368
|
+
{rows.map((r) => <Row key={r.name} r={r} form={form} showDesc={showDesc} />)}
|
|
369
|
+
</div>
|
|
370
|
+
))}
|
|
371
|
+
</Section>
|
|
372
|
+
|
|
373
|
+
<Section title='Set' intent={c.success} count={setCount}>
|
|
374
|
+
<GroupedRows groups={form.visibleGroups} status='set' skip={skip} form={form} showDesc={showDesc} />
|
|
375
|
+
</Section>
|
|
376
|
+
|
|
377
|
+
<Section title='Optional — not set' intent={c.muted} count={optionalCount} defaultOpen={false}>
|
|
378
|
+
<GroupedRows groups={form.visibleGroups} status='unset' skip={skip} form={form} showDesc={showDesc} />
|
|
379
|
+
</Section>
|
|
380
|
+
</Wrap>
|
|
381
|
+
);
|
|
382
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VARIANT 4 — "Minimal" (Notion / Linear inspired)
|
|
3
|
+
*
|
|
4
|
+
* Direction: the lightest possible treatment. No surfaces, no boxes, no group
|
|
5
|
+
* panels — just a quiet `label · value` list with a hairline status tick on the
|
|
6
|
+
* left edge. TAP a row to reveal its description and edit it inline — NO hover
|
|
7
|
+
* dependency, so it behaves identically on phone. Densest of the four.
|
|
8
|
+
*/
|
|
9
|
+
import { ReqoreIcon, ReqoreP } from '@qoretechnologies/reqore';
|
|
10
|
+
import { IQorusFormField, IQorusFormSchema } from '@qoretechnologies/ts-toolkit';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import styled from 'styled-components';
|
|
13
|
+
import {
|
|
14
|
+
InlineEdit,
|
|
15
|
+
STATUS_COLOR,
|
|
16
|
+
ValueView,
|
|
17
|
+
VariantToolbar,
|
|
18
|
+
useVariantColors,
|
|
19
|
+
useVariantForm,
|
|
20
|
+
} from './variantParts';
|
|
21
|
+
|
|
22
|
+
const Wrap = styled.div<{ $hover: string; $line: string; $faint: string }>`
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-flow: column;
|
|
25
|
+
gap: 4px;
|
|
26
|
+
font-size: 13px;
|
|
27
|
+
|
|
28
|
+
.vm-group {
|
|
29
|
+
font-size: 11px;
|
|
30
|
+
letter-spacing: 1px;
|
|
31
|
+
text-transform: uppercase;
|
|
32
|
+
padding: 16px 0 6px 14px;
|
|
33
|
+
}
|
|
34
|
+
.vm-row {
|
|
35
|
+
display: grid;
|
|
36
|
+
grid-template-columns: minmax(180px, 320px) minmax(0, 1fr);
|
|
37
|
+
gap: 14px;
|
|
38
|
+
align-items: baseline;
|
|
39
|
+
padding: 7px 12px 7px 14px;
|
|
40
|
+
border-left: 2px solid transparent;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
border-radius: 0 6px 6px 0;
|
|
43
|
+
}
|
|
44
|
+
.vm-row[aria-expanded='true'] {
|
|
45
|
+
background: ${({ $hover }) => $hover};
|
|
46
|
+
}
|
|
47
|
+
@media (hover: hover) {
|
|
48
|
+
.vm-row:hover {
|
|
49
|
+
background: ${({ $hover }) => $hover};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
.vm-label {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 6px;
|
|
56
|
+
min-width: 0;
|
|
57
|
+
}
|
|
58
|
+
.vm-detail {
|
|
59
|
+
grid-column: 1 / -1;
|
|
60
|
+
color: ${({ $faint }) => $faint};
|
|
61
|
+
padding: 4px 0 2px;
|
|
62
|
+
font-size: 12px;
|
|
63
|
+
line-height: 1.5;
|
|
64
|
+
}
|
|
65
|
+
@media (max-width: 620px) {
|
|
66
|
+
.vm-row {
|
|
67
|
+
grid-template-columns: 1fr;
|
|
68
|
+
gap: 2px;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
export const VariantMinimal = ({
|
|
74
|
+
options,
|
|
75
|
+
values,
|
|
76
|
+
}: {
|
|
77
|
+
options: IQorusFormSchema;
|
|
78
|
+
values: Record<string, IQorusFormField>;
|
|
79
|
+
}) => {
|
|
80
|
+
const c = useVariantColors();
|
|
81
|
+
const form = useVariantForm(options, values);
|
|
82
|
+
const s = form.summary;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Wrap $hover={c.hover} $line={c.line} $faint={c.faint}>
|
|
86
|
+
<div
|
|
87
|
+
style={{
|
|
88
|
+
display: 'flex',
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
gap: 10,
|
|
91
|
+
padding: '0 12px 10px',
|
|
92
|
+
borderBottom: `1px solid ${c.line}`,
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<span style={{ width: 8, height: 8, borderRadius: '50%', background: s.attention ? c.warning : c.success }} />
|
|
96
|
+
<ReqoreP effect={{ weight: 'bold' }}>{s.pct}%</ReqoreP>
|
|
97
|
+
<span style={{ color: c.muted, fontSize: 12 }}>{s.set}/{s.total} set</span>
|
|
98
|
+
{s.attention ?
|
|
99
|
+
<button
|
|
100
|
+
type='button'
|
|
101
|
+
onClick={form.toggleAttention}
|
|
102
|
+
style={{
|
|
103
|
+
background: 'none',
|
|
104
|
+
border: 'none',
|
|
105
|
+
cursor: 'pointer',
|
|
106
|
+
color: c.warning,
|
|
107
|
+
fontSize: 12,
|
|
108
|
+
textDecoration: 'underline',
|
|
109
|
+
textUnderlineOffset: 3,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
{form.filter === 'attention' ? '← all' : `${s.attention} to resolve →`}
|
|
113
|
+
</button>
|
|
114
|
+
: null}
|
|
115
|
+
</div>
|
|
116
|
+
<VariantToolbar form={form} />
|
|
117
|
+
|
|
118
|
+
{form.visibleGroups.map((g) => (
|
|
119
|
+
<React.Fragment key={g.name}>
|
|
120
|
+
<div className='vm-group' style={{ color: c.faint }}>
|
|
121
|
+
{g.label}
|
|
122
|
+
</div>
|
|
123
|
+
{g.rows.map((r) => {
|
|
124
|
+
const editing = form.editing === r.name;
|
|
125
|
+
const hasDetail = !!(r.shortDesc || r.longDesc);
|
|
126
|
+
return (
|
|
127
|
+
<div
|
|
128
|
+
key={r.name}
|
|
129
|
+
className='vm-row'
|
|
130
|
+
role='button'
|
|
131
|
+
tabIndex={0}
|
|
132
|
+
aria-expanded={editing}
|
|
133
|
+
style={{ borderLeftColor: r.status === 'set' || r.status === 'unset' ? 'transparent' : STATUS_COLOR(r.status, c) }}
|
|
134
|
+
onClick={() => !r.readOnly && form.startEdit(r.name)}
|
|
135
|
+
>
|
|
136
|
+
<span className='vm-label' style={{ color: c.text, fontWeight: 500 }}>
|
|
137
|
+
{r.label}
|
|
138
|
+
{r.required ?
|
|
139
|
+
<ReqoreIcon icon='Asterisk' size='8px' style={{ color: c.danger }} />
|
|
140
|
+
: null}
|
|
141
|
+
</span>
|
|
142
|
+
<span style={{ color: c.muted, minWidth: 0, display: 'inline-flex', alignItems: 'baseline', gap: 8 }}>
|
|
143
|
+
<ValueView value={r.value} />
|
|
144
|
+
{r.status === 'invalid' || r.status === 'todo' ?
|
|
145
|
+
<span style={{ color: STATUS_COLOR(r.status, c), fontSize: 12 }}>· {r.reason}</span>
|
|
146
|
+
: null}
|
|
147
|
+
</span>
|
|
148
|
+
{editing ?
|
|
149
|
+
<>
|
|
150
|
+
{hasDetail ?
|
|
151
|
+
<div className='vm-detail'>
|
|
152
|
+
{r.shortDesc ? <div>{r.shortDesc}</div> : null}
|
|
153
|
+
{r.longDesc ?
|
|
154
|
+
<div style={{ marginTop: r.shortDesc ? 4 : 0 }}>
|
|
155
|
+
{r.longDesc.replace(/[#`*]/g, '')}
|
|
156
|
+
</div>
|
|
157
|
+
: null}
|
|
158
|
+
</div>
|
|
159
|
+
: null}
|
|
160
|
+
<InlineEdit row={r} onDone={form.stopEdit} />
|
|
161
|
+
</>
|
|
162
|
+
: null}
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
})}
|
|
166
|
+
</React.Fragment>
|
|
167
|
+
))}
|
|
168
|
+
</Wrap>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A richer, realistic demo schema for the Focus-Pro variant — exercises the
|
|
3
|
+
* features the basic fixture lacks: NAMED groups, a one-of REQUIRED GROUP,
|
|
4
|
+
* a complex nested hash (Show-more preview), allowed-values, files/lists, plus
|
|
5
|
+
* a mix of set / invalid / required-unset / optional states. Modelled loosely
|
|
6
|
+
* on a Qorus datasource connection config.
|
|
7
|
+
*/
|
|
8
|
+
import { IQorusFormField, IQorusFormSchema } from '@qoretechnologies/ts-toolkit';
|
|
9
|
+
|
|
10
|
+
export const getFocusDemoOptions = (): IQorusFormSchema =>
|
|
11
|
+
({
|
|
12
|
+
// --- Connection -------------------------------------------------------
|
|
13
|
+
url: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
ui_type: 'string',
|
|
16
|
+
display_name: 'Connection URL',
|
|
17
|
+
short_desc: 'The full datasource URL (driver://user@host:port/db).',
|
|
18
|
+
desc: 'Accepts any Qore datasource URL. Templates like `$config:db-url` are supported.',
|
|
19
|
+
group: 'connection',
|
|
20
|
+
required: true,
|
|
21
|
+
supports_templates: true,
|
|
22
|
+
},
|
|
23
|
+
port: {
|
|
24
|
+
type: 'int',
|
|
25
|
+
ui_type: 'number',
|
|
26
|
+
display_name: 'Port',
|
|
27
|
+
short_desc: 'TCP port of the database server.',
|
|
28
|
+
group: 'connection',
|
|
29
|
+
default_value: { type: 'int', value: 5432 },
|
|
30
|
+
},
|
|
31
|
+
database: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
ui_type: 'string',
|
|
34
|
+
display_name: 'Database name',
|
|
35
|
+
short_desc: 'Name of the schema/database to connect to.',
|
|
36
|
+
group: 'connection',
|
|
37
|
+
required: true,
|
|
38
|
+
},
|
|
39
|
+
// --- Authentication (one-of required group) ---------------------------
|
|
40
|
+
token: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
ui_type: 'string',
|
|
43
|
+
display_name: 'API token',
|
|
44
|
+
short_desc: 'Bearer token — use this OR a username/password.',
|
|
45
|
+
group: 'authentication',
|
|
46
|
+
required_groups: ['auth'],
|
|
47
|
+
supports_templates: true,
|
|
48
|
+
},
|
|
49
|
+
username: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
ui_type: 'string',
|
|
52
|
+
display_name: 'Username',
|
|
53
|
+
short_desc: 'Database user — paired with a password.',
|
|
54
|
+
group: 'authentication',
|
|
55
|
+
required_groups: ['auth'],
|
|
56
|
+
},
|
|
57
|
+
password: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
ui_type: 'string',
|
|
60
|
+
display_name: 'Password',
|
|
61
|
+
short_desc: 'Database password.',
|
|
62
|
+
group: 'authentication',
|
|
63
|
+
required_groups: ['auth'],
|
|
64
|
+
supports_templates: true,
|
|
65
|
+
},
|
|
66
|
+
// --- Advanced ---------------------------------------------------------
|
|
67
|
+
options: {
|
|
68
|
+
type: 'hash',
|
|
69
|
+
ui_type: 'hash',
|
|
70
|
+
display_name: 'Driver options',
|
|
71
|
+
short_desc: 'Extra key/value options passed to the driver.',
|
|
72
|
+
group: 'advanced',
|
|
73
|
+
},
|
|
74
|
+
poolSize: {
|
|
75
|
+
type: 'int',
|
|
76
|
+
ui_type: 'number',
|
|
77
|
+
display_name: 'Pool size',
|
|
78
|
+
short_desc: 'Max simultaneous connections.',
|
|
79
|
+
group: 'advanced',
|
|
80
|
+
default_value: { type: 'int', value: 10 },
|
|
81
|
+
},
|
|
82
|
+
timeout: {
|
|
83
|
+
type: 'int',
|
|
84
|
+
ui_type: 'number',
|
|
85
|
+
display_name: 'Timeout (s)',
|
|
86
|
+
short_desc: 'Connection timeout in seconds.',
|
|
87
|
+
group: 'advanced',
|
|
88
|
+
},
|
|
89
|
+
logLevel: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
ui_type: 'string',
|
|
92
|
+
display_name: 'Log level',
|
|
93
|
+
short_desc: 'Verbosity of the connection log.',
|
|
94
|
+
group: 'advanced',
|
|
95
|
+
allowed_values: [
|
|
96
|
+
{ value: 'error', display_name: 'Error' },
|
|
97
|
+
{ value: 'info', display_name: 'Info' },
|
|
98
|
+
{ value: 'debug', display_name: 'Debug' },
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
// --- Optional (non-preselected — render in the Optional group) ---------
|
|
102
|
+
sslCert: {
|
|
103
|
+
type: 'file',
|
|
104
|
+
ui_type: 'file',
|
|
105
|
+
display_name: 'SSL certificate',
|
|
106
|
+
short_desc: 'Client certificate for mutual TLS.',
|
|
107
|
+
},
|
|
108
|
+
tags: {
|
|
109
|
+
type: 'list',
|
|
110
|
+
ui_type: 'list',
|
|
111
|
+
display_name: 'Tags',
|
|
112
|
+
short_desc: 'Free-form labels for this connection.',
|
|
113
|
+
},
|
|
114
|
+
}) as unknown as IQorusFormSchema;
|
|
115
|
+
|
|
116
|
+
export const focusDemoValue: Record<string, IQorusFormField> = {
|
|
117
|
+
url: { type: 'string', value: 'pgsql://hq.qoretechnologies.com:5432/omq' },
|
|
118
|
+
// database: intentionally unset → required to-do
|
|
119
|
+
// auth group: intentionally none set → one-of unsatisfied → attention
|
|
120
|
+
options: {
|
|
121
|
+
type: 'hash',
|
|
122
|
+
value: {
|
|
123
|
+
sslmode: { type: 'string', value: 'require' },
|
|
124
|
+
application_name: { type: 'string', value: 'qorus-ide' },
|
|
125
|
+
connect_timeout: { type: 'int', value: 8 },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
logLevel: { type: 'string', value: 'info' },
|
|
129
|
+
// timeout: invalid (entered 0)
|
|
130
|
+
timeout: { type: 'int', value: 0 as any },
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Demo-only invalid reasons (in the engine these come from the validity pass).
|
|
134
|
+
export const focusDemoInvalid: Record<string, string> = {
|
|
135
|
+
timeout: 'Must be greater than 0',
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Display labels for the named groups.
|
|
139
|
+
export const focusDemoGroupLabels: Record<string, string> = {
|
|
140
|
+
connection: 'Connection',
|
|
141
|
+
authentication: 'Authentication',
|
|
142
|
+
advanced: 'Advanced',
|
|
143
|
+
optional: 'Optional',
|
|
144
|
+
general: 'General',
|
|
145
|
+
};
|