diff --git a/apps/nextjs/src/components/board/sections/category/category-menu-actions.tsx b/apps/nextjs/src/components/board/sections/category/category-menu-actions.tsx index 5ac4c9e09..15de9c00c 100644 --- a/apps/nextjs/src/components/board/sections/category/category-menu-actions.tsx +++ b/apps/nextjs/src/components/board/sections/category/category-menu-actions.tsx @@ -1,5 +1,6 @@ import { useCallback } from "react"; +import { fetchApi } from "@homarr/api/client"; import { createId } from "@homarr/db/client"; import { useConfirmModal, useModalAction } from "@homarr/modals"; import { useI18n } from "@homarr/translation/client"; @@ -7,6 +8,7 @@ import { useI18n } from "@homarr/translation/client"; import type { CategorySection } from "~/app/[locale]/boards/_types"; import { useCategoryActions } from "./category-actions"; import { CategoryEditModal } from "./category-edit-modal"; +import { filterByItemKind } from "./filter"; export const useCategoryMenuActions = (category: CategorySection) => { const { openModal } = useModalAction(CategoryEditModal); @@ -97,6 +99,28 @@ export const useCategoryMenuActions = (category: CategorySection) => { ); }, [category, openModal, renameCategory, t]); + const openAllInNewTabs = useCallback(async () => { + const appIds = filterByItemKind(category.items, "app").map((item) => { + return item.options.appId; + }); + + const apps = await fetchApi.app.byIds.query(appIds); + 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.category.openAllInNewTabs.title"), + children: t("section.category.openAllInNewTabs.text"), + }); + break; + } + }, [category, t, openConfirmModal]); + return { addCategoryAbove, addCategoryBelow, @@ -104,5 +128,6 @@ export const useCategoryMenuActions = (category: CategorySection) => { moveCategoryDown, remove, edit, + openAllInNewTabs, }; }; diff --git a/apps/nextjs/src/components/board/sections/category/category-menu.tsx b/apps/nextjs/src/components/board/sections/category/category-menu.tsx index 80e9cd96e..69c72a347 100644 --- a/apps/nextjs/src/components/board/sections/category/category-menu.tsx +++ b/apps/nextjs/src/components/board/sections/category/category-menu.tsx @@ -5,6 +5,7 @@ import { ActionIcon, Menu } from "@mantine/core"; import { IconDotsVertical, IconEdit, + IconExternalLink, IconRowInsertBottom, IconRowInsertTop, IconTransitionBottom, @@ -12,6 +13,7 @@ import { IconTrash, } from "@tabler/icons-react"; +import type { MaybePromise } from "@homarr/common/types"; import { useScopedI18n } from "@homarr/translation/client"; import type { TablerIcon } from "@homarr/ui"; @@ -27,8 +29,6 @@ export const CategoryMenu = ({ category }: Props) => { const actions = useActions(category); const t = useScopedI18n("section.category"); - if (actions.length === 0) return null; - return ( @@ -37,18 +37,20 @@ export const CategoryMenu = ({ category }: Props) => { - {actions.map((action) => ( - - {"group" in action && {t(action.group)}} - } - onClick={action.onClick} - color={"color" in action ? action.color : undefined} - > - {t(action.label)} - - - ))} + {actions.map((action) => { + return ( + + {"group" in action && {t(action.group)}} + } + onClick={action.onClick} + color={"color" in action ? action.color : undefined} + > + {t(action.label)} + + + ); + })} ); @@ -106,15 +108,21 @@ const useEditModeActions = (category: CategorySection) => { ] as const satisfies ActionDefinition[]; }; -// TODO: once apps are added we can use this for the open many apps action -const useNonEditModeActions = (_category: CategorySection) => { - return [] as const satisfies ActionDefinition[]; +const useNonEditModeActions = (category: CategorySection) => { + const { openAllInNewTabs } = useCategoryMenuActions(category); + return [ + { + icon: IconExternalLink, + label: "action.openAllInNewTabs", + onClick: openAllInNewTabs, + }, + ] as const satisfies ActionDefinition[]; }; interface ActionDefinition { icon: TablerIcon; label: string; - onClick: () => void; + onClick: () => MaybePromise; color?: string; group?: string; } diff --git a/apps/nextjs/src/components/board/sections/category/filter.ts b/apps/nextjs/src/components/board/sections/category/filter.ts new file mode 100644 index 000000000..87bc2c210 --- /dev/null +++ b/apps/nextjs/src/components/board/sections/category/filter.ts @@ -0,0 +1,14 @@ +import type { WidgetKind } from "@homarr/definitions"; +import type { WidgetComponentProps } from "@homarr/widgets"; +import { reduceWidgetOptionsWithDefaultValues } from "@homarr/widgets"; + +import type { Item } from "~/app/[locale]/boards/_types"; + +export const filterByItemKind = (items: Item[], kind: TKind) => { + return items + .filter((item) => item.kind === kind) + .map((item) => ({ + ...item, + options: reduceWidgetOptionsWithDefaultValues(kind, item.options) as WidgetComponentProps["options"], + })); +}; diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 440a3a3b3..0be4ef5f2 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -946,7 +946,8 @@ "moveUp": "Move up", "moveDown": "Move down", "createAbove": "New category above", - "createBelow": "New category below" + "createBelow": "New category below", + "openAllInNewTabs": "Open all in tabs" }, "create": { "title": "New category", @@ -965,6 +966,10 @@ "create": "New category", "changePosition": "Change position" } + }, + "openAllInNewTabs": { + "title": "Open all in tabs", + "text": "Some browsers may block the bulk-opening of tabs for security reasons. Homarr was unable to open all windows, because your browser blocked this action. Please allow \"Open pop-up windows\" and re-try." } } },