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 { 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") {
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user