@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
|
@@ -1,81 +1,154 @@
|
|
|
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="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
|
+
<div class="approvable-footer" aria-hidden="true">
|
|
128
|
+
<span class="approvable-skeleton-bar approvable-skeleton-bar--soft" style="display:block;width:132px" />
|
|
129
|
+
</div>
|
|
130
|
+
</template>
|
|
131
|
+
|
|
132
|
+
<template v-else>
|
|
133
|
+
<ol v-if="stepWindow.steps.length" :class="approvableStepsClass">
|
|
134
|
+
<approvable-step
|
|
135
|
+
v-for="approvableStep of stepWindow.steps"
|
|
136
|
+
:key="approvableStep.id"
|
|
137
|
+
:approvable="approval.approvable"
|
|
138
|
+
:resource="value"
|
|
139
|
+
:value="approvableStep"
|
|
140
|
+
@isLoading="(value) => (isLoading = value)"
|
|
141
|
+
@refresh="onRefresh"
|
|
142
|
+
>
|
|
143
|
+
<template v-for="slot in Object.keys($slots)" v-slot:[slot]="scoped">
|
|
144
|
+
<slot v-bind="scoped ?? {}" :name="slot" />
|
|
145
|
+
</template>
|
|
146
|
+
</approvable-step>
|
|
147
|
+
|
|
148
|
+
</ol>
|
|
77
149
|
|
|
78
|
-
|
|
150
|
+
<p v-if="progressLabel" class="approvable-footer">{{ progressLabel }}</p>
|
|
151
|
+
</template>
|
|
79
152
|
</template>
|
|
80
153
|
|
|
81
154
|
<Teleport to="body">
|
|
@@ -121,9 +194,10 @@
|
|
|
121
194
|
</template>
|
|
122
195
|
|
|
123
196
|
<script setup>
|
|
124
|
-
import { ref, computed } from 'vue'
|
|
125
|
-
import { createApprovableViewModel, formatMessage } from '@stsdti/approvable-core'
|
|
197
|
+
import { ref, computed, watch } from 'vue'
|
|
198
|
+
import { createApprovableViewModel, createStepWindow, formatMessage } from '@stsdti/approvable-core'
|
|
126
199
|
import ApprovableAuditTimeline from './ApprovableAuditTimeline.vue'
|
|
200
|
+
import ApprovableCompact from './ApprovableCompact.vue'
|
|
127
201
|
import ApprovableStep from './ApprovableStep.vue'
|
|
128
202
|
import ApproveStatusInfo from './ApproveStatusInfo.vue'
|
|
129
203
|
import { useMessages } from '../../messages.js'
|
|
@@ -132,15 +206,34 @@ const props = defineProps({
|
|
|
132
206
|
value: {},
|
|
133
207
|
showApprovableStatus: { type: Boolean, default: true },
|
|
134
208
|
title: { default: 'Approval' },
|
|
135
|
-
isDirectionRow: { default: false }
|
|
209
|
+
isDirectionRow: { default: false },
|
|
210
|
+
/**
|
|
211
|
+
* How many steps stay on screen above and below the active one, with the
|
|
212
|
+
* rest folded behind a toggle. `null` shows the whole trail.
|
|
213
|
+
*/
|
|
214
|
+
visibleStepRange: { type: Number, default: null },
|
|
215
|
+
/**
|
|
216
|
+
* Start collapsed: the status, a dot per step, and the active step's controls
|
|
217
|
+
* on one line. Supports `v-model:compact`, and the panel's own chevron drives
|
|
218
|
+
* it either way.
|
|
219
|
+
*/
|
|
220
|
+
compact: { type: Boolean, default: false },
|
|
221
|
+
/**
|
|
222
|
+
* Whether the chevron that switches between the two forms is offered at all.
|
|
223
|
+
* `null` means "whenever the panel was asked for compact" — a caller that
|
|
224
|
+
* wants the control but the full trail first passes `true` explicitly.
|
|
225
|
+
*/
|
|
226
|
+
collapsible: { type: Boolean, default: null }
|
|
136
227
|
})
|
|
137
228
|
|
|
138
|
-
const emits = defineEmits(['refresh', 'isLoading'])
|
|
229
|
+
const emits = defineEmits(['refresh', 'isLoading', 'update:compact'])
|
|
139
230
|
|
|
140
231
|
const isLoading = ref(false)
|
|
141
232
|
const auditTimeline = ref(null)
|
|
142
233
|
const historyOpen = ref(false)
|
|
143
234
|
const historyCount = ref(null)
|
|
235
|
+
const stepsExpanded = ref(false)
|
|
236
|
+
const isCompact = ref(props.compact)
|
|
144
237
|
|
|
145
238
|
const messages = useMessages()
|
|
146
239
|
|
|
@@ -153,8 +246,25 @@ const SKELETON_ROWS = [
|
|
|
153
246
|
{ name: '78px', meta: '48px', delay: '0.2s', metaDelay: '0.35s' }
|
|
154
247
|
]
|
|
155
248
|
|
|
249
|
+
// The prop leads when the host drives it, and the chevron writes back through
|
|
250
|
+
// `update:compact` — so `v-model:compact` and a plain default both behave the
|
|
251
|
+
// way a caller expects.
|
|
252
|
+
watch(() => props.compact, (value) => (isCompact.value = value))
|
|
253
|
+
|
|
254
|
+
const setCompact = (value) => {
|
|
255
|
+
isCompact.value = value
|
|
256
|
+
emits('update:compact', value)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const isCollapsible = computed(() => props.collapsible ?? props.compact)
|
|
260
|
+
|
|
156
261
|
const approval = computed(() => createApprovableViewModel(props.value))
|
|
157
262
|
|
|
263
|
+
const stepWindow = computed(() => createStepWindow(approval.value.steps, {
|
|
264
|
+
range: props.visibleStepRange,
|
|
265
|
+
isExpanded: stepsExpanded.value
|
|
266
|
+
}))
|
|
267
|
+
|
|
158
268
|
/** Names the dialog, and once the history has loaded, counts it. */
|
|
159
269
|
const historyTitle = computed(() => (
|
|
160
270
|
historyCount.value === null
|
|
@@ -165,6 +275,11 @@ const historyTitle = computed(() => (
|
|
|
165
275
|
/** Nothing to veil yet, so the panel shows its own shape instead. */
|
|
166
276
|
const isInitialLoad = computed(() => isLoading.value && !approval.value.steps.length)
|
|
167
277
|
|
|
278
|
+
const rootClass = computed(() => ({
|
|
279
|
+
approvable: true,
|
|
280
|
+
'approvable--compact': isCompact.value
|
|
281
|
+
}))
|
|
282
|
+
|
|
168
283
|
const approvableStepsClass = computed(() => ({
|
|
169
284
|
'approvable-steps': true,
|
|
170
285
|
'approvable-steps--row': props.isDirectionRow
|
|
@@ -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>
|