Files
av-planner/src/components/sharing/DiagramSharingModal.tsx
T
aarbit 26f41b3d4a Rename Project to Diagram throughout the app
Per organized-ideas.md §8: the storage layer (DiagramRepository etc.) was
already renamed in an earlier phase; this finishes it everywhere else.

- domain/types.ts: Project -> Diagram. domain/project.ts -> domain/diagram.ts
  (createEmptyProject -> createEmptyDiagram, default name "Untitled Diagram").
- domain/compatibility.ts, domain/bom.ts: Project param/type -> Diagram.
- data/exportImport.ts: ProjectImportError -> DiagramImportError,
  projectToJson/downloadProjectFile/readProjectFile/normalizeProject -> their
  Diagram equivalents.
- state/projectStore.ts -> state/diagramStore.ts: useProjectStore ->
  useDiagramStore, the `project` field -> `diagram`, newProject/renameProject/
  importProject/applyRestoredProject -> *Diagram, restoredProjectUpdatedAt ->
  restoredDiagramUpdatedAt.
- Every component updated to match, compiler-guided (tsc -b enumerated each
  remaining call site after the core rename, the same approach used for the
  earlier catalog-parameter refactor).
- README updated for the terminology, and to match the repository class
  names (which had already been renamed but the README hadn't caught up).

No backend/schema changes: the JSON shape stored in diagrams.data never
changed, only TypeScript-side identifiers, so existing diagrams are
unaffected. One SQL comment fixed for accuracy (no migration needed).

Verified: tsc -b and oxlint clean; grepped src/ for any remaining
Project/project reference (none) after the sweep.
2026-09-11 12:10:11 -05:00

129 lines
5.4 KiB
TypeScript

import { useEffect, useState } from 'react'
import type { CollaboratorPermission } from '../../data/DiagramCollaboratorRepository'
import { SelfCollaboratorError, UnknownUsernameError } from '../../data/DiagramCollaboratorRepository'
import { useDiagramCollaboratorStore } from '../../state/diagramCollaboratorStore'
import { useDiagramStore } from '../../state/diagramStore'
import Modal from '../common/Modal'
export default function DiagramSharingModal({ onClose }: { onClose: () => void }) {
const diagram = useDiagramStore((s) => s.diagram)
const access = useDiagramStore((s) => s.access)
const isOwner = access?.myPermission === 'owner'
const diagramId = diagram.id
const collaborators = useDiagramCollaboratorStore((s) => s.collaborators)
const isLoaded = useDiagramCollaboratorStore((s) => s.isLoaded)
const load = useDiagramCollaboratorStore((s) => s.load)
const add = useDiagramCollaboratorStore((s) => s.add)
const updatePermission = useDiagramCollaboratorStore((s) => s.updatePermission)
const remove = useDiagramCollaboratorStore((s) => s.remove)
const [username, setUsername] = useState('')
const [permission, setPermission] = useState<CollaboratorPermission>('view')
const [error, setError] = useState<string | null>(null)
const [busy, setBusy] = useState(false)
useEffect(() => {
load(diagramId)
}, [diagramId, load])
const handleAdd = async () => {
const trimmed = username.trim()
if (!trimmed) return
setError(null)
setBusy(true)
try {
await add(diagramId, trimmed, permission)
setUsername('')
} catch (err) {
setError(
err instanceof UnknownUsernameError || err instanceof SelfCollaboratorError
? err.message
: 'Could not add that person. Try again.',
)
} finally {
setBusy(false)
}
}
const handleRemove = (userId: string, name: string) => {
const confirmed = window.confirm(`Remove "${name}" from this diagram? They'll immediately lose access.`)
if (confirmed) remove(diagramId, userId)
}
return (
<Modal title={`Share "${diagram.name}"`} onClose={onClose} width="max-w-md">
<div className="space-y-3">
{isOwner && (
<div className="space-y-1.5 rounded border border-slate-200 bg-slate-50 p-2">
<div className="flex gap-1.5">
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
placeholder="Username"
className="w-0 min-w-0 flex-1 rounded border border-slate-300 px-1.5 py-1 text-xs"
/>
<select
value={permission}
onChange={(e) => setPermission(e.target.value as CollaboratorPermission)}
className="rounded border border-slate-300 px-1 py-1 text-xs"
>
<option value="view">Can view</option>
<option value="edit">Can edit</option>
</select>
<button
onClick={handleAdd}
disabled={busy || !username.trim()}
className="shrink-0 rounded bg-indigo-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-40"
>
Add
</button>
</div>
{error && <p className="text-[11px] text-red-600">{error}</p>}
</div>
)}
<div className="space-y-1.5">
{!isLoaded && <p className="py-2 text-center text-xs italic text-slate-400">Loading…</p>}
{isLoaded && collaborators.length === 0 && (
<p className="py-2 text-center text-xs italic text-slate-400">
Not shared with anyone yet{isOwner ? ' — add someone above.' : '.'}
</p>
)}
{collaborators.map((c) => (
<div key={c.userId} className="flex items-center justify-between gap-2 rounded border border-slate-200 bg-white px-2 py-1.5 text-xs">
<span className="font-medium text-slate-700">{c.username}</span>
{isOwner ? (
<div className="flex shrink-0 items-center gap-1.5">
<select
value={c.permission}
onChange={(e) => updatePermission(diagramId, c.userId, e.target.value as CollaboratorPermission)}
className="rounded border border-slate-300 px-1 py-1 text-[11px]"
>
<option value="view">Can view</option>
<option value="edit">Can edit</option>
</select>
<button
onClick={() => handleRemove(c.userId, c.username)}
className="rounded bg-slate-100 px-2 py-1 text-[11px] font-medium text-slate-600 hover:bg-red-50 hover:text-red-600"
>
Remove
</button>
</div>
) : (
<span className="shrink-0 text-[11px] text-slate-400">{c.permission === 'edit' ? 'Can edit' : 'Can view'}</span>
)}
</div>
))}
</div>
</div>
<div className="mt-4 flex justify-end border-t border-slate-100 pt-3">
<button onClick={onClose} className="rounded px-3 py-1.5 text-xs text-slate-600 hover:bg-slate-100">
Close
</button>
</div>
</Modal>
)
}