364 lines
14 KiB
Vue
364 lines
14 KiB
Vue
<script setup lang="ts">
|
|
import { useForm } from '@inertiajs/vue3';
|
|
import { computed, ref, watch } from 'vue';
|
|
import {
|
|
destroyPriority,
|
|
destroyType,
|
|
storePriority,
|
|
storeType,
|
|
updatePriority,
|
|
updateType,
|
|
} from '@/actions/App/Http/Controllers/ProjectTicketMetadataSettingsController';
|
|
import FormFeedback from '@/components/FormFeedback.vue';
|
|
import InputError from '@/components/InputError.vue';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { useFormProtection } from '@/composables/useFormProtection';
|
|
import { useLocalization } from '@/composables/useLocalization';
|
|
import type { TicketPriorityModel } from '@/types/models/ticket-priority-model';
|
|
import type { TicketTypeModel } from '@/types/models/ticket-type-model';
|
|
import type { MetadataEditorTarget } from '@/types/project-metadata';
|
|
|
|
const props = defineProps<{
|
|
projectId: number;
|
|
target: MetadataEditorTarget | null;
|
|
types: TicketTypeModel[];
|
|
priorities: TicketPriorityModel[];
|
|
}>();
|
|
const emit = defineEmits<{ close: [] }>();
|
|
const { t } = useLocalization();
|
|
const form = useForm({
|
|
name: '',
|
|
position: 1,
|
|
is_parent: false,
|
|
replacement_ticket_type_id: '',
|
|
});
|
|
const protection = useFormProtection(form, {
|
|
isDirty: () => props.target !== null && form.isDirty,
|
|
});
|
|
const deletionError = ref<string | null>(null);
|
|
const deleting = computed(() => props.target?.action === 'delete');
|
|
const entries = computed(() =>
|
|
props.target?.kind === 'type' ? props.types : props.priorities,
|
|
);
|
|
const replacements = computed(() =>
|
|
props.types.filter((type) => type.id !== props.target?.entry?.id),
|
|
);
|
|
const needsReplacement = computed(
|
|
() =>
|
|
deleting.value &&
|
|
props.target?.kind === 'type' &&
|
|
(props.target.entry?.tickets_count ?? 0) > 0,
|
|
);
|
|
const title = computed(() => {
|
|
const target = props.target;
|
|
if (!target) {
|
|
return '';
|
|
}
|
|
if (target.action === 'create') {
|
|
return t(
|
|
target.kind === 'type'
|
|
? 'metadata.addType'
|
|
: 'metadata.addPriority',
|
|
);
|
|
}
|
|
return t(
|
|
target.action === 'edit'
|
|
? 'metadata.editEntry'
|
|
: 'metadata.deleteEntry',
|
|
{ name: target.entry.name },
|
|
);
|
|
});
|
|
watch(
|
|
() => props.target,
|
|
(target) => {
|
|
if (!target) {
|
|
return;
|
|
}
|
|
form.defaults({
|
|
name: target.entry?.name ?? '',
|
|
position: target.entry?.position ?? 1,
|
|
is_parent: target.entry?.is_parent ?? false,
|
|
replacement_ticket_type_id: '',
|
|
});
|
|
form.resetAndClearErrors();
|
|
protection.clearFeedback();
|
|
deletionError.value = null;
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
function close(open: boolean): void {
|
|
if (open || form.processing || !protection.confirmLeave()) {
|
|
return;
|
|
}
|
|
form.resetAndClearErrors();
|
|
protection.clearFeedback();
|
|
emit('close');
|
|
}
|
|
function submit(): void {
|
|
const target = props.target;
|
|
if (
|
|
!target ||
|
|
form.processing ||
|
|
(deleting.value && entries.value.length <= 1)
|
|
) {
|
|
return;
|
|
}
|
|
const route =
|
|
target.kind === 'type'
|
|
? target.action === 'create'
|
|
? storeType(props.projectId)
|
|
: target.action === 'edit'
|
|
? updateType([props.projectId, target.entry.id])
|
|
: destroyType([props.projectId, target.entry.id])
|
|
: target.action === 'create'
|
|
? storePriority(props.projectId)
|
|
: target.action === 'edit'
|
|
? updatePriority([props.projectId, target.entry.id])
|
|
: destroyPriority([props.projectId, target.entry.id]);
|
|
form.transform((data) =>
|
|
target.action === 'delete'
|
|
? target.kind === 'type'
|
|
? {
|
|
replacement_ticket_type_id:
|
|
data.replacement_ticket_type_id || null,
|
|
}
|
|
: {}
|
|
: target.action === 'edit'
|
|
? { name: data.name, position: data.position }
|
|
: target.kind === 'type'
|
|
? { name: data.name, is_parent: data.is_parent }
|
|
: { name: data.name },
|
|
);
|
|
deletionError.value = null;
|
|
protection.submit(route, {
|
|
preserveScroll: true,
|
|
onSuccess: (page) => {
|
|
if (page.props.error) {
|
|
deletionError.value =
|
|
typeof page.props.status === 'string'
|
|
? page.props.status
|
|
: t('common.error');
|
|
return;
|
|
}
|
|
form.resetAndClearErrors();
|
|
emit('close');
|
|
},
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Dialog :open="target !== null" @update:open="close">
|
|
<DialogContent
|
|
:show-close-button="!form.processing"
|
|
@escape-key-down="form.processing && $event.preventDefault()"
|
|
@interact-outside="form.processing && $event.preventDefault()"
|
|
>
|
|
<DialogHeader>
|
|
<DialogTitle class="break-words">{{ title }}</DialogTitle>
|
|
<DialogDescription>{{
|
|
t(
|
|
deleting
|
|
? target?.kind === 'type'
|
|
? 'metadata.deleteTypeHelp'
|
|
: 'metadata.deletePriorityHelp'
|
|
: 'metadata.editHelp',
|
|
)
|
|
}}</DialogDescription>
|
|
</DialogHeader>
|
|
<form
|
|
class="grid min-w-0 gap-4"
|
|
:aria-busy="form.processing"
|
|
@submit.prevent="submit"
|
|
>
|
|
<FormFeedback
|
|
:state="deletionError ? 'idle' : protection.state.value"
|
|
@retry="submit"
|
|
/>
|
|
<p
|
|
v-if="deletionError"
|
|
role="alert"
|
|
class="text-destructive text-sm"
|
|
>
|
|
{{ deletionError }}
|
|
</p>
|
|
<fieldset
|
|
:disabled="form.processing"
|
|
class="grid min-w-0 gap-4"
|
|
>
|
|
<template v-if="!deleting">
|
|
<div class="grid gap-2">
|
|
<Label for="metadata-name">{{
|
|
t('metadata.name')
|
|
}}</Label>
|
|
<Input
|
|
id="metadata-name"
|
|
v-model="form.name"
|
|
name="name"
|
|
required
|
|
maxlength="255"
|
|
:aria-invalid="!!form.errors.name"
|
|
aria-describedby="metadata-name-error"
|
|
/>
|
|
<InputError
|
|
id="metadata-name-error"
|
|
:message="form.errors.name"
|
|
/>
|
|
</div>
|
|
<div
|
|
v-if="target?.action === 'edit'"
|
|
class="grid gap-2"
|
|
>
|
|
<Label for="metadata-position">{{
|
|
t('workflow.position')
|
|
}}</Label>
|
|
<Input
|
|
id="metadata-position"
|
|
v-model="form.position"
|
|
name="position"
|
|
type="number"
|
|
min="1"
|
|
:max="entries.length"
|
|
step="1"
|
|
required
|
|
:aria-invalid="!!form.errors.position"
|
|
aria-describedby="metadata-position-help metadata-position-error"
|
|
/>
|
|
<p
|
|
id="metadata-position-help"
|
|
class="text-caption text-muted-foreground"
|
|
>
|
|
{{
|
|
t('metadata.positionHelp', {
|
|
count: entries.length,
|
|
})
|
|
}}
|
|
</p>
|
|
<InputError
|
|
id="metadata-position-error"
|
|
:message="form.errors.position"
|
|
/>
|
|
</div>
|
|
<div v-if="target?.kind === 'type'" class="grid gap-2">
|
|
<Label
|
|
v-if="target.action === 'create'"
|
|
for="metadata-parent"
|
|
class="flex items-center gap-3"
|
|
>
|
|
<Checkbox
|
|
id="metadata-parent"
|
|
v-model="form.is_parent"
|
|
name="is_parent"
|
|
:invalid="!!form.errors.is_parent"
|
|
aria-describedby="metadata-parent-help metadata-parent-error"
|
|
/>{{ t('metadata.allowChildren') }}
|
|
</Label>
|
|
<p v-else class="text-sm font-medium">
|
|
{{
|
|
t(
|
|
form.is_parent
|
|
? 'metadata.parentType'
|
|
: 'metadata.standardType',
|
|
)
|
|
}}
|
|
</p>
|
|
<p
|
|
id="metadata-parent-help"
|
|
class="text-caption text-muted-foreground"
|
|
>
|
|
{{ t('metadata.parentFixed') }}
|
|
</p>
|
|
<InputError
|
|
id="metadata-parent-error"
|
|
:message="form.errors.is_parent"
|
|
/>
|
|
</div>
|
|
</template>
|
|
<div v-if="needsReplacement" class="grid gap-2">
|
|
<p class="text-sm">
|
|
{{
|
|
t('metadata.replacementHelp', {
|
|
count: target?.entry?.tickets_count ?? 0,
|
|
})
|
|
}}
|
|
</p>
|
|
<Label for="metadata-replacement">{{
|
|
t('metadata.replacement')
|
|
}}</Label>
|
|
<select
|
|
id="metadata-replacement"
|
|
v-model="form.replacement_ticket_type_id"
|
|
name="replacement_ticket_type_id"
|
|
required
|
|
class="border-input bg-background focus-visible:ring-ring min-h-11 w-full min-w-0 rounded-md border px-3 text-sm focus-visible:ring-2"
|
|
:aria-invalid="
|
|
!!form.errors.replacement_ticket_type_id
|
|
"
|
|
aria-describedby="metadata-replacement-help metadata-replacement-error"
|
|
>
|
|
<option value="">
|
|
{{ t('metadata.chooseType') }}
|
|
</option>
|
|
<option
|
|
v-for="entry in replacements"
|
|
:key="entry.id"
|
|
:value="entry.id"
|
|
>
|
|
{{ entry.name
|
|
}}{{
|
|
entry.is_parent
|
|
? ` (${t('metadata.parentType')})`
|
|
: ''
|
|
}}
|
|
</option>
|
|
</select>
|
|
<p
|
|
id="metadata-replacement-help"
|
|
class="text-caption text-muted-foreground"
|
|
>
|
|
{{ t('metadata.parentReplacement') }}
|
|
</p>
|
|
</div>
|
|
<InputError
|
|
id="metadata-replacement-error"
|
|
:message="form.errors.replacement_ticket_type_id"
|
|
/>
|
|
</fieldset>
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
:disabled="form.processing"
|
|
@click="close(false)"
|
|
>{{ t('common.cancel') }}</Button
|
|
>
|
|
<Button
|
|
type="submit"
|
|
:variant="deleting ? 'destructive' : 'default'"
|
|
:loading="form.processing"
|
|
:disabled="deleting && entries.length <= 1"
|
|
>{{
|
|
deleting
|
|
? t('workflow.confirmDelete')
|
|
: target?.action === 'create'
|
|
? title
|
|
: t('common.save')
|
|
}}</Button
|
|
>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</template>
|