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:
+58
-4
@@ -15,6 +15,7 @@ import { useAuth } from "../features/auth/AuthContext";
|
||||
import { LoginPage } from "../features/auth/pages/LoginPage";
|
||||
import { RuntimeConfigProvider } from "../features/system/RuntimeConfigProvider";
|
||||
import { TitleBar } from "../components/window/TitleBar";
|
||||
import type { ChartWorkspaceScope } from "../features/chartworkspace/types";
|
||||
|
||||
export type AppPage =
|
||||
| "dashboard"
|
||||
@@ -37,6 +38,30 @@ export type AppPage =
|
||||
function App() {
|
||||
const { authenticated } = useAuth();
|
||||
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/");
|
||||
|
||||
@@ -72,7 +97,12 @@ function App() {
|
||||
<div className="pt-9">
|
||||
<div className="h-[calc(100vh-36px)] overflow-hidden">
|
||||
<RuntimeConfigProvider>
|
||||
<AppShell activePage={activePage} onNavigate={setActivePage}>
|
||||
<AppShell
|
||||
activePage={activePage}
|
||||
selectedWorkspaceByScope={selectedWorkspaceByScope}
|
||||
onNavigate={setActivePage}
|
||||
onOpenWorkspace={openWorkspace}
|
||||
>
|
||||
{({ theme }) => {
|
||||
if (activePage === "meteo") {
|
||||
return (
|
||||
@@ -84,17 +114,41 @@ function App() {
|
||||
}
|
||||
|
||||
if (activePage === "meteoCharts") {
|
||||
return <MeteoChartsPage theme={theme} />;
|
||||
return (
|
||||
<MeteoChartsPage
|
||||
theme={theme}
|
||||
workspaceId={selectedWorkspaceByScope.METEO}
|
||||
onWorkspaceChange={(workspaceId) =>
|
||||
updateSelectedWorkspace("METEO", workspaceId)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 === "maincharts") {
|
||||
return <MainChartsPage theme={theme} />;
|
||||
return (
|
||||
<MainChartsPage
|
||||
theme={theme}
|
||||
workspaceId={selectedWorkspaceByScope.GLOBAL}
|
||||
onWorkspaceChange={(workspaceId) =>
|
||||
updateSelectedWorkspace("GLOBAL", workspaceId)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (activePage === "settings") {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTelemetryStream } from "../../features/telemetry/hooks/useTelemetryS
|
||||
import { useCurrentUser } from "../../features/auth/hooks/useCurrentUser";
|
||||
import type { TelemetrySnapshot } from "../../types/telemetry";
|
||||
import type { AppPage } from "../../app/App";
|
||||
import type { ChartWorkspaceScope } from "../../features/chartworkspace/types";
|
||||
|
||||
type Theme = "dark" | "light";
|
||||
|
||||
@@ -14,13 +15,21 @@ type AppShellRenderProps = {
|
||||
|
||||
type AppShellProps = {
|
||||
activePage: AppPage;
|
||||
selectedWorkspaceByScope: Partial<Record<ChartWorkspaceScope, number | null>>;
|
||||
onNavigate: (page: AppPage) => void;
|
||||
onOpenWorkspace: (scope: ChartWorkspaceScope, workspaceId: number) => void;
|
||||
children: (props: AppShellRenderProps) => ReactNode;
|
||||
};
|
||||
|
||||
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 currentUser = useCurrentUser();
|
||||
|
||||
@@ -107,7 +116,9 @@ export function AppShell({ activePage, onNavigate, children }: AppShellProps) {
|
||||
userInitials={currentUser.initials}
|
||||
userName={currentUser.name}
|
||||
userRole={currentUser.role}
|
||||
selectedWorkspaceByScope={selectedWorkspaceByScope}
|
||||
onNavigate={onNavigate}
|
||||
onOpenWorkspace={onOpenWorkspace}
|
||||
onToggleCollapsed={() => setSidebarCollapsed((current) => !current)}
|
||||
onToggleTheme={toggleTheme}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
BarChart3,
|
||||
ChevronDown,
|
||||
@@ -26,6 +26,11 @@ import {
|
||||
import logo from "../../assets/logo5.png";
|
||||
import type { AppPage } from "../../app/App";
|
||||
import { useAuth } from "../../features/auth/AuthContext";
|
||||
import {
|
||||
listChartWorkspaces,
|
||||
type ChartWorkspaceResponse,
|
||||
} from "../../features/chartworkspace/api/chartWorkspaceApi";
|
||||
import type { ChartWorkspaceScope } from "../../features/chartworkspace/types";
|
||||
|
||||
type SidebarProps = {
|
||||
theme: "dark" | "light";
|
||||
@@ -34,7 +39,9 @@ type SidebarProps = {
|
||||
userInitials: string;
|
||||
userName: string;
|
||||
userRole: string;
|
||||
selectedWorkspaceByScope: Partial<Record<ChartWorkspaceScope, number | null>>;
|
||||
onNavigate: (page: AppPage) => void;
|
||||
onOpenWorkspace: (scope: ChartWorkspaceScope, workspaceId: number) => void;
|
||||
onToggleCollapsed: () => void;
|
||||
onToggleTheme: () => void;
|
||||
};
|
||||
@@ -82,7 +89,9 @@ export function Sidebar({
|
||||
userInitials,
|
||||
userName,
|
||||
userRole,
|
||||
selectedWorkspaceByScope,
|
||||
onNavigate,
|
||||
onOpenWorkspace,
|
||||
onToggleCollapsed,
|
||||
onToggleTheme,
|
||||
}: SidebarProps) {
|
||||
@@ -90,16 +99,64 @@ export function Sidebar({
|
||||
const isDark = theme === "dark";
|
||||
const ThemeIcon = isDark ? Moon : Sun;
|
||||
|
||||
const [generalOpen, setGeneralOpen] = useState(true);
|
||||
const [meteoOpen, setMeteoOpen] = useState(true);
|
||||
const [climateOpen, setClimateOpen] = useState(false);
|
||||
const [irrigationOpen, setIrrigationOpen] = useState(false);
|
||||
const [activeTreeItem, setActiveTreeItem] = useState<string | null>(null);
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
const [sidebarHovered, setSidebarHovered] = useState(false);
|
||||
const [workspaceLists, setWorkspaceLists] = useState<
|
||||
Partial<Record<ChartWorkspaceScope, ChartWorkspaceResponse[]>>
|
||||
>({});
|
||||
|
||||
const userButtonRef = useRef<HTMLButtonElement | 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(() => {
|
||||
if (!userMenuOpen) return;
|
||||
|
||||
@@ -149,6 +206,7 @@ export function Sidebar({
|
||||
const firstItem = sectionItems[section][0];
|
||||
|
||||
setActiveTreeItem(`${section}:${firstItem.label}`);
|
||||
setGeneralOpen(false);
|
||||
|
||||
if (firstItem.page) {
|
||||
onNavigate(firstItem.page);
|
||||
@@ -288,13 +346,30 @@ export function Sidebar({
|
||||
theme={theme}
|
||||
collapsed={collapsed}
|
||||
label="Gráficos Gerais"
|
||||
page="maincharts"
|
||||
icon={BarChart3}
|
||||
activePage={activePage}
|
||||
activeTreeItem={activeTreeItem}
|
||||
onNavigate={(page) => {
|
||||
open={generalOpen}
|
||||
workspaces={workspaceLists.GLOBAL ?? []}
|
||||
active={activePage === "maincharts"}
|
||||
selectedWorkspaceId={selectedWorkspaceByScope.GLOBAL ?? null}
|
||||
onToggle={() => {
|
||||
if (collapsed) {
|
||||
onToggleCollapsed();
|
||||
}
|
||||
|
||||
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"
|
||||
activeTreeItem={activeTreeItem}
|
||||
activePage={activePage}
|
||||
workspaceGroup={{
|
||||
scope: "METEO",
|
||||
page: "meteoCharts",
|
||||
workspaces: workspaceLists.METEO ?? [],
|
||||
selectedWorkspaceId: selectedWorkspaceByScope.METEO ?? null,
|
||||
}}
|
||||
onItemClick={handleTreeClick}
|
||||
onWorkspaceClick={(scope, workspaceId) => {
|
||||
setActiveTreeItem(null);
|
||||
setGeneralOpen(false);
|
||||
setMeteoOpen(true);
|
||||
setClimateOpen(false);
|
||||
setIrrigationOpen(false);
|
||||
onOpenWorkspace(scope, workspaceId);
|
||||
}}
|
||||
/>
|
||||
|
||||
<TreeSection
|
||||
@@ -339,7 +428,21 @@ export function Sidebar({
|
||||
sectionKey="climate"
|
||||
activeTreeItem={activeTreeItem}
|
||||
activePage={activePage}
|
||||
workspaceGroup={{
|
||||
scope: "CLIMATE",
|
||||
page: "climateCharts",
|
||||
workspaces: workspaceLists.CLIMATE ?? [],
|
||||
selectedWorkspaceId: selectedWorkspaceByScope.CLIMATE ?? null,
|
||||
}}
|
||||
onItemClick={handleTreeClick}
|
||||
onWorkspaceClick={(scope, workspaceId) => {
|
||||
setActiveTreeItem(null);
|
||||
setGeneralOpen(false);
|
||||
setMeteoOpen(false);
|
||||
setClimateOpen(true);
|
||||
setIrrigationOpen(false);
|
||||
onOpenWorkspace(scope, workspaceId);
|
||||
}}
|
||||
/>
|
||||
|
||||
<TreeSection
|
||||
@@ -594,7 +697,9 @@ function TreeSection({
|
||||
sectionKey,
|
||||
activeTreeItem,
|
||||
activePage,
|
||||
workspaceGroup,
|
||||
onItemClick,
|
||||
onWorkspaceClick,
|
||||
}: {
|
||||
theme: "dark" | "light";
|
||||
collapsed: boolean;
|
||||
@@ -606,7 +711,14 @@ function TreeSection({
|
||||
sectionKey: string;
|
||||
activeTreeItem: string | null;
|
||||
activePage: AppPage;
|
||||
workspaceGroup?: {
|
||||
scope: ChartWorkspaceScope;
|
||||
page: AppPage;
|
||||
workspaces: ChartWorkspaceResponse[];
|
||||
selectedWorkspaceId: number | null;
|
||||
};
|
||||
onItemClick: (key: string, page?: AppPage) => void;
|
||||
onWorkspaceClick?: (scope: ChartWorkspaceScope, workspaceId: number) => void;
|
||||
}) {
|
||||
const isDark = theme === "dark";
|
||||
const hasActiveChild = items.some((item) => {
|
||||
@@ -659,10 +771,14 @@ function TreeSection({
|
||||
const active =
|
||||
activeTreeItem === key ||
|
||||
Boolean(item.page && activePage === item.page);
|
||||
const showWorkspaces =
|
||||
workspaceGroup &&
|
||||
item.page === workspaceGroup.page &&
|
||||
workspaceGroup.workspaces.length > 0;
|
||||
|
||||
return (
|
||||
<div key={item.label}>
|
||||
<button
|
||||
key={item.label}
|
||||
type="button"
|
||||
onClick={() => onItemClick(key, item.page)}
|
||||
className={
|
||||
@@ -687,6 +803,20 @@ function TreeSection({
|
||||
|
||||
<span className="truncate">{item.label}</span>
|
||||
</button>
|
||||
|
||||
{showWorkspaces && (
|
||||
<WorkspaceTreeList
|
||||
theme={theme}
|
||||
workspaces={workspaceGroup.workspaces}
|
||||
activePage={activePage}
|
||||
page={workspaceGroup.page}
|
||||
selectedWorkspaceId={workspaceGroup.selectedWorkspaceId}
|
||||
onOpenWorkspace={(workspaceId) =>
|
||||
onWorkspaceClick?.(workspaceGroup.scope, workspaceId)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</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 }) {
|
||||
return (
|
||||
<span
|
||||
@@ -791,16 +1075,7 @@ function Divider({ isDark }: { isDark: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
function NavItem({
|
||||
theme,
|
||||
collapsed,
|
||||
label,
|
||||
page,
|
||||
icon: Icon,
|
||||
activePage,
|
||||
activeTreeItem,
|
||||
onNavigate,
|
||||
}: {
|
||||
type StandardNavItemProps = {
|
||||
theme: "dark" | "light";
|
||||
collapsed: boolean;
|
||||
label: string;
|
||||
@@ -809,7 +1084,37 @@ function NavItem({
|
||||
activePage: AppPage;
|
||||
activeTreeItem: string | null;
|
||||
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 active = activePage === page && activeTreeItem === null;
|
||||
|
||||
|
||||
@@ -43,9 +43,9 @@ export function LoginPage() {
|
||||
/>
|
||||
<div>
|
||||
<p className="text-xs font-black uppercase tracking-[0.22em] text-[#4FD1C5]">
|
||||
Litoral Regas
|
||||
Central LRX
|
||||
</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>
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ export type ChartWorkspaceSaveRequest = {
|
||||
name?: string;
|
||||
sortOrder?: number;
|
||||
defaultWorkspace?: boolean;
|
||||
layoutMode: ChartLayoutMode;
|
||||
chartsJson: string;
|
||||
layoutMode?: ChartLayoutMode;
|
||||
chartsJson?: string;
|
||||
};
|
||||
|
||||
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.",
|
||||
addLabel = "Novo Gráfico",
|
||||
savedLabel = "Abrir Guardado",
|
||||
showSavedButton = true,
|
||||
minHeightClass = "min-h-0 flex-1",
|
||||
onAddChart,
|
||||
onOpenSaved,
|
||||
@@ -90,6 +91,7 @@ export function EmptyWorkspace({
|
||||
description?: string;
|
||||
addLabel?: string;
|
||||
savedLabel?: string;
|
||||
showSavedButton?: boolean;
|
||||
minHeightClass?: string;
|
||||
onAddChart: () => void;
|
||||
onOpenSaved: () => void;
|
||||
@@ -150,6 +152,7 @@ export function EmptyWorkspace({
|
||||
{addLabel}
|
||||
</button>
|
||||
|
||||
{showSavedButton && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenSaved}
|
||||
@@ -162,6 +165,7 @@ export function EmptyWorkspace({
|
||||
<Search className="h-5 w-5" />
|
||||
{savedLabel}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -189,7 +193,7 @@ export function WorkspaceSelector({
|
||||
creating: boolean;
|
||||
canCreateWorkspace: boolean;
|
||||
onSelectWorkspace: (workspaceId: number) => void;
|
||||
onCreateWorkspace: (name: string) => void;
|
||||
onCreateWorkspace: (name: string) => void | Promise<void>;
|
||||
onRenameWorkspace: (workspaceId: number, name: string) => void | Promise<void>;
|
||||
onDeleteWorkspace: (workspaceId: number) => void | Promise<void>;
|
||||
}) {
|
||||
@@ -208,7 +212,7 @@ export function WorkspaceSelector({
|
||||
);
|
||||
|
||||
const createWorkspace = (name: string) => {
|
||||
onCreateWorkspace(name);
|
||||
void onCreateWorkspace(name);
|
||||
setCreateOpen(false);
|
||||
setOpen(false);
|
||||
};
|
||||
@@ -514,7 +518,7 @@ function WorkspaceNameModal({
|
||||
);
|
||||
}
|
||||
|
||||
function NewWorkspaceModal({
|
||||
export function NewWorkspaceModal({
|
||||
theme,
|
||||
creating,
|
||||
onClose,
|
||||
|
||||
@@ -44,7 +44,7 @@ type UseChartWorkspacePersistenceParams = {
|
||||
|
||||
export function useChartWorkspacePersistence({
|
||||
scope,
|
||||
workspaceId = null,
|
||||
workspaceId,
|
||||
layoutMode,
|
||||
charts,
|
||||
onLoaded,
|
||||
@@ -58,7 +58,11 @@ export function useChartWorkspacePersistence({
|
||||
const loadedWorkspaceKeyRef = useRef<string | null>(null);
|
||||
|
||||
const workspaceKey =
|
||||
workspaceId === null ? `scope:${scope}` : `id:${workspaceId}`;
|
||||
workspaceId === undefined
|
||||
? `scope:${scope}`
|
||||
: workspaceId === null
|
||||
? `none:${scope}`
|
||||
: `id:${workspaceId}`;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -70,8 +74,17 @@ export function useChartWorkspacePersistence({
|
||||
loadedWorkspaceKeyRef.current = null;
|
||||
|
||||
try {
|
||||
if (workspaceId === null) {
|
||||
if (!cancelled) {
|
||||
loadedWorkspaceKeyRef.current = workspaceKey;
|
||||
setLoaded(true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const payload =
|
||||
await loadChartWorkspace(scope, workspaceId);
|
||||
await loadChartWorkspace(scope, workspaceId ?? null);
|
||||
|
||||
if (!payload) {
|
||||
return;
|
||||
@@ -110,6 +123,7 @@ export function useChartWorkspacePersistence({
|
||||
|
||||
useEffect(() => {
|
||||
if (!loaded || !saveEnabled) return;
|
||||
if (workspaceId === null) return;
|
||||
if (loadedWorkspaceKeyRef.current !== workspaceKey) return;
|
||||
|
||||
if (saveTimeoutRef.current !== null) {
|
||||
@@ -127,7 +141,13 @@ export function useChartWorkspacePersistence({
|
||||
layoutMode,
|
||||
chartsJson: JSON.stringify(charts),
|
||||
},
|
||||
workspaceId,
|
||||
workspaceId ?? null,
|
||||
);
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("chart-workspaces:changed", {
|
||||
detail: { scope, workspaceId },
|
||||
}),
|
||||
);
|
||||
|
||||
setError(null);
|
||||
|
||||
@@ -74,11 +74,7 @@ export function useChartWorkspaceSelection({
|
||||
await saveChartWorkspace(
|
||||
scope,
|
||||
{
|
||||
name: workspace.name,
|
||||
sortOrder: workspace.sortOrder,
|
||||
defaultWorkspace: true,
|
||||
layoutMode: workspace.layoutMode,
|
||||
chartsJson: workspace.chartsJson,
|
||||
},
|
||||
workspace.id,
|
||||
);
|
||||
@@ -97,7 +93,7 @@ export function useChartWorkspaceSelection({
|
||||
}, [scope, workspaces]);
|
||||
|
||||
const createWorkspace = useCallback(async (name: string) => {
|
||||
if (workspaces.length >= 10 || creating) return;
|
||||
if (workspaces.length >= 10 || creating) return null;
|
||||
|
||||
try {
|
||||
setCreating(true);
|
||||
@@ -120,9 +116,11 @@ export function useChartWorkspaceSelection({
|
||||
]);
|
||||
setActiveWorkspaceId(createdWorkspace.id);
|
||||
setError(null);
|
||||
return createdWorkspace;
|
||||
} catch (exception) {
|
||||
console.error("Failed to create chart workspace", exception);
|
||||
setError("Nao foi possivel criar o workspace.");
|
||||
return null;
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
@@ -142,26 +140,7 @@ export function useChartWorkspaceSelection({
|
||||
setWorkspaces(remaining);
|
||||
|
||||
if (activeWorkspaceId === workspaceId) {
|
||||
const nextWorkspace =
|
||||
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,
|
||||
);
|
||||
}
|
||||
setActiveWorkspaceId(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 { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { Minus, Square, X } from "lucide-react";
|
||||
@@ -30,6 +30,21 @@ export function WorkspaceWindowPage({ theme }: WorkspaceWindowPageProps) {
|
||||
const currentWindow = getCurrentWindow();
|
||||
const { scope, workspaceId, title } = parseWorkspaceRoute();
|
||||
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(() => {
|
||||
void currentWindow.setTitle(windowTitle);
|
||||
@@ -57,19 +72,15 @@ export function WorkspaceWindowPage({ theme }: WorkspaceWindowPageProps) {
|
||||
}, [scope, workspaceId]);
|
||||
|
||||
useEffect(() => {
|
||||
const unlistenPromise = currentWindow.onCloseRequested(() => {
|
||||
if (!workspaceId) return;
|
||||
|
||||
void emit("workspace-window://closed", {
|
||||
scope,
|
||||
workspaceId,
|
||||
});
|
||||
const unlistenPromise = currentWindow.onCloseRequested((event) => {
|
||||
event.preventDefault();
|
||||
void closeWorkspaceWindow();
|
||||
});
|
||||
|
||||
return () => {
|
||||
void unlistenPromise.then((unlisten) => unlisten());
|
||||
};
|
||||
}, [currentWindow, scope, workspaceId]);
|
||||
}, [closeWorkspaceWindow, currentWindow]);
|
||||
|
||||
const titleBarClass = isDark
|
||||
? "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
|
||||
type="button"
|
||||
title="Fechar"
|
||||
onClick={() => currentWindow.close()}
|
||||
onClick={() => void closeWorkspaceWindow()}
|
||||
className={closeButtonClass}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
EmptyWorkspace,
|
||||
LayoutButton,
|
||||
ModeButton,
|
||||
NewWorkspaceModal,
|
||||
WorkspaceSelector,
|
||||
} from "../../chartworkspace/components/ChartWorkspaceControls";
|
||||
import { openWorkspaceWindow } from "../../chartworkspace/utils/openWorkspaceWindow";
|
||||
@@ -57,6 +58,7 @@ type ClimateChartsPageProps = {
|
||||
theme: "dark" | "light";
|
||||
workspaceId?: number | null;
|
||||
workspaceWindow?: boolean;
|
||||
onWorkspaceChange?: (workspaceId: number | null) => void;
|
||||
};
|
||||
|
||||
const RADIUS = "rounded-[6px]";
|
||||
@@ -65,8 +67,9 @@ const INITIAL_CHARTS: ChartWorkspaceItem[] = [];
|
||||
|
||||
export function ClimateChartsPage({
|
||||
theme,
|
||||
workspaceId = null,
|
||||
workspaceId,
|
||||
workspaceWindow = false,
|
||||
onWorkspaceChange,
|
||||
}: ClimateChartsPageProps) {
|
||||
const isDark = theme === "dark";
|
||||
|
||||
@@ -79,6 +82,7 @@ export function ClimateChartsPage({
|
||||
const [savedOpen, setSavedOpen] = useState(false);
|
||||
const [movingChartId, setMovingChartId] = useState<string | null>(null);
|
||||
const [newChartOpen, setNewChartOpen] = useState(false);
|
||||
const [newWorkspaceOpen, setNewWorkspaceOpen] = useState(false);
|
||||
const [placingChartId, setPlacingChartId] = useState<string | null>(null);
|
||||
const [detachedWorkspaceIds, setDetachedWorkspaceIds] = useState<Set<number>>(() => new Set());
|
||||
|
||||
@@ -87,7 +91,8 @@ export function ClimateChartsPage({
|
||||
defaultName: "Workspace Clima",
|
||||
});
|
||||
|
||||
const activeWorkspaceId = workspaceId ?? workspaceSelection.activeWorkspaceId;
|
||||
const activeWorkspaceId =
|
||||
workspaceId !== undefined ? workspaceId : workspaceSelection.activeWorkspaceId;
|
||||
const activeWorkspace =
|
||||
workspaceSelection.workspaces.find((workspace) => workspace.id === activeWorkspaceId) ??
|
||||
workspaceSelection.activeWorkspace;
|
||||
@@ -96,26 +101,38 @@ export function ClimateChartsPage({
|
||||
const workspaceWindowLabel = (workspaceId: number) =>
|
||||
`workspace-climate-${workspaceId}`;
|
||||
|
||||
const renameWorkspace = async (workspaceId: number, name: string) => {
|
||||
const workspace = workspaceSelection.workspaces.find(
|
||||
(item) => item.id === workspaceId,
|
||||
const notifyWorkspaceListChanged = () => {
|
||||
window.dispatchEvent(
|
||||
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(
|
||||
"CLIMATE",
|
||||
{
|
||||
name,
|
||||
sortOrder: workspace.sortOrder,
|
||||
defaultWorkspace: workspace.defaultWorkspace,
|
||||
layoutMode: workspace.layoutMode,
|
||||
chartsJson: workspace.chartsJson,
|
||||
},
|
||||
{ name },
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
workspaceSelection.updateWorkspaceName(workspaceId, name);
|
||||
notifyWorkspaceListChanged();
|
||||
|
||||
const existing = await WebviewWindow.getByLabel(
|
||||
workspaceWindowLabel(workspaceId),
|
||||
@@ -141,6 +158,13 @@ export function ClimateChartsPage({
|
||||
});
|
||||
|
||||
await workspaceSelection.deleteWorkspace(workspaceId);
|
||||
|
||||
if (activeWorkspaceId === workspaceId) {
|
||||
workspaceSelection.setActiveWorkspace(null);
|
||||
onWorkspaceChange?.(null);
|
||||
}
|
||||
|
||||
notifyWorkspaceListChanged();
|
||||
};
|
||||
|
||||
const detachWorkspace = () => {
|
||||
@@ -148,17 +172,8 @@ export function ClimateChartsPage({
|
||||
|
||||
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);
|
||||
}
|
||||
onWorkspaceChange?.(null);
|
||||
|
||||
void openWorkspaceWindow(
|
||||
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(() => {
|
||||
setCharts([]);
|
||||
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 openCharts = charts.filter(
|
||||
@@ -566,13 +629,13 @@ export function ClimateChartsPage({
|
||||
<WorkspaceSelector
|
||||
theme={theme}
|
||||
workspaces={workspaceSelection.workspaces}
|
||||
activeWorkspaceId={workspaceSelection.activeWorkspaceId}
|
||||
activeWorkspaceId={activeWorkspaceId}
|
||||
detachedWorkspaceIds={detachedWorkspaceIds}
|
||||
loading={workspaceSelection.loading}
|
||||
creating={workspaceSelection.creating}
|
||||
canCreateWorkspace={workspaceSelection.canCreateWorkspace}
|
||||
onSelectWorkspace={workspaceSelection.selectWorkspace}
|
||||
onCreateWorkspace={(name) => void workspaceSelection.createWorkspace(name)}
|
||||
onSelectWorkspace={(workspaceId) => void selectWorkspace(workspaceId)}
|
||||
onCreateWorkspace={createWorkspace}
|
||||
onRenameWorkspace={(workspaceId, name) => void renameWorkspace(workspaceId, name)}
|
||||
onDeleteWorkspace={(workspaceId) => void deleteWorkspace(workspaceId)}
|
||||
/>
|
||||
@@ -705,11 +768,31 @@ export function ClimateChartsPage({
|
||||
{visibleCharts.length === 0 ? (
|
||||
<EmptyWorkspace
|
||||
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={() => {
|
||||
if (activeWorkspaceId === null) {
|
||||
setNewWorkspaceOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (canAddMoreCharts) setNewChartOpen(true);
|
||||
}}
|
||||
onOpenSaved={() => setSavedOpen(true)}
|
||||
onOpenSaved={() => {
|
||||
if (activeWorkspaceId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSavedOpen(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<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 && (
|
||||
<ChartConfigModal
|
||||
theme={theme}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
EmptyWorkspace,
|
||||
LayoutButton,
|
||||
ModeButton,
|
||||
NewWorkspaceModal,
|
||||
WorkspaceSelector,
|
||||
} from "../../chartworkspace/components/ChartWorkspaceControls";
|
||||
import { openWorkspaceWindow } from "../../chartworkspace/utils/openWorkspaceWindow";
|
||||
@@ -13,7 +14,6 @@ import { saveChartWorkspace } from "../../chartworkspace/api/chartWorkspaceApi";
|
||||
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import {
|
||||
Cog,
|
||||
Copy,
|
||||
Maximize2,
|
||||
AreaChart,
|
||||
BarChart3,
|
||||
@@ -57,6 +57,7 @@ type MainChartsPageProps = {
|
||||
theme: "dark" | "light";
|
||||
workspaceId?: number | null;
|
||||
workspaceWindow?: boolean;
|
||||
onWorkspaceChange?: (workspaceId: number | null) => void;
|
||||
};
|
||||
|
||||
const RADIUS = "rounded-[6px]";
|
||||
@@ -65,8 +66,9 @@ const INITIAL_CHARTS: ChartWorkspaceItem[] = [];
|
||||
|
||||
export function MainChartsPage({
|
||||
theme,
|
||||
workspaceId = null,
|
||||
workspaceId,
|
||||
workspaceWindow = false,
|
||||
onWorkspaceChange,
|
||||
}: MainChartsPageProps) {
|
||||
const isDark = theme === "dark";
|
||||
|
||||
@@ -79,6 +81,7 @@ export function MainChartsPage({
|
||||
const [savedOpen, setSavedOpen] = useState(false);
|
||||
const [movingChartId, setMovingChartId] = useState<string | null>(null);
|
||||
const [newChartOpen, setNewChartOpen] = useState(false);
|
||||
const [newWorkspaceOpen, setNewWorkspaceOpen] = useState(false);
|
||||
const [placingChartId, setPlacingChartId] = useState<string | null>(null);
|
||||
const [detachedWorkspaceIds, setDetachedWorkspaceIds] = useState<Set<number>>(() => new Set());
|
||||
|
||||
@@ -87,7 +90,8 @@ export function MainChartsPage({
|
||||
defaultName: "Workspace Geral",
|
||||
});
|
||||
|
||||
const activeWorkspaceId = workspaceId ?? workspaceSelection.activeWorkspaceId;
|
||||
const activeWorkspaceId =
|
||||
workspaceId !== undefined ? workspaceId : workspaceSelection.activeWorkspaceId;
|
||||
const activeWorkspace =
|
||||
workspaceSelection.workspaces.find((workspace) => workspace.id === activeWorkspaceId) ??
|
||||
workspaceSelection.activeWorkspace;
|
||||
@@ -96,26 +100,38 @@ export function MainChartsPage({
|
||||
const workspaceWindowLabel = (workspaceId: number) =>
|
||||
`workspace-global-${workspaceId}`;
|
||||
|
||||
const renameWorkspace = async (workspaceId: number, name: string) => {
|
||||
const workspace = workspaceSelection.workspaces.find(
|
||||
(item) => item.id === workspaceId,
|
||||
const notifyWorkspaceListChanged = () => {
|
||||
window.dispatchEvent(
|
||||
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(
|
||||
"GLOBAL",
|
||||
{
|
||||
name,
|
||||
sortOrder: workspace.sortOrder,
|
||||
defaultWorkspace: workspace.defaultWorkspace,
|
||||
layoutMode: workspace.layoutMode,
|
||||
chartsJson: workspace.chartsJson,
|
||||
},
|
||||
{ name },
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
workspaceSelection.updateWorkspaceName(workspaceId, name);
|
||||
notifyWorkspaceListChanged();
|
||||
|
||||
const existing = await WebviewWindow.getByLabel(
|
||||
workspaceWindowLabel(workspaceId),
|
||||
@@ -141,6 +157,13 @@ export function MainChartsPage({
|
||||
});
|
||||
|
||||
await workspaceSelection.deleteWorkspace(workspaceId);
|
||||
|
||||
if (activeWorkspaceId === workspaceId) {
|
||||
workspaceSelection.setActiveWorkspace(null);
|
||||
onWorkspaceChange?.(null);
|
||||
}
|
||||
|
||||
notifyWorkspaceListChanged();
|
||||
};
|
||||
|
||||
const detachWorkspace = () => {
|
||||
@@ -148,17 +171,8 @@ export function MainChartsPage({
|
||||
|
||||
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);
|
||||
}
|
||||
onWorkspaceChange?.(null);
|
||||
|
||||
void openWorkspaceWindow(
|
||||
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(() => {
|
||||
setCharts([]);
|
||||
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 openCharts = charts.filter(
|
||||
@@ -343,31 +405,6 @@ export function MainChartsPage({
|
||||
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) => {
|
||||
setCharts((current) =>
|
||||
current.map((chart) =>
|
||||
@@ -641,13 +678,13 @@ export function MainChartsPage({
|
||||
<WorkspaceSelector
|
||||
theme={theme}
|
||||
workspaces={workspaceSelection.workspaces}
|
||||
activeWorkspaceId={workspaceSelection.activeWorkspaceId}
|
||||
activeWorkspaceId={activeWorkspaceId}
|
||||
detachedWorkspaceIds={detachedWorkspaceIds}
|
||||
loading={workspaceSelection.loading}
|
||||
creating={workspaceSelection.creating}
|
||||
canCreateWorkspace={workspaceSelection.canCreateWorkspace}
|
||||
onSelectWorkspace={workspaceSelection.selectWorkspace}
|
||||
onCreateWorkspace={(name) => void workspaceSelection.createWorkspace(name)}
|
||||
onSelectWorkspace={(workspaceId) => void selectWorkspace(workspaceId)}
|
||||
onCreateWorkspace={createWorkspace}
|
||||
onRenameWorkspace={(workspaceId, name) => void renameWorkspace(workspaceId, name)}
|
||||
onDeleteWorkspace={(workspaceId) => void deleteWorkspace(workspaceId)}
|
||||
/>
|
||||
@@ -780,12 +817,32 @@ export function MainChartsPage({
|
||||
{visibleCharts.length === 0 ? (
|
||||
<EmptyWorkspace
|
||||
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]"
|
||||
onAddChart={() => {
|
||||
if (activeWorkspaceId === null) {
|
||||
setNewWorkspaceOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (canAddMoreCharts) setNewChartOpen(true);
|
||||
}}
|
||||
onOpenSaved={() => setSavedOpen(true)}
|
||||
onOpenSaved={() => {
|
||||
if (activeWorkspaceId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSavedOpen(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<section className={layoutGridClass(layoutMode)}>
|
||||
@@ -799,7 +856,6 @@ export function MainChartsPage({
|
||||
movingChartId={movingChartId}
|
||||
setMovingChartId={setMovingChartId}
|
||||
swapCharts={swapCharts}
|
||||
duplicateChart={duplicateChart}
|
||||
closeChart={closeChart}
|
||||
setConfigChartId={setConfigChartId}
|
||||
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 && (
|
||||
<ChartConfigModal
|
||||
theme={theme}
|
||||
@@ -895,7 +962,6 @@ function WorkspaceChartContainer({
|
||||
movingChartId,
|
||||
setMovingChartId,
|
||||
swapCharts,
|
||||
duplicateChart,
|
||||
closeChart,
|
||||
setConfigChartId,
|
||||
setChartMode,
|
||||
@@ -914,7 +980,6 @@ function WorkspaceChartContainer({
|
||||
movingChartId: string | null;
|
||||
setMovingChartId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
swapCharts: (sourceId: string, targetId: string) => void;
|
||||
duplicateChart: (chartId: string) => void;
|
||||
closeChart: (chartId: string) => void | Promise<void>;
|
||||
setConfigChartId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
setChartMode: (chartId: string, mode: WorkspaceChartMode) => void;
|
||||
@@ -1004,10 +1069,6 @@ function WorkspaceChartContainer({
|
||||
<Cog className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<button type="button" title="Duplicar" className={floatingIconClass(theme)} onClick={() => duplicateChart(chartItem.id)}>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title="Fechar"
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
ChevronDown,
|
||||
Cog,
|
||||
Columns2,
|
||||
Copy,
|
||||
Grid2X2,
|
||||
GripVertical,
|
||||
LineChart,
|
||||
@@ -29,6 +28,7 @@ import {
|
||||
EmptyWorkspace,
|
||||
LayoutButton,
|
||||
ModeButton,
|
||||
NewWorkspaceModal,
|
||||
WorkspaceSelector,
|
||||
} from "../../chartworkspace/components/ChartWorkspaceControls";
|
||||
import { openWorkspaceWindow } from "../../chartworkspace/utils/openWorkspaceWindow";
|
||||
@@ -62,6 +62,7 @@ type MeteoChartsPageProps = {
|
||||
theme: "dark" | "light";
|
||||
workspaceId?: number | null;
|
||||
workspaceWindow?: boolean;
|
||||
onWorkspaceChange?: (workspaceId: number | null) => void;
|
||||
};
|
||||
|
||||
type HistorianPoint = {
|
||||
@@ -78,8 +79,9 @@ const INITIAL_CHARTS: ChartWorkspaceItem[] = [];
|
||||
|
||||
export function MeteoChartsPage({
|
||||
theme,
|
||||
workspaceId = null,
|
||||
workspaceId,
|
||||
workspaceWindow = false,
|
||||
onWorkspaceChange,
|
||||
}: MeteoChartsPageProps) {
|
||||
|
||||
const isDark = theme === "dark";
|
||||
@@ -96,6 +98,7 @@ export function MeteoChartsPage({
|
||||
const [savedOpen, setSavedOpen] = useState(false);
|
||||
const [movingChartId, setMovingChartId] = useState<string | null>(null);
|
||||
const [newChartOpen, setNewChartOpen] = useState(false);
|
||||
const [newWorkspaceOpen, setNewWorkspaceOpen] = useState(false);
|
||||
const [placingChartId, setPlacingChartId] = useState<string | null>(null);
|
||||
const [detachedWorkspaceIds, setDetachedWorkspaceIds] = useState<Set<number>>(() => new Set());
|
||||
|
||||
@@ -104,7 +107,8 @@ export function MeteoChartsPage({
|
||||
defaultName: "Workspace Meteo",
|
||||
});
|
||||
|
||||
const activeWorkspaceId = workspaceId ?? workspaceSelection.activeWorkspaceId;
|
||||
const activeWorkspaceId =
|
||||
workspaceId !== undefined ? workspaceId : workspaceSelection.activeWorkspaceId;
|
||||
const activeWorkspace =
|
||||
workspaceSelection.workspaces.find((workspace) => workspace.id === activeWorkspaceId) ??
|
||||
workspaceSelection.activeWorkspace;
|
||||
@@ -113,26 +117,38 @@ export function MeteoChartsPage({
|
||||
const workspaceWindowLabel = (workspaceId: number) =>
|
||||
`workspace-meteo-${workspaceId}`;
|
||||
|
||||
const renameWorkspace = async (workspaceId: number, name: string) => {
|
||||
const workspace = workspaceSelection.workspaces.find(
|
||||
(item) => item.id === workspaceId,
|
||||
const notifyWorkspaceListChanged = () => {
|
||||
window.dispatchEvent(
|
||||
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(
|
||||
"METEO",
|
||||
{
|
||||
name,
|
||||
sortOrder: workspace.sortOrder,
|
||||
defaultWorkspace: workspace.defaultWorkspace,
|
||||
layoutMode: workspace.layoutMode,
|
||||
chartsJson: workspace.chartsJson,
|
||||
},
|
||||
{ name },
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
workspaceSelection.updateWorkspaceName(workspaceId, name);
|
||||
notifyWorkspaceListChanged();
|
||||
|
||||
const existing = await WebviewWindow.getByLabel(
|
||||
workspaceWindowLabel(workspaceId),
|
||||
@@ -158,6 +174,13 @@ export function MeteoChartsPage({
|
||||
});
|
||||
|
||||
await workspaceSelection.deleteWorkspace(workspaceId);
|
||||
|
||||
if (activeWorkspaceId === workspaceId) {
|
||||
workspaceSelection.setActiveWorkspace(null);
|
||||
onWorkspaceChange?.(null);
|
||||
}
|
||||
|
||||
notifyWorkspaceListChanged();
|
||||
};
|
||||
|
||||
const detachWorkspace = () => {
|
||||
@@ -165,17 +188,8 @@ export function MeteoChartsPage({
|
||||
|
||||
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);
|
||||
}
|
||||
onWorkspaceChange?.(null);
|
||||
|
||||
void openWorkspaceWindow(
|
||||
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(() => {
|
||||
setCharts([]);
|
||||
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 openCharts = charts.filter(
|
||||
@@ -337,31 +399,6 @@ export function MeteoChartsPage({
|
||||
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) => {
|
||||
setCharts((current) =>
|
||||
current.map((chart) =>
|
||||
@@ -624,13 +661,13 @@ export function MeteoChartsPage({
|
||||
<WorkspaceSelector
|
||||
theme={theme}
|
||||
workspaces={workspaceSelection.workspaces}
|
||||
activeWorkspaceId={workspaceSelection.activeWorkspaceId}
|
||||
activeWorkspaceId={activeWorkspaceId}
|
||||
detachedWorkspaceIds={detachedWorkspaceIds}
|
||||
loading={workspaceSelection.loading}
|
||||
creating={workspaceSelection.creating}
|
||||
canCreateWorkspace={workspaceSelection.canCreateWorkspace}
|
||||
onSelectWorkspace={workspaceSelection.selectWorkspace}
|
||||
onCreateWorkspace={(name) => void workspaceSelection.createWorkspace(name)}
|
||||
onSelectWorkspace={(workspaceId) => void selectWorkspace(workspaceId)}
|
||||
onCreateWorkspace={createWorkspace}
|
||||
onRenameWorkspace={(workspaceId, name) => void renameWorkspace(workspaceId, name)}
|
||||
onDeleteWorkspace={(workspaceId) => void deleteWorkspace(workspaceId)}
|
||||
/>
|
||||
@@ -763,12 +800,31 @@ export function MeteoChartsPage({
|
||||
{visibleCharts.length === 0 ? (
|
||||
<EmptyWorkspace
|
||||
theme={theme}
|
||||
canAddMoreCharts={canAddMoreCharts}
|
||||
description="Abra um gráfico guardado ou crie um novo gráfico meteorológico para continuar."
|
||||
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."
|
||||
: "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={() => {
|
||||
if (activeWorkspaceId === null) {
|
||||
setNewWorkspaceOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (canAddMoreCharts) setNewChartOpen(true);
|
||||
}}
|
||||
onOpenSaved={() => setSavedOpen(true)}
|
||||
onOpenSaved={() => {
|
||||
if (activeWorkspaceId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSavedOpen(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<section className={layoutGridClass(layoutMode)}>
|
||||
@@ -783,7 +839,6 @@ export function MeteoChartsPage({
|
||||
movingChartId={movingChartId}
|
||||
setMovingChartId={setMovingChartId}
|
||||
swapCharts={swapCharts}
|
||||
duplicateChart={duplicateChart}
|
||||
closeChart={closeChart}
|
||||
setConfigChartId={setConfigChartId}
|
||||
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 && (
|
||||
<ChartConfigModal
|
||||
theme={theme}
|
||||
@@ -889,7 +955,6 @@ function WorkspaceChartContainer({
|
||||
movingChartId,
|
||||
setMovingChartId,
|
||||
swapCharts,
|
||||
duplicateChart,
|
||||
closeChart,
|
||||
setConfigChartId,
|
||||
setChartMode,
|
||||
@@ -907,7 +972,6 @@ function WorkspaceChartContainer({
|
||||
movingChartId: string | null;
|
||||
setMovingChartId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
swapCharts: (sourceId: string, targetId: string) => void;
|
||||
duplicateChart: (chartId: string) => void;
|
||||
closeChart: (chartId: string) => void | Promise<void>;
|
||||
setConfigChartId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
setChartMode: (chartId: string, mode: WorkspaceChartMode) => void;
|
||||
@@ -992,15 +1056,6 @@ function WorkspaceChartContainer({
|
||||
<Cog className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title="Duplicar"
|
||||
className={floatingIconClass(theme)}
|
||||
onClick={() => duplicateChart(chartItem.id)}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title="Fechar"
|
||||
|
||||
@@ -8,10 +8,12 @@ import {
|
||||
} from "react";
|
||||
|
||||
import { fetchRuntimeConfig } from "../../lib/api/systemApi";
|
||||
import { ApiResponseError } from "../../lib/api/readJsonResponse";
|
||||
import {
|
||||
clearRuntimeConfig,
|
||||
setRuntimeConfig as setRuntimeConfigSnapshot,
|
||||
} from "../../lib/api/runtimeConfigStore";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import type { RuntimeConfig } from "../../types/system";
|
||||
|
||||
type RuntimeConfigContextValue = {
|
||||
@@ -23,6 +25,7 @@ const RuntimeConfigContext = createContext<RuntimeConfigContextValue | null>(
|
||||
);
|
||||
|
||||
export function RuntimeConfigProvider({ children }: { children: ReactNode }) {
|
||||
const { logout } = useAuth();
|
||||
const [runtimeConfig, setRuntimeConfig] = useState<RuntimeConfig | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -41,6 +44,15 @@ export function RuntimeConfigProvider({ children }: { children: ReactNode }) {
|
||||
if (cancelled) return;
|
||||
|
||||
clearRuntimeConfig();
|
||||
|
||||
if (
|
||||
exception instanceof ApiResponseError &&
|
||||
(exception.status === 401 || exception.status === 403)
|
||||
) {
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
|
||||
setError(
|
||||
exception instanceof Error
|
||||
? exception.message
|
||||
@@ -52,7 +64,7 @@ export function RuntimeConfigProvider({ children }: { children: ReactNode }) {
|
||||
cancelled = true;
|
||||
clearRuntimeConfig();
|
||||
};
|
||||
}, []);
|
||||
}, [logout]);
|
||||
|
||||
const value = useMemo<RuntimeConfigContextValue | null>(
|
||||
() => (runtimeConfig ? { runtimeConfig } : null),
|
||||
|
||||
@@ -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>(
|
||||
response: Response,
|
||||
context: string,
|
||||
@@ -5,7 +16,11 @@ export async function readJsonResponse<T>(
|
||||
const text = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${context}: ${response.status} ${text}`.trim());
|
||||
throw new ApiResponseError(
|
||||
`${context}: ${response.status} ${text}`.trim(),
|
||||
response.status,
|
||||
text,
|
||||
);
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
@@ -23,7 +38,11 @@ export async function readOptionalJsonResponse<T>(
|
||||
const text = await response.text();
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user