111 lines
3.3 KiB
TypeScript
111 lines
3.3 KiB
TypeScript
import { useCallback, useMemo } from "react";
|
|
|
|
import { clientApi } from "@homarr/api/client";
|
|
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
|
|
import { useConfirmModal } from "@homarr/modals";
|
|
import { useSettings } from "@homarr/settings";
|
|
import { useI18n } from "@homarr/translation/client";
|
|
|
|
import type { Item } from "~/app/[locale]/boards/_types";
|
|
import { filterByItemKind } from "./category/filter";
|
|
|
|
interface SectionItemLayout {
|
|
layoutId: string;
|
|
sectionId: string;
|
|
}
|
|
|
|
interface SectionLayout {
|
|
layoutId: string;
|
|
parentSectionId: string;
|
|
}
|
|
|
|
interface SectionTreeItem {
|
|
layouts: readonly SectionItemLayout[];
|
|
}
|
|
|
|
interface SectionTreeSection {
|
|
id: string;
|
|
kind: string;
|
|
layouts?: readonly SectionLayout[];
|
|
}
|
|
|
|
interface SectionTree<TItem extends SectionTreeItem> {
|
|
items: readonly TItem[];
|
|
sections: readonly SectionTreeSection[];
|
|
}
|
|
|
|
/**
|
|
* Finds every item contained by a section in one responsive layout. Dynamic
|
|
* sections can be nested, so their descendants are traversed recursively.
|
|
*/
|
|
export const getSectionItemsForLayout = <TItem extends SectionTreeItem>(
|
|
board: SectionTree<TItem>,
|
|
rootSectionId: string,
|
|
layoutId: string,
|
|
) => {
|
|
const childrenByParent = new Map<string, string[]>();
|
|
|
|
for (const section of board.sections) {
|
|
if (section.kind !== "dynamic") continue;
|
|
|
|
const layout = section.layouts?.find((candidate) => candidate.layoutId === layoutId);
|
|
if (!layout) continue;
|
|
|
|
const children = childrenByParent.get(layout.parentSectionId) ?? [];
|
|
children.push(section.id);
|
|
childrenByParent.set(layout.parentSectionId, children);
|
|
}
|
|
|
|
const includedSectionIds = new Set([rootSectionId]);
|
|
const queue = [rootSectionId];
|
|
|
|
for (const sectionId of queue) {
|
|
for (const childSectionId of childrenByParent.get(sectionId) ?? []) {
|
|
if (includedSectionIds.has(childSectionId)) continue;
|
|
|
|
includedSectionIds.add(childSectionId);
|
|
queue.push(childSectionId);
|
|
}
|
|
}
|
|
|
|
return board.items.filter((item) =>
|
|
item.layouts.some((layout) => layout.layoutId === layoutId && includedSectionIds.has(layout.sectionId)),
|
|
);
|
|
};
|
|
|
|
export const useOpenSectionApps = (sectionId: string, enabled: boolean) => {
|
|
const board = useRequiredBoard();
|
|
const currentLayoutId = useCurrentLayout();
|
|
const settings = useSettings();
|
|
const { openConfirmModal } = useConfirmModal();
|
|
const t = useI18n();
|
|
const appIds = useMemo(() => {
|
|
const items: Item[] = getSectionItemsForLayout(board, sectionId, currentLayoutId);
|
|
return Array.from(new Set(filterByItemKind(items, settings, "app").map((item) => item.options.appId)));
|
|
}, [board, currentLayoutId, sectionId, settings]);
|
|
const { data: apps = [], isLoading } = clientApi.app.byIds.useQuery(appIds, {
|
|
enabled: enabled && appIds.length > 0,
|
|
staleTime: 30_000,
|
|
});
|
|
|
|
const open = useCallback(() => {
|
|
const appsWithUrls = apps.filter((app) => app.href && app.href.length > 0);
|
|
|
|
for (const app of appsWithUrls) {
|
|
const openedWindow = window.open(app.href ?? undefined);
|
|
if (openedWindow) continue;
|
|
|
|
openConfirmModal({
|
|
title: t("section.openAllInNewTabs.title"),
|
|
children: t("section.openAllInNewTabs.text"),
|
|
});
|
|
break;
|
|
}
|
|
}, [apps, openConfirmModal, t]);
|
|
|
|
return {
|
|
open,
|
|
isLoading: enabled && appIds.length > 0 && isLoading,
|
|
};
|
|
};
|