Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0dd37d1706 | |||
| 19bc4e8a23 |
@@ -0,0 +1,59 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
Guidance for AI coding agents working in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
LinkDesk is a VFX/animation production management system: FastAPI backend + Vue 3 frontend, run as separate dev servers with a Vite proxy bridging them.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**Backend** (`backend/`) — FastAPI + SQLAlchemy (SQLite by default, set via `DATABASE_URL` env var).
|
||||||
|
- `main.py` — entry point; registers all routers and mounts `/uploads` static dir.
|
||||||
|
- `database.py` — SQLAlchemy engine and `get_db` session dependency.
|
||||||
|
- `models/` — ORM models: project, shot, asset, task, episode, user, notification, activity, api_key.
|
||||||
|
- `routers/` — one file per resource, each prefixed in `main.py`.
|
||||||
|
- `schemas/` — Pydantic request/response schemas (separate from ORM models).
|
||||||
|
- `utils/` — shared helpers (file handling, notifications).
|
||||||
|
|
||||||
|
**Frontend** (`frontend/src/`) — Vue 3 + TypeScript + Pinia + Vue Router + shadcn-vue + TanStack Table.
|
||||||
|
- `router/index.ts` — routes with `requiresAuth`, role (`roles: [...]`), and admin-only (`adminPermission: 'required'`) guards.
|
||||||
|
- `stores/` — Pinia stores per domain (auth, projects, assets, tasks, episodes, notifications, user, settings, taskStatuses).
|
||||||
|
- `services/` — axios wrappers per resource; `api.ts` is the base client. All calls use `/api` prefix, proxied by Vite to `http://localhost:8000`.
|
||||||
|
- `components/` — domain folders (`asset/`, `shot/`, `project/`, `layout/`, `auth/`, `episode/`, `task/`) + `ui/` for shadcn primitives.
|
||||||
|
- `views/` — page-level components; project detail uses nested child routes under `/projects/:projectId`.
|
||||||
|
- `composables/` — shared composition logic.
|
||||||
|
- `types/` — shared TypeScript interfaces.
|
||||||
|
|
||||||
|
**Auth**: JWT access + refresh tokens in `localStorage`. `api.ts` interceptor auto-refreshes on 401. Router guard initializes auth from stored token on first navigation.
|
||||||
|
|
||||||
|
**Roles**: `coordinator`, `director`, `developer` + `isAdmin` flag. Admins can access any role-gated route.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
# Windows: .venv\Scripts\activate | bash/mac: source .venv/bin/activate
|
||||||
|
uvicorn main:app --reload --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev # http://localhost:5173
|
||||||
|
npm run type-check # vue-tsc --noEmit
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### First-time setup
|
||||||
|
Copy `backend/.env.example` to `backend/.env` and set `SECRET_KEY`. The database schema is created automatically on first run via `Base.metadata.create_all`.
|
||||||
|
|
||||||
|
## Coding conventions
|
||||||
|
|
||||||
|
- Backend: follow existing router/schema/model separation. Pydantic schemas live in `schemas/`, ORM models in `models/`. Never mix them.
|
||||||
|
- Frontend: services call the API; stores hold state; components consume stores. Don't call `apiClient` directly from components.
|
||||||
|
- Use `@/` alias for all frontend imports (maps to `frontend/src/`).
|
||||||
|
- Match existing style exactly — don't refactor adjacent code while fixing something.
|
||||||
|
- Minimum code that solves the problem. No speculative features or abstractions.
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
LinkDesk is a VFX/animation production management system. It has a FastAPI backend and a Vue 3 frontend, running as separate dev servers with a Vite proxy.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**Backend** (`backend/`) — FastAPI + SQLAlchemy (SQLite by default, configurable via `DATABASE_URL`).
|
||||||
|
- `main.py` — app entry point; mounts routers and static uploads.
|
||||||
|
- `database.py` — SQLAlchemy engine and `get_db` dependency.
|
||||||
|
- `models/` — SQLAlchemy ORM models (project, shot, asset, task, episode, user, notification, activity, api_key).
|
||||||
|
- `routers/` — one file per resource; each router is `include_router`'d with a prefix in `main.py`.
|
||||||
|
- `schemas/` — Pydantic request/response schemas (separate from models).
|
||||||
|
- `utils/` — shared helpers (file handling, notifications).
|
||||||
|
|
||||||
|
**Frontend** (`frontend/src/`) — Vue 3 + TypeScript + Pinia + Vue Router + shadcn-vue (Radix UI) + TanStack Table.
|
||||||
|
- `main.ts` — app bootstrap.
|
||||||
|
- `router/index.ts` — route definitions; guards enforce `requiresAuth`, role-based (`roles: [...]`), and admin-only (`adminPermission: 'required'`) access.
|
||||||
|
- `stores/` — Pinia stores (auth, projects, assets, tasks, episodes, notifications, user, settings, taskStatuses).
|
||||||
|
- `services/` — axios wrappers per resource (`api.ts` is the base client). All calls go through `/api` which Vite proxies to `http://localhost:8000`.
|
||||||
|
- `components/` — organized by domain (`asset/`, `shot/`, `project/`, `layout/`, `auth/`, `episode/`, `task/`) plus `ui/` for shadcn primitives.
|
||||||
|
- `views/` — page-level components; project detail uses nested child routes under `/projects/:projectId`.
|
||||||
|
- `composables/` — shared Vue composition logic (`useDetailPanel`, `useAvatarUrl`).
|
||||||
|
- `types/` — shared TypeScript interfaces.
|
||||||
|
|
||||||
|
**Auth flow**: JWT access + refresh tokens stored in `localStorage`. `api.ts` interceptor auto-refreshes on 401. Router guard initializes auth from stored token on first navigation.
|
||||||
|
|
||||||
|
**Role model**: `coordinator`, `director`, `developer` roles + `isAdmin` flag. Admin can access any role-gated route.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
# Activate venv first (Windows)
|
||||||
|
.venv\Scripts\activate # or: source .venv/bin/activate on bash
|
||||||
|
uvicorn main:app --reload --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev # http://localhost:5173
|
||||||
|
npm run type-check # TypeScript check (vue-tsc --noEmit)
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
Copy `.env.example` to `.env` in `backend/` and set `SECRET_KEY`. Database auto-creates on first run via SQLAlchemy `create_all`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# CLAUDE.md — 12-rule template
|
||||||
|
|
||||||
|
These rules apply to every task in this project unless explicitly overridden.
|
||||||
|
Bias: caution over speed on non-trivial work. Use judgment on trivial tasks.
|
||||||
|
|
||||||
|
## Rule 1 — Think Before Coding
|
||||||
|
State assumptions explicitly. If uncertain, ask rather than guess.
|
||||||
|
Present multiple interpretations when ambiguity exists.
|
||||||
|
Push back when a simpler approach exists.
|
||||||
|
Stop when confused. Name what's unclear.
|
||||||
|
|
||||||
|
## Rule 2 — Simplicity First
|
||||||
|
Minimum code that solves the problem. Nothing speculative.
|
||||||
|
No features beyond what was asked. No abstractions for single-use code.
|
||||||
|
Test: would a senior engineer say this is overcomplicated? If yes, simplify.
|
||||||
|
|
||||||
|
## Rule 3 — Surgical Changes
|
||||||
|
Touch only what you must. Clean up only your own mess.
|
||||||
|
Don't "improve" adjacent code, comments, or formatting.
|
||||||
|
Don't refactor what isn't broken. Match existing style.
|
||||||
|
|
||||||
|
## Rule 4 — Goal-Driven Execution
|
||||||
|
Define success criteria. Loop until verified.
|
||||||
|
Don't follow steps. Define success and iterate.
|
||||||
|
Strong success criteria let you loop independently.
|
||||||
|
|
||||||
|
## Rule 5 — Use the model only for judgment calls
|
||||||
|
Use me for: classification, drafting, summarization, extraction.
|
||||||
|
Do NOT use me for: routing, retries, deterministic transforms.
|
||||||
|
If code can answer, code answers.
|
||||||
|
|
||||||
|
## Rule 6 — Token budgets are not advisory
|
||||||
|
Per-task: 4,000 tokens. Per-session: 30,000 tokens.
|
||||||
|
If approaching budget, summarize and start fresh.
|
||||||
|
Surface the breach. Do not silently overrun.
|
||||||
|
|
||||||
|
## Rule 7 — Surface conflicts, don't average them
|
||||||
|
If two patterns contradict, pick one (more recent / more tested).
|
||||||
|
Explain why. Flag the other for cleanup.
|
||||||
|
Don't blend conflicting patterns.
|
||||||
|
|
||||||
|
## Rule 8 — Read before you write
|
||||||
|
Before adding code, read exports, immediate callers, shared utilities.
|
||||||
|
"Looks orthogonal" is dangerous. If unsure why code is structured a way, ask.
|
||||||
|
|
||||||
|
## Rule 9 — Tests verify intent, not just behavior
|
||||||
|
Tests must encode WHY behavior matters, not just WHAT it does.
|
||||||
|
A test that can't fail when business logic changes is wrong.
|
||||||
|
|
||||||
|
## Rule 10 — Checkpoint after every significant step
|
||||||
|
Summarize what was done, what's verified, what's left.
|
||||||
|
Don't continue from a state you can't describe back.
|
||||||
|
If you lose track, stop and restate.
|
||||||
|
|
||||||
|
## Rule 11 — Match the codebase's conventions, even if you disagree
|
||||||
|
Conformance > taste inside the codebase.
|
||||||
|
If you genuinely think a convention is harmful, surface it. Don't fork silently.
|
||||||
|
|
||||||
|
## Rule 12 — Fail loud
|
||||||
|
"Completed" is wrong if anything was skipped silently.
|
||||||
|
"Tests pass" is wrong if any were skipped.
|
||||||
|
Default to surfacing uncertainty, not hiding it.
|
||||||
@@ -277,5 +277,5 @@ function navigateToProject(projectId: number) {
|
|||||||
|
|
||||||
import { useAvatarUrl } from '@/composables/useAvatarUrl'
|
import { useAvatarUrl } from '@/composables/useAvatarUrl'
|
||||||
|
|
||||||
const { getAvatarUrl } = useAvatarUrl()
|
const { getAvatarUrl, getInitialsAvatarUrl } = useAvatarUrl()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -37,6 +37,9 @@
|
|||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
|
|
||||||
|
<!-- View Settings Section - Only show on shot/project pages -->
|
||||||
|
<SidebarColumnSwitch v-if="userRole !== 'developer' && isOnShotPage" />
|
||||||
|
|
||||||
<!-- Projects Section -->
|
<!-- Projects Section -->
|
||||||
<!-- <SidebarGroup v-if="userRole !== 'developer'">
|
<!-- <SidebarGroup v-if="userRole !== 'developer'">
|
||||||
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
||||||
@@ -91,6 +94,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -122,14 +126,21 @@ import {
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import ProjectSwitcher from './ProjectSwitcher.vue'
|
import ProjectSwitcher from './ProjectSwitcher.vue'
|
||||||
import UserMenu from './UserMenu.vue'
|
import UserMenu from './UserMenu.vue'
|
||||||
|
import SidebarColumnSwitch from '@/components/ui/sidebar/SidebarColumnSwitch.vue'
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const { state } = useSidebar()
|
const { state } = useSidebar()
|
||||||
|
const route = useRoute()
|
||||||
const user = computed(() => authStore.user)
|
const user = computed(() => authStore.user)
|
||||||
const userRole = computed(() => authStore.user?.role || 'artist')
|
const userRole = computed(() => authStore.user?.role || 'artist')
|
||||||
|
|
||||||
// Check if sidebar is collapsed
|
// Check if sidebar is collapsed
|
||||||
const isCollapsed = computed(() => state.value === 'collapsed')
|
const isCollapsed = computed(() => state.value === 'collapsed')
|
||||||
|
|
||||||
|
// Check if on shot page - show column switch only on shot pages
|
||||||
|
const isOnShotPage = computed(() => {
|
||||||
|
return route.path.includes('/shots') || route.path.includes('/project/')
|
||||||
|
})
|
||||||
|
|
||||||
const props = withDefaults(defineProps<SidebarProps>(), {
|
const props = withDefaults(defineProps<SidebarProps>(), {
|
||||||
collapsible: "icon",
|
collapsible: "icon",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ import {
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
import { User, UserX, Search, Check, X } from 'lucide-vue-next'
|
import { User, Search, Check, X } from 'lucide-vue-next'
|
||||||
import TaskStatusBadge from '@/components/task/TaskStatusBadge.vue'
|
import TaskStatusBadge from '@/components/task/TaskStatusBadge.vue'
|
||||||
import { TaskStatus } from '@/services/shot'
|
import { TaskStatus } from '@/services/shot'
|
||||||
import { taskService } from '@/services/task'
|
import { taskService } from '@/services/task'
|
||||||
|
|||||||
@@ -166,7 +166,7 @@
|
|||||||
leave-to-class="translate-x-full"
|
leave-to-class="translate-x-full"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="showPanel"
|
v-if="showPanel && selectedShot"
|
||||||
class="fixed right-0 top-[113px] bottom-0 w-96 bg-background border-l shadow-lg z-50 hidden lg:block overflow-y-auto"
|
class="fixed right-0 top-[113px] bottom-0 w-96 bg-background border-l shadow-lg z-50 hidden lg:block overflow-y-auto"
|
||||||
>
|
>
|
||||||
<ShotDetailPanel
|
<ShotDetailPanel
|
||||||
@@ -277,13 +277,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, shallowRef, markRaw, nextTick } from 'vue'
|
import { ref, computed, watch, shallowRef, markRaw, nextTick, onMounted } from 'vue'
|
||||||
import {
|
import {
|
||||||
Search, Plus, Camera, AlertCircle, RefreshCw,
|
Search, Plus, Camera, AlertCircle, RefreshCw,
|
||||||
Layers, MoreHorizontal, Edit, ListTodo, Trash2
|
Layers, MoreHorizontal, Edit, ListTodo, Trash2
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { useColumnVisibilityStore } from '@/stores/columnVisibility'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -378,6 +379,7 @@ const validationError = ref<ValidationErrorDisplay | null>(null)
|
|||||||
|
|
||||||
// TanStack Table state
|
// TanStack Table state
|
||||||
const sorting = ref<SortingState>([])
|
const sorting = ref<SortingState>([])
|
||||||
|
const columnVisibilityStore = useColumnVisibilityStore()
|
||||||
const columnVisibility = ref<VisibilityState>({})
|
const columnVisibility = ref<VisibilityState>({})
|
||||||
const rowSelection = ref<Record<string, boolean>>({})
|
const rowSelection = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
@@ -385,16 +387,12 @@ const rowSelection = ref<Record<string, boolean>>({})
|
|||||||
const selectedCount = computed(() => {
|
const selectedCount = computed(() => {
|
||||||
return Object.keys(rowSelection.value).length
|
return Object.keys(rowSelection.value).length
|
||||||
})
|
})
|
||||||
|
|
||||||
const initializeColumnVisibility = () => {
|
const initializeColumnVisibility = () => {
|
||||||
const stored = sessionStorage.getItem('shotBrowser.columnVisibility')
|
// Initialize the global store
|
||||||
if (stored) {
|
columnVisibilityStore.initialize()
|
||||||
try {
|
// Use store's visibility as the initial value
|
||||||
columnVisibility.value = JSON.parse(stored)
|
columnVisibility.value = columnVisibilityStore.visibility
|
||||||
} catch {
|
|
||||||
// Fall back to defaults
|
|
||||||
columnVisibility.value = {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeColumnVisibility()
|
initializeColumnVisibility()
|
||||||
@@ -538,7 +536,10 @@ let visibilityUpdateTimeout: ReturnType<typeof setTimeout> | null = null
|
|||||||
const handleColumnVisibilityChange = (visibility: VisibilityState) => {
|
const handleColumnVisibilityChange = (visibility: VisibilityState) => {
|
||||||
columnVisibility.value = visibility
|
columnVisibility.value = visibility
|
||||||
|
|
||||||
// Debounce session storage updates
|
// Also update the global store
|
||||||
|
columnVisibilityStore.setColumnVisibility(visibility)
|
||||||
|
|
||||||
|
// Debounce session storage updates (for backwards compatibility)
|
||||||
if (visibilityUpdateTimeout) clearTimeout(visibilityUpdateTimeout)
|
if (visibilityUpdateTimeout) clearTimeout(visibilityUpdateTimeout)
|
||||||
visibilityUpdateTimeout = setTimeout(() => {
|
visibilityUpdateTimeout = setTimeout(() => {
|
||||||
sessionStorage.setItem('shotBrowser.columnVisibility', JSON.stringify(visibility))
|
sessionStorage.setItem('shotBrowser.columnVisibility', JSON.stringify(visibility))
|
||||||
@@ -588,7 +589,7 @@ const handleTaskAssignmentUpdated = (shotId: number, taskType: string, userId: n
|
|||||||
if (shot && shot.task_details) {
|
if (shot && shot.task_details) {
|
||||||
const taskDetail = shot.task_details.find(detail => detail.task_type === taskType)
|
const taskDetail = shot.task_details.find(detail => detail.task_type === taskType)
|
||||||
if (taskDetail) {
|
if (taskDetail) {
|
||||||
taskDetail.assigned_user_id = userId
|
taskDetail.assigned_user_id = userId ?? undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -912,4 +913,12 @@ watch(() => showEditDialog.value, (isOpen) => {
|
|||||||
|
|
||||||
// Watch for changes that require column recreation (after all functions are defined)
|
// Watch for changes that require column recreation (after all functions are defined)
|
||||||
watch([() => allTaskTypes.value, () => episodes.value, () => props.projectId], updateColumns, { immediate: true })
|
watch([() => allTaskTypes.value, () => episodes.value, () => props.projectId], updateColumns, { immediate: true })
|
||||||
|
|
||||||
|
// Watch for changes from global column visibility store
|
||||||
|
watch(() => columnVisibilityStore.visibility, (newVisibility) => {
|
||||||
|
// Only update if the change didn't come from this component
|
||||||
|
if (JSON.stringify(newVisibility) !== JSON.stringify(columnVisibility.value)) {
|
||||||
|
columnVisibility.value = newVisibility
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
</script>
|
</script>
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import { Columns } from 'lucide-vue-next'
|
import { Columns } from 'lucide-vue-next'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -95,9 +95,9 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import type { VisibilityState } from '@tanstack/vue-table'
|
import type { VisibilityState } from '@tanstack/vue-table'
|
||||||
|
import { useColumnVisibilityStore } from '@/stores/columnVisibility'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
columnVisibility: VisibilityState
|
|
||||||
allTaskTypes: string[]
|
allTaskTypes: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +107,26 @@ const emit = defineEmits<{
|
|||||||
'update:columnVisibility': [visibility: VisibilityState]
|
'update:columnVisibility': [visibility: VisibilityState]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// Use global store
|
||||||
|
const columnVisibilityStore = useColumnVisibilityStore()
|
||||||
|
|
||||||
|
// Local reactive state for checkbox visibility
|
||||||
|
const localVisibility = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
// Watch for store changes and update local state
|
||||||
|
watch(
|
||||||
|
() => columnVisibilityStore.columnVisibility,
|
||||||
|
(newVal) => {
|
||||||
|
localVisibility.value = { ...newVal }
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize store on mount
|
||||||
|
onMounted(() => {
|
||||||
|
columnVisibilityStore.initialize()
|
||||||
|
})
|
||||||
|
|
||||||
const selectedColumn = ref('toggle')
|
const selectedColumn = ref('toggle')
|
||||||
|
|
||||||
const handleColumnToggle = (value: string) => {
|
const handleColumnToggle = (value: string) => {
|
||||||
@@ -114,9 +134,9 @@ const handleColumnToggle = (value: string) => {
|
|||||||
selectedColumn.value = 'toggle'
|
selectedColumn.value = 'toggle'
|
||||||
}
|
}
|
||||||
|
|
||||||
const isColumnVisible = (columnId: string) => {
|
const isColumnVisible = (columnId: string): boolean => {
|
||||||
// If not in visibility state, column is visible by default
|
// If not in visibility state, column is visible by default
|
||||||
return props.columnVisibility[columnId] !== false
|
return localVisibility.value[columnId] !== false
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCheckboxChange = (column: string, event: Event) => {
|
const handleCheckboxChange = (column: string, event: Event) => {
|
||||||
@@ -129,9 +149,8 @@ const toggleColumn = (column: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateColumn = (column: string, checked: boolean) => {
|
const updateColumn = (column: string, checked: boolean) => {
|
||||||
const newVisibility = { ...props.columnVisibility }
|
// Always use global store
|
||||||
newVisibility[column] = checked
|
columnVisibilityStore.updateColumn(column, checked)
|
||||||
emit('update:columnVisibility', newVisibility)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatTaskType = (taskType: string) => {
|
const formatTaskType = (taskType: string) => {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
'selecting': isRangeSelecting
|
'selecting': isRangeSelecting
|
||||||
}"
|
}"
|
||||||
@click="handleRowClick(row.original, $event, row)"
|
@click="handleRowClick(row.original, $event, row)"
|
||||||
|
@dblclick="emit('row-dblclick', row.original)"
|
||||||
@mousedown="handleMouseDown"
|
@mousedown="handleMouseDown"
|
||||||
@mouseup="handleMouseUp"
|
@mouseup="handleMouseUp"
|
||||||
>
|
>
|
||||||
@@ -103,6 +104,7 @@ const emit = defineEmits<{
|
|||||||
'update:columnVisibility': [visibility: VisibilityState]
|
'update:columnVisibility': [visibility: VisibilityState]
|
||||||
'update:rowSelection': [selection: Record<string, boolean>]
|
'update:rowSelection': [selection: Record<string, boolean>]
|
||||||
'row-click': [shot: Shot, event: MouseEvent]
|
'row-click': [shot: Shot, event: MouseEvent]
|
||||||
|
'row-dblclick': [shot: Shot]
|
||||||
'selection-cleared': []
|
'selection-cleared': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<template>
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarGroupLabel>View</SidebarGroupLabel>
|
||||||
|
<SidebarGroup class="py-0" as-child>
|
||||||
|
<Collapsible v-model:open="isBaseColumnsOpen" class="group/collapsible">
|
||||||
|
<CollapsibleTrigger as-child>
|
||||||
|
<SidebarGroupLabel class="px-2 hover:bg-sidebar-accent text-sm hover:text-sidebar-accent-foreground cursor-pointer">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
Columns
|
||||||
|
<ChevronRight
|
||||||
|
class="h-4 w-4 transition-transform duration-200"
|
||||||
|
:class="isBaseColumnsOpen ? 'rotate-90' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SidebarGroupLabel>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<Command class="bg-transparent border-0 p-0">
|
||||||
|
<CommandList>
|
||||||
|
<CommandGroup>
|
||||||
|
<template v-for="column in baseColumns" :key="column.id">
|
||||||
|
<CommandItem
|
||||||
|
:value="column.id"
|
||||||
|
@select="toggleColumn(column.id, !isColumnVisible(column.id))"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||||
|
isColumnVisible(column.id)
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'opacity-50 [&_svg]:invisible'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<Check class="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<span class="text-sm text-sidebar-foreground">{{ column.label }}</span>
|
||||||
|
</CommandItem>
|
||||||
|
</template>
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</SidebarGroup>
|
||||||
|
<SidebarGroup class="py-0">
|
||||||
|
<Collapsible v-model:open="isTaskColumnsOpen" class="group/collapsible">
|
||||||
|
<CollapsibleTrigger as-child>
|
||||||
|
<SidebarGroupLabel class="px-2 hover:bg-sidebar-accent text-sm hover:text-sidebar-accent-foreground cursor-pointer">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
Task Columns
|
||||||
|
<ChevronRight
|
||||||
|
class="h-4 w-4 transition-transform duration-200"
|
||||||
|
:class="isTaskColumnsOpen ? 'rotate-90' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SidebarGroupLabel>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<Command class="bg-transparent border-0 p-0">
|
||||||
|
<CommandList>
|
||||||
|
<CommandGroup>
|
||||||
|
<template v-for="column in taskColumns" :key="column.id">
|
||||||
|
<CommandItem
|
||||||
|
:value="column.id"
|
||||||
|
@select="toggleColumn(column.id, !isColumnVisible(column.id))"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||||
|
isColumnVisible(column.id)
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'opacity-50 [&_svg]:invisible'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<Check class="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<span class="text-sm text-sidebar-foreground">{{ column.label }}</span>
|
||||||
|
</CommandItem>
|
||||||
|
</template>
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</SidebarGroup>
|
||||||
|
</SidebarGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { Check, ChevronDown, ChevronRight } from 'lucide-vue-next'
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
CommandSeparator,
|
||||||
|
} from '@/components/ui/command'
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@/components/ui/collapsible'
|
||||||
|
import {
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
} from '@/components/ui/sidebar'
|
||||||
|
import { useColumnVisibilityStore } from '@/stores/columnVisibility'
|
||||||
|
import { customTaskTypeService } from '@/services/customTaskType'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const columnVisibilityStore = useColumnVisibilityStore()
|
||||||
|
|
||||||
|
// Collapsible state
|
||||||
|
const isBaseColumnsOpen = ref(true)
|
||||||
|
const isTaskColumnsOpen = ref(true)
|
||||||
|
|
||||||
|
// Task types state
|
||||||
|
const taskTypes = ref<string[]>([])
|
||||||
|
|
||||||
|
// Base columns - synced with Shot Data Table default columns
|
||||||
|
const baseColumns = [
|
||||||
|
{ id: 'thumbnail', label: 'Thumbnail' },
|
||||||
|
{ id: 'name', label: 'Shot Name' },
|
||||||
|
{ id: 'episode', label: 'Episode' },
|
||||||
|
{ id: 'frames', label: 'Frames' },
|
||||||
|
{ id: 'status', label: 'Status' },
|
||||||
|
]
|
||||||
|
const taskColumns = computed(() => {
|
||||||
|
const columns = taskTypes.value.map(taskType => ({
|
||||||
|
id: taskType,
|
||||||
|
label: taskType.charAt(0).toUpperCase() + taskType.slice(1),
|
||||||
|
}))
|
||||||
|
return [...columns]
|
||||||
|
})
|
||||||
|
// All columns including task types
|
||||||
|
const allColumns = computed(() => {
|
||||||
|
const taskColumns = taskTypes.value.map(taskType => ({
|
||||||
|
id: taskType,
|
||||||
|
label: taskType.charAt(0).toUpperCase() + taskType.slice(1),
|
||||||
|
}))
|
||||||
|
return [...baseColumns, ...taskColumns]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Local reactive state
|
||||||
|
const localVisibility = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
// Watch for store changes and update local state
|
||||||
|
watch(
|
||||||
|
() => columnVisibilityStore.columnVisibility,
|
||||||
|
(newVal) => {
|
||||||
|
localVisibility.value = { ...newVal }
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const isColumnVisible = (columnId: string): boolean => {
|
||||||
|
// If not in visibility state, column is visible by default
|
||||||
|
return localVisibility.value[columnId] !== false
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleColumn = (columnId: string, value: boolean) => {
|
||||||
|
columnVisibilityStore.updateColumn(columnId, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch task types based on project ID from route
|
||||||
|
const fetchTaskTypes = async () => {
|
||||||
|
// Extract projectId from route params
|
||||||
|
const projectId = route.params.projectId as string | undefined
|
||||||
|
|
||||||
|
// Clear task types if no projectId
|
||||||
|
if (!projectId) {
|
||||||
|
taskTypes.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const projectIdNum = parseInt(projectId, 10)
|
||||||
|
if (!isNaN(projectIdNum)) {
|
||||||
|
const data = await customTaskTypeService.getAllTaskTypes(projectIdNum)
|
||||||
|
taskTypes.value = data.shot_task_types || []
|
||||||
|
} else {
|
||||||
|
taskTypes.value = []
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch task types:', err)
|
||||||
|
taskTypes.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle - initialize store and fetch task types
|
||||||
|
onMounted(() => {
|
||||||
|
columnVisibilityStore.initialize()
|
||||||
|
fetchTaskTypes()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for route changes to refetch task types
|
||||||
|
watch(() => route.params.projectId, () => {
|
||||||
|
fetchTaskTypes()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -60,6 +60,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/project/ProjectShotsView.vue'),
|
component: () => import('@/views/project/ProjectShotsView.vue'),
|
||||||
meta: { tab: 'shots', tabLabel: 'Shots', hasEpisode: true }
|
meta: { tab: 'shots', tabLabel: 'Shots', hasEpisode: true }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'shots/:shotId',
|
||||||
|
name: 'ShotDetail',
|
||||||
|
component: () => import('@/views/project/ShotDetailView.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'assets',
|
path: 'assets',
|
||||||
name: 'ProjectAssets',
|
name: 'ProjectAssets',
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import type { VisibilityState } from '@tanstack/vue-table'
|
||||||
|
|
||||||
|
// Default column visibility - basic columns visible, task type columns hidden by default
|
||||||
|
const DEFAULT_VISIBILITY: VisibilityState = {
|
||||||
|
thumbnail: true,
|
||||||
|
name: true,
|
||||||
|
episode: true,
|
||||||
|
frames: true,
|
||||||
|
status: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'shot-column-visibility'
|
||||||
|
|
||||||
|
export const useColumnVisibilityStore = defineStore('columnVisibility', () => {
|
||||||
|
// State
|
||||||
|
const columnVisibility = ref<VisibilityState>({})
|
||||||
|
let isInitialized = false
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
const visibility = computed(() => {
|
||||||
|
// Auto-initialize if empty
|
||||||
|
if (Object.keys(columnVisibility.value).length === 0) {
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
return columnVisibility.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const isColumnVisible = (columnId: string): boolean => {
|
||||||
|
// Auto-initialize if empty
|
||||||
|
if (Object.keys(columnVisibility.value).length === 0) {
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
// If not in visibility state, column is visible by default
|
||||||
|
return columnVisibility.value[columnId] !== false
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleColumns = computed(() => {
|
||||||
|
return Object.entries(columnVisibility.value)
|
||||||
|
.filter(([, visible]) => visible !== false)
|
||||||
|
.map(([column]) => column)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const initialize = () => {
|
||||||
|
if (isInitialized) return
|
||||||
|
|
||||||
|
// Get saved visibility from localStorage
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(saved)
|
||||||
|
// Merge with defaults to ensure all columns have a state
|
||||||
|
columnVisibility.value = { ...DEFAULT_VISIBILITY, ...parsed }
|
||||||
|
} catch {
|
||||||
|
// If parsing fails, use defaults
|
||||||
|
columnVisibility.value = { ...DEFAULT_VISIBILITY }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No saved state, use defaults
|
||||||
|
columnVisibility.value = { ...DEFAULT_VISIBILITY }
|
||||||
|
}
|
||||||
|
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleColumn = (columnId: string) => {
|
||||||
|
const newVisibility = !isColumnVisible(columnId)
|
||||||
|
updateColumn(columnId, newVisibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateColumn = (columnId: string, visible: boolean) => {
|
||||||
|
columnVisibility.value = {
|
||||||
|
...columnVisibility.value,
|
||||||
|
[columnId]: visible,
|
||||||
|
}
|
||||||
|
saveToStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const setColumnVisibility = (visibility: VisibilityState) => {
|
||||||
|
columnVisibility.value = { ...DEFAULT_VISIBILITY, ...visibility }
|
||||||
|
saveToStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetToDefaults = () => {
|
||||||
|
columnVisibility.value = { ...DEFAULT_VISIBILITY }
|
||||||
|
saveToStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveToStorage = () => {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(columnVisibility.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
columnVisibility,
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
visibility,
|
||||||
|
isColumnVisible,
|
||||||
|
visibleColumns,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
initialize,
|
||||||
|
toggleColumn,
|
||||||
|
updateColumn,
|
||||||
|
setColumnVisibility,
|
||||||
|
resetToDefaults,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -109,3 +109,25 @@
|
|||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.scrollbar::-webkit-scrollbar {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar::-webkit-scrollbar-track {
|
||||||
|
border-radius: 100vh;
|
||||||
|
background: #f7f4ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: #e0cbcb;
|
||||||
|
border-radius: 100vh;
|
||||||
|
border: 3px solid #f6f7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #c0a0b9;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ export interface UserInfo {
|
|||||||
first_name: string
|
first_name: string
|
||||||
last_name: string
|
last_name: string
|
||||||
email: string
|
email: string
|
||||||
|
avatar_url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Activity {
|
export interface Activity {
|
||||||
|
|||||||
Vendored
+6
-2
@@ -1,6 +1,10 @@
|
|||||||
// src/vue-shims.d.ts
|
// src/vue-shims.d.ts
|
||||||
|
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
|
||||||
|
// Declare default export for all .vue files
|
||||||
|
// This is needed for TypeScript to understand Vue 3 <script setup> components
|
||||||
declare module "*.vue" {
|
declare module "*.vue" {
|
||||||
import Vue from "vue";
|
const component: DefineComponent<{}, {}, any>
|
||||||
export default Vue;
|
export default component
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-03-05
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
The VFX Studio application currently has a ShotColumnVisibilityControl component in the Shot page toolbar that allows users to toggle which columns to display in the Shot data table. However, this control is only accessible when users are on the Shot page. Users have requested the ability to quickly toggle Shot table columns from the Sidebar so they can adjust column visibility without navigating to the Shot page first.
|
||||||
|
|
||||||
|
This change will add a column display switch to the application Sidebar, making column visibility controls accessible from anywhere in the application.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- Add a column visibility control to the AppSidebar component
|
||||||
|
- Create a global column visibility state accessible throughout the application
|
||||||
|
- Ensure column preferences persist across user sessions using localStorage
|
||||||
|
- Maintain consistency with the existing ShotColumnVisibilityControl component
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- Backend API changes (not required)
|
||||||
|
- Database schema changes (not required)
|
||||||
|
- Modifying the existing Shot page column visibility functionality
|
||||||
|
- Adding column visibility controls for other data tables (e.g., Tasks, Assets)
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 1. Global State Management Approach
|
||||||
|
**Decision:** Use a Pinia store for global column visibility state with localStorage persistence.
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- Pinia is already the state management solution used in the project
|
||||||
|
- localStorage provides simple persistence without backend changes
|
||||||
|
- This approach aligns with existing patterns in the codebase
|
||||||
|
|
||||||
|
**Alternative Considered:** Use Vue's provide/inject pattern
|
||||||
|
- Rejected because it doesn't provide automatic persistence and is less scalable
|
||||||
|
|
||||||
|
### 2. Sidebar Integration Location
|
||||||
|
**Decision:** Add the column display switch as a new SidebarGroup in the AppSidebar component, positioned in the SidebarContent area.
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- Keeps the control accessible but not obtrusive
|
||||||
|
- Follows the existing Sidebar organization pattern (Navigation, Administration, Developer Tools)
|
||||||
|
- Allows for future expansion to other column visibility controls
|
||||||
|
|
||||||
|
**Alternative Considered:** Add to SidebarFooter
|
||||||
|
- Rejected because Footer is already used for UserMenu and would become cluttered
|
||||||
|
|
||||||
|
### 3. Reuse Existing Component vs. New Implementation
|
||||||
|
**Decision:** Create a new Sidebar-specific component that adapts the existing ShotColumnVisibilityControl logic.
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- The existing ShotColumnVisibilityControl is tightly coupled to the Shot page context
|
||||||
|
- A sidebar-specific component can be simpler and more focused
|
||||||
|
- Allows for different UI presentation in sidebarmore compact)
|
||||||
|
|
||||||
|
** context (Alternative Considered:** Reuse ShotColumnVisibilityControl directly
|
||||||
|
- Rejected because it has Shot-specific UI elements (toolbar styling, select dropdown)
|
||||||
|
|
||||||
|
### 4. Default Column Visibility
|
||||||
|
**Decision:** Default visibility: Thumbnail, Shot Name, Episode, Status visible; all task type columns hidden.
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- Matches typical VFX production workflow priorities
|
||||||
|
- Reduces visual clutter for new users
|
||||||
|
- Aligns with the current Shot page defaults
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
**[Risk]** State synchronization between Sidebar control and Shot page control
|
||||||
|
→ **Mitigation:** Both controls will read from and write to the same Pinia store, ensuring consistency
|
||||||
|
|
||||||
|
**[Risk]** Performance impact from reactive column visibility state
|
||||||
|
→ **Mitigation:** Column visibility is a simple object; Vue's reactivity system handles this efficiently
|
||||||
|
|
||||||
|
**[Risk]** User confusion if Sidebar is collapsed
|
||||||
|
→ **Mitigation:** The column control will be hidden when sidebar is collapsed (consistent with other sidebar content)
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. Create new Pinia store for column visibility (e.g., `useColumnVisibilityStore`)
|
||||||
|
2. Create new SidebarColumnSwitch component
|
||||||
|
3. Add SidebarColumnSwitch to AppSidebar.vue
|
||||||
|
4. Update ShotColumnVisibilityControl to use the new store
|
||||||
|
5. Test state synchronization between Sidebar and Shot page
|
||||||
|
6. No rollback needed - this is a purely additive feature
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. Should the column visibility settings be per-project or global across all projects?
|
||||||
|
- Current design assumes global (simpler implementation)
|
||||||
|
- Could be enhanced later if users request per-project settings
|
||||||
|
|
||||||
|
2. Should we show all task type columns or only a subset in the Sidebar?
|
||||||
|
- Current design shows all available task types
|
||||||
|
- Could be limited to top 5 most-used types if the list becomes too long
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
The Shot page already has a column visibility control component (`ShotColumnVisibilityControl.vue`) that allows users to toggle which columns to display in the Shot data table. However, users need to navigate to the Shot page to access this functionality. Adding a column display switch directly to the Sidebar will improve user experience by providing quick access to toggle Shot table columns from anywhere in the application, without needing to first navigate to the Shot page.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Add a new Sidebar menu item or control section in the Sidebar for Shot column visibility
|
||||||
|
- Create a global column visibility state that can be accessed from the Sidebar
|
||||||
|
- Integrate the existing column toggle logic into the Sidebar component
|
||||||
|
- Persist user column preferences globally (so they apply across all views)
|
||||||
|
- Add appropriate UI components (switch/checkbox) in the Sidebar for quick column toggling
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- `shot-column-display-switch`: Add a Sidebar control for toggling Shot data table column visibility globally
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- None. This is a new feature that doesn't change existing requirements.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Frontend: New Sidebar component or modification to AppSidebar.vue
|
||||||
|
- State Management: May need to add a Pinia store or extend existing store for global column visibility state
|
||||||
|
- No API changes required
|
||||||
|
- Existing ShotColumnVisibilityControl component can be reused or adapted
|
||||||
|
- Minimal impact on existing functionality
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Sidebar Shot Column Display Switch
|
||||||
|
The system SHALL provide a column visibility control in the Sidebar that allows users to toggle which columns are displayed in the Shot data table from any page in the application.
|
||||||
|
|
||||||
|
#### Scenario: User toggles column from Sidebar
|
||||||
|
- **WHEN** user interacts with the column display switch in the Sidebar
|
||||||
|
- **THEN** the Shot data table column visibility is updated globally across all views
|
||||||
|
|
||||||
|
#### Scenario: User views available column options
|
||||||
|
- **WHEN** user expands the column display control in the Sidebar
|
||||||
|
- **THEN** the system displays all available Shot table columns with their current visibility state
|
||||||
|
|
||||||
|
#### Scenario: Column preferences persist across sessions
|
||||||
|
- **WHEN** user toggles a column visibility setting
|
||||||
|
- **THEN** the preference SHALL be saved and restored when the user returns to the application
|
||||||
|
|
||||||
|
### Requirement: Global Column Visibility State
|
||||||
|
The system SHALL maintain a global column visibility state that is accessible from both the Sidebar and the Shot page column visibility control.
|
||||||
|
|
||||||
|
#### Scenario: Sidebar control reflects current visibility state
|
||||||
|
- **WHEN** user opens the Sidebar column control
|
||||||
|
- **THEN** the checkboxes SHALL reflect the current column visibility state from the global state
|
||||||
|
|
||||||
|
#### Scenario: Changes in Sidebar reflect in Shot page
|
||||||
|
- **WHEN** user toggles a column from the Sidebar
|
||||||
|
- **THEN** the Shot page column visibility control SHALL show the updated state
|
||||||
|
|
||||||
|
### Requirement: Default Column Visibility
|
||||||
|
The system SHALL provide sensible default column visibility settings that apply when no user preference has been saved.
|
||||||
|
|
||||||
|
#### Scenario: New user views Shot table
|
||||||
|
- **WHEN** a new user (no saved preferences) views the Shot data table
|
||||||
|
- **THEN** default columns (Thumbnail, Shot Name, Episode, Status) SHALL be visible
|
||||||
|
- **AND** task type columns SHALL be hidden by default
|
||||||
|
|
||||||
|
### Requirement: Column Types Supported
|
||||||
|
The system SHALL support toggling visibility for the following column types:
|
||||||
|
- Thumbnail
|
||||||
|
- Shot Name
|
||||||
|
- Episode
|
||||||
|
- Status
|
||||||
|
- Task type columns (dynamic based on project task types)
|
||||||
|
|
||||||
|
#### Scenario: User toggles thumbnail column
|
||||||
|
- **WHEN** user toggles the Thumbnail column visibility
|
||||||
|
- **THEN** the thumbnail column SHALL show/hide in the Shot data table
|
||||||
|
|
||||||
|
#### Scenario: User toggles task type column
|
||||||
|
- **WHEN** user toggles a task type column (e.g., "Animation", "Compositing")
|
||||||
|
- **THEN** that task status column SHALL show/hide in the Shot data table
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
## 1. Create Column Visibility Store
|
||||||
|
|
||||||
|
- [x] 1.1 Create Pinia store `useColumnVisibilityStore` in `frontend/src/stores/`
|
||||||
|
- [x] 1.2 Add localStorage persistence for column visibility state
|
||||||
|
- [x] 1.3 Define default column visibility (Thumbnail, Shot Name, Episode, Status visible; task types hidden)
|
||||||
|
- [x] 1.4 Add getter methods for checking column visibility
|
||||||
|
- [x] 1.5 Add action methods for toggling column visibility
|
||||||
|
|
||||||
|
## 2. Create Sidebar Column Switch Component
|
||||||
|
|
||||||
|
- [x] 2.1 Create new component `SidebarColumnSwitch.vue` in `frontend/src/components/ui/sidebar/`
|
||||||
|
- [x] 2.2 Implement column visibility toggle UI (checkboxes or switches)
|
||||||
|
- [x] 2.3 Integrate with `useColumnVisibilityStore`
|
||||||
|
- [x] 2.4 Style component for sidebar context (compact, collapsible)
|
||||||
|
|
||||||
|
## 3. Integrate Component into Sidebar
|
||||||
|
|
||||||
|
- [x] 3.1 Import `SidebarColumnSwitch` into `AppSidebar.vue`
|
||||||
|
- [x] 3.2 Add new `SidebarGroup` for "View Settings" in `AppSidebar.vue`
|
||||||
|
- [x] 3.3 Place `SidebarColumnSwitch` inside the new group
|
||||||
|
- [ ] 3.4 Test visibility when sidebar is collapsed
|
||||||
|
|
||||||
|
## 4. Update Existing Shot Column Control
|
||||||
|
|
||||||
|
- [x] 4.1 Modify `ShotColumnVisibilityControl.vue` to use `useColumnVisibilityStore`
|
||||||
|
- [x] 4.2 Remove local state management from Shot page column control
|
||||||
|
- [x] 4.3 Ensure changes in Shot page reflect in Sidebar and vice versa
|
||||||
|
|
||||||
|
## 5. Testing and Verification
|
||||||
|
|
||||||
|
- [ ] 5.1 Test column toggle from Sidebar updates Shot table
|
||||||
|
- [ ] 5.2 Test column toggle from Shot page updates Sidebar control
|
||||||
|
- [ ] 5.3 Test persistence - preferences survive page refresh
|
||||||
|
- [ ] 5.4 Test default visibility for new users
|
||||||
|
- [ ] 5.5 Test behavior when sidebar is collapsed/expanded
|
||||||
|
|
||||||
|
## 6. Edge Cases
|
||||||
|
|
||||||
|
- [ ] 6.1 Handle case when no project is selected
|
||||||
|
- [ ] 6.2 Handle case when task types list is empty
|
||||||
|
- [ ] 6.3 Test responsiveness on mobile (sidebar behavior)
|
||||||
Reference in New Issue
Block a user