@kernhq/module-tracker 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +16 -3
- package/src/client/api-instance.ts +34 -0
- package/src/client/components/BoardCard.svelte +182 -0
- package/src/client/components/BoardView.svelte +248 -0
- package/src/client/components/CommentComposer.svelte +219 -0
- package/src/client/components/CommentThread.svelte +389 -0
- package/src/client/components/CustomField.svelte +333 -0
- package/src/client/components/DueDate.svelte +44 -0
- package/src/client/components/FilterMenu.svelte +91 -0
- package/src/client/components/GroupHeader.svelte +137 -0
- package/src/client/components/HomeLinks.svelte +38 -0
- package/src/client/components/IssueApprovals.svelte +217 -0
- package/src/client/components/IssueConnections.svelte +424 -0
- package/src/client/components/IssueDetailPanel.svelte +1337 -0
- package/src/client/components/IssueInline.svelte +75 -0
- package/src/client/components/IssueListView.svelte +153 -0
- package/src/client/components/IssuePicker.svelte +180 -0
- package/src/client/components/IssueRow.svelte +171 -0
- package/src/client/components/IssueTime.svelte +306 -0
- package/src/client/components/KqlInput.svelte +295 -0
- package/src/client/components/NewIssueDialog.svelte +210 -0
- package/src/client/components/NewProjectDialog.svelte +235 -0
- package/src/client/components/PriorityGlyph.svelte +52 -0
- package/src/client/components/SaveViewDialog.svelte +153 -0
- package/src/client/components/SidebarProjects.svelte +263 -0
- package/src/client/components/SidebarViews.svelte +336 -0
- package/src/client/components/StatusIcon.svelte +50 -0
- package/src/client/components/TrackerControls.svelte +109 -0
- package/src/client/components/TrackerSidebar.svelte +96 -0
- package/src/client/context.svelte.ts +105 -0
- package/src/client/core-api.ts +35 -0
- package/src/client/csv.test.ts +50 -0
- package/src/client/csv.ts +60 -0
- package/src/client/filters.ts +94 -0
- package/src/client/i18n.ts +3260 -0
- package/src/client/index.ts +12 -0
- package/src/client/labels.ts +200 -0
- package/src/client/mock.ts +2729 -0
- package/src/client/module.ts +491 -0
- package/src/client/nav.test.ts +59 -0
- package/src/client/nav.ts +84 -0
- package/src/client/pages/IntakePage.svelte +287 -0
- package/src/client/pages/IssuesPage.svelte +701 -0
- package/src/client/pages/ProjectPage.svelte +29 -0
- package/src/client/pages/ReportsPage.svelte +365 -0
- package/src/client/permissions.ts +33 -0
- package/src/client/planning/PlanningSections.svelte +259 -0
- package/src/client/project/ComponentsPage.svelte +423 -0
- package/src/client/project/CyclesPage.svelte +488 -0
- package/src/client/project/MilestonesPage.svelte +342 -0
- package/src/client/project/PlanCard.svelte +191 -0
- package/src/client/project/ProjectShell.svelte +93 -0
- package/src/client/project/TemplatesPage.svelte +321 -0
- package/src/client/project/context.svelte.ts +58 -0
- package/src/client/query.ts +50 -0
- package/src/client/recurrence.test.ts +37 -0
- package/src/client/recurrence.ts +89 -0
- package/src/client/richtext.ts +155 -0
- package/src/client/settings/CycleList.svelte +422 -0
- package/src/client/settings/FieldEditor.svelte +324 -0
- package/src/client/settings/FieldsSettings.svelte +273 -0
- package/src/client/settings/ImportSettings.svelte +410 -0
- package/src/client/settings/LayoutEditor.svelte +263 -0
- package/src/client/settings/PlanningList.svelte +249 -0
- package/src/client/settings/PlanningSettings.svelte +179 -0
- package/src/client/settings/ProjectsSettings.svelte +462 -0
- package/src/client/settings/RepeatingSettings.svelte +293 -0
- package/src/client/settings/TypeEditor.svelte +214 -0
- package/src/client/settings/TypesSettings.svelte +384 -0
- package/src/client/settings/WorkflowsSettings.svelte +645 -0
- package/src/client/settings/mutations.ts +19 -0
- package/src/client/time.test.ts +39 -0
- package/src/client/time.ts +34 -0
- package/src/client/tracker.test.ts +399 -0
- package/src/client/views.ts +27 -0
- package/src/client/widgets/AssignedCountWidget.svelte +14 -0
- package/src/client/widgets/CountWidget.svelte +56 -0
- package/src/client/widgets/CycleWidget.svelte +85 -0
- package/src/client/widgets/DueSoonCountWidget.svelte +14 -0
- package/src/client/widgets/IssuesWidget.svelte +222 -0
- package/src/client/widgets/ThroughputWidget.svelte +68 -0
- package/src/client/widgets/TimerWidget.svelte +116 -0
- package/src/client/widgets/VelocityWidget.svelte +55 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Badge, Button, IconButton, Input, Spinner } from '@kernhq/ui'
|
|
3
|
+
import { t } from '../i18n.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One list of small named things a project sorts work by: components, versions, labels.
|
|
7
|
+
*
|
|
8
|
+
* All three are the same shape — a name, something to say about it, and the ability to add, rename
|
|
9
|
+
* and remove — so they share one list rather than three that drift. What differs is what each row
|
|
10
|
+
* says beside its name, which the caller supplies.
|
|
11
|
+
*/
|
|
12
|
+
export interface PlanningItem {
|
|
13
|
+
id: string
|
|
14
|
+
name: string
|
|
15
|
+
/** what this row says beside its name: a count, a date, a state */
|
|
16
|
+
note?: string | null
|
|
17
|
+
badge?: string | null
|
|
18
|
+
color?: string | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
title: string
|
|
23
|
+
description: string
|
|
24
|
+
items: PlanningItem[]
|
|
25
|
+
loading?: boolean
|
|
26
|
+
editable: boolean
|
|
27
|
+
addLabel: string
|
|
28
|
+
emptyLabel: string
|
|
29
|
+
busy?: boolean
|
|
30
|
+
onadd: (name: string) => void
|
|
31
|
+
onrename: (id: string, name: string) => void
|
|
32
|
+
onremove: (id: string) => void
|
|
33
|
+
/** an extra action per row, when one exists (releasing a version) */
|
|
34
|
+
rowAction?: { label: (item: PlanningItem) => string; run: (item: PlanningItem) => void }
|
|
35
|
+
}
|
|
36
|
+
let {
|
|
37
|
+
title,
|
|
38
|
+
description,
|
|
39
|
+
items,
|
|
40
|
+
loading = false,
|
|
41
|
+
editable,
|
|
42
|
+
addLabel,
|
|
43
|
+
emptyLabel,
|
|
44
|
+
busy = false,
|
|
45
|
+
onadd,
|
|
46
|
+
onrename,
|
|
47
|
+
onremove,
|
|
48
|
+
rowAction,
|
|
49
|
+
}: Props = $props()
|
|
50
|
+
|
|
51
|
+
let adding = $state('')
|
|
52
|
+
let editingId = $state<string | null>(null)
|
|
53
|
+
let draft = $state('')
|
|
54
|
+
/** Named on screen before it happens, never behind `window.confirm`. */
|
|
55
|
+
let confirmingId = $state<string | null>(null)
|
|
56
|
+
|
|
57
|
+
const submit = () => {
|
|
58
|
+
const name = adding.trim()
|
|
59
|
+
if (!name || busy) return
|
|
60
|
+
onadd(name)
|
|
61
|
+
adding = ''
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const startEdit = (item: PlanningItem) => {
|
|
65
|
+
editingId = item.id
|
|
66
|
+
draft = item.name
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const commitEdit = () => {
|
|
70
|
+
const id = editingId
|
|
71
|
+
const name = draft.trim()
|
|
72
|
+
editingId = null
|
|
73
|
+
if (id && name) onrename(id, name)
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<section class="plist" data-testid="planning-{title.toLowerCase().replace(/\s+/g, '-')}">
|
|
78
|
+
<header>
|
|
79
|
+
<div>
|
|
80
|
+
<h3>{title}</h3>
|
|
81
|
+
<p>{description}</p>
|
|
82
|
+
</div>
|
|
83
|
+
</header>
|
|
84
|
+
|
|
85
|
+
{#if loading}
|
|
86
|
+
<div class="state"><Spinner /></div>
|
|
87
|
+
{:else if !items.length && !editable}
|
|
88
|
+
<p class="empty">{emptyLabel}</p>
|
|
89
|
+
{:else}
|
|
90
|
+
<ul>
|
|
91
|
+
{#each items as item (item.id)}
|
|
92
|
+
<li>
|
|
93
|
+
{#if item.color}<span class="swatch" style:background={item.color}></span>{/if}
|
|
94
|
+
{#if editingId === item.id}
|
|
95
|
+
<Input
|
|
96
|
+
value={draft}
|
|
97
|
+
oninput={(e: Event) => (draft = (e.currentTarget as HTMLInputElement).value)}
|
|
98
|
+
onblur={commitEdit}
|
|
99
|
+
onkeydown={(e: KeyboardEvent) => {
|
|
100
|
+
if (e.key === 'Enter') commitEdit()
|
|
101
|
+
if (e.key === 'Escape') editingId = null
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
{:else}
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
class="name"
|
|
108
|
+
disabled={!editable}
|
|
109
|
+
onclick={() => startEdit(item)}
|
|
110
|
+
data-item={item.name}
|
|
111
|
+
>
|
|
112
|
+
{item.name}
|
|
113
|
+
</button>
|
|
114
|
+
{/if}
|
|
115
|
+
{#if item.badge}<Badge>{item.badge}</Badge>{/if}
|
|
116
|
+
{#if item.note}<span class="note">{item.note}</span>{/if}
|
|
117
|
+
{#if editable && rowAction}
|
|
118
|
+
<Button size="sm" variant="ghost" onclick={() => rowAction.run(item)}>
|
|
119
|
+
{rowAction.label(item)}
|
|
120
|
+
</Button>
|
|
121
|
+
{/if}
|
|
122
|
+
{#if editable}
|
|
123
|
+
<IconButton
|
|
124
|
+
icon="x"
|
|
125
|
+
size={22}
|
|
126
|
+
label={t('planning_remove', { name: item.name })}
|
|
127
|
+
onclick={() => (confirmingId = item.id)}
|
|
128
|
+
/>
|
|
129
|
+
{/if}
|
|
130
|
+
</li>
|
|
131
|
+
{#if confirmingId === item.id}
|
|
132
|
+
<li class="confirm" role="alertdialog">
|
|
133
|
+
<span>{t('planning_remove_body', { name: item.name })}</span>
|
|
134
|
+
<Button size="sm" variant="danger" onclick={() => { onremove(item.id); confirmingId = null }}>
|
|
135
|
+
{t('common.delete')}
|
|
136
|
+
</Button>
|
|
137
|
+
<Button size="sm" variant="ghost" onclick={() => (confirmingId = null)}>{t('common.cancel')}</Button>
|
|
138
|
+
</li>
|
|
139
|
+
{/if}
|
|
140
|
+
{:else}
|
|
141
|
+
<li class="empty-row">{emptyLabel}</li>
|
|
142
|
+
{/each}
|
|
143
|
+
</ul>
|
|
144
|
+
|
|
145
|
+
{#if editable}
|
|
146
|
+
<!-- The button carries its own handler rather than relying on the form: `Button` renders
|
|
147
|
+
`type="button"`, so a submit would depend on how its props happen to spread. -->
|
|
148
|
+
<form class="add" onsubmit={(e) => { e.preventDefault(); submit() }}>
|
|
149
|
+
<Input
|
|
150
|
+
bind:value={adding}
|
|
151
|
+
placeholder={addLabel}
|
|
152
|
+
data-testid="planning-add"
|
|
153
|
+
onkeydown={(e: KeyboardEvent) => {
|
|
154
|
+
if (e.key === 'Enter') {
|
|
155
|
+
e.preventDefault()
|
|
156
|
+
submit()
|
|
157
|
+
}
|
|
158
|
+
}}
|
|
159
|
+
/>
|
|
160
|
+
<Button size="sm" disabled={!adding.trim() || busy} onclick={submit}>{t('common.add')}</Button>
|
|
161
|
+
</form>
|
|
162
|
+
{/if}
|
|
163
|
+
{/if}
|
|
164
|
+
</section>
|
|
165
|
+
|
|
166
|
+
<style>
|
|
167
|
+
.plist {
|
|
168
|
+
padding: 14px 0;
|
|
169
|
+
border-bottom: 1px solid var(--kern-border-hairline);
|
|
170
|
+
}
|
|
171
|
+
.plist:last-child {
|
|
172
|
+
border-bottom: 0;
|
|
173
|
+
}
|
|
174
|
+
h3 {
|
|
175
|
+
margin: 0;
|
|
176
|
+
font-size: 13px;
|
|
177
|
+
font-weight: 600;
|
|
178
|
+
}
|
|
179
|
+
header p {
|
|
180
|
+
margin: 2px 0 0;
|
|
181
|
+
font-size: 12px;
|
|
182
|
+
color: var(--kern-ink-400);
|
|
183
|
+
}
|
|
184
|
+
ul {
|
|
185
|
+
list-style: none;
|
|
186
|
+
margin: 8px 0 0;
|
|
187
|
+
padding: 0;
|
|
188
|
+
}
|
|
189
|
+
li {
|
|
190
|
+
display: flex;
|
|
191
|
+
align-items: center;
|
|
192
|
+
gap: 8px;
|
|
193
|
+
padding: 4px 0;
|
|
194
|
+
}
|
|
195
|
+
.swatch {
|
|
196
|
+
width: 9px;
|
|
197
|
+
height: 9px;
|
|
198
|
+
border-radius: 3px;
|
|
199
|
+
flex: none;
|
|
200
|
+
}
|
|
201
|
+
.name {
|
|
202
|
+
flex: 1;
|
|
203
|
+
min-width: 0;
|
|
204
|
+
padding: 3px 6px;
|
|
205
|
+
margin-inline-start: -6px;
|
|
206
|
+
border: 0;
|
|
207
|
+
border-radius: var(--kern-r-sm);
|
|
208
|
+
background: none;
|
|
209
|
+
color: inherit;
|
|
210
|
+
font: inherit;
|
|
211
|
+
font-size: 13px;
|
|
212
|
+
text-align: start;
|
|
213
|
+
cursor: pointer;
|
|
214
|
+
overflow: hidden;
|
|
215
|
+
text-overflow: ellipsis;
|
|
216
|
+
white-space: nowrap;
|
|
217
|
+
}
|
|
218
|
+
.name:disabled {
|
|
219
|
+
cursor: default;
|
|
220
|
+
}
|
|
221
|
+
.name:not(:disabled):hover {
|
|
222
|
+
background: var(--kern-surface-hover);
|
|
223
|
+
}
|
|
224
|
+
.note {
|
|
225
|
+
font-size: 12px;
|
|
226
|
+
color: var(--kern-ink-400);
|
|
227
|
+
}
|
|
228
|
+
.confirm {
|
|
229
|
+
gap: 8px;
|
|
230
|
+
font-size: 12.5px;
|
|
231
|
+
color: var(--kern-danger);
|
|
232
|
+
}
|
|
233
|
+
.empty,
|
|
234
|
+
.empty-row {
|
|
235
|
+
margin: 6px 0 0;
|
|
236
|
+
font-size: 13px;
|
|
237
|
+
color: var(--kern-ink-400);
|
|
238
|
+
}
|
|
239
|
+
.add {
|
|
240
|
+
display: flex;
|
|
241
|
+
gap: 8px;
|
|
242
|
+
margin-top: 10px;
|
|
243
|
+
}
|
|
244
|
+
.state {
|
|
245
|
+
display: grid;
|
|
246
|
+
place-items: center;
|
|
247
|
+
padding: 18px;
|
|
248
|
+
}
|
|
249
|
+
</style>
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
navigation,
|
|
5
|
+
Select,
|
|
6
|
+
SettingsPage,
|
|
7
|
+
SettingsSection,
|
|
8
|
+
Spinner,
|
|
9
|
+
session,
|
|
10
|
+
toast,
|
|
11
|
+
} from '@kernhq/ui'
|
|
12
|
+
import { createMutation, createQuery } from '@tanstack/svelte-query'
|
|
13
|
+
import { getTrackerApi } from '../api-instance.js'
|
|
14
|
+
import { t } from '../i18n.js'
|
|
15
|
+
import { canTracker } from '../permissions.js'
|
|
16
|
+
import PlanningSections from '../planning/PlanningSections.svelte'
|
|
17
|
+
import { trackerKeys } from '../query.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What a project plans and sorts its work by: cycles, milestones, components, versions and labels.
|
|
21
|
+
*
|
|
22
|
+
* All five have had a server since the module existed and no screen at all, so a project could only
|
|
23
|
+
* ever use what its template happened to seed — and cycles, which no template seeds, meant the
|
|
24
|
+
* sprint progress bar in the issue header had nothing to measure and never appeared. They are
|
|
25
|
+
* project-scoped, which is why this page starts by asking which project — the other tracker
|
|
26
|
+
* settings are workspace wide and do not.
|
|
27
|
+
*
|
|
28
|
+
* The lists themselves live in `PlanningSections`, because a project's own pages in the tracker show
|
|
29
|
+
* exactly the same ones: this screen is for setting several projects up at once, that one is for
|
|
30
|
+
* adding a component while looking at the work it is for.
|
|
31
|
+
*/
|
|
32
|
+
const api = getTrackerApi()
|
|
33
|
+
|
|
34
|
+
const slug = $derived(navigation.workspaceSlug)
|
|
35
|
+
const workspaceId = $derived(session.workspaces.find((w) => w.slug === slug)?.id ?? '')
|
|
36
|
+
const canManage = $derived(canTracker('projectManage'))
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Which project the page is on, and how a link can say so.
|
|
40
|
+
*
|
|
41
|
+
* `?project=<id>` is what the sidebar's project menu links to — landing on the project you clicked
|
|
42
|
+
* rather than on whichever one happens to be first. An id for a project this workspace does not
|
|
43
|
+
* have is ignored rather than obeyed, so a stale link opens the page instead of an empty one.
|
|
44
|
+
*/
|
|
45
|
+
let selectedId = $state<string | null>(null)
|
|
46
|
+
const asked = $derived(navigation.search.project)
|
|
47
|
+
|
|
48
|
+
const projectsQuery = createQuery(() => ({
|
|
49
|
+
queryKey: trackerKeys.projects(workspaceId),
|
|
50
|
+
queryFn: () => api.projects.list({ workspaceId }),
|
|
51
|
+
enabled: Boolean(workspaceId),
|
|
52
|
+
}))
|
|
53
|
+
const projects = $derived(projectsQuery.data ?? [])
|
|
54
|
+
const projectId = $derived(
|
|
55
|
+
selectedId ?? (asked && projects.some((p) => p.id === asked) ? asked : (projects[0]?.id ?? '')),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The intake link.
|
|
60
|
+
*
|
|
61
|
+
* `setIntake` returns the token and nothing else stores it, so the page keeps what it was handed.
|
|
62
|
+
* Turning intake off returns `null`, which is the same thing as "there is no link" — the form then
|
|
63
|
+
* says it is unavailable rather than accepting requests nobody reads.
|
|
64
|
+
*/
|
|
65
|
+
let intakeToken = $state<string | null>(null)
|
|
66
|
+
const intakeUrl = $derived(
|
|
67
|
+
intakeToken ? `${typeof location === 'undefined' ? '' : location.origin}/request/${intakeToken}` : '',
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const setIntake = createMutation(() => ({
|
|
71
|
+
mutationFn: (input: { enabled: boolean; rotate: boolean }) =>
|
|
72
|
+
api.projects.setIntake({ workspaceId, projectId, ...input }),
|
|
73
|
+
onSuccess: (result: { token: string | null }) => {
|
|
74
|
+
intakeToken = result.token
|
|
75
|
+
toast.success(result.token ? t('intake_opened') : t('intake_closed'))
|
|
76
|
+
},
|
|
77
|
+
onError: (error: Error) => toast.error(error.message),
|
|
78
|
+
}))
|
|
79
|
+
|
|
80
|
+
/** Switching project drops the link: it belonged to the project that was showing. */
|
|
81
|
+
$effect(() => {
|
|
82
|
+
void projectId
|
|
83
|
+
intakeToken = null
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const copyLink = async () => {
|
|
87
|
+
try {
|
|
88
|
+
await navigator.clipboard.writeText(intakeUrl)
|
|
89
|
+
toast.success(t('intake_copied'))
|
|
90
|
+
} catch {
|
|
91
|
+
// A denied clipboard is not a failure worth an error: the link is on screen to select.
|
|
92
|
+
toast.info(t('intake_copy_manually'))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<SettingsPage title={t('settings_planning')} description={t('settings_planning_hint')}>
|
|
98
|
+
{#if projectsQuery.isPending}
|
|
99
|
+
<SettingsSection><div class="state"><Spinner /></div></SettingsSection>
|
|
100
|
+
{:else if !projects.length}
|
|
101
|
+
<SettingsSection>
|
|
102
|
+
<p class="state">{t('settings_planning_no_projects')}</p>
|
|
103
|
+
</SettingsSection>
|
|
104
|
+
{:else}
|
|
105
|
+
<SettingsSection title={t('settings_planning_project')}>
|
|
106
|
+
<Select
|
|
107
|
+
value={projectId}
|
|
108
|
+
options={projects.map((p) => ({ value: p.id, label: `${p.key} · ${p.name}` }))}
|
|
109
|
+
onValueChange={(v: string) => (selectedId = v)}
|
|
110
|
+
/>
|
|
111
|
+
</SettingsSection>
|
|
112
|
+
|
|
113
|
+
<SettingsSection title={t('intake_title')} description={t('intake_hint')}>
|
|
114
|
+
{#if intakeToken}
|
|
115
|
+
<p class="intake-link">
|
|
116
|
+
<code data-testid="intake-link">{intakeUrl}</code>
|
|
117
|
+
</p>
|
|
118
|
+
<div class="row">
|
|
119
|
+
<Button size="sm" variant="ghost" onclick={() => copyLink()} data-testid="intake-copy">
|
|
120
|
+
{t('intake_copy')}
|
|
121
|
+
</Button>
|
|
122
|
+
<Button size="sm" variant="ghost" onclick={() => setIntake.mutate({ enabled: true, rotate: true })}>
|
|
123
|
+
{t('intake_rotate')}
|
|
124
|
+
</Button>
|
|
125
|
+
<Button
|
|
126
|
+
size="sm"
|
|
127
|
+
variant="ghost"
|
|
128
|
+
onclick={() => setIntake.mutate({ enabled: false, rotate: false })}
|
|
129
|
+
data-testid="intake-close"
|
|
130
|
+
>
|
|
131
|
+
{t('intake_close')}
|
|
132
|
+
</Button>
|
|
133
|
+
</div>
|
|
134
|
+
<p class="note">{t('intake_rotate_hint')}</p>
|
|
135
|
+
{:else}
|
|
136
|
+
<Button
|
|
137
|
+
size="sm"
|
|
138
|
+
disabled={!canManage}
|
|
139
|
+
loading={setIntake.isPending}
|
|
140
|
+
onclick={() => setIntake.mutate({ enabled: true, rotate: false })}
|
|
141
|
+
data-testid="intake-open"
|
|
142
|
+
>
|
|
143
|
+
{t('intake_open')}
|
|
144
|
+
</Button>
|
|
145
|
+
{/if}
|
|
146
|
+
</SettingsSection>
|
|
147
|
+
|
|
148
|
+
<SettingsSection>
|
|
149
|
+
<PlanningSections {workspaceId} {projectId} editable={canManage} />
|
|
150
|
+
</SettingsSection>
|
|
151
|
+
{/if}
|
|
152
|
+
</SettingsPage>
|
|
153
|
+
|
|
154
|
+
<style>
|
|
155
|
+
.intake-link {
|
|
156
|
+
margin: 0 0 8px;
|
|
157
|
+
font-size: 12.5px;
|
|
158
|
+
overflow-wrap: anywhere;
|
|
159
|
+
}
|
|
160
|
+
.intake-link code {
|
|
161
|
+
font-family: var(--kern-font-mono);
|
|
162
|
+
}
|
|
163
|
+
.row {
|
|
164
|
+
display: flex;
|
|
165
|
+
gap: 6px;
|
|
166
|
+
flex-wrap: wrap;
|
|
167
|
+
}
|
|
168
|
+
.note {
|
|
169
|
+
margin: 8px 0 0;
|
|
170
|
+
font-size: 12px;
|
|
171
|
+
color: var(--kern-ink-400);
|
|
172
|
+
}
|
|
173
|
+
.state {
|
|
174
|
+
display: grid;
|
|
175
|
+
place-items: center;
|
|
176
|
+
padding: 24px;
|
|
177
|
+
font-size: 13px;
|
|
178
|
+
}
|
|
179
|
+
</style>
|