200 lines
7.2 KiB
Vue
200 lines
7.2 KiB
Vue
<script setup lang="ts">
|
|
import { useForm } from '@inertiajs/vue3';
|
|
import { computed, watch } from 'vue';
|
|
import {
|
|
updateTeamPermissions,
|
|
updateUserPermissions,
|
|
} from '@/actions/App/Http/Controllers/ProjectController';
|
|
import FormFeedback from '@/components/FormFeedback.vue';
|
|
import InputError from '@/components/InputError.vue';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { useFormProtection } from '@/composables/useFormProtection';
|
|
import { useLocalization } from '@/composables/useLocalization';
|
|
import type { PermissionOption } from '@/types';
|
|
import {
|
|
projectPermissionDescriptions,
|
|
projectPermissionLabels,
|
|
} from '@/types/project-access';
|
|
import type { ProjectAccessTarget } from '@/types/project-access';
|
|
|
|
const props = defineProps<{
|
|
projectId: number;
|
|
target: ProjectAccessTarget | null;
|
|
permissionOptions: PermissionOption[];
|
|
}>();
|
|
const emit = defineEmits<{ close: [] }>();
|
|
const { t } = useLocalization();
|
|
const form = useForm({ permissions: [] as string[] });
|
|
const protection = useFormProtection(form, {
|
|
isDirty: () => props.target !== null && form.isDirty,
|
|
});
|
|
const selectionError = computed(
|
|
() =>
|
|
Object.entries(form.errors).find(([key]) =>
|
|
key.startsWith('permissions'),
|
|
)?.[1],
|
|
);
|
|
const options = computed(() =>
|
|
props.permissionOptions
|
|
.filter((option) => option.value in projectPermissionLabels)
|
|
.map((option) => ({
|
|
value: option.value,
|
|
label: t(
|
|
projectPermissionLabels[
|
|
option.value as keyof typeof projectPermissionLabels
|
|
],
|
|
),
|
|
description: t(
|
|
projectPermissionDescriptions[
|
|
option.value as keyof typeof projectPermissionDescriptions
|
|
],
|
|
),
|
|
})),
|
|
);
|
|
watch(
|
|
() => props.target,
|
|
(target) => {
|
|
form.defaults({ permissions: [...(target?.permissions ?? [])] });
|
|
form.resetAndClearErrors();
|
|
protection.clearFeedback();
|
|
},
|
|
);
|
|
function close(open: boolean): void {
|
|
if (open || form.processing || !protection.confirmLeave()) {
|
|
return;
|
|
}
|
|
form.resetAndClearErrors();
|
|
emit('close');
|
|
}
|
|
function submit(): void {
|
|
if (!props.target || form.processing) {
|
|
return;
|
|
}
|
|
const route =
|
|
props.target.kind === 'user'
|
|
? updateUserPermissions([props.projectId, props.target.id])
|
|
: updateTeamPermissions([props.projectId, props.target.id]);
|
|
protection.submit(route, {
|
|
preserveScroll: true,
|
|
onSuccess: () => {
|
|
form.defaults();
|
|
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">{{
|
|
t('projectAccess.editFor', { name: target?.name ?? '' })
|
|
}}</DialogTitle>
|
|
<DialogDescription>{{
|
|
t(
|
|
target?.kind === 'team'
|
|
? 'projectAccess.teamHelp'
|
|
: 'projectAccess.directHelp',
|
|
)
|
|
}}</DialogDescription>
|
|
</DialogHeader>
|
|
<form class="grid min-w-0 gap-5" @submit.prevent="submit">
|
|
<FormFeedback
|
|
:state="protection.state.value"
|
|
:title="
|
|
protection.responseStatus.value === 403
|
|
? t('projectAccess.forbidden')
|
|
: undefined
|
|
"
|
|
:description="
|
|
protection.responseStatus.value === 403
|
|
? t('projectAccess.forbiddenHelp')
|
|
: undefined
|
|
"
|
|
:retryable="protection.responseStatus.value !== 403"
|
|
@retry="submit"
|
|
/>
|
|
<fieldset
|
|
:disabled="form.processing"
|
|
class="grid min-w-0 gap-3"
|
|
:aria-invalid="!!selectionError"
|
|
aria-describedby="project-permission-error"
|
|
>
|
|
<legend class="mb-3 text-sm font-semibold">
|
|
{{
|
|
t(
|
|
target?.kind === 'team'
|
|
? 'projectAccess.teamPermissions'
|
|
: 'projectAccess.directPermissions',
|
|
)
|
|
}}
|
|
</legend>
|
|
<label
|
|
v-for="option in options"
|
|
:key="option.value"
|
|
class="hover:bg-accent flex cursor-pointer items-start gap-3 rounded-lg border p-3"
|
|
>
|
|
<input
|
|
v-model="form.permissions"
|
|
type="checkbox"
|
|
:value="option.value"
|
|
:aria-label="option.label"
|
|
class="accent-primary focus-visible:ring-ring mt-0.5 size-5 shrink-0 focus-visible:ring-2"
|
|
/>
|
|
<span class="grid gap-1"
|
|
><span class="text-sm font-medium">{{
|
|
option.label
|
|
}}</span
|
|
><span class="text-muted-foreground text-xs">{{
|
|
option.description
|
|
}}</span></span
|
|
>
|
|
</label>
|
|
<InputError
|
|
id="project-permission-error"
|
|
:message="selectionError"
|
|
/>
|
|
</fieldset>
|
|
<p class="text-muted-foreground text-sm">
|
|
{{ t('projectAccess.clearHelp') }}
|
|
</p>
|
|
<p
|
|
v-if="target?.globalAdmin"
|
|
class="bg-accent text-accent-foreground rounded-lg p-3 text-sm"
|
|
>
|
|
{{ t('projectAccess.globalHelp') }}
|
|
</p>
|
|
<p class="text-muted-foreground text-xs">
|
|
{{ t('projectAccess.selfHelp') }}
|
|
</p>
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
:disabled="form.processing"
|
|
@click="close(false)"
|
|
>{{ t('common.cancel') }}</Button
|
|
>
|
|
<Button type="submit" :disabled="form.processing">{{
|
|
t('projectAccess.save')
|
|
}}</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</template>
|