formatted
This commit is contained in:
@@ -16,6 +16,7 @@ class BoardColumn extends Model
|
|||||||
{
|
{
|
||||||
/** @use HasFactory<BoardColumnFactory> */
|
/** @use HasFactory<BoardColumnFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
use SyncsProjectTemplateOwnership;
|
use SyncsProjectTemplateOwnership;
|
||||||
|
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class NotificationAction extends Model
|
|||||||
{
|
{
|
||||||
/** @use HasFactory<NotificationActionFactory> */
|
/** @use HasFactory<NotificationActionFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
use SyncsProjectTemplateOwnership;
|
use SyncsProjectTemplateOwnership;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class RepositoryConnection extends Model
|
|||||||
{
|
{
|
||||||
/** @use HasFactory<RepositoryConnectionFactory> */
|
/** @use HasFactory<RepositoryConnectionFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
use SyncsProjectTemplateOwnership;
|
use SyncsProjectTemplateOwnership;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class TicketFieldDefinition extends Model
|
|||||||
|
|
||||||
/** @use HasFactory<TicketFieldDefinitionFactory> */
|
/** @use HasFactory<TicketFieldDefinitionFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
use SyncsProjectTemplateOwnership;
|
use SyncsProjectTemplateOwnership;
|
||||||
|
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class TicketLinkType extends Model
|
|||||||
{
|
{
|
||||||
/** @use HasFactory<TicketLinkTypeFactory> */
|
/** @use HasFactory<TicketLinkTypeFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
use SyncsProjectTemplateOwnership;
|
use SyncsProjectTemplateOwnership;
|
||||||
|
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class TicketStatus extends Model
|
|||||||
{
|
{
|
||||||
/** @use HasFactory<TicketStatusFactory> */
|
/** @use HasFactory<TicketStatusFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
use SyncsProjectTemplateOwnership;
|
use SyncsProjectTemplateOwnership;
|
||||||
|
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class TicketType extends Model
|
|||||||
{
|
{
|
||||||
/** @use HasFactory<TicketTypeFactory> */
|
/** @use HasFactory<TicketTypeFactory> */
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
use SyncsProjectTemplateOwnership;
|
use SyncsProjectTemplateOwnership;
|
||||||
|
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
|
|||||||
Generated
-14
@@ -4,7 +4,6 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "KanbanGit",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inertiajs/vue3": "^2.3.7",
|
"@inertiajs/vue3": "^2.3.7",
|
||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
@@ -1303,7 +1302,6 @@
|
|||||||
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
|
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@@ -1320,7 +1318,6 @@
|
|||||||
"integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==",
|
"integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.12.2",
|
"@eslint-community/regexpp": "^4.12.2",
|
||||||
"@typescript-eslint/scope-manager": "8.58.1",
|
"@typescript-eslint/scope-manager": "8.58.1",
|
||||||
@@ -1360,7 +1357,6 @@
|
|||||||
"integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==",
|
"integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.58.1",
|
"@typescript-eslint/scope-manager": "8.58.1",
|
||||||
"@typescript-eslint/types": "8.58.1",
|
"@typescript-eslint/types": "8.58.1",
|
||||||
@@ -2111,7 +2107,6 @@
|
|||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -3020,7 +3015,6 @@
|
|||||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -3081,7 +3075,6 @@
|
|||||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"eslint-config-prettier": "bin/cli.js"
|
"eslint-config-prettier": "bin/cli.js"
|
||||||
},
|
},
|
||||||
@@ -3208,7 +3201,6 @@
|
|||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -3351,7 +3343,6 @@
|
|||||||
"integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==",
|
"integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
"globals": "^13.24.0",
|
"globals": "^13.24.0",
|
||||||
@@ -5483,7 +5474,6 @@
|
|||||||
"integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==",
|
"integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
@@ -6525,7 +6515,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -6591,7 +6580,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"napi-postinstall": "^0.3.0"
|
"napi-postinstall": "^0.3.0"
|
||||||
},
|
},
|
||||||
@@ -6642,7 +6630,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz",
|
||||||
"integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==",
|
"integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.33.0",
|
"lightningcss": "^1.33.0",
|
||||||
"picomatch": "^4.0.5",
|
"picomatch": "^4.0.5",
|
||||||
@@ -6998,7 +6985,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz",
|
||||||
"integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==",
|
"integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.30",
|
"@vue/compiler-dom": "3.5.30",
|
||||||
"@vue/compiler-sfc": "3.5.30",
|
"@vue/compiler-sfc": "3.5.30",
|
||||||
|
|||||||
@@ -121,9 +121,7 @@ const parseChecklistLine = (
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const indentLevel = clampIndent(
|
const indentLevel = clampIndent(Math.floor(match.groups.indent.length / 2));
|
||||||
Math.floor(match.groups.indent.length / 2),
|
|
||||||
);
|
|
||||||
let content = match.groups.content.trim();
|
let content = match.groups.content.trim();
|
||||||
let checked = false;
|
let checked = false;
|
||||||
|
|
||||||
|
|||||||
@@ -52,12 +52,16 @@ const breadcrumbs: BreadcrumbItem[] = [
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<Form
|
<Form
|
||||||
v-bind="UserManagementController.store.form()"
|
v-bind="UserManagementController.store.form()"
|
||||||
:transform="(data) => ({
|
:transform="
|
||||||
...data,
|
(data) => ({
|
||||||
must_change_password_on_next_login:
|
...data,
|
||||||
data.must_change_password_on_next_login === true ||
|
must_change_password_on_next_login:
|
||||||
data.must_change_password_on_next_login === 'on',
|
data.must_change_password_on_next_login ===
|
||||||
})"
|
true ||
|
||||||
|
data.must_change_password_on_next_login ===
|
||||||
|
'on',
|
||||||
|
})
|
||||||
|
"
|
||||||
class="space-y-6"
|
class="space-y-6"
|
||||||
v-slot="{ errors, processing }"
|
v-slot="{ errors, processing }"
|
||||||
>
|
>
|
||||||
@@ -128,13 +132,19 @@ const breadcrumbs: BreadcrumbItem[] = [
|
|||||||
<span class="block font-medium">
|
<span class="block font-medium">
|
||||||
Must change password on next login
|
Must change password on next login
|
||||||
</span>
|
</span>
|
||||||
<span class="text-muted-foreground block text-sm">
|
<span
|
||||||
Redirect the user to the security settings page until they save a new password.
|
class="text-muted-foreground block text-sm"
|
||||||
|
>
|
||||||
|
Redirect the user to the security
|
||||||
|
settings page until they save a new
|
||||||
|
password.
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
<InputError
|
<InputError
|
||||||
:message="errors.must_change_password_on_next_login"
|
:message="
|
||||||
|
errors.must_change_password_on_next_login
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ import {
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import AppLayout from '@/layouts/AppLayout.vue';
|
import AppLayout from '@/layouts/AppLayout.vue';
|
||||||
import { index as projectsIndex, show as projectShow } from '@/routes/projects/index';
|
import {
|
||||||
|
index as projectsIndex,
|
||||||
|
show as projectShow,
|
||||||
|
} from '@/routes/projects/index';
|
||||||
import {
|
import {
|
||||||
create as projectTemplateCreate,
|
create as projectTemplateCreate,
|
||||||
store as projectTemplateStore,
|
store as projectTemplateStore,
|
||||||
@@ -40,7 +43,9 @@ const breadcrumbs: BreadcrumbItem[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
copy_areas: props.copyAreaOptions.map((copyAreaOption) => copyAreaOption.value),
|
copy_areas: props.copyAreaOptions.map(
|
||||||
|
(copyAreaOption) => copyAreaOption.value,
|
||||||
|
),
|
||||||
name: `${props.project.name} template`,
|
name: `${props.project.name} template`,
|
||||||
slug: `${props.project.key.toLowerCase()}-template`,
|
slug: `${props.project.key.toLowerCase()}-template`,
|
||||||
});
|
});
|
||||||
@@ -48,7 +53,9 @@ const form = useForm({
|
|||||||
const copyAreasError = computed(() => {
|
const copyAreasError = computed(() => {
|
||||||
return (
|
return (
|
||||||
form.errors.copy_areas ??
|
form.errors.copy_areas ??
|
||||||
Object.entries(form.errors).find(([key]) => key.startsWith('copy_areas.'))?.[1] ??
|
Object.entries(form.errors).find(([key]) =>
|
||||||
|
key.startsWith('copy_areas.'),
|
||||||
|
)?.[1] ??
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -66,7 +73,9 @@ const submit = (): void => {
|
|||||||
|
|
||||||
<div class="mx-auto flex w-full max-w-3xl flex-1 flex-col gap-6 p-4">
|
<div class="mx-auto flex w-full max-w-3xl flex-1 flex-col gap-6 p-4">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<Badge variant="outline" class="px-3 py-1">{{ project.key }}</Badge>
|
<Badge variant="outline" class="px-3 py-1">{{
|
||||||
|
project.key
|
||||||
|
}}</Badge>
|
||||||
<Heading
|
<Heading
|
||||||
:title="`Save ${project.name} as new template`"
|
:title="`Save ${project.name} as new template`"
|
||||||
description="Capture reusable project configuration without copying operational ticket data."
|
description="Capture reusable project configuration without copying operational ticket data."
|
||||||
@@ -77,7 +86,8 @@ const submit = (): void => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Template details</CardTitle>
|
<CardTitle>Template details</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Pick the configuration groups that should be reusable in future projects.
|
Pick the configuration groups that should be reusable in
|
||||||
|
future projects.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
@@ -85,15 +95,26 @@ const submit = (): void => {
|
|||||||
<form class="space-y-6" @submit.prevent="submit">
|
<form class="space-y-6" @submit.prevent="submit">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="name">Template name</Label>
|
<Label for="name">Template name</Label>
|
||||||
<Input id="name" v-model="form.name" required placeholder="Foundation template" />
|
<Input
|
||||||
|
id="name"
|
||||||
|
v-model="form.name"
|
||||||
|
required
|
||||||
|
placeholder="Foundation template"
|
||||||
|
/>
|
||||||
<InputError :message="form.errors.name" />
|
<InputError :message="form.errors.name" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="slug">Template slug</Label>
|
<Label for="slug">Template slug</Label>
|
||||||
<Input id="slug" v-model="form.slug" required placeholder="foundation-template" />
|
<Input
|
||||||
|
id="slug"
|
||||||
|
v-model="form.slug"
|
||||||
|
required
|
||||||
|
placeholder="foundation-template"
|
||||||
|
/>
|
||||||
<p class="text-muted-foreground text-sm">
|
<p class="text-muted-foreground text-sm">
|
||||||
Slugs are normalized automatically and used to identify reusable templates.
|
Slugs are normalized automatically and used to
|
||||||
|
identify reusable templates.
|
||||||
</p>
|
</p>
|
||||||
<InputError :message="form.errors.slug" />
|
<InputError :message="form.errors.slug" />
|
||||||
</div>
|
</div>
|
||||||
@@ -102,7 +123,8 @@ const submit = (): void => {
|
|||||||
<div>
|
<div>
|
||||||
<Label>Configuration groups</Label>
|
<Label>Configuration groups</Label>
|
||||||
<p class="text-muted-foreground text-sm">
|
<p class="text-muted-foreground text-sm">
|
||||||
Only the selected groups will be stored in the template.
|
Only the selected groups will be stored in
|
||||||
|
the template.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -119,8 +141,12 @@ const submit = (): void => {
|
|||||||
class="mt-1"
|
class="mt-1"
|
||||||
/>
|
/>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div class="font-medium">{{ copyAreaOption.label }}</div>
|
<div class="font-medium">
|
||||||
<p class="text-muted-foreground text-sm">
|
{{ copyAreaOption.label }}
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="text-muted-foreground text-sm"
|
||||||
|
>
|
||||||
{{ copyAreaOption.description }}
|
{{ copyAreaOption.description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,7 +161,9 @@ const submit = (): void => {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button as-child variant="outline">
|
<Button as-child variant="outline">
|
||||||
<Link :href="projectShow(project.id)">Cancel</Link>
|
<Link :href="projectShow(project.id)"
|
||||||
|
>Cancel</Link
|
||||||
|
>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -44,12 +44,16 @@ const form = useForm({
|
|||||||
const copyAreasError = computed(() => {
|
const copyAreasError = computed(() => {
|
||||||
return (
|
return (
|
||||||
form.errors.copy_areas ??
|
form.errors.copy_areas ??
|
||||||
Object.entries(form.errors).find(([key]) => key.startsWith('copy_areas.'))?.[1] ??
|
Object.entries(form.errors).find(([key]) =>
|
||||||
|
key.startsWith('copy_areas.'),
|
||||||
|
)?.[1] ??
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const linkedProjectCount = computed(() => props.template.linked_project_count ?? 0);
|
const linkedProjectCount = computed(
|
||||||
|
() => props.template.linked_project_count ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
const submit = (): void => {
|
const submit = (): void => {
|
||||||
form.patch(projectTemplatesUpdate.url(props.template.id), {
|
form.patch(projectTemplatesUpdate.url(props.template.id), {
|
||||||
@@ -65,9 +69,12 @@ const submit = (): void => {
|
|||||||
<div class="mx-auto flex w-full max-w-3xl flex-1 flex-col gap-6 p-4">
|
<div class="mx-auto flex w-full max-w-3xl flex-1 flex-col gap-6 p-4">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<Badge variant="outline" class="px-3 py-1">{{ template.slug }}</Badge>
|
<Badge variant="outline" class="px-3 py-1">{{
|
||||||
|
template.slug
|
||||||
|
}}</Badge>
|
||||||
<Badge v-if="linkedProjectCount > 0" variant="secondary">
|
<Badge v-if="linkedProjectCount > 0" variant="secondary">
|
||||||
{{ linkedProjectCount }} linked {{ linkedProjectCount === 1 ? 'project' : 'projects' }}
|
{{ linkedProjectCount }} linked
|
||||||
|
{{ linkedProjectCount === 1 ? 'project' : 'projects' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Heading
|
<Heading
|
||||||
@@ -81,10 +88,14 @@ const submit = (): void => {
|
|||||||
<CardTitle>Template details</CardTitle>
|
<CardTitle>Template details</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
<template v-if="template.source_project">
|
<template v-if="template.source_project">
|
||||||
This template baseline is rebuilt from {{ template.source_project.name }} ({{ template.source_project.key }}) when you save.
|
This template baseline is rebuilt from
|
||||||
|
{{ template.source_project.name }} ({{
|
||||||
|
template.source_project.key
|
||||||
|
}}) when you save.
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
This template baseline is rebuilt from its source project when you save.
|
This template baseline is rebuilt from its source
|
||||||
|
project when you save.
|
||||||
</template>
|
</template>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -93,15 +104,25 @@ const submit = (): void => {
|
|||||||
<form class="space-y-6" @submit.prevent="submit">
|
<form class="space-y-6" @submit.prevent="submit">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="name">Template name</Label>
|
<Label for="name">Template name</Label>
|
||||||
<Input id="name" v-model="form.name" required placeholder="Foundation template" />
|
<Input
|
||||||
|
id="name"
|
||||||
|
v-model="form.name"
|
||||||
|
required
|
||||||
|
placeholder="Foundation template"
|
||||||
|
/>
|
||||||
<InputError :message="form.errors.name" />
|
<InputError :message="form.errors.name" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="slug">Template slug</Label>
|
<Label for="slug">Template slug</Label>
|
||||||
<Input id="slug" :model-value="template.slug" disabled />
|
<Input
|
||||||
|
id="slug"
|
||||||
|
:model-value="template.slug"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
<p class="text-muted-foreground text-sm">
|
<p class="text-muted-foreground text-sm">
|
||||||
The slug stays locked so existing template references remain stable.
|
The slug stays locked so existing template
|
||||||
|
references remain stable.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -109,7 +130,8 @@ const submit = (): void => {
|
|||||||
<div>
|
<div>
|
||||||
<Label>Configuration groups</Label>
|
<Label>Configuration groups</Label>
|
||||||
<p class="text-muted-foreground text-sm">
|
<p class="text-muted-foreground text-sm">
|
||||||
Saving rebuilds the selected groups from the current source-project configuration.
|
Saving rebuilds the selected groups from the
|
||||||
|
current source-project configuration.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -126,8 +148,12 @@ const submit = (): void => {
|
|||||||
class="mt-1"
|
class="mt-1"
|
||||||
/>
|
/>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div class="font-medium">{{ copyAreaOption.label }}</div>
|
<div class="font-medium">
|
||||||
<p class="text-muted-foreground text-sm">
|
{{ copyAreaOption.label }}
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="text-muted-foreground text-sm"
|
||||||
|
>
|
||||||
{{ copyAreaOption.description }}
|
{{ copyAreaOption.description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,19 +169,39 @@ const submit = (): void => {
|
|||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div class="font-medium">Warning</div>
|
<div class="font-medium">Warning</div>
|
||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
Saving this template updates the baseline for {{ linkedProjectCount }} linked {{ linkedProjectCount === 1 ? 'project' : 'projects' }}.
|
Saving this template updates the baseline
|
||||||
Inherited areas will follow the new template version, while existing overrides stay untouched.
|
for {{ linkedProjectCount }} linked
|
||||||
|
{{
|
||||||
|
linkedProjectCount === 1
|
||||||
|
? 'project'
|
||||||
|
: 'projects'
|
||||||
|
}}. Inherited areas will follow the new
|
||||||
|
template version, while existing overrides
|
||||||
|
stay untouched.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Label class="flex cursor-pointer items-start gap-3">
|
<Label
|
||||||
<input v-model="form.confirm_linked_projects_update" type="checkbox" class="mt-1" />
|
class="flex cursor-pointer items-start gap-3"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="
|
||||||
|
form.confirm_linked_projects_update
|
||||||
|
"
|
||||||
|
type="checkbox"
|
||||||
|
class="mt-1"
|
||||||
|
/>
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
I understand that linked projects inheriting from this template will be affected.
|
I understand that linked projects inheriting
|
||||||
|
from this template will be affected.
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<InputError :message="form.errors.confirm_linked_projects_update" />
|
<InputError
|
||||||
|
:message="
|
||||||
|
form.errors.confirm_linked_projects_update
|
||||||
|
"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@@ -164,7 +210,9 @@ const submit = (): void => {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button as-child variant="outline">
|
<Button as-child variant="outline">
|
||||||
<Link :href="projectTemplatesIndex()">Cancel</Link>
|
<Link :href="projectTemplatesIndex()"
|
||||||
|
>Cancel</Link
|
||||||
|
>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ import {
|
|||||||
edit as projectTemplatesEdit,
|
edit as projectTemplatesEdit,
|
||||||
index as projectTemplatesIndex,
|
index as projectTemplatesIndex,
|
||||||
} from '@/routes/project-templates';
|
} from '@/routes/project-templates';
|
||||||
import { create as projectsCreate, index as projectsIndex } from '@/routes/projects/index';
|
import {
|
||||||
|
create as projectsCreate,
|
||||||
|
index as projectsIndex,
|
||||||
|
} from '@/routes/projects/index';
|
||||||
import type { BreadcrumbItem, ProjectTemplatesIndexPageProps } from '@/types';
|
import type { BreadcrumbItem, ProjectTemplatesIndexPageProps } from '@/types';
|
||||||
|
|
||||||
const props = defineProps<ProjectTemplatesIndexPageProps>();
|
const props = defineProps<ProjectTemplatesIndexPageProps>();
|
||||||
@@ -40,7 +43,9 @@ const removeTemplate = (templateId: number): void => {
|
|||||||
<Head title="Project templates" />
|
<Head title="Project templates" />
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col gap-6 p-4">
|
<div class="flex flex-1 flex-col gap-6 p-4">
|
||||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
<div
|
||||||
|
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
|
||||||
|
>
|
||||||
<Heading
|
<Heading
|
||||||
title="Project templates"
|
title="Project templates"
|
||||||
description="Reusable published project templates. Internal project base templates stay hidden here by default."
|
description="Reusable published project templates. Internal project base templates stay hidden here by default."
|
||||||
@@ -64,23 +69,43 @@ const removeTemplate = (templateId: number): void => {
|
|||||||
{{ status }}
|
{{ status }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="templates.length === 0" class="text-muted-foreground text-sm">
|
<div
|
||||||
|
v-if="templates.length === 0"
|
||||||
|
class="text-muted-foreground text-sm"
|
||||||
|
>
|
||||||
No published project templates have been saved yet.
|
No published project templates have been saved yet.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
<div v-else class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||||
<Card v-for="template in templates" :key="template.id" class="h-full">
|
<Card
|
||||||
|
v-for="template in templates"
|
||||||
|
:key="template.id"
|
||||||
|
class="h-full"
|
||||||
|
>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<CardTitle>{{ template.name }}</CardTitle>
|
<CardTitle>{{ template.name }}</CardTitle>
|
||||||
<Badge variant="secondary">{{ template.slug }}</Badge>
|
<Badge variant="secondary">{{
|
||||||
<Badge v-if="template.linked_project_count" variant="outline">
|
template.slug
|
||||||
{{ template.linked_project_count }} linked {{ template.linked_project_count === 1 ? 'project' : 'projects' }}
|
}}</Badge>
|
||||||
|
<Badge
|
||||||
|
v-if="template.linked_project_count"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{{ template.linked_project_count }} linked
|
||||||
|
{{
|
||||||
|
template.linked_project_count === 1
|
||||||
|
? 'project'
|
||||||
|
: 'projects'
|
||||||
|
}}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
<template v-if="template.source_project">
|
<template v-if="template.source_project">
|
||||||
Saved from {{ template.source_project.name }} ({{ template.source_project.key }}).
|
Saved from
|
||||||
|
{{ template.source_project.name }} ({{
|
||||||
|
template.source_project.key
|
||||||
|
}}).
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
Saved template configuration.
|
Saved template configuration.
|
||||||
@@ -106,7 +131,13 @@ const removeTemplate = (templateId: number): void => {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button as-child variant="outline">
|
<Button as-child variant="outline">
|
||||||
<Link :href="projectsCreate({ query: { template: template.id } })">
|
<Link
|
||||||
|
:href="
|
||||||
|
projectsCreate({
|
||||||
|
query: { template: template.id },
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
Use template
|
Use template
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -31,15 +31,19 @@ const breadcrumbs: BreadcrumbItem[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const defaultTemplateId = props.selectedSourceTemplateId ?? props.sourceTemplates[0]?.id ?? null;
|
const defaultTemplateId =
|
||||||
|
props.selectedSourceTemplateId ?? props.sourceTemplates[0]?.id ?? null;
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
copy_areas: [] as string[],
|
copy_areas: [] as string[],
|
||||||
creation_mode: defaultTemplateId !== null ? 'template' : 'scratch',
|
creation_mode: defaultTemplateId !== null ? 'template' : 'scratch',
|
||||||
key: '',
|
key: '',
|
||||||
name: '',
|
name: '',
|
||||||
source_project_id: props.sourceProjects[0]?.id ? String(props.sourceProjects[0].id) : '',
|
source_project_id: props.sourceProjects[0]?.id
|
||||||
source_template_id: defaultTemplateId !== null ? String(defaultTemplateId) : '',
|
? String(props.sourceProjects[0].id)
|
||||||
|
: '',
|
||||||
|
source_template_id:
|
||||||
|
defaultTemplateId !== null ? String(defaultTemplateId) : '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasSourceProjects = computed(() => props.sourceProjects.length > 0);
|
const hasSourceProjects = computed(() => props.sourceProjects.length > 0);
|
||||||
@@ -48,7 +52,9 @@ const hasSourceTemplates = computed(() => props.sourceTemplates.length > 0);
|
|||||||
const copyAreasError = computed(() => {
|
const copyAreasError = computed(() => {
|
||||||
return (
|
return (
|
||||||
form.errors.copy_areas ??
|
form.errors.copy_areas ??
|
||||||
Object.entries(form.errors).find(([key]) => key.startsWith('copy_areas.'))?.[1] ??
|
Object.entries(form.errors).find(([key]) =>
|
||||||
|
key.startsWith('copy_areas.'),
|
||||||
|
)?.[1] ??
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -93,7 +99,9 @@ const submit = (): void => {
|
|||||||
<div class="grid gap-3">
|
<div class="grid gap-3">
|
||||||
<Label>How do you want to start?</Label>
|
<Label>How do you want to start?</Label>
|
||||||
<div class="grid gap-3 sm:grid-cols-3">
|
<div class="grid gap-3 sm:grid-cols-3">
|
||||||
<Label class="flex cursor-pointer items-start gap-3 rounded-xl border p-4">
|
<Label
|
||||||
|
class="flex cursor-pointer items-start gap-3 rounded-xl border p-4"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
v-model="form.creation_mode"
|
v-model="form.creation_mode"
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -101,16 +109,25 @@ const submit = (): void => {
|
|||||||
class="mt-1"
|
class="mt-1"
|
||||||
/>
|
/>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div class="font-medium">Create from scratch</div>
|
<div class="font-medium">
|
||||||
<p class="text-muted-foreground text-sm">
|
Create from scratch
|
||||||
Start with the default project setup.
|
</div>
|
||||||
|
<p
|
||||||
|
class="text-muted-foreground text-sm"
|
||||||
|
>
|
||||||
|
Start with the default project
|
||||||
|
setup.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Label
|
<Label
|
||||||
class="flex items-start gap-3 rounded-xl border p-4"
|
class="flex items-start gap-3 rounded-xl border p-4"
|
||||||
:class="hasSourceProjects ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'"
|
:class="
|
||||||
|
hasSourceProjects
|
||||||
|
? 'cursor-pointer'
|
||||||
|
: 'cursor-not-allowed opacity-60'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-model="form.creation_mode"
|
v-model="form.creation_mode"
|
||||||
@@ -120,16 +137,26 @@ const submit = (): void => {
|
|||||||
:disabled="!hasSourceProjects"
|
:disabled="!hasSourceProjects"
|
||||||
/>
|
/>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div class="font-medium">Create from existing project</div>
|
<div class="font-medium">
|
||||||
<p class="text-muted-foreground text-sm">
|
Create from existing project
|
||||||
Copy only the settings you explicitly select. Tickets are never duplicated.
|
</div>
|
||||||
|
<p
|
||||||
|
class="text-muted-foreground text-sm"
|
||||||
|
>
|
||||||
|
Copy only the settings you
|
||||||
|
explicitly select. Tickets are never
|
||||||
|
duplicated.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Label
|
<Label
|
||||||
class="flex items-start gap-3 rounded-xl border p-4"
|
class="flex items-start gap-3 rounded-xl border p-4"
|
||||||
:class="hasSourceTemplates ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'"
|
:class="
|
||||||
|
hasSourceTemplates
|
||||||
|
? 'cursor-pointer'
|
||||||
|
: 'cursor-not-allowed opacity-60'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-model="form.creation_mode"
|
v-model="form.creation_mode"
|
||||||
@@ -139,9 +166,14 @@ const submit = (): void => {
|
|||||||
:disabled="!hasSourceTemplates"
|
:disabled="!hasSourceTemplates"
|
||||||
/>
|
/>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div class="font-medium">Create from template</div>
|
<div class="font-medium">
|
||||||
<p class="text-muted-foreground text-sm">
|
Create from template
|
||||||
Reuse a saved configuration template.
|
</div>
|
||||||
|
<p
|
||||||
|
class="text-muted-foreground text-sm"
|
||||||
|
>
|
||||||
|
Reuse a saved configuration
|
||||||
|
template.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
@@ -172,7 +204,9 @@ const submit = (): void => {
|
|||||||
/>
|
/>
|
||||||
<p class="text-muted-foreground text-sm">
|
<p class="text-muted-foreground text-sm">
|
||||||
Example ticket keys will look like
|
Example ticket keys will look like
|
||||||
<span class="text-foreground font-medium">CORE-1</span>.
|
<span class="text-foreground font-medium"
|
||||||
|
>CORE-1</span
|
||||||
|
>.
|
||||||
</p>
|
</p>
|
||||||
<InputError :message="form.errors.key" />
|
<InputError :message="form.errors.key" />
|
||||||
</div>
|
</div>
|
||||||
@@ -182,33 +216,43 @@ const submit = (): void => {
|
|||||||
class="space-y-6 rounded-xl border border-dashed p-4"
|
class="space-y-6 rounded-xl border border-dashed p-4"
|
||||||
>
|
>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="source_project_id">Source project</Label>
|
<Label for="source_project_id"
|
||||||
|
>Source project</Label
|
||||||
|
>
|
||||||
<select
|
<select
|
||||||
id="source_project_id"
|
id="source_project_id"
|
||||||
v-model="form.source_project_id"
|
v-model="form.source_project_id"
|
||||||
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
:disabled="!hasSourceProjects"
|
:disabled="!hasSourceProjects"
|
||||||
>
|
>
|
||||||
<option value="">Select a source project</option>
|
<option value="">
|
||||||
|
Select a source project
|
||||||
|
</option>
|
||||||
<option
|
<option
|
||||||
v-for="sourceProject in sourceProjects"
|
v-for="sourceProject in sourceProjects"
|
||||||
:key="sourceProject.id"
|
:key="sourceProject.id"
|
||||||
:value="String(sourceProject.id)"
|
:value="String(sourceProject.id)"
|
||||||
>
|
>
|
||||||
{{ sourceProject.name }} ({{ sourceProject.key }})
|
{{ sourceProject.name }} ({{
|
||||||
|
sourceProject.key
|
||||||
|
}})
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<p class="text-muted-foreground text-sm">
|
<p class="text-muted-foreground text-sm">
|
||||||
The new project keeps its own identity, key, and future ticket numbering.
|
The new project keeps its own identity, key,
|
||||||
|
and future ticket numbering.
|
||||||
</p>
|
</p>
|
||||||
<InputError :message="form.errors.source_project_id" />
|
<InputError
|
||||||
|
:message="form.errors.source_project_id"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-3">
|
<div class="grid gap-3">
|
||||||
<div>
|
<div>
|
||||||
<Label>Copy options</Label>
|
<Label>Copy options</Label>
|
||||||
<p class="text-muted-foreground text-sm">
|
<p class="text-muted-foreground text-sm">
|
||||||
Choose which settings should be copied into the new project.
|
Choose which settings should be copied
|
||||||
|
into the new project.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -225,8 +269,12 @@ const submit = (): void => {
|
|||||||
class="mt-1"
|
class="mt-1"
|
||||||
/>
|
/>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div class="font-medium">{{ copyAreaOption.label }}</div>
|
<div class="font-medium">
|
||||||
<p class="text-muted-foreground text-sm">
|
{{ copyAreaOption.label }}
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="text-muted-foreground text-sm"
|
||||||
|
>
|
||||||
{{ copyAreaOption.description }}
|
{{ copyAreaOption.description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -234,7 +282,8 @@ const submit = (): void => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-muted-foreground text-sm">
|
<p class="text-muted-foreground text-sm">
|
||||||
Operational ticket data is always left behind so the new project starts clean.
|
Operational ticket data is always left
|
||||||
|
behind so the new project starts clean.
|
||||||
</p>
|
</p>
|
||||||
<InputError :message="copyAreasError" />
|
<InputError :message="copyAreasError" />
|
||||||
</div>
|
</div>
|
||||||
@@ -249,7 +298,7 @@ const submit = (): void => {
|
|||||||
<select
|
<select
|
||||||
id="source_template_id"
|
id="source_template_id"
|
||||||
v-model="form.source_template_id"
|
v-model="form.source_template_id"
|
||||||
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
:disabled="!hasSourceTemplates"
|
:disabled="!hasSourceTemplates"
|
||||||
>
|
>
|
||||||
<option value="">Select a template</option>
|
<option value="">Select a template</option>
|
||||||
@@ -258,21 +307,29 @@ const submit = (): void => {
|
|||||||
:key="sourceTemplate.id"
|
:key="sourceTemplate.id"
|
||||||
:value="String(sourceTemplate.id)"
|
:value="String(sourceTemplate.id)"
|
||||||
>
|
>
|
||||||
{{ sourceTemplate.name }} ({{ sourceTemplate.slug }})
|
{{ sourceTemplate.name }} ({{
|
||||||
|
sourceTemplate.slug
|
||||||
|
}})
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<p class="text-muted-foreground text-sm">
|
<p class="text-muted-foreground text-sm">
|
||||||
Templates apply their saved configuration groups to the new project.
|
Templates apply their saved configuration
|
||||||
|
groups to the new project.
|
||||||
</p>
|
</p>
|
||||||
<InputError :message="form.errors.source_template_id" />
|
<InputError
|
||||||
|
:message="form.errors.source_template_id"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else-if="!hasSourceProjects && !hasSourceTemplates"
|
v-else-if="
|
||||||
|
!hasSourceProjects && !hasSourceTemplates
|
||||||
|
"
|
||||||
class="text-muted-foreground text-sm"
|
class="text-muted-foreground text-sm"
|
||||||
>
|
>
|
||||||
No accessible source projects or saved templates are available yet.
|
No accessible source projects or saved templates are
|
||||||
|
available yet.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
|||||||
@@ -117,7 +117,9 @@ const projectAreas = [
|
|||||||
Built from template {{ templateState.templateName }}
|
Built from template {{ templateState.templateName }}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
This project keeps its template link. Inherited areas follow the template baseline; overridden areas were customized locally.
|
This project keeps its template link. Inherited areas
|
||||||
|
follow the template baseline; overridden areas were
|
||||||
|
customized locally.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="flex flex-wrap gap-2">
|
<CardContent class="flex flex-wrap gap-2">
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
buildAcceptanceCriteria,
|
buildAcceptanceCriteria,
|
||||||
formatChecklistLines,
|
formatChecklistLines,
|
||||||
normalizeAcceptanceCriteria,
|
normalizeAcceptanceCriteria,
|
||||||
parseChecklistLines,
|
|
||||||
syncAcceptanceCriteriaDescendantCheckedState,
|
syncAcceptanceCriteriaDescendantCheckedState,
|
||||||
} from '@/lib/acceptance-criteria';
|
} from '@/lib/acceptance-criteria';
|
||||||
import ProjectTicketBranchController from '@/actions/App/Http/Controllers/ProjectTicketBranchController';
|
import ProjectTicketBranchController from '@/actions/App/Http/Controllers/ProjectTicketBranchController';
|
||||||
@@ -123,7 +122,9 @@ const breadcrumbs: BreadcrumbItem[] = [
|
|||||||
const acceptanceCriteriaEditorState = acceptanceCriteriaToEditorState(
|
const acceptanceCriteriaEditorState = acceptanceCriteriaToEditorState(
|
||||||
props.ticket.acceptance_criteria,
|
props.ticket.acceptance_criteria,
|
||||||
);
|
);
|
||||||
const acceptanceCriteriaChecklist = ref(acceptanceCriteriaEditorState.checklist);
|
const acceptanceCriteriaChecklist = ref(
|
||||||
|
acceptanceCriteriaEditorState.checklist,
|
||||||
|
);
|
||||||
const savedAcceptanceCriteria = ref(
|
const savedAcceptanceCriteria = ref(
|
||||||
normalizeAcceptanceCriteria(props.ticket.acceptance_criteria),
|
normalizeAcceptanceCriteria(props.ticket.acceptance_criteria),
|
||||||
);
|
);
|
||||||
@@ -131,7 +132,9 @@ const isPersistingAcceptanceCriteriaToggle = ref(false);
|
|||||||
const acceptanceCriteriaToggleError = ref('');
|
const acceptanceCriteriaToggleError = ref('');
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
acceptance_criteria: buildAcceptanceCriteria(acceptanceCriteriaChecklist.value),
|
acceptance_criteria: buildAcceptanceCriteria(
|
||||||
|
acceptanceCriteriaChecklist.value,
|
||||||
|
),
|
||||||
assignee_user_id: props.ticket.assignee_user_id ?? '',
|
assignee_user_id: props.ticket.assignee_user_id ?? '',
|
||||||
author_user_id: props.ticket.author_user_id ?? '',
|
author_user_id: props.ticket.author_user_id ?? '',
|
||||||
blocked_comment: props.ticket.blocked_comment ?? '',
|
blocked_comment: props.ticket.blocked_comment ?? '',
|
||||||
@@ -157,8 +160,8 @@ const form = useForm({
|
|||||||
story_points: props.ticket.story_points,
|
story_points: props.ticket.story_points,
|
||||||
});
|
});
|
||||||
|
|
||||||
const acceptanceCriteriaPreviewItems = computed(() =>
|
const acceptanceCriteriaPreviewItems = computed(
|
||||||
savedAcceptanceCriteria.value,
|
() => savedAcceptanceCriteria.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasUnsavedAcceptanceCriteriaDraft = computed(
|
const hasUnsavedAcceptanceCriteriaDraft = computed(
|
||||||
@@ -318,8 +321,7 @@ watch(
|
|||||||
|
|
||||||
branchNameValidation.value = {
|
branchNameValidation.value = {
|
||||||
is_valid: false,
|
is_valid: false,
|
||||||
message:
|
message: `${appName} could not validate this branch name right now.`,
|
||||||
`${appName} could not validate this branch name right now.`,
|
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
if (!abortController.signal.aborted) {
|
if (!abortController.signal.aborted) {
|
||||||
@@ -361,7 +363,9 @@ const submitBranchCreation = (): void => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const buildAcceptanceCriteriaForSubmit = () => {
|
const buildAcceptanceCriteriaForSubmit = () => {
|
||||||
const draftItems = buildAcceptanceCriteria(acceptanceCriteriaChecklist.value);
|
const draftItems = buildAcceptanceCriteria(
|
||||||
|
acceptanceCriteriaChecklist.value,
|
||||||
|
);
|
||||||
|
|
||||||
return draftItems.map((item, index) => ({
|
return draftItems.map((item, index) => ({
|
||||||
...item,
|
...item,
|
||||||
@@ -371,7 +375,9 @@ const buildAcceptanceCriteriaForSubmit = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildSavedTicketPayload = (acceptanceCriteria = savedAcceptanceCriteria.value) => ({
|
const buildSavedTicketPayload = (
|
||||||
|
acceptanceCriteria = savedAcceptanceCriteria.value,
|
||||||
|
) => ({
|
||||||
acceptance_criteria: acceptanceCriteria,
|
acceptance_criteria: acceptanceCriteria,
|
||||||
assignee_user_id: props.ticket.assignee_user_id,
|
assignee_user_id: props.ticket.assignee_user_id,
|
||||||
author_user_id: props.ticket.author_user_id,
|
author_user_id: props.ticket.author_user_id,
|
||||||
@@ -407,7 +413,9 @@ const persistAcceptanceCriteriaToggle = async (
|
|||||||
index,
|
index,
|
||||||
checked,
|
checked,
|
||||||
);
|
);
|
||||||
const previousChecklist = formatChecklistLines(savedAcceptanceCriteria.value);
|
const previousChecklist = formatChecklistLines(
|
||||||
|
savedAcceptanceCriteria.value,
|
||||||
|
);
|
||||||
const nextChecklist = formatChecklistLines(nextAcceptanceCriteria);
|
const nextChecklist = formatChecklistLines(nextAcceptanceCriteria);
|
||||||
const csrfToken = document
|
const csrfToken = document
|
||||||
.querySelector('meta[name="csrf-token"]')
|
.querySelector('meta[name="csrf-token"]')
|
||||||
@@ -761,14 +769,18 @@ const formatDateTime = (value: string): string => {
|
|||||||
placeholder="[ ] User can sign in [x] Validation is shown for invalid credentials"
|
placeholder="[ ] User can sign in [x] Validation is shown for invalid credentials"
|
||||||
/>
|
/>
|
||||||
<p class="text-muted-foreground text-xs">
|
<p class="text-muted-foreground text-xs">
|
||||||
Use <code>[ ]</code> / <code>[x]</code> for checklist items. Indent with two spaces for nested steps.
|
Use <code>[ ]</code> /
|
||||||
Text changes are saved with the main ticket update.
|
<code>[x]</code> for checklist items.
|
||||||
|
Indent with two spaces for nested steps.
|
||||||
|
Text changes are saved with the main
|
||||||
|
ticket update.
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
v-if="hasUnsavedAcceptanceCriteriaDraft"
|
v-if="hasUnsavedAcceptanceCriteriaDraft"
|
||||||
class="text-muted-foreground text-xs"
|
class="text-muted-foreground text-xs"
|
||||||
>
|
>
|
||||||
Save checklist text changes before toggling completion directly.
|
Save checklist text changes before
|
||||||
|
toggling completion directly.
|
||||||
</p>
|
</p>
|
||||||
<InputError
|
<InputError
|
||||||
:message="acceptanceCriteriaError"
|
:message="acceptanceCriteriaError"
|
||||||
@@ -779,7 +791,10 @@ const formatDateTime = (value: string): string => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="acceptanceCriteriaPreviewItems.length > 0"
|
v-if="
|
||||||
|
acceptanceCriteriaPreviewItems.length >
|
||||||
|
0
|
||||||
|
"
|
||||||
class="border-border rounded-xl border p-4 text-sm"
|
class="border-border rounded-xl border p-4 text-sm"
|
||||||
>
|
>
|
||||||
<p class="mb-3 font-medium">
|
<p class="mb-3 font-medium">
|
||||||
@@ -787,7 +802,9 @@ const formatDateTime = (value: string): string => {
|
|||||||
</p>
|
</p>
|
||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
<li
|
<li
|
||||||
v-for="(item, index) in acceptanceCriteriaPreviewItems"
|
v-for="(
|
||||||
|
item, index
|
||||||
|
) in acceptanceCriteriaPreviewItems"
|
||||||
:key="item.id ?? index"
|
:key="item.id ?? index"
|
||||||
:style="{
|
:style="{
|
||||||
marginLeft: `${item.indent * 1.25}rem`,
|
marginLeft: `${item.indent * 1.25}rem`,
|
||||||
@@ -805,7 +822,12 @@ const formatDateTime = (value: string): string => {
|
|||||||
hasUnsavedAcceptanceCriteriaDraft
|
hasUnsavedAcceptanceCriteriaDraft
|
||||||
"
|
"
|
||||||
class="border-input text-primary focus:ring-ring mt-0.5 size-4 rounded"
|
class="border-input text-primary focus:ring-ring mt-0.5 size-4 rounded"
|
||||||
@change="onAcceptanceCriteriaCheckboxChange(index, $event)"
|
@change="
|
||||||
|
onAcceptanceCriteriaCheckboxChange(
|
||||||
|
index,
|
||||||
|
$event,
|
||||||
|
)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
:for="`acceptance-criteria-${item.id ?? index}`"
|
:for="`acceptance-criteria-${item.id ?? index}`"
|
||||||
@@ -1530,10 +1552,7 @@ const formatDateTime = (value: string): string => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card
|
<Card v-if="ticket.ticket_type?.is_parent" class="h-fit">
|
||||||
v-if="ticket.ticket_type?.is_parent"
|
|
||||||
class="h-fit"
|
|
||||||
>
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Child tickets</CardTitle>
|
<CardTitle>Child tickets</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
@@ -1572,10 +1591,12 @@ const formatDateTime = (value: string): string => {
|
|||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Link
|
<Link
|
||||||
:href="showTicket({
|
:href="
|
||||||
project: project.id,
|
showTicket({
|
||||||
ticket: currentParentTicket.id,
|
project: project.id,
|
||||||
})"
|
ticket: currentParentTicket.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
class="border-border hover:border-primary/60 block rounded-xl border px-4 py-4 transition"
|
class="border-border hover:border-primary/60 block rounded-xl border px-4 py-4 transition"
|
||||||
>
|
>
|
||||||
<div class="font-medium">
|
<div class="font-medium">
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ const returnHref =
|
|||||||
props.source === 'board'
|
props.source === 'board'
|
||||||
? boardIndex(props.project.id)
|
? boardIndex(props.project.id)
|
||||||
: backlogIndex(props.project.id);
|
: backlogIndex(props.project.id);
|
||||||
const returnLabel = props.source === 'board' ? 'Back to board' : 'Back to backlog';
|
const returnLabel =
|
||||||
|
props.source === 'board' ? 'Back to board' : 'Back to backlog';
|
||||||
|
|
||||||
const breadcrumbs: BreadcrumbItem[] = [
|
const breadcrumbs: BreadcrumbItem[] = [
|
||||||
{
|
{
|
||||||
@@ -64,7 +65,9 @@ const hasTicketMetadata =
|
|||||||
const acceptanceCriteriaChecklist = ref('');
|
const acceptanceCriteriaChecklist = ref('');
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
acceptance_criteria: buildAcceptanceCriteria(acceptanceCriteriaChecklist.value),
|
acceptance_criteria: buildAcceptanceCriteria(
|
||||||
|
acceptanceCriteriaChecklist.value,
|
||||||
|
),
|
||||||
assignee_user_id: '',
|
assignee_user_id: '',
|
||||||
description: '',
|
description: '',
|
||||||
parent_ticket_id: '',
|
parent_ticket_id: '',
|
||||||
@@ -92,7 +95,9 @@ const acceptanceCriteriaError = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const submit = (): void => {
|
const submit = (): void => {
|
||||||
form.acceptance_criteria = buildAcceptanceCriteria(acceptanceCriteriaChecklist.value);
|
form.acceptance_criteria = buildAcceptanceCriteria(
|
||||||
|
acceptanceCriteriaChecklist.value,
|
||||||
|
);
|
||||||
|
|
||||||
form.submit(ProjectTicketController.store({ project: props.project.id }), {
|
form.submit(ProjectTicketController.store({ project: props.project.id }), {
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
@@ -173,7 +178,9 @@ const ticketStatusLabel = (ticketStatus: TicketStatusModel): string => {
|
|||||||
<InputError :message="form.errors.title" />
|
<InputError :message="form.errors.title" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]">
|
<div
|
||||||
|
class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]"
|
||||||
|
>
|
||||||
<div class="grid gap-6">
|
<div class="grid gap-6">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="description">Description</Label>
|
<Label for="description">Description</Label>
|
||||||
@@ -185,30 +192,46 @@ const ticketStatusLabel = (ticketStatus: TicketStatusModel): string => {
|
|||||||
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] disabled:cursor-not-allowed disabled:opacity-60"
|
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] disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
placeholder="Describe the expected behavior and relevant context."
|
placeholder="Describe the expected behavior and relevant context."
|
||||||
/>
|
/>
|
||||||
<InputError :message="form.errors.description" />
|
<InputError
|
||||||
|
:message="form.errors.description"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-4">
|
<div class="grid gap-4">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="acceptance_criteria_checklist">
|
<Label
|
||||||
|
for="acceptance_criteria_checklist"
|
||||||
|
>
|
||||||
Acceptance criteria checklist
|
Acceptance criteria checklist
|
||||||
</Label>
|
</Label>
|
||||||
<textarea
|
<textarea
|
||||||
id="acceptance_criteria_checklist"
|
id="acceptance_criteria_checklist"
|
||||||
v-model="acceptanceCriteriaChecklist"
|
v-model="
|
||||||
|
acceptanceCriteriaChecklist
|
||||||
|
"
|
||||||
rows="6"
|
rows="6"
|
||||||
:disabled="form.processing"
|
:disabled="form.processing"
|
||||||
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] disabled:cursor-not-allowed disabled:opacity-60"
|
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] disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
placeholder="[ ] User can sign in [x] Validation is shown for invalid credentials"
|
placeholder="[ ] User can sign in [x] Validation is shown for invalid credentials"
|
||||||
/>
|
/>
|
||||||
<p class="text-muted-foreground text-xs">
|
<p
|
||||||
Use <code>[ ]</code> / <code>[x]</code> for checklist items. Indent with two spaces for nested steps.
|
class="text-muted-foreground text-xs"
|
||||||
|
>
|
||||||
|
Use <code>[ ]</code> /
|
||||||
|
<code>[x]</code> for checklist
|
||||||
|
items. Indent with two spaces for
|
||||||
|
nested steps.
|
||||||
</p>
|
</p>
|
||||||
<InputError :message="acceptanceCriteriaError" />
|
<InputError
|
||||||
|
:message="acceptanceCriteriaError"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="acceptanceCriteriaPreviewItems.length > 0"
|
v-if="
|
||||||
|
acceptanceCriteriaPreviewItems.length >
|
||||||
|
0
|
||||||
|
"
|
||||||
class="border-border rounded-xl border p-4 text-sm"
|
class="border-border rounded-xl border p-4 text-sm"
|
||||||
>
|
>
|
||||||
<p class="mb-3 font-medium">
|
<p class="mb-3 font-medium">
|
||||||
@@ -216,14 +239,18 @@ const ticketStatusLabel = (ticketStatus: TicketStatusModel): string => {
|
|||||||
</p>
|
</p>
|
||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
<li
|
<li
|
||||||
v-for="(item, index) in acceptanceCriteriaPreviewItems"
|
v-for="(
|
||||||
|
item, index
|
||||||
|
) in acceptanceCriteriaPreviewItems"
|
||||||
:key="index"
|
:key="index"
|
||||||
:style="{
|
:style="{
|
||||||
marginLeft: `${item.indent * 1.25}rem`,
|
marginLeft: `${item.indent * 1.25}rem`,
|
||||||
}"
|
}"
|
||||||
class="flex items-start gap-2"
|
class="flex items-start gap-2"
|
||||||
>
|
>
|
||||||
<span class="mt-0.5">{{ item.checked ? '☑' : '☐' }}</span>
|
<span class="mt-0.5">{{
|
||||||
|
item.checked ? '☑' : '☐'
|
||||||
|
}}</span>
|
||||||
<span>{{ item.text }}</span>
|
<span>{{ item.text }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -248,7 +275,9 @@ const ticketStatusLabel = (ticketStatus: TicketStatusModel): string => {
|
|||||||
{{ ticketType.name }}
|
{{ ticketType.name }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<InputError :message="form.errors.ticket_type_id" />
|
<InputError
|
||||||
|
:message="form.errors.ticket_type_id"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
@@ -269,7 +298,11 @@ const ticketStatusLabel = (ticketStatus: TicketStatusModel): string => {
|
|||||||
{{ ticketPriority.name }}
|
{{ ticketPriority.name }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<InputError :message="form.errors.ticket_priority_id" />
|
<InputError
|
||||||
|
:message="
|
||||||
|
form.errors.ticket_priority_id
|
||||||
|
"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
@@ -285,10 +318,14 @@ const ticketStatusLabel = (ticketStatus: TicketStatusModel): string => {
|
|||||||
:key="ticketStatus.id"
|
:key="ticketStatus.id"
|
||||||
:value="String(ticketStatus.id)"
|
:value="String(ticketStatus.id)"
|
||||||
>
|
>
|
||||||
{{ ticketStatusLabel(ticketStatus) }}
|
{{
|
||||||
|
ticketStatusLabel(ticketStatus)
|
||||||
|
}}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<InputError :message="form.errors.ticket_status_id" />
|
<InputError
|
||||||
|
:message="form.errors.ticket_status_id"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
@@ -310,7 +347,9 @@ const ticketStatusLabel = (ticketStatus: TicketStatusModel): string => {
|
|||||||
{{ user.name }}
|
{{ user.name }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<InputError :message="form.errors.assignee_user_id" />
|
<InputError
|
||||||
|
:message="form.errors.assignee_user_id"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
@@ -323,16 +362,24 @@ const ticketStatusLabel = (ticketStatus: TicketStatusModel): string => {
|
|||||||
:disabled="form.processing"
|
:disabled="form.processing"
|
||||||
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] disabled:cursor-not-allowed disabled:opacity-60"
|
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] disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
>
|
>
|
||||||
<option value="">No parent ticket</option>
|
<option value="">
|
||||||
|
No parent ticket
|
||||||
|
</option>
|
||||||
<option
|
<option
|
||||||
v-for="parentTicketOption in parentTicketOptions"
|
v-for="parentTicketOption in parentTicketOptions"
|
||||||
:key="parentTicketOption.id"
|
:key="parentTicketOption.id"
|
||||||
:value="String(parentTicketOption.id)"
|
:value="
|
||||||
|
String(parentTicketOption.id)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ `${parentTicketOption.ticket_key} - ${parentTicketOption.title}` }}
|
{{
|
||||||
|
`${parentTicketOption.ticket_key} - ${parentTicketOption.title}`
|
||||||
|
}}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<InputError :message="form.errors.parent_ticket_id" />
|
<InputError
|
||||||
|
:message="form.errors.parent_ticket_id"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -349,7 +396,9 @@ const ticketStatusLabel = (ticketStatus: TicketStatusModel): string => {
|
|||||||
step="1"
|
step="1"
|
||||||
:disabled="form.processing"
|
:disabled="form.processing"
|
||||||
/>
|
/>
|
||||||
<InputError :message="form.errors.story_points" />
|
<InputError
|
||||||
|
:message="form.errors.story_points"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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