Exosphere is a set of small, modular, self-hostable community tools built on the AT Protocol. app.exosphere.site
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: improve label edition

Hugo a2edf991 1c11a0d4

+41 -43
+1
packages/client/src/components/inline-label-editor.css.ts
··· 9 9 display: "inline-flex", 10 10 alignItems: "center", 11 11 gap: "4px", 12 + inlineSize: "3.5rem", 12 13 background: "none", 13 14 border: "none", 14 15 color: vars.color.textMuted,
-3
packages/client/src/components/inline-label-editor.tsx
··· 9 9 labels, 10 10 selectedIds, 11 11 onToggle, 12 - disabled = false, 13 12 }: { 14 13 labels: LabelOption[]; 15 14 selectedIds: string[]; 16 15 onToggle: (labelId: string, selected: boolean) => void; 17 - disabled?: boolean; 18 16 }) { 19 17 const open = useSignal(false); 20 18 const wrapperRef = useRef<HTMLDivElement>(null); ··· 65 63 <input 66 64 type="checkbox" 67 65 checked={isSelected} 68 - disabled={disabled} 69 66 onChange={() => onToggle(label.id, !isSelected)} 70 67 /> 71 68 <LabelBadge label={label} />
+20 -20
packages/feature-requests/src/ui/pages/feature-request.tsx
··· 1 - import { useSignal, useComputed } from "@preact/signals"; 1 + import { useSignal, useComputed, batch } from "@preact/signals"; 2 2 import { auth } from "@exosphere/client/auth"; 3 3 import { useLocation, useRoute, spherePath } from "@exosphere/client/router"; 4 4 import { useCanDo } from "@exosphere/client/permissions"; ··· 568 568 569 569 const availableLabels = useSignal<LabelData[]>([]); 570 570 const localLabelIds = useSignal<string[] | null>(null); 571 - const savingLabels = useSignal(false); 571 + const labelSaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null); 572 572 573 573 useEffect(() => { 574 574 if (canChangeStatus.value) { ··· 635 635 } 636 636 }; 637 637 638 - const handleLabelToggle = async (labelId: string, selected: boolean) => { 639 - if (!fr || savingLabels.value) return; 638 + const handleLabelToggle = (labelId: string, selected: boolean) => { 639 + if (!fr) return; 640 640 const label = availableLabels.value.find((l) => l.id === labelId); 641 641 if (!label) return; 642 - const prev = fr.labels; 643 - const prevIds = prev.map((l) => l.id); 644 - const nextIds = selected ? [...prevIds, labelId] : prevIds.filter((id) => id !== labelId); 645 - const nextLabels = selected ? [...prev, label] : prev.filter((l) => l.id !== labelId); 646 - fr.labels = nextLabels; 647 - localLabelIds.value = nextIds; 648 - savingLabels.value = true; 649 - try { 650 - await updateFeatureRequestLabels(fr.id, nextIds); 651 - } catch { 652 - fr.labels = prev; 653 - localLabelIds.value = prev.map((l) => l.id); 654 - } finally { 655 - savingLabels.value = false; 656 - } 642 + const nextLabels = selected 643 + ? [...fr.labels, label] 644 + : fr.labels.filter((l) => l.id !== labelId); 645 + const nextIds = nextLabels.map((l) => l.id); 646 + batch(() => { 647 + fr.labels = nextLabels; 648 + localLabelIds.value = nextIds; 649 + }); 650 + if (labelSaveTimer.current) clearTimeout(labelSaveTimer.current); 651 + labelSaveTimer.current = setTimeout(async () => { 652 + try { 653 + await updateFeatureRequestLabels(fr.id, localLabelIds.value!); 654 + } catch { 655 + // Refetch to get the true server state 656 + } 657 + }, 300); 657 658 }; 658 659 659 660 return ( ··· 726 727 labels={availableLabels.value} 727 728 selectedIds={localLabelIds.value ?? fr.labels.map((l) => l.id)} 728 729 onToggle={handleLabelToggle} 729 - disabled={savingLabels.value} 730 730 /> 731 731 ) : null} 732 732 </RequestCard>
+20 -20
packages/kanban/src/ui/pages/task.tsx
··· 1 - import { useSignal } from "@preact/signals"; 1 + import { useSignal, batch } from "@preact/signals"; 2 2 import { auth } from "@exosphere/client/auth"; 3 3 import { useLocation, useRoute, spherePath } from "@exosphere/client/router"; 4 4 import { useCanDo } from "@exosphere/client/permissions"; ··· 396 396 397 397 const availableLabels = useSignal<LabelData[]>([]); 398 398 const localLabelIds = useSignal<string[] | null>(null); 399 - const savingLabels = useSignal(false); 399 + const labelSaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null); 400 400 401 401 useEffect(() => { 402 402 if (canManage.value) { ··· 466 466 } 467 467 }; 468 468 469 - const handleLabelToggle = async (labelId: string, selected: boolean) => { 470 - if (!task || savingLabels.value) return; 469 + const handleLabelToggle = (labelId: string, selected: boolean) => { 470 + if (!task) return; 471 471 const label = availableLabels.value.find((l) => l.id === labelId); 472 472 if (!label) return; 473 - const prev = task.labels; 474 - const prevIds = prev.map((l) => l.id); 475 - const nextIds = selected ? [...prevIds, labelId] : prevIds.filter((id) => id !== labelId); 476 - const nextLabels = selected ? [...prev, label] : prev.filter((l) => l.id !== labelId); 477 - task.labels = nextLabels; 478 - localLabelIds.value = nextIds; 479 - savingLabels.value = true; 480 - try { 481 - await updateTaskLabels(task.id, nextIds); 482 - } catch { 483 - task.labels = prev; 484 - localLabelIds.value = prev.map((l) => l.id); 485 - } finally { 486 - savingLabels.value = false; 487 - } 473 + const nextLabels = selected 474 + ? [...task.labels, label] 475 + : task.labels.filter((l) => l.id !== labelId); 476 + const nextIds = nextLabels.map((l) => l.id); 477 + batch(() => { 478 + task.labels = nextLabels; 479 + localLabelIds.value = nextIds; 480 + }); 481 + if (labelSaveTimer.current) clearTimeout(labelSaveTimer.current); 482 + labelSaveTimer.current = setTimeout(async () => { 483 + try { 484 + await updateTaskLabels(task.id, localLabelIds.value!); 485 + } catch { 486 + // Refetch to get the true server state 487 + } 488 + }, 300); 488 489 }; 489 490 490 491 return ( ··· 607 608 labels={availableLabels.value} 608 609 selectedIds={localLabelIds.value ?? task.labels.map((l) => l.id)} 609 610 onToggle={handleLabelToggle} 610 - disabled={savingLabels.value} 611 611 /> 612 612 ) : task.labels.length > 0 ? ( 613 613 <div class={ui.metaRow}>