feat(admin) : add sortable condition defaults
CI / Verify (push) Successful in 1m15s
CI / Build immutable images and deploy (push) Successful in 2m47s

This commit is contained in:
2026-09-11 16:35:17 +07:00 Unverified
parent 98025810e8
commit ecbb8dda55
+188 -74
View File
@@ -1128,6 +1128,140 @@ function SortableExtraItem({
);
}
type ConditionNavItem = {
clientId: string;
name: string;
};
function SortableConditionTab({
item,
index,
active,
onSelect,
onRename,
}: {
item: ConditionNavItem;
index: number;
active: boolean;
onSelect: () => void;
onRename: () => void;
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: item.clientId });
const label = item.name.trim() || `Condition ${index + 1}`;
return (
<div
ref={setNodeRef}
style={{ transform: CSS.Transform.toString(transform), transition }}
className={cn("flex shrink-0 items-center rounded-md border", isDragging && "z-10 opacity-60")}
>
<ToggleGroupItem
value={item.clientId}
className="border-0 aria-pressed:bg-primary aria-pressed:text-primary-foreground"
onClick={onSelect}
onDoubleClick={onRename}
>
{label}
{index === 0 ? <Badge variant={active ? "secondary" : "outline"}>Default</Badge> : null}
</ToggleGroupItem>
<SimpleTooltip text={`ลากเพื่อจัดลำดับ ${label}`}>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="touch-none rounded-l-none"
{...attributes}
{...listeners}
>
<GripVerticalIcon />
<span className="sr-only">ลากเพื่อจัดลำดับ {label}</span>
</Button>
</SimpleTooltip>
</div>
);
}
function SortableConditionSwitcher({
ariaLabel,
items,
activeIndex,
onActiveIndexChange,
onReorder,
onRename,
}: {
ariaLabel: string;
items: ConditionNavItem[];
activeIndex: number;
onActiveIndexChange: (index: number) => void;
onReorder: (from: number, to: number) => void;
onRename: (index: number) => void;
}) {
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
);
const activeId = items[activeIndex]?.clientId;
return (
<ScrollableNav ariaLabel={ariaLabel} className="min-w-0 flex-1">
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={({ active, over }) => {
if (!over || active.id === over.id) return;
const from = items.findIndex((item) => item.clientId === active.id);
const to = items.findIndex((item) => item.clientId === over.id);
if (from !== -1 && to !== -1) onReorder(from, to);
}}
>
<SortableContext items={items.map((item) => item.clientId)} strategy={rectSortingStrategy}>
<ToggleGroup
aria-label={ariaLabel}
variant="outline"
value={activeId ? [activeId] : []}
onValueChange={(values) => {
const index = items.findIndex((item) => item.clientId === values[0]);
if (index !== -1) onActiveIndexChange(index);
}}
>
{items.map((item, index) => (
<SortableConditionTab
key={item.clientId}
item={item}
index={index}
active={item.clientId === activeId}
onSelect={() => onActiveIndexChange(index)}
onRename={() => onRename(index)}
/>
))}
</ToggleGroup>
</SortableContext>
</DndContext>
</ScrollableNav>
);
}
function reorderConditions<T extends ConditionNavItem>(
items: T[],
activeIndex: number,
from: number,
to: number,
) {
const activeId = items[activeIndex]?.clientId;
const reordered = arrayMove(items, from, to);
return {
items: reordered,
activeIndex: Math.max(0, reordered.findIndex((item) => item.clientId === activeId)),
};
}
export function GuideSectionEditor(props: GuideSectionEditorProps) {
return (
<div className="flex flex-col gap-6">
@@ -1249,27 +1383,18 @@ function WeaponEditor({ guide, weapons, characters, initial }: EditorBase) {
/>
</Field>
<div className="flex flex-wrap gap-2">
<ScrollableNav ariaLabel="เลือกเงื่อนไขอาวุธ" className="min-w-0 flex-1">
<ToggleGroup
aria-label="เลือกเงื่อนไขอาวุธ"
variant="outline"
value={[String(activeGroupIndex)]}
onValueChange={(values) => {
if (values[0]) setActiveGroupIndex(Number(values[0]));
}}
>
{groups.map((group, index) => (
<ToggleGroupItem
key={group.clientId}
value={String(index)}
className="aria-pressed:bg-primary aria-pressed:text-primary-foreground"
onDoubleClick={() => setRenamingWeaponGroupIndex(index)}
>
{group.name.trim() || `Condition ${index + 1}`}
</ToggleGroupItem>
))}
</ToggleGroup>
</ScrollableNav>
<SortableConditionSwitcher
ariaLabel="เลือกเงื่อนไขอาวุธ"
items={groups}
activeIndex={activeGroupIndex}
onActiveIndexChange={setActiveGroupIndex}
onRename={setRenamingWeaponGroupIndex}
onReorder={(from, to) => {
const reordered = reorderConditions(groups, activeGroupIndex, from, to);
setGroups(reordered.items);
setActiveGroupIndex(reordered.activeIndex);
}}
/>
<Button type="button" variant="outline" disabled={groups.length >= 20} onClick={() => {
setGroups([...groups, { clientId: createClientId("weapon-condition"), name: "", note: "", rows: [] }]);
setActiveGroupIndex(groups.length);
@@ -2108,17 +2233,18 @@ function ArtifactEditor({
<FieldSet>
<FieldLegend>Target Stats</FieldLegend>
<div className="flex flex-wrap gap-2">
<ScrollableNav ariaLabel="เลือกเงื่อนไขค่าสถานะเป้าหมาย" className="min-w-0 flex-1">
<ToggleGroup aria-label="เลือกเงื่อนไขค่าสถานะเป้าหมาย" variant="outline" value={[String(activeTargetGroupIndex)]} onValueChange={(values) => {
if (values[0]) setActiveTargetGroupIndex(Number(values[0]));
}}>
{targetGroups.map((group, index) => (
<ToggleGroupItem key={group.clientId} value={String(index)} className="aria-pressed:bg-primary aria-pressed:text-primary-foreground" onDoubleClick={() => setRenamingTargetGroupIndex(index)}>
{group.name.trim() || `Condition ${index + 1}`}
</ToggleGroupItem>
))}
</ToggleGroup>
</ScrollableNav>
<SortableConditionSwitcher
ariaLabel="เลือกเงื่อนไขค่าสถานะเป้าหมาย"
items={targetGroups}
activeIndex={activeTargetGroupIndex}
onActiveIndexChange={setActiveTargetGroupIndex}
onRename={setRenamingTargetGroupIndex}
onReorder={(from, to) => {
const reordered = reorderConditions(targetGroups, activeTargetGroupIndex, from, to);
setTargetGroups(reordered.items);
setActiveTargetGroupIndex(reordered.activeIndex);
}}
/>
<ConditionRenameDialog
open={renamingTargetGroupIndex !== null}
fallback={`Condition ${(renamingTargetGroupIndex ?? 0) + 1}`}
@@ -2417,27 +2543,18 @@ function ConstellationEditor({
/>
</Field>
<div className="flex flex-wrap gap-2">
<ScrollableNav ariaLabel="เลือกเงื่อนไขกลุ่มดาว" className="min-w-0 flex-1">
<ToggleGroup
aria-label="เลือกเงื่อนไขกลุ่มดาว"
variant="outline"
value={[String(activeConstellationGroupIndex)]}
onValueChange={(values) => {
if (values[0]) setActiveConstellationGroupIndex(Number(values[0]));
}}
>
{groups.map((group, index) => (
<ToggleGroupItem
key={group.clientId}
value={String(index)}
className="aria-pressed:bg-primary aria-pressed:text-primary-foreground"
onDoubleClick={() => setRenamingConstellationGroupIndex(index)}
>
{group.name.trim() || `Condition ${index + 1}`}
</ToggleGroupItem>
))}
</ToggleGroup>
</ScrollableNav>
<SortableConditionSwitcher
ariaLabel="เลือกเงื่อนไขกลุ่มดาว"
items={groups}
activeIndex={activeConstellationGroupIndex}
onActiveIndexChange={setActiveConstellationGroupIndex}
onRename={setRenamingConstellationGroupIndex}
onReorder={(from, to) => {
const reordered = reorderConditions(groups, activeConstellationGroupIndex, from, to);
setGroups(reordered.items);
setActiveConstellationGroupIndex(reordered.activeIndex);
}}
/>
<Button
type="button"
variant="outline"
@@ -3110,27 +3227,24 @@ function TeamEditor({
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex flex-wrap gap-2">
<ScrollableNav ariaLabel="เลือกเงื่อนไข Team" className="min-w-0 flex-1">
<ToggleGroup
aria-label="เลือกเงื่อนไข Team"
variant="outline"
value={[String(activeTeamGroupIndex)]}
onValueChange={(values) => {
const nextIndex = Number(values[0]);
const group = teamGroups[nextIndex];
if (!group) return;
setActiveTeamGroupIndex(nextIndex);
setSelectedTeamIds(new Set());
setSourceTeamId(group.teams[0]?.clientId ?? "");
}}
>
{teamGroups.map((group, index) => (
<ToggleGroupItem key={group.clientId} value={String(index)} onDoubleClick={() => setRenamingTeamGroupIndex(index)} className="aria-pressed:bg-primary aria-pressed:text-primary-foreground">
{group.name.trim() || `Condition ${index + 1}`}
</ToggleGroupItem>
))}
</ToggleGroup>
</ScrollableNav>
<SortableConditionSwitcher
ariaLabel="เลือกเงื่อนไข Team"
items={teamGroups}
activeIndex={activeTeamGroupIndex}
onActiveIndexChange={(nextIndex) => {
const group = teamGroups[nextIndex];
if (!group) return;
setActiveTeamGroupIndex(nextIndex);
setSelectedTeamIds(new Set());
setSourceTeamId(group.teams[0]?.clientId ?? "");
}}
onRename={setRenamingTeamGroupIndex}
onReorder={(from, to) => {
const reordered = reorderConditions(teamGroups, activeTeamGroupIndex, from, to);
setTeamGroups(reordered.items);
setActiveTeamGroupIndex(reordered.activeIndex);
}}
/>
<ConditionRenameDialog
open={renamingTeamGroupIndex !== null}
fallback={`Condition ${(renamingTeamGroupIndex ?? 0) + 1}`}