diff --git a/CLAUDE.md b/CLAUDE.md index d24514d..f49f1bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,6 +12,8 @@ Зависимости через uv (`uv add`); gnexus-gauth — git-зависимость. - Frontend: Vue 3 + TypeScript + Vite. UI только через gnexus-ui-kit (git-зависимость, НЕ форкать и не копировать стили — кит часто обновляется; использовать публичный API). + Принцип: используем максимум готовых Vue-компонентов кита (Gn*), сырой HTML — + только когда в ките нет подходящего компонента. - Не редактировать файлы зависимостей в node_modules/.venv. - Формат ответов API: /api/* префикс. Конфиг — из окружения (pydantic-settings), .env не коммитим. diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 3ae9db2..9713063 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,14 +1,21 @@ \ No newline at end of file diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 3e50515..10704f2 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -29,6 +29,15 @@ done_at: string | null } +export interface TaskUpdateInput { + title?: string + description?: string + project_id?: number | null + tag_ids?: number[] + priority?: number | null + status?: string +} + export class ApiError extends Error { status: number constructor(status: number, message: string) { @@ -65,7 +74,7 @@ method: 'POST', body: JSON.stringify({ title, description }), }), - updateTask: (id: number, patch: Partial & { tag_ids?: number[] }) => + updateTask: (id: number, patch: TaskUpdateInput) => request(`/api/tasks/${id}`, { method: 'PATCH', body: JSON.stringify(patch) }), approveTask: (id: number) => request(`/api/tasks/${id}/approve`, { method: 'POST' }), deleteTask: (id: number) => request<{ ok: boolean }>(`/api/tasks/${id}`, { method: 'DELETE' }), diff --git a/frontend/src/gnexus-ui-kit.d.ts b/frontend/src/gnexus-ui-kit.d.ts new file mode 100644 index 0000000..730839f --- /dev/null +++ b/frontend/src/gnexus-ui-kit.d.ts @@ -0,0 +1,70 @@ +// gnexus-ui-kit поставляется без TS-деклараций (JS + JSDoc) — объявляем any. +declare module 'gnexus-ui-kit/vue' { + import type { DefineComponent } from 'vue' + + type AnyComponent = DefineComponent, Record, unknown> + + export const GnexusUiVue: any + export const GnAccordion: AnyComponent + export const GnActionCard: AnyComponent + export const GnActionList: AnyComponent + export const GnAlert: AnyComponent + export const GnAvatar: AnyComponent + export const GnBadge: AnyComponent + export const GnButton: AnyComponent + export const GnCard: AnyComponent + export const GnCheckbox: AnyComponent + export const GnChip: AnyComponent + export const GnChipGroup: AnyComponent + export const GnCombobox: AnyComponent + export const GnConfirmDialog: AnyComponent + export const GnDefinitionList: AnyComponent + export const GnDescriptionList: AnyComponent + export const GnDrawer: AnyComponent + export const GnDropdown: AnyComponent + export const GnEmptyState: AnyComponent + export const GnFileUpload: AnyComponent + export const GnHorizontalCard: AnyComponent + export const GnIconButton: AnyComponent + export const GnIdentity: AnyComponent + export const GnInput: AnyComponent + export const GnInputGroup: AnyComponent + export const GnList: AnyComponent + export const GnLoader: AnyComponent + export const GnLoginCard: AnyComponent + export const GnMetricCard: AnyComponent + export const GnModal: AnyComponent + export const GnNavigationShell: AnyComponent + export const GnNavList: AnyComponent + export const GnPageHeader: AnyComponent + export const GnPagination: AnyComponent + export const GnPopover: AnyComponent + export const GnProgress: AnyComponent + export const GnProgressStages: AnyComponent + export const GnRadio: AnyComponent + export const GnRadioGroup: AnyComponent + export const GnRange: AnyComponent + export const GnRepeater: AnyComponent + export const GnRouterTabs: AnyComponent + export const GnSearchField: AnyComponent + export const GnSelect: AnyComponent + export const GnSkeleton: AnyComponent + export const GnSteps: AnyComponent + export const GnStatusCard: AnyComponent + export const GnSwitch: AnyComponent + export const GnTable: AnyComponent + export const GnTabs: AnyComponent + export const GnTextarea: AnyComponent + export const GnTimeline: AnyComponent + export const GnToastProvider: AnyComponent + export const GnTooltip: AnyComponent + export const GnToolbar: AnyComponent + export const GnUsageMeter: AnyComponent + export const GnUserCard: AnyComponent + export function useToast(): { + success: (opts: { title?: string; text?: string }) => void + error: (opts: { title?: string; text?: string }) => void + info: (opts: { title?: string; text?: string }) => void + warning: (opts: { title?: string; text?: string }) => void + } +} \ No newline at end of file diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 279d525..2b1d4e2 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,5 +1,10 @@ import { createApp } from 'vue' import { createRouter, createWebHistory } from 'vue-router' + +import 'gnexus-ui-kit/dist/css/kit.css' +import 'gnexus-ui-kit/dist/assets/fonts/phosphor-icons/src/css/icons.css' +import { GnexusUiVue } from 'gnexus-ui-kit/vue' + import App from './App.vue' import StackView from './views/StackView.vue' import TreeView from './views/TreeView.vue' @@ -13,4 +18,4 @@ ], }) -createApp(App).use(router).mount('#app') \ No newline at end of file +createApp(App).use(router).use(GnexusUiVue).mount('#app') \ No newline at end of file diff --git a/frontend/src/types.ts b/frontend/src/types.ts new file mode 100644 index 0000000..01afc29 --- /dev/null +++ b/frontend/src/types.ts @@ -0,0 +1,34 @@ +export interface Tag { + id: number + name: string +} + +export interface Project { + id: number + name: string + relevance_status: string + priority: number | null + note: string +} + +export interface Task { + id: number + title: string + description: string + task_type: string + status: string + detail_state: string + parent_task_id: number | null + project: Project | null + priority: number | null + tags: Tag[] + created_at: string + approved_at: string | null + done_at: string | null +} + +export interface UserInfo { + user_id: string + email: string + avatar_url: string | null +} \ No newline at end of file diff --git a/frontend/src/views/StackView.vue b/frontend/src/views/StackView.vue index 3730b77..6e8f27f 100644 --- a/frontend/src/views/StackView.vue +++ b/frontend/src/views/StackView.vue @@ -15,8 +15,8 @@ const editForm = ref({ title: '', description: '', - project_id: null as number | null, - tag_ids: [] as number[], + project_id: '' as string | number, + tagSelection: {} as Record, priority: null as number | null, }) @@ -44,13 +44,21 @@ } } +function selectedTagIds(): number[] { + return Object.entries(editForm.value.tagSelection) + .filter(([, on]) => on) + .map(([id]) => Number(id)) +} + function startEdit(task: Task) { editing.value = task + const selection: Record = {} + task.tags.forEach((t) => (selection[t.id] = true)) editForm.value = { title: task.title, description: task.description, - project_id: task.project?.id ?? null, - tag_ids: task.tags.map((t) => t.id), + project_id: task.project?.id ?? '', + tagSelection: selection, priority: task.priority, } } @@ -59,7 +67,13 @@ if (!editing.value) return error.value = '' try { - await api.updateTask(editing.value.id, editForm.value) + await api.updateTask(editing.value.id, { + title: editForm.value.title, + description: editForm.value.description, + project_id: editForm.value.project_id === '' ? null : Number(editForm.value.project_id), + tag_ids: selectedTagIds(), + priority: editForm.value.priority, + }) await api.approveTask(editing.value.id) editing.value = null await loadStack() @@ -106,12 +120,17 @@ try { const t = await api.createTag(name.trim()) tags.value.push(t) - editForm.value.tag_ids.push(t.id) + editForm.value.tagSelection[t.id] = true } catch (e) { error.value = String(e) } } +const projectOptions = () => [ + { value: '', label: '— без проекта —' }, + ...projects.value.map((p) => ({ value: String(p.id), label: p.name })), +] + onMounted(async () => { await loadStack() projects.value = await api.listProjects() @@ -120,88 +139,144 @@