ref: change ui
This commit is contained in:
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@600;700;800&family=Inter:wght@400;500;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500..800&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<title>My Workspace</title>
|
<title>My Workspace</title>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex min-h-0 flex-1 flex-col">
|
<div class="flex min-h-0 flex-1 flex-col">
|
||||||
<div v-if="$slots.filters" class="mb-4 flex flex-wrap items-end gap-3">
|
<div v-if="$slots.filters" class="mb-5 flex flex-wrap items-end gap-3">
|
||||||
<slot name="filters" />
|
<slot name="filters" />
|
||||||
</div>
|
</div>
|
||||||
<section class="panel flex min-h-0 flex-1 flex-col overflow-hidden">
|
<section class="panel flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||||
<header
|
<header
|
||||||
class="flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3"
|
class="flex flex-wrap items-center justify-between gap-3 border-b px-5 py-3.5"
|
||||||
style="border-color: var(--hairline)"
|
style="border-color: var(--hairline)"
|
||||||
>
|
>
|
||||||
<h2 class="table-title m-0">{{ title }}</h2>
|
<h2 class="table-title m-0">{{ title }}</h2>
|
||||||
|
|||||||
@@ -1,32 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="panel flex h-full flex-col overflow-hidden">
|
<div class="flex h-full flex-col">
|
||||||
<div class="flex flex-col gap-3 border-b p-3 sm:flex-row sm:items-center" style="border-color: var(--hairline)">
|
<AppTableShell title="Documents">
|
||||||
<IconField class="min-w-0 flex-1">
|
<template #filters>
|
||||||
<InputIcon class="pi pi-search" />
|
<IconField class="sm:w-[320px]">
|
||||||
<InputText
|
<InputIcon class="pi pi-search" />
|
||||||
:model-value="searchTerm"
|
<InputText
|
||||||
placeholder="Search documents"
|
:model-value="searchTerm"
|
||||||
class="search-input w-full"
|
placeholder="Search documents..."
|
||||||
@update:model-value="onSearch"
|
class="search-input w-full"
|
||||||
/>
|
@update:model-value="onSearch"
|
||||||
</IconField>
|
/>
|
||||||
<div v-if="canCreate" class="flex shrink-0 gap-2">
|
</IconField>
|
||||||
<Button label="Document" icon="pi pi-plus" size="small" @click="$emit('create', 'Document')" />
|
<Select v-model="filters.type" :options="typeOptions" optionLabel="label" optionValue="value" placeholder="Type" showClear class="w-[140px]" />
|
||||||
<Button label="Folder" icon="pi pi-folder-open" severity="secondary" outlined size="small" @click="$emit('create', 'Folder')" />
|
<Select v-model="filters.owner" :options="members" optionLabel="displayName" optionValue="userId" placeholder="Owner" showClear class="w-[170px]" />
|
||||||
</div>
|
<Select v-model="filters.updatedBy" :options="members" optionLabel="displayName" optionValue="userId" placeholder="Last edited by" showClear class="w-[180px]" />
|
||||||
</div>
|
</template>
|
||||||
|
<template #actions>
|
||||||
|
<template v-if="canCreate">
|
||||||
|
<Button label="Document" icon="pi pi-plus" severity="secondary" outlined @click="$emit('create', 'Document')" />
|
||||||
|
<Button label="Folder" icon="pi pi-folder-open" @click="$emit('create', 'Folder')" />
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
<p v-if="canCreate" class="muted-note border-b px-3 py-2" style="border-color: var(--hairline)">
|
<p v-if="canCreate" class="muted-note border-b px-4 py-2" style="border-color: var(--hairline)">
|
||||||
{{ creatingLabel }}
|
{{ creatingLabel }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="min-h-0 flex-1 overflow-auto">
|
|
||||||
<TreeTable
|
<TreeTable
|
||||||
v-model:selectionKeys="selectionKeys"
|
v-model:selectionKeys="selectionKeys"
|
||||||
|
v-model:expandedKeys="expandedKeys"
|
||||||
:value="nodes"
|
:value="nodes"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
dataKey="key"
|
||||||
selectionMode="single"
|
selectionMode="single"
|
||||||
|
scrollable
|
||||||
|
scrollHeight="flex"
|
||||||
|
class="min-h-0 min-w-[720px] flex-1"
|
||||||
@node-select="onNodeSelect"
|
@node-select="onNodeSelect"
|
||||||
|
@node-unselect="onNodeUnselect"
|
||||||
>
|
>
|
||||||
<Column field="title" header="Name" expander style="min-width: 240px">
|
<Column field="title" header="Name" expander style="min-width: 240px">
|
||||||
<template #body="{ node }">
|
<template #body="{ node }">
|
||||||
@@ -55,21 +66,46 @@
|
|||||||
<span class="muted-note">{{ formatDate(node.data.updatedAt) }}</span>
|
<span class="muted-note">{{ formatDate(node.data.updatedAt) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
<Column header="" style="width: 110px">
|
||||||
|
<template #body="{ node }">
|
||||||
|
<div v-if="canEdit || canDelete" class="flex justify-end gap-1">
|
||||||
|
<Button
|
||||||
|
v-if="canEdit"
|
||||||
|
icon="pi pi-pencil"
|
||||||
|
text
|
||||||
|
severity="secondary"
|
||||||
|
size="small"
|
||||||
|
:aria-label="`Rename ${node.data.title}`"
|
||||||
|
@click.stop="$emit('rename', node.data)"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="canDelete"
|
||||||
|
icon="pi pi-trash"
|
||||||
|
text
|
||||||
|
severity="danger"
|
||||||
|
size="small"
|
||||||
|
:aria-label="`Delete ${node.data.title}`"
|
||||||
|
@click.stop="$emit('delete', node.data)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="grid place-items-center gap-2 px-4 py-14 text-center">
|
<div class="grid place-items-center gap-2 px-4 py-14 text-center">
|
||||||
<i class="pi pi-file-edit text-2xl" style="color: var(--ink-muted)"></i>
|
<i class="pi pi-file-edit text-2xl" style="color: var(--ink-muted)"></i>
|
||||||
<p class="font-semibold" style="color: var(--ink)">No documents yet</p>
|
<p class="font-semibold" style="color: var(--ink)">{{ hasActiveFilters ? 'No matching documents' : 'No documents yet' }}</p>
|
||||||
<p class="muted-note max-w-[280px]">
|
<p class="muted-note max-w-[280px]">
|
||||||
{{ canCreate ? 'Create a document to start writing, or a folder to group them.' : 'Ask a project owner for document access.' }}
|
{{ hasActiveFilters ? 'Try clearing the filters.' : (canCreate ? 'Create a document to start writing, or a folder to group them.' : 'Ask a project owner for document access.') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</TreeTable>
|
</TreeTable>
|
||||||
</div>
|
</AppTableShell>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import AppTableShell from './AppTableShell.vue'
|
||||||
import type { DocumentNode, DocumentType, ProjectMember } from '../types'
|
import type { DocumentNode, DocumentType, ProjectMember } from '../types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -79,6 +115,8 @@ const props = defineProps<{
|
|||||||
searchTerm: string
|
searchTerm: string
|
||||||
creatingLabel: string
|
creatingLabel: string
|
||||||
canCreate: boolean
|
canCreate: boolean
|
||||||
|
canEdit: boolean
|
||||||
|
canDelete: boolean
|
||||||
members: ProjectMember[]
|
members: ProjectMember[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -86,6 +124,8 @@ const emit = defineEmits<{
|
|||||||
'update:searchTerm': [value: string]
|
'update:searchTerm': [value: string]
|
||||||
select: [id: string]
|
select: [id: string]
|
||||||
create: [type: DocumentType]
|
create: [type: DocumentType]
|
||||||
|
rename: [node: DocumentNode]
|
||||||
|
delete: [node: DocumentNode]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function onSearch(value: unknown) {
|
function onSearch(value: unknown) {
|
||||||
@@ -111,8 +151,39 @@ function toTreeTableNode(node: DocumentNode): DocumentTreeTableNode {
|
|||||||
return { key: node.id, data: node, children: sortNodes(node.children).map(toTreeTableNode) }
|
return { key: node.id, data: node, children: sortNodes(node.children).map(toTreeTableNode) }
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodes = computed(() => sortNodes(props.tree).map(toTreeTableNode))
|
const filters = reactive<{ type: DocumentType | null; owner: string | null; updatedBy: string | null }>({
|
||||||
|
type: null,
|
||||||
|
owner: null,
|
||||||
|
updatedBy: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const typeOptions = [
|
||||||
|
{ label: 'Document', value: 'Document' },
|
||||||
|
{ label: 'Folder', value: 'Folder' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const hasActiveFilters = computed(() => !!(filters.type || filters.owner || filters.updatedBy))
|
||||||
|
|
||||||
|
function matchesFilters(node: DocumentNode): boolean {
|
||||||
|
if (filters.type && node.type !== filters.type) return false
|
||||||
|
if (filters.owner && node.createdBy !== filters.owner) return false
|
||||||
|
if (filters.updatedBy && node.updatedBy !== filters.updatedBy) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterNodes(nodes: DocumentNode[]): DocumentNode[] {
|
||||||
|
const result: DocumentNode[] = []
|
||||||
|
for (const node of nodes) {
|
||||||
|
const children = filterNodes(node.children)
|
||||||
|
if (matchesFilters(node) || children.length > 0) result.push({ ...node, children })
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredTree = computed(() => (hasActiveFilters.value ? filterNodes(props.tree) : props.tree))
|
||||||
|
const nodes = computed(() => sortNodes(filteredTree.value).map(toTreeTableNode))
|
||||||
const selectionKeys = ref<Record<string, boolean>>({})
|
const selectionKeys = ref<Record<string, boolean>>({})
|
||||||
|
const expandedKeys = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
function resolveUserName(userId: string) {
|
function resolveUserName(userId: string) {
|
||||||
return props.members.find((m) => m.userId === userId)?.displayName ?? '—'
|
return props.members.find((m) => m.userId === userId)?.displayName ?? '—'
|
||||||
@@ -123,7 +194,18 @@ function formatDate(v?: string | null) {
|
|||||||
return Number.isNaN(date.valueOf()) ? '—' : date.toLocaleDateString()
|
return Number.isNaN(date.valueOf()) ? '—' : date.toLocaleDateString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleFolder(node?: { key?: string; data?: DocumentNode }) {
|
||||||
|
if (node?.key && node.data?.type === 'Folder') {
|
||||||
|
expandedKeys.value = { ...expandedKeys.value, [node.key]: !expandedKeys.value[node.key] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onNodeSelect(node: { key?: string; data?: DocumentNode }) {
|
function onNodeSelect(node: { key?: string; data?: DocumentNode }) {
|
||||||
if (node.key) emit('select', node.key)
|
if (node.data?.type === 'Document' && node.key) emit('select', node.key)
|
||||||
|
else toggleFolder(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNodeUnselect(node: { key?: string; data?: DocumentNode }) {
|
||||||
|
toggleFolder(node)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+20
-19
@@ -12,16 +12,16 @@
|
|||||||
<aside
|
<aside
|
||||||
class="fixed inset-y-0 left-0 z-40 flex w-[236px] transform flex-col border-r px-3 py-4 transition-transform duration-200 lg:static"
|
class="fixed inset-y-0 left-0 z-40 flex w-[236px] transform flex-col border-r px-3 py-4 transition-transform duration-200 lg:static"
|
||||||
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:hidden'"
|
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:hidden'"
|
||||||
style="background: var(--panel); border-color: var(--hairline)"
|
style="background: var(--primary); border-color: var(--primary)"
|
||||||
>
|
>
|
||||||
<div class="mb-5 flex items-center justify-between px-2">
|
<div class="mb-5 flex items-center justify-between px-2">
|
||||||
<router-link to="/projects" class="flex items-center gap-2.5">
|
<router-link to="/projects" class="flex items-center gap-2.5">
|
||||||
<span
|
<span
|
||||||
class="grid h-8 w-8 place-items-center rounded-[10px] font-display text-[15px] font-extrabold text-white"
|
class="grid h-8 w-8 place-items-center rounded-[10px] font-display text-[15px] font-extrabold"
|
||||||
style="background: var(--primary)"
|
style="background: #fff; color: var(--primary)"
|
||||||
>M</span
|
>M</span
|
||||||
>
|
>
|
||||||
<span class="font-display text-[15px] font-extrabold tracking-[-0.01em]" style="color: var(--ink)">
|
<span class="font-display text-[15px] font-extrabold tracking-[-0.01em]" style="color: #fff">
|
||||||
Workspace
|
Workspace
|
||||||
</span>
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
rounded
|
rounded
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
class="lg:hidden"
|
class="lg:hidden"
|
||||||
|
style="color: #fff"
|
||||||
aria-label="Close menu"
|
aria-label="Close menu"
|
||||||
@click="sidebarOpen = false"
|
@click="sidebarOpen = false"
|
||||||
/>
|
/>
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
<i class="pi pi-cog"></i> Settings
|
<i class="pi pi-cog"></i> Settings
|
||||||
<i class="pi ml-auto text-xs" :class="settingsExpanded ? 'pi-chevron-down' : 'pi-chevron-right'"></i>
|
<i class="pi ml-auto text-xs" :class="settingsExpanded ? 'pi-chevron-down' : 'pi-chevron-right'"></i>
|
||||||
</button>
|
</button>
|
||||||
<div v-show="settingsExpanded" class="ml-3 mt-2 flex flex-col gap-0.5 border-l pl-2" style="border-color: var(--hairline)">
|
<div v-show="settingsExpanded" class="ml-3 mt-2 flex flex-col gap-0.5 border-l pl-2" style="border-color: rgba(255, 255, 255, 0.3)">
|
||||||
<router-link v-if="auth.canView('masterdata')" class="nav-link nav-link-sub submenu-link" to="/settings/masterdata" @click="handleNavClick">
|
<router-link v-if="auth.canView('masterdata')" class="nav-link nav-link-sub submenu-link" to="/settings/masterdata" @click="handleNavClick">
|
||||||
Master Data
|
Master Data
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -100,7 +101,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="flex-1 overflow-auto">
|
<main class="flex-1 overflow-auto">
|
||||||
<div class="mx-auto h-full w-full max-w-[1280px] p-4 sm:p-6 lg:px-8 lg:py-7">
|
<div class="mx-auto h-full w-full max-w-[1200px] p-5 sm:p-7 lg:px-9 lg:py-8">
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -149,23 +150,23 @@ watch(
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.65rem;
|
gap: 0.65rem;
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
padding: 0.5rem 0.875rem;
|
padding: 0.5rem 0.875rem;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--ink-muted);
|
color: rgba(255, 255, 255, 0.85);
|
||||||
transition: background-color 0.15s ease, color 0.15s ease;
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
}
|
}
|
||||||
.nav-link i {
|
.nav-link i {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
.nav-link:hover {
|
.nav-link:hover {
|
||||||
background-color: var(--primary-soft);
|
background-color: rgba(255, 255, 255, 0.14);
|
||||||
color: var(--ink);
|
color: #fff;
|
||||||
}
|
}
|
||||||
.nav-link.router-link-active {
|
.nav-link.router-link-active {
|
||||||
background-color: var(--primary-soft);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
color: var(--primary);
|
color: #fff;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.nav-link-sub {
|
.nav-link-sub {
|
||||||
@@ -175,8 +176,8 @@ watch(
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
.nav-link-sub.router-link-active {
|
.nav-link-sub.router-link-active {
|
||||||
background-color: var(--primary-soft);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
color: var(--primary);
|
color: #fff;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.nav-link-sub.router-link-active::before {
|
.nav-link-sub.router-link-active::before {
|
||||||
@@ -187,10 +188,10 @@ watch(
|
|||||||
height: 60%;
|
height: 60%;
|
||||||
width: 3px;
|
width: 3px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--magenta);
|
background: #fff;
|
||||||
}
|
}
|
||||||
.app-dark .nav-link.router-link-active {
|
.app-dark .nav-link.router-link-active {
|
||||||
color: #9ba6f8;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.nav-link.router-link-active::before {
|
.nav-link.router-link-active::before {
|
||||||
content: '';
|
content: '';
|
||||||
@@ -200,11 +201,11 @@ watch(
|
|||||||
height: 60%;
|
height: 60%;
|
||||||
width: 3px;
|
width: 3px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--magenta);
|
background: #fff;
|
||||||
}
|
}
|
||||||
.submenu-link.router-link-active {
|
.submenu-link.router-link-active {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
color: #6a7cd0 !important;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
.submenu-link.router-link-active::before {
|
.submenu-link.router-link-active::before {
|
||||||
content: '';
|
content: '';
|
||||||
@@ -214,6 +215,6 @@ watch(
|
|||||||
height: 60%;
|
height: 60%;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--magenta);
|
background: #fff;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+15
-14
@@ -12,16 +12,16 @@
|
|||||||
<aside
|
<aside
|
||||||
class="fixed inset-y-0 left-0 z-40 flex w-[236px] transform flex-col border-r px-3 py-4 transition-transform duration-200 lg:static"
|
class="fixed inset-y-0 left-0 z-40 flex w-[236px] transform flex-col border-r px-3 py-4 transition-transform duration-200 lg:static"
|
||||||
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:hidden'"
|
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:hidden'"
|
||||||
style="background: var(--panel); border-color: var(--hairline)"
|
style="background: var(--primary); border-color: var(--primary)"
|
||||||
>
|
>
|
||||||
<div class="mb-5 flex items-center justify-between px-2">
|
<div class="mb-5 flex items-center justify-between px-2">
|
||||||
<router-link :to="auth.isAdmin ? '/projects' : '/select-project'" class="flex items-center gap-2.5">
|
<router-link :to="auth.isAdmin ? '/projects' : '/select-project'" class="flex items-center gap-2.5">
|
||||||
<span
|
<span
|
||||||
class="grid h-8 w-8 place-items-center rounded-[10px] font-display text-[15px] font-extrabold text-white"
|
class="grid h-8 w-8 place-items-center rounded-[10px] font-display text-[15px] font-extrabold"
|
||||||
style="background: var(--primary)"
|
style="background: #fff; color: var(--primary)"
|
||||||
>M</span
|
>M</span
|
||||||
>
|
>
|
||||||
<span class="font-display text-[15px] font-extrabold tracking-[-0.01em]" style="color: var(--ink)">
|
<span class="font-display text-[15px] font-extrabold tracking-[-0.01em]" style="color: #fff">
|
||||||
Workspace
|
Workspace
|
||||||
</span>
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -31,13 +31,14 @@
|
|||||||
rounded
|
rounded
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
class="lg:hidden"
|
class="lg:hidden"
|
||||||
|
style="color: #fff"
|
||||||
aria-label="Close menu"
|
aria-label="Close menu"
|
||||||
@click="sidebarOpen = false"
|
@click="sidebarOpen = false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav v-if="currentProject" class="flex flex-col gap-0.5">
|
<nav v-if="currentProject" class="flex flex-col gap-0.5">
|
||||||
<div class="truncate px-3 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wide" style="color: var(--ink-muted)">
|
<div class="truncate px-3 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wide" style="color: rgba(255, 255, 255, 0.75)">
|
||||||
{{ currentProject.name }}
|
{{ currentProject.name }}
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
@@ -107,7 +108,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="flex-1 overflow-auto">
|
<main class="flex-1 overflow-auto">
|
||||||
<div class="mx-auto h-full w-full max-w-[1280px] p-4 sm:p-6 lg:px-8 lg:py-7">
|
<div class="mx-auto h-full w-full max-w-[1200px] p-5 sm:p-7 lg:px-9 lg:py-8">
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -209,27 +210,27 @@ onMounted(() => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.65rem;
|
gap: 0.65rem;
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
padding: 0.5rem 0.875rem;
|
padding: 0.5rem 0.875rem;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--ink-muted);
|
color: rgba(255, 255, 255, 0.85);
|
||||||
transition: background-color 0.15s ease, color 0.15s ease;
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
}
|
}
|
||||||
.nav-link i {
|
.nav-link i {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
.nav-link:hover {
|
.nav-link:hover {
|
||||||
background-color: var(--primary-soft);
|
background-color: rgba(255, 255, 255, 0.14);
|
||||||
color: var(--ink);
|
color: #fff;
|
||||||
}
|
}
|
||||||
.nav-link.router-link-active {
|
.nav-link.router-link-active {
|
||||||
background-color: var(--primary-soft);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
color: var(--primary);
|
color: #fff;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.app-dark .nav-link.router-link-active {
|
.app-dark .nav-link.router-link-active {
|
||||||
color: #9ba6f8;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.nav-link.router-link-active::before {
|
.nav-link.router-link-active::before {
|
||||||
content: '';
|
content: '';
|
||||||
@@ -239,6 +240,6 @@ onMounted(() => {
|
|||||||
height: 60%;
|
height: 60%;
|
||||||
width: 3px;
|
width: 3px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--magenta);
|
background: #fff;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+78
-58
@@ -3,9 +3,9 @@
|
|||||||
@custom-variant dark (&:where(.app-dark, .app-dark *));
|
@custom-variant dark (&:where(.app-dark, .app-dark *));
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
/* Display = heavy geometric grotesque (DESIGN.md substitute for ABC Ginto Nord) */
|
/* Display = Bricolage Grotesque (characterful grotesque), body = IBM Plex Sans */
|
||||||
--font-sans: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
--font-sans: 'IBM Plex Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
--font-display: 'Hanken Grotesk', 'Inter', Helvetica, Arial, sans-serif;
|
--font-display: 'Bricolage Grotesque', 'IBM Plex Sans', Helvetica, Arial, sans-serif;
|
||||||
|
|
||||||
/* Blurple — brand primary, remaps every indigo-* utility */
|
/* Blurple — brand primary, remaps every indigo-* utility */
|
||||||
--color-indigo-50: #eef0fe;
|
--color-indigo-50: #eef0fe;
|
||||||
@@ -51,42 +51,42 @@
|
|||||||
--color-green-900: #073a1f;
|
--color-green-900: #073a1f;
|
||||||
--color-green-950: #042313;
|
--color-green-950: #042313;
|
||||||
|
|
||||||
/* Cool indigo-tinted neutrals — remaps every slate-* utility */
|
/* Warm neutral stones — remaps every slate-* utility (no indigo tint) */
|
||||||
--color-slate-50: #f6f7fb;
|
--color-slate-50: #f8f7f4;
|
||||||
--color-slate-100: #eef0f7;
|
--color-slate-100: #f0eee9;
|
||||||
--color-slate-200: #dde0ed;
|
--color-slate-200: #e5e2db;
|
||||||
--color-slate-300: #c1c6dd;
|
--color-slate-300: #cfcbc2;
|
||||||
--color-slate-400: #8e95b5;
|
--color-slate-400: #a3a09a;
|
||||||
--color-slate-500: #666d92;
|
--color-slate-500: #6e6b66;
|
||||||
--color-slate-600: #4a5177;
|
--color-slate-600: #52504c;
|
||||||
--color-slate-700: #2d3358;
|
--color-slate-700: #3b3a37;
|
||||||
--color-slate-800: #1e2353;
|
--color-slate-800: #26262a;
|
||||||
--color-slate-900: #141840;
|
--color-slate-900: #17181c;
|
||||||
--color-slate-950: #0a0d3a;
|
--color-slate-950: #0e0f12;
|
||||||
|
|
||||||
--shadow-subtle: rgba(69, 42, 124, 0.1) 0px 3px 34px;
|
--shadow-subtle: rgba(28, 25, 20, 0.08) 0 8px 32px;
|
||||||
--shadow-micro: rgba(20, 24, 64, 0.06) 0px 1px 3px;
|
--shadow-micro: rgba(28, 25, 20, 0.05) 0 1px 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--font-family: var(--font-sans);
|
--font-family: var(--font-sans);
|
||||||
--ink: #141840;
|
--ink: #17181c;
|
||||||
--ink-muted: #666d92;
|
--ink-muted: #6e6b66;
|
||||||
--primary: #5865f2;
|
--primary: #5865f2;
|
||||||
--magenta: #ec48bd;
|
--magenta: #ec48bd;
|
||||||
--canvas: #f6f7fb;
|
--canvas: #f8f7f4;
|
||||||
--panel: #ffffff;
|
--panel: #ffffff;
|
||||||
--hairline: #dde0ed;
|
--hairline: #e5e2db;
|
||||||
--primary-soft: rgba(88, 101, 242, 0.12);
|
--primary-soft: rgba(88, 101, 242, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-dark {
|
.app-dark {
|
||||||
--ink: #ffffff;
|
--ink: #f3f2ef;
|
||||||
--ink-muted: #8e95b5;
|
--ink-muted: #a3a09a;
|
||||||
--canvas: #0a0d3a;
|
--canvas: #0e0f12;
|
||||||
--panel: #141840;
|
--panel: #17191e;
|
||||||
--hairline: #2d3358;
|
--hairline: #2c2f36;
|
||||||
--primary-soft: rgba(123, 136, 245, 0.2);
|
--primary-soft: rgba(123, 136, 245, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -174,25 +174,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-dark .ck.ck-editor {
|
.app-dark .ck.ck-editor {
|
||||||
--ck-color-base-background: #0a0d3a;
|
--ck-color-base-background: #0e0f12;
|
||||||
--ck-color-base-border: #2d3358;
|
--ck-color-base-border: #2c2f36;
|
||||||
--ck-color-base-text: #ffffff;
|
--ck-color-base-text: #f3f2ef;
|
||||||
--ck-color-text: #ffffff;
|
--ck-color-text: #f3f2ef;
|
||||||
--ck-color-focus-border: #5865f2;
|
--ck-color-focus-border: #5865f2;
|
||||||
--ck-color-toolbar-background: #141840;
|
--ck-color-toolbar-background: #17191e;
|
||||||
--ck-color-toolbar-border: #2d3358;
|
--ck-color-toolbar-border: #2c2f36;
|
||||||
--ck-color-dropdown-panel-background: #141840;
|
--ck-color-dropdown-panel-background: #17191e;
|
||||||
--ck-color-panel-background: #141840;
|
--ck-color-panel-background: #17191e;
|
||||||
--ck-color-panel-border: #2d3358;
|
--ck-color-panel-border: #2c2f36;
|
||||||
--ck-color-button-default-hover-background: #2d3358;
|
--ck-color-button-default-hover-background: #2c2f36;
|
||||||
--ck-color-button-default-active-background: #2d3358;
|
--ck-color-button-default-active-background: #2c2f36;
|
||||||
--ck-color-button-on-background: #2d3358;
|
--ck-color-button-on-background: #2c2f36;
|
||||||
--ck-color-input-background: #0a0d3a;
|
--ck-color-input-background: #0e0f12;
|
||||||
--ck-color-input-border: #2d3358;
|
--ck-color-input-border: #2c2f36;
|
||||||
--ck-color-input-text: #ffffff;
|
--ck-color-input-text: #f3f2ef;
|
||||||
--ck-color-tooltip-background: #2d3358;
|
--ck-color-tooltip-background: #2c2f36;
|
||||||
--ck-color-tooltip-text: #ffffff;
|
--ck-color-tooltip-text: #f3f2ef;
|
||||||
--ck-color-table-border: #4a5177;
|
--ck-color-table-border: #52504c;
|
||||||
--ck-color-link-default: #9ba6f8;
|
--ck-color-link-default: #9ba6f8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,28 +203,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.field > label {
|
.field > label {
|
||||||
@apply mb-1.5 block text-[13px] font-semibold uppercase tracking-[0.06em];
|
@apply mb-1.5 block text-[12px] font-semibold uppercase tracking-[0.08em];
|
||||||
color: var(--ink-muted);
|
color: var(--ink-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: clamp(28px, 3vw, 38px);
|
font-size: clamp(26px, 2.6vw, 36px);
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
line-height: 1.05;
|
line-height: 1.1;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.025em;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-subtitle {
|
.page-subtitle {
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
line-height: 1.5;
|
line-height: 1.55;
|
||||||
color: var(--ink-muted);
|
color: var(--ink-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted-note {
|
.muted-note {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.45;
|
line-height: 1.5;
|
||||||
color: var(--ink-muted);
|
color: var(--ink-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,16 +232,16 @@
|
|||||||
.eyebrow {
|
.eyebrow {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.18em;
|
letter-spacing: 0.16em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--ink-muted);
|
color: var(--ink-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-title {
|
.table-title {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: 1.25rem;
|
font-size: 1.0625rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.015em;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,13 +249,33 @@
|
|||||||
.panel {
|
.panel {
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border: 1px solid var(--hairline);
|
border: 1px solid var(--hairline);
|
||||||
border-radius: 16px;
|
border-radius: 14px;
|
||||||
|
box-shadow: var(--shadow-micro);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table headers read as quiet eyebrows, not rows of body text */
|
||||||
|
.p-datatable .p-datatable-header-cell,
|
||||||
|
.p-treetable .p-treetable-header-cell {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input.p-inputtext,
|
.search-input.p-inputtext,
|
||||||
.search-input.p-select {
|
.search-input.p-select {
|
||||||
border-radius: 12px !important;
|
border-radius: 12px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Numeric columns line up table to table */
|
||||||
|
.p-datatable-table,
|
||||||
|
.p-treetable-table {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Restrained "blurple" is an accent, not a wash */
|
||||||
|
::selection {
|
||||||
|
background: var(--primary-soft);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|||||||
+29
-29
@@ -32,32 +32,32 @@ const green = {
|
|||||||
|
|
||||||
const lightSurface = {
|
const lightSurface = {
|
||||||
0: '#ffffff',
|
0: '#ffffff',
|
||||||
50: '#f6f7fb',
|
50: '#f8f7f4',
|
||||||
100: '#eef0f7',
|
100: '#f0eee9',
|
||||||
200: '#dde0ed',
|
200: '#e5e2db',
|
||||||
300: '#c1c6dd',
|
300: '#cfcbc2',
|
||||||
400: '#8e95b5',
|
400: '#a3a09a',
|
||||||
500: '#666d92',
|
500: '#6e6b66',
|
||||||
600: '#4a5177',
|
600: '#52504c',
|
||||||
700: '#2d3358',
|
700: '#3b3a37',
|
||||||
800: '#1e2353',
|
800: '#26262a',
|
||||||
900: '#141840',
|
900: '#17181c',
|
||||||
950: '#0a0d3a',
|
950: '#0e0f12',
|
||||||
}
|
}
|
||||||
|
|
||||||
const darkSurface = {
|
const darkSurface = {
|
||||||
0: '#ffffff',
|
0: '#ffffff',
|
||||||
50: '#f6f7fb',
|
50: '#f8f7f4',
|
||||||
100: '#dde0ed',
|
100: '#e5e2db',
|
||||||
200: '#c1c6dd',
|
200: '#cfcbc2',
|
||||||
300: '#8e95b5',
|
300: '#a3a09a',
|
||||||
400: '#666d92',
|
400: '#6e6b66',
|
||||||
500: '#4a5177',
|
500: '#52504c',
|
||||||
600: '#2d3358',
|
600: '#2c2f36',
|
||||||
700: '#242a4d',
|
700: '#26282e',
|
||||||
800: '#1e2353',
|
800: '#1d1f24',
|
||||||
900: '#141840',
|
900: '#17191e',
|
||||||
950: '#0a0d3a',
|
950: '#0e0f12',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default definePreset(Aura, {
|
export default definePreset(Aura, {
|
||||||
@@ -113,34 +113,34 @@ export default definePreset(Aura, {
|
|||||||
background: '#ffffff',
|
background: '#ffffff',
|
||||||
borderColor: '{surface.200}',
|
borderColor: '{surface.200}',
|
||||||
color: '{text.color}',
|
color: '{text.color}',
|
||||||
shadow: 'rgba(69, 42, 124, 0.14) 0px 12px 48px',
|
shadow: 'rgba(24, 22, 18, 0.12) 0px 12px 48px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
surface: darkSurface,
|
surface: darkSurface,
|
||||||
text: {
|
text: {
|
||||||
color: '#ffffff',
|
color: '#f3f2ef',
|
||||||
hoverColor: '#ffffff',
|
hoverColor: '#ffffff',
|
||||||
mutedColor: '{surface.300}',
|
mutedColor: '{surface.300}',
|
||||||
hoverMutedColor: '{surface.200}',
|
hoverMutedColor: '{surface.200}',
|
||||||
},
|
},
|
||||||
primary: {
|
primary: {
|
||||||
color: '{primary.400}',
|
color: '{primary.400}',
|
||||||
contrastColor: '#0a0d3a',
|
contrastColor: '#17191e',
|
||||||
hoverColor: '{primary.300}',
|
hoverColor: '{primary.300}',
|
||||||
activeColor: '{primary.200}',
|
activeColor: '{primary.200}',
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
background: '#141840',
|
background: '#17191e',
|
||||||
borderColor: '{surface.600}',
|
borderColor: '{surface.600}',
|
||||||
},
|
},
|
||||||
overlay: {
|
overlay: {
|
||||||
modal: {
|
modal: {
|
||||||
background: '#141840',
|
background: '#17191e',
|
||||||
borderColor: '{surface.600}',
|
borderColor: '{surface.600}',
|
||||||
color: '#ffffff',
|
color: '#f3f2ef',
|
||||||
shadow: 'rgba(0, 0, 0, 0.5) 0px 12px 48px',
|
shadow: 'rgba(0, 0, 0, 0.55) 0px 12px 48px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,13 +13,15 @@
|
|||||||
<!-- Left panel: login image -->
|
<!-- Left panel: login image -->
|
||||||
<section class="brand-panel relative hidden overflow-hidden lg:flex lg:items-center lg:justify-center">
|
<section class="brand-panel relative hidden overflow-hidden lg:flex lg:items-center lg:justify-center">
|
||||||
<img src="/login.png" alt="Login" class="absolute inset-0 h-full w-full object-cover" />
|
<img src="/login.png" alt="Login" class="absolute inset-0 h-full w-full object-cover" />
|
||||||
<h1 class="relative z-10 -mt-48 font-display text-5xl font-extrabold tracking-tight text-white drop-shadow-lg">MY WORKSPACE</h1>
|
<h1 class="relative z-10 -mt-48 font-display text-5xl font-extrabold tracking-[-0.03em] text-white drop-shadow-lg">MY WORKSPACE</h1>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="flex items-center justify-center p-6" style="background: var(--canvas)">
|
<section class="flex items-center justify-center p-6 sm:p-10" style="background: var(--canvas)">
|
||||||
<div class="w-full max-w-[360px]">
|
<div class="w-full max-w-[360px]">
|
||||||
<div class="mb-7">
|
<div class="mb-8">
|
||||||
|
<span class="eyebrow">My Workspace</span>
|
||||||
<h2 class="page-title mt-2">Sign in</h2>
|
<h2 class="page-title mt-2">Sign in</h2>
|
||||||
|
<p class="page-subtitle mt-2">Projects, documents and tasks in one place.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form @submit.prevent="submit">
|
<form @submit.prevent="submit">
|
||||||
|
|||||||
@@ -7,9 +7,13 @@
|
|||||||
v-model:search-term="searchTerm"
|
v-model:search-term="searchTerm"
|
||||||
:creating-label="creatingLabel"
|
:creating-label="creatingLabel"
|
||||||
:can-create="canCreate"
|
:can-create="canCreate"
|
||||||
|
:can-edit="canEdit"
|
||||||
|
:can-delete="canDelete"
|
||||||
:members="members"
|
:members="members"
|
||||||
@select="selectDocument"
|
@select="selectDocument"
|
||||||
@create="openCreate"
|
@create="openCreate"
|
||||||
|
@rename="openRowRename"
|
||||||
|
@delete="confirmDelete"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -118,6 +122,7 @@ const createPath = ref('/')
|
|||||||
const creating = ref(false)
|
const creating = ref(false)
|
||||||
const renameDialog = ref(false)
|
const renameDialog = ref(false)
|
||||||
const renameTitle = ref('')
|
const renameTitle = ref('')
|
||||||
|
const renameTarget = ref<{ id: string; title: string; type: DocumentType } | null>(null)
|
||||||
const moveDialog = ref(false)
|
const moveDialog = ref(false)
|
||||||
const moveTarget = ref<string | null>(null)
|
const moveTarget = ref<string | null>(null)
|
||||||
|
|
||||||
@@ -294,16 +299,25 @@ async function onCreate() {
|
|||||||
|
|
||||||
function openRename() {
|
function openRename() {
|
||||||
if (doc.value) {
|
if (doc.value) {
|
||||||
|
renameTarget.value = doc.value
|
||||||
renameTitle.value = doc.value.title
|
renameTitle.value = doc.value.title
|
||||||
renameDialog.value = true
|
renameDialog.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openRowRename(node: DocumentNode) {
|
||||||
|
renameTarget.value = node
|
||||||
|
renameTitle.value = node.title
|
||||||
|
renameDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
async function onRename() {
|
async function onRename() {
|
||||||
if (!doc.value || !renameTitle.value) return
|
const target = renameTarget.value
|
||||||
|
if (!target || !renameTitle.value) return
|
||||||
try {
|
try {
|
||||||
const updated = await updateDocument(doc.value.id, { title: renameTitle.value, content: doc.value.content })
|
const content = target.type === 'Document' ? (await getDocument(target.id)).content : undefined
|
||||||
doc.value = updated
|
const updated = await updateDocument(target.id, { title: renameTitle.value, content })
|
||||||
|
if (doc.value?.id === target.id) doc.value = updated
|
||||||
renameDialog.value = false
|
renameDialog.value = false
|
||||||
toast.add({ severity: 'success', summary: 'Renamed', life: 2000 })
|
toast.add({ severity: 'success', summary: 'Renamed', life: 2000 })
|
||||||
await loadTree()
|
await loadTree()
|
||||||
@@ -342,29 +356,31 @@ function findNode(nodes: DocumentNode[], id: string): DocumentNode | null {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteMessage(): string {
|
function deleteMessage(target: { id: string; title: string; type: DocumentType }): string {
|
||||||
if (!doc.value) return ''
|
if (target.type !== 'Folder') return `Delete "${target.title}"?`
|
||||||
if (doc.value.type !== 'Folder') return `Delete "${doc.value.title}"?`
|
const node = findNode(tree.value, target.id)
|
||||||
const node = findNode(tree.value, doc.value.id)
|
|
||||||
const count = node ? countDescendants(node.children) : 0
|
const count = node ? countDescendants(node.children) : 0
|
||||||
return count > 0
|
return count > 0
|
||||||
? `Delete folder "${doc.value.title}" and the ${count} item${count === 1 ? '' : 's'} inside it? This cannot be undone.`
|
? `Delete folder "${target.title}" and the ${count} item${count === 1 ? '' : 's'} inside it? This cannot be undone.`
|
||||||
: `Delete folder "${doc.value.title}"?`
|
: `Delete folder "${target.title}"?`
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmDelete() {
|
function confirmDelete(target?: { id: string; title: string; type: DocumentType }) {
|
||||||
if (!doc.value) return
|
const item = target ?? doc.value
|
||||||
|
if (!item) return
|
||||||
confirm.require({
|
confirm.require({
|
||||||
message: deleteMessage(),
|
message: deleteMessage(item),
|
||||||
header: 'Delete',
|
header: 'Delete',
|
||||||
acceptProps: { severity: 'danger' },
|
acceptProps: { severity: 'danger' },
|
||||||
rejectProps: { severity: 'secondary', outlined: true },
|
rejectProps: { severity: 'secondary', outlined: true },
|
||||||
accept: async () => {
|
accept: async () => {
|
||||||
try {
|
try {
|
||||||
await deleteDocument(doc.value!.id)
|
await deleteDocument(item.id)
|
||||||
doc.value = null
|
if (doc.value?.id === item.id) {
|
||||||
selectedId.value = null
|
doc.value = null
|
||||||
viewerVisible.value = false
|
selectedId.value = null
|
||||||
|
viewerVisible.value = false
|
||||||
|
}
|
||||||
toast.add({ severity: 'success', summary: 'Deleted', life: 2000 })
|
toast.add({ severity: 'success', summary: 'Deleted', life: 2000 })
|
||||||
await loadTree()
|
await loadTree()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,6 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col">
|
<div class="flex h-full flex-col">
|
||||||
<div class="mb-3 flex items-center justify-end">
|
<div class="mb-3 flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<IconField class="sm:w-[280px]">
|
||||||
|
<InputIcon class="pi pi-search" />
|
||||||
|
<InputText v-model.trim="taskSearch" placeholder="Search tasks..." class="search-input w-full" />
|
||||||
|
</IconField>
|
||||||
|
<Select
|
||||||
|
v-model="assigneeFilter"
|
||||||
|
:options="assigneeOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
placeholder="Assignee"
|
||||||
|
showClear
|
||||||
|
class="w-[180px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Button v-if="auth.can('tasks', 'create')" label="New Task" icon="pi pi-plus" @click="openCreate" />
|
<Button v-if="auth.can('tasks', 'create')" label="New Task" icon="pi pi-plus" @click="openCreate" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -87,6 +102,26 @@ const members = ref<{ userId: string; displayName: string }[]>([])
|
|||||||
const draggingId = ref<string | null>(null)
|
const draggingId = ref<string | null>(null)
|
||||||
const dragOverStatus = ref<TaskStatus | null>(null)
|
const dragOverStatus = ref<TaskStatus | null>(null)
|
||||||
|
|
||||||
|
const taskSearch = ref('')
|
||||||
|
const assigneeFilter = ref<string | null>(null)
|
||||||
|
|
||||||
|
const assigneeOptions = computed(() =>
|
||||||
|
members.value.map((m) => ({ label: m.displayName, value: m.userId })),
|
||||||
|
)
|
||||||
|
|
||||||
|
const filteredTasks = computed(() => {
|
||||||
|
const q = taskSearch.value.toLowerCase()
|
||||||
|
return tasks.value.filter((t) => {
|
||||||
|
if (assigneeFilter.value && t.assigneeId !== assigneeFilter.value) return false
|
||||||
|
if (!q) return true
|
||||||
|
return (
|
||||||
|
t.title.toLowerCase().includes(q) ||
|
||||||
|
(t.description ?? '').toLowerCase().includes(q) ||
|
||||||
|
(t.assigneeName ?? '').toLowerCase().includes(q)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const editingTask = ref<Task | null>(null)
|
const editingTask = ref<Task | null>(null)
|
||||||
|
|
||||||
@@ -103,7 +138,7 @@ const columns = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
function tasksIn(status: TaskStatus) {
|
function tasksIn(status: TaskStatus) {
|
||||||
return tasks.value.filter((t) => t.status === status)
|
return filteredTasks.value.filter((t) => t.status === status)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
|
|||||||
Reference in New Issue
Block a user