@stsdti/approvable-vue 0.1.7 → 0.1.12
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 +778 -567
- package/dist/approvable-vue.mjs.map +1 -1
- package/package.json +2 -2
- package/src/components/approvable/Approvable.vue +174 -82
- 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 -12
|
@@ -1,81 +1,149 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div v-if="approval.hasApprovable" class="
|
|
2
|
+
<div v-if="approval.hasApprovable" :class="rootClass">
|
|
3
3
|
<!-- A refresh has content to veil; an initial load has none, and gets the
|
|
4
4
|
skeleton below instead. -->
|
|
5
5
|
<div v-if="isLoading && approval.steps.length" class="approvable-loading">
|
|
6
6
|
<span class="approvable-spinner" />
|
|
7
7
|
</div>
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
<!-- The collapsed form replaces the trail rather than summarising it on top
|
|
10
|
+
of one, so it is the whole panel while it is on. -->
|
|
11
|
+
<template v-if="isCompact">
|
|
10
12
|
<h2 class="approvable-sr-only">{{ title }}</h2>
|
|
11
13
|
|
|
12
|
-
<
|
|
14
|
+
<div v-if="isInitialLoad" class="approvable-compact">
|
|
15
|
+
<span class="approvable-sr-only" aria-live="polite">{{ messages.loadingStatus }}</span>
|
|
13
16
|
<span aria-hidden="true" class="approvable-skeleton-bar approvable-skeleton-bar--pill" />
|
|
14
|
-
<span class="approvable-skeleton-spacer" />
|
|
15
17
|
<span
|
|
16
18
|
aria-hidden="true"
|
|
17
19
|
class="approvable-skeleton-bar approvable-skeleton-bar--soft"
|
|
18
|
-
style="width:
|
|
20
|
+
style="width:96px;animation-delay:0.15s"
|
|
19
21
|
/>
|
|
20
|
-
|
|
22
|
+
<span aria-hidden="true" class="approvable-skeleton-spacer" />
|
|
23
|
+
<span aria-hidden="true" class="approvable-skeleton-disc" style="animation-delay:0.25s" />
|
|
24
|
+
</div>
|
|
21
25
|
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
<approvable-compact
|
|
27
|
+
v-else
|
|
28
|
+
:show-approvable-status="showApprovableStatus"
|
|
29
|
+
:value="value"
|
|
30
|
+
@expand="setCompact(false)"
|
|
31
|
+
@isLoading="(value) => (isLoading = value)"
|
|
32
|
+
@refresh="onRefresh"
|
|
33
|
+
>
|
|
34
|
+
<template v-for="slot in Object.keys($slots)" v-slot:[slot]="scoped">
|
|
35
|
+
<slot v-bind="scoped ?? {}" :name="slot" />
|
|
36
|
+
</template>
|
|
37
|
+
</approvable-compact>
|
|
38
|
+
</template>
|
|
28
39
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
</
|
|
32
|
-
<button
|
|
33
|
-
v-if="approval.id"
|
|
34
|
-
type="button"
|
|
35
|
-
class="approvable-history-btn"
|
|
36
|
-
:title="messages.historyTitle"
|
|
37
|
-
:aria-label="messages.historyTitle"
|
|
38
|
-
@click="historyOpen = true"
|
|
39
|
-
>
|
|
40
|
-
{{ messages.history }}
|
|
41
|
-
</button>
|
|
42
|
-
</template>
|
|
43
|
-
</div>
|
|
40
|
+
<template v-else>
|
|
41
|
+
<div class="approvable-header">
|
|
42
|
+
<h2 class="approvable-sr-only">{{ title }}</h2>
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
44
|
+
<template v-if="isInitialLoad">
|
|
45
|
+
<span aria-hidden="true" class="approvable-skeleton-bar approvable-skeleton-bar--pill" />
|
|
46
|
+
<span class="approvable-skeleton-spacer" />
|
|
47
|
+
<span
|
|
48
|
+
aria-hidden="true"
|
|
49
|
+
class="approvable-skeleton-bar approvable-skeleton-bar--soft"
|
|
50
|
+
style="width:104px;animation-delay:0.2s"
|
|
51
|
+
/>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<template v-else>
|
|
55
|
+
<approve-status-info
|
|
56
|
+
v-if="showApprovableStatus"
|
|
57
|
+
:label="approval.statusName"
|
|
58
|
+
:value="approval.status"
|
|
59
|
+
/>
|
|
60
|
+
|
|
61
|
+
<span class="approvable-updated-at">
|
|
62
|
+
{{ approval.updatedAt }}
|
|
63
|
+
</span>
|
|
64
|
+
<!-- Only worth offering when the window is actually hiding something —
|
|
65
|
+
a short trail shows in full either way. -->
|
|
66
|
+
<button
|
|
67
|
+
v-if="stepWindow.isCollapsible"
|
|
68
|
+
type="button"
|
|
69
|
+
class="approvable-steps-toggle-btn"
|
|
70
|
+
:aria-expanded="stepsExpanded"
|
|
71
|
+
@click="stepsExpanded = !stepsExpanded"
|
|
72
|
+
>
|
|
73
|
+
{{ stepsExpanded
|
|
74
|
+
? messages.stepsShowLess
|
|
75
|
+
: formatMessage(messages.stepsShowMore, { count: stepWindow.totalCount }) }}
|
|
76
|
+
</button>
|
|
77
|
+
|
|
78
|
+
<button
|
|
79
|
+
v-if="isHistoryVisible && approval.id"
|
|
80
|
+
type="button"
|
|
81
|
+
class="approvable-history-btn"
|
|
82
|
+
:title="messages.historyTitle"
|
|
83
|
+
:aria-label="messages.historyTitle"
|
|
84
|
+
@click="historyOpen = true"
|
|
85
|
+
>
|
|
86
|
+
{{ messages.history }}
|
|
87
|
+
</button>
|
|
88
|
+
|
|
89
|
+
<!-- The way back to the collapsed form: the same chevron the compact
|
|
90
|
+
row carries, turned over. One control, two directions. -->
|
|
91
|
+
<button
|
|
92
|
+
v-if="isCollapsible"
|
|
93
|
+
type="button"
|
|
94
|
+
class="approvable-compact-toggle approvable-compact-toggle--expanded"
|
|
95
|
+
:aria-expanded="true"
|
|
96
|
+
:aria-label="messages.compactCollapse"
|
|
97
|
+
:title="messages.compactCollapse"
|
|
98
|
+
@click="setCompact(true)"
|
|
99
|
+
>
|
|
100
|
+
<svg
|
|
101
|
+
aria-hidden="true"
|
|
102
|
+
class="approvable-icon"
|
|
103
|
+
fill="none"
|
|
104
|
+
stroke="currentColor"
|
|
105
|
+
stroke-linecap="round"
|
|
106
|
+
stroke-linejoin="round"
|
|
107
|
+
stroke-width="2"
|
|
108
|
+
viewBox="0 0 16 16"
|
|
109
|
+
>
|
|
110
|
+
<path d="M4.5 6.4 8 9.9l3.5-3.5" />
|
|
111
|
+
</svg>
|
|
112
|
+
</button>
|
|
113
|
+
</template>
|
|
58
114
|
</div>
|
|
59
|
-
</template>
|
|
60
115
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<approvable-
|
|
64
|
-
|
|
65
|
-
:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
116
|
+
<template v-if="isInitialLoad">
|
|
117
|
+
<span class="approvable-sr-only" aria-live="polite">{{ messages.loadingStatus }}</span>
|
|
118
|
+
<div v-for="row in SKELETON_ROWS" :key="row.delay" aria-hidden="true" class="approvable-skeleton-row">
|
|
119
|
+
<span class="approvable-skeleton-disc" :style="{ animationDelay: row.delay }" />
|
|
120
|
+
<span class="approvable-skeleton-bar" :style="{ width: row.name, animationDelay: row.delay }" />
|
|
121
|
+
<span class="approvable-skeleton-spacer" />
|
|
122
|
+
<span
|
|
123
|
+
class="approvable-skeleton-bar approvable-skeleton-bar--soft"
|
|
124
|
+
:style="{ width: row.meta, animationDelay: row.metaDelay }"
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
</template>
|
|
128
|
+
|
|
129
|
+
<template v-else>
|
|
130
|
+
<ol v-if="stepWindow.steps.length" :class="approvableStepsClass">
|
|
131
|
+
<approvable-step
|
|
132
|
+
v-for="approvableStep of stepWindow.steps"
|
|
133
|
+
:key="approvableStep.id"
|
|
134
|
+
:approvable="approval.approvable"
|
|
135
|
+
:resource="value"
|
|
136
|
+
:value="approvableStep"
|
|
137
|
+
@isLoading="(value) => (isLoading = value)"
|
|
138
|
+
@refresh="onRefresh"
|
|
139
|
+
>
|
|
140
|
+
<template v-for="slot in Object.keys($slots)" v-slot:[slot]="scoped">
|
|
141
|
+
<slot v-bind="scoped ?? {}" :name="slot" />
|
|
142
|
+
</template>
|
|
143
|
+
</approvable-step>
|
|
77
144
|
|
|
78
|
-
|
|
145
|
+
</ol>
|
|
146
|
+
</template>
|
|
79
147
|
</template>
|
|
80
148
|
|
|
81
149
|
<Teleport to="body">
|
|
@@ -121,9 +189,10 @@
|
|
|
121
189
|
</template>
|
|
122
190
|
|
|
123
191
|
<script setup>
|
|
124
|
-
import { ref, computed } from 'vue'
|
|
125
|
-
import { createApprovableViewModel, formatMessage } from '@stsdti/approvable-core'
|
|
192
|
+
import { ref, computed, watch } from 'vue'
|
|
193
|
+
import { createApprovableViewModel, createStepWindow, formatMessage } from '@stsdti/approvable-core'
|
|
126
194
|
import ApprovableAuditTimeline from './ApprovableAuditTimeline.vue'
|
|
195
|
+
import ApprovableCompact from './ApprovableCompact.vue'
|
|
127
196
|
import ApprovableStep from './ApprovableStep.vue'
|
|
128
197
|
import ApproveStatusInfo from './ApproveStatusInfo.vue'
|
|
129
198
|
import { useMessages } from '../../messages.js'
|
|
@@ -131,16 +200,37 @@ import { useMessages } from '../../messages.js'
|
|
|
131
200
|
const props = defineProps({
|
|
132
201
|
value: {},
|
|
133
202
|
showApprovableStatus: { type: Boolean, default: true },
|
|
203
|
+
/** Whether the History button — and so the audit dialog — is offered. */
|
|
204
|
+
isHistoryVisible: { type: Boolean, default: true },
|
|
134
205
|
title: { default: 'Approval' },
|
|
135
|
-
isDirectionRow: { default: false }
|
|
206
|
+
isDirectionRow: { default: false },
|
|
207
|
+
/**
|
|
208
|
+
* How many steps stay on screen above and below the active one, with the
|
|
209
|
+
* rest folded behind a toggle. `null` shows the whole trail.
|
|
210
|
+
*/
|
|
211
|
+
visibleStepRange: { type: Number, default: null },
|
|
212
|
+
/**
|
|
213
|
+
* Start collapsed: the status, a dot per step, and the active step's controls
|
|
214
|
+
* on one line. Supports `v-model:compact`, and the panel's own chevron drives
|
|
215
|
+
* it either way.
|
|
216
|
+
*/
|
|
217
|
+
compact: { type: Boolean, default: false },
|
|
218
|
+
/**
|
|
219
|
+
* Whether the chevron that switches between the two forms is offered at all.
|
|
220
|
+
* `null` means "whenever the panel was asked for compact" — a caller that
|
|
221
|
+
* wants the control but the full trail first passes `true` explicitly.
|
|
222
|
+
*/
|
|
223
|
+
collapsible: { type: Boolean, default: null }
|
|
136
224
|
})
|
|
137
225
|
|
|
138
|
-
const emits = defineEmits(['refresh', 'isLoading'])
|
|
226
|
+
const emits = defineEmits(['refresh', 'isLoading', 'update:compact'])
|
|
139
227
|
|
|
140
228
|
const isLoading = ref(false)
|
|
141
229
|
const auditTimeline = ref(null)
|
|
142
230
|
const historyOpen = ref(false)
|
|
143
231
|
const historyCount = ref(null)
|
|
232
|
+
const stepsExpanded = ref(false)
|
|
233
|
+
const isCompact = ref(props.compact)
|
|
144
234
|
|
|
145
235
|
const messages = useMessages()
|
|
146
236
|
|
|
@@ -153,8 +243,25 @@ const SKELETON_ROWS = [
|
|
|
153
243
|
{ name: '78px', meta: '48px', delay: '0.2s', metaDelay: '0.35s' }
|
|
154
244
|
]
|
|
155
245
|
|
|
246
|
+
// The prop leads when the host drives it, and the chevron writes back through
|
|
247
|
+
// `update:compact` — so `v-model:compact` and a plain default both behave the
|
|
248
|
+
// way a caller expects.
|
|
249
|
+
watch(() => props.compact, (value) => (isCompact.value = value))
|
|
250
|
+
|
|
251
|
+
const setCompact = (value) => {
|
|
252
|
+
isCompact.value = value
|
|
253
|
+
emits('update:compact', value)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const isCollapsible = computed(() => props.collapsible ?? props.compact)
|
|
257
|
+
|
|
156
258
|
const approval = computed(() => createApprovableViewModel(props.value))
|
|
157
259
|
|
|
260
|
+
const stepWindow = computed(() => createStepWindow(approval.value.steps, {
|
|
261
|
+
range: props.visibleStepRange,
|
|
262
|
+
isExpanded: stepsExpanded.value
|
|
263
|
+
}))
|
|
264
|
+
|
|
158
265
|
/** Names the dialog, and once the history has loaded, counts it. */
|
|
159
266
|
const historyTitle = computed(() => (
|
|
160
267
|
historyCount.value === null
|
|
@@ -165,31 +272,16 @@ const historyTitle = computed(() => (
|
|
|
165
272
|
/** Nothing to veil yet, so the panel shows its own shape instead. */
|
|
166
273
|
const isInitialLoad = computed(() => isLoading.value && !approval.value.steps.length)
|
|
167
274
|
|
|
275
|
+
const rootClass = computed(() => ({
|
|
276
|
+
approvable: true,
|
|
277
|
+
'approvable--compact': isCompact.value
|
|
278
|
+
}))
|
|
279
|
+
|
|
168
280
|
const approvableStepsClass = computed(() => ({
|
|
169
281
|
'approvable-steps': true,
|
|
170
282
|
'approvable-steps--row': props.isDirectionRow
|
|
171
283
|
}))
|
|
172
284
|
|
|
173
|
-
/** "Step 2 of 3 — awaiting Priya Raman", with the tail dropped when nothing is pending. */
|
|
174
|
-
const progressLabel = computed(() => {
|
|
175
|
-
const { activeStepNumber, visibleStepCount, activeApproverName } = approval.value
|
|
176
|
-
|
|
177
|
-
if (!activeStepNumber || !visibleStepCount) {
|
|
178
|
-
return null
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const progress = formatMessage(messages.stepProgress, {
|
|
182
|
-
current: activeStepNumber,
|
|
183
|
-
total: visibleStepCount
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
if (!activeApproverName) {
|
|
187
|
-
return progress
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return `${progress} — ${formatMessage(messages.awaitingApprover, { name: activeApproverName })}`
|
|
191
|
-
})
|
|
192
|
-
|
|
193
285
|
const onRefresh = () => {
|
|
194
286
|
auditTimeline.value?.refresh()
|
|
195
287
|
emits('refresh')
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="approvable-compact">
|
|
3
|
+
<div class="approvable-compact-main">
|
|
4
|
+
<div class="approvable-compact-line">
|
|
5
|
+
<approve-status-info
|
|
6
|
+
v-if="showApprovableStatus"
|
|
7
|
+
:label="approval.statusName"
|
|
8
|
+
:value="approval.status"
|
|
9
|
+
/>
|
|
10
|
+
|
|
11
|
+
<!-- One dot per visible step, in workflow order: the strip is the only
|
|
12
|
+
thing the collapsed form keeps of the trail, so it has to carry
|
|
13
|
+
both how many steps there are and where each one landed. -->
|
|
14
|
+
<span
|
|
15
|
+
v-if="compact.dots.length"
|
|
16
|
+
class="approvable-compact-dots"
|
|
17
|
+
role="img"
|
|
18
|
+
:aria-label="stepsLabel"
|
|
19
|
+
>
|
|
20
|
+
<span
|
|
21
|
+
v-for="dot in compact.dots"
|
|
22
|
+
:key="dot.id"
|
|
23
|
+
:class="dotClass(dot)"
|
|
24
|
+
:title="dotTitle(dot)"
|
|
25
|
+
/>
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<p v-if="focus" class="approvable-compact-who">
|
|
30
|
+
<slot :step="compact.focusStep" name="approvableStepInfo">
|
|
31
|
+
<span class="approvable-compact-name">{{ focus.approverName }}</span>
|
|
32
|
+
</slot>
|
|
33
|
+
<slot :role="focus.approverRole" :step="compact.focusStep" name="approvableStepRole">
|
|
34
|
+
<template v-if="focus.approverRole">
|
|
35
|
+
<span aria-hidden="true" class="approvable-compact-sep">·</span>{{ focus.approverRole }}
|
|
36
|
+
</template>
|
|
37
|
+
</slot>
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="approvable-compact-side">
|
|
42
|
+
<!-- Only the step being waited on is actionable; a finished approval
|
|
43
|
+
collapses to its outcome and nothing to press. -->
|
|
44
|
+
<approvable-step-actions
|
|
45
|
+
v-if="compact.hasActiveStep && compact.focusStep"
|
|
46
|
+
:approvable="approval.approvable"
|
|
47
|
+
:resource="value"
|
|
48
|
+
:value="compact.focusStep"
|
|
49
|
+
@is-loading="(isLoading) => emits('isLoading', isLoading)"
|
|
50
|
+
@refresh="emits('refresh')"
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
class="approvable-compact-toggle"
|
|
56
|
+
:aria-expanded="false"
|
|
57
|
+
:aria-label="messages.compactExpand"
|
|
58
|
+
:title="messages.compactExpand"
|
|
59
|
+
@click="emits('expand')"
|
|
60
|
+
>
|
|
61
|
+
<svg
|
|
62
|
+
aria-hidden="true"
|
|
63
|
+
class="approvable-icon"
|
|
64
|
+
fill="none"
|
|
65
|
+
stroke="currentColor"
|
|
66
|
+
stroke-linecap="round"
|
|
67
|
+
stroke-linejoin="round"
|
|
68
|
+
stroke-width="2"
|
|
69
|
+
viewBox="0 0 16 16"
|
|
70
|
+
>
|
|
71
|
+
<path d="M4.5 6.4 8 9.9l3.5-3.5" />
|
|
72
|
+
</svg>
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<script setup>
|
|
79
|
+
import { computed } from 'vue'
|
|
80
|
+
import {
|
|
81
|
+
createApprovableViewModel,
|
|
82
|
+
createCompactViewModel,
|
|
83
|
+
formatMessage
|
|
84
|
+
} from '@stsdti/approvable-core'
|
|
85
|
+
import ApprovableStepActions from './ApprovableStepActions.vue'
|
|
86
|
+
import ApproveStatusInfo from './ApproveStatusInfo.vue'
|
|
87
|
+
import { useMessages } from '../../messages.js'
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The collapsed form: status pill, a dot per step, who it is waiting on, and
|
|
91
|
+
* that step's controls — one row, for a header or a table cell where the full
|
|
92
|
+
* trail does not fit. The caller swaps it for {@link Approvable}'s full list
|
|
93
|
+
* on `expand`.
|
|
94
|
+
*/
|
|
95
|
+
const props = defineProps({
|
|
96
|
+
value: {
|
|
97
|
+
type: Object,
|
|
98
|
+
required: true
|
|
99
|
+
},
|
|
100
|
+
showApprovableStatus: { type: Boolean, default: true }
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const emits = defineEmits(['expand', 'isLoading', 'refresh'])
|
|
104
|
+
|
|
105
|
+
const messages = useMessages()
|
|
106
|
+
|
|
107
|
+
const approval = computed(() => createApprovableViewModel(props.value))
|
|
108
|
+
|
|
109
|
+
const compact = computed(() => createCompactViewModel(approval.value.steps))
|
|
110
|
+
|
|
111
|
+
const focus = computed(() => compact.value.focus)
|
|
112
|
+
|
|
113
|
+
/** "Step 5 of 9" — the strip's accessible name, and its own tooltip. */
|
|
114
|
+
const stepsLabel = computed(() => formatMessage(messages.stepProgress, {
|
|
115
|
+
current: compact.value.focusStepNumber ?? 0,
|
|
116
|
+
total: compact.value.stepCount
|
|
117
|
+
}))
|
|
118
|
+
|
|
119
|
+
const dotClass = (dot) => ({
|
|
120
|
+
'approvable-compact-dot': true,
|
|
121
|
+
[`approvable-compact-dot--${dot.tone}`]: true
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
/** Hovering a dot names the step it stands for, so the strip is readable. */
|
|
125
|
+
const dotTitle = (dot) => [
|
|
126
|
+
formatMessage(messages.stepProgress, { current: dot.number, total: compact.value.stepCount }),
|
|
127
|
+
dot.approverName,
|
|
128
|
+
dot.statusName
|
|
129
|
+
].filter(Boolean).join(' — ')
|
|
130
|
+
</script>
|
|
@@ -36,59 +36,13 @@
|
|
|
36
36
|
<time v-if="step.performedAt" class="approvable-step-date">{{ step.performedAt }}</time>
|
|
37
37
|
</span>
|
|
38
38
|
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
:action-ui="button.action_ui"
|
|
47
|
-
:approvable="approvable"
|
|
48
|
-
:button="button"
|
|
49
|
-
:buttons="[button]"
|
|
50
|
-
:complete="onApproveStatusChange"
|
|
51
|
-
:resource="resource"
|
|
52
|
-
:step="value"
|
|
53
|
-
@is-loading="(isLoading) => emits('isLoading', isLoading)"
|
|
54
|
-
@refresh="emits('refresh')"
|
|
55
|
-
/>
|
|
56
|
-
<component
|
|
57
|
-
:is="linkComponent"
|
|
58
|
-
v-else-if="button.link"
|
|
59
|
-
v-bind="linkProps(linkComponent, button.link)"
|
|
60
|
-
>
|
|
61
|
-
<button
|
|
62
|
-
:title="button.tooltip"
|
|
63
|
-
:class="buttonClass(button)"
|
|
64
|
-
type="button"
|
|
65
|
-
@click="onApproveStatusChange(button)"
|
|
66
|
-
>
|
|
67
|
-
<approvable-icon :icon="button.icon" />
|
|
68
|
-
</button>
|
|
69
|
-
</component>
|
|
70
|
-
<button
|
|
71
|
-
v-else
|
|
72
|
-
:title="button.tooltip"
|
|
73
|
-
:class="buttonClass(button)"
|
|
74
|
-
type="button"
|
|
75
|
-
@click="onApproveStatusChange(button)"
|
|
76
|
-
>
|
|
77
|
-
<approvable-icon :icon="button.icon" />
|
|
78
|
-
{{ button.text }}
|
|
79
|
-
</button>
|
|
80
|
-
</template>
|
|
81
|
-
</div>
|
|
82
|
-
|
|
83
|
-
<div v-if="value.can_comment" class="approvable-btn-group">
|
|
84
|
-
<approvable-step-comment
|
|
85
|
-
:approvable-step="value"
|
|
86
|
-
:disabled="!value.can_comment"
|
|
87
|
-
@refresh="emits('refresh')"
|
|
88
|
-
@is-loading="(isLoading) => emits('isLoading', isLoading)"
|
|
89
|
-
/>
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
39
|
+
<approvable-step-actions
|
|
40
|
+
:approvable="approvable"
|
|
41
|
+
:resource="resource"
|
|
42
|
+
:value="value"
|
|
43
|
+
@is-loading="(isLoading) => emits('isLoading', isLoading)"
|
|
44
|
+
@refresh="emits('refresh')"
|
|
45
|
+
/>
|
|
92
46
|
</div>
|
|
93
47
|
|
|
94
48
|
<p v-if="step.comment.text" class="approvable-step-comment">
|
|
@@ -112,13 +66,9 @@ import { computed, ref } from 'vue'
|
|
|
112
66
|
import {
|
|
113
67
|
approvableStatusTone,
|
|
114
68
|
approvableStepGlyph,
|
|
115
|
-
createStepViewModel
|
|
116
|
-
performStepAction
|
|
69
|
+
createStepViewModel
|
|
117
70
|
} from '@stsdti/approvable-core'
|
|
118
|
-
import
|
|
119
|
-
import ApprovableStepComment from './ApprovableStepComment.vue'
|
|
120
|
-
import { approvableActionRenderer } from './action-renderers.js'
|
|
121
|
-
import { linkProps, useLinkComponent } from '../../config.js'
|
|
71
|
+
import ApprovableStepActions from './ApprovableStepActions.vue'
|
|
122
72
|
import { useMessages } from '../../messages.js'
|
|
123
73
|
|
|
124
74
|
const emits = defineEmits(['isLoading', 'refresh'])
|
|
@@ -138,7 +88,6 @@ const props = defineProps({
|
|
|
138
88
|
}
|
|
139
89
|
})
|
|
140
90
|
|
|
141
|
-
const linkComponent = useLinkComponent()
|
|
142
91
|
const messages = useMessages()
|
|
143
92
|
|
|
144
93
|
const isCommentExpanded = ref(false)
|
|
@@ -197,39 +146,4 @@ const discClass = computed(() => ({
|
|
|
197
146
|
const commentText = computed(() => (
|
|
198
147
|
isCommentExpanded.value ? step.value.comment.text : step.value.comment.preview
|
|
199
148
|
))
|
|
200
|
-
|
|
201
|
-
const buttonClass = (button) => ({
|
|
202
|
-
'approvable-btn': true,
|
|
203
|
-
// A button carrying only an icon collapses to a circle, so the row keeps its
|
|
204
|
-
// rhythm instead of stretching around an empty label.
|
|
205
|
-
'approvable-btn--icon': !button.text,
|
|
206
|
-
'approvable-btn--primary': button.result === 'approved'
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
const buttonRenderer = (button) => {
|
|
210
|
-
if (!step.value.isActive) {
|
|
211
|
-
return null
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return approvableActionRenderer(button?.action_ui)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const onApproveStatusChange = async (buttonOrId) => {
|
|
218
|
-
emits('isLoading', true)
|
|
219
|
-
|
|
220
|
-
const result = await performStepAction({
|
|
221
|
-
stepId: step.value.id,
|
|
222
|
-
button: buttonOrId,
|
|
223
|
-
buttons: step.value.buttons
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
emits('isLoading', false)
|
|
227
|
-
|
|
228
|
-
// Refresh after any attempted request, success or failure — a failed action
|
|
229
|
-
// still means the local state may be stale. Only a skipped one (no id, no
|
|
230
|
-
// button, or a presentational button) reloads nothing, since it sent nothing.
|
|
231
|
-
if (!result.skipped) {
|
|
232
|
-
emits('refresh')
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
149
|
</script>
|