fix: application list item store not separated

This commit is contained in:
2025-01-26 18:06:46 +00:00
parent e367182dfa
commit fd2c3f118e
3 changed files with 112 additions and 66 deletions

View File

@@ -6,20 +6,20 @@ import {
useEffect, useEffect,
useRef, useRef,
} from "react" } from "react"
import { create } from "zustand" import { createStore, useStore } from "zustand"
import { useShallow } from "zustand/react/shallow" import { useShallow } from "zustand/react/shallow"
import { Button } from "~/components/button" import { Button } from "~/components/button"
import { DEFAULT_NODE, type Entry } from "~/home/graph" import { DEFAULT_NODE, type Entry } from "~/home/graph"
import { useStore } from "./store" import { useRootStore } from "./store"
function ApplicationList() { function ApplicationList() {
const entries = useStore(useShallow((state) => state.entries)) const entries = useRootStore(useShallow((state) => state.entries))
return Object.values(entries).map((entry) => ( return Object.values(entries).map((entry) => (
<ApplicationListItem key={entry.name} entry={entry} /> <ApplicationListItem key={entry.name} entry={entry} />
)) ))
} }
interface ListItemStore { interface ListItemState {
isAddingStage: boolean isAddingStage: boolean
newStageValue: string newStageValue: string
@@ -28,67 +28,101 @@ interface ListItemStore {
addStage: (entryName: string) => void addStage: (entryName: string) => void
} }
const useListItemStore = create<ListItemStore>()((set, get) => ({ function createListItemStore() {
isAddingStage: false, return createStore<ListItemState>()((set, get) => ({
newStageValue: "", isAddingStage: false,
newStageValue: "",
setNewStageValue: (newStageValue: string) => set({ newStageValue }), setNewStageValue: (newStageValue: string) => set({ newStageValue }),
setIsAddingStage: (isAddingStage: boolean) => set({ isAddingStage }), setIsAddingStage: (isAddingStage: boolean) => set({ isAddingStage }),
addStage: (entryName) => { addStage: (entryName) => {
const store = useStore.getState() const store = useRootStore.getState()
let stage = get().newStageValue let stage = get().newStageValue
if (stage) { if (stage) {
if (stage === DEFAULT_NODE.acceptedNode.key.toLowerCase()) { if (stage === DEFAULT_NODE.acceptedNode.key.toLowerCase()) {
stage = DEFAULT_NODE.acceptedNode.key stage = DEFAULT_NODE.acceptedNode.key
} else if (stage === DEFAULT_NODE.rejectedNode.key.toLowerCase()) { } else if (stage === DEFAULT_NODE.rejectedNode.key.toLowerCase()) {
stage = DEFAULT_NODE.rejectedNode.key stage = DEFAULT_NODE.rejectedNode.key
}
store.addStageInEntry(stage, entryName)
set({ isAddingStage: false, newStageValue: "" })
} }
store.addStageInEntry(stage, entryName) },
set({ isAddingStage: false, newStageValue: "" }) }))
} }
}, type ListItemStore = ReturnType<typeof createListItemStore>
}))
const ListItemStoreContext = createContext<ListItemStore>(
null as unknown as ListItemStore,
)
function useListItemStore<T>(selector: (state: ListItemState) => T): T {
const store = useContext(ListItemStoreContext)
return useStore(store, selector)
}
const EntryContext = createContext<Entry>(null as unknown as Entry) const EntryContext = createContext<Entry>(null as unknown as Entry)
const ApplicationListItem = memo(({ entry }: { entry: Entry }) => ( const ApplicationListItem = memo(({ entry }: { entry: Entry }) => {
<EntryContext value={entry}> const store = useRef<ListItemStore | null>(null)
<details className="w-full px-2 pb-2 -mx-2"> if (!store.current) {
<summary className="cursor-pointer">{entry.name}</summary> store.current = createListItemStore()
<ol className="pl-3 list-decimal list-inside text-sm"> }
{entry.stages.map((step) => (
<StageItem key={step} step={step} />
))}
<NewStageInput />
</ol>
<ApplicationActions />
</details>
</EntryContext>
))
const StageItem = memo(({ step }: { step: string }) => {
const entry = useContext(EntryContext)
const deleteStageInEntry = useStore((state) => state.deleteStageInEntry)
return ( return (
<li key={step} className="w-full group justify-between px-1"> <ListItemStoreContext value={store.current}>
<div className="w-[90%] inline-flex flex-row items-center justify-between"> <EntryContext value={entry}>
{step} <details className="w-full px-2 pb-2 -mx-2">
{step !== DEFAULT_NODE.applicationSubmittedNode.key ? ( <summary className="cursor-pointer">{entry.name}</summary>
<Button <ol className="pl-3 list-decimal list-inside text-sm">
variant="small" {entry.stages.map((step) => (
onClick={() => { <StageItem key={step} stage={step} />
deleteStageInEntry(step, entry.name) ))}
}} <NewStageInput />
className="invisible group-hover:visible" </ol>
> <ApplicationActions />
Delete </details>
</Button> </EntryContext>
) : null} </ListItemStoreContext>
</div>
</li>
) )
}) })
const StageItem = memo(({ stage }: { stage: string }) => (
<li key={stage} className="w-full group justify-between px-1">
<div className="w-[90%] inline-flex flex-row flex-wrap items-center justify-between">
{stage}
{stage !== DEFAULT_NODE.applicationSubmittedNode.key ? (
<StageItemActions stage={stage} />
) : null}
</div>
</li>
))
function StageItemActions({ stage }: { stage: string }) {
const entry = useContext(EntryContext)
const deleteStageInEntry = useRootStore((state) => state.deleteStageInEntry)
return (
<div className="flex flex-row space-x-2 invisible group-hover:visible">
<Button
variant="small"
onClick={() => {
deleteStageInEntry(stage, entry.name)
}}
>
Edit
</Button>
<Button
variant="small"
onClick={() => {
deleteStageInEntry(stage, entry.name)
}}
>
Delete
</Button>
</div>
)
}
function NewStageInput() { function NewStageInput() {
const isAddingStage = useListItemStore((state) => state.isAddingStage) const isAddingStage = useListItemStore((state) => state.isAddingStage)
const inputRef = useRef<HTMLInputElement | null>(null) const inputRef = useRef<HTMLInputElement | null>(null)
@@ -142,8 +176,8 @@ function ApplicationActions() {
const DefaultActions = memo(() => { const DefaultActions = memo(() => {
const entry = useContext(EntryContext) const entry = useContext(EntryContext)
const setIsAddingStage = useListItemStore((state) => state.setIsAddingStage) const setIsAddingStage = useListItemStore((state) => state.setIsAddingStage)
const addStageToEntry = useStore((state) => state.addStageInEntry) const addStageToEntry = useRootStore((state) => state.addStageInEntry)
const deleteEntry = useStore((state) => state.deleteEntry) const deleteEntry = useRootStore((state) => state.deleteEntry)
const isApplicationFinalized = const isApplicationFinalized =
entry.stages.at(-1) === DEFAULT_NODE.acceptedNode.key || entry.stages.at(-1) === DEFAULT_NODE.acceptedNode.key ||

View File

@@ -2,7 +2,7 @@ import { create } from "zustand/index"
import { immer } from "zustand/middleware/immer" import { immer } from "zustand/middleware/immer"
import { DEFAULT_NODE, type Entry, type Node } from "~/home/graph" import { DEFAULT_NODE, type Entry, type Node } from "~/home/graph"
interface Store { interface RootStore {
nodes: Record<string, Node> nodes: Record<string, Node>
starts: Node["key"][] starts: Node["key"][]
entries: Record<string, Entry> entries: Record<string, Entry>
@@ -10,11 +10,12 @@ interface Store {
addEntry: (name: string) => void addEntry: (name: string) => void
hasEntry: (name: string) => boolean hasEntry: (name: string) => boolean
addStageInEntry: (stage: string, entryName: string) => void addStageInEntry: (stage: string, entryName: string) => void
editStageInEntry: (i: number, stage: string, entryName: string) => void
deleteStageInEntry: (stage: string, entryName: string) => void deleteStageInEntry: (stage: string, entryName: string) => void
deleteEntry: (entryName: string) => void deleteEntry: (entryName: string) => void
} }
const useStore = create<Store>()( const useRootStore = create<RootStore>()(
immer((set, get) => ({ immer((set, get) => ({
isAddingEntry: false, isAddingEntry: false,
nodes: { nodes: {
@@ -72,6 +73,17 @@ const useStore = create<Store>()(
} }
}), }),
editStageInEntry: (i, stage, entryName) =>
set((state) => {
const entry = state.entries[entryName]
if (entry && stage) {
if (i >= entry.stages.length) {
return
}
entry.stages[i] = stage
}
}),
deleteStageInEntry: (stage, entryName) => deleteStageInEntry: (stage, entryName) =>
set((state) => { set((state) => {
if (stage === DEFAULT_NODE.applicationSubmittedNode.key) { if (stage === DEFAULT_NODE.applicationSubmittedNode.key) {
@@ -119,4 +131,4 @@ const useStore = create<Store>()(
})), })),
) )
export { useStore } export { useRootStore }

View File

@@ -4,7 +4,7 @@ import Chart from "react-google-charts"
import { Button } from "~/components/button" import { Button } from "~/components/button"
import { ApplicationList } from "~/home/application-list" import { ApplicationList } from "~/home/application-list"
import type { Node } from "~/home/graph" import type { Node } from "~/home/graph"
import { useStore } from "~/home/store" import { useRootStore } from "~/home/store"
import { Queue } from "~/queue" import { Queue } from "~/queue"
import { useUiMode } from "~/use-ui-mode" import { useUiMode } from "~/use-ui-mode"
@@ -35,8 +35,8 @@ export default function Home() {
} }
function FlufferChart() { function FlufferChart() {
const nodes = useStore((state) => state.nodes) const nodes = useRootStore((state) => state.nodes)
const starts = useStore((state) => state.starts) const starts = useRootStore((state) => state.starts)
const uiMode = useUiMode() const uiMode = useUiMode()
const data = useMemo(() => { const data = useMemo(() => {
@@ -86,8 +86,8 @@ function FlufferChart() {
function AddApplicationForm() { function AddApplicationForm() {
const [isAddingEntry, setIsAddingEntry] = useState(false) const [isAddingEntry, setIsAddingEntry] = useState(false)
const addEntry = useStore((state) => state.addEntry) const addEntry = useRootStore((state) => state.addEntry)
const hasEntry = useStore((state) => state.hasEntry) const hasEntry = useRootStore((state) => state.hasEntry)
const inputRef = useRef<HTMLInputElement | null>(null) const inputRef = useRef<HTMLInputElement | null>(null)
useEffect(() => { useEffect(() => {