@stsdti/approvable-vue 0.1.7 → 0.1.11
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/dist/approvable-vue.mjs +815 -584
- package/dist/approvable-vue.mjs.map +1 -1
- package/package.json +2 -2
- package/src/components/approvable/Approvable.vue +177 -62
- package/src/components/approvable/ApprovableCompact.vue +130 -0
- package/src/components/approvable/ApprovableStep.vue +9 -95
- package/src/components/approvable/ApprovableStepActions.vue +135 -0
- package/src/index.js +6 -0
- package/src/themes/approvable.css +179 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="hasActions" class="approvable-step-actions">
|
|
3
|
+
<div class="approvable-btn-group">
|
|
4
|
+
<!-- APPROVABLE BUTTONS (custom renderer per button, else basic) -->
|
|
5
|
+
<template v-for="(button, index) in step.buttons" :key="button.id ?? index">
|
|
6
|
+
<component
|
|
7
|
+
:is="buttonRenderer(button)"
|
|
8
|
+
v-if="buttonRenderer(button)"
|
|
9
|
+
:action-ui="button.action_ui"
|
|
10
|
+
:approvable="approvable"
|
|
11
|
+
:button="button"
|
|
12
|
+
:buttons="[button]"
|
|
13
|
+
:complete="onApproveStatusChange"
|
|
14
|
+
:resource="resource"
|
|
15
|
+
:step="value"
|
|
16
|
+
@is-loading="(isLoading) => emits('isLoading', isLoading)"
|
|
17
|
+
@refresh="emits('refresh')"
|
|
18
|
+
/>
|
|
19
|
+
<component
|
|
20
|
+
:is="linkComponent"
|
|
21
|
+
v-else-if="button.link"
|
|
22
|
+
v-bind="linkProps(linkComponent, button.link)"
|
|
23
|
+
>
|
|
24
|
+
<button
|
|
25
|
+
:title="button.tooltip"
|
|
26
|
+
:class="buttonClass(button)"
|
|
27
|
+
type="button"
|
|
28
|
+
@click="onApproveStatusChange(button)"
|
|
29
|
+
>
|
|
30
|
+
<approvable-icon :icon="button.icon" />
|
|
31
|
+
</button>
|
|
32
|
+
</component>
|
|
33
|
+
<button
|
|
34
|
+
v-else
|
|
35
|
+
:title="button.tooltip"
|
|
36
|
+
:class="buttonClass(button)"
|
|
37
|
+
type="button"
|
|
38
|
+
@click="onApproveStatusChange(button)"
|
|
39
|
+
>
|
|
40
|
+
<approvable-icon :icon="button.icon" />
|
|
41
|
+
{{ button.text }}
|
|
42
|
+
</button>
|
|
43
|
+
</template>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div v-if="value.can_comment" class="approvable-btn-group">
|
|
47
|
+
<approvable-step-comment
|
|
48
|
+
:approvable-step="value"
|
|
49
|
+
:disabled="!value.can_comment"
|
|
50
|
+
@refresh="emits('refresh')"
|
|
51
|
+
@is-loading="(isLoading) => emits('isLoading', isLoading)"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</template>
|
|
56
|
+
|
|
57
|
+
<script setup>
|
|
58
|
+
import { computed } from 'vue'
|
|
59
|
+
import { createStepViewModel, performStepAction } from '@stsdti/approvable-core'
|
|
60
|
+
import ApprovableIcon from './ApprovableIcon.vue'
|
|
61
|
+
import ApprovableStepComment from './ApprovableStepComment.vue'
|
|
62
|
+
import { approvableActionRenderer } from './action-renderers.js'
|
|
63
|
+
import { linkProps, useLinkComponent } from '../../config.js'
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The controls a step offers — its workflow buttons and, where allowed, the
|
|
67
|
+
* comment popover.
|
|
68
|
+
*
|
|
69
|
+
* Extracted from the step row because the compact form renders exactly these
|
|
70
|
+
* controls beside a one-line summary instead of beside a trail. Two copies of
|
|
71
|
+
* this markup would be two places for a custom action renderer, a navigating
|
|
72
|
+
* button or the refresh-after-action rule to quietly diverge.
|
|
73
|
+
*/
|
|
74
|
+
const emits = defineEmits(['isLoading', 'refresh'])
|
|
75
|
+
|
|
76
|
+
const props = defineProps({
|
|
77
|
+
value: {
|
|
78
|
+
type: Object,
|
|
79
|
+
required: true
|
|
80
|
+
},
|
|
81
|
+
approvable: {
|
|
82
|
+
type: Object,
|
|
83
|
+
required: true
|
|
84
|
+
},
|
|
85
|
+
resource: {
|
|
86
|
+
type: Object,
|
|
87
|
+
required: true
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const linkComponent = useLinkComponent()
|
|
92
|
+
|
|
93
|
+
const step = computed(() => createStepViewModel(props.value))
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A row shows controls when it has any — a finished step usually has none, and
|
|
97
|
+
* shows when it was decided instead.
|
|
98
|
+
*/
|
|
99
|
+
const hasActions = computed(() => step.value.buttons.length > 0 || !!props.value.can_comment)
|
|
100
|
+
|
|
101
|
+
const buttonClass = (button) => ({
|
|
102
|
+
'approvable-btn': true,
|
|
103
|
+
// A button carrying only an icon collapses to a circle, so the row keeps its
|
|
104
|
+
// rhythm instead of stretching around an empty label.
|
|
105
|
+
'approvable-btn--icon': !button.text,
|
|
106
|
+
'approvable-btn--primary': button.result === 'approved'
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const buttonRenderer = (button) => {
|
|
110
|
+
if (!step.value.isActive) {
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return approvableActionRenderer(button?.action_ui)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const onApproveStatusChange = async (buttonOrId) => {
|
|
118
|
+
emits('isLoading', true)
|
|
119
|
+
|
|
120
|
+
const result = await performStepAction({
|
|
121
|
+
stepId: step.value.id,
|
|
122
|
+
button: buttonOrId,
|
|
123
|
+
buttons: step.value.buttons
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
emits('isLoading', false)
|
|
127
|
+
|
|
128
|
+
// Refresh after any attempted request, success or failure — a failed action
|
|
129
|
+
// still means the local state may be stale. Only a skipped one (no id, no
|
|
130
|
+
// button, or a presentational button) reloads nothing, since it sent nothing.
|
|
131
|
+
if (!result.skipped) {
|
|
132
|
+
emits('refresh')
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
</script>
|
package/src/index.js
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import Approvable from './components/approvable/Approvable.vue'
|
|
14
|
+
import ApprovableCompact from './components/approvable/ApprovableCompact.vue'
|
|
14
15
|
import ApprovableStep from './components/approvable/ApprovableStep.vue'
|
|
16
|
+
import ApprovableStepActions from './components/approvable/ApprovableStepActions.vue'
|
|
15
17
|
import ApprovableButtons from './components/approvable/ApprovableButtons.vue'
|
|
16
18
|
import ApprovableStepComment from './components/approvable/ApprovableStepComment.vue'
|
|
17
19
|
import ApprovableAuditTimeline from './components/approvable/ApprovableAuditTimeline.vue'
|
|
@@ -31,7 +33,9 @@ import {
|
|
|
31
33
|
|
|
32
34
|
export {
|
|
33
35
|
Approvable,
|
|
36
|
+
ApprovableCompact,
|
|
34
37
|
ApprovableStep,
|
|
38
|
+
ApprovableStepActions,
|
|
35
39
|
ApprovableButtons,
|
|
36
40
|
ApprovableStepComment,
|
|
37
41
|
ApprovableAuditTimeline,
|
|
@@ -80,7 +84,9 @@ export {
|
|
|
80
84
|
|
|
81
85
|
const components = {
|
|
82
86
|
Approvable,
|
|
87
|
+
ApprovableCompact,
|
|
83
88
|
ApprovableStep,
|
|
89
|
+
ApprovableStepActions,
|
|
84
90
|
ApprovableButtons,
|
|
85
91
|
ApprovableStepComment,
|
|
86
92
|
ApprovableAuditTimeline,
|
|
@@ -266,6 +266,29 @@
|
|
|
266
266
|
text-underline-offset: 3px;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
/* The control that folds a long trail down to `visibleStepRange`. A quiet
|
|
270
|
+
outline pill rather than a second header link: it changes what the panel
|
|
271
|
+
shows, where History opens something, and the difference should be legible
|
|
272
|
+
before it is read. */
|
|
273
|
+
.approvable-steps-toggle-btn {
|
|
274
|
+
flex: none;
|
|
275
|
+
padding: 4px 10px;
|
|
276
|
+
border: 1px solid var(--approvable-control-border);
|
|
277
|
+
border-radius: 999px;
|
|
278
|
+
background: var(--approvable-control);
|
|
279
|
+
color: var(--approvable-control-ink);
|
|
280
|
+
font-family: inherit;
|
|
281
|
+
font-size: 11px;
|
|
282
|
+
font-weight: 600;
|
|
283
|
+
line-height: 1.2;
|
|
284
|
+
white-space: nowrap;
|
|
285
|
+
cursor: pointer;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.approvable-steps-toggle-btn:hover {
|
|
289
|
+
background: var(--approvable-control-hover);
|
|
290
|
+
}
|
|
291
|
+
|
|
269
292
|
/* --------------------------------------------------------------------------
|
|
270
293
|
Status badge — the header pill, and anywhere else a status is named
|
|
271
294
|
-------------------------------------------------------------------------- */
|
|
@@ -337,6 +360,161 @@
|
|
|
337
360
|
50% { opacity: 1; transform: scale(1); }
|
|
338
361
|
}
|
|
339
362
|
|
|
363
|
+
/* --------------------------------------------------------------------------
|
|
364
|
+
Compact — the collapsed, one-line form
|
|
365
|
+
|
|
366
|
+
Status, a dot per step, who it is waiting on and that step's controls, on a
|
|
367
|
+
single row. Built for a table cell or a page header, where the full trail
|
|
368
|
+
does not fit and its absence is the point.
|
|
369
|
+
-------------------------------------------------------------------------- */
|
|
370
|
+
.approvable-compact {
|
|
371
|
+
display: flex;
|
|
372
|
+
align-items: center;
|
|
373
|
+
gap: 11px;
|
|
374
|
+
padding: 10px 12px;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.approvable-compact-main {
|
|
378
|
+
flex: 1;
|
|
379
|
+
min-width: 0;
|
|
380
|
+
display: flex;
|
|
381
|
+
flex-direction: column;
|
|
382
|
+
gap: 3px;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.approvable-compact-line {
|
|
386
|
+
display: flex;
|
|
387
|
+
align-items: center;
|
|
388
|
+
gap: 7px;
|
|
389
|
+
min-width: 0;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* The whole chain, one dot per visible step, in workflow order. Small enough
|
|
393
|
+
to sit beside the badge without competing with it, and the only place the
|
|
394
|
+
collapsed form admits how long the approval actually is. */
|
|
395
|
+
.approvable-compact-dots {
|
|
396
|
+
display: flex;
|
|
397
|
+
align-items: center;
|
|
398
|
+
gap: 3px;
|
|
399
|
+
flex: none;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.approvable-compact-dot {
|
|
403
|
+
width: 5px;
|
|
404
|
+
height: 5px;
|
|
405
|
+
flex: none;
|
|
406
|
+
border-radius: 999px;
|
|
407
|
+
background: var(--approvable-neutral-tint);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.approvable-compact-dot--success {
|
|
411
|
+
background: var(--approvable-success-text);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.approvable-compact-dot--danger {
|
|
415
|
+
background: var(--approvable-danger-text);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.approvable-compact-dot--muted {
|
|
419
|
+
background: var(--approvable-muted-soft);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.approvable-compact-dot--pending {
|
|
423
|
+
background: var(--approvable-control-border);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/* The step being waited on is larger and pulses — the same beat the header's
|
|
427
|
+
waiting badge keeps, so the two read as one signal rather than two. */
|
|
428
|
+
.approvable-compact-dot--active {
|
|
429
|
+
width: 7px;
|
|
430
|
+
height: 7px;
|
|
431
|
+
background: var(--approvable-accent);
|
|
432
|
+
animation: approvable-pulse 1.6s ease-in-out infinite;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.approvable-compact-who {
|
|
436
|
+
margin: 0;
|
|
437
|
+
min-width: 0;
|
|
438
|
+
font-size: 11.5px;
|
|
439
|
+
font-weight: 400;
|
|
440
|
+
line-height: 1.35;
|
|
441
|
+
color: var(--approvable-muted);
|
|
442
|
+
white-space: nowrap;
|
|
443
|
+
overflow: hidden;
|
|
444
|
+
text-overflow: ellipsis;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.approvable-compact-name {
|
|
448
|
+
font-weight: 600;
|
|
449
|
+
color: var(--approvable-ink);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.approvable-compact-sep {
|
|
453
|
+
margin: 0 5px;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.approvable-compact-side {
|
|
457
|
+
flex: none;
|
|
458
|
+
display: flex;
|
|
459
|
+
align-items: center;
|
|
460
|
+
gap: 6px;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/* The row's own actions sit flush with the chevron rather than in the padded
|
|
464
|
+
block a step row gives them. */
|
|
465
|
+
.approvable-compact-side .approvable-step-actions {
|
|
466
|
+
margin: 0;
|
|
467
|
+
padding: 0;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/* The control that swaps the two forms. A circle, so it reads as a disclosure
|
|
471
|
+
rather than as one more thing to approve. */
|
|
472
|
+
.approvable-compact-toggle {
|
|
473
|
+
flex: none;
|
|
474
|
+
width: 26px;
|
|
475
|
+
height: 26px;
|
|
476
|
+
display: inline-flex;
|
|
477
|
+
align-items: center;
|
|
478
|
+
justify-content: center;
|
|
479
|
+
padding: 0;
|
|
480
|
+
border: 1px solid var(--approvable-control-border);
|
|
481
|
+
border-radius: 999px;
|
|
482
|
+
background: var(--approvable-control);
|
|
483
|
+
color: var(--approvable-control-ink);
|
|
484
|
+
cursor: pointer;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.approvable-compact-toggle:hover {
|
|
488
|
+
background: var(--approvable-control-hover);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.approvable-compact-toggle:focus-visible {
|
|
492
|
+
outline: 2px solid var(--approvable-accent);
|
|
493
|
+
outline-offset: 2px;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/* Same chevron either way; the direction is the state. */
|
|
497
|
+
.approvable-compact-toggle--expanded svg {
|
|
498
|
+
transform: rotate(180deg);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/* Narrow enough that the name and its controls cannot share a line, so the
|
|
502
|
+
controls drop below and keep their hit targets. */
|
|
503
|
+
@media (max-width: 420px) {
|
|
504
|
+
.approvable-compact {
|
|
505
|
+
align-items: flex-start;
|
|
506
|
+
flex-wrap: wrap;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/* The controls wrap onto their own rows here, so the chevron sits with the
|
|
510
|
+
first of them rather than floating between two. */
|
|
511
|
+
.approvable-compact-side {
|
|
512
|
+
width: 100%;
|
|
513
|
+
align-items: flex-start;
|
|
514
|
+
justify-content: flex-end;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
340
518
|
/* --------------------------------------------------------------------------
|
|
341
519
|
Step rows
|
|
342
520
|
-------------------------------------------------------------------------- */
|
|
@@ -575,6 +753,7 @@
|
|
|
575
753
|
.approvable-btn:focus-visible,
|
|
576
754
|
.approvable-history-btn:focus-visible,
|
|
577
755
|
.approvable-step-comment-more:focus-visible,
|
|
756
|
+
.approvable-steps-toggle-btn:focus-visible,
|
|
578
757
|
.approvable-timeline-refresh:focus-visible,
|
|
579
758
|
.approvable-dialog-close:focus-visible {
|
|
580
759
|
outline: 2px solid var(--approvable-accent);
|