@visns-studio/visns-components 6.24.4 → 6.25.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 +4 -2
- package/src/components/Autocomplete.jsx +189 -119
- package/src/components/DataGrid.jsx +472 -31
- package/src/components/Navigation.jsx +475 -51
- package/src/components/auth/ClientAuthFrame.jsx +5 -0
- package/src/components/auth/ClientAuthScreen.jsx +29 -0
- package/src/components/columns/ColumnRenderers.jsx +3 -46
- package/src/components/columns/StackedRow.jsx +186 -0
- package/src/components/controls/DataGridSearch.jsx +110 -2
- package/src/components/controls/DataGridSortSheet.jsx +155 -0
- package/src/components/generic/GenericAuth.jsx +50 -18
- package/src/components/generic/GenericDashboard.jsx +20 -1
- package/src/components/generic/GenericDetail.jsx +446 -259
- package/src/components/mapboxSearchBox.js +640 -0
- package/src/components/navBadges.js +63 -1
- package/src/components/navDrawer.js +147 -0
- package/src/components/sms/SmsThreadPanel.jsx +34 -6
- package/src/components/sms/smsHelpers.js +15 -0
- package/src/components/styles/ClientAuth.module.scss +39 -0
- package/src/components/styles/DataGrid.module.scss +158 -5
- package/src/components/styles/Field.module.scss +52 -1
- package/src/components/styles/Form.module.scss +82 -0
- package/src/components/styles/GenericClientPortal.module.scss +72 -20
- package/src/components/styles/GenericDashboard.module.scss +50 -0
- package/src/components/styles/GenericDetail.module.scss +63 -1
- package/src/components/styles/GenericDynamic.module.scss +23 -0
- package/src/components/styles/GenericFormBuilder.module.scss +11 -0
- package/src/components/styles/GenericIndex.module.scss +6 -1
- package/src/components/styles/Navigation.module.scss +460 -7
- package/src/components/styles/Sms.module.scss +92 -0
- package/src/components/styles/StackedRow.module.scss +182 -0
- package/src/components/styles/TicketConversation.module.scss +76 -0
- package/src/components/styles/Vault.module.scss +192 -0
- package/src/components/styles/density.css +10 -0
- package/src/components/styles/global-datagrid.css +163 -0
- package/src/components/styles/global.css +20 -0
- package/src/components/tickets/TicketConversation.jsx +13 -8
- package/src/components/utils/ConfirmDialog.js +22 -3
- package/src/components/utils/cardLayout.js +666 -0
- package/src/components/utils/contactChannels.js +130 -0
- package/src/components/utils/editPlacement.js +95 -0
- package/src/components/utils/useDensity.js +303 -7
- package/src/index.js +32 -0
|
@@ -68,6 +68,10 @@ import {
|
|
|
68
68
|
isBlankValue,
|
|
69
69
|
overviewFieldLabel,
|
|
70
70
|
} from '../utils/displayValue';
|
|
71
|
+
import {
|
|
72
|
+
isHeaderPlacement,
|
|
73
|
+
resolveEditPlacement,
|
|
74
|
+
} from '../utils/editPlacement';
|
|
71
75
|
import OcrTemplateEnhanced from './OcrTemplateEnhanced';
|
|
72
76
|
|
|
73
77
|
import 'react-big-calendar/lib/css/react-big-calendar.css';
|
|
@@ -239,6 +243,19 @@ function GenericDetail({
|
|
|
239
243
|
urlParam = 'dataId',
|
|
240
244
|
userProfile,
|
|
241
245
|
actions = null,
|
|
246
|
+
/**
|
|
247
|
+
* Where the record's action cluster sits — `'floating'` (the default, the
|
|
248
|
+
* bottom-right bar this library has always drawn) or `'header'` (inline on
|
|
249
|
+
* the right of the title row).
|
|
250
|
+
*
|
|
251
|
+
* This is an OVERRIDE, not the primary switch: the value normally comes
|
|
252
|
+
* from the view config as `setting.editPlacement`, so a page mounted by a
|
|
253
|
+
* bare `<GenericDetail setting={…} />` — which is most of them — can ask
|
|
254
|
+
* for it without a host editing JSX. See `utils/editPlacement.js` for the
|
|
255
|
+
* whole argument. Given here it wins, because a host that has already
|
|
256
|
+
* wrapped this component for one page knows something the config does not.
|
|
257
|
+
*/
|
|
258
|
+
editPlacement = null,
|
|
242
259
|
/**
|
|
243
260
|
* Host-supplied components for `type: "custom"` tabs, keyed by the tab's
|
|
244
261
|
* `renderer`.
|
|
@@ -264,6 +281,15 @@ function GenericDetail({
|
|
|
264
281
|
|
|
265
282
|
const { filters, page, stages, tabs } = setting;
|
|
266
283
|
|
|
284
|
+
/**
|
|
285
|
+
* `'floating'` unless this page's config (or its host) asked otherwise.
|
|
286
|
+
* Resolved once per render, from one implementation, so the three places
|
|
287
|
+
* that draw a cluster and the title row that may receive it cannot
|
|
288
|
+
* disagree about where the buttons are meant to be.
|
|
289
|
+
*/
|
|
290
|
+
const actionPlacement = resolveEditPlacement(setting, editPlacement);
|
|
291
|
+
const drawActionsInHeader = isHeaderPlacement(actionPlacement);
|
|
292
|
+
|
|
267
293
|
/** Fileupload states */
|
|
268
294
|
const [files, setFiles] = useState([]);
|
|
269
295
|
|
|
@@ -3101,192 +3127,10 @@ function GenericDetail({
|
|
|
3101
3127
|
</div>
|
|
3102
3128
|
)}
|
|
3103
3129
|
|
|
3104
|
-
{
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
<button
|
|
3109
|
-
className={styles.btn}
|
|
3110
|
-
title={
|
|
3111
|
-
activeTabConfig.form
|
|
3112
|
-
.editLabel || 'Edit'
|
|
3113
|
-
}
|
|
3114
|
-
aria-label={
|
|
3115
|
-
activeTabConfig.form
|
|
3116
|
-
.editLabel || 'Edit'
|
|
3117
|
-
}
|
|
3118
|
-
onClick={() => {
|
|
3119
|
-
if (
|
|
3120
|
-
activeTabConfig.form
|
|
3121
|
-
.inPlaceEdit
|
|
3122
|
-
) {
|
|
3123
|
-
startInPlaceEditing(
|
|
3124
|
-
activeTabConfig.id
|
|
3125
|
-
);
|
|
3126
|
-
} else {
|
|
3127
|
-
modalOpen(
|
|
3128
|
-
'update',
|
|
3129
|
-
routeParams[
|
|
3130
|
-
urlParam
|
|
3131
|
-
]
|
|
3132
|
-
);
|
|
3133
|
-
}
|
|
3134
|
-
}}
|
|
3135
|
-
>
|
|
3136
|
-
<Pencil
|
|
3137
|
-
size={18}
|
|
3138
|
-
strokeWidth={2.25}
|
|
3139
|
-
/>
|
|
3140
|
-
<span>
|
|
3141
|
-
{activeTabConfig.form
|
|
3142
|
-
.editLabel ||
|
|
3143
|
-
'Edit'}
|
|
3144
|
-
</span>
|
|
3145
|
-
</button>
|
|
3146
|
-
) : (
|
|
3147
|
-
editingTabId ===
|
|
3148
|
-
activeTabConfig.id && (
|
|
3149
|
-
<>
|
|
3150
|
-
{/* Cancel is the way back out of editing, not a
|
|
3151
|
-
destructive act — it was painted in error red,
|
|
3152
|
-
the loudest thing on the page, next to the
|
|
3153
|
-
Save it competes with. Ghost secondary. */}
|
|
3154
|
-
<button
|
|
3155
|
-
className={
|
|
3156
|
-
styles.btnSecondary
|
|
3157
|
-
}
|
|
3158
|
-
onClick={
|
|
3159
|
-
cancelInPlaceEditing
|
|
3160
|
-
}
|
|
3161
|
-
>
|
|
3162
|
-
Cancel
|
|
3163
|
-
</button>
|
|
3164
|
-
<button
|
|
3165
|
-
className={
|
|
3166
|
-
styles.btn
|
|
3167
|
-
}
|
|
3168
|
-
onClick={() => {
|
|
3169
|
-
// Find the form's save button and click it
|
|
3170
|
-
const saveButton =
|
|
3171
|
-
document.querySelector(
|
|
3172
|
-
`.inPlaceEdit .${styles.saveBtn}`
|
|
3173
|
-
);
|
|
3174
|
-
if (
|
|
3175
|
-
saveButton
|
|
3176
|
-
) {
|
|
3177
|
-
saveButton.click();
|
|
3178
|
-
} else {
|
|
3179
|
-
// Fallback to any button with data-type="save"
|
|
3180
|
-
const fallbackButton =
|
|
3181
|
-
document.querySelector(
|
|
3182
|
-
`.inPlaceEdit button[data-type="save"]`
|
|
3183
|
-
);
|
|
3184
|
-
if (
|
|
3185
|
-
fallbackButton
|
|
3186
|
-
) {
|
|
3187
|
-
fallbackButton.click();
|
|
3188
|
-
}
|
|
3189
|
-
}
|
|
3190
|
-
|
|
3191
|
-
// Reset the editing state
|
|
3192
|
-
setInPlaceEditing(
|
|
3193
|
-
false
|
|
3194
|
-
);
|
|
3195
|
-
setEditingTabId(
|
|
3196
|
-
null
|
|
3197
|
-
);
|
|
3198
|
-
|
|
3199
|
-
// Reload data
|
|
3200
|
-
handleReload();
|
|
3201
|
-
}}
|
|
3202
|
-
>
|
|
3203
|
-
Save
|
|
3204
|
-
</button>
|
|
3205
|
-
</>
|
|
3206
|
-
)
|
|
3207
|
-
)}
|
|
3208
|
-
|
|
3209
|
-
{/* Check if sections contain "ocrTemplate" type and render the "Save" button under the same polActions */}
|
|
3210
|
-
{activeTabConfig.sections &&
|
|
3211
|
-
activeTabConfig.sections
|
|
3212
|
-
.length > 0 &&
|
|
3213
|
-
activeTabConfig.sections.map(
|
|
3214
|
-
(section, sectionIndex) =>
|
|
3215
|
-
section.content &&
|
|
3216
|
-
section.content.some(
|
|
3217
|
-
(item) =>
|
|
3218
|
-
item.type ===
|
|
3219
|
-
'ocrTemplate'
|
|
3220
|
-
) && (
|
|
3221
|
-
<button
|
|
3222
|
-
key={`save-button-${sectionIndex}`}
|
|
3223
|
-
className={
|
|
3224
|
-
styles.btn
|
|
3225
|
-
}
|
|
3226
|
-
onClick={() => {
|
|
3227
|
-
// Handle the specific "ocrTemplate" item
|
|
3228
|
-
const ocrItem =
|
|
3229
|
-
section.content.find(
|
|
3230
|
-
(
|
|
3231
|
-
item
|
|
3232
|
-
) =>
|
|
3233
|
-
item.type ===
|
|
3234
|
-
'ocrTemplate'
|
|
3235
|
-
);
|
|
3236
|
-
handleOcrTemplateSave(
|
|
3237
|
-
ocrItem.id
|
|
3238
|
-
); // Now you can safely use ocrItem.id
|
|
3239
|
-
}}
|
|
3240
|
-
>
|
|
3241
|
-
Save
|
|
3242
|
-
</button>
|
|
3243
|
-
)
|
|
3244
|
-
)}
|
|
3245
|
-
|
|
3246
|
-
{/* Check if export exists and render the "Export" button under the same polActions */}
|
|
3247
|
-
{activeTabConfig.export && (
|
|
3248
|
-
<button
|
|
3249
|
-
className={styles.btn}
|
|
3250
|
-
onClick={() =>
|
|
3251
|
-
handleExport(
|
|
3252
|
-
activeTabConfig.export
|
|
3253
|
-
)
|
|
3254
|
-
}
|
|
3255
|
-
>
|
|
3256
|
-
{activeTabConfig.export
|
|
3257
|
-
.label || 'Export'}
|
|
3258
|
-
</button>
|
|
3259
|
-
)}
|
|
3260
|
-
|
|
3261
|
-
{activeTabConfig.customAction && (
|
|
3262
|
-
<button
|
|
3263
|
-
className={styles.btn}
|
|
3264
|
-
onClick={() =>
|
|
3265
|
-
handleCustomAction(
|
|
3266
|
-
activeTabConfig.customAction
|
|
3267
|
-
)
|
|
3268
|
-
}
|
|
3269
|
-
>
|
|
3270
|
-
{
|
|
3271
|
-
activeTabConfig
|
|
3272
|
-
.customAction.label
|
|
3273
|
-
}
|
|
3274
|
-
</button>
|
|
3275
|
-
)}
|
|
3276
|
-
|
|
3277
|
-
{activeTabConfig.customActions && Array.isArray(activeTabConfig.customActions) && activeTabConfig.customActions.map((customAction, index) => (
|
|
3278
|
-
<button
|
|
3279
|
-
key={index}
|
|
3280
|
-
className={styles.btn}
|
|
3281
|
-
onClick={() =>
|
|
3282
|
-
handleCustomAction(customAction)
|
|
3283
|
-
}
|
|
3284
|
-
>
|
|
3285
|
-
{getActionLabel(customAction)}
|
|
3286
|
-
</button>
|
|
3287
|
-
))}
|
|
3288
|
-
</div>
|
|
3289
|
-
)}
|
|
3130
|
+
{!drawActionsInHeader &&
|
|
3131
|
+
actionCluster(
|
|
3132
|
+
renderOverviewActions(activeTabConfig)
|
|
3133
|
+
)}
|
|
3290
3134
|
</>
|
|
3291
3135
|
);
|
|
3292
3136
|
case 'qrCode':
|
|
@@ -3297,12 +3141,14 @@ function GenericDetail({
|
|
|
3297
3141
|
/>
|
|
3298
3142
|
);
|
|
3299
3143
|
case 'table':
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3144
|
+
// ajaxSetting and columns are what a grid IS;
|
|
3145
|
+
// form (create/edit) and settings (row actions)
|
|
3146
|
+
// are what a grid can DO, and a read-only tab
|
|
3147
|
+
// legitimately has neither. Requiring all four
|
|
3148
|
+
// rendered an empty strip with no fetch and no
|
|
3149
|
+
// error — a config mistake that looked like a
|
|
3150
|
+
// data problem, twice, in the field.
|
|
3151
|
+
if (config.ajaxSetting && config.columns) {
|
|
3306
3152
|
const processUrlParams = (obj) => {
|
|
3307
3153
|
if (Array.isArray(obj)) {
|
|
3308
3154
|
return obj.map(processUrlParams);
|
|
@@ -3554,72 +3400,15 @@ function GenericDetail({
|
|
|
3554
3400
|
</div>
|
|
3555
3401
|
</div>
|
|
3556
3402
|
|
|
3557
|
-
{
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
className={styles.btn}
|
|
3562
|
-
onClick={
|
|
3563
|
-
handleCheckboxUpdate
|
|
3564
|
-
}
|
|
3565
|
-
>
|
|
3566
|
-
{activeTabConfig.functions
|
|
3567
|
-
.checkboxUpdate.label ||
|
|
3568
|
-
'Update'}
|
|
3569
|
-
</button>
|
|
3570
|
-
</div>
|
|
3571
|
-
)}
|
|
3572
|
-
|
|
3573
|
-
{(activeTabConfig.functions
|
|
3574
|
-
?.checkboxDelete ||
|
|
3575
|
-
activeTabConfig.customAction ||
|
|
3576
|
-
(activeTabConfig.customActions && activeTabConfig.customActions.length > 0)) && (
|
|
3577
|
-
<div className={styles.polActions}>
|
|
3578
|
-
{activeTabConfig.functions
|
|
3579
|
-
?.checkboxDelete && (
|
|
3580
|
-
<button
|
|
3581
|
-
className={styles.btn}
|
|
3582
|
-
onClick={
|
|
3583
|
-
handleCheckboxDelete
|
|
3584
|
-
}
|
|
3585
|
-
>
|
|
3586
|
-
{activeTabConfig
|
|
3587
|
-
.functions
|
|
3588
|
-
.checkboxDelete
|
|
3589
|
-
.label || 'Delete'}
|
|
3590
|
-
</button>
|
|
3591
|
-
)}
|
|
3592
|
-
|
|
3593
|
-
{activeTabConfig.customAction && (
|
|
3594
|
-
<button
|
|
3595
|
-
className={styles.btn}
|
|
3596
|
-
onClick={() =>
|
|
3597
|
-
handleCustomAction(
|
|
3598
|
-
activeTabConfig.customAction
|
|
3599
|
-
)
|
|
3600
|
-
}
|
|
3601
|
-
>
|
|
3602
|
-
{
|
|
3603
|
-
activeTabConfig
|
|
3604
|
-
.customAction
|
|
3605
|
-
.label
|
|
3606
|
-
}
|
|
3607
|
-
</button>
|
|
3608
|
-
)}
|
|
3403
|
+
{!drawActionsInHeader &&
|
|
3404
|
+
actionCluster(
|
|
3405
|
+
renderTableUpdateAction(activeTabConfig)
|
|
3406
|
+
)}
|
|
3609
3407
|
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
onClick={() =>
|
|
3615
|
-
handleCustomAction(customAction)
|
|
3616
|
-
}
|
|
3617
|
-
>
|
|
3618
|
-
{getActionLabel(customAction)}
|
|
3619
|
-
</button>
|
|
3620
|
-
))}
|
|
3621
|
-
</div>
|
|
3622
|
-
)}
|
|
3408
|
+
{!drawActionsInHeader &&
|
|
3409
|
+
actionCluster(
|
|
3410
|
+
renderTableBulkActions(activeTabConfig)
|
|
3411
|
+
)}
|
|
3623
3412
|
</>
|
|
3624
3413
|
);
|
|
3625
3414
|
} else {
|
|
@@ -3846,7 +3635,16 @@ function GenericDetail({
|
|
|
3846
3635
|
),
|
|
3847
3636
|
};
|
|
3848
3637
|
|
|
3849
|
-
|
|
3638
|
+
// A table tab is allowed to have NO form — a
|
|
3639
|
+
// read-only listing whose rows link elsewhere
|
|
3640
|
+
// (a client's Projects tab is one). This
|
|
3641
|
+
// branch assumed every filtered table carried
|
|
3642
|
+
// form.fields and crashed the whole page on
|
|
3643
|
+
// the first one that did not.
|
|
3644
|
+
const updatedForm = !activeTabConfig.form
|
|
3645
|
+
?.fields
|
|
3646
|
+
? activeTabConfig.form
|
|
3647
|
+
: {
|
|
3850
3648
|
...activeTabConfig.form,
|
|
3851
3649
|
fields: activeTabConfig.form.fields.map(
|
|
3852
3650
|
(field) => {
|
|
@@ -4023,6 +3821,328 @@ function GenericDetail({
|
|
|
4023
3821
|
setEditingTabId(null);
|
|
4024
3822
|
};
|
|
4025
3823
|
|
|
3824
|
+
/* ---------------------------------------------------------------------
|
|
3825
|
+
The action clusters, and why they are functions rather than JSX in
|
|
3826
|
+
place.
|
|
3827
|
+
|
|
3828
|
+
`editPlacement` draws these EITHER in the title row or pinned
|
|
3829
|
+
bottom-right, and a placement switch is the classic way to end up with
|
|
3830
|
+
two copies of a button. The Edit button's own logic is
|
|
3831
|
+
`inPlaceEditing`, `editingTabId`, `startInPlaceEditing`, and a Save that
|
|
3832
|
+
reaches into the DOM for `.inPlaceEdit button[data-type="save"]` — copy
|
|
3833
|
+
that into a second branch and one of the two silently stops saving the
|
|
3834
|
+
first time anything about in-place editing changes, on whichever
|
|
3835
|
+
placement nobody was looking at.
|
|
3836
|
+
|
|
3837
|
+
So each cluster is built exactly once, here, and the render decides
|
|
3838
|
+
only WHERE to hang it. `actionCluster` is the one wrapper: it is the
|
|
3839
|
+
same `<div>` in both placements, carrying a modifier class in the
|
|
3840
|
+
header one — the floating variant keeps its pill, its backdrop and its
|
|
3841
|
+
`:has(> :nth-child(2))` rules untouched.
|
|
3842
|
+
|
|
3843
|
+
They take the tab config as an argument rather than closing over the
|
|
3844
|
+
state copy, because the title row is rendered BEFORE `renderContent()`
|
|
3845
|
+
walks the tabs and would otherwise be a render behind. Same reason the
|
|
3846
|
+
parameter is named `activeTabConfig`: the bodies are the original JSX,
|
|
3847
|
+
moved and not rewritten.
|
|
3848
|
+
--------------------------------------------------------------------- */
|
|
3849
|
+
|
|
3850
|
+
/** The one wrapper. Returns nothing at all when there is nothing to draw. */
|
|
3851
|
+
const actionCluster = (children) =>
|
|
3852
|
+
children ? (
|
|
3853
|
+
<div
|
|
3854
|
+
className={
|
|
3855
|
+
drawActionsInHeader
|
|
3856
|
+
? `${styles.polActions} ${styles.polActionsHeader}`
|
|
3857
|
+
: styles.polActions
|
|
3858
|
+
}
|
|
3859
|
+
>
|
|
3860
|
+
{children}
|
|
3861
|
+
</div>
|
|
3862
|
+
) : null;
|
|
3863
|
+
|
|
3864
|
+
/**
|
|
3865
|
+
* An overview tab's Edit — or, once editing, its Cancel and Save — plus
|
|
3866
|
+
* the OCR save, the export and any custom actions the tab declares.
|
|
3867
|
+
*/
|
|
3868
|
+
const renderOverviewActions = (activeTabConfig) => {
|
|
3869
|
+
if (!activeTabConfig?.form) {
|
|
3870
|
+
return null;
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
return (
|
|
3874
|
+
<>
|
|
3875
|
+
{/* Render the "Edit" button if activeTabConfig.form exists */}
|
|
3876
|
+
{!inPlaceEditing ? (
|
|
3877
|
+
<button
|
|
3878
|
+
className={styles.btn}
|
|
3879
|
+
title={
|
|
3880
|
+
activeTabConfig.form
|
|
3881
|
+
.editLabel || 'Edit'
|
|
3882
|
+
}
|
|
3883
|
+
aria-label={
|
|
3884
|
+
activeTabConfig.form
|
|
3885
|
+
.editLabel || 'Edit'
|
|
3886
|
+
}
|
|
3887
|
+
onClick={() => {
|
|
3888
|
+
if (
|
|
3889
|
+
activeTabConfig.form
|
|
3890
|
+
.inPlaceEdit
|
|
3891
|
+
) {
|
|
3892
|
+
startInPlaceEditing(
|
|
3893
|
+
activeTabConfig.id
|
|
3894
|
+
);
|
|
3895
|
+
} else {
|
|
3896
|
+
modalOpen(
|
|
3897
|
+
'update',
|
|
3898
|
+
routeParams[
|
|
3899
|
+
urlParam
|
|
3900
|
+
]
|
|
3901
|
+
);
|
|
3902
|
+
}
|
|
3903
|
+
}}
|
|
3904
|
+
>
|
|
3905
|
+
<Pencil
|
|
3906
|
+
size={18}
|
|
3907
|
+
strokeWidth={2.25}
|
|
3908
|
+
/>
|
|
3909
|
+
<span>
|
|
3910
|
+
{activeTabConfig.form
|
|
3911
|
+
.editLabel ||
|
|
3912
|
+
'Edit'}
|
|
3913
|
+
</span>
|
|
3914
|
+
</button>
|
|
3915
|
+
) : (
|
|
3916
|
+
editingTabId ===
|
|
3917
|
+
activeTabConfig.id && (
|
|
3918
|
+
<>
|
|
3919
|
+
{/* Cancel is the way back out of editing, not a
|
|
3920
|
+
destructive act — it was painted in error red,
|
|
3921
|
+
the loudest thing on the page, next to the
|
|
3922
|
+
Save it competes with. Ghost secondary. */}
|
|
3923
|
+
<button
|
|
3924
|
+
className={
|
|
3925
|
+
styles.btnSecondary
|
|
3926
|
+
}
|
|
3927
|
+
onClick={
|
|
3928
|
+
cancelInPlaceEditing
|
|
3929
|
+
}
|
|
3930
|
+
>
|
|
3931
|
+
Cancel
|
|
3932
|
+
</button>
|
|
3933
|
+
<button
|
|
3934
|
+
className={
|
|
3935
|
+
styles.btn
|
|
3936
|
+
}
|
|
3937
|
+
onClick={() => {
|
|
3938
|
+
// Find the form's save button and click it
|
|
3939
|
+
const saveButton =
|
|
3940
|
+
document.querySelector(
|
|
3941
|
+
`.inPlaceEdit .${styles.saveBtn}`
|
|
3942
|
+
);
|
|
3943
|
+
if (
|
|
3944
|
+
saveButton
|
|
3945
|
+
) {
|
|
3946
|
+
saveButton.click();
|
|
3947
|
+
} else {
|
|
3948
|
+
// Fallback to any button with data-type="save"
|
|
3949
|
+
const fallbackButton =
|
|
3950
|
+
document.querySelector(
|
|
3951
|
+
`.inPlaceEdit button[data-type="save"]`
|
|
3952
|
+
);
|
|
3953
|
+
if (
|
|
3954
|
+
fallbackButton
|
|
3955
|
+
) {
|
|
3956
|
+
fallbackButton.click();
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
// Reset the editing state
|
|
3961
|
+
setInPlaceEditing(
|
|
3962
|
+
false
|
|
3963
|
+
);
|
|
3964
|
+
setEditingTabId(
|
|
3965
|
+
null
|
|
3966
|
+
);
|
|
3967
|
+
|
|
3968
|
+
// Reload data
|
|
3969
|
+
handleReload();
|
|
3970
|
+
}}
|
|
3971
|
+
>
|
|
3972
|
+
Save
|
|
3973
|
+
</button>
|
|
3974
|
+
</>
|
|
3975
|
+
)
|
|
3976
|
+
)}
|
|
3977
|
+
|
|
3978
|
+
{/* Check if sections contain "ocrTemplate" type and render the "Save" button under the same polActions */}
|
|
3979
|
+
{activeTabConfig.sections &&
|
|
3980
|
+
activeTabConfig.sections
|
|
3981
|
+
.length > 0 &&
|
|
3982
|
+
activeTabConfig.sections.map(
|
|
3983
|
+
(section, sectionIndex) =>
|
|
3984
|
+
section.content &&
|
|
3985
|
+
section.content.some(
|
|
3986
|
+
(item) =>
|
|
3987
|
+
item.type ===
|
|
3988
|
+
'ocrTemplate'
|
|
3989
|
+
) && (
|
|
3990
|
+
<button
|
|
3991
|
+
key={`save-button-${sectionIndex}`}
|
|
3992
|
+
className={
|
|
3993
|
+
styles.btn
|
|
3994
|
+
}
|
|
3995
|
+
onClick={() => {
|
|
3996
|
+
// Handle the specific "ocrTemplate" item
|
|
3997
|
+
const ocrItem =
|
|
3998
|
+
section.content.find(
|
|
3999
|
+
(
|
|
4000
|
+
item
|
|
4001
|
+
) =>
|
|
4002
|
+
item.type ===
|
|
4003
|
+
'ocrTemplate'
|
|
4004
|
+
);
|
|
4005
|
+
handleOcrTemplateSave(
|
|
4006
|
+
ocrItem.id
|
|
4007
|
+
); // Now you can safely use ocrItem.id
|
|
4008
|
+
}}
|
|
4009
|
+
>
|
|
4010
|
+
Save
|
|
4011
|
+
</button>
|
|
4012
|
+
)
|
|
4013
|
+
)}
|
|
4014
|
+
|
|
4015
|
+
{/* Check if export exists and render the "Export" button under the same polActions */}
|
|
4016
|
+
{activeTabConfig.export && (
|
|
4017
|
+
<button
|
|
4018
|
+
className={styles.btn}
|
|
4019
|
+
onClick={() =>
|
|
4020
|
+
handleExport(
|
|
4021
|
+
activeTabConfig.export
|
|
4022
|
+
)
|
|
4023
|
+
}
|
|
4024
|
+
>
|
|
4025
|
+
{activeTabConfig.export
|
|
4026
|
+
.label || 'Export'}
|
|
4027
|
+
</button>
|
|
4028
|
+
)}
|
|
4029
|
+
|
|
4030
|
+
{activeTabConfig.customAction && (
|
|
4031
|
+
<button
|
|
4032
|
+
className={styles.btn}
|
|
4033
|
+
onClick={() =>
|
|
4034
|
+
handleCustomAction(
|
|
4035
|
+
activeTabConfig.customAction
|
|
4036
|
+
)
|
|
4037
|
+
}
|
|
4038
|
+
>
|
|
4039
|
+
{
|
|
4040
|
+
activeTabConfig
|
|
4041
|
+
.customAction.label
|
|
4042
|
+
}
|
|
4043
|
+
</button>
|
|
4044
|
+
)}
|
|
4045
|
+
|
|
4046
|
+
{activeTabConfig.customActions && Array.isArray(activeTabConfig.customActions) && activeTabConfig.customActions.map((customAction, index) => (
|
|
4047
|
+
<button
|
|
4048
|
+
key={index}
|
|
4049
|
+
className={styles.btn}
|
|
4050
|
+
onClick={() =>
|
|
4051
|
+
handleCustomAction(customAction)
|
|
4052
|
+
}
|
|
4053
|
+
>
|
|
4054
|
+
{getActionLabel(customAction)}
|
|
4055
|
+
</button>
|
|
4056
|
+
))}
|
|
4057
|
+
</>
|
|
4058
|
+
);
|
|
4059
|
+
};
|
|
4060
|
+
|
|
4061
|
+
/** A table tab's "update the ticked rows" button. */
|
|
4062
|
+
const renderTableUpdateAction = (activeTabConfig) => {
|
|
4063
|
+
if (!activeTabConfig?.functions?.checkboxUpdate) {
|
|
4064
|
+
return null;
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
return (
|
|
4068
|
+
<>
|
|
4069
|
+
<button
|
|
4070
|
+
className={styles.btn}
|
|
4071
|
+
onClick={
|
|
4072
|
+
handleCheckboxUpdate
|
|
4073
|
+
}
|
|
4074
|
+
>
|
|
4075
|
+
{activeTabConfig.functions
|
|
4076
|
+
.checkboxUpdate.label ||
|
|
4077
|
+
'Update'}
|
|
4078
|
+
</button>
|
|
4079
|
+
</>
|
|
4080
|
+
);
|
|
4081
|
+
};
|
|
4082
|
+
|
|
4083
|
+
/** A table tab's bulk delete, and the custom actions beside it. */
|
|
4084
|
+
const renderTableBulkActions = (activeTabConfig) => {
|
|
4085
|
+
if (
|
|
4086
|
+
!(
|
|
4087
|
+
activeTabConfig?.functions?.checkboxDelete ||
|
|
4088
|
+
activeTabConfig?.customAction ||
|
|
4089
|
+
(activeTabConfig?.customActions &&
|
|
4090
|
+
activeTabConfig.customActions.length > 0)
|
|
4091
|
+
)
|
|
4092
|
+
) {
|
|
4093
|
+
return null;
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
return (
|
|
4097
|
+
<>
|
|
4098
|
+
{activeTabConfig.functions
|
|
4099
|
+
?.checkboxDelete && (
|
|
4100
|
+
<button
|
|
4101
|
+
className={styles.btn}
|
|
4102
|
+
onClick={
|
|
4103
|
+
handleCheckboxDelete
|
|
4104
|
+
}
|
|
4105
|
+
>
|
|
4106
|
+
{activeTabConfig
|
|
4107
|
+
.functions
|
|
4108
|
+
.checkboxDelete
|
|
4109
|
+
.label || 'Delete'}
|
|
4110
|
+
</button>
|
|
4111
|
+
)}
|
|
4112
|
+
|
|
4113
|
+
{activeTabConfig.customAction && (
|
|
4114
|
+
<button
|
|
4115
|
+
className={styles.btn}
|
|
4116
|
+
onClick={() =>
|
|
4117
|
+
handleCustomAction(
|
|
4118
|
+
activeTabConfig.customAction
|
|
4119
|
+
)
|
|
4120
|
+
}
|
|
4121
|
+
>
|
|
4122
|
+
{
|
|
4123
|
+
activeTabConfig
|
|
4124
|
+
.customAction
|
|
4125
|
+
.label
|
|
4126
|
+
}
|
|
4127
|
+
</button>
|
|
4128
|
+
)}
|
|
4129
|
+
|
|
4130
|
+
{activeTabConfig.customActions && Array.isArray(activeTabConfig.customActions) && activeTabConfig.customActions.map((customAction, index) => (
|
|
4131
|
+
<button
|
|
4132
|
+
key={index}
|
|
4133
|
+
className={styles.btn}
|
|
4134
|
+
onClick={() =>
|
|
4135
|
+
handleCustomAction(customAction)
|
|
4136
|
+
}
|
|
4137
|
+
>
|
|
4138
|
+
{getActionLabel(customAction)}
|
|
4139
|
+
</button>
|
|
4140
|
+
))}
|
|
4141
|
+
</>
|
|
4142
|
+
);
|
|
4143
|
+
};
|
|
4144
|
+
|
|
4145
|
+
|
|
4026
4146
|
/**
|
|
4027
4147
|
* Detail tabs are a horizontal strip above the content panel.
|
|
4028
4148
|
*
|
|
@@ -4046,6 +4166,68 @@ function GenericDetail({
|
|
|
4046
4166
|
*/
|
|
4047
4167
|
const tabsUnderlined = isFlatFilterList(filters);
|
|
4048
4168
|
|
|
4169
|
+
/**
|
|
4170
|
+
* The active tab's action cluster, when this page asked for it in the
|
|
4171
|
+
* title row. `null` on the floating default, so the title row is
|
|
4172
|
+
* byte-identical for every host that did not opt in.
|
|
4173
|
+
*
|
|
4174
|
+
* The active tab is worked out here rather than read off the
|
|
4175
|
+
* `activeTabConfig` STATE, and that is not a preference: the state is set
|
|
4176
|
+
* by an effect, so it is a render behind, and a header that lags one tab
|
|
4177
|
+
* behind the panel under it draws the previous tab's Edit button. The two
|
|
4178
|
+
* lines below are the same lookup `renderContent()` makes, in the same
|
|
4179
|
+
* order, on the same `subnav` — which also runs during THIS render, but
|
|
4180
|
+
* later in the tree than the title row.
|
|
4181
|
+
*
|
|
4182
|
+
* The tab TYPE gate mirrors the switch exactly, and deliberately so:
|
|
4183
|
+
* placement moves a cluster, it must never add one. An overview tab draws
|
|
4184
|
+
* the Edit cluster; a table tab draws its bulk actions, and only once its
|
|
4185
|
+
* grid config has arrived, which is the same `config.ajaxSetting &&
|
|
4186
|
+
* config.columns` test that decides whether the table case renders at all.
|
|
4187
|
+
* Every other tab type has never had a cluster and still does not.
|
|
4188
|
+
*/
|
|
4189
|
+
const headerActions = (() => {
|
|
4190
|
+
if (!drawActionsInHeader || !subnav || subnav.length === 0) {
|
|
4191
|
+
return null;
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
const activeTab = subnav.find((s) => s.show === true);
|
|
4195
|
+
const activeTabConfig = activeTab
|
|
4196
|
+
? (tabs || []).find((t) => t.id === activeTab.id)
|
|
4197
|
+
: null;
|
|
4198
|
+
|
|
4199
|
+
if (!activeTabConfig || !activeTabConfig.type) {
|
|
4200
|
+
return null;
|
|
4201
|
+
}
|
|
4202
|
+
|
|
4203
|
+
if (activeTabConfig.type === 'overview') {
|
|
4204
|
+
return actionCluster(renderOverviewActions(activeTabConfig));
|
|
4205
|
+
}
|
|
4206
|
+
|
|
4207
|
+
if (
|
|
4208
|
+
activeTabConfig.type === 'table' &&
|
|
4209
|
+
config.ajaxSetting &&
|
|
4210
|
+
config.columns
|
|
4211
|
+
) {
|
|
4212
|
+
const update = renderTableUpdateAction(activeTabConfig);
|
|
4213
|
+
const bulk = renderTableBulkActions(activeTabConfig);
|
|
4214
|
+
|
|
4215
|
+
/* One cluster, not two. Pinned bottom-right these are separate
|
|
4216
|
+
bars stacked at the same coordinates; on a single row they are
|
|
4217
|
+
simply buttons beside each other. */
|
|
4218
|
+
return update || bulk
|
|
4219
|
+
? actionCluster(
|
|
4220
|
+
<>
|
|
4221
|
+
{update}
|
|
4222
|
+
{bulk}
|
|
4223
|
+
</>
|
|
4224
|
+
)
|
|
4225
|
+
: null;
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
return null;
|
|
4229
|
+
})();
|
|
4230
|
+
|
|
4049
4231
|
return (
|
|
4050
4232
|
<>
|
|
4051
4233
|
<div className={styles.grid}>
|
|
@@ -4058,8 +4240,9 @@ function GenericDetail({
|
|
|
4058
4240
|
}`}
|
|
4059
4241
|
>
|
|
4060
4242
|
<Breadcrumb data={data} page={page} />
|
|
4061
|
-
{(actions || total > 0) && (
|
|
4243
|
+
{(actions || total > 0 || headerActions) && (
|
|
4062
4244
|
<div
|
|
4245
|
+
className={styles.titleActions}
|
|
4063
4246
|
style={{
|
|
4064
4247
|
display: 'flex',
|
|
4065
4248
|
alignItems: 'center',
|
|
@@ -4074,6 +4257,10 @@ function GenericDetail({
|
|
|
4074
4257
|
</div>
|
|
4075
4258
|
)}
|
|
4076
4259
|
{actions}
|
|
4260
|
+
{/* Last, so a host's own `actions` keep the
|
|
4261
|
+
position they have always had and the
|
|
4262
|
+
record's Edit sits outermost right. */}
|
|
4263
|
+
{headerActions}
|
|
4077
4264
|
</div>
|
|
4078
4265
|
)}
|
|
4079
4266
|
</div>
|