wip
This commit is contained in:
@@ -0,0 +1,424 @@
|
||||
<script setup>
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiDeleteOutline,
|
||||
mdiPlus,
|
||||
} from '@mdi/js';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
members: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
errors: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
labels: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const roleOptions = [
|
||||
'administrator',
|
||||
'manager',
|
||||
'workspace-member',
|
||||
'client',
|
||||
'provider',
|
||||
];
|
||||
const membershipOptions = ['Team', 'Client'];
|
||||
const targetTypes = ['Role', 'Membership', 'Member'];
|
||||
|
||||
function emitSteps(steps) {
|
||||
emit('update:modelValue', steps.map((step, index) => ({
|
||||
...step,
|
||||
sortOrder: index,
|
||||
})));
|
||||
}
|
||||
|
||||
function createStep() {
|
||||
emitSteps([
|
||||
...props.modelValue,
|
||||
{
|
||||
name: props.labels.defaultStepName(props.modelValue.length + 1),
|
||||
sortOrder: props.modelValue.length,
|
||||
targetType: 'Role',
|
||||
targetValue: 'manager',
|
||||
requiredApproverCount: 1,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function updateStep(index, updates) {
|
||||
const steps = props.modelValue.map((step, stepIndex) => {
|
||||
if (stepIndex !== index) {
|
||||
return step;
|
||||
}
|
||||
|
||||
const nextStep = {
|
||||
...step,
|
||||
...updates,
|
||||
};
|
||||
|
||||
if (updates.targetType) {
|
||||
nextStep.targetValue = defaultTargetValue(updates.targetType);
|
||||
}
|
||||
|
||||
return nextStep;
|
||||
});
|
||||
|
||||
emitSteps(steps);
|
||||
}
|
||||
|
||||
function defaultTargetValue(targetType) {
|
||||
if (targetType === 'Membership') {
|
||||
return membershipOptions[0];
|
||||
}
|
||||
|
||||
if (targetType === 'Member') {
|
||||
return props.members[0]?.id ?? '';
|
||||
}
|
||||
|
||||
return roleOptions[1];
|
||||
}
|
||||
|
||||
function getSelectedMemberIds(step) {
|
||||
return (step.targetValue ?? '')
|
||||
.split(',')
|
||||
.map(value => value.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function updateMemberTargets(index, selectedOptions) {
|
||||
const targetValue = Array.from(selectedOptions)
|
||||
.map(option => option.value)
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
|
||||
updateStep(index, { targetValue });
|
||||
}
|
||||
|
||||
function moveStep(index, offset) {
|
||||
const nextIndex = index + offset;
|
||||
|
||||
if (nextIndex < 0 || nextIndex >= props.modelValue.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const steps = [...props.modelValue];
|
||||
const [step] = steps.splice(index, 1);
|
||||
steps.splice(nextIndex, 0, step);
|
||||
emitSteps(steps);
|
||||
}
|
||||
|
||||
function removeStep(index) {
|
||||
emitSteps(props.modelValue.filter((_, stepIndex) => stepIndex !== index));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="approval-workflow-editor">
|
||||
<div class="approval-editor-header">
|
||||
<div>
|
||||
<strong>{{ labels.title }}</strong>
|
||||
<span>{{ labels.description }}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="secondary-button"
|
||||
:disabled="disabled"
|
||||
@click="createStep"
|
||||
>
|
||||
<v-icon :icon="mdiPlus" />
|
||||
<span>{{ labels.addStep }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!modelValue.length"
|
||||
class="approval-empty"
|
||||
>
|
||||
{{ labels.empty }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="approval-step-list"
|
||||
>
|
||||
<section
|
||||
v-for="(step, index) in modelValue"
|
||||
:key="step.id ?? `${index}-${step.sortOrder}`"
|
||||
class="approval-step-card"
|
||||
>
|
||||
<div class="approval-step-heading">
|
||||
<div>
|
||||
<small>{{ labels.stepNumber(index + 1) }}</small>
|
||||
<strong>{{ step.name || labels.unnamedStep }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="approval-step-actions">
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="labels.moveUp"
|
||||
:disabled="disabled || index === 0"
|
||||
@click="moveStep(index, -1)"
|
||||
>
|
||||
<v-icon :icon="mdiArrowUp" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="labels.moveDown"
|
||||
:disabled="disabled || index === modelValue.length - 1"
|
||||
@click="moveStep(index, 1)"
|
||||
>
|
||||
<v-icon :icon="mdiArrowDown" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="labels.removeStep"
|
||||
:disabled="disabled"
|
||||
@click="removeStep(index)"
|
||||
>
|
||||
<v-icon :icon="mdiDeleteOutline" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="approval-step-fields">
|
||||
<label class="field">
|
||||
<span>{{ labels.fields.name }}</span>
|
||||
<input
|
||||
:value="step.name"
|
||||
type="text"
|
||||
:disabled="disabled"
|
||||
@input="updateStep(index, { name: $event.target.value })"
|
||||
/>
|
||||
<small
|
||||
v-if="errors[index]?.name"
|
||||
class="field-error"
|
||||
>
|
||||
{{ errors[index].name }}
|
||||
</small>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>{{ labels.fields.targetType }}</span>
|
||||
<select
|
||||
:value="step.targetType"
|
||||
:disabled="disabled"
|
||||
@change="updateStep(index, { targetType: $event.target.value })"
|
||||
>
|
||||
<option
|
||||
v-for="targetType in targetTypes"
|
||||
:key="targetType"
|
||||
:value="targetType"
|
||||
>
|
||||
{{ labels.targetTypes[targetType] }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>{{ labels.fields.targetValue }}</span>
|
||||
<select
|
||||
v-if="step.targetType === 'Role'"
|
||||
:value="step.targetValue"
|
||||
:disabled="disabled"
|
||||
@change="updateStep(index, { targetValue: $event.target.value })"
|
||||
>
|
||||
<option
|
||||
v-for="role in roleOptions"
|
||||
:key="role"
|
||||
:value="role"
|
||||
>
|
||||
{{ labels.roles[role] }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
v-else-if="step.targetType === 'Membership'"
|
||||
:value="step.targetValue"
|
||||
:disabled="disabled"
|
||||
@change="updateStep(index, { targetValue: $event.target.value })"
|
||||
>
|
||||
<option
|
||||
v-for="membership in membershipOptions"
|
||||
:key="membership"
|
||||
:value="membership"
|
||||
>
|
||||
{{ labels.memberships[membership] }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
v-else
|
||||
:value="getSelectedMemberIds(step)"
|
||||
:disabled="disabled"
|
||||
multiple
|
||||
size="5"
|
||||
@change="updateMemberTargets(index, $event.target.selectedOptions)"
|
||||
>
|
||||
<option
|
||||
v-for="member in members"
|
||||
:key="member.id"
|
||||
:value="member.id"
|
||||
>
|
||||
{{ member.displayName }} - {{ member.email }}
|
||||
</option>
|
||||
</select>
|
||||
<small
|
||||
v-if="step.targetType === 'Member'"
|
||||
class="field-help"
|
||||
>
|
||||
{{ labels.selectMembers }}
|
||||
</small>
|
||||
|
||||
<small
|
||||
v-if="errors[index]?.targetValue"
|
||||
class="field-error"
|
||||
>
|
||||
{{ errors[index].targetValue }}
|
||||
</small>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>{{ labels.fields.requiredApproverCount }}</span>
|
||||
<input
|
||||
:value="step.requiredApproverCount"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
:disabled="disabled"
|
||||
@input="updateStep(index, { requiredApproverCount: Number($event.target.value) })"
|
||||
/>
|
||||
<small
|
||||
v-if="errors[index]?.requiredApproverCount"
|
||||
class="field-error"
|
||||
>
|
||||
{{ errors[index].requiredApproverCount }}
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.approval-workflow-editor {
|
||||
@apply flex flex-col gap-3;
|
||||
}
|
||||
|
||||
.approval-editor-header {
|
||||
@apply flex flex-col gap-3 rounded-[1rem] border px-4 py-4 sm:flex-row sm:items-center sm:justify-between;
|
||||
background: #fffaf2;
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
}
|
||||
|
||||
.approval-editor-header div,
|
||||
.approval-step-heading div:first-child {
|
||||
@apply flex min-w-0 flex-col gap-1;
|
||||
}
|
||||
|
||||
.approval-editor-header strong,
|
||||
.approval-step-heading strong {
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.approval-editor-header span,
|
||||
.approval-empty,
|
||||
.approval-step-heading small {
|
||||
@apply text-sm leading-6;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.approval-step-list {
|
||||
@apply flex flex-col gap-3;
|
||||
}
|
||||
|
||||
.approval-empty,
|
||||
.approval-step-card {
|
||||
@apply rounded-[1rem] border px-4 py-4;
|
||||
background: #fffaf2;
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
}
|
||||
|
||||
.approval-step-card {
|
||||
@apply flex flex-col gap-4;
|
||||
}
|
||||
|
||||
.approval-step-heading {
|
||||
@apply flex items-start justify-between gap-3;
|
||||
}
|
||||
|
||||
.approval-step-actions {
|
||||
@apply flex flex-shrink-0 gap-2;
|
||||
}
|
||||
|
||||
.approval-step-actions button {
|
||||
@apply inline-flex h-9 w-9 items-center justify-center rounded-full;
|
||||
background: rgba(23, 32, 51, 0.08);
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.approval-step-actions button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.42;
|
||||
}
|
||||
|
||||
.approval-step-fields {
|
||||
@apply grid gap-3 md:grid-cols-2;
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-full px-4 py-2 text-sm font-semibold;
|
||||
background: rgba(23, 32, 51, 0.08);
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.secondary-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.56;
|
||||
}
|
||||
|
||||
.field {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
.field span {
|
||||
@apply text-sm font-semibold;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field select {
|
||||
@apply rounded-[1rem] border px-4 py-3 text-sm;
|
||||
background: #fffdf8;
|
||||
border-color: rgba(23, 32, 51, 0.1);
|
||||
color: #172033;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.field-error {
|
||||
@apply text-sm leading-6;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.field-help {
|
||||
@apply text-sm leading-6;
|
||||
color: #526178;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user