Skip to content

Commit 44dc06e

Browse files
committed
Reduce codebase duplication and complexity
- Disable SSR (was enabled globally but disabled on every route) - Extract shared Card types to app/types/card.ts (7 duplicates → 1) - Extract useViewData composable from useKanban + useListView (~150 lines saved) - Extract useMutation composable to DRY 65 try/catch/toast blocks - Merge ColumnConfigModal + ListColumnConfigModal into ViewConfigModal (~400 lines saved) - Move OpenAPI spec from 2535-line inline JS to server/assets/openapi.json - Extract enrichCardsWithMetadata server utility (3 endpoints deduplicated) - Extract ViewHeader component from board + list page headers (~80 lines saved)
1 parent 8f26951 commit 44dc06e

20 files changed

Lines changed: 5330 additions & 3693 deletions

File tree

app/components/ListColumnConfigModal.vue

Lines changed: 0 additions & 424 deletions
This file was deleted.
Lines changed: 111 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ const draggable = defineAsyncComponent(() => import('vuedraggable'))
33
44
interface ColumnItem {
55
id: string
6-
name: string
6+
name?: string
77
color?: string | null
88
position?: number
9+
field?: string
910
}
1011
1112
const props = defineProps<{
13+
mode: 'board' | 'list'
1214
columns: ColumnItem[]
13-
boardId: string
1415
availableColumns?: ColumnItem[]
1516
canAddColumns?: boolean
1617
tags?: Array<{ id: string, name: string, color: string }>
@@ -22,7 +23,7 @@ const props = defineProps<{
2223
const open = defineModel<boolean>('open', { default: false })
2324
2425
const emit = defineEmits<{
25-
'add': [name: string, color?: string]
26+
'add': [nameOrField: string, color?: string]
2627
'update': [columnId: string, updates: { name?: string, color?: string }]
2728
'delete': [columnId: string]
2829
'reorder': [columns: { id: string, position: number }[]]
@@ -32,7 +33,35 @@ const emit = defineEmits<{
3233
'delete-view': []
3334
}>()
3435
35-
// Local state — buffered until Save
36+
// ─── List-mode field metadata ───
37+
const ALL_FIELDS = [
38+
{ field: 'done', label: 'Done', icon: 'i-lucide-circle-check-big' },
39+
{ field: 'ticketId', label: 'Ticket ID', icon: 'i-lucide-hash' },
40+
{ field: 'title', label: 'Title', icon: 'i-lucide-type' },
41+
{ field: 'status', label: 'Status', icon: 'i-lucide-circle-dot' },
42+
{ field: 'assignee', label: 'Assignee', icon: 'i-lucide-user' },
43+
{ field: 'priority', label: 'Priority', icon: 'i-lucide-signal' },
44+
{ field: 'tags', label: 'Tags', icon: 'i-lucide-tag' },
45+
{ field: 'dueDate', label: 'Due Date', icon: 'i-lucide-calendar' },
46+
{ field: 'createdAt', label: 'Created', icon: 'i-lucide-calendar-plus' },
47+
{ field: 'updatedAt', label: 'Updated', icon: 'i-lucide-calendar-clock' },
48+
{ field: 'description', label: 'Description', icon: 'i-lucide-text' }
49+
]
50+
51+
const activeFields = computed(() => new Set(localColumns.value.map(c => c.field)))
52+
const availableFields = computed(() =>
53+
ALL_FIELDS.filter(f => !activeFields.value.has(f.field))
54+
)
55+
56+
function fieldLabel(field: string) {
57+
return ALL_FIELDS.find(f => f.field === field)?.label || field
58+
}
59+
60+
function fieldIcon(field: string) {
61+
return ALL_FIELDS.find(f => f.field === field)?.icon || 'i-lucide-columns-3'
62+
}
63+
64+
// ─── Local state — buffered until Save ───
3665
const localColumns = ref<ColumnItem[]>([])
3766
const localTagFilters = ref<Set<string>>(new Set())
3867
const editName = ref('')
@@ -73,11 +102,12 @@ function onDragEnd() {
73102
// Just reorder locally — emitted on save
74103
}
75104
105+
// ─── Board-mode: new column ───
76106
const newColumnName = ref('')
77107
const newColumnColor = ref('#6366f1')
78108
const newColorOpen = ref(false)
79109
80-
function addColumn() {
110+
function addBoardColumn() {
81111
if (!newColumnName.value.trim()) return
82112
emit('add', newColumnName.value.trim(), newColumnColor.value)
83113
newColumnName.value = ''
@@ -92,6 +122,7 @@ function pickColor(colId: string, color: string) {
92122
emit('update', colId, { color })
93123
}
94124
125+
// ─── Tag filter toggle ───
95126
function toggleTagFilter(tagId: string) {
96127
const next = new Set(localTagFilters.value)
97128
if (next.has(tagId)) {
@@ -102,7 +133,7 @@ function toggleTagFilter(tagId: string) {
102133
localTagFilters.value = next
103134
}
104135
105-
// Dirty detection
136+
// ─── Dirty detection ───
106137
const isDirty = computed(() => {
107138
if (editName.value.trim() !== snapshotName.value) return true
108139
const currentOrder = localColumns.value.map(c => c.id)
@@ -115,7 +146,7 @@ const isDirty = computed(() => {
115146
return false
116147
})
117148
118-
// Save — emit only what changed
149+
// ─── Save — emit only what changed ───
119150
function save() {
120151
if (!isDirty.value) {
121152
open.value = false
@@ -160,7 +191,7 @@ function discardAndClose() {
160191
open.value = false
161192
}
162193
163-
// Delete — CardModal-style: button → inline confirmation
194+
// ─── Delete view — inline confirmation ───
164195
const showDeleteConfirm = ref(false)
165196
const deleteConfirmName = ref('')
166197
const deletingView = ref(false)
@@ -179,7 +210,7 @@ function handleDeleteView() {
179210
<template>
180211
<UModal
181212
v-model:open="open"
182-
:title="viewName !== undefined ? undefined : 'Board Settings'"
213+
:title="viewName !== undefined ? undefined : (mode === 'board' ? 'Board Settings' : 'List Settings')"
183214
:ui="viewName !== undefined ? { header: 'hidden', body: 'pt-0 sm:pt-0', footer: 'p-0 sm:p-0' } : { footer: 'p-0 sm:p-0' }"
184215
>
185216
<template #body>
@@ -190,7 +221,7 @@ function handleDeleteView() {
190221
<input
191222
v-model="editName"
192223
type="text"
193-
placeholder="Board name..."
224+
:placeholder="mode === 'board' ? 'Board name...' : 'List name...'"
194225
class="w-full text-[16px] font-semibold text-zinc-900 dark:text-zinc-100 placeholder-zinc-300 dark:placeholder-zinc-600 bg-transparent border-0 border-b border-transparent focus:border-zinc-200 dark:focus:border-zinc-700 rounded-none outline-none! ring-0! tracking-[-0.01em] leading-snug py-2 transition-colors"
195226
@keydown.enter="($event.target as HTMLInputElement).blur()"
196227
>
@@ -202,7 +233,9 @@ function handleDeleteView() {
202233
name="i-lucide-columns-3"
203234
class="text-[13px] text-zinc-400 dark:text-zinc-500"
204235
/>
205-
<span class="text-[12px] font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-[0.08em]">Columns</span>
236+
<span class="text-[12px] font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-[0.08em]">
237+
{{ mode === 'board' ? 'Columns' : 'Active Columns' }}
238+
</span>
206239
</div>
207240
<ClientOnly>
208241
<draggable
@@ -222,30 +255,41 @@ function handleDeleteView() {
222255
name="i-lucide-grip-vertical"
223256
class="drag-handle text-zinc-300 dark:text-zinc-600 hover:text-zinc-500 dark:hover:text-zinc-400 cursor-grab active:cursor-grabbing text-[15px] shrink-0 transition-colors"
224257
/>
225-
<UPopover
226-
v-if="canAddColumns"
227-
v-model:open="colorPopoverOpen[col.id]"
228-
>
229-
<button
230-
type="button"
231-
class="w-3.5 h-3.5 rounded-full shrink-0 ring-1 ring-black/10 dark:ring-white/10 hover:ring-2 hover:ring-indigo-400 transition-all cursor-pointer"
258+
<!-- Board mode: color dot (editable if canAddColumns) -->
259+
<template v-if="mode === 'board'">
260+
<UPopover
261+
v-if="canAddColumns"
262+
v-model:open="colorPopoverOpen[col.id]"
263+
>
264+
<button
265+
type="button"
266+
class="w-3.5 h-3.5 rounded-full shrink-0 ring-1 ring-black/10 dark:ring-white/10 hover:ring-2 hover:ring-indigo-400 transition-all cursor-pointer"
267+
:style="{ backgroundColor: col.color || '#a1a1aa' }"
268+
/>
269+
<template #content>
270+
<div class="p-2">
271+
<ColorPicker
272+
:model-value="col.color || '#a1a1aa'"
273+
@update:model-value="pickColor(col.id, $event)"
274+
/>
275+
</div>
276+
</template>
277+
</UPopover>
278+
<div
279+
v-else
280+
class="w-3.5 h-3.5 rounded-full shrink-0 ring-1 ring-black/10 dark:ring-white/10"
232281
:style="{ backgroundColor: col.color || '#a1a1aa' }"
233282
/>
234-
<template #content>
235-
<div class="p-2">
236-
<ColorPicker
237-
:model-value="col.color || '#a1a1aa'"
238-
@update:model-value="pickColor(col.id, $event)"
239-
/>
240-
</div>
241-
</template>
242-
</UPopover>
243-
<div
244-
v-else
245-
class="w-3.5 h-3.5 rounded-full shrink-0 ring-1 ring-black/10 dark:ring-white/10"
246-
:style="{ backgroundColor: col.color || '#a1a1aa' }"
283+
</template>
284+
<!-- List mode: field icon -->
285+
<UIcon
286+
v-if="mode === 'list'"
287+
:name="fieldIcon(col.field || '')"
288+
class="text-[14px] text-zinc-400 dark:text-zinc-500 shrink-0"
247289
/>
248-
<span class="text-[14px] font-medium flex-1">{{ col.name }}</span>
290+
<span class="text-[14px] font-medium flex-1">
291+
{{ mode === 'board' ? col.name : fieldLabel(col.field || '') }}
292+
</span>
249293
<div class="flex items-center gap-0.5 opacity-0 sm:group-hover:opacity-100 max-sm:opacity-60 transition-opacity">
250294
<UTooltip text="Remove column">
251295
<UButton
@@ -264,10 +308,11 @@ function handleDeleteView() {
264308

265309
<USeparator class="my-2" />
266310

311+
<!-- Board mode: add new column -->
267312
<form
268-
v-if="canAddColumns"
313+
v-if="mode === 'board' && canAddColumns"
269314
class="flex items-center gap-2"
270-
@submit.prevent="addColumn"
315+
@submit.prevent="addBoardColumn"
271316
>
272317
<UPopover v-model:open="newColorOpen">
273318
<button
@@ -295,7 +340,8 @@ function handleDeleteView() {
295340
/>
296341
</form>
297342

298-
<template v-if="availableColumns?.length">
343+
<!-- Board mode: available columns to link -->
344+
<template v-if="mode === 'board' && availableColumns?.length">
299345
<USeparator class="my-2" />
300346
<div class="flex items-center gap-1.5 mb-1">
301347
<UIcon
@@ -324,6 +370,36 @@ function handleDeleteView() {
324370
</div>
325371
</template>
326372

373+
<!-- List mode: available fields -->
374+
<template v-if="mode === 'list' && availableFields.length">
375+
<div class="flex items-center gap-1.5 mb-1">
376+
<UIcon
377+
name="i-lucide-plus-circle"
378+
class="text-[13px] text-zinc-400 dark:text-zinc-500"
379+
/>
380+
<span class="text-[12px] font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-[0.08em]">Available Fields</span>
381+
</div>
382+
<div
383+
v-for="f in availableFields"
384+
:key="f.field"
385+
class="flex items-center gap-2 px-2 py-2 rounded-lg hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors group"
386+
>
387+
<UIcon
388+
:name="f.icon"
389+
class="text-[14px] text-zinc-300 dark:text-zinc-600 shrink-0"
390+
/>
391+
<span class="text-[14px] font-medium flex-1 text-zinc-400 dark:text-zinc-500">{{ f.label }}</span>
392+
<UButton
393+
icon="i-lucide-plus"
394+
variant="ghost"
395+
color="neutral"
396+
size="xs"
397+
@click="emit('add', f.field)"
398+
/>
399+
</div>
400+
</template>
401+
402+
<!-- Tag filters (shared) -->
327403
<template v-if="tags?.length">
328404
<USeparator class="my-2" />
329405
<div class="flex items-center gap-1.5 mb-1">

0 commit comments

Comments
 (0)