< Summary

Information
Class: tui-item-edit.tsx
Assembly: app.features.items
File(s): /home/runner/work/ClutterStock/ClutterStock/frontend/app/features/items/tui-item-edit.tsx
Tag: 58_25416222083
Line coverage
0%
Covered lines: 0
Uncovered lines: 54
Coverable lines: 54
Total lines: 193
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 49
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/ClutterStock/ClutterStock/frontend/app/features/items/tui-item-edit.tsx

#LineLine coverage
 1import { useEffect, useMemo, useRef, useState } from "react";
 2import { useFetcher } from "react-router";
 3import type { ItemResponse, LocationResponse, RoomResponse } from "~/api/client";
 4import { TuiCombobox, type TuiComboboxOption } from "./tui-combobox";
 5
 6type ActionData =
 7  | { ok: true; intent: "update-item"; item: ItemResponse }
 8  | { ok: true; intent: "delete-item" }
 9  | { ok: false; error: string };
 10
 011export function TuiItemEdit({ item, rooms, locations, onCancel, onSaved, onDelete }: {
 12  item: ItemResponse;
 13  rooms: RoomResponse[];
 14  locations: LocationResponse[];
 15  onCancel: () => void;
 16  onSaved: (saved: ItemResponse) => void;
 17  onDelete: () => void;
 18}) {
 019  const fetcher = useFetcher<ActionData>();
 020  const submitting = fetcher.state !== "idle";
 021  const [validationError, setValidationError] = useState<string | null>(null);
 022  const [roomId, setRoomId] = useState<number | null>(item.roomId ?? null);
 023  const formRef = useRef<HTMLFormElement>(null);
 24
 025  const roomOptions = useMemo<TuiComboboxOption[]>(() => {
 026    const opts: TuiComboboxOption[] = [];
 027    for (const loc of locations) {
 028      const locRooms = rooms.filter(r => r.locationId === loc.id);
 029      if (locRooms.length === 0) continue;
 030      opts.push({ kind: "group", label: loc.name ?? "Location" });
 031      for (const r of locRooms) {
 032        opts.push({ kind: "option", label: r.name ?? "Room", value: r.id! });
 33      }
 34    }
 035    return opts;
 36  }, [rooms, locations]);
 37
 038  const actionError = fetcher.state === "idle" && fetcher.data && !fetcher.data.ok
 39    ? fetcher.data.error : null;
 040  const error = validationError ?? actionError;
 41
 042  const onSavedRef = useRef(onSaved);
 043  useEffect(() => { onSavedRef.current = onSaved; });
 44
 045  const wasSubmittingRef = useRef(false);
 046  useEffect(() => {
 047    if (fetcher.state === "submitting") { wasSubmittingRef.current = true; return; }
 048    if (!wasSubmittingRef.current || fetcher.state !== "idle" || !fetcher.data) return;
 049    wasSubmittingRef.current = false;
 050    if (fetcher.data.ok && fetcher.data.intent === "update-item") {
 051      onSavedRef.current(fetcher.data.item);
 52    }
 53  }, [fetcher.state, fetcher.data]);
 54
 055  function submitForm() {
 056    const form = formRef.current;
 057    if (!form) return;
 058    const fd = new FormData(form);
 059    const name = (fd.get("name") as string ?? "").trim();
 060    if (!name) { setValidationError("name is required"); return; }
 061    setValidationError(null);
 062    fetcher.submit(fd, { method: "post" });
 63  }
 64
 065  function handleKeyDown(e: React.KeyboardEvent<HTMLFormElement>) {
 066    if (e.key === "Escape") {
 067      e.preventDefault();
 068      onCancel();
 069      return;
 70    }
 071    if ((e.ctrlKey || e.metaKey) && (e.key === "Enter" || e.key === "s" || e.key === "S")) {
 072      e.preventDefault();
 073      submitForm();
 74    }
 75  }
 76
 077  return (
 78    <fetcher.Form
 79      method="post"
 80      ref={formRef}
 81      onKeyDown={handleKeyDown}
 082      onSubmit={(e) => { e.preventDefault(); submitForm(); }}
 83      className="tui-panel tui-detail tui-edit"
 84      style={{ flexShrink: 0, height: 200 }}
 85    >
 86      <span className="tui-panel-title">{`─[ edit · #${item.id} ]─`}</span>
 87
 88      <input type="hidden" name="_intent" value="update-item" />
 89      <input type="hidden" name="itemId" value={item.id ?? ""} />
 90
 91      <div className="tui-detail-grid tui-edit-grid">
 92        <EditField label="name" name="name" defaultValue={item.name ?? ""} autoFocus required />
 93        <EditField label="category" name="category" defaultValue={item.category ?? ""} />
 94        <div className="tui-detail-field tui-edit-row tui-edit-row--combo">
 95          <span className="tui-detail-label">{"room    "}</span>
 96          <span className="tui-detail-colon">:</span>{" "}
 97          <TuiCombobox
 98            name="roomId"
 99            value={roomId}
 100            options={roomOptions}
 101            onChange={setRoomId}
 102            disabled={submitting}
 103            ariaLabel="Room"
 104          />
 105        </div>
 106        <ReadField label="location" value={resolveLocationName(roomId, rooms, locations)} />
 107        <EditField label="desc"     name="description" defaultValue={item.description ?? ""} fullWidth />
 108        <div className="tui-detail-notes tui-edit-row">
 109          <span className="tui-detail-label">notes   </span>
 110          <span className="tui-detail-colon">:</span>{" "}
 111          <span className="tui-edit-bracket">[</span>
 112          <input
 113            type="text"
 114            name="notes"
 115            defaultValue={item.notes ?? ""}
 116            className="tui-edit-input"
 117            disabled={submitting}
 118          />
 119          <span className="tui-edit-bracket">]</span>
 120        </div>
 121      </div>
 122
 123      {error && <div className="tui-edit-error">! {error}</div>}
 124
 125      <div className="tui-detail-actions">
 126        <button type="submit" disabled={submitting} className="tui-detail-action">
 127          [^S] {submitting ? "saving…" : "save"}
 128        </button>
 129        <span className="tui-detail-sep">·</span>
 130        <button type="button" onClick={onCancel} disabled={submitting} className="tui-detail-action">
 131          [Esc] cancel
 132        </button>
 133        <span className="tui-detail-sep">·</span>
 134        <button
 135          type="button"
 136          onClick={onDelete}
 137          disabled={submitting}
 138          className="tui-detail-action tui-detail-action--danger"
 139        >
 140          [d] delete
 141        </button>
 142      </div>
 143    </fetcher.Form>
 144  );
 145}
 146
 0147function EditField({ label, name, defaultValue, autoFocus, required, fullWidth }: {
 148  label: string;
 149  name: string;
 150  defaultValue: string;
 151  autoFocus?: boolean;
 152  required?: boolean;
 153  fullWidth?: boolean;
 154}) {
 0155  return (
 156    <div className={`tui-detail-field tui-edit-row${fullWidth ? " tui-edit-row--full" : ""}`}>
 157      <span className="tui-detail-label">{label.padEnd(8, " ")}</span>
 158      <span className="tui-detail-colon">:</span>{" "}
 159      <span className="tui-edit-bracket">[</span>
 160      <input
 161        type="text"
 162        name={name}
 163        defaultValue={defaultValue}
 164        autoFocus={autoFocus}
 165        required={required}
 166        className="tui-edit-input"
 167      />
 168      <span className="tui-edit-bracket">]</span>
 169    </div>
 170  );
 171}
 172
 0173function ReadField({ label, value }: { label: string; value: React.ReactNode }) {
 0174  return (
 175    <div className="tui-detail-field">
 176      <span className="tui-detail-label">{label.padEnd(8, " ")}</span>
 177      <span className="tui-detail-colon">:</span>{" "}
 178      <span className="tui-detail-value">{value}</span>
 179    </div>
 180  );
 181}
 182
 0183function resolveLocationName(
 184  roomId: number | null,
 185  rooms: RoomResponse[],
 186  locations: LocationResponse[],
 187): string {
 0188  if (roomId == null) return "—";
 0189  const room = rooms.find(r => r.id === roomId);
 0190  if (!room || room.locationId == null) return "—";
 0191  const loc = locations.find(l => l.id === room.locationId);
 0192  return loc?.name ?? "—";
 193}