feat(boards): add quick app add menu item (#2681)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
@@ -25,9 +25,11 @@ import { useEditMode } from "@homarr/boards/edit-mode";
|
|||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
import { env } from "@homarr/common/env";
|
import { env } from "@homarr/common/env";
|
||||||
import { useConfirmModal, useModalAction } from "@homarr/modals";
|
import { useConfirmModal, useModalAction } from "@homarr/modals";
|
||||||
|
import { AppSelectModal } from "@homarr/modals-collection";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
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 { ItemSelectModal } from "~/components/board/items/item-select-modal";
|
||||||
import { useBoardPermissions } from "~/components/board/permissions/client";
|
import { useBoardPermissions } from "~/components/board/permissions/client";
|
||||||
import { useCategoryActions } from "~/components/board/sections/category/category-actions";
|
import { useCategoryActions } from "~/components/board/sections/category/category-actions";
|
||||||
@@ -62,8 +64,10 @@ export const BoardContentHeaderActions = () => {
|
|||||||
const AddMenu = () => {
|
const AddMenu = () => {
|
||||||
const { openModal: openCategoryEditModal } = useModalAction(CategoryEditModal);
|
const { openModal: openCategoryEditModal } = useModalAction(CategoryEditModal);
|
||||||
const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal);
|
const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal);
|
||||||
|
const { openModal: openAppSelectModal } = useModalAction(AppSelectModal);
|
||||||
const { addCategoryToEnd } = useCategoryActions();
|
const { addCategoryToEnd } = useCategoryActions();
|
||||||
const { addDynamicSection } = useDynamicSectionActions();
|
const { addDynamicSection } = useDynamicSectionActions();
|
||||||
|
const { createItem } = useItemActions();
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const handleAddCategory = useCallback(
|
const handleAddCategory = useCallback(
|
||||||
@@ -90,6 +94,17 @@ const AddMenu = () => {
|
|||||||
openItemSelectModal();
|
openItemSelectModal();
|
||||||
}, [openItemSelectModal]);
|
}, [openItemSelectModal]);
|
||||||
|
|
||||||
|
const handleSelectApp = useCallback(() => {
|
||||||
|
openAppSelectModal({
|
||||||
|
onSelect: (appId) => {
|
||||||
|
createItem({
|
||||||
|
kind: "app",
|
||||||
|
options: { appId },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [openAppSelectModal, createItem]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu position="bottom-end" withArrow>
|
<Menu position="bottom-end" withArrow>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
@@ -101,10 +116,14 @@ const AddMenu = () => {
|
|||||||
</HeaderButton>
|
</HeaderButton>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown style={{ transform: "translate(-3px, 0)" }}>
|
<Menu.Dropdown style={{ transform: "translate(-3px, 0)" }}>
|
||||||
<Menu.Item leftSection={<IconBox size={20} />} onClick={handleSelectItem}>
|
<Menu.Item leftSection={<IconResize size={20} />} onClick={handleSelectItem}>
|
||||||
{t("item.action.create")}
|
{t("item.action.create")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
|
<Menu.Item leftSection={<IconBox size={20} />} onClick={handleSelectApp}>
|
||||||
|
{t("app.action.add")}
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
<Menu.Item leftSection={<IconBoxAlignTop size={20} />} onClick={handleAddCategory}>
|
<Menu.Item leftSection={<IconBoxAlignTop size={20} />} onClick={handleAddCategory}>
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import { getSectionElements } from "./section-elements";
|
|||||||
|
|
||||||
export interface CreateItemInput {
|
export interface CreateItemInput {
|
||||||
kind: WidgetKind;
|
kind: WidgetKind;
|
||||||
|
options?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createItemCallback =
|
export const createItemCallback =
|
||||||
({ kind }: CreateItemInput) =>
|
({ kind, options = {} }: CreateItemInput) =>
|
||||||
(previous: Board): Board => {
|
(previous: Board): Board => {
|
||||||
const firstSection = previous.sections
|
const firstSection = previous.sections
|
||||||
.filter((section): section is EmptySection => section.kind === "empty")
|
.filter((section): section is EmptySection => section.kind === "empty")
|
||||||
@@ -24,7 +25,7 @@ export const createItemCallback =
|
|||||||
const widget = {
|
const widget = {
|
||||||
id: createId(),
|
id: createId(),
|
||||||
kind,
|
kind,
|
||||||
options: {},
|
options,
|
||||||
layouts: createItemLayouts(previous, firstSection),
|
layouts: createItemLayouts(previous, firstSection),
|
||||||
integrationIds: [],
|
integrationIds: [],
|
||||||
advancedOptions: {
|
advancedOptions: {
|
||||||
|
|||||||
121
packages/modals-collection/src/apps/app-select-modal.tsx
Normal file
121
packages/modals-collection/src/apps/app-select-modal.tsx
Normal file
@@ -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<AppSelectModalProps>(({ 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 (
|
||||||
|
<Stack>
|
||||||
|
<Input
|
||||||
|
value={search}
|
||||||
|
onChange={(event) => setSearch(event.currentTarget.value)}
|
||||||
|
leftSection={<IconSearch />}
|
||||||
|
placeholder={`${t("app.action.select.search")}...`}
|
||||||
|
data-autofocus
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter" && filteredApps.length === 1 && filteredApps[0]) {
|
||||||
|
handleSelect(filteredApps[0].id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={{ xs: 12, sm: 4, md: 3 }}>
|
||||||
|
<Card h="100%">
|
||||||
|
<Stack justify="space-between" h="100%">
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Center>
|
||||||
|
<IconPlus size={24} />
|
||||||
|
</Center>
|
||||||
|
<Text lh={1.2} style={{ whiteSpace: "normal" }} ta="center">
|
||||||
|
{t("app.action.create.title")}
|
||||||
|
</Text>
|
||||||
|
<Text lh={1.2} style={{ whiteSpace: "normal" }} size="xs" ta="center" c="dimmed">
|
||||||
|
{t("app.action.create.description")}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Button onClick={handleAddNewApp} variant="light" size="xs" mt="auto" radius="md" fullWidth>
|
||||||
|
{t("app.action.create.action")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
|
||||||
|
{filteredApps.map((app) => (
|
||||||
|
<Grid.Col key={app.id} span={{ xs: 12, sm: 4, md: 3 }}>
|
||||||
|
<Card h="100%">
|
||||||
|
<Stack justify="space-between" h="100%">
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Center>
|
||||||
|
<Image src={app.iconUrl || ""} alt={app.name} width={24} height={24} />
|
||||||
|
</Center>
|
||||||
|
<Text lh={1.2} style={{ whiteSpace: "normal" }} ta="center">
|
||||||
|
{app.name}
|
||||||
|
</Text>
|
||||||
|
<Text lh={1.2} style={{ whiteSpace: "normal" }} size="xs" ta="center" c="dimmed">
|
||||||
|
{app.description ?? ""}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Button onClick={() => handleSelect(app.id)} variant="light" size="xs" mt="auto" radius="md" fullWidth>
|
||||||
|
{t("app.action.select.action", { app: app.name })}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{filteredApps.length === 0 && !isPending && (
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Center p="xl">
|
||||||
|
<Text c="dimmed">{t("app.action.select.noResults")}</Text>
|
||||||
|
</Center>
|
||||||
|
</Grid.Col>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}).withOptions({
|
||||||
|
defaultTitle: (t) => t("app.action.select.title"),
|
||||||
|
size: "xl",
|
||||||
|
});
|
||||||
@@ -1 +1,2 @@
|
|||||||
|
export { AppSelectModal } from "./app-select-modal";
|
||||||
export { QuickAddAppModal } from "./quick-add-app/quick-add-app-modal";
|
export { QuickAddAppModal } from "./quick-add-app/quick-add-app-modal";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import type { MaybePromise } from "@homarr/common/types";
|
||||||
import { AppForm } from "@homarr/forms-collection";
|
import { AppForm } from "@homarr/forms-collection";
|
||||||
import { createModal } from "@homarr/modals";
|
import { createModal } from "@homarr/modals";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
@@ -8,7 +9,7 @@ import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
|||||||
import type { appManageSchema } from "@homarr/validation/app";
|
import type { appManageSchema } from "@homarr/validation/app";
|
||||||
|
|
||||||
interface QuickAddAppModalProps {
|
interface QuickAddAppModalProps {
|
||||||
onClose: (createdAppId: string) => Promise<void>;
|
onClose: (createdAppId: string) => MaybePromise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QuickAddAppModal = createModal<QuickAddAppModalProps>(({ actions, innerProps }) => {
|
export const QuickAddAppModal = createModal<QuickAddAppModalProps>(({ actions, innerProps }) => {
|
||||||
|
|||||||
@@ -611,8 +611,18 @@
|
|||||||
"action": {
|
"action": {
|
||||||
"select": {
|
"select": {
|
||||||
"label": "Select app",
|
"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": {
|
"integration": {
|
||||||
|
|||||||
Reference in New Issue
Block a user