Merge branch 'worktree-agent-a00c5cfa' into Develop
# Conflicts: # .planning/REQUIREMENTS.md # .planning/STATE.md # src/client/components/CatalogSearchOverlay.tsx # src/client/routes/threads/$threadId.tsx
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useFormatters } from "../hooks/useFormatters";
|
||||
import type { CandidateDelta } from "../hooks/useImpactDeltas";
|
||||
import { LucideIcon } from "../lib/iconData";
|
||||
@@ -46,7 +47,7 @@ export function CandidateCard({
|
||||
delta,
|
||||
}: CandidateCardProps) {
|
||||
const { weight, price } = useFormatters();
|
||||
const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel);
|
||||
const navigate = useNavigate();
|
||||
const openConfirmDeleteCandidate = useUIStore(
|
||||
(s) => s.openConfirmDeleteCandidate,
|
||||
);
|
||||
@@ -56,7 +57,12 @@ export function CandidateCard({
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openCandidateEditPanel(id)}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: "/threads/$threadId/candidates/$candidateId",
|
||||
params: { threadId: String(threadId), candidateId: String(id) },
|
||||
})
|
||||
}
|
||||
className="relative w-full text-left bg-white rounded-xl border border-gray-100 hover:border-gray-200 hover:shadow-sm transition-all overflow-hidden group"
|
||||
>
|
||||
{/* Hover-reveal action buttons */}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCreateCandidate, useUpdateCandidate } from "../hooks/useCandidates";
|
||||
import { useThread } from "../hooks/useThreads";
|
||||
import { useUIStore } from "../stores/uiStore";
|
||||
import { CategoryPicker } from "./CategoryPicker";
|
||||
import { ImageUpload } from "./ImageUpload";
|
||||
|
||||
@@ -9,6 +8,7 @@ interface CandidateFormProps {
|
||||
mode: "add" | "edit";
|
||||
threadId: number;
|
||||
candidateId?: number | null;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
interface FormData {
|
||||
@@ -39,11 +39,11 @@ export function CandidateForm({
|
||||
mode,
|
||||
threadId,
|
||||
candidateId,
|
||||
onClose,
|
||||
}: CandidateFormProps) {
|
||||
const { data: thread } = useThread(threadId);
|
||||
const createCandidate = useCreateCandidate(threadId);
|
||||
const updateCandidate = useUpdateCandidate(threadId);
|
||||
const closeCandidatePanel = useUIStore((s) => s.closeCandidatePanel);
|
||||
|
||||
const [form, setForm] = useState<FormData>(INITIAL_FORM);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
@@ -124,13 +124,13 @@ export function CandidateForm({
|
||||
createCandidate.mutate(payload, {
|
||||
onSuccess: () => {
|
||||
setForm(INITIAL_FORM);
|
||||
closeCandidatePanel();
|
||||
onClose?.();
|
||||
},
|
||||
});
|
||||
} else if (candidateId != null) {
|
||||
updateCandidate.mutate(
|
||||
{ candidateId, ...payload },
|
||||
{ onSuccess: () => closeCandidatePanel() },
|
||||
{ onSuccess: () => onClose?.() },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { Reorder } from "framer-motion";
|
||||
import { useRef } from "react";
|
||||
import { useFormatters } from "../hooks/useFormatters";
|
||||
@@ -60,7 +61,7 @@ export function CandidateListItem({
|
||||
}: CandidateListItemProps) {
|
||||
const isDragging = useRef(false);
|
||||
const { weight, price } = useFormatters();
|
||||
const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel);
|
||||
const navigate = useNavigate();
|
||||
const openConfirmDeleteCandidate = useUIStore(
|
||||
(s) => s.openConfirmDeleteCandidate,
|
||||
);
|
||||
@@ -104,7 +105,13 @@ export function CandidateListItem({
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (isDragging.current) return;
|
||||
openCandidateEditPanel(candidate.id);
|
||||
navigate({
|
||||
to: "/threads/$threadId/candidates/$candidateId",
|
||||
params: {
|
||||
threadId: String(candidate.threadId),
|
||||
candidateId: String(candidate.id),
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="flex-1 min-w-0 text-left"
|
||||
>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useFormatters } from "../hooks/useFormatters";
|
||||
import { useDuplicateItem } from "../hooks/useItems";
|
||||
import { LucideIcon } from "../lib/iconData";
|
||||
@@ -36,14 +37,16 @@ export function ItemCard({
|
||||
onClassificationCycle,
|
||||
}: ItemCardProps) {
|
||||
const { weight, price } = useFormatters();
|
||||
const openEditPanel = useUIStore((s) => s.openEditPanel);
|
||||
const navigate = useNavigate();
|
||||
const openExternalLink = useUIStore((s) => s.openExternalLink);
|
||||
const duplicateItem = useDuplicateItem();
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openEditPanel(id)}
|
||||
onClick={() =>
|
||||
navigate({ to: "/items/$itemId", params: { itemId: String(id) } })
|
||||
}
|
||||
className="relative w-full text-left bg-white rounded-xl border border-gray-100 hover:border-gray-200 hover:shadow-sm transition-all overflow-hidden group"
|
||||
>
|
||||
{!onRemove && (
|
||||
@@ -54,7 +57,10 @@ export function ItemCard({
|
||||
e.stopPropagation();
|
||||
duplicateItem.mutate(id, {
|
||||
onSuccess: (newItem) => {
|
||||
openEditPanel(newItem.id);
|
||||
navigate({
|
||||
to: "/items/$itemId",
|
||||
params: { itemId: String(newItem.id) },
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
@@ -63,7 +69,10 @@ export function ItemCard({
|
||||
e.stopPropagation();
|
||||
duplicateItem.mutate(id, {
|
||||
onSuccess: (newItem) => {
|
||||
openEditPanel(newItem.id);
|
||||
navigate({
|
||||
to: "/items/$itemId",
|
||||
params: { itemId: String(newItem.id) },
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ImageUpload } from "./ImageUpload";
|
||||
interface ItemFormProps {
|
||||
mode: "add" | "edit";
|
||||
itemId?: number | null;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
interface FormData {
|
||||
@@ -31,11 +32,10 @@ const INITIAL_FORM: FormData = {
|
||||
imageFilename: null,
|
||||
};
|
||||
|
||||
export function ItemForm({ mode, itemId }: ItemFormProps) {
|
||||
export function ItemForm({ mode, itemId, onClose }: ItemFormProps) {
|
||||
const { data: items } = useItems();
|
||||
const createItem = useCreateItem();
|
||||
const updateItem = useUpdateItem();
|
||||
const closePanel = useUIStore((s) => s.closePanel);
|
||||
const openConfirmDelete = useUIStore((s) => s.openConfirmDelete);
|
||||
|
||||
const [form, setForm] = useState<FormData>(INITIAL_FORM);
|
||||
@@ -112,13 +112,13 @@ export function ItemForm({ mode, itemId }: ItemFormProps) {
|
||||
createItem.mutate(payload, {
|
||||
onSuccess: () => {
|
||||
setForm(INITIAL_FORM);
|
||||
closePanel();
|
||||
onClose?.();
|
||||
},
|
||||
});
|
||||
} else if (itemId != null) {
|
||||
updateItem.mutate(
|
||||
{ id: itemId, ...payload },
|
||||
{ onSuccess: () => closePanel() },
|
||||
{ onSuccess: () => onClose?.() },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as GlobalItemsIndexRouteImport } from './routes/global-items/index'
|
||||
import { Route as CollectionIndexRouteImport } from './routes/collection/index'
|
||||
import { Route as UsersUserIdRouteImport } from './routes/users/$userId'
|
||||
import { Route as ThreadsThreadIdRouteImport } from './routes/threads/$threadId'
|
||||
import { Route as SetupsSetupIdRouteImport } from './routes/setups/$setupId'
|
||||
import { Route as ItemsItemIdRouteImport } from './routes/items/$itemId'
|
||||
import { Route as GlobalItemsGlobalItemIdRouteImport } from './routes/global-items/$globalItemId'
|
||||
@@ -52,11 +51,6 @@ const UsersUserIdRoute = UsersUserIdRouteImport.update({
|
||||
path: '/users/$userId',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ThreadsThreadIdRoute = ThreadsThreadIdRouteImport.update({
|
||||
id: '/threads/$threadId',
|
||||
path: '/threads/$threadId',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const SetupsSetupIdRoute = SetupsSetupIdRouteImport.update({
|
||||
id: '/setups/$setupId',
|
||||
path: '/setups/$setupId',
|
||||
@@ -91,7 +85,6 @@ export interface FileRoutesByFullPath {
|
||||
'/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute
|
||||
'/items/$itemId': typeof ItemsItemIdRoute
|
||||
'/setups/$setupId': typeof SetupsSetupIdRoute
|
||||
'/threads/$threadId': typeof ThreadsThreadIdRouteWithChildren
|
||||
'/users/$userId': typeof UsersUserIdRoute
|
||||
'/collection/': typeof CollectionIndexRoute
|
||||
'/global-items/': typeof GlobalItemsIndexRoute
|
||||
@@ -119,7 +112,6 @@ export interface FileRoutesById {
|
||||
'/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute
|
||||
'/items/$itemId': typeof ItemsItemIdRoute
|
||||
'/setups/$setupId': typeof SetupsSetupIdRoute
|
||||
'/threads/$threadId': typeof ThreadsThreadIdRouteWithChildren
|
||||
'/users/$userId': typeof UsersUserIdRoute
|
||||
'/collection/': typeof CollectionIndexRoute
|
||||
'/global-items/': typeof GlobalItemsIndexRoute
|
||||
@@ -135,7 +127,6 @@ export interface FileRouteTypes {
|
||||
| '/global-items/$globalItemId'
|
||||
| '/items/$itemId'
|
||||
| '/setups/$setupId'
|
||||
| '/threads/$threadId'
|
||||
| '/users/$userId'
|
||||
| '/collection/'
|
||||
| '/global-items/'
|
||||
@@ -162,7 +153,6 @@ export interface FileRouteTypes {
|
||||
| '/global-items/$globalItemId'
|
||||
| '/items/$itemId'
|
||||
| '/setups/$setupId'
|
||||
| '/threads/$threadId'
|
||||
| '/users/$userId'
|
||||
| '/collection/'
|
||||
| '/global-items/'
|
||||
@@ -177,7 +167,6 @@ export interface RootRouteChildren {
|
||||
GlobalItemsGlobalItemIdRoute: typeof GlobalItemsGlobalItemIdRoute
|
||||
ItemsItemIdRoute: typeof ItemsItemIdRoute
|
||||
SetupsSetupIdRoute: typeof SetupsSetupIdRoute
|
||||
ThreadsThreadIdRoute: typeof ThreadsThreadIdRouteWithChildren
|
||||
UsersUserIdRoute: typeof UsersUserIdRoute
|
||||
CollectionIndexRoute: typeof CollectionIndexRoute
|
||||
GlobalItemsIndexRoute: typeof GlobalItemsIndexRoute
|
||||
@@ -227,13 +216,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof UsersUserIdRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/threads/$threadId': {
|
||||
id: '/threads/$threadId'
|
||||
path: '/threads/$threadId'
|
||||
fullPath: '/threads/$threadId'
|
||||
preLoaderRoute: typeof ThreadsThreadIdRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/setups/$setupId': {
|
||||
id: '/setups/$setupId'
|
||||
path: '/setups/$setupId'
|
||||
@@ -272,21 +254,6 @@ declare module '@tanstack/react-router' {
|
||||
}
|
||||
}
|
||||
|
||||
interface ThreadsThreadIdRouteChildren {
|
||||
ThreadsThreadIdIndexRoute: typeof ThreadsThreadIdIndexRoute
|
||||
ThreadsThreadIdCandidatesCandidateIdRoute: typeof ThreadsThreadIdCandidatesCandidateIdRoute
|
||||
}
|
||||
|
||||
const ThreadsThreadIdRouteChildren: ThreadsThreadIdRouteChildren = {
|
||||
ThreadsThreadIdIndexRoute: ThreadsThreadIdIndexRoute,
|
||||
ThreadsThreadIdCandidatesCandidateIdRoute:
|
||||
ThreadsThreadIdCandidatesCandidateIdRoute,
|
||||
}
|
||||
|
||||
const ThreadsThreadIdRouteWithChildren = ThreadsThreadIdRoute._addFileChildren(
|
||||
ThreadsThreadIdRouteChildren,
|
||||
)
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
LoginRoute: LoginRoute,
|
||||
@@ -294,7 +261,6 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
GlobalItemsGlobalItemIdRoute: GlobalItemsGlobalItemIdRoute,
|
||||
ItemsItemIdRoute: ItemsItemIdRoute,
|
||||
SetupsSetupIdRoute: SetupsSetupIdRoute,
|
||||
ThreadsThreadIdRoute: ThreadsThreadIdRouteWithChildren,
|
||||
UsersUserIdRoute: UsersUserIdRoute,
|
||||
CollectionIndexRoute: CollectionIndexRoute,
|
||||
GlobalItemsIndexRoute: GlobalItemsIndexRoute,
|
||||
|
||||
@@ -9,14 +9,11 @@ import {
|
||||
} from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import "../app.css";
|
||||
import { CandidateForm } from "../components/CandidateForm";
|
||||
import { CatalogSearchOverlay } from "../components/CatalogSearchOverlay";
|
||||
import { ConfirmDialog } from "../components/ConfirmDialog";
|
||||
import { ExternalLinkDialog } from "../components/ExternalLinkDialog";
|
||||
import { FabMenu } from "../components/FabMenu";
|
||||
import { ItemForm } from "../components/ItemForm";
|
||||
import { OnboardingWizard } from "../components/OnboardingWizard";
|
||||
import { SlideOutPanel } from "../components/SlideOutPanel";
|
||||
import { TotalsBar } from "../components/TotalsBar";
|
||||
import { useAuth } from "../hooks/useAuth";
|
||||
import { useDeleteCandidate } from "../hooks/useCandidates";
|
||||
@@ -79,16 +76,6 @@ function RootLayout() {
|
||||
const { data: auth, isLoading: authLoading } = useAuth();
|
||||
const isAuthenticated = !!auth?.user;
|
||||
|
||||
// Item panel state
|
||||
const panelMode = useUIStore((s) => s.panelMode);
|
||||
const editingItemId = useUIStore((s) => s.editingItemId);
|
||||
const closePanel = useUIStore((s) => s.closePanel);
|
||||
|
||||
// Candidate panel state
|
||||
const candidatePanelMode = useUIStore((s) => s.candidatePanelMode);
|
||||
const editingCandidateId = useUIStore((s) => s.editingCandidateId);
|
||||
const closeCandidatePanel = useUIStore((s) => s.closeCandidatePanel);
|
||||
|
||||
// Candidate delete state
|
||||
const confirmDeleteCandidateId = useUIStore(
|
||||
(s) => s.confirmDeleteCandidateId,
|
||||
@@ -114,9 +101,6 @@ function RootLayout() {
|
||||
!wizardDismissed &&
|
||||
isAuthenticated;
|
||||
|
||||
const isItemPanelOpen = panelMode !== "closed";
|
||||
const isCandidatePanelOpen = candidatePanelMode !== "closed";
|
||||
|
||||
// Route matching for contextual behavior
|
||||
const matchRoute = useMatchRoute();
|
||||
|
||||
@@ -186,40 +170,6 @@ function RootLayout() {
|
||||
<TotalsBar {...finalTotalsProps} />
|
||||
<Outlet />
|
||||
|
||||
{/* Item Slide-out Panel */}
|
||||
<SlideOutPanel
|
||||
isOpen={isItemPanelOpen}
|
||||
onClose={closePanel}
|
||||
title={panelMode === "add" ? "Add Item" : "Edit Item"}
|
||||
>
|
||||
{panelMode === "add" && <ItemForm mode="add" />}
|
||||
{panelMode === "edit" && (
|
||||
<ItemForm mode="edit" itemId={editingItemId} />
|
||||
)}
|
||||
</SlideOutPanel>
|
||||
|
||||
{/* Candidate Slide-out Panel */}
|
||||
{currentThreadId != null && (
|
||||
<SlideOutPanel
|
||||
isOpen={isCandidatePanelOpen}
|
||||
onClose={closeCandidatePanel}
|
||||
title={
|
||||
candidatePanelMode === "add" ? "Add Candidate" : "Edit Candidate"
|
||||
}
|
||||
>
|
||||
{candidatePanelMode === "add" && (
|
||||
<CandidateForm mode="add" threadId={currentThreadId} />
|
||||
)}
|
||||
{candidatePanelMode === "edit" && (
|
||||
<CandidateForm
|
||||
mode="edit"
|
||||
threadId={currentThreadId}
|
||||
candidateId={editingCandidateId}
|
||||
/>
|
||||
)}
|
||||
</SlideOutPanel>
|
||||
)}
|
||||
|
||||
{/* Item Confirm Delete Dialog */}
|
||||
<ConfirmDialog />
|
||||
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
interface UIState {
|
||||
// Item panel state
|
||||
panelMode: "closed" | "add" | "edit";
|
||||
editingItemId: number | null;
|
||||
// Item delete state
|
||||
confirmDeleteItemId: number | null;
|
||||
|
||||
openAddPanel: () => void;
|
||||
openEditPanel: (itemId: number) => void;
|
||||
closePanel: () => void;
|
||||
openConfirmDelete: (itemId: number) => void;
|
||||
closeConfirmDelete: () => void;
|
||||
|
||||
// Candidate panel state
|
||||
candidatePanelMode: "closed" | "add" | "edit";
|
||||
editingCandidateId: number | null;
|
||||
// Candidate delete state
|
||||
confirmDeleteCandidateId: number | null;
|
||||
|
||||
openCandidateAddPanel: () => void;
|
||||
openCandidateEditPanel: (id: number) => void;
|
||||
closeCandidatePanel: () => void;
|
||||
openConfirmDeleteCandidate: (id: number) => void;
|
||||
closeConfirmDeleteCandidate: () => void;
|
||||
|
||||
@@ -70,28 +60,15 @@ interface UIState {
|
||||
}
|
||||
|
||||
export const useUIStore = create<UIState>((set) => ({
|
||||
// Item panel
|
||||
panelMode: "closed",
|
||||
editingItemId: null,
|
||||
// Item delete
|
||||
confirmDeleteItemId: null,
|
||||
|
||||
openAddPanel: () => set({ panelMode: "add", editingItemId: null }),
|
||||
openEditPanel: (itemId) => set({ panelMode: "edit", editingItemId: itemId }),
|
||||
closePanel: () => set({ panelMode: "closed", editingItemId: null }),
|
||||
openConfirmDelete: (itemId) => set({ confirmDeleteItemId: itemId }),
|
||||
closeConfirmDelete: () => set({ confirmDeleteItemId: null }),
|
||||
|
||||
// Candidate panel
|
||||
candidatePanelMode: "closed",
|
||||
editingCandidateId: null,
|
||||
// Candidate delete
|
||||
confirmDeleteCandidateId: null,
|
||||
|
||||
openCandidateAddPanel: () =>
|
||||
set({ candidatePanelMode: "add", editingCandidateId: null }),
|
||||
openCandidateEditPanel: (id) =>
|
||||
set({ candidatePanelMode: "edit", editingCandidateId: id }),
|
||||
closeCandidatePanel: () =>
|
||||
set({ candidatePanelMode: "closed", editingCandidateId: null }),
|
||||
openConfirmDeleteCandidate: (id) => set({ confirmDeleteCandidateId: id }),
|
||||
closeConfirmDeleteCandidate: () => set({ confirmDeleteCandidateId: null }),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user