made more ticket components
This commit is contained in:
@@ -1,6 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Link, usePage } from '@inertiajs/vue3';
|
import { Link, usePage } from '@inertiajs/vue3';
|
||||||
import { Folder, LayoutGrid, Menu, Search, Shield, Users } from 'lucide-vue-next';
|
import {
|
||||||
|
Folder,
|
||||||
|
LayoutGrid,
|
||||||
|
Menu,
|
||||||
|
Search,
|
||||||
|
Shield,
|
||||||
|
Users,
|
||||||
|
} from 'lucide-vue-next';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import AppLogo from '@/components/AppLogo.vue';
|
import AppLogo from '@/components/AppLogo.vue';
|
||||||
import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
||||||
@@ -85,7 +92,7 @@ const mainNavItems = computed<NavItem[]>(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="sticky top-0 z-50 bg-background/80 backdrop-blur-md">
|
<div class="bg-background/80 sticky top-0 z-50 backdrop-blur-md">
|
||||||
<div class="mx-auto flex h-16 items-center px-4 md:max-w-7xl">
|
<div class="mx-auto flex h-16 items-center px-4 md:max-w-7xl">
|
||||||
<!-- Mobile Menu -->
|
<!-- Mobile Menu -->
|
||||||
<div class="lg:hidden">
|
<div class="lg:hidden">
|
||||||
|
|||||||
@@ -0,0 +1,494 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Link, useForm, usePage } from '@inertiajs/vue3';
|
||||||
|
import { CheckCircle2, GitBranchPlus, XCircle } from 'lucide-vue-next';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import ProjectTicketBranchController from '@/actions/App/Http/Controllers/ProjectTicketBranchController';
|
||||||
|
import InputError from '@/components/InputError.vue';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { edit as editGitConnections } from '@/routes/git-connections/index';
|
||||||
|
import {
|
||||||
|
frostedAsideCardClass,
|
||||||
|
inputClass,
|
||||||
|
nativeSelectClass,
|
||||||
|
} from '@/lib/ticket-styles';
|
||||||
|
import type { BranchNameValidationModel } from '@/types/models/branch-name-validation-model';
|
||||||
|
import type { RepositoryConnectionModel } from '@/types/models/repository-connection-model';
|
||||||
|
import type { TicketRepositoryBranchModel } from '@/types/models/ticket-repository-branch-model';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
canEditTicket: boolean;
|
||||||
|
projectId: number;
|
||||||
|
repositoryBranches: TicketRepositoryBranchModel[];
|
||||||
|
suggestedBranchName: string;
|
||||||
|
ticketId: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const page = usePage();
|
||||||
|
const appName = String(page.props.name);
|
||||||
|
|
||||||
|
const visibleRepositoryBranches = computed(() =>
|
||||||
|
props.repositoryBranches.filter(
|
||||||
|
(repositoryBranch) =>
|
||||||
|
repositoryBranch.branches.length > 0 ||
|
||||||
|
repositoryBranch.error !== null ||
|
||||||
|
!repositoryBranch.repository_connection.has_personal_access,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const linkedRepositoryConnections = computed<RepositoryConnectionModel[]>(() =>
|
||||||
|
props.repositoryBranches.map(
|
||||||
|
(repositoryBranch) => repositoryBranch.repository_connection,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const canCreateBranches = computed(
|
||||||
|
() => props.canEditTicket && linkedRepositoryConnections.value.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultBranchRepositoryConnectionId = computed(() =>
|
||||||
|
String(
|
||||||
|
linkedRepositoryConnections.value.find(
|
||||||
|
(repositoryConnection) => repositoryConnection.has_personal_access,
|
||||||
|
)?.id ??
|
||||||
|
linkedRepositoryConnections.value[0]?.id ??
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isCreateBranchDialogOpen = ref(false);
|
||||||
|
const isCheckingBranchName = ref(false);
|
||||||
|
const branchNameValidation = ref<BranchNameValidationModel | null>(null);
|
||||||
|
|
||||||
|
const branchCreationForm = useForm<{
|
||||||
|
branch_name: string;
|
||||||
|
repository_connection_id: string;
|
||||||
|
}>({
|
||||||
|
branch_name: props.suggestedBranchName,
|
||||||
|
repository_connection_id: defaultBranchRepositoryConnectionId.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedBranchRepositoryConnection = computed(
|
||||||
|
(): RepositoryConnectionModel | null =>
|
||||||
|
linkedRepositoryConnections.value.find(
|
||||||
|
(repositoryConnection) =>
|
||||||
|
String(repositoryConnection.id) ===
|
||||||
|
branchCreationForm.repository_connection_id,
|
||||||
|
) ?? null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedBranchRepositoryHasPersonalAccess = computed(
|
||||||
|
() =>
|
||||||
|
selectedBranchRepositoryConnection.value?.has_personal_access ?? false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const initializeBranchCreationForm = (): void => {
|
||||||
|
branchCreationForm.clearErrors();
|
||||||
|
branchCreationForm.branch_name = props.suggestedBranchName;
|
||||||
|
branchCreationForm.repository_connection_id =
|
||||||
|
defaultBranchRepositoryConnectionId.value;
|
||||||
|
branchNameValidation.value = null;
|
||||||
|
isCheckingBranchName.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => isCreateBranchDialogOpen.value,
|
||||||
|
(isOpen) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
branchCreationForm.clearErrors();
|
||||||
|
branchNameValidation.value = null;
|
||||||
|
isCheckingBranchName.value = false;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeBranchCreationForm();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[
|
||||||
|
() => isCreateBranchDialogOpen.value,
|
||||||
|
() => branchCreationForm.repository_connection_id,
|
||||||
|
() => branchCreationForm.branch_name,
|
||||||
|
],
|
||||||
|
(
|
||||||
|
[isOpen, repositoryConnectionId, branchName],
|
||||||
|
_previousValues,
|
||||||
|
onCleanup,
|
||||||
|
) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repositoryConnection = selectedBranchRepositoryConnection.value;
|
||||||
|
|
||||||
|
if (repositoryConnection === null || repositoryConnectionId === '') {
|
||||||
|
branchNameValidation.value = null;
|
||||||
|
isCheckingBranchName.value = false;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!repositoryConnection.has_personal_access) {
|
||||||
|
branchNameValidation.value = {
|
||||||
|
is_valid: false,
|
||||||
|
message:
|
||||||
|
'Connect a matching Git connection in your user settings first.',
|
||||||
|
};
|
||||||
|
isCheckingBranchName.value = false;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (branchName.trim() === '') {
|
||||||
|
branchNameValidation.value = {
|
||||||
|
is_valid: false,
|
||||||
|
message: 'Branch name is required.',
|
||||||
|
};
|
||||||
|
isCheckingBranchName.value = false;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
const timeoutId = window.setTimeout(async () => {
|
||||||
|
isCheckingBranchName.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
ProjectTicketBranchController.validate.url(
|
||||||
|
{
|
||||||
|
project: props.projectId,
|
||||||
|
ticket: props.ticketId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
branch_name: branchName,
|
||||||
|
repository_connection_id:
|
||||||
|
repositoryConnectionId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
{
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
},
|
||||||
|
method: 'GET',
|
||||||
|
signal: abortController.signal,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Branch validation failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
branchNameValidation.value =
|
||||||
|
(await response.json()) as BranchNameValidationModel;
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
error instanceof DOMException &&
|
||||||
|
error.name === 'AbortError'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
branchNameValidation.value = {
|
||||||
|
is_valid: false,
|
||||||
|
message: `${appName} could not validate this branch name right now.`,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
if (!abortController.signal.aborted) {
|
||||||
|
isCheckingBranchName.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
abortController.abort();
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
isCheckingBranchName.value = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const submitBranchCreation = (): void => {
|
||||||
|
if (
|
||||||
|
!canCreateBranches.value ||
|
||||||
|
!selectedBranchRepositoryHasPersonalAccess.value ||
|
||||||
|
branchNameValidation.value?.is_valid !== true
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
branchCreationForm.submit(
|
||||||
|
ProjectTicketBranchController.store({
|
||||||
|
project: props.projectId,
|
||||||
|
ticket: props.ticketId,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
isCreateBranchDialogOpen.value = false;
|
||||||
|
},
|
||||||
|
preserveScroll: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :class="frostedAsideCardClass">
|
||||||
|
<CardHeader
|
||||||
|
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
|
||||||
|
>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<CardTitle>Branches</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Matching branches are discovered from linked repositories
|
||||||
|
when the branch name starts with this ticket key.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog v-model:open="isCreateBranchDialogOpen">
|
||||||
|
<Button
|
||||||
|
v-if="canCreateBranches"
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
class="gap-2"
|
||||||
|
@click="isCreateBranchDialogOpen = true"
|
||||||
|
>
|
||||||
|
<GitBranchPlus class="size-4" />
|
||||||
|
Create branch
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<DialogContent class="sm:max-w-lg">
|
||||||
|
<DialogHeader class="space-y-3">
|
||||||
|
<DialogTitle>Create branch from ticket</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Pick a linked repository, confirm the suggested
|
||||||
|
branch name, and create it directly in Gitea.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="grid gap-4"
|
||||||
|
@submit.prevent="submitBranchCreation"
|
||||||
|
>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="repository_connection_id">
|
||||||
|
Repository
|
||||||
|
</Label>
|
||||||
|
<select
|
||||||
|
id="repository_connection_id"
|
||||||
|
v-model="
|
||||||
|
branchCreationForm.repository_connection_id
|
||||||
|
"
|
||||||
|
:disabled="branchCreationForm.processing"
|
||||||
|
:class="nativeSelectClass"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="repositoryConnection in linkedRepositoryConnections"
|
||||||
|
:key="repositoryConnection.id"
|
||||||
|
:value="String(repositoryConnection.id)"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
`${repositoryConnection.repository_full_name}${repositoryConnection.has_personal_access ? '' : ' (connect personal Git access)'}`
|
||||||
|
}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<InputError
|
||||||
|
:message="
|
||||||
|
branchCreationForm.errors
|
||||||
|
.repository_connection_id
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="branch_name">Branch name</Label>
|
||||||
|
<Input
|
||||||
|
id="branch_name"
|
||||||
|
v-model="branchCreationForm.branch_name"
|
||||||
|
:disabled="
|
||||||
|
branchCreationForm.processing ||
|
||||||
|
!selectedBranchRepositoryHasPersonalAccess
|
||||||
|
"
|
||||||
|
:class="inputClass"
|
||||||
|
/>
|
||||||
|
<InputError
|
||||||
|
:message="branchCreationForm.errors.branch_name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="rounded-xl border px-4 py-3"
|
||||||
|
:class="
|
||||||
|
branchNameValidation?.is_valid
|
||||||
|
? 'border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-900/70 dark:bg-emerald-950/40 dark:text-emerald-100'
|
||||||
|
: 'border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-900/70 dark:bg-amber-950/40 dark:text-amber-100'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<CheckCircle2
|
||||||
|
v-if="branchNameValidation?.is_valid"
|
||||||
|
class="mt-0.5 size-4 shrink-0"
|
||||||
|
/>
|
||||||
|
<XCircle
|
||||||
|
v-else-if="branchNameValidation !== null"
|
||||||
|
class="mt-0.5 size-4 shrink-0"
|
||||||
|
/>
|
||||||
|
<p class="text-sm">
|
||||||
|
{{
|
||||||
|
isCheckingBranchName
|
||||||
|
? 'Checking whether this branch name is valid and still available...'
|
||||||
|
: (branchNameValidation?.message ??
|
||||||
|
'Select a linked repository to validate the branch name.')
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
!selectedBranchRepositoryHasPersonalAccess &&
|
||||||
|
selectedBranchRepositoryConnection !== null
|
||||||
|
"
|
||||||
|
class="border-border rounded-xl border border-dashed px-4 py-4"
|
||||||
|
>
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
You can only create branches in repositories
|
||||||
|
that match one of your personal Git connections.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
as-child
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="mt-3"
|
||||||
|
>
|
||||||
|
<Link :href="editGitConnections()">
|
||||||
|
Open Git connections
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter class="gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
@click="isCreateBranchDialogOpen = false"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
:disabled="
|
||||||
|
branchCreationForm.processing ||
|
||||||
|
isCheckingBranchName ||
|
||||||
|
branchNameValidation?.is_valid !== true
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Create now
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<div
|
||||||
|
v-if="visibleRepositoryBranches.length === 0"
|
||||||
|
class="border-border text-muted-foreground rounded-xl border border-dashed px-4 py-6 text-sm"
|
||||||
|
>
|
||||||
|
No matching branches were found across the linked repositories
|
||||||
|
you can access.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="repositoryBranch in visibleRepositoryBranches"
|
||||||
|
:key="repositoryBranch.repository_connection.id"
|
||||||
|
class="border-border rounded-xl border px-4 py-4"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<Badge variant="secondary">
|
||||||
|
{{
|
||||||
|
repositoryBranch.repository_connection
|
||||||
|
.provider_label
|
||||||
|
}}
|
||||||
|
</Badge>
|
||||||
|
<span class="font-medium">
|
||||||
|
{{
|
||||||
|
repositoryBranch.repository_connection
|
||||||
|
.repository_full_name
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted-foreground mt-2 text-sm">
|
||||||
|
{{ repositoryBranch.repository_connection.base_url }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
!repositoryBranch.repository_connection
|
||||||
|
.has_personal_access
|
||||||
|
"
|
||||||
|
class="mt-3 space-y-3"
|
||||||
|
>
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
Connect a matching Git account in your user settings
|
||||||
|
to inspect branches for this repository.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Button as-child variant="outline" size="sm">
|
||||||
|
<Link :href="editGitConnections()">
|
||||||
|
Open Git connections
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="repositoryBranch.error"
|
||||||
|
class="mt-3 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:border-amber-900/70 dark:bg-amber-950/40 dark:text-amber-100"
|
||||||
|
>
|
||||||
|
{{ repositoryBranch.error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<Badge
|
||||||
|
v-for="branch in repositoryBranch.branches"
|
||||||
|
:key="branch.name"
|
||||||
|
variant="outline"
|
||||||
|
class="px-3 py-1"
|
||||||
|
>
|
||||||
|
{{ branch.name }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import TicketTreeList from '@/components/TicketTreeList.vue';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { frostedAsideCardClass } from '@/lib/ticket-styles';
|
||||||
|
import type { TicketTreeNodeModel } from '@/types/models/ticket-tree-node-model';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
childTicketTree: TicketTreeNodeModel[];
|
||||||
|
projectId: number;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :class="frostedAsideCardClass">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Child tickets</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Parent tickets expose their full child tree here, including
|
||||||
|
nested parent chains.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<div
|
||||||
|
v-if="childTicketTree.length === 0"
|
||||||
|
class="border-border text-muted-foreground rounded-xl border border-dashed px-4 py-6 text-sm"
|
||||||
|
>
|
||||||
|
No child tickets are assigned to this ticket yet.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TicketTreeList
|
||||||
|
v-else
|
||||||
|
:nodes="childTicketTree"
|
||||||
|
:project-id="projectId"
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useForm } from '@inertiajs/vue3';
|
||||||
|
import ProjectTicketController from '@/actions/App/Http/Controllers/ProjectTicketController';
|
||||||
|
import InputError from '@/components/InputError.vue';
|
||||||
|
import TicketPriorityIcon from '@/components/TicketPriorityIcon.vue';
|
||||||
|
import TicketTypeIcon from '@/components/TicketTypeIcon.vue';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import {
|
||||||
|
fieldLabelClass,
|
||||||
|
frostedCardClass,
|
||||||
|
inputClass,
|
||||||
|
nativeSelectClass,
|
||||||
|
selectContentClass,
|
||||||
|
selectTriggerClass,
|
||||||
|
textareaClass,
|
||||||
|
} from '@/lib/ticket-styles';
|
||||||
|
import type { ProjectModel } from '@/types/models/project-model';
|
||||||
|
import type { TicketFieldDefinitionModel } from '@/types/models/ticket-field-definition-model';
|
||||||
|
import type { TicketModel } from '@/types/models/ticket-model';
|
||||||
|
import type { TicketPriorityModel } from '@/types/models/ticket-priority-model';
|
||||||
|
import type { TicketReferenceModel } from '@/types/models/ticket-reference-model';
|
||||||
|
import type { TicketTypeModel } from '@/types/models/ticket-type-model';
|
||||||
|
import type { UserModel } from '@/types/models/user-model';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
canEditAuthor: boolean;
|
||||||
|
canEditTicket: boolean;
|
||||||
|
canViewAuthor: boolean;
|
||||||
|
parentTicketOptions: TicketReferenceModel[];
|
||||||
|
project: ProjectModel;
|
||||||
|
ticket: TicketModel;
|
||||||
|
ticketFieldDefinitions: TicketFieldDefinitionModel[];
|
||||||
|
ticketPriorities: TicketPriorityModel[];
|
||||||
|
ticketTypes: TicketTypeModel[];
|
||||||
|
users: UserModel[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
acceptance_criteria: props.ticket.acceptance_criteria ?? '',
|
||||||
|
assignee_user_id: props.ticket.assignee_user_id ?? '',
|
||||||
|
author_user_id: props.ticket.author_user_id ?? '',
|
||||||
|
blocked_comment: props.ticket.blocked_comment ?? '',
|
||||||
|
description: props.ticket.description ?? '',
|
||||||
|
field_values: props.ticketFieldDefinitions.map((ticketFieldDefinition) => {
|
||||||
|
const currentFieldValue = props.ticket.field_values?.find(
|
||||||
|
(fieldValue) =>
|
||||||
|
fieldValue.ticket_field_definition_id ===
|
||||||
|
ticketFieldDefinition.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ticket_field_definition_id: ticketFieldDefinition.id,
|
||||||
|
value_number: currentFieldValue?.value_number ?? '',
|
||||||
|
value_user_id: currentFieldValue?.value_user_id ?? '',
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
is_blocked: props.ticket.is_blocked,
|
||||||
|
parent_ticket_id: props.ticket.parent_ticket_id ?? '',
|
||||||
|
ticket_priority_id: String(props.ticket.ticket_priority_id),
|
||||||
|
ticket_type_id: String(props.ticket.ticket_type_id),
|
||||||
|
title: props.ticket.title,
|
||||||
|
story_points: props.ticket.story_points,
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = (): void => {
|
||||||
|
if (!props.canEditTicket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.submit(
|
||||||
|
ProjectTicketController.update({
|
||||||
|
project: props.project.id,
|
||||||
|
ticket: props.ticket.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
preserveScroll: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :class="frostedCardClass">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle
|
||||||
|
class="font-display dark:text-foreground text-2xl text-[#2d3335]"
|
||||||
|
>
|
||||||
|
Ticket details
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription class="max-w-2xl leading-6">
|
||||||
|
{{
|
||||||
|
canEditTicket
|
||||||
|
? 'Tracked ticket changes create worklog entries automatically.'
|
||||||
|
: 'You can review the tracked fields, but editing requires project edit permission.'
|
||||||
|
}}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<form class="grid gap-6" @submit.prevent="submit">
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="title" :class="fieldLabelClass">Title</Label>
|
||||||
|
<Input
|
||||||
|
id="title"
|
||||||
|
v-model="form.title"
|
||||||
|
:disabled="!canEditTicket || form.processing"
|
||||||
|
:class="inputClass"
|
||||||
|
/>
|
||||||
|
<InputError :message="form.errors.title" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2 md:grid-cols-2 md:gap-6">
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="ticket_type_id" :class="fieldLabelClass">
|
||||||
|
Type
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
v-model="form.ticket_type_id"
|
||||||
|
:disabled="!canEditTicket || form.processing"
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="ticket_type_id"
|
||||||
|
:class="selectTriggerClass"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Choose a type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent :class="selectContentClass">
|
||||||
|
<SelectItem
|
||||||
|
v-for="ticketType in ticketTypes"
|
||||||
|
:key="ticketType.id"
|
||||||
|
:value="String(ticketType.id)"
|
||||||
|
>
|
||||||
|
<TicketTypeIcon class="size-5" />
|
||||||
|
<span>{{ ticketType.name }}</span>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<InputError :message="form.errors.ticket_type_id" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label
|
||||||
|
for="ticket_priority_id"
|
||||||
|
:class="fieldLabelClass"
|
||||||
|
>
|
||||||
|
Priority
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
v-model="form.ticket_priority_id"
|
||||||
|
:disabled="!canEditTicket || form.processing"
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="ticket_priority_id"
|
||||||
|
:class="selectTriggerClass"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Choose a priority" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent :class="selectContentClass">
|
||||||
|
<SelectItem
|
||||||
|
v-for="ticketPriority in ticketPriorities"
|
||||||
|
:key="ticketPriority.id"
|
||||||
|
:value="String(ticketPriority.id)"
|
||||||
|
>
|
||||||
|
<TicketPriorityIcon class="size-5" />
|
||||||
|
<span>{{ ticketPriority.name }}</span>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<InputError :message="form.errors.ticket_priority_id" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2 md:grid-cols-2 md:gap-6">
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="assignee_user_id" :class="fieldLabelClass">
|
||||||
|
Assignee
|
||||||
|
</Label>
|
||||||
|
<select
|
||||||
|
id="assignee_user_id"
|
||||||
|
v-model="form.assignee_user_id"
|
||||||
|
:disabled="!canEditTicket || form.processing"
|
||||||
|
:class="nativeSelectClass"
|
||||||
|
>
|
||||||
|
<option value="">Unassigned</option>
|
||||||
|
<option
|
||||||
|
v-for="user in users"
|
||||||
|
:key="user.id"
|
||||||
|
:value="user.id"
|
||||||
|
>
|
||||||
|
{{ user.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<InputError :message="form.errors.assignee_user_id" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="canViewAuthor" class="grid gap-2">
|
||||||
|
<Label for="author_user_id" :class="fieldLabelClass">
|
||||||
|
Author
|
||||||
|
</Label>
|
||||||
|
<select
|
||||||
|
id="author_user_id"
|
||||||
|
v-model="form.author_user_id"
|
||||||
|
:disabled="!canEditAuthor || form.processing"
|
||||||
|
:class="nativeSelectClass"
|
||||||
|
>
|
||||||
|
<option value="">No author</option>
|
||||||
|
<option
|
||||||
|
v-for="user in users"
|
||||||
|
:key="user.id"
|
||||||
|
:value="user.id"
|
||||||
|
>
|
||||||
|
{{ user.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<InputError :message="form.errors.author_user_id" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="project.show_story_points" class="grid gap-2">
|
||||||
|
<Label for="story_points" :class="fieldLabelClass">
|
||||||
|
Estimated story points
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="story_points"
|
||||||
|
name="story_points"
|
||||||
|
v-model="form.story_points"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
:class="inputClass"
|
||||||
|
/>
|
||||||
|
<InputError :message="form.errors.story_points" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="parent_ticket_id" :class="fieldLabelClass">
|
||||||
|
Parent ticket
|
||||||
|
</Label>
|
||||||
|
<select
|
||||||
|
id="parent_ticket_id"
|
||||||
|
v-model="form.parent_ticket_id"
|
||||||
|
:disabled="!canEditTicket || form.processing"
|
||||||
|
:class="nativeSelectClass"
|
||||||
|
>
|
||||||
|
<option value="">No parent ticket</option>
|
||||||
|
<option
|
||||||
|
v-for="parentTicketOption in parentTicketOptions"
|
||||||
|
:key="parentTicketOption.id"
|
||||||
|
:value="parentTicketOption.id"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
`${parentTicketOption.ticket_key} - ${parentTicketOption.title}`
|
||||||
|
}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<InputError :message="form.errors.parent_ticket_id" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="description" :class="fieldLabelClass">
|
||||||
|
Description
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
v-model="form.description"
|
||||||
|
:disabled="!canEditTicket || form.processing"
|
||||||
|
:class="textareaClass"
|
||||||
|
/>
|
||||||
|
<InputError :message="form.errors.description" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="acceptance_criteria" :class="fieldLabelClass">
|
||||||
|
Acceptance criteria
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="acceptance_criteria"
|
||||||
|
v-model="form.acceptance_criteria"
|
||||||
|
:disabled="!canEditTicket || form.processing"
|
||||||
|
:class="textareaClass"
|
||||||
|
/>
|
||||||
|
<InputError :message="form.errors.acceptance_criteria" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="dark:bg-muted/25 grid gap-4 rounded-[1.5rem] border border-dashed border-[#adb3b5]/35 bg-[#f8f9fa]/80 p-5 dark:border-white/10"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<Checkbox
|
||||||
|
id="is_blocked"
|
||||||
|
:checked="form.is_blocked"
|
||||||
|
:disabled="!canEditTicket || form.processing"
|
||||||
|
@update:checked="
|
||||||
|
(value: boolean | 'indeterminate') =>
|
||||||
|
(form.is_blocked = value === true)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<Label for="is_blocked" :class="fieldLabelClass">
|
||||||
|
Mark ticket as blocked
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="blocked_comment" :class="fieldLabelClass">
|
||||||
|
Blocker comment
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="blocked_comment"
|
||||||
|
v-model="form.blocked_comment"
|
||||||
|
:disabled="!canEditTicket || form.processing"
|
||||||
|
placeholder="Describe what currently blocks progress on this ticket."
|
||||||
|
:class="textareaClass"
|
||||||
|
/>
|
||||||
|
<InputError :message="form.errors.blocked_comment" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h3 class="text-sm font-semibold">Custom fields</h3>
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
Project-configured fields can be tracked directly on
|
||||||
|
the ticket.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="ticketFieldDefinitions.length === 0"
|
||||||
|
class="border-border text-muted-foreground rounded-xl border border-dashed px-4 py-6 text-sm"
|
||||||
|
>
|
||||||
|
No custom fields are configured for this project yet.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid gap-4 md:grid-cols-2">
|
||||||
|
<div
|
||||||
|
v-for="(
|
||||||
|
ticketFieldDefinition, index
|
||||||
|
) in ticketFieldDefinitions"
|
||||||
|
:key="ticketFieldDefinition.id"
|
||||||
|
class="grid gap-2"
|
||||||
|
>
|
||||||
|
<Label
|
||||||
|
:for="`field-${ticketFieldDefinition.id}`"
|
||||||
|
:class="fieldLabelClass"
|
||||||
|
>
|
||||||
|
{{ ticketFieldDefinition.label }}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
v-if="
|
||||||
|
ticketFieldDefinition.field_type === 'user'
|
||||||
|
"
|
||||||
|
:id="`field-${ticketFieldDefinition.id}`"
|
||||||
|
v-model="form.field_values[index].value_user_id"
|
||||||
|
:disabled="!canEditTicket || form.processing"
|
||||||
|
:class="nativeSelectClass"
|
||||||
|
>
|
||||||
|
<option value="">Unassigned</option>
|
||||||
|
<option
|
||||||
|
v-for="user in users"
|
||||||
|
:key="user.id"
|
||||||
|
:value="user.id"
|
||||||
|
>
|
||||||
|
{{ user.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
v-else
|
||||||
|
:id="`field-${ticketFieldDefinition.id}`"
|
||||||
|
v-model="form.field_values[index].value_number"
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
:disabled="!canEditTicket || form.processing"
|
||||||
|
:class="inputClass"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputError
|
||||||
|
:message="
|
||||||
|
form.errors[
|
||||||
|
`field_values.${index}.value_user_id`
|
||||||
|
] ??
|
||||||
|
form.errors[
|
||||||
|
`field_values.${index}.value_number`
|
||||||
|
]
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="canEditTicket" class="flex items-center gap-3">
|
||||||
|
<Button type="submit" :disabled="form.processing">
|
||||||
|
Save changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Form, Link } from '@inertiajs/vue3';
|
||||||
|
import ProjectTicketLinkController from '@/actions/App/Http/Controllers/ProjectTicketLinkController';
|
||||||
|
import InputError from '@/components/InputError.vue';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { frostedAsideCardClass, nativeSelectClass } from '@/lib/ticket-styles';
|
||||||
|
import { show as showTicket } from '@/routes/projects/tickets';
|
||||||
|
import type { TicketLinkModel } from '@/types/models/ticket-link-model';
|
||||||
|
import type { TicketLinkTypeModel } from '@/types/models/ticket-link-type-model';
|
||||||
|
import type { TicketReferenceModel } from '@/types/models/ticket-reference-model';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
canEditTicket: boolean;
|
||||||
|
linkedTickets: TicketLinkModel[];
|
||||||
|
projectId: number;
|
||||||
|
ticketId: number;
|
||||||
|
ticketLinkOptions: TicketReferenceModel[];
|
||||||
|
ticketLinkTypes: TicketLinkTypeModel[];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :class="frostedAsideCardClass">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Linked tickets</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Track cross-ticket relationships with project-defined link
|
||||||
|
labels.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<Form
|
||||||
|
v-if="
|
||||||
|
canEditTicket &&
|
||||||
|
ticketLinkTypes.length > 0 &&
|
||||||
|
ticketLinkOptions.length > 0
|
||||||
|
"
|
||||||
|
:action="
|
||||||
|
ProjectTicketLinkController.store.url({
|
||||||
|
project: projectId,
|
||||||
|
ticket: ticketId,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
class="border-border grid gap-4 rounded-xl border border-dashed p-4"
|
||||||
|
method="post"
|
||||||
|
v-slot="{ errors, processing }"
|
||||||
|
>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="ticket_link_type_id">Link type</Label>
|
||||||
|
<select
|
||||||
|
id="ticket_link_type_id"
|
||||||
|
name="ticket_link_type_id"
|
||||||
|
:class="nativeSelectClass"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="ticketLinkType in ticketLinkTypes"
|
||||||
|
:key="ticketLinkType.id"
|
||||||
|
:value="ticketLinkType.id"
|
||||||
|
>
|
||||||
|
{{ ticketLinkType.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<InputError :message="errors.ticket_link_type_id" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="linked_ticket_id">Linked ticket</Label>
|
||||||
|
<select
|
||||||
|
id="linked_ticket_id"
|
||||||
|
name="linked_ticket_id"
|
||||||
|
:class="nativeSelectClass"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="ticketLinkOption in ticketLinkOptions"
|
||||||
|
:key="ticketLinkOption.id"
|
||||||
|
:value="ticketLinkOption.id"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
`${ticketLinkOption.ticket_key} - ${ticketLinkOption.title}`
|
||||||
|
}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<InputError :message="errors.linked_ticket_id" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="submit" :disabled="processing">Save link</Button>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="linkedTickets.length === 0"
|
||||||
|
class="border-border text-muted-foreground rounded-xl border border-dashed px-4 py-6 text-sm"
|
||||||
|
>
|
||||||
|
No ticket links are configured for this ticket yet.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="linkedTicket in linkedTickets"
|
||||||
|
:key="linkedTicket.id"
|
||||||
|
class="border-border rounded-xl border px-4 py-4"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<Badge variant="secondary">
|
||||||
|
{{ linkedTicket.ticket_link_type.label }}
|
||||||
|
</Badge>
|
||||||
|
<Link
|
||||||
|
:href="
|
||||||
|
showTicket({
|
||||||
|
project: projectId,
|
||||||
|
ticket: linkedTicket.linked_ticket.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
class="text-foreground hover:text-primary font-medium transition"
|
||||||
|
>
|
||||||
|
{{ linkedTicket.linked_ticket.ticket_key }}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted-foreground mt-2 text-sm">
|
||||||
|
{{ linkedTicket.linked_ticket.title }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
v-if="canEditTicket"
|
||||||
|
:action="
|
||||||
|
ProjectTicketLinkController.destroy.url({
|
||||||
|
project: projectId,
|
||||||
|
ticket: ticketId,
|
||||||
|
ticketLink: linkedTicket.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
class="mt-3 flex justify-end"
|
||||||
|
method="delete"
|
||||||
|
v-slot="{ processing: deleting }"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
:disabled="deleting"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Link } from '@inertiajs/vue3';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { frostedAsideCardClass } from '@/lib/ticket-styles';
|
||||||
|
import { edit as editGitConnections } from '@/routes/git-connections/index';
|
||||||
|
import type { TicketRepositoryPullRequestModel } from '@/types/models/ticket-repository-pull-request-model';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
repositoryPullRequests: TicketRepositoryPullRequestModel[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const visibleRepositoryPullRequests = computed(() =>
|
||||||
|
props.repositoryPullRequests.filter(
|
||||||
|
(repositoryPullRequest) =>
|
||||||
|
repositoryPullRequest.pull_requests.length > 0 ||
|
||||||
|
repositoryPullRequest.error !== null ||
|
||||||
|
!repositoryPullRequest.repository_connection.has_personal_access,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :class="frostedAsideCardClass">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Pull requests</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Matching pull requests are detected from linked repositories by
|
||||||
|
checking whether the source branch starts with this ticket key.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<div
|
||||||
|
v-if="visibleRepositoryPullRequests.length === 0"
|
||||||
|
class="border-border text-muted-foreground rounded-xl border border-dashed px-4 py-6 text-sm"
|
||||||
|
>
|
||||||
|
No matching pull requests were found across the linked
|
||||||
|
repositories you can access.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="repositoryPullRequest in visibleRepositoryPullRequests"
|
||||||
|
:key="repositoryPullRequest.repository_connection.id"
|
||||||
|
class="border-border rounded-xl border px-4 py-4"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<Badge variant="secondary">
|
||||||
|
{{
|
||||||
|
repositoryPullRequest.repository_connection
|
||||||
|
.provider_label
|
||||||
|
}}
|
||||||
|
</Badge>
|
||||||
|
<span class="font-medium">
|
||||||
|
{{
|
||||||
|
repositoryPullRequest.repository_connection
|
||||||
|
.repository_full_name
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted-foreground mt-2 text-sm">
|
||||||
|
{{
|
||||||
|
repositoryPullRequest.repository_connection.base_url
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
!repositoryPullRequest.repository_connection
|
||||||
|
.has_personal_access
|
||||||
|
"
|
||||||
|
class="mt-3 space-y-3"
|
||||||
|
>
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
Connect a matching Git account in your user settings
|
||||||
|
to inspect pull requests for this repository.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Button as-child variant="outline" size="sm">
|
||||||
|
<Link :href="editGitConnections()">
|
||||||
|
Open Git connections
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="repositoryPullRequest.error"
|
||||||
|
class="mt-3 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:border-amber-900/70 dark:bg-amber-950/40 dark:text-amber-100"
|
||||||
|
>
|
||||||
|
{{ repositoryPullRequest.error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="mt-3 space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="pullRequest in repositoryPullRequest.pull_requests"
|
||||||
|
:key="pullRequest.html_url"
|
||||||
|
class="border-border/70 rounded-xl border px-4 py-3"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<Badge variant="outline">
|
||||||
|
#{{ pullRequest.number }}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="secondary">
|
||||||
|
{{ pullRequest.status }}
|
||||||
|
</Badge>
|
||||||
|
<a
|
||||||
|
:href="pullRequest.html_url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
class="text-foreground hover:text-primary font-medium transition"
|
||||||
|
>
|
||||||
|
{{ pullRequest.title }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted-foreground mt-2 text-sm">
|
||||||
|
Source branch: {{ pullRequest.source_branch }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import FeatureComingSoonPanel from '@/components/FeatureComingSoonPanel.vue';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { frostedAsideCardClass } from '@/lib/ticket-styles';
|
||||||
|
import type { TicketModel } from '@/types/models/ticket-model';
|
||||||
|
import type { TicketRepositoryBranchModel } from '@/types/models/ticket-repository-branch-model';
|
||||||
|
import type { TicketRepositoryPullRequestModel } from '@/types/models/ticket-repository-pull-request-model';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
repositoryBranches: TicketRepositoryBranchModel[];
|
||||||
|
repositoryPullRequests: TicketRepositoryPullRequestModel[];
|
||||||
|
ticket: TicketModel;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const repositoryBranchCount = computed(() =>
|
||||||
|
props.repositoryBranches.reduce(
|
||||||
|
(count, repositoryBranch) => count + repositoryBranch.branches.length,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const pullRequestCount = computed(() =>
|
||||||
|
props.repositoryPullRequests.reduce(
|
||||||
|
(count, repositoryPullRequest) =>
|
||||||
|
count + repositoryPullRequest.pull_requests.length,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatDateTime = (value: string): string => {
|
||||||
|
return new Intl.DateTimeFormat(undefined, {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
timeStyle: 'short',
|
||||||
|
}).format(new Date(value));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<Card :class="frostedAsideCardClass">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle
|
||||||
|
class="font-display dark:text-foreground text-xl text-[#2d3335]"
|
||||||
|
>
|
||||||
|
Snapshot
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Quick context for the ticket without scrolling through the
|
||||||
|
full detail form.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent class="space-y-3">
|
||||||
|
<div
|
||||||
|
class="dark:bg-muted/35 rounded-[1.25rem] bg-[#f1f4f5] px-4 py-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.8)]"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="dark:text-muted-foreground text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-[#5a6062]"
|
||||||
|
>
|
||||||
|
Assignee
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="dark:text-foreground mt-1 text-sm font-medium text-[#2d3335]"
|
||||||
|
>
|
||||||
|
{{ ticket.assignee?.name ?? 'Unassigned' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="dark:bg-muted/25 rounded-[1.25rem] bg-[#f8f9fa] px-4 py-3"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="dark:text-muted-foreground text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-[#5a6062]"
|
||||||
|
>
|
||||||
|
Author
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="dark:text-foreground mt-1 text-sm font-medium text-[#2d3335]"
|
||||||
|
>
|
||||||
|
{{ ticket.author?.name ?? 'No author' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2 2xl:grid-cols-1">
|
||||||
|
<div
|
||||||
|
class="dark:bg-muted/25 rounded-[1.25rem] bg-[#f8f9fa] px-4 py-3"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="dark:text-muted-foreground text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-[#5a6062]"
|
||||||
|
>
|
||||||
|
Branches
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="dark:text-foreground mt-1 text-lg font-semibold text-[#2d3335]"
|
||||||
|
>
|
||||||
|
{{ repositoryBranchCount }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="dark:bg-muted/25 rounded-[1.25rem] bg-[#f8f9fa] px-4 py-3"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="dark:text-muted-foreground text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-[#5a6062]"
|
||||||
|
>
|
||||||
|
Pull requests
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="dark:text-foreground mt-1 text-lg font-semibold text-[#2d3335]"
|
||||||
|
>
|
||||||
|
{{ pullRequestCount }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="dark:bg-muted/25 rounded-[1.25rem] bg-[#f8f9fa] px-4 py-3"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="dark:text-muted-foreground text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-[#5a6062]"
|
||||||
|
>
|
||||||
|
Last updated
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="dark:text-foreground mt-1 text-sm font-medium text-[#2d3335]"
|
||||||
|
>
|
||||||
|
{{ formatDateTime(ticket.updated_at) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<FeatureComingSoonPanel
|
||||||
|
feature="Comments & discussion"
|
||||||
|
description="The Stitch design already reserves this full rail for threaded comments, collaboration, and follow-up context. The visual shell is in place here until the feature is wired."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { frostedAsideCardClass } from '@/lib/ticket-styles';
|
||||||
|
import type { TicketWorklogEntryModel } from '@/types/models/ticket-worklog-entry-model';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
worklogEntries: TicketWorklogEntryModel[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const formatDateTime = (value: string): string => {
|
||||||
|
return new Intl.DateTimeFormat(undefined, {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
timeStyle: 'short',
|
||||||
|
}).format(new Date(value));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :class="frostedAsideCardClass">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Worklog</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Every tracked ticket change is recorded with actor and
|
||||||
|
timestamp.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<div
|
||||||
|
v-if="worklogEntries.length === 0"
|
||||||
|
class="border-border text-muted-foreground rounded-xl border border-dashed px-4 py-6 text-sm"
|
||||||
|
>
|
||||||
|
No worklog entries exist for this ticket yet.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex flex-col gap-4">
|
||||||
|
<div
|
||||||
|
v-for="worklogEntry in worklogEntries"
|
||||||
|
:key="worklogEntry.id"
|
||||||
|
class="border-border rounded-xl border px-4 py-4"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<Badge variant="outline">
|
||||||
|
{{ worklogEntry.entry_type }}
|
||||||
|
</Badge>
|
||||||
|
<span class="text-foreground text-sm font-medium">
|
||||||
|
{{ worklogEntry.actor?.name ?? 'System' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-foreground mt-3 text-sm">
|
||||||
|
{{ worklogEntry.summary }}
|
||||||
|
</p>
|
||||||
|
<p class="text-muted-foreground mt-2 text-xs">
|
||||||
|
{{ formatDateTime(worklogEntry.created_at) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@@ -43,7 +43,7 @@ const { appearance, updateAppearance } = useAppearance();
|
|||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuLabel class="text-xs font-medium text-muted-foreground">
|
<DropdownMenuLabel class="text-muted-foreground text-xs font-medium">
|
||||||
Appearance
|
Appearance
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
export const frostedCardClass =
|
||||||
|
'rounded-[1.75rem] border-white/70 bg-white/85 shadow-[0_22px_65px_-40px_rgba(45,51,53,0.38)] backdrop-blur-sm dark:border-white/10 dark:bg-card/90 dark:shadow-black/25';
|
||||||
|
|
||||||
|
export const frostedAsideCardClass =
|
||||||
|
'h-fit rounded-[1.75rem] border-white/70 bg-white/85 shadow-[0_22px_65px_-40px_rgba(45,51,53,0.38)] backdrop-blur-sm dark:border-white/10 dark:bg-card/90 dark:shadow-black/25';
|
||||||
|
|
||||||
|
export const airyOutlineButtonClass =
|
||||||
|
'rounded-full border-white/70 bg-white/80 text-[#2d3335] shadow-[0_18px_45px_-34px_rgba(45,51,53,0.34)] hover:bg-white dark:border-white/10 dark:bg-card/85 dark:text-foreground dark:hover:bg-card';
|
||||||
|
|
||||||
|
export const inputClass =
|
||||||
|
'h-12 rounded-xl border-white/70 bg-white/80 text-[#2d3335] shadow-[0_18px_45px_-34px_rgba(45,51,53,0.22)] focus-visible:border-[#9de77c] focus-visible:ring-[#abf688]/35 dark:border-white/10 dark:bg-card/90 dark:text-foreground dark:focus-visible:border-primary';
|
||||||
|
|
||||||
|
export const textareaClass =
|
||||||
|
'min-h-32 rounded-[1.25rem] border-white/70 bg-white/80 text-[#2d3335] shadow-[0_18px_45px_-34px_rgba(45,51,53,0.22)] focus-visible:border-[#9de77c] focus-visible:ring-[#abf688]/35 dark:border-white/10 dark:bg-card/90 dark:text-foreground dark:focus-visible:border-primary';
|
||||||
|
|
||||||
|
export const nativeSelectClass =
|
||||||
|
'h-12 w-full rounded-xl border border-white/70 bg-white/80 px-3 py-2 text-sm text-[#2d3335] shadow-[0_18px_45px_-34px_rgba(45,51,53,0.22)] outline-none transition-[color,box-shadow] focus:border-[#9de77c] focus:ring-4 focus:ring-[#abf688]/35 disabled:cursor-not-allowed disabled:opacity-60 dark:border-white/10 dark:bg-card/90 dark:text-foreground dark:focus:border-primary dark:focus:ring-primary/20';
|
||||||
|
|
||||||
|
export const selectTriggerClass =
|
||||||
|
'h-12 w-full rounded-xl border-white/70 bg-white/80 text-left text-[#2d3335] shadow-[0_18px_45px_-34px_rgba(45,51,53,0.22)] focus:border-[#9de77c] focus:ring-[#abf688]/35 dark:border-white/10 dark:bg-card/90 dark:text-foreground dark:focus:border-primary';
|
||||||
|
|
||||||
|
export const selectContentClass =
|
||||||
|
'rounded-[1.25rem] border-white/80 bg-white/95 shadow-[0_24px_60px_-40px_rgba(45,51,53,0.34)] backdrop-blur-sm dark:border-white/10 dark:bg-card/95';
|
||||||
|
|
||||||
|
export const fieldLabelClass =
|
||||||
|
'text-[0.72rem] font-semibold uppercase tracking-[0.18em] text-[#5a6062]';
|
||||||
@@ -15,7 +15,10 @@ import AppLayout from '@/layouts/AppLayout.vue';
|
|||||||
import { index as boardIndex } from '@/routes/projects/board';
|
import { index as boardIndex } from '@/routes/projects/board';
|
||||||
import { index as backlogIndex } from '@/routes/projects/backlog';
|
import { index as backlogIndex } from '@/routes/projects/backlog';
|
||||||
import { index, show } from '@/routes/projects/index';
|
import { index, show } from '@/routes/projects/index';
|
||||||
import { create as createTicket, show as showTicket } from '@/routes/projects/tickets';
|
import {
|
||||||
|
create as createTicket,
|
||||||
|
show as showTicket,
|
||||||
|
} from '@/routes/projects/tickets';
|
||||||
import type { BreadcrumbItem, ProjectBacklogPageProps } from '@/types';
|
import type { BreadcrumbItem, ProjectBacklogPageProps } from '@/types';
|
||||||
import type { TicketModel } from '@/types/models/ticket-model';
|
import type { TicketModel } from '@/types/models/ticket-model';
|
||||||
import type { TicketStatusModel } from '@/types/models/ticket-status-model';
|
import type { TicketStatusModel } from '@/types/models/ticket-status-model';
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ import { index as backlogIndex } from '@/routes/projects/backlog';
|
|||||||
import { index as projectIndex, show } from '@/routes/projects/index';
|
import { index as projectIndex, show } from '@/routes/projects/index';
|
||||||
import { board as boardSettings } from '@/routes/projects/settings';
|
import { board as boardSettings } from '@/routes/projects/settings';
|
||||||
import { index as boardIndex } from '@/routes/projects/board';
|
import { index as boardIndex } from '@/routes/projects/board';
|
||||||
import { create as createTicket, show as showTicket } from '@/routes/projects/tickets';
|
import {
|
||||||
|
create as createTicket,
|
||||||
|
show as showTicket,
|
||||||
|
} from '@/routes/projects/tickets';
|
||||||
import type { BreadcrumbItem, ProjectBoardPageProps } from '@/types';
|
import type { BreadcrumbItem, ProjectBoardPageProps } from '@/types';
|
||||||
import type { TicketModel } from '@/types/models/ticket-model';
|
import type { TicketModel } from '@/types/models/ticket-model';
|
||||||
import type { TicketStatusModel } from '@/types/models/ticket-status-model';
|
import type { TicketStatusModel } from '@/types/models/ticket-status-model';
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -287,9 +287,9 @@ const submitUpdate = (): void => {
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<p class="text-muted-foreground text-sm">
|
<p class="text-muted-foreground text-sm">
|
||||||
Without a matching personal connection, {{ appName }} only
|
Without a matching personal connection,
|
||||||
shows the repository name and links you to your user
|
{{ appName }} only shows the repository name and links
|
||||||
settings.
|
you to your user settings.
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -614,8 +614,8 @@ const submitUpdate = (): void => {
|
|||||||
>
|
>
|
||||||
This project link is visible, but you need a
|
This project link is visible, but you need a
|
||||||
matching Git connection in your user
|
matching Git connection in your user
|
||||||
settings before {{ appName }} can open or query
|
settings before {{ appName }} can open or
|
||||||
this repository for you.
|
query this repository for you.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mt-4 flex justify-end">
|
<div class="mt-4 flex justify-end">
|
||||||
@@ -650,9 +650,9 @@ const submitUpdate = (): void => {
|
|||||||
<CardHeader class="border-b">
|
<CardHeader class="border-b">
|
||||||
<CardTitle>Detected personal sources</CardTitle>
|
<CardTitle>Detected personal sources</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{{ appName }} loads repositories from your personal
|
{{ appName }} loads repositories from your
|
||||||
Git connections and offers them for project
|
personal Git connections and offers them for
|
||||||
linking.
|
project linking.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,9 @@ const breadcrumbItems: BreadcrumbItem[] = [
|
|||||||
/>
|
/>
|
||||||
<p class="text-muted-foreground text-sm">
|
<p class="text-muted-foreground text-sm">
|
||||||
Each user needs their own application key. Without a
|
Each user needs their own application key. Without a
|
||||||
matching personal connection, {{ appName }} can only show
|
matching personal connection, {{ appName }} can only
|
||||||
repository names and will link you back to this page.
|
show repository names and will link you back to this
|
||||||
|
page.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user