дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
Services followed configuration detection order, so frequently used services could not be kept together. Services can now be dragged before or after a sibling, with an insertion indicator and edge scrolling, or moved with Move Up/Down from the toolbar and context menu. The order is stored per project location and type, separately from configuration definitions. New configurations append, windows stay in sync, and a failed save keeps the order for the session with a warning. Sorting in Tree is opt-in, so other trees are unchanged. Co-authored-by: wangxga <[email protected]>
81 рядки
3.0 KiB
TypeScript
81 рядки
3.0 KiB
TypeScript
/** Per-project display order, independent of detected or saved run configurations. */
|
|
import { useEffect } from "react";
|
|
import { create } from "zustand";
|
|
|
|
export type ServiceOrder = Record<string, string[]>;
|
|
const STORAGE_KEY = "openidea.services.runOrder:";
|
|
const EMPTY: ServiceOrder = {};
|
|
const unsaved = new Map<string, ServiceOrder>();
|
|
const useOrder = create<{ location: string | null; order: ServiceOrder }>(() => ({ location: null, order: EMPTY }));
|
|
|
|
export function readServiceOrder(location: string): ServiceOrder {
|
|
const pending = unsaved.get(location);
|
|
if (pending) return pending;
|
|
try {
|
|
const raw: unknown = JSON.parse(localStorage.getItem(STORAGE_KEY + location) ?? "{}");
|
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
return Object.fromEntries(
|
|
Object.entries(raw).flatMap(([kind, ids]) =>
|
|
Array.isArray(ids)
|
|
? [[kind, [...new Set(ids.filter((id): id is string => typeof id === "string" && id.length > 0))]]]
|
|
: [],
|
|
),
|
|
);
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
export function orderServices<T extends { id: string }>(entries: readonly T[], saved: readonly string[] = []): T[] {
|
|
const rank = new Map(saved.map((id, i) => [id, i]));
|
|
return [...entries].sort((a, b) => (rank.get(a.id) ?? saved.length) - (rank.get(b.id) ?? saved.length));
|
|
}
|
|
|
|
export function moveServiceOrder(
|
|
saved: readonly string[],
|
|
ids: readonly string[],
|
|
source: string,
|
|
target: string,
|
|
position: "before" | "after",
|
|
): string[] {
|
|
if (source === target || !ids.includes(source) || !ids.includes(target)) return [...saved];
|
|
const order = [...new Set([...saved, ...ids])].filter((id) => id !== source);
|
|
order.splice(order.indexOf(target) + (position === "after" ? 1 : 0), 0, source);
|
|
return order;
|
|
}
|
|
|
|
export function currentServiceOrder(location: string): ServiceOrder {
|
|
const s = useOrder.getState();
|
|
return s.location === location ? s.order : readServiceOrder(location);
|
|
}
|
|
|
|
/** Returns false if only the in-memory order could be saved. */
|
|
export function saveServiceOrder(location: string, order: ServiceOrder): boolean {
|
|
useOrder.setState({ location, order });
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY + location, JSON.stringify(order));
|
|
unsaved.delete(location);
|
|
return true;
|
|
} catch {
|
|
unsaved.set(location, order);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function useServiceOrder(location: string | null): ServiceOrder {
|
|
const state = useOrder();
|
|
useEffect(() => {
|
|
const reload = () => useOrder.setState({ location, order: location ? readServiceOrder(location) : EMPTY });
|
|
reload();
|
|
const storage = (e: StorageEvent) => {
|
|
if ((e.key === null || e.key === STORAGE_KEY + location) && (!e.storageArea || e.storageArea === localStorage)) {
|
|
if (location) unsaved.delete(location);
|
|
reload();
|
|
}
|
|
};
|
|
window.addEventListener("storage", storage);
|
|
return () => window.removeEventListener("storage", storage);
|
|
}, [location]);
|
|
return state.location === location ? state.order : location ? readServiceOrder(location) : EMPTY;
|
|
}
|