Files
homarr/apps/nextjs/src/components/board/sections/use-open-section-apps.ts
T

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,
};
};