update UI

This commit is contained in:
2026-03-13 22:37:02 +08:00
parent 23edd0faab
commit 43289845fc
8 changed files with 440 additions and 87 deletions

View File

@@ -27,6 +27,7 @@ import {
import { createStyles } from "antd-style";
import zhCN from "antd/locale/zh_CN";
import { useDomainStore } from "@renderer/stores";
import { useUIStore } from "@renderer/stores";
import { DomainManager } from "@renderer/components/DomainManager";
import { AppList } from "@renderer/components/AppList";
import { AppDetail } from "@renderer/components/AppDetail";
@@ -153,10 +154,15 @@ const App: React.FC = () => {
} = theme.useToken();
const { currentDomain } = useDomainStore();
const {
sidebarWidth,
siderCollapsed,
domainExpanded,
setSidebarWidth,
setSiderCollapsed,
setDomainExpanded,
} = useUIStore();
const [deployDialogOpen, setDeployDialogOpen] = React.useState(false);
const [domainExpanded, setDomainExpanded] = React.useState(true);
const [siderCollapsed, setSiderCollapsed] = React.useState(false);
const [siderWidth, setSiderWidth] = React.useState(DEFAULT_SIDER_WIDTH);
const [isResizing, setIsResizing] = React.useState(false);
const domainSectionHeight = domainExpanded
@@ -170,7 +176,7 @@ const App: React.FC = () => {
setIsResizing(true);
const startX = e.clientX;
const startWidth = siderWidth;
const startWidth = sidebarWidth;
const handleMouseMove = (moveEvent: MouseEvent) => {
const delta = moveEvent.clientX - startX;
@@ -178,7 +184,7 @@ const App: React.FC = () => {
MAX_SIDER_WIDTH,
Math.max(MIN_SIDER_WIDTH, startWidth + delta),
);
setSiderWidth(newWidth);
setSidebarWidth(newWidth);
};
const handleMouseUp = () => {
@@ -190,7 +196,7 @@ const App: React.FC = () => {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[siderWidth],
[sidebarWidth, setSidebarWidth],
);
const toggleSider = () => {
@@ -203,9 +209,9 @@ const App: React.FC = () => {
<Layout className={styles.layout}>
{/* Left Sider - Domain List & App List */}
<Sider
width={siderWidth}
width={sidebarWidth}
className={`${styles.sider} ${siderCollapsed ? styles.siderCollapsed : ""}`}
style={{ width: siderCollapsed ? 0 : siderWidth }}
style={{ width: siderCollapsed ? 0 : sidebarWidth }}
>
{!siderCollapsed && (
<>
@@ -260,7 +266,7 @@ const App: React.FC = () => {
{/* Main Content */}
<Layout
className={`${styles.mainLayout} ${siderCollapsed ? styles.mainLayoutCollapsed : ""}`}
style={{ marginLeft: siderCollapsed ? 0 : siderWidth }}
style={{ marginLeft: siderCollapsed ? 0 : sidebarWidth }}
>
<Header className={styles.header}>
<div className={styles.headerLeft}>

View File

@@ -1,18 +1,33 @@
/**
* AppList Component
* Displays apps in a clickable list (no pagination)
* Displays apps in a clickable list with sorting and pinning
*/
import React from "react";
import { List, Button, Input, Empty, Spin, Typography, Space } from "antd";
import {
List,
Button,
Input,
Empty,
Spin,
Typography,
Space,
Tooltip,
Select,
} from "antd";
import {
ReloadOutlined,
SearchOutlined,
AppstoreOutlined,
PushpinOutlined,
PushpinFilled,
SortAscendingOutlined,
SortDescendingOutlined,
} from "@ant-design/icons";
import { createStyles } from "antd-style";
import { useAppStore } from "@renderer/stores";
import { useDomainStore } from "@renderer/stores";
import { useUIStore } from "@renderer/stores";
import type { AppDetail } from "@shared/types/kintone";
const { Text } = Typography;
@@ -35,7 +50,7 @@ const useStyles = createStyles(({ token, css }) => ({
`,
searchWrapper: css`
flex: 1;
max-width: 300px;
max-width: 200px;
`,
content: css`
flex: 1;
@@ -68,6 +83,9 @@ const useStyles = createStyles(({ token, css }) => ({
background: ${token.colorPrimaryBgHover} !important;
border-left: 3px solid ${token.colorPrimary} !important;
`,
listItemPinned: css`
background: ${token.colorWarningBg} !important;
`,
appInfo: css`
display: flex;
align-items: center;
@@ -100,6 +118,18 @@ const useStyles = createStyles(({ token, css }) => ({
color: ${token.colorTextSecondary};
font-size: 12px;
`,
pinIcon: css`
color: ${token.colorTextTertiary};
cursor: pointer;
font-size: 14px;
&:hover {
color: ${token.colorWarning};
}
`,
pinIconPinned: css`
color: ${token.colorWarning};
`,
}));
const AppList: React.FC = () => {
@@ -118,6 +148,18 @@ const AppList: React.FC = () => {
setSearchText,
setSelectedAppId,
} = useAppStore();
const {
pinnedApps,
appSortBy,
appSortOrder,
togglePinnedApp,
setAppSortBy,
setAppSortOrder,
} = useUIStore();
const currentPinnedApps = currentDomain
? pinnedApps[currentDomain.id] || []
: [];
// Load apps from Kintone
const handleLoadApps = async () => {
@@ -132,11 +174,7 @@ const AppList: React.FC = () => {
});
if (result.success) {
// Sort by appId descending (newest first)
const sortedApps = [...result.data].sort(
(a, b) => parseInt(b.appId, 10) - parseInt(a.appId, 10),
);
setApps(sortedApps);
setApps(result.data);
} else {
setError(result.error || "加载应用失败");
}
@@ -147,35 +185,107 @@ const AppList: React.FC = () => {
}
};
// Filter apps by search text
const filteredApps = React.useMemo(() => {
if (!searchText) return apps;
// Sort apps
const sortApps = (appsToSort: typeof apps) => {
const sorted = [...appsToSort];
sorted.sort((a, b) => {
let comparison = 0;
switch (appSortBy) {
case "name":
comparison = a.name.localeCompare(b.name);
break;
case "appId":
comparison = parseInt(a.appId, 10) - parseInt(b.appId, 10);
break;
default:
comparison = parseInt(a.appId, 10) - parseInt(b.appId, 10);
}
return appSortOrder === "asc" ? comparison : -comparison;
});
return sorted;
};
// Filter and sort apps
const processedApps = React.useMemo(() => {
let filtered = apps;
// Filter by search text
if (searchText) {
const lowerSearch = searchText.toLowerCase();
return apps.filter(
filtered = apps.filter(
(app) =>
app.name.toLowerCase().includes(lowerSearch) ||
app.appId.includes(searchText) ||
(app.code && app.code.toLowerCase().includes(lowerSearch)),
);
}, [apps, searchText]);
}
// Sort
return sortApps(filtered);
}, [apps, searchText, appSortBy, appSortOrder]);
// Separate pinned and unpinned apps
const { pinnedAppsList, unpinnedAppsList } = React.useMemo(() => {
const pinned: typeof apps = [];
const unpinned: typeof apps = [];
processedApps.forEach((app) => {
if (currentPinnedApps.includes(app.appId)) {
pinned.push(app);
} else {
unpinned.push(app);
}
});
return { pinnedAppsList: pinned, unpinnedAppsList: unpinned };
}, [processedApps, currentPinnedApps]);
// Final display list: pinned first, then unpinned
const displayApps = [...pinnedAppsList, ...unpinnedAppsList];
// Handle item click
const handleItemClick = (app: AppDetail) => {
setSelectedAppId(app.appId);
};
// Handle pin toggle
const handlePinToggle = (e: React.MouseEvent, appId: string) => {
e.stopPropagation();
if (currentDomain) {
togglePinnedApp(currentDomain.id, appId);
}
};
// Toggle sort order
const toggleSortOrder = () => {
setAppSortOrder(appSortOrder === "asc" ? "desc" : "asc");
};
// Render list item
const renderItem = (app: AppDetail) => {
const isActive = selectedAppId === app.appId;
const isPinned = currentPinnedApps.includes(app.appId);
return (
<List.Item
className={`${styles.listItem} ${isActive ? styles.listItemActive : ""}`}
className={`${styles.listItem} ${isActive ? styles.listItemActive : ""} ${isPinned ? styles.listItemPinned : ""}`}
onClick={() => handleItemClick(app)}
>
<div className={styles.appInfo}>
<Tooltip title={isPinned ? "取消置顶" : "置顶应用"}>
<span
className={`${styles.pinIcon} ${isPinned ? styles.pinIconPinned : ""}`}
onClick={(e) => handlePinToggle(e, app.appId)}
>
{isPinned ? <PushpinFilled /> : <PushpinOutlined />}
</span>
</Tooltip>
<AppstoreOutlined style={{ color: "#1890ff", fontSize: 16 }} />
<Tooltip title={app.name}>
<span className={styles.appName}>{app.name}</span>
</Tooltip>
<Text code className={styles.appId}>
ID: {app.appId}
</Text>
@@ -194,7 +304,7 @@ const AppList: React.FC = () => {
return (
<div className={styles.container}>
{/* Header with search and load button */}
{/* Header with search, sort and load button */}
<div className={styles.header}>
<div className={styles.searchWrapper}>
<Input
@@ -204,16 +314,44 @@ const AppList: React.FC = () => {
onChange={(e) => setSearchText(e.target.value)}
allowClear
disabled={apps.length === 0 && !searchText}
size="small"
/>
</div>
<Space>
<Select
value={appSortBy}
onChange={setAppSortBy}
size="small"
options={[
{ label: "应用ID", value: "appId" },
{ label: "名称", value: "name" },
]}
style={{ width: 90 }}
/>
<Tooltip title={appSortOrder === "asc" ? "升序" : "降序"}>
<Button
type="primary"
type="text"
size="small"
icon={
appSortOrder === "asc" ? (
<SortAscendingOutlined />
) : (
<SortDescendingOutlined />
)
}
onClick={toggleSortOrder}
/>
</Tooltip>
<Tooltip title={apps.length > 0 ? "重新加载" : "加载应用"}>
<Button
type="text"
icon={<ReloadOutlined />}
onClick={handleLoadApps}
loading={loading}
>
{apps.length > 0 ? "重新加载" : "加载应用"}
</Button>
size="small"
/>
</Tooltip>
</Space>
</div>
{/* Content */}
@@ -235,7 +373,7 @@ const AppList: React.FC = () => {
</div>
) : (
<List
dataSource={filteredApps}
dataSource={displayApps}
renderItem={renderItem}
locale={{ emptyText: "没有匹配的应用" }}
/>
@@ -252,7 +390,7 @@ const AppList: React.FC = () => {
</Text>
)}
<Text type="secondary" style={{ marginLeft: 16 }}>
{filteredApps.length}
{displayApps.length}
</Text>
</div>
</div>

View File

@@ -218,15 +218,17 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
</Form.Item>
<Form.Item style={{ marginTop: 24, marginBottom: 0 }}>
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
<Button onClick={onClose}></Button>
<div style={{ display: "flex", justifyContent: "space-between", gap: 8 }}>
<Button onClick={handleTestConnection} loading={testing}>
</Button>
<div style={{ display: "flex", gap: 8 }}>
<Button onClick={onClose}></Button>
<Button type="primary" onClick={handleSubmit} loading={loading}>
{isEdit ? "更新" : "创建"}
</Button>
</div>
</div>
</Form.Item>
</Form>
</Modal>

View File

@@ -1,6 +1,6 @@
/**
* DomainList Component
* Displays list of domains with connection status
* Displays list of domains with connection status and drag-to-reorder
*/
import React from "react";
@@ -12,9 +12,10 @@ import {
CheckCircleOutlined,
CloseCircleOutlined,
QuestionCircleOutlined,
HolderOutlined,
} from "@ant-design/icons";
import { createStyles } from "antd-style";
import { useDomainStore } from "@renderer/stores";
import { useDomainStore, useUIStore } from "@renderer/stores";
import type { Domain } from "@shared/types/domain";
const useStyles = createStyles(({ token, css }) => ({
@@ -25,6 +26,7 @@ const useStyles = createStyles(({ token, css }) => ({
border: 1px solid ${token.colorBorderSecondary};
margin-bottom: ${token.marginSM}px;
transition: all 0.2s;
cursor: pointer;
&:hover {
border-color: ${token.colorPrimary};
@@ -35,18 +37,30 @@ const useStyles = createStyles(({ token, css }) => ({
border-color: ${token.colorPrimary};
background: ${token.colorPrimaryBg};
`,
itemDragging: css`
opacity: 0.5;
border: 2px dashed ${token.colorPrimary};
`,
domainInfo: css`
display: flex;
align-items: center;
gap: ${token.paddingSM}px;
flex: 1;
min-width: 0;
`,
domainName: css`
font-weight: ${token.fontWeightStrong};
font-size: ${token.fontSizeLG}px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`,
domainUrl: css`
color: ${token.colorTextSecondary};
font-size: ${token.fontSizeSM}px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`,
actions: css`
display: flex;
@@ -55,6 +69,31 @@ const useStyles = createStyles(({ token, css }) => ({
statusTag: css`
margin-left: ${token.paddingSM}px;
`,
dragHandle: css`
cursor: grab;
color: ${token.colorTextTertiary};
display: flex;
align-items: center;
&:hover {
color: ${token.colorText};
}
&:active {
cursor: grabbing;
}
`,
itemContent: css`
display: flex;
justify-content: space-between;
align-items: center;
gap: ${token.paddingSM}px;
`,
domainText: css`
flex: 1;
min-width: 0;
overflow: hidden;
`,
}));
interface DomainListProps {
@@ -70,7 +109,10 @@ const DomainList: React.FC<DomainListProps> = ({ onEdit }) => {
switchDomain,
deleteDomain,
testConnection,
reorderDomains,
} = useDomainStore();
const { domainIconColors } = useUIStore();
const [draggingIndex, setDraggingIndex] = React.useState<number | null>(null);
const handleSelect = (domain: Domain) => {
switchDomain(domain);
@@ -104,32 +146,63 @@ const DomainList: React.FC<DomainListProps> = ({ onEdit }) => {
}
};
const getDomainIconColor = (domainId: string, isSelected: boolean) => {
if (domainIconColors[domainId]) {
return domainIconColors[domainId];
}
return isSelected ? "#1890ff" : "#87d068";
};
// Drag and drop handlers
const handleDragStart = (index: number) => {
setDraggingIndex(index);
};
const handleDragOver = (e: React.DragEvent, index: number) => {
e.preventDefault();
if (draggingIndex === null || draggingIndex === index) return;
};
const handleDrop = (e: React.DragEvent, index: number) => {
e.preventDefault();
if (draggingIndex === null || draggingIndex === index) return;
reorderDomains(draggingIndex, index);
setDraggingIndex(null);
};
const handleDragEnd = () => {
setDraggingIndex(null);
};
return (
<List
dataSource={domains}
renderItem={(domain) => {
renderItem={(domain, index) => {
const isSelected = currentDomain?.id === domain.id;
const isDragging = draggingIndex === index;
return (
<div
className={`${styles.item} ${isSelected ? styles.selectedItem : ""}`}
className={`${styles.item} ${isSelected ? styles.selectedItem : ""} ${isDragging ? styles.itemDragging : ""}`}
onClick={() => handleSelect(domain)}
draggable
onDragStart={() => handleDragStart(index)}
onDragOver={(e) => handleDragOver(e, index)}
onDrop={(e) => handleDrop(e, index)}
onDragEnd={handleDragEnd}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div className={styles.itemContent}>
<div className={styles.dragHandle}>
<HolderOutlined />
</div>
<div className={styles.domainInfo}>
<Avatar
icon={<CloudServerOutlined />}
style={{
backgroundColor: isSelected ? "#1890ff" : "#87d068",
backgroundColor: getDomainIconColor(domain.id, isSelected),
}}
/>
<div>
<div className={styles.domainText}>
<div className={styles.domainName}>
{domain.name}
{getStatusTag(domain.id)}

View File

@@ -9,13 +9,12 @@ import React from "react";
import { Button, Empty, Spin, Tooltip, Avatar, Space } from "antd";
import {
PlusOutlined,
ReloadOutlined,
CloudServerOutlined,
UpOutlined,
DownOutlined,
} from "@ant-design/icons";
import { createStyles } from "antd-style";
import { useDomainStore } from "@renderer/stores";
import { useDomainStore, useUIStore } from "@renderer/stores";
import DomainList from "./DomainList";
import DomainForm from "./DomainForm";
@@ -104,24 +103,31 @@ const useStyles = createStyles(({ token, css }) => ({
font-size: ${token.fontSizeSM}px;
padding: ${token.paddingSM}px ${token.paddingMD}px;
`,
// Bottom toggle area - click to expand/collapse
bottomToggleArea: css`
// Bottom toggle icon - minimal style
bottomToggleIcon: css`
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
padding: ${token.paddingSM}px;
cursor: pointer;
border-top: 1px solid ${token.colorBorderSecondary};
transition: background 0.2s;
color: ${token.colorTextSecondary};
color: ${token.colorTextTertiary};
font-size: 12px;
gap: ${token.paddingXS}px;
z-index: 10;
opacity: 0;
transition: all 0.2s;
&:hover {
background: ${token.colorBgTextHover};
color: ${token.colorPrimary};
opacity: 1;
background: ${token.colorFillTertiary};
}
`,
bottomToggleVisible: css`
opacity: 0.6;
`,
}));
interface DomainManagerProps {
@@ -135,8 +141,10 @@ const DomainManager: React.FC<DomainManagerProps> = ({
}) => {
const { styles } = useStyles();
const { domains, loading, loadDomains, currentDomain } = useDomainStore();
const { domainIconColors } = useUIStore();
const [formOpen, setFormOpen] = React.useState(false);
const [editingDomain, setEditingDomain] = React.useState<string | null>(null);
const [isHoveringToggle, setIsHoveringToggle] = React.useState(false);
React.useEffect(() => {
loadDomains();
@@ -157,9 +165,11 @@ const DomainManager: React.FC<DomainManagerProps> = ({
setEditingDomain(null);
};
const handleRefresh = () => {
loadDomains();
const getDomainIconColor = (domainId: string | undefined) => {
if (!domainId) return "#d9d9d9";
return domainIconColors[domainId] || "#1890ff";
};
// Collapsed view - show current domain only
if (collapsed) {
return (
@@ -170,7 +180,7 @@ const DomainManager: React.FC<DomainManagerProps> = ({
icon={<CloudServerOutlined />}
size="small"
style={{
backgroundColor: currentDomain ? "#1890ff" : "#d9d9d9",
backgroundColor: getDomainIconColor(currentDomain?.id),
}}
/>
<div style={{ flex: 1, minWidth: 0 }}>
@@ -201,10 +211,14 @@ const DomainManager: React.FC<DomainManagerProps> = ({
</Tooltip>
</div>
{/* Bottom toggle area - click to expand */}
<div className={styles.bottomToggleArea} onClick={onToggleCollapse}>
{/* Bottom toggle icon */}
<div
className={`${styles.bottomToggleIcon} ${isHoveringToggle ? styles.bottomToggleVisible : ""}`}
onClick={onToggleCollapse}
onMouseEnter={() => setIsHoveringToggle(true)}
onMouseLeave={() => setIsHoveringToggle(false)}
>
<DownOutlined />
<span></span>
</div>
<DomainForm
@@ -218,17 +232,10 @@ const DomainManager: React.FC<DomainManagerProps> = ({
// Expanded view - full list
return (
<div className={styles.container}>
<div className={styles.container} style={{ position: "relative" }}>
<div className={styles.header}>
<h2 className={styles.title}>Domain </h2>
<div className={styles.actions}>
<Tooltip title="刷新">
<Button
icon={<ReloadOutlined />}
onClick={handleRefresh}
loading={loading}
/>
</Tooltip>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
</Button>
@@ -254,10 +261,14 @@ const DomainManager: React.FC<DomainManagerProps> = ({
)}
</div>
{/* Bottom toggle area - click to collapse */}
<div className={styles.bottomToggleArea} onClick={onToggleCollapse}>
{/* Bottom toggle icon */}
<div
className={`${styles.bottomToggleIcon} ${isHoveringToggle ? styles.bottomToggleVisible : ""}`}
onClick={onToggleCollapse}
onMouseEnter={() => setIsHoveringToggle(true)}
onMouseLeave={() => setIsHoveringToggle(false)}
>
<UpOutlined />
<span></span>
</div>
<DomainForm

View File

@@ -26,6 +26,8 @@ interface DomainState {
addDomain: (domain: Domain) => void;
updateDomain: (domain: Domain) => void;
removeDomain: (id: string) => void;
reorderDomains: (fromIndex: number, toIndex: number) => void;
setCurrentDomain: (domain: Domain | null) => void;
setCurrentDomain: (domain: Domain | null) => void;
setConnectionStatus: (
id: string,
@@ -80,6 +82,14 @@ export const useDomainStore = create<DomainState>()(
state.currentDomain?.id === id ? null : state.currentDomain,
})),
reorderDomains: (fromIndex, toIndex) =>
set((state) => {
const domains = [...state.domains];
const [removed] = domains.splice(fromIndex, 1);
domains.splice(toIndex, 0, removed);
return { domains };
}),
setCurrentDomain: (domain) => set({ currentDomain: domain }),
setConnectionStatus: (id, status, error) =>
@@ -116,6 +126,15 @@ export const useDomainStore = create<DomainState>()(
},
createDomain: async (params: CreateDomainParams) => {
// Check for duplicate domain
const existingDomain = get().domains.find(
(d) => d.domain === params.domain && d.username === params.username
);
if (existingDomain) {
set({ error: "该 Domain 已存在", loading: false });
return false;
}
set({ loading: true, error: null });
try {
const result = await window.api.createDomain(params);

View File

@@ -7,3 +7,5 @@ export { useDomainStore } from "./domainStore";
export { useAppStore } from "./appStore";
export { useDeployStore } from "./deployStore";
export { useVersionStore } from "./versionStore";
export { useUIStore } from "./uiStore";

View File

@@ -0,0 +1,102 @@
/**
* UI Store
* Manages UI state persistence (sidebar width, collapsed states, etc.)
*/
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface UIState {
// Sidebar state
sidebarWidth: number;
siderCollapsed: boolean;
domainExpanded: boolean;
// Domain customizations
domainIconColors: Record<string, string>;
// App customizations
pinnedApps: Record<string, string[]>; // domainId -> appId[]
appSortBy: "createdAt" | "modifiedAt" | "name" | "appId";
appSortOrder: "asc" | "desc";
// Actions
setSidebarWidth: (width: number) => void;
setSiderCollapsed: (collapsed: boolean) => void;
setDomainExpanded: (expanded: boolean) => void;
setDomainIconColor: (domainId: string, color: string) => void;
setPinnedApps: (domainId: string, appIds: string[]) => void;
togglePinnedApp: (domainId: string, appId: string) => void;
setAppSortBy: (sortBy: "createdAt" | "modifiedAt" | "name" | "appId") => void;
setAppSortOrder: (order: "asc" | "desc") => void;
}
const MIN_SIDEBAR_WIDTH = 280;
const MAX_SIDEBAR_WIDTH = 500;
const DEFAULT_SIDEBAR_WIDTH = 360;
export const useUIStore = create<UIState>()(
persist(
(set, get) => ({
// Initial state
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
siderCollapsed: false,
domainExpanded: true,
domainIconColors: {},
pinnedApps: {},
appSortBy: "appId",
appSortOrder: "desc",
// Actions
setSidebarWidth: (width) =>
set({
sidebarWidth: Math.min(
MAX_SIDEBAR_WIDTH,
Math.max(MIN_SIDEBAR_WIDTH, width),
),
}),
setSiderCollapsed: (collapsed) => set({ siderCollapsed: collapsed }),
setDomainExpanded: (expanded) => set({ domainExpanded: expanded }),
setDomainIconColor: (domainId, color) =>
set((state) => ({
domainIconColors: { ...state.domainIconColors, [domainId]: color },
})),
setPinnedApps: (domainId, appIds) =>
set((state) => ({
pinnedApps: { ...state.pinnedApps, [domainId]: appIds },
})),
togglePinnedApp: (domainId, appId) =>
set((state) => {
const currentPinned = state.pinnedApps[domainId] || [];
const isPinned = currentPinned.includes(appId);
const newPinned = isPinned
? currentPinned.filter((id) => id !== appId)
: [appId, ...currentPinned];
return {
pinnedApps: { ...state.pinnedApps, [domainId]: newPinned },
};
}),
setAppSortBy: (sortBy) => set({ appSortBy: sortBy }),
setAppSortOrder: (order) => set({ appSortOrder: order }),
}),
{
name: "ui-storage",
partialize: (state) => ({
sidebarWidth: state.sidebarWidth,
siderCollapsed: state.siderCollapsed,
domainExpanded: state.domainExpanded,
domainIconColors: state.domainIconColors,
pinnedApps: state.pinnedApps,
appSortBy: state.appSortBy,
appSortOrder: state.appSortOrder,
}),
},
),
);