Implement Fluid Emerald app redesign
linter / quality (pull_request) Successful in 1m18s
tests / ci (8.4) (pull_request) Successful in 1m54s

This commit is contained in:
2026-07-18 16:45:54 +02:00
parent 59817ad30f
commit a46695c2eb
62 changed files with 2165 additions and 1434 deletions
@@ -79,10 +79,10 @@ const statusAllowsTarget = (
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<AppLayout :breadcrumbs="breadcrumbs" :project="project">
<Head :title="`${project.name} board settings`" />
<div class="flex flex-1 flex-col gap-6 p-4">
<div class="app-page">
<div
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
>
@@ -131,10 +131,7 @@ const statusAllowsTarget = (
</div>
</div>
<div
v-if="status"
class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900 dark:border-emerald-900/70 dark:bg-emerald-950/40 dark:text-emerald-100"
>
<div v-if="status" class="status-success">
{{ status }}
</div>
@@ -144,7 +141,9 @@ const statusAllowsTarget = (
Template inheritance: {{ templateState.templateName }}
</CardTitle>
<CardDescription>
This project stays linked to its template. Sections below show whether the current configuration is still inherited or has local overrides.
This project stays linked to its template. Sections
below show whether the current configuration is still
inherited or has local overrides.
</CardDescription>
</CardHeader>
<CardContent class="flex flex-wrap gap-2">
+116 -41
View File
@@ -89,9 +89,12 @@ const submitSwitchTemplate = (): void => {
return;
}
switchTemplateForm.post(ProjectController.switchTemplate.url(props.project.id), {
preserveScroll: true,
});
switchTemplateForm.post(
ProjectController.switchTemplate.url(props.project.id),
{
preserveScroll: true,
},
);
};
const submitResetTemplate = (): void => {
@@ -99,9 +102,12 @@ const submitResetTemplate = (): void => {
return;
}
resetTemplateForm.post(ProjectController.resetTemplate.url(props.project.id), {
preserveScroll: true,
});
resetTemplateForm.post(
ProjectController.resetTemplate.url(props.project.id),
{
preserveScroll: true,
},
);
};
const submitDelete = (): void => {
@@ -116,10 +122,10 @@ const submitDelete = (): void => {
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<AppLayout :breadcrumbs="breadcrumbs" :project="project">
<Head :title="`${project.name} general settings`" />
<div class="flex flex-1 flex-col gap-6 p-4">
<div class="app-page">
<div
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
>
@@ -164,10 +170,7 @@ const submitDelete = (): void => {
</div>
</div>
<div
v-if="status"
class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900 dark:border-emerald-900/70 dark:bg-emerald-950/40 dark:text-emerald-100"
>
<div v-if="status" class="status-success">
{{ status }}
</div>
@@ -209,18 +212,27 @@ const submitDelete = (): void => {
<CardHeader>
<CardTitle>Template baseline</CardTitle>
<CardDescription>
Switch this project to another published template or reset it back to the currently linked template baseline. Both actions can overwrite local configuration.
Switch this project to another published template or
reset it back to the currently linked template baseline.
Both actions can overwrite local configuration.
</CardDescription>
</CardHeader>
<CardContent class="space-y-6">
<div class="grid gap-4 md:grid-cols-2">
<div class="border-border/70 rounded-xl border p-4">
<p class="text-muted-foreground text-xs font-semibold uppercase tracking-[0.18em]">
<p
class="text-muted-foreground text-xs font-semibold uppercase tracking-[0.18em]"
>
Current linked template
</p>
<p class="text-foreground mt-2 text-sm font-semibold">
{{ currentTemplate?.name ?? 'No linked template' }}
<p
class="text-foreground mt-2 text-sm font-semibold"
>
{{
currentTemplate?.name ??
'No linked template'
}}
</p>
<p class="text-muted-foreground mt-2 text-sm">
{{
@@ -232,33 +244,52 @@ const submitDelete = (): void => {
</div>
<div class="border-border/70 rounded-xl border p-4">
<p class="text-muted-foreground text-xs font-semibold uppercase tracking-[0.18em]">
<p
class="text-muted-foreground text-xs font-semibold uppercase tracking-[0.18em]"
>
Warning
</p>
<p class="text-foreground mt-2 text-sm font-medium">
Template changes can replace board workflow, ticket metadata, repositories, and notifications for copied areas.
Template changes can replace board workflow,
ticket metadata, repositories, and notifications
for copied areas.
</p>
<p class="text-muted-foreground mt-2 text-sm">
Confirm each action with the exact project key to avoid accidental overwrites.
Confirm each action with the exact project key
to avoid accidental overwrites.
</p>
</div>
</div>
<div class="grid gap-6 xl:grid-cols-2">
<form class="space-y-4 rounded-xl border p-4" @submit.prevent="submitSwitchTemplate">
<form
class="space-y-4 rounded-xl border p-4"
@submit.prevent="submitSwitchTemplate"
>
<div class="space-y-1">
<h3 class="text-sm font-semibold">Switch to another template</h3>
<h3 class="text-sm font-semibold">
Switch to another template
</h3>
<p class="text-muted-foreground text-sm">
Applying a different template replaces the copied areas with that template's current baseline.
Applying a different template replaces the
copied areas with that template's current
baseline.
</p>
</div>
<div class="grid gap-2">
<Label for="source_template_id">Published template</Label>
<Label for="source_template_id"
>Published template</Label
>
<select
id="source_template_id"
v-model="switchTemplateForm.source_template_id"
:disabled="switchTemplateForm.processing || availableSwitchTemplates.length === 0"
v-model="
switchTemplateForm.source_template_id
"
:disabled="
switchTemplateForm.processing ||
availableSwitchTemplates.length === 0
"
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
>
<option :value="undefined" disabled>
@@ -272,63 +303,107 @@ const submitDelete = (): void => {
{{ template.name }}
</option>
</select>
<InputError :message="switchTemplateForm.errors.source_template_id" />
<InputError
:message="
switchTemplateForm.errors
.source_template_id
"
/>
</div>
<div class="grid gap-2">
<Label for="switch_confirmation_key">Confirmation key</Label>
<Label for="switch_confirmation_key"
>Confirmation key</Label
>
<Input
id="switch_confirmation_key"
v-model="switchTemplateForm.confirmation_key"
v-model="
switchTemplateForm.confirmation_key
"
:disabled="switchTemplateForm.processing"
placeholder="Type the project key exactly"
/>
<InputError :message="switchTemplateForm.errors.confirmation_key" />
<InputError
:message="
switchTemplateForm.errors
.confirmation_key
"
/>
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div
class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
>
<p class="text-muted-foreground text-sm">
Existing project-specific configuration in copied areas may be replaced or removed.
Existing project-specific configuration in
copied areas may be replaced or removed.
</p>
<Button
type="submit"
variant="outline"
:disabled="switchTemplateForm.processing || !canSwitchTemplate"
:disabled="
switchTemplateForm.processing ||
!canSwitchTemplate
"
>
Switch template
</Button>
</div>
</form>
<form class="space-y-4 rounded-xl border p-4" @submit.prevent="submitResetTemplate">
<form
class="space-y-4 rounded-xl border p-4"
@submit.prevent="submitResetTemplate"
>
<div class="space-y-1">
<h3 class="text-sm font-semibold">Reset to current template baseline</h3>
<h3 class="text-sm font-semibold">
Reset to current template baseline
</h3>
<p class="text-muted-foreground text-sm">
Re-apply the currently linked template and clear local divergence inside copied areas.
Re-apply the currently linked template and
clear local divergence inside copied areas.
</p>
</div>
<div class="grid gap-2">
<Label for="reset_confirmation_key">Confirmation key</Label>
<Label for="reset_confirmation_key"
>Confirmation key</Label
>
<Input
id="reset_confirmation_key"
v-model="resetTemplateForm.confirmation_key"
:disabled="resetTemplateForm.processing || !currentTemplate"
:disabled="
resetTemplateForm.processing ||
!currentTemplate
"
placeholder="Type the project key exactly"
/>
<InputError :message="resetTemplateForm.errors.confirmation_key" />
<InputError
:message="
resetTemplateForm.errors
.confirmation_key
"
/>
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div
class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
>
<p class="text-muted-foreground text-sm">
This restores the current template baseline for copied areas and rewinds project-level overrides there.
This restores the current template baseline
for copied areas and rewinds project-level
overrides there.
</p>
<Button
type="submit"
variant="outline"
:disabled="resetTemplateForm.processing || !isResetConfirmationValid || !currentTemplate"
:disabled="
resetTemplateForm.processing ||
!isResetConfirmationValid ||
!currentTemplate
"
>
Reset to baseline
</Button>
@@ -51,10 +51,10 @@ const replacementTicketTypes = (ticketTypeId: number) =>
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<AppLayout :breadcrumbs="breadcrumbs" :project="project">
<Head :title="`${project.name} ticket settings`" />
<div class="flex flex-1 flex-col gap-6 p-4">
<div class="app-page">
<div
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
>
@@ -121,7 +121,9 @@ const replacementTicketTypes = (ticketTypeId: number) =>
Template inheritance: {{ templateState.templateName }}
</CardTitle>
<CardDescription>
Inherited sections stay synced with the template baseline. Local edits in these settings are treated as project-specific overrides.
Inherited sections stay synced with the template
baseline. Local edits in these settings are treated as
project-specific overrides.
</CardDescription>
</CardHeader>
<CardContent class="flex flex-wrap gap-2">
@@ -699,7 +701,9 @@ const replacementTicketTypes = (ticketTypeId: number) =>
required
placeholder="Blocked by"
/>
<InputError :message="errors.counterpart_name" />
<InputError
:message="errors.counterpart_name"
/>
</div>
<Button type="submit" :disabled="processing">
@@ -745,7 +749,9 @@ const replacementTicketTypes = (ticketTypeId: number) =>
<Input
:id="`link-type-counterpart-name-${ticketLinkType.id}`"
name="counterpart_name"
:default-value="ticketLinkType.counterpart_label"
:default-value="
ticketLinkType.counterpart_label
"
required
/>
<InputError
@@ -89,7 +89,8 @@ const breadcrumbs: BreadcrumbItem[] = [
let filterStateCounter = 0;
const nextFilterStateId = (): string => `notification-filter-${filterStateCounter++}`;
const nextFilterStateId = (): string =>
`notification-filter-${filterStateCounter++}`;
const parseInFilterValues = (value: string): string[] =>
value
@@ -100,7 +101,8 @@ const parseInFilterValues = (value: string): string[] =>
const supportsFilterValue = (operator: string): boolean =>
!['exists', 'not_exists'].includes(operator);
const supportsArrayFilterValue = (operator: string): boolean => operator === 'in';
const supportsArrayFilterValue = (operator: string): boolean =>
operator === 'in';
const createFilter = (): NotificationFilterFormState => ({
attribute: defaultFilterAttribute,
@@ -159,8 +161,9 @@ const resetCreateFilters = (): void => {
createFilterState.filters = [];
};
const editFilters = (notificationActionId: number): NotificationFilterFormState[] =>
editFilterState[notificationActionId] ?? [];
const editFilters = (
notificationActionId: number,
): NotificationFilterFormState[] => editFilterState[notificationActionId] ?? [];
const addEditFilter = (notificationActionId: number): void => {
editFilterState[notificationActionId] = [
@@ -169,10 +172,13 @@ const addEditFilter = (notificationActionId: number): void => {
];
};
const removeEditFilter = (notificationActionId: number, index: number): void => {
editFilterState[notificationActionId] = editFilters(notificationActionId).filter(
(_, candidateIndex) => candidateIndex !== index,
);
const removeEditFilter = (
notificationActionId: number,
index: number,
): void => {
editFilterState[notificationActionId] = editFilters(
notificationActionId,
).filter((_, candidateIndex) => candidateIndex !== index);
};
const createSerializedFilters = computed<string>(() =>
@@ -246,10 +252,10 @@ const notificationActionTimestamp = (
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<AppLayout :breadcrumbs="breadcrumbs" :project="project">
<Head :title="`${project.name} notification settings`" />
<div class="flex flex-1 flex-col gap-6 p-4">
<div class="app-page">
<div
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
>
@@ -298,10 +304,7 @@ const notificationActionTimestamp = (
</div>
</div>
<div
v-if="status"
class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900 dark:border-emerald-900/70 dark:bg-emerald-950/40 dark:text-emerald-100"
>
<div v-if="status" class="status-success">
{{ status }}
</div>
@@ -311,7 +314,8 @@ const notificationActionTimestamp = (
Template inheritance: {{ templateState.templateName }}
</CardTitle>
<CardDescription>
Notification rules can remain inherited from the template or diverge here as project-level overrides.
Notification rules can remain inherited from the
template or diverge here as project-level overrides.
</CardDescription>
</CardHeader>
<CardContent class="flex flex-wrap gap-2">
@@ -543,7 +547,10 @@ const notificationActionTimestamp = (
/>
<div
v-if="createFilterState.filters.length === 0"
v-if="
createFilterState.filters.length ===
0
"
class="text-muted-foreground text-sm"
>
No filters configured. This action runs
@@ -552,8 +559,7 @@ const notificationActionTimestamp = (
<div
v-for="(
filter,
filterIndex
filter, filterIndex
) in createFilterState.filters"
:key="filter.id"
class="grid gap-3 rounded-md border p-3"
@@ -572,8 +578,12 @@ const notificationActionTimestamp = (
>
<option
v-for="filterAttributeOption in filterAttributeOptions"
:key="filterAttributeOption.value"
:value="filterAttributeOption.value"
:key="
filterAttributeOption.value
"
:value="
filterAttributeOption.value
"
>
{{
filterAttributeOption.label
@@ -595,8 +605,12 @@ const notificationActionTimestamp = (
>
<option
v-for="filterOperatorOption in filterOperatorOptions"
:key="filterOperatorOption.value"
:value="filterOperatorOption.value"
:key="
filterOperatorOption.value
"
:value="
filterOperatorOption.value
"
>
{{
filterOperatorOption.label
@@ -607,7 +621,11 @@ const notificationActionTimestamp = (
</div>
<div
v-if="supportsFilterValue(filter.operator)"
v-if="
supportsFilterValue(
filter.operator,
)
"
class="grid gap-2"
>
<Label
@@ -662,7 +680,9 @@ const notificationActionTimestamp = (
</div>
<InputError
:message="notificationFilterError(errors)"
:message="
notificationFilterError(errors)
"
/>
</div>
@@ -1029,8 +1049,7 @@ const notificationActionTimestamp = (
<div
v-for="(
filter,
filterIndex
filter, filterIndex
) in editFilters(notificationAction.id)"
:key="filter.id"
class="grid gap-3 rounded-md border p-3"
@@ -1049,8 +1068,12 @@ const notificationActionTimestamp = (
>
<option
v-for="filterAttributeOption in filterAttributeOptions"
:key="filterAttributeOption.value"
:value="filterAttributeOption.value"
:key="
filterAttributeOption.value
"
:value="
filterAttributeOption.value
"
>
{{
filterAttributeOption.label
@@ -1072,8 +1095,12 @@ const notificationActionTimestamp = (
>
<option
v-for="filterOperatorOption in filterOperatorOptions"
:key="filterOperatorOption.value"
:value="filterOperatorOption.value"
:key="
filterOperatorOption.value
"
:value="
filterOperatorOption.value
"
>
{{
filterOperatorOption.label
@@ -1084,7 +1111,11 @@ const notificationActionTimestamp = (
</div>
<div
v-if="supportsFilterValue(filter.operator)"
v-if="
supportsFilterValue(
filter.operator,
)
"
class="grid gap-2"
>
<Label
@@ -1144,7 +1175,9 @@ const notificationActionTimestamp = (
</div>
<InputError
:message="notificationFilterError(errors)"
:message="
notificationFilterError(errors)
"
/>
</div>
@@ -218,10 +218,10 @@ const submitUpdate = (): void => {
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<AppLayout :breadcrumbs="breadcrumbs" :project="project">
<Head :title="`${project.name} repository settings`" />
<div class="flex flex-1 flex-col gap-6 p-4">
<div class="app-page">
<div
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
>
@@ -270,10 +270,7 @@ const submitUpdate = (): void => {
</div>
</div>
<div
v-if="status"
class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900 dark:border-emerald-900/70 dark:bg-emerald-950/40 dark:text-emerald-100"
>
<div v-if="status" class="status-success">
{{ status }}
</div>
@@ -283,7 +280,8 @@ const submitUpdate = (): void => {
Template inheritance: {{ templateState.templateName }}
</CardTitle>
<CardDescription>
Repository links can stay inherited from the template or be replaced with project-specific overrides.
Repository links can stay inherited from the template or
be replaced with project-specific overrides.
</CardDescription>
</CardHeader>
<CardContent class="flex flex-wrap gap-2">
@@ -320,9 +318,9 @@ const submitUpdate = (): void => {
</Link>
</Button>
<p class="text-muted-foreground text-sm">
Without a matching personal connection, {{ appName }} only
shows the repository name and links you to your user
settings.
Without a matching personal connection,
{{ appName }} only shows the repository name and links
you to your user settings.
</p>
</CardContent>
</Card>
@@ -647,8 +645,8 @@ const submitUpdate = (): void => {
>
This project link is visible, but you need a
matching Git connection in your user
settings before {{ appName }} can open or query
this repository for you.
settings before {{ appName }} can open or
query this repository for you.
</p>
<div class="mt-4 flex justify-end">
@@ -683,9 +681,9 @@ const submitUpdate = (): void => {
<CardHeader class="border-b">
<CardTitle>Detected personal sources</CardTitle>
<CardDescription>
{{ appName }} loads repositories from your personal
Git connections and offers them for project
linking.
{{ appName }} loads repositories from your
personal Git connections and offers them for
project linking.
</CardDescription>
</CardHeader>