Adds proper tree items to the sidebar for the workspace items, fixes some minor bugs related to the window detachment logic, etc

This commit is contained in:
litoral05
2026-06-09 17:24:13 +01:00
parent 8f3c9d6da8
commit 1ef2fcb97c
14 changed files with 891 additions and 266 deletions
+58 -4
View File
@@ -15,6 +15,7 @@ import { useAuth } from "../features/auth/AuthContext";
import { LoginPage } from "../features/auth/pages/LoginPage"; import { LoginPage } from "../features/auth/pages/LoginPage";
import { RuntimeConfigProvider } from "../features/system/RuntimeConfigProvider"; import { RuntimeConfigProvider } from "../features/system/RuntimeConfigProvider";
import { TitleBar } from "../components/window/TitleBar"; import { TitleBar } from "../components/window/TitleBar";
import type { ChartWorkspaceScope } from "../features/chartworkspace/types";
export type AppPage = export type AppPage =
| "dashboard" | "dashboard"
@@ -37,6 +38,30 @@ export type AppPage =
function App() { function App() {
const { authenticated } = useAuth(); const { authenticated } = useAuth();
const [activePage, setActivePage] = useState<AppPage>("dashboard"); const [activePage, setActivePage] = useState<AppPage>("dashboard");
const [selectedWorkspaceByScope, setSelectedWorkspaceByScope] = useState<
Partial<Record<ChartWorkspaceScope, number | null>>
>({});
const openWorkspace = (scope: ChartWorkspaceScope, workspaceId: number) => {
setSelectedWorkspaceByScope((current) => ({
...current,
[scope]: workspaceId,
}));
if (scope === "GLOBAL") setActivePage("maincharts");
if (scope === "METEO") setActivePage("meteoCharts");
if (scope === "CLIMATE") setActivePage("climateCharts");
};
const updateSelectedWorkspace = (
scope: ChartWorkspaceScope,
workspaceId: number | null,
) => {
setSelectedWorkspaceByScope((current) => ({
...current,
[scope]: workspaceId,
}));
};
const isWorkspaceWindow = window.location.pathname.startsWith("/workspace-window/"); const isWorkspaceWindow = window.location.pathname.startsWith("/workspace-window/");
@@ -72,7 +97,12 @@ function App() {
<div className="pt-9"> <div className="pt-9">
<div className="h-[calc(100vh-36px)] overflow-hidden"> <div className="h-[calc(100vh-36px)] overflow-hidden">
<RuntimeConfigProvider> <RuntimeConfigProvider>
<AppShell activePage={activePage} onNavigate={setActivePage}> <AppShell
activePage={activePage}
selectedWorkspaceByScope={selectedWorkspaceByScope}
onNavigate={setActivePage}
onOpenWorkspace={openWorkspace}
>
{({ theme }) => { {({ theme }) => {
if (activePage === "meteo") { if (activePage === "meteo") {
return ( return (
@@ -84,17 +114,41 @@ function App() {
} }
if (activePage === "meteoCharts") { if (activePage === "meteoCharts") {
return <MeteoChartsPage theme={theme} />; return (
<MeteoChartsPage
theme={theme}
workspaceId={selectedWorkspaceByScope.METEO}
onWorkspaceChange={(workspaceId) =>
updateSelectedWorkspace("METEO", workspaceId)
}
/>
);
} }
if (activePage === "climateCharts") { if (activePage === "climateCharts") {
return <ClimateChartsPage theme={theme} />; return (
<ClimateChartsPage
theme={theme}
workspaceId={selectedWorkspaceByScope.CLIMATE}
onWorkspaceChange={(workspaceId) =>
updateSelectedWorkspace("CLIMATE", workspaceId)
}
/>
);
} }
if (activePage === "console") return <ConsolePage theme={theme} />; if (activePage === "console") return <ConsolePage theme={theme} />;
if (activePage === "maincharts") { if (activePage === "maincharts") {
return <MainChartsPage theme={theme} />; return (
<MainChartsPage
theme={theme}
workspaceId={selectedWorkspaceByScope.GLOBAL}
onWorkspaceChange={(workspaceId) =>
updateSelectedWorkspace("GLOBAL", workspaceId)
}
/>
);
} }
if (activePage === "settings") { if (activePage === "settings") {
+12 -1
View File
@@ -4,6 +4,7 @@ import { useTelemetryStream } from "../../features/telemetry/hooks/useTelemetryS
import { useCurrentUser } from "../../features/auth/hooks/useCurrentUser"; import { useCurrentUser } from "../../features/auth/hooks/useCurrentUser";
import type { TelemetrySnapshot } from "../../types/telemetry"; import type { TelemetrySnapshot } from "../../types/telemetry";
import type { AppPage } from "../../app/App"; import type { AppPage } from "../../app/App";
import type { ChartWorkspaceScope } from "../../features/chartworkspace/types";
type Theme = "dark" | "light"; type Theme = "dark" | "light";
@@ -14,13 +15,21 @@ type AppShellRenderProps = {
type AppShellProps = { type AppShellProps = {
activePage: AppPage; activePage: AppPage;
selectedWorkspaceByScope: Partial<Record<ChartWorkspaceScope, number | null>>;
onNavigate: (page: AppPage) => void; onNavigate: (page: AppPage) => void;
onOpenWorkspace: (scope: ChartWorkspaceScope, workspaceId: number) => void;
children: (props: AppShellRenderProps) => ReactNode; children: (props: AppShellRenderProps) => ReactNode;
}; };
const THEME_STORAGE_KEY = "app-theme"; const THEME_STORAGE_KEY = "app-theme";
export function AppShell({ activePage, onNavigate, children }: AppShellProps) { export function AppShell({
activePage,
selectedWorkspaceByScope,
onNavigate,
onOpenWorkspace,
children,
}: AppShellProps) {
const telemetry = useTelemetryStream(); const telemetry = useTelemetryStream();
const currentUser = useCurrentUser(); const currentUser = useCurrentUser();
@@ -107,7 +116,9 @@ export function AppShell({ activePage, onNavigate, children }: AppShellProps) {
userInitials={currentUser.initials} userInitials={currentUser.initials}
userName={currentUser.name} userName={currentUser.name}
userRole={currentUser.role} userRole={currentUser.role}
selectedWorkspaceByScope={selectedWorkspaceByScope}
onNavigate={onNavigate} onNavigate={onNavigate}
onOpenWorkspace={onOpenWorkspace}
onToggleCollapsed={() => setSidebarCollapsed((current) => !current)} onToggleCollapsed={() => setSidebarCollapsed((current) => !current)}
onToggleTheme={toggleTheme} onToggleTheme={toggleTheme}
/> />
+323 -18
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { import {
BarChart3, BarChart3,
ChevronDown, ChevronDown,
@@ -26,6 +26,11 @@ import {
import logo from "../../assets/logo5.png"; import logo from "../../assets/logo5.png";
import type { AppPage } from "../../app/App"; import type { AppPage } from "../../app/App";
import { useAuth } from "../../features/auth/AuthContext"; import { useAuth } from "../../features/auth/AuthContext";
import {
listChartWorkspaces,
type ChartWorkspaceResponse,
} from "../../features/chartworkspace/api/chartWorkspaceApi";
import type { ChartWorkspaceScope } from "../../features/chartworkspace/types";
type SidebarProps = { type SidebarProps = {
theme: "dark" | "light"; theme: "dark" | "light";
@@ -34,7 +39,9 @@ type SidebarProps = {
userInitials: string; userInitials: string;
userName: string; userName: string;
userRole: string; userRole: string;
selectedWorkspaceByScope: Partial<Record<ChartWorkspaceScope, number | null>>;
onNavigate: (page: AppPage) => void; onNavigate: (page: AppPage) => void;
onOpenWorkspace: (scope: ChartWorkspaceScope, workspaceId: number) => void;
onToggleCollapsed: () => void; onToggleCollapsed: () => void;
onToggleTheme: () => void; onToggleTheme: () => void;
}; };
@@ -82,7 +89,9 @@ export function Sidebar({
userInitials, userInitials,
userName, userName,
userRole, userRole,
selectedWorkspaceByScope,
onNavigate, onNavigate,
onOpenWorkspace,
onToggleCollapsed, onToggleCollapsed,
onToggleTheme, onToggleTheme,
}: SidebarProps) { }: SidebarProps) {
@@ -90,16 +99,64 @@ export function Sidebar({
const isDark = theme === "dark"; const isDark = theme === "dark";
const ThemeIcon = isDark ? Moon : Sun; const ThemeIcon = isDark ? Moon : Sun;
const [generalOpen, setGeneralOpen] = useState(true);
const [meteoOpen, setMeteoOpen] = useState(true); const [meteoOpen, setMeteoOpen] = useState(true);
const [climateOpen, setClimateOpen] = useState(false); const [climateOpen, setClimateOpen] = useState(false);
const [irrigationOpen, setIrrigationOpen] = useState(false); const [irrigationOpen, setIrrigationOpen] = useState(false);
const [activeTreeItem, setActiveTreeItem] = useState<string | null>(null); const [activeTreeItem, setActiveTreeItem] = useState<string | null>(null);
const [userMenuOpen, setUserMenuOpen] = useState(false); const [userMenuOpen, setUserMenuOpen] = useState(false);
const [sidebarHovered, setSidebarHovered] = useState(false); const [sidebarHovered, setSidebarHovered] = useState(false);
const [workspaceLists, setWorkspaceLists] = useState<
Partial<Record<ChartWorkspaceScope, ChartWorkspaceResponse[]>>
>({});
const userButtonRef = useRef<HTMLButtonElement | null>(null); const userButtonRef = useRef<HTMLButtonElement | null>(null);
const userMenuRef = useRef<HTMLDivElement | null>(null); const userMenuRef = useRef<HTMLDivElement | null>(null);
const refreshWorkspaces = useCallback(async (scope?: ChartWorkspaceScope) => {
const scopes: ChartWorkspaceScope[] = scope
? [scope]
: ["GLOBAL", "METEO", "CLIMATE"];
const loadedEntries = await Promise.all(
scopes.map(async (workspaceScope) => {
try {
return [workspaceScope, await listChartWorkspaces(workspaceScope)] as const;
} catch (error) {
console.error(`Failed to load sidebar workspaces for ${workspaceScope}`, error);
return [workspaceScope, [] as ChartWorkspaceResponse[]] as const;
}
}),
);
setWorkspaceLists((current) => {
const next = { ...current };
for (const [workspaceScope, workspaces] of loadedEntries) {
next[workspaceScope] = workspaces;
}
return next;
});
}, []);
useEffect(() => {
void refreshWorkspaces();
}, [refreshWorkspaces]);
useEffect(() => {
const handleWorkspaceChange = (event: Event) => {
const detail = (event as CustomEvent<{ scope?: ChartWorkspaceScope }>).detail;
void refreshWorkspaces(detail?.scope);
};
window.addEventListener("chart-workspaces:changed", handleWorkspaceChange);
return () => {
window.removeEventListener("chart-workspaces:changed", handleWorkspaceChange);
};
}, [refreshWorkspaces]);
useEffect(() => { useEffect(() => {
if (!userMenuOpen) return; if (!userMenuOpen) return;
@@ -149,6 +206,7 @@ export function Sidebar({
const firstItem = sectionItems[section][0]; const firstItem = sectionItems[section][0];
setActiveTreeItem(`${section}:${firstItem.label}`); setActiveTreeItem(`${section}:${firstItem.label}`);
setGeneralOpen(false);
if (firstItem.page) { if (firstItem.page) {
onNavigate(firstItem.page); onNavigate(firstItem.page);
@@ -288,13 +346,30 @@ export function Sidebar({
theme={theme} theme={theme}
collapsed={collapsed} collapsed={collapsed}
label="Gráficos Gerais" label="Gráficos Gerais"
page="maincharts"
icon={BarChart3} icon={BarChart3}
activePage={activePage} open={generalOpen}
activeTreeItem={activeTreeItem} workspaces={workspaceLists.GLOBAL ?? []}
onNavigate={(page) => { active={activePage === "maincharts"}
selectedWorkspaceId={selectedWorkspaceByScope.GLOBAL ?? null}
onToggle={() => {
if (collapsed) {
onToggleCollapsed();
}
setActiveTreeItem(null); setActiveTreeItem(null);
onNavigate(page); setGeneralOpen((current) => !current);
setMeteoOpen(false);
setClimateOpen(false);
setIrrigationOpen(false);
onNavigate("maincharts");
}}
onOpenWorkspace={(workspaceId) => {
setActiveTreeItem(null);
setGeneralOpen(true);
setMeteoOpen(false);
setClimateOpen(false);
setIrrigationOpen(false);
onOpenWorkspace("GLOBAL", workspaceId);
}} }}
/> />
@@ -325,7 +400,21 @@ export function Sidebar({
sectionKey="meteo" sectionKey="meteo"
activeTreeItem={activeTreeItem} activeTreeItem={activeTreeItem}
activePage={activePage} activePage={activePage}
workspaceGroup={{
scope: "METEO",
page: "meteoCharts",
workspaces: workspaceLists.METEO ?? [],
selectedWorkspaceId: selectedWorkspaceByScope.METEO ?? null,
}}
onItemClick={handleTreeClick} onItemClick={handleTreeClick}
onWorkspaceClick={(scope, workspaceId) => {
setActiveTreeItem(null);
setGeneralOpen(false);
setMeteoOpen(true);
setClimateOpen(false);
setIrrigationOpen(false);
onOpenWorkspace(scope, workspaceId);
}}
/> />
<TreeSection <TreeSection
@@ -339,7 +428,21 @@ export function Sidebar({
sectionKey="climate" sectionKey="climate"
activeTreeItem={activeTreeItem} activeTreeItem={activeTreeItem}
activePage={activePage} activePage={activePage}
workspaceGroup={{
scope: "CLIMATE",
page: "climateCharts",
workspaces: workspaceLists.CLIMATE ?? [],
selectedWorkspaceId: selectedWorkspaceByScope.CLIMATE ?? null,
}}
onItemClick={handleTreeClick} onItemClick={handleTreeClick}
onWorkspaceClick={(scope, workspaceId) => {
setActiveTreeItem(null);
setGeneralOpen(false);
setMeteoOpen(false);
setClimateOpen(true);
setIrrigationOpen(false);
onOpenWorkspace(scope, workspaceId);
}}
/> />
<TreeSection <TreeSection
@@ -594,7 +697,9 @@ function TreeSection({
sectionKey, sectionKey,
activeTreeItem, activeTreeItem,
activePage, activePage,
workspaceGroup,
onItemClick, onItemClick,
onWorkspaceClick,
}: { }: {
theme: "dark" | "light"; theme: "dark" | "light";
collapsed: boolean; collapsed: boolean;
@@ -606,7 +711,14 @@ function TreeSection({
sectionKey: string; sectionKey: string;
activeTreeItem: string | null; activeTreeItem: string | null;
activePage: AppPage; activePage: AppPage;
workspaceGroup?: {
scope: ChartWorkspaceScope;
page: AppPage;
workspaces: ChartWorkspaceResponse[];
selectedWorkspaceId: number | null;
};
onItemClick: (key: string, page?: AppPage) => void; onItemClick: (key: string, page?: AppPage) => void;
onWorkspaceClick?: (scope: ChartWorkspaceScope, workspaceId: number) => void;
}) { }) {
const isDark = theme === "dark"; const isDark = theme === "dark";
const hasActiveChild = items.some((item) => { const hasActiveChild = items.some((item) => {
@@ -659,10 +771,14 @@ function TreeSection({
const active = const active =
activeTreeItem === key || activeTreeItem === key ||
Boolean(item.page && activePage === item.page); Boolean(item.page && activePage === item.page);
const showWorkspaces =
workspaceGroup &&
item.page === workspaceGroup.page &&
workspaceGroup.workspaces.length > 0;
return ( return (
<div key={item.label}>
<button <button
key={item.label}
type="button" type="button"
onClick={() => onItemClick(key, item.page)} onClick={() => onItemClick(key, item.page)}
className={ className={
@@ -687,6 +803,20 @@ function TreeSection({
<span className="truncate">{item.label}</span> <span className="truncate">{item.label}</span>
</button> </button>
{showWorkspaces && (
<WorkspaceTreeList
theme={theme}
workspaces={workspaceGroup.workspaces}
activePage={activePage}
page={workspaceGroup.page}
selectedWorkspaceId={workspaceGroup.selectedWorkspaceId}
onOpenWorkspace={(workspaceId) =>
onWorkspaceClick?.(workspaceGroup.scope, workspaceId)
}
/>
)}
</div>
); );
})} })}
</div> </div>
@@ -715,6 +845,160 @@ function SectionLabel({
); );
} }
function WorkspaceNavSection({
theme,
collapsed,
label,
icon: Icon,
open,
workspaces,
active,
selectedWorkspaceId,
onToggle,
onOpenWorkspace,
}: {
theme: "dark" | "light";
collapsed: boolean;
label: string;
icon: React.ElementType;
open: boolean;
workspaces: ChartWorkspaceResponse[];
active: boolean;
selectedWorkspaceId: number | null;
onToggle: () => void;
onOpenWorkspace: (workspaceId: number) => void;
}) {
const isDark = theme === "dark";
return (
<div>
<CollapsedTooltipWrapper
collapsed={collapsed}
label={label}
isDark={isDark}
>
<button
type="button"
onClick={onToggle}
title={undefined}
className={navButtonClass(isDark, active, collapsed)}
>
{active && <ActiveIndicator isDark={isDark} />}
<Icon className={navIconClass(isDark, active)} />
{!collapsed && (
<>
<span className="min-w-0 flex-1 truncate">{label}</span>
<ChevronDown
className={`h-4 w-4 shrink-0 text-[#6F819B] transition-transform duration-200 ${open ? "rotate-180" : ""
}`}
/>
</>
)}
</button>
</CollapsedTooltipWrapper>
{!collapsed && open && (
<WorkspaceTreeList
theme={theme}
workspaces={workspaces}
activePage={active ? "maincharts" : "dashboard"}
page="maincharts"
selectedWorkspaceId={selectedWorkspaceId}
onOpenWorkspace={onOpenWorkspace}
/>
)}
</div>
);
}
function WorkspaceTreeList({
theme,
workspaces,
activePage,
page,
selectedWorkspaceId,
onOpenWorkspace,
}: {
theme: "dark" | "light";
workspaces: ChartWorkspaceResponse[];
activePage: AppPage;
page: AppPage;
selectedWorkspaceId: number | null;
onOpenWorkspace: (workspaceId: number) => void;
}) {
const isDark = theme === "dark";
return (
<div
className={
isDark
? "ml-[18px] mt-1 space-y-1 border-l border-[#263247] pl-3"
: "ml-[18px] mt-1 space-y-1 border-l border-[#CBD5E1] pl-3"
}
>
{workspaces.map((workspace) => {
const active =
activePage === page && workspace.id === selectedWorkspaceId;
const chartCount = getWorkspaceChartCount(workspace);
return (
<button
key={workspace.id}
type="button"
onClick={() => onOpenWorkspace(workspace.id)}
className={
active
? isDark
? "group flex w-full cursor-pointer items-center gap-2 rounded-[5px] border border-[#2C3D56] bg-[#101D30] px-3 py-2 text-left text-[12px] font-black text-white"
: "group flex w-full cursor-pointer items-center gap-2 rounded-[5px] border border-[#CBD5E1] bg-white px-3 py-2 text-left text-[12px] font-black text-[#0F172A]"
: isDark
? "group flex w-full cursor-pointer items-center gap-2 rounded-[5px] px-3 py-2 text-left text-[12px] font-bold text-[#7D8EA8] transition hover:bg-[#111A2B] hover:text-slate-200"
: "group flex w-full cursor-pointer items-center gap-2 rounded-[5px] px-3 py-2 text-left text-[12px] font-bold text-slate-500 transition hover:bg-white hover:text-[#0F172A]"
}
>
<span
className={
active
? "h-2 w-2 shrink-0 rounded-full bg-[#4FD1C5]"
: "h-2 w-2 shrink-0 rounded-full bg-[#61738C] group-hover:bg-[#4FD1C5]"
}
/>
<span className="min-w-0 flex-1 truncate">{workspace.name}</span>
<span className="shrink-0 text-[10px] font-black text-[#61738C]">
{chartCount}/10
</span>
</button>
);
})}
{workspaces.length === 0 && (
<div
className={
isDark
? "rounded-[5px] px-3 py-2 text-[12px] font-bold text-[#61738C]"
: "rounded-[5px] px-3 py-2 text-[12px] font-bold text-slate-400"
}
>
Sem workspaces.
</div>
)}
</div>
);
}
function getWorkspaceChartCount(workspace: ChartWorkspaceResponse) {
try {
const charts = JSON.parse(workspace.chartsJson) as unknown[];
return Array.isArray(charts) ? charts.length : 0;
} catch {
return 0;
}
}
function ActiveIndicator({ isDark }: { isDark: boolean }) { function ActiveIndicator({ isDark }: { isDark: boolean }) {
return ( return (
<span <span
@@ -791,16 +1075,7 @@ function Divider({ isDark }: { isDark: boolean }) {
); );
} }
function NavItem({ type StandardNavItemProps = {
theme,
collapsed,
label,
page,
icon: Icon,
activePage,
activeTreeItem,
onNavigate,
}: {
theme: "dark" | "light"; theme: "dark" | "light";
collapsed: boolean; collapsed: boolean;
label: string; label: string;
@@ -809,7 +1084,37 @@ function NavItem({
activePage: AppPage; activePage: AppPage;
activeTreeItem: string | null; activeTreeItem: string | null;
onNavigate: (page: AppPage) => void; onNavigate: (page: AppPage) => void;
}) { };
type WorkspaceNavItemProps = {
theme: "dark" | "light";
collapsed: boolean;
label: string;
icon: React.ElementType;
open: boolean;
workspaces: ChartWorkspaceResponse[];
active: boolean;
selectedWorkspaceId: number | null;
onToggle: () => void;
onOpenWorkspace: (workspaceId: number) => void;
};
function NavItem(props: StandardNavItemProps | WorkspaceNavItemProps) {
if ("open" in props) {
return <WorkspaceNavSection {...props} />;
}
const {
theme,
collapsed,
label,
page,
icon: Icon,
activePage,
activeTreeItem,
onNavigate,
} = props;
const isDark = theme === "dark"; const isDark = theme === "dark";
const active = activePage === page && activeTreeItem === null; const active = activePage === page && activeTreeItem === null;
+2 -2
View File
@@ -43,9 +43,9 @@ export function LoginPage() {
/> />
<div> <div>
<p className="text-xs font-black uppercase tracking-[0.22em] text-[#4FD1C5]"> <p className="text-xs font-black uppercase tracking-[0.22em] text-[#4FD1C5]">
Litoral Regas Central LRX
</p> </p>
<h1 className="mt-1 text-2xl font-black">Iniciar sessao</h1> <h1 className="mt-1 text-2xl font-black">Iniciar sessão</h1>
</div> </div>
</div> </div>
@@ -20,8 +20,8 @@ export type ChartWorkspaceSaveRequest = {
name?: string; name?: string;
sortOrder?: number; sortOrder?: number;
defaultWorkspace?: boolean; defaultWorkspace?: boolean;
layoutMode: ChartLayoutMode; layoutMode?: ChartLayoutMode;
chartsJson: string; chartsJson?: string;
}; };
function workspaceUrl(workspaceId: number | null, scope: ChartWorkspaceScope) { function workspaceUrl(workspaceId: number | null, scope: ChartWorkspaceScope) {
@@ -80,6 +80,7 @@ export function EmptyWorkspace({
description = "Abra um gráfico guardado ou crie um novo gráfico para continuar.", description = "Abra um gráfico guardado ou crie um novo gráfico para continuar.",
addLabel = "Novo Gráfico", addLabel = "Novo Gráfico",
savedLabel = "Abrir Guardado", savedLabel = "Abrir Guardado",
showSavedButton = true,
minHeightClass = "min-h-0 flex-1", minHeightClass = "min-h-0 flex-1",
onAddChart, onAddChart,
onOpenSaved, onOpenSaved,
@@ -90,6 +91,7 @@ export function EmptyWorkspace({
description?: string; description?: string;
addLabel?: string; addLabel?: string;
savedLabel?: string; savedLabel?: string;
showSavedButton?: boolean;
minHeightClass?: string; minHeightClass?: string;
onAddChart: () => void; onAddChart: () => void;
onOpenSaved: () => void; onOpenSaved: () => void;
@@ -150,6 +152,7 @@ export function EmptyWorkspace({
{addLabel} {addLabel}
</button> </button>
{showSavedButton && (
<button <button
type="button" type="button"
onClick={onOpenSaved} onClick={onOpenSaved}
@@ -162,6 +165,7 @@ export function EmptyWorkspace({
<Search className="h-5 w-5" /> <Search className="h-5 w-5" />
{savedLabel} {savedLabel}
</button> </button>
)}
</div> </div>
</div> </div>
</section> </section>
@@ -189,7 +193,7 @@ export function WorkspaceSelector({
creating: boolean; creating: boolean;
canCreateWorkspace: boolean; canCreateWorkspace: boolean;
onSelectWorkspace: (workspaceId: number) => void; onSelectWorkspace: (workspaceId: number) => void;
onCreateWorkspace: (name: string) => void; onCreateWorkspace: (name: string) => void | Promise<void>;
onRenameWorkspace: (workspaceId: number, name: string) => void | Promise<void>; onRenameWorkspace: (workspaceId: number, name: string) => void | Promise<void>;
onDeleteWorkspace: (workspaceId: number) => void | Promise<void>; onDeleteWorkspace: (workspaceId: number) => void | Promise<void>;
}) { }) {
@@ -208,7 +212,7 @@ export function WorkspaceSelector({
); );
const createWorkspace = (name: string) => { const createWorkspace = (name: string) => {
onCreateWorkspace(name); void onCreateWorkspace(name);
setCreateOpen(false); setCreateOpen(false);
setOpen(false); setOpen(false);
}; };
@@ -514,7 +518,7 @@ function WorkspaceNameModal({
); );
} }
function NewWorkspaceModal({ export function NewWorkspaceModal({
theme, theme,
creating, creating,
onClose, onClose,
@@ -44,7 +44,7 @@ type UseChartWorkspacePersistenceParams = {
export function useChartWorkspacePersistence({ export function useChartWorkspacePersistence({
scope, scope,
workspaceId = null, workspaceId,
layoutMode, layoutMode,
charts, charts,
onLoaded, onLoaded,
@@ -58,7 +58,11 @@ export function useChartWorkspacePersistence({
const loadedWorkspaceKeyRef = useRef<string | null>(null); const loadedWorkspaceKeyRef = useRef<string | null>(null);
const workspaceKey = const workspaceKey =
workspaceId === null ? `scope:${scope}` : `id:${workspaceId}`; workspaceId === undefined
? `scope:${scope}`
: workspaceId === null
? `none:${scope}`
: `id:${workspaceId}`;
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@@ -70,8 +74,17 @@ export function useChartWorkspacePersistence({
loadedWorkspaceKeyRef.current = null; loadedWorkspaceKeyRef.current = null;
try { try {
if (workspaceId === null) {
if (!cancelled) {
loadedWorkspaceKeyRef.current = workspaceKey;
setLoaded(true);
}
return;
}
const payload = const payload =
await loadChartWorkspace(scope, workspaceId); await loadChartWorkspace(scope, workspaceId ?? null);
if (!payload) { if (!payload) {
return; return;
@@ -110,6 +123,7 @@ export function useChartWorkspacePersistence({
useEffect(() => { useEffect(() => {
if (!loaded || !saveEnabled) return; if (!loaded || !saveEnabled) return;
if (workspaceId === null) return;
if (loadedWorkspaceKeyRef.current !== workspaceKey) return; if (loadedWorkspaceKeyRef.current !== workspaceKey) return;
if (saveTimeoutRef.current !== null) { if (saveTimeoutRef.current !== null) {
@@ -127,7 +141,13 @@ export function useChartWorkspacePersistence({
layoutMode, layoutMode,
chartsJson: JSON.stringify(charts), chartsJson: JSON.stringify(charts),
}, },
workspaceId, workspaceId ?? null,
);
window.dispatchEvent(
new CustomEvent("chart-workspaces:changed", {
detail: { scope, workspaceId },
}),
); );
setError(null); setError(null);
@@ -74,11 +74,7 @@ export function useChartWorkspaceSelection({
await saveChartWorkspace( await saveChartWorkspace(
scope, scope,
{ {
name: workspace.name,
sortOrder: workspace.sortOrder,
defaultWorkspace: true, defaultWorkspace: true,
layoutMode: workspace.layoutMode,
chartsJson: workspace.chartsJson,
}, },
workspace.id, workspace.id,
); );
@@ -97,7 +93,7 @@ export function useChartWorkspaceSelection({
}, [scope, workspaces]); }, [scope, workspaces]);
const createWorkspace = useCallback(async (name: string) => { const createWorkspace = useCallback(async (name: string) => {
if (workspaces.length >= 10 || creating) return; if (workspaces.length >= 10 || creating) return null;
try { try {
setCreating(true); setCreating(true);
@@ -120,9 +116,11 @@ export function useChartWorkspaceSelection({
]); ]);
setActiveWorkspaceId(createdWorkspace.id); setActiveWorkspaceId(createdWorkspace.id);
setError(null); setError(null);
return createdWorkspace;
} catch (exception) { } catch (exception) {
console.error("Failed to create chart workspace", exception); console.error("Failed to create chart workspace", exception);
setError("Nao foi possivel criar o workspace."); setError("Nao foi possivel criar o workspace.");
return null;
} finally { } finally {
setCreating(false); setCreating(false);
} }
@@ -142,26 +140,7 @@ export function useChartWorkspaceSelection({
setWorkspaces(remaining); setWorkspaces(remaining);
if (activeWorkspaceId === workspaceId) { if (activeWorkspaceId === workspaceId) {
const nextWorkspace = setActiveWorkspaceId(null);
remaining.find((item) => item.defaultWorkspace) ??
remaining[0] ??
null;
setActiveWorkspaceId(nextWorkspace?.id ?? null);
if (nextWorkspace) {
await saveChartWorkspace(
scope,
{
name: nextWorkspace.name,
sortOrder: nextWorkspace.sortOrder,
defaultWorkspace: true,
layoutMode: nextWorkspace.layoutMode,
chartsJson: nextWorkspace.chartsJson,
},
nextWorkspace.id,
);
}
} }
setError(null); setError(null);
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { emit, listen } from "@tauri-apps/api/event"; import { emit, listen } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import { Minus, Square, X } from "lucide-react"; import { Minus, Square, X } from "lucide-react";
@@ -30,6 +30,21 @@ export function WorkspaceWindowPage({ theme }: WorkspaceWindowPageProps) {
const currentWindow = getCurrentWindow(); const currentWindow = getCurrentWindow();
const { scope, workspaceId, title } = parseWorkspaceRoute(); const { scope, workspaceId, title } = parseWorkspaceRoute();
const [windowTitle, setWindowTitle] = useState(title); const [windowTitle, setWindowTitle] = useState(title);
const closingRef = useRef(false);
const closeWorkspaceWindow = useCallback(async () => {
if (closingRef.current) return;
closingRef.current = true;
if (workspaceId) {
await emit("workspace-window://closed", {
scope,
workspaceId,
});
}
await currentWindow.destroy();
}, [currentWindow, scope, workspaceId]);
useEffect(() => { useEffect(() => {
void currentWindow.setTitle(windowTitle); void currentWindow.setTitle(windowTitle);
@@ -57,19 +72,15 @@ export function WorkspaceWindowPage({ theme }: WorkspaceWindowPageProps) {
}, [scope, workspaceId]); }, [scope, workspaceId]);
useEffect(() => { useEffect(() => {
const unlistenPromise = currentWindow.onCloseRequested(() => { const unlistenPromise = currentWindow.onCloseRequested((event) => {
if (!workspaceId) return; event.preventDefault();
void closeWorkspaceWindow();
void emit("workspace-window://closed", {
scope,
workspaceId,
});
}); });
return () => { return () => {
void unlistenPromise.then((unlisten) => unlisten()); void unlistenPromise.then((unlisten) => unlisten());
}; };
}, [currentWindow, scope, workspaceId]); }, [closeWorkspaceWindow, currentWindow]);
const titleBarClass = isDark const titleBarClass = isDark
? "flex h-10 shrink-0 items-center border-b border-white/10 bg-[#071421] text-slate-100" ? "flex h-10 shrink-0 items-center border-b border-white/10 bg-[#071421] text-slate-100"
@@ -151,7 +162,7 @@ export function WorkspaceWindowPage({ theme }: WorkspaceWindowPageProps) {
<button <button
type="button" type="button"
title="Fechar" title="Fechar"
onClick={() => currentWindow.close()} onClick={() => void closeWorkspaceWindow()}
className={closeButtonClass} className={closeButtonClass}
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
+123 -29
View File
@@ -5,6 +5,7 @@ import {
EmptyWorkspace, EmptyWorkspace,
LayoutButton, LayoutButton,
ModeButton, ModeButton,
NewWorkspaceModal,
WorkspaceSelector, WorkspaceSelector,
} from "../../chartworkspace/components/ChartWorkspaceControls"; } from "../../chartworkspace/components/ChartWorkspaceControls";
import { openWorkspaceWindow } from "../../chartworkspace/utils/openWorkspaceWindow"; import { openWorkspaceWindow } from "../../chartworkspace/utils/openWorkspaceWindow";
@@ -57,6 +58,7 @@ type ClimateChartsPageProps = {
theme: "dark" | "light"; theme: "dark" | "light";
workspaceId?: number | null; workspaceId?: number | null;
workspaceWindow?: boolean; workspaceWindow?: boolean;
onWorkspaceChange?: (workspaceId: number | null) => void;
}; };
const RADIUS = "rounded-[6px]"; const RADIUS = "rounded-[6px]";
@@ -65,8 +67,9 @@ const INITIAL_CHARTS: ChartWorkspaceItem[] = [];
export function ClimateChartsPage({ export function ClimateChartsPage({
theme, theme,
workspaceId = null, workspaceId,
workspaceWindow = false, workspaceWindow = false,
onWorkspaceChange,
}: ClimateChartsPageProps) { }: ClimateChartsPageProps) {
const isDark = theme === "dark"; const isDark = theme === "dark";
@@ -79,6 +82,7 @@ export function ClimateChartsPage({
const [savedOpen, setSavedOpen] = useState(false); const [savedOpen, setSavedOpen] = useState(false);
const [movingChartId, setMovingChartId] = useState<string | null>(null); const [movingChartId, setMovingChartId] = useState<string | null>(null);
const [newChartOpen, setNewChartOpen] = useState(false); const [newChartOpen, setNewChartOpen] = useState(false);
const [newWorkspaceOpen, setNewWorkspaceOpen] = useState(false);
const [placingChartId, setPlacingChartId] = useState<string | null>(null); const [placingChartId, setPlacingChartId] = useState<string | null>(null);
const [detachedWorkspaceIds, setDetachedWorkspaceIds] = useState<Set<number>>(() => new Set()); const [detachedWorkspaceIds, setDetachedWorkspaceIds] = useState<Set<number>>(() => new Set());
@@ -87,7 +91,8 @@ export function ClimateChartsPage({
defaultName: "Workspace Clima", defaultName: "Workspace Clima",
}); });
const activeWorkspaceId = workspaceId ?? workspaceSelection.activeWorkspaceId; const activeWorkspaceId =
workspaceId !== undefined ? workspaceId : workspaceSelection.activeWorkspaceId;
const activeWorkspace = const activeWorkspace =
workspaceSelection.workspaces.find((workspace) => workspace.id === activeWorkspaceId) ?? workspaceSelection.workspaces.find((workspace) => workspace.id === activeWorkspaceId) ??
workspaceSelection.activeWorkspace; workspaceSelection.activeWorkspace;
@@ -96,26 +101,38 @@ export function ClimateChartsPage({
const workspaceWindowLabel = (workspaceId: number) => const workspaceWindowLabel = (workspaceId: number) =>
`workspace-climate-${workspaceId}`; `workspace-climate-${workspaceId}`;
const renameWorkspace = async (workspaceId: number, name: string) => { const notifyWorkspaceListChanged = () => {
const workspace = workspaceSelection.workspaces.find( window.dispatchEvent(
(item) => item.id === workspaceId, new CustomEvent("chart-workspaces:changed", {
detail: { scope: "CLIMATE" },
}),
); );
};
if (!workspace) return; const selectWorkspace = async (workspaceId: number) => {
await workspaceSelection.selectWorkspace(workspaceId);
onWorkspaceChange?.(workspaceId);
};
const createWorkspace = async (name: string) => {
const createdWorkspace = await workspaceSelection.createWorkspace(name);
if (createdWorkspace) {
onWorkspaceChange?.(createdWorkspace.id);
notifyWorkspaceListChanged();
}
};
const renameWorkspace = async (workspaceId: number, name: string) => {
await saveChartWorkspace( await saveChartWorkspace(
"CLIMATE", "CLIMATE",
{ { name },
name,
sortOrder: workspace.sortOrder,
defaultWorkspace: workspace.defaultWorkspace,
layoutMode: workspace.layoutMode,
chartsJson: workspace.chartsJson,
},
workspaceId, workspaceId,
); );
workspaceSelection.updateWorkspaceName(workspaceId, name); workspaceSelection.updateWorkspaceName(workspaceId, name);
notifyWorkspaceListChanged();
const existing = await WebviewWindow.getByLabel( const existing = await WebviewWindow.getByLabel(
workspaceWindowLabel(workspaceId), workspaceWindowLabel(workspaceId),
@@ -141,6 +158,13 @@ export function ClimateChartsPage({
}); });
await workspaceSelection.deleteWorkspace(workspaceId); await workspaceSelection.deleteWorkspace(workspaceId);
if (activeWorkspaceId === workspaceId) {
workspaceSelection.setActiveWorkspace(null);
onWorkspaceChange?.(null);
}
notifyWorkspaceListChanged();
}; };
const detachWorkspace = () => { const detachWorkspace = () => {
@@ -148,17 +172,8 @@ export function ClimateChartsPage({
setDetachedWorkspaceIds((current) => new Set(current).add(activeWorkspaceId)); setDetachedWorkspaceIds((current) => new Set(current).add(activeWorkspaceId));
const nextWorkspace = workspaceSelection.workspaces.find(
(workspace) =>
workspace.id !== activeWorkspaceId &&
!detachedWorkspaceIds.has(workspace.id),
);
if (nextWorkspace) {
void workspaceSelection.selectWorkspace(nextWorkspace.id);
} else {
workspaceSelection.setActiveWorkspace(null); workspaceSelection.setActiveWorkspace(null);
} onWorkspaceChange?.(null);
void openWorkspaceWindow( void openWorkspaceWindow(
activeWorkspaceId, activeWorkspaceId,
@@ -187,6 +202,54 @@ export function ClimateChartsPage({
}; };
}, []); }, []);
useEffect(() => {
if (workspaceWindow || workspaceSelection.loading) return;
let cancelled = false;
async function syncDetachedWorkspaceWindows() {
const nextDetachedWorkspaceIds = new Set<number>();
for (const workspace of workspaceSelection.workspaces) {
const existing = await WebviewWindow.getByLabel(
workspaceWindowLabel(workspace.id),
);
if (existing) {
nextDetachedWorkspaceIds.add(workspace.id);
}
}
if (cancelled) return;
setDetachedWorkspaceIds(nextDetachedWorkspaceIds);
const selectedWorkspaceId = activeWorkspaceId;
if (
selectedWorkspaceId !== null &&
nextDetachedWorkspaceIds.has(selectedWorkspaceId)
) {
workspaceSelection.setActiveWorkspace(null);
onWorkspaceChange?.(null);
}
}
void syncDetachedWorkspaceWindows();
return () => {
cancelled = true;
};
}, [
workspaceSelection.workspaces,
activeWorkspaceId,
workspaceSelection.loading,
workspaceSelection.selectWorkspace,
workspaceSelection.setActiveWorkspace,
workspaceWindow,
onWorkspaceChange,
]);
useEffect(() => { useEffect(() => {
setCharts([]); setCharts([]);
setConfigChartId(null); setConfigChartId(null);
@@ -231,7 +294,7 @@ export function ClimateChartsPage({
}, },
}); });
const canAddMoreCharts = charts.length < MAX_CHARTS; const canAddMoreCharts = activeWorkspaceId !== null && charts.length < MAX_CHARTS;
const visibleCharts = useMemo(() => { const visibleCharts = useMemo(() => {
const openCharts = charts.filter( const openCharts = charts.filter(
@@ -566,13 +629,13 @@ export function ClimateChartsPage({
<WorkspaceSelector <WorkspaceSelector
theme={theme} theme={theme}
workspaces={workspaceSelection.workspaces} workspaces={workspaceSelection.workspaces}
activeWorkspaceId={workspaceSelection.activeWorkspaceId} activeWorkspaceId={activeWorkspaceId}
detachedWorkspaceIds={detachedWorkspaceIds} detachedWorkspaceIds={detachedWorkspaceIds}
loading={workspaceSelection.loading} loading={workspaceSelection.loading}
creating={workspaceSelection.creating} creating={workspaceSelection.creating}
canCreateWorkspace={workspaceSelection.canCreateWorkspace} canCreateWorkspace={workspaceSelection.canCreateWorkspace}
onSelectWorkspace={workspaceSelection.selectWorkspace} onSelectWorkspace={(workspaceId) => void selectWorkspace(workspaceId)}
onCreateWorkspace={(name) => void workspaceSelection.createWorkspace(name)} onCreateWorkspace={createWorkspace}
onRenameWorkspace={(workspaceId, name) => void renameWorkspace(workspaceId, name)} onRenameWorkspace={(workspaceId, name) => void renameWorkspace(workspaceId, name)}
onDeleteWorkspace={(workspaceId) => void deleteWorkspace(workspaceId)} onDeleteWorkspace={(workspaceId) => void deleteWorkspace(workspaceId)}
/> />
@@ -705,11 +768,31 @@ export function ClimateChartsPage({
{visibleCharts.length === 0 ? ( {visibleCharts.length === 0 ? (
<EmptyWorkspace <EmptyWorkspace
theme={theme} theme={theme}
canAddMoreCharts={canAddMoreCharts} canAddMoreCharts={activeWorkspaceId === null || canAddMoreCharts}
title={activeWorkspaceId === null ? "Nenhum workspace aberto" : undefined}
description={
activeWorkspaceId === null
? "Crie um workspace novo ou abra um workspace guardado para continuar."
: undefined
}
addLabel={activeWorkspaceId === null ? "Novo Workspace" : "Novo Gráfico"}
savedLabel={activeWorkspaceId === null ? "Abrir Workspace" : "Abrir Guardado"}
showSavedButton={activeWorkspaceId !== null}
onAddChart={() => { onAddChart={() => {
if (activeWorkspaceId === null) {
setNewWorkspaceOpen(true);
return;
}
if (canAddMoreCharts) setNewChartOpen(true); if (canAddMoreCharts) setNewChartOpen(true);
}} }}
onOpenSaved={() => setSavedOpen(true)} onOpenSaved={() => {
if (activeWorkspaceId === null) {
return;
}
setSavedOpen(true);
}}
/> />
) : ( ) : (
<section className={layoutGridClass(layoutMode)}> <section className={layoutGridClass(layoutMode)}>
@@ -785,6 +868,17 @@ export function ClimateChartsPage({
/> />
)} )}
{newWorkspaceOpen && (
<NewWorkspaceModal
theme={theme}
creating={workspaceSelection.creating}
onClose={() => setNewWorkspaceOpen(false)}
onCreate={(name) => {
void createWorkspace(name).then(() => setNewWorkspaceOpen(false));
}}
/>
)}
{configChart && ( {configChart && (
<ChartConfigModal <ChartConfigModal
theme={theme} theme={theme}
+123 -62
View File
@@ -5,6 +5,7 @@ import {
EmptyWorkspace, EmptyWorkspace,
LayoutButton, LayoutButton,
ModeButton, ModeButton,
NewWorkspaceModal,
WorkspaceSelector, WorkspaceSelector,
} from "../../chartworkspace/components/ChartWorkspaceControls"; } from "../../chartworkspace/components/ChartWorkspaceControls";
import { openWorkspaceWindow } from "../../chartworkspace/utils/openWorkspaceWindow"; import { openWorkspaceWindow } from "../../chartworkspace/utils/openWorkspaceWindow";
@@ -13,7 +14,6 @@ import { saveChartWorkspace } from "../../chartworkspace/api/chartWorkspaceApi";
import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
import { import {
Cog, Cog,
Copy,
Maximize2, Maximize2,
AreaChart, AreaChart,
BarChart3, BarChart3,
@@ -57,6 +57,7 @@ type MainChartsPageProps = {
theme: "dark" | "light"; theme: "dark" | "light";
workspaceId?: number | null; workspaceId?: number | null;
workspaceWindow?: boolean; workspaceWindow?: boolean;
onWorkspaceChange?: (workspaceId: number | null) => void;
}; };
const RADIUS = "rounded-[6px]"; const RADIUS = "rounded-[6px]";
@@ -65,8 +66,9 @@ const INITIAL_CHARTS: ChartWorkspaceItem[] = [];
export function MainChartsPage({ export function MainChartsPage({
theme, theme,
workspaceId = null, workspaceId,
workspaceWindow = false, workspaceWindow = false,
onWorkspaceChange,
}: MainChartsPageProps) { }: MainChartsPageProps) {
const isDark = theme === "dark"; const isDark = theme === "dark";
@@ -79,6 +81,7 @@ export function MainChartsPage({
const [savedOpen, setSavedOpen] = useState(false); const [savedOpen, setSavedOpen] = useState(false);
const [movingChartId, setMovingChartId] = useState<string | null>(null); const [movingChartId, setMovingChartId] = useState<string | null>(null);
const [newChartOpen, setNewChartOpen] = useState(false); const [newChartOpen, setNewChartOpen] = useState(false);
const [newWorkspaceOpen, setNewWorkspaceOpen] = useState(false);
const [placingChartId, setPlacingChartId] = useState<string | null>(null); const [placingChartId, setPlacingChartId] = useState<string | null>(null);
const [detachedWorkspaceIds, setDetachedWorkspaceIds] = useState<Set<number>>(() => new Set()); const [detachedWorkspaceIds, setDetachedWorkspaceIds] = useState<Set<number>>(() => new Set());
@@ -87,7 +90,8 @@ export function MainChartsPage({
defaultName: "Workspace Geral", defaultName: "Workspace Geral",
}); });
const activeWorkspaceId = workspaceId ?? workspaceSelection.activeWorkspaceId; const activeWorkspaceId =
workspaceId !== undefined ? workspaceId : workspaceSelection.activeWorkspaceId;
const activeWorkspace = const activeWorkspace =
workspaceSelection.workspaces.find((workspace) => workspace.id === activeWorkspaceId) ?? workspaceSelection.workspaces.find((workspace) => workspace.id === activeWorkspaceId) ??
workspaceSelection.activeWorkspace; workspaceSelection.activeWorkspace;
@@ -96,26 +100,38 @@ export function MainChartsPage({
const workspaceWindowLabel = (workspaceId: number) => const workspaceWindowLabel = (workspaceId: number) =>
`workspace-global-${workspaceId}`; `workspace-global-${workspaceId}`;
const renameWorkspace = async (workspaceId: number, name: string) => { const notifyWorkspaceListChanged = () => {
const workspace = workspaceSelection.workspaces.find( window.dispatchEvent(
(item) => item.id === workspaceId, new CustomEvent("chart-workspaces:changed", {
detail: { scope: "GLOBAL" },
}),
); );
};
if (!workspace) return; const selectWorkspace = async (workspaceId: number) => {
await workspaceSelection.selectWorkspace(workspaceId);
onWorkspaceChange?.(workspaceId);
};
const createWorkspace = async (name: string) => {
const createdWorkspace = await workspaceSelection.createWorkspace(name);
if (createdWorkspace) {
onWorkspaceChange?.(createdWorkspace.id);
notifyWorkspaceListChanged();
}
};
const renameWorkspace = async (workspaceId: number, name: string) => {
await saveChartWorkspace( await saveChartWorkspace(
"GLOBAL", "GLOBAL",
{ { name },
name,
sortOrder: workspace.sortOrder,
defaultWorkspace: workspace.defaultWorkspace,
layoutMode: workspace.layoutMode,
chartsJson: workspace.chartsJson,
},
workspaceId, workspaceId,
); );
workspaceSelection.updateWorkspaceName(workspaceId, name); workspaceSelection.updateWorkspaceName(workspaceId, name);
notifyWorkspaceListChanged();
const existing = await WebviewWindow.getByLabel( const existing = await WebviewWindow.getByLabel(
workspaceWindowLabel(workspaceId), workspaceWindowLabel(workspaceId),
@@ -141,6 +157,13 @@ export function MainChartsPage({
}); });
await workspaceSelection.deleteWorkspace(workspaceId); await workspaceSelection.deleteWorkspace(workspaceId);
if (activeWorkspaceId === workspaceId) {
workspaceSelection.setActiveWorkspace(null);
onWorkspaceChange?.(null);
}
notifyWorkspaceListChanged();
}; };
const detachWorkspace = () => { const detachWorkspace = () => {
@@ -148,17 +171,8 @@ export function MainChartsPage({
setDetachedWorkspaceIds((current) => new Set(current).add(activeWorkspaceId)); setDetachedWorkspaceIds((current) => new Set(current).add(activeWorkspaceId));
const nextWorkspace = workspaceSelection.workspaces.find(
(workspace) =>
workspace.id !== activeWorkspaceId &&
!detachedWorkspaceIds.has(workspace.id),
);
if (nextWorkspace) {
void workspaceSelection.selectWorkspace(nextWorkspace.id);
} else {
workspaceSelection.setActiveWorkspace(null); workspaceSelection.setActiveWorkspace(null);
} onWorkspaceChange?.(null);
void openWorkspaceWindow( void openWorkspaceWindow(
activeWorkspaceId, activeWorkspaceId,
@@ -209,6 +223,54 @@ export function MainChartsPage({
}; };
}, []); }, []);
useEffect(() => {
if (workspaceWindow || workspaceSelection.loading) return;
let cancelled = false;
async function syncDetachedWorkspaceWindows() {
const nextDetachedWorkspaceIds = new Set<number>();
for (const workspace of workspaceSelection.workspaces) {
const existing = await WebviewWindow.getByLabel(
workspaceWindowLabel(workspace.id),
);
if (existing) {
nextDetachedWorkspaceIds.add(workspace.id);
}
}
if (cancelled) return;
setDetachedWorkspaceIds(nextDetachedWorkspaceIds);
const selectedWorkspaceId = activeWorkspaceId;
if (
selectedWorkspaceId !== null &&
nextDetachedWorkspaceIds.has(selectedWorkspaceId)
) {
workspaceSelection.setActiveWorkspace(null);
onWorkspaceChange?.(null);
}
}
void syncDetachedWorkspaceWindows();
return () => {
cancelled = true;
};
}, [
workspaceSelection.workspaces,
activeWorkspaceId,
workspaceSelection.loading,
workspaceSelection.selectWorkspace,
workspaceSelection.setActiveWorkspace,
workspaceWindow,
onWorkspaceChange,
]);
useEffect(() => { useEffect(() => {
setCharts([]); setCharts([]);
setConfigChartId(null); setConfigChartId(null);
@@ -253,7 +315,7 @@ export function MainChartsPage({
}, },
}); });
const canAddMoreCharts = charts.length < MAX_CHARTS; const canAddMoreCharts = activeWorkspaceId !== null && charts.length < MAX_CHARTS;
const visibleCharts = useMemo(() => { const visibleCharts = useMemo(() => {
const openCharts = charts.filter( const openCharts = charts.filter(
@@ -343,31 +405,6 @@ export function MainChartsPage({
setNewChartOpen(false); setNewChartOpen(false);
}; };
const duplicateChart = (chartId: string) => {
if (!canAddMoreCharts) return;
const chart = charts.find((item) => item.id === chartId);
if (!chart) return;
const copy: ChartWorkspaceItem = {
...chart,
id: `chart-${Date.now()}`,
title: `${chart.title} cópia`,
};
setCharts((current) => {
const next = [...current, copy];
if (next.length >= 3) {
setLayoutMode("fourGrid");
} else if (next.length === 2) {
setLayoutMode("twoColumns");
}
return next;
});
};
const setChartMode = (chartId: string, mode: WorkspaceChartMode) => { const setChartMode = (chartId: string, mode: WorkspaceChartMode) => {
setCharts((current) => setCharts((current) =>
current.map((chart) => current.map((chart) =>
@@ -641,13 +678,13 @@ export function MainChartsPage({
<WorkspaceSelector <WorkspaceSelector
theme={theme} theme={theme}
workspaces={workspaceSelection.workspaces} workspaces={workspaceSelection.workspaces}
activeWorkspaceId={workspaceSelection.activeWorkspaceId} activeWorkspaceId={activeWorkspaceId}
detachedWorkspaceIds={detachedWorkspaceIds} detachedWorkspaceIds={detachedWorkspaceIds}
loading={workspaceSelection.loading} loading={workspaceSelection.loading}
creating={workspaceSelection.creating} creating={workspaceSelection.creating}
canCreateWorkspace={workspaceSelection.canCreateWorkspace} canCreateWorkspace={workspaceSelection.canCreateWorkspace}
onSelectWorkspace={workspaceSelection.selectWorkspace} onSelectWorkspace={(workspaceId) => void selectWorkspace(workspaceId)}
onCreateWorkspace={(name) => void workspaceSelection.createWorkspace(name)} onCreateWorkspace={createWorkspace}
onRenameWorkspace={(workspaceId, name) => void renameWorkspace(workspaceId, name)} onRenameWorkspace={(workspaceId, name) => void renameWorkspace(workspaceId, name)}
onDeleteWorkspace={(workspaceId) => void deleteWorkspace(workspaceId)} onDeleteWorkspace={(workspaceId) => void deleteWorkspace(workspaceId)}
/> />
@@ -780,12 +817,32 @@ export function MainChartsPage({
{visibleCharts.length === 0 ? ( {visibleCharts.length === 0 ? (
<EmptyWorkspace <EmptyWorkspace
theme={theme} theme={theme}
canAddMoreCharts={canAddMoreCharts} canAddMoreCharts={activeWorkspaceId === null || canAddMoreCharts}
title={activeWorkspaceId === null ? "Nenhum workspace aberto" : undefined}
description={
activeWorkspaceId === null
? "Crie um workspace novo ou abra um workspace guardado para continuar."
: undefined
}
addLabel={activeWorkspaceId === null ? "Novo Workspace" : "Novo Gráfico"}
savedLabel={activeWorkspaceId === null ? "Abrir Workspace" : "Abrir Guardado"}
showSavedButton={activeWorkspaceId !== null}
minHeightClass="min-h-[520px]" minHeightClass="min-h-[520px]"
onAddChart={() => { onAddChart={() => {
if (activeWorkspaceId === null) {
setNewWorkspaceOpen(true);
return;
}
if (canAddMoreCharts) setNewChartOpen(true); if (canAddMoreCharts) setNewChartOpen(true);
}} }}
onOpenSaved={() => setSavedOpen(true)} onOpenSaved={() => {
if (activeWorkspaceId === null) {
return;
}
setSavedOpen(true);
}}
/> />
) : ( ) : (
<section className={layoutGridClass(layoutMode)}> <section className={layoutGridClass(layoutMode)}>
@@ -799,7 +856,6 @@ export function MainChartsPage({
movingChartId={movingChartId} movingChartId={movingChartId}
setMovingChartId={setMovingChartId} setMovingChartId={setMovingChartId}
swapCharts={swapCharts} swapCharts={swapCharts}
duplicateChart={duplicateChart}
closeChart={closeChart} closeChart={closeChart}
setConfigChartId={setConfigChartId} setConfigChartId={setConfigChartId}
setChartMode={setChartMode} setChartMode={setChartMode}
@@ -864,6 +920,17 @@ export function MainChartsPage({
/> />
)} )}
{newWorkspaceOpen && (
<NewWorkspaceModal
theme={theme}
creating={workspaceSelection.creating}
onClose={() => setNewWorkspaceOpen(false)}
onCreate={(name) => {
void createWorkspace(name).then(() => setNewWorkspaceOpen(false));
}}
/>
)}
{configChart && ( {configChart && (
<ChartConfigModal <ChartConfigModal
theme={theme} theme={theme}
@@ -895,7 +962,6 @@ function WorkspaceChartContainer({
movingChartId, movingChartId,
setMovingChartId, setMovingChartId,
swapCharts, swapCharts,
duplicateChart,
closeChart, closeChart,
setConfigChartId, setConfigChartId,
setChartMode, setChartMode,
@@ -914,7 +980,6 @@ function WorkspaceChartContainer({
movingChartId: string | null; movingChartId: string | null;
setMovingChartId: React.Dispatch<React.SetStateAction<string | null>>; setMovingChartId: React.Dispatch<React.SetStateAction<string | null>>;
swapCharts: (sourceId: string, targetId: string) => void; swapCharts: (sourceId: string, targetId: string) => void;
duplicateChart: (chartId: string) => void;
closeChart: (chartId: string) => void | Promise<void>; closeChart: (chartId: string) => void | Promise<void>;
setConfigChartId: React.Dispatch<React.SetStateAction<string | null>>; setConfigChartId: React.Dispatch<React.SetStateAction<string | null>>;
setChartMode: (chartId: string, mode: WorkspaceChartMode) => void; setChartMode: (chartId: string, mode: WorkspaceChartMode) => void;
@@ -1004,10 +1069,6 @@ function WorkspaceChartContainer({
<Cog className="h-4 w-4" /> <Cog className="h-4 w-4" />
</button> </button>
<button type="button" title="Duplicar" className={floatingIconClass(theme)} onClick={() => duplicateChart(chartItem.id)}>
<Copy className="h-4 w-4" />
</button>
<button <button
type="button" type="button"
title="Fechar" title="Fechar"
+123 -68
View File
@@ -7,7 +7,6 @@ import {
ChevronDown, ChevronDown,
Cog, Cog,
Columns2, Columns2,
Copy,
Grid2X2, Grid2X2,
GripVertical, GripVertical,
LineChart, LineChart,
@@ -29,6 +28,7 @@ import {
EmptyWorkspace, EmptyWorkspace,
LayoutButton, LayoutButton,
ModeButton, ModeButton,
NewWorkspaceModal,
WorkspaceSelector, WorkspaceSelector,
} from "../../chartworkspace/components/ChartWorkspaceControls"; } from "../../chartworkspace/components/ChartWorkspaceControls";
import { openWorkspaceWindow } from "../../chartworkspace/utils/openWorkspaceWindow"; import { openWorkspaceWindow } from "../../chartworkspace/utils/openWorkspaceWindow";
@@ -62,6 +62,7 @@ type MeteoChartsPageProps = {
theme: "dark" | "light"; theme: "dark" | "light";
workspaceId?: number | null; workspaceId?: number | null;
workspaceWindow?: boolean; workspaceWindow?: boolean;
onWorkspaceChange?: (workspaceId: number | null) => void;
}; };
type HistorianPoint = { type HistorianPoint = {
@@ -78,8 +79,9 @@ const INITIAL_CHARTS: ChartWorkspaceItem[] = [];
export function MeteoChartsPage({ export function MeteoChartsPage({
theme, theme,
workspaceId = null, workspaceId,
workspaceWindow = false, workspaceWindow = false,
onWorkspaceChange,
}: MeteoChartsPageProps) { }: MeteoChartsPageProps) {
const isDark = theme === "dark"; const isDark = theme === "dark";
@@ -96,6 +98,7 @@ export function MeteoChartsPage({
const [savedOpen, setSavedOpen] = useState(false); const [savedOpen, setSavedOpen] = useState(false);
const [movingChartId, setMovingChartId] = useState<string | null>(null); const [movingChartId, setMovingChartId] = useState<string | null>(null);
const [newChartOpen, setNewChartOpen] = useState(false); const [newChartOpen, setNewChartOpen] = useState(false);
const [newWorkspaceOpen, setNewWorkspaceOpen] = useState(false);
const [placingChartId, setPlacingChartId] = useState<string | null>(null); const [placingChartId, setPlacingChartId] = useState<string | null>(null);
const [detachedWorkspaceIds, setDetachedWorkspaceIds] = useState<Set<number>>(() => new Set()); const [detachedWorkspaceIds, setDetachedWorkspaceIds] = useState<Set<number>>(() => new Set());
@@ -104,7 +107,8 @@ export function MeteoChartsPage({
defaultName: "Workspace Meteo", defaultName: "Workspace Meteo",
}); });
const activeWorkspaceId = workspaceId ?? workspaceSelection.activeWorkspaceId; const activeWorkspaceId =
workspaceId !== undefined ? workspaceId : workspaceSelection.activeWorkspaceId;
const activeWorkspace = const activeWorkspace =
workspaceSelection.workspaces.find((workspace) => workspace.id === activeWorkspaceId) ?? workspaceSelection.workspaces.find((workspace) => workspace.id === activeWorkspaceId) ??
workspaceSelection.activeWorkspace; workspaceSelection.activeWorkspace;
@@ -113,26 +117,38 @@ export function MeteoChartsPage({
const workspaceWindowLabel = (workspaceId: number) => const workspaceWindowLabel = (workspaceId: number) =>
`workspace-meteo-${workspaceId}`; `workspace-meteo-${workspaceId}`;
const renameWorkspace = async (workspaceId: number, name: string) => { const notifyWorkspaceListChanged = () => {
const workspace = workspaceSelection.workspaces.find( window.dispatchEvent(
(item) => item.id === workspaceId, new CustomEvent("chart-workspaces:changed", {
detail: { scope: "METEO" },
}),
); );
};
if (!workspace) return; const selectWorkspace = async (workspaceId: number) => {
await workspaceSelection.selectWorkspace(workspaceId);
onWorkspaceChange?.(workspaceId);
};
const createWorkspace = async (name: string) => {
const createdWorkspace = await workspaceSelection.createWorkspace(name);
if (createdWorkspace) {
onWorkspaceChange?.(createdWorkspace.id);
notifyWorkspaceListChanged();
}
};
const renameWorkspace = async (workspaceId: number, name: string) => {
await saveChartWorkspace( await saveChartWorkspace(
"METEO", "METEO",
{ { name },
name,
sortOrder: workspace.sortOrder,
defaultWorkspace: workspace.defaultWorkspace,
layoutMode: workspace.layoutMode,
chartsJson: workspace.chartsJson,
},
workspaceId, workspaceId,
); );
workspaceSelection.updateWorkspaceName(workspaceId, name); workspaceSelection.updateWorkspaceName(workspaceId, name);
notifyWorkspaceListChanged();
const existing = await WebviewWindow.getByLabel( const existing = await WebviewWindow.getByLabel(
workspaceWindowLabel(workspaceId), workspaceWindowLabel(workspaceId),
@@ -158,6 +174,13 @@ export function MeteoChartsPage({
}); });
await workspaceSelection.deleteWorkspace(workspaceId); await workspaceSelection.deleteWorkspace(workspaceId);
if (activeWorkspaceId === workspaceId) {
workspaceSelection.setActiveWorkspace(null);
onWorkspaceChange?.(null);
}
notifyWorkspaceListChanged();
}; };
const detachWorkspace = () => { const detachWorkspace = () => {
@@ -165,17 +188,8 @@ export function MeteoChartsPage({
setDetachedWorkspaceIds((current) => new Set(current).add(activeWorkspaceId)); setDetachedWorkspaceIds((current) => new Set(current).add(activeWorkspaceId));
const nextWorkspace = workspaceSelection.workspaces.find(
(workspace) =>
workspace.id !== activeWorkspaceId &&
!detachedWorkspaceIds.has(workspace.id),
);
if (nextWorkspace) {
void workspaceSelection.selectWorkspace(nextWorkspace.id);
} else {
workspaceSelection.setActiveWorkspace(null); workspaceSelection.setActiveWorkspace(null);
} onWorkspaceChange?.(null);
void openWorkspaceWindow( void openWorkspaceWindow(
activeWorkspaceId, activeWorkspaceId,
@@ -207,6 +221,54 @@ export function MeteoChartsPage({
}; };
}, []); }, []);
useEffect(() => {
if (workspaceWindow || workspaceSelection.loading) return;
let cancelled = false;
async function syncDetachedWorkspaceWindows() {
const nextDetachedWorkspaceIds = new Set<number>();
for (const workspace of workspaceSelection.workspaces) {
const existing = await WebviewWindow.getByLabel(
workspaceWindowLabel(workspace.id),
);
if (existing) {
nextDetachedWorkspaceIds.add(workspace.id);
}
}
if (cancelled) return;
setDetachedWorkspaceIds(nextDetachedWorkspaceIds);
const selectedWorkspaceId = activeWorkspaceId;
if (
selectedWorkspaceId !== null &&
nextDetachedWorkspaceIds.has(selectedWorkspaceId)
) {
workspaceSelection.setActiveWorkspace(null);
onWorkspaceChange?.(null);
}
}
void syncDetachedWorkspaceWindows();
return () => {
cancelled = true;
};
}, [
workspaceSelection.workspaces,
activeWorkspaceId,
workspaceSelection.loading,
workspaceSelection.selectWorkspace,
workspaceSelection.setActiveWorkspace,
workspaceWindow,
onWorkspaceChange,
]);
useEffect(() => { useEffect(() => {
setCharts([]); setCharts([]);
setConfigChartId(null); setConfigChartId(null);
@@ -249,7 +311,7 @@ export function MeteoChartsPage({
}, },
}); });
const canAddMoreCharts = charts.length < MAX_CHARTS; const canAddMoreCharts = activeWorkspaceId !== null && charts.length < MAX_CHARTS;
const visibleCharts = useMemo(() => { const visibleCharts = useMemo(() => {
const openCharts = charts.filter( const openCharts = charts.filter(
@@ -337,31 +399,6 @@ export function MeteoChartsPage({
setNewChartOpen(false); setNewChartOpen(false);
}; };
const duplicateChart = (chartId: string) => {
if (!canAddMoreCharts) return;
const chart = charts.find((item) => item.id === chartId);
if (!chart) return;
const copy: ChartWorkspaceItem = {
...chart,
id: `chart-${Date.now()}`,
title: `${chart.title} cópia`,
};
setCharts((current) => {
const next = [...current, copy];
if (next.length >= 3) {
setLayoutMode("fourGrid");
} else if (next.length === 2) {
setLayoutMode("twoColumns");
}
return next;
});
};
const setChartMode = (chartId: string, mode: WorkspaceChartMode) => { const setChartMode = (chartId: string, mode: WorkspaceChartMode) => {
setCharts((current) => setCharts((current) =>
current.map((chart) => current.map((chart) =>
@@ -624,13 +661,13 @@ export function MeteoChartsPage({
<WorkspaceSelector <WorkspaceSelector
theme={theme} theme={theme}
workspaces={workspaceSelection.workspaces} workspaces={workspaceSelection.workspaces}
activeWorkspaceId={workspaceSelection.activeWorkspaceId} activeWorkspaceId={activeWorkspaceId}
detachedWorkspaceIds={detachedWorkspaceIds} detachedWorkspaceIds={detachedWorkspaceIds}
loading={workspaceSelection.loading} loading={workspaceSelection.loading}
creating={workspaceSelection.creating} creating={workspaceSelection.creating}
canCreateWorkspace={workspaceSelection.canCreateWorkspace} canCreateWorkspace={workspaceSelection.canCreateWorkspace}
onSelectWorkspace={workspaceSelection.selectWorkspace} onSelectWorkspace={(workspaceId) => void selectWorkspace(workspaceId)}
onCreateWorkspace={(name) => void workspaceSelection.createWorkspace(name)} onCreateWorkspace={createWorkspace}
onRenameWorkspace={(workspaceId, name) => void renameWorkspace(workspaceId, name)} onRenameWorkspace={(workspaceId, name) => void renameWorkspace(workspaceId, name)}
onDeleteWorkspace={(workspaceId) => void deleteWorkspace(workspaceId)} onDeleteWorkspace={(workspaceId) => void deleteWorkspace(workspaceId)}
/> />
@@ -763,12 +800,31 @@ export function MeteoChartsPage({
{visibleCharts.length === 0 ? ( {visibleCharts.length === 0 ? (
<EmptyWorkspace <EmptyWorkspace
theme={theme} theme={theme}
canAddMoreCharts={canAddMoreCharts} canAddMoreCharts={activeWorkspaceId === null || canAddMoreCharts}
description="Abra um gráfico guardado ou crie um novo gráfico meteorológico para continuar." title={activeWorkspaceId === null ? "Nenhum workspace aberto" : undefined}
description={
activeWorkspaceId === null
? "Crie um workspace novo ou abra um workspace guardado para continuar."
: "Abra um gráfico guardado ou crie um novo gráfico meteorológico para continuar."
}
addLabel={activeWorkspaceId === null ? "Novo Workspace" : "Novo Gráfico"}
savedLabel={activeWorkspaceId === null ? "Abrir Workspace" : "Abrir Guardado"}
showSavedButton={activeWorkspaceId !== null}
onAddChart={() => { onAddChart={() => {
if (activeWorkspaceId === null) {
setNewWorkspaceOpen(true);
return;
}
if (canAddMoreCharts) setNewChartOpen(true); if (canAddMoreCharts) setNewChartOpen(true);
}} }}
onOpenSaved={() => setSavedOpen(true)} onOpenSaved={() => {
if (activeWorkspaceId === null) {
return;
}
setSavedOpen(true);
}}
/> />
) : ( ) : (
<section className={layoutGridClass(layoutMode)}> <section className={layoutGridClass(layoutMode)}>
@@ -783,7 +839,6 @@ export function MeteoChartsPage({
movingChartId={movingChartId} movingChartId={movingChartId}
setMovingChartId={setMovingChartId} setMovingChartId={setMovingChartId}
swapCharts={swapCharts} swapCharts={swapCharts}
duplicateChart={duplicateChart}
closeChart={closeChart} closeChart={closeChart}
setConfigChartId={setConfigChartId} setConfigChartId={setConfigChartId}
setChartMode={setChartMode} setChartMode={setChartMode}
@@ -857,6 +912,17 @@ export function MeteoChartsPage({
/> />
)} )}
{newWorkspaceOpen && (
<NewWorkspaceModal
theme={theme}
creating={workspaceSelection.creating}
onClose={() => setNewWorkspaceOpen(false)}
onCreate={(name) => {
void createWorkspace(name).then(() => setNewWorkspaceOpen(false));
}}
/>
)}
{configChart && ( {configChart && (
<ChartConfigModal <ChartConfigModal
theme={theme} theme={theme}
@@ -889,7 +955,6 @@ function WorkspaceChartContainer({
movingChartId, movingChartId,
setMovingChartId, setMovingChartId,
swapCharts, swapCharts,
duplicateChart,
closeChart, closeChart,
setConfigChartId, setConfigChartId,
setChartMode, setChartMode,
@@ -907,7 +972,6 @@ function WorkspaceChartContainer({
movingChartId: string | null; movingChartId: string | null;
setMovingChartId: React.Dispatch<React.SetStateAction<string | null>>; setMovingChartId: React.Dispatch<React.SetStateAction<string | null>>;
swapCharts: (sourceId: string, targetId: string) => void; swapCharts: (sourceId: string, targetId: string) => void;
duplicateChart: (chartId: string) => void;
closeChart: (chartId: string) => void | Promise<void>; closeChart: (chartId: string) => void | Promise<void>;
setConfigChartId: React.Dispatch<React.SetStateAction<string | null>>; setConfigChartId: React.Dispatch<React.SetStateAction<string | null>>;
setChartMode: (chartId: string, mode: WorkspaceChartMode) => void; setChartMode: (chartId: string, mode: WorkspaceChartMode) => void;
@@ -992,15 +1056,6 @@ function WorkspaceChartContainer({
<Cog className="h-4 w-4" /> <Cog className="h-4 w-4" />
</button> </button>
<button
type="button"
title="Duplicar"
className={floatingIconClass(theme)}
onClick={() => duplicateChart(chartItem.id)}
>
<Copy className="h-4 w-4" />
</button>
<button <button
type="button" type="button"
title="Fechar" title="Fechar"
+13 -1
View File
@@ -8,10 +8,12 @@ import {
} from "react"; } from "react";
import { fetchRuntimeConfig } from "../../lib/api/systemApi"; import { fetchRuntimeConfig } from "../../lib/api/systemApi";
import { ApiResponseError } from "../../lib/api/readJsonResponse";
import { import {
clearRuntimeConfig, clearRuntimeConfig,
setRuntimeConfig as setRuntimeConfigSnapshot, setRuntimeConfig as setRuntimeConfigSnapshot,
} from "../../lib/api/runtimeConfigStore"; } from "../../lib/api/runtimeConfigStore";
import { useAuth } from "../auth/AuthContext";
import type { RuntimeConfig } from "../../types/system"; import type { RuntimeConfig } from "../../types/system";
type RuntimeConfigContextValue = { type RuntimeConfigContextValue = {
@@ -23,6 +25,7 @@ const RuntimeConfigContext = createContext<RuntimeConfigContextValue | null>(
); );
export function RuntimeConfigProvider({ children }: { children: ReactNode }) { export function RuntimeConfigProvider({ children }: { children: ReactNode }) {
const { logout } = useAuth();
const [runtimeConfig, setRuntimeConfig] = useState<RuntimeConfig | null>(null); const [runtimeConfig, setRuntimeConfig] = useState<RuntimeConfig | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -41,6 +44,15 @@ export function RuntimeConfigProvider({ children }: { children: ReactNode }) {
if (cancelled) return; if (cancelled) return;
clearRuntimeConfig(); clearRuntimeConfig();
if (
exception instanceof ApiResponseError &&
(exception.status === 401 || exception.status === 403)
) {
logout();
return;
}
setError( setError(
exception instanceof Error exception instanceof Error
? exception.message ? exception.message
@@ -52,7 +64,7 @@ export function RuntimeConfigProvider({ children }: { children: ReactNode }) {
cancelled = true; cancelled = true;
clearRuntimeConfig(); clearRuntimeConfig();
}; };
}, []); }, [logout]);
const value = useMemo<RuntimeConfigContextValue | null>( const value = useMemo<RuntimeConfigContextValue | null>(
() => (runtimeConfig ? { runtimeConfig } : null), () => (runtimeConfig ? { runtimeConfig } : null),
+21 -2
View File
@@ -1,3 +1,14 @@
export class ApiResponseError extends Error {
constructor(
message: string,
public readonly status: number,
public readonly body: string,
) {
super(message);
this.name = "ApiResponseError";
}
}
export async function readJsonResponse<T>( export async function readJsonResponse<T>(
response: Response, response: Response,
context: string, context: string,
@@ -5,7 +16,11 @@ export async function readJsonResponse<T>(
const text = await response.text(); const text = await response.text();
if (!response.ok) { if (!response.ok) {
throw new Error(`${context}: ${response.status} ${text}`.trim()); throw new ApiResponseError(
`${context}: ${response.status} ${text}`.trim(),
response.status,
text,
);
} }
if (!text) { if (!text) {
@@ -23,7 +38,11 @@ export async function readOptionalJsonResponse<T>(
const text = await response.text(); const text = await response.text();
if (!response.ok) { if (!response.ok) {
throw new Error(`${context}: ${response.status} ${text}`.trim()); throw new ApiResponseError(
`${context}: ${response.status} ${text}`.trim(),
response.status,
text,
);
} }
return text ? (JSON.parse(text) as T) : fallback; return text ? (JSON.parse(text) as T) : fallback;