diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx index 45436b476..5be662f0f 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx @@ -25,9 +25,11 @@ import { useEditMode } from "@homarr/boards/edit-mode"; import { revalidatePathActionAsync } from "@homarr/common/client"; import { env } from "@homarr/common/env"; import { useConfirmModal, useModalAction } from "@homarr/modals"; +import { AppSelectModal } from "@homarr/modals-collection"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n, useScopedI18n } from "@homarr/translation/client"; +import { useItemActions } from "~/components/board/items/item-actions"; import { ItemSelectModal } from "~/components/board/items/item-select-modal"; import { useBoardPermissions } from "~/components/board/permissions/client"; import { useCategoryActions } from "~/components/board/sections/category/category-actions"; @@ -62,8 +64,10 @@ export const BoardContentHeaderActions = () => { const AddMenu = () => { const { openModal: openCategoryEditModal } = useModalAction(CategoryEditModal); const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal); + const { openModal: openAppSelectModal } = useModalAction(AppSelectModal); const { addCategoryToEnd } = useCategoryActions(); const { addDynamicSection } = useDynamicSectionActions(); + const { createItem } = useItemActions(); const t = useI18n(); const handleAddCategory = useCallback( @@ -90,6 +94,17 @@ const AddMenu = () => { openItemSelectModal(); }, [openItemSelectModal]); + const handleSelectApp = useCallback(() => { + openAppSelectModal({ + onSelect: (appId) => { + createItem({ + kind: "app", + options: { appId }, + }); + }, + }); + }, [openAppSelectModal, createItem]); + return ( @@ -101,10 +116,14 @@ const AddMenu = () => { - } onClick={handleSelectItem}> + } onClick={handleSelectItem}> {t("item.action.create")} + } onClick={handleSelectApp}> + {t("app.action.add")} + + } onClick={handleAddCategory}> diff --git a/apps/nextjs/src/components/board/items/actions/create-item.ts b/apps/nextjs/src/components/board/items/actions/create-item.ts index d29d407b5..7011787ad 100644 --- a/apps/nextjs/src/components/board/items/actions/create-item.ts +++ b/apps/nextjs/src/components/board/items/actions/create-item.ts @@ -9,10 +9,11 @@ import { getSectionElements } from "./section-elements"; export interface CreateItemInput { kind: WidgetKind; + options?: Record; } export const createItemCallback = - ({ kind }: CreateItemInput) => + ({ kind, options = {} }: CreateItemInput) => (previous: Board): Board => { const firstSection = previous.sections .filter((section): section is EmptySection => section.kind === "empty") @@ -24,7 +25,7 @@ export const createItemCallback = const widget = { id: createId(), kind, - options: {}, + options, layouts: createItemLayouts(previous, firstSection), integrationIds: [], advancedOptions: { diff --git a/packages/modals-collection/src/apps/app-select-modal.tsx b/packages/modals-collection/src/apps/app-select-modal.tsx new file mode 100644 index 000000000..f6d7f2f50 --- /dev/null +++ b/packages/modals-collection/src/apps/app-select-modal.tsx @@ -0,0 +1,121 @@ +import { useMemo, useState } from "react"; +import Image from "next/image"; +import { Button, Card, Center, Grid, Input, Stack, Text } from "@mantine/core"; +import { IconPlus, IconSearch } from "@tabler/icons-react"; + +import { clientApi } from "@homarr/api/client"; +import { createModal, useModalAction } from "@homarr/modals"; +import { useI18n } from "@homarr/translation/client"; + +import { QuickAddAppModal } from "./quick-add-app/quick-add-app-modal"; + +interface AppSelectModalProps { + onSelect?: (appId: string) => void; +} + +export const AppSelectModal = createModal(({ actions, innerProps }) => { + const [search, setSearch] = useState(""); + const t = useI18n(); + const { data: apps = [], isPending } = clientApi.app.selectable.useQuery(); + const { openModal: openQuickAddAppModal } = useModalAction(QuickAddAppModal); + + const filteredApps = useMemo( + () => + apps + .filter((app) => app.name.toLowerCase().includes(search.toLowerCase())) + .sort((a, b) => a.name.localeCompare(b.name)), + [apps, search], + ); + + const handleSelect = (appId: string) => { + if (innerProps.onSelect) { + innerProps.onSelect(appId); + } + actions.closeModal(); + }; + + const handleAddNewApp = () => { + openQuickAddAppModal({ + onClose(createdAppId) { + if (innerProps.onSelect) { + innerProps.onSelect(createdAppId); + } + actions.closeModal(); + }, + }); + }; + + return ( + + setSearch(event.currentTarget.value)} + leftSection={} + placeholder={`${t("app.action.select.search")}...`} + data-autofocus + onKeyDown={(event) => { + if (event.key === "Enter" && filteredApps.length === 1 && filteredApps[0]) { + handleSelect(filteredApps[0].id); + } + }} + /> + + + + + + +
+ +
+ + {t("app.action.create.title")} + + + {t("app.action.create.description")} + +
+ +
+
+
+ + {filteredApps.map((app) => ( + + + + +
+ {app.name} +
+ + {app.name} + + + {app.description ?? ""} + +
+ +
+
+
+ ))} + + {filteredApps.length === 0 && !isPending && ( + +
+ {t("app.action.select.noResults")} +
+
+ )} +
+
+ ); +}).withOptions({ + defaultTitle: (t) => t("app.action.select.title"), + size: "xl", +}); diff --git a/packages/modals-collection/src/apps/index.ts b/packages/modals-collection/src/apps/index.ts index abac24663..ec272e2d4 100644 --- a/packages/modals-collection/src/apps/index.ts +++ b/packages/modals-collection/src/apps/index.ts @@ -1 +1,2 @@ +export { AppSelectModal } from "./app-select-modal"; export { QuickAddAppModal } from "./quick-add-app/quick-add-app-modal"; diff --git a/packages/modals-collection/src/apps/quick-add-app/quick-add-app-modal.tsx b/packages/modals-collection/src/apps/quick-add-app/quick-add-app-modal.tsx index 9f2e5e290..547840ccd 100644 --- a/packages/modals-collection/src/apps/quick-add-app/quick-add-app-modal.tsx +++ b/packages/modals-collection/src/apps/quick-add-app/quick-add-app-modal.tsx @@ -1,6 +1,7 @@ import type { z } from "zod"; import { clientApi } from "@homarr/api/client"; +import type { MaybePromise } from "@homarr/common/types"; import { AppForm } from "@homarr/forms-collection"; import { createModal } from "@homarr/modals"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; @@ -8,7 +9,7 @@ import { useI18n, useScopedI18n } from "@homarr/translation/client"; import type { appManageSchema } from "@homarr/validation/app"; interface QuickAddAppModalProps { - onClose: (createdAppId: string) => Promise; + onClose: (createdAppId: string) => MaybePromise; } export const QuickAddAppModal = createModal(({ actions, innerProps }) => { diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index e4c50bf11..8f34e8b7c 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -611,8 +611,18 @@ "action": { "select": { "label": "Select app", - "notFound": "No app found" - } + "notFound": "No app found", + "search": "Search for an app", + "noResults": "No results", + "action": "Select {app}", + "title": "Select an app to add to this board" + }, + "create": { + "title": "Create new app", + "description": "Create a new app ", + "action": "Open app creation" + }, + "add": "Add an app" } }, "integration": {