feat(boards): add quick app add menu item (#2681)

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
Thomas Camlong
2025-04-11 20:55:00 +02:00
committed by GitHub
parent 4baec7e3ff
commit 7a3c836a70
6 changed files with 159 additions and 6 deletions

View 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",
});

View File

@@ -1 +1,2 @@
export { AppSelectModal } from "./app-select-modal";
export { QuickAddAppModal } from "./quick-add-app/quick-add-app-modal";

View File

@@ -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<void>;
onClose: (createdAppId: string) => MaybePromise<void>;
}
export const QuickAddAppModal = createModal<QuickAddAppModalProps>(({ actions, innerProps }) => {

View File

@@ -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": {