fix: allow closing terminal chart slots
This commit is contained in:
@@ -269,6 +269,7 @@ const MAX_TERMINAL_GRID_SIDE = 3;
|
|||||||
const MAX_TERMINAL_CHARTS = 9;
|
const MAX_TERMINAL_CHARTS = 9;
|
||||||
const MOBILE_TERMINAL_CHARTS = 1;
|
const MOBILE_TERMINAL_CHARTS = 1;
|
||||||
const DEFAULT_TERMINAL_GRID_SIDE = 2;
|
const DEFAULT_TERMINAL_GRID_SIDE = 2;
|
||||||
|
const TERMINAL_SLOTS_STORAGE_KEY = "polyweather_terminal_slots";
|
||||||
|
|
||||||
function clampGridSide(value: number) {
|
function clampGridSide(value: number) {
|
||||||
if (!Number.isFinite(value)) return DEFAULT_TERMINAL_GRID_SIDE;
|
if (!Number.isFinite(value)) return DEFAULT_TERMINAL_GRID_SIDE;
|
||||||
@@ -295,6 +296,45 @@ function normalizeSlotList(slots: Array<string | null>, totalSlots: number) {
|
|||||||
return [...slots, ...Array(totalSlots - slots.length).fill(null)];
|
return [...slots, ...Array(totalSlots - slots.length).fill(null)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readStoredTerminalSlots(totalSlots = MAX_TERMINAL_CHARTS) {
|
||||||
|
const emptySlots = Array(totalSlots).fill(null) as Array<string | null>;
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return { slots: emptySlots, hasPersistedSlots: false };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(TERMINAL_SLOTS_STORAGE_KEY);
|
||||||
|
if (stored === null) return { slots: emptySlots, hasPersistedSlots: false };
|
||||||
|
const parsed = JSON.parse(stored);
|
||||||
|
if (!Array.isArray(parsed)) return { slots: emptySlots, hasPersistedSlots: true };
|
||||||
|
const normalized = parsed.map((value) => {
|
||||||
|
const text = String(value || "").toLowerCase().trim();
|
||||||
|
return text || null;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
slots: normalizeSlotList(normalized, totalSlots),
|
||||||
|
hasPersistedSlots: true,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { slots: emptySlots, hasPersistedSlots: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldAutofillInitialSlots({
|
||||||
|
filteredRegionRows,
|
||||||
|
hasPersistedSlots,
|
||||||
|
slots,
|
||||||
|
}: {
|
||||||
|
filteredRegionRows: ScanOpportunityRow[];
|
||||||
|
hasPersistedSlots: boolean;
|
||||||
|
slots: Array<string | null>;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
!hasPersistedSlots &&
|
||||||
|
filteredRegionRows.length > 0 &&
|
||||||
|
slots.every((slot) => slot === null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function t(key: keyof typeof TERM, isEn: boolean) {
|
function t(key: keyof typeof TERM, isEn: boolean) {
|
||||||
return isEn ? TERM[key].en : TERM[key].zh;
|
return isEn ? TERM[key].en : TERM[key].zh;
|
||||||
}
|
}
|
||||||
@@ -662,19 +702,11 @@ function PolyWeatherTerminal({
|
|||||||
|
|
||||||
const totalSlots = getSlotCount(gridCols, gridRows);
|
const totalSlots = getSlotCount(gridCols, gridRows);
|
||||||
|
|
||||||
|
const hasPersistedSlots = useRef(false);
|
||||||
const [slots, setSlots] = useState<Array<string | null>>(() => {
|
const [slots, setSlots] = useState<Array<string | null>>(() => {
|
||||||
if (typeof window !== "undefined") {
|
const stored = readStoredTerminalSlots();
|
||||||
try {
|
hasPersistedSlots.current = stored.hasPersistedSlots;
|
||||||
const stored = localStorage.getItem("polyweather_terminal_slots");
|
return stored.slots;
|
||||||
if (stored) {
|
|
||||||
const parsed = JSON.parse(stored);
|
|
||||||
if (Array.isArray(parsed)) {
|
|
||||||
return normalizeSlotList(parsed, MAX_TERMINAL_CHARTS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
return Array(MAX_TERMINAL_CHARTS).fill(null);
|
|
||||||
});
|
});
|
||||||
const [activeSlotIndex, setActiveSlotIndex] = useState<number>(0);
|
const [activeSlotIndex, setActiveSlotIndex] = useState<number>(0);
|
||||||
const [maximizedSlotIndex, setMaximizedSlotIndex] = useState<number | null>(null);
|
const [maximizedSlotIndex, setMaximizedSlotIndex] = useState<number | null>(null);
|
||||||
@@ -758,13 +790,20 @@ function PolyWeatherTerminal({
|
|||||||
}, [rows, selectedRegionKey]);
|
}, [rows, selectedRegionKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filteredRegionRows.length && slots.every((s) => s === null)) {
|
if (
|
||||||
|
shouldAutofillInitialSlots({
|
||||||
|
filteredRegionRows,
|
||||||
|
hasPersistedSlots: hasPersistedSlots.current,
|
||||||
|
slots,
|
||||||
|
})
|
||||||
|
) {
|
||||||
const next = Array(MAX_TERMINAL_CHARTS)
|
const next = Array(MAX_TERMINAL_CHARTS)
|
||||||
.fill(null)
|
.fill(null)
|
||||||
.map((_, idx) => filteredRegionRows[idx]?.city || null);
|
.map((_, idx) => filteredRegionRows[idx]?.city || null);
|
||||||
setSlots(next);
|
setSlots(next);
|
||||||
|
hasPersistedSlots.current = true;
|
||||||
try {
|
try {
|
||||||
localStorage.setItem("polyweather_terminal_slots", JSON.stringify(next));
|
localStorage.setItem(TERMINAL_SLOTS_STORAGE_KEY, JSON.stringify(next));
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
}, [filteredRegionRows, slots]);
|
}, [filteredRegionRows, slots]);
|
||||||
@@ -774,8 +813,9 @@ function PolyWeatherTerminal({
|
|||||||
const next = [...slots];
|
const next = [...slots];
|
||||||
next[index] = city;
|
next[index] = city;
|
||||||
setSlots(next);
|
setSlots(next);
|
||||||
|
hasPersistedSlots.current = true;
|
||||||
try {
|
try {
|
||||||
localStorage.setItem("polyweather_terminal_slots", JSON.stringify(next));
|
localStorage.setItem(TERMINAL_SLOTS_STORAGE_KEY, JSON.stringify(next));
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1008,7 +1048,6 @@ function PolyWeatherTerminal({
|
|||||||
}}
|
}}
|
||||||
onReportIssue={openChartFeedback}
|
onReportIssue={openChartFeedback}
|
||||||
isMaximized={true}
|
isMaximized={true}
|
||||||
disableClose={visibleSlots.filter(Boolean).length <= 1}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{activeSearchSlotIndex === maximizedSlotIndex && (
|
{activeSearchSlotIndex === maximizedSlotIndex && (
|
||||||
@@ -1097,7 +1136,6 @@ function PolyWeatherTerminal({
|
|||||||
}}
|
}}
|
||||||
onReportIssue={openChartFeedback}
|
onReportIssue={openChartFeedback}
|
||||||
isMaximized={false}
|
isMaximized={false}
|
||||||
disableClose={visibleSlots.filter(Boolean).length <= 1}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{activeSearchSlotIndex === slotIndex && (
|
{activeSearchSlotIndex === slotIndex && (
|
||||||
|
|||||||
@@ -57,6 +57,16 @@ export function runTests() {
|
|||||||
dashboardSource.includes("disableClose={true}"),
|
dashboardSource.includes("disableClose={true}"),
|
||||||
"mobile terminal should render one selected chart and suggest landscape for the full grid",
|
"mobile terminal should render one selected chart and suggest landscape for the full grid",
|
||||||
);
|
);
|
||||||
|
assert(
|
||||||
|
!dashboardSource.includes("disableClose={visibleSlots.filter(Boolean).length <= 1}"),
|
||||||
|
"desktop chart slots must allow clearing the last visible city so users can close a 1x1 chart",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
dashboardSource.includes("readStoredTerminalSlots") &&
|
||||||
|
dashboardSource.includes("hasPersistedSlots") &&
|
||||||
|
dashboardSource.includes("shouldAutofillInitialSlots"),
|
||||||
|
"terminal slots must distinguish first-load default fill from a user-cleared all-empty layout",
|
||||||
|
);
|
||||||
assert(
|
assert(
|
||||||
selectorSource.includes("[1, 2, 3].map") &&
|
selectorSource.includes("[1, 2, 3].map") &&
|
||||||
selectorSource.includes("grid grid-cols-3"),
|
selectorSource.includes("grid grid-cols-3"),
|
||||||
|
|||||||
Reference in New Issue
Block a user