feat: add app widget (#206)

* refactor: move server api to api package

* feat: add app widget

* refactor: add element size for widget components on board

* feat: add resize listener for widget width

* feat: add widget app input

* refactor: add better responsibe layout, add missing translations

* fix: ci issues

* fix: deepsource issues

* chore: address pull request feedback
This commit is contained in:
Meier Lukas
2024-03-12 21:23:25 +01:00
committed by GitHub
parent 7d5b999ab8
commit c4ff968cbc
31 changed files with 561 additions and 78 deletions

View File

@@ -1,7 +1,7 @@
import { api } from "@homarr/api/server";
import { getI18n } from "@homarr/translation/server";
import { Container, Stack, Title } from "@homarr/ui";
import { api } from "~/trpc/server";
import { AppEditForm } from "./_app-edit-form";
interface AppEditPageProps {

View File

@@ -1,6 +1,7 @@
import Link from "next/link";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
import { getI18n } from "@homarr/translation/server";
import {
ActionIcon,
@@ -18,7 +19,6 @@ import {
Title,
} from "@homarr/ui";
import { api } from "~/trpc/server";
import { AppDeleteButton } from "./_app-delete-button";
export default async function AppsPage() {

View File

@@ -1,8 +1,8 @@
import { api } from "@homarr/api/server";
import { getIntegrationName } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { Container, Group, Stack, Title } from "@homarr/ui";
import { api } from "~/trpc/server";
import { IntegrationAvatar } from "../../_integration-avatar";
import { EditIntegrationForm } from "./_integration-edit-form";

View File

@@ -1,6 +1,7 @@
import Link from "next/link";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
import { objectEntries } from "@homarr/common";
import type { IntegrationKind } from "@homarr/definitions";
import { getIntegrationName } from "@homarr/definitions";
@@ -32,7 +33,6 @@ import {
Title,
} from "@homarr/ui";
import { api } from "~/trpc/server";
import { ActiveTabAccordion } from "../../../../components/active-tab-accordion";
import { IntegrationAvatar } from "./_integration-avatar";
import { DeleteIntegrationActionButton } from "./_integration-buttons";

View File

@@ -1,4 +1,5 @@
import { api } from "~/trpc/server";
import { api } from "@homarr/api/server";
import { createBoardPage } from "../_creator";
export default createBoardPage<{ locale: string }>({

View File

@@ -1,4 +1,5 @@
import { api } from "~/trpc/server";
import { api } from "@homarr/api/server";
import { createBoardPage } from "../_creator";
export default createBoardPage<{ locale: string; name: string }>({

View File

@@ -1,5 +1,6 @@
import type { PropsWithChildren } from "react";
import { api } from "@homarr/api/server";
import { capitalize } from "@homarr/common";
import type { TranslationObject } from "@homarr/translation";
import { getScopedI18n } from "@homarr/translation/server";
@@ -20,7 +21,6 @@ import {
Title,
} from "@homarr/ui";
import { api } from "~/trpc/server";
import { ActiveTabAccordion } from "../../../../../components/active-tab-accordion";
import { BackgroundSettingsContent } from "./_background";
import { ColorSettingsContent } from "./_colors";

View File

@@ -1,9 +1,9 @@
import React from "react";
import { api } from "@homarr/api/server";
import { getScopedI18n } from "@homarr/translation/server";
import { Card, Grid, GridCol, Group, Text, Title } from "@homarr/ui";
import { api } from "~/trpc/server";
import { CreateBoardButton } from "./_components/create-board-button";
import { DeleteBoardButton } from "./_components/delete-board-button";

View File

@@ -1,5 +1,6 @@
import { notFound } from "next/navigation";
import { api } from "@homarr/api/server";
import { getScopedI18n } from "@homarr/translation/server";
import {
Accordion,
@@ -17,7 +18,6 @@ import {
Title,
} from "@homarr/ui";
import { api } from "~/trpc/server";
import { DangerZoneAccordion } from "./_components/dangerZone.accordion";
import { ProfileAccordion } from "./_components/profile.accordion";
import { SecurityAccordionComponent } from "./_components/security.accordion";

View File

@@ -1,6 +1,6 @@
import { api } from "@homarr/api/server";
import { getScopedI18n } from "@homarr/translation/server";
import { api } from "~/trpc/server";
import { UserListComponent } from "./_components/user-list.component";
export async function generateMetadata() {

View File

@@ -8,6 +8,7 @@ import { ItemSelectModal } from "~/components/board/items/item-select-modal";
import { BoardRenameModal } from "~/components/board/modals/board-rename-modal";
import { CategoryEditModal } from "~/components/board/sections/category/category-edit-modal";
import { AddBoardModal } from "~/components/manage/boards/add-board-modal";
import { PreviewDimensionsModal } from "./widgets/[kind]/_dimension-modal";
export const [ModalsManager, modalEvents] = createModalManager({
categoryEditModal: CategoryEditModal,
@@ -15,4 +16,5 @@ export const [ModalsManager, modalEvents] = createModalManager({
itemSelectModal: ItemSelectModal,
addBoardModal: AddBoardModal,
boardRenameModal: BoardRenameModal,
dimensionsModal: PreviewDimensionsModal,
});

View File

@@ -1,9 +1,19 @@
"use client";
import { useState } from "react";
import { useCallback, useMemo, useState } from "react";
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
import { ActionIcon, Affix, IconPencil } from "@homarr/ui";
import { showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import {
ActionIcon,
Affix,
Card,
IconDimensions,
IconPencil,
IconToggleLeft,
IconToggleRight,
} from "@homarr/ui";
import {
loadWidgetDynamic,
reduceWidgetOptionsWithDefaultValues,
@@ -11,6 +21,7 @@ import {
} from "@homarr/widgets";
import { modalEvents } from "../../modals";
import type { Dimensions } from "./_dimension-modal";
interface WidgetPreviewPageContentProps {
kind: WidgetKind;
@@ -26,7 +37,16 @@ export const WidgetPreviewPageContent = ({
kind,
integrationData,
}: WidgetPreviewPageContentProps) => {
const currentDefinition = widgetImports[kind].definition;
const t = useScopedI18n("widgetPreview");
const currentDefinition = useMemo(
() => widgetImports[kind].definition,
[kind],
);
const [editMode, setEditMode] = useState(false);
const [dimensions, setDimensions] = useState<Dimensions>({
width: 128,
height: 128,
});
const [state, setState] = useState<{
options: Record<string, unknown>;
integrations: string[];
@@ -37,44 +57,97 @@ export const WidgetPreviewPageContent = ({
const Comp = loadWidgetDynamic(kind);
const openWitgetEditModal = useCallback(() => {
return modalEvents.openManagedModal({
modal: "widgetEditModal",
innerProps: {
kind,
value: state,
onSuccessfulEdit: (value) => {
setState(value);
},
integrationData: integrationData.filter(
(integration) =>
"supportedIntegrations" in currentDefinition &&
(currentDefinition.supportedIntegrations as string[]).some(
(kind) => kind === integration.kind,
),
),
integrationSupport: "supportedIntegrations" in currentDefinition,
},
});
}, [kind, state, integrationData, currentDefinition]);
const toggleEditMode = useCallback(() => {
setEditMode((editMode) => !editMode);
showSuccessNotification({
message: editMode ? t("toggle.disabled") : t("toggle.enabled"),
});
}, [editMode, t]);
const openDimensionsModal = useCallback(() => {
modalEvents.openManagedModal({
modal: "dimensionsModal",
title: t("dimensions.title"),
innerProps: {
dimensions,
setDimensions,
},
});
}, [dimensions, t]);
return (
<>
<Comp
options={state.options as never}
integrations={state.integrations.map(
(id) => integrationData.find((x) => x.id === id)!,
)}
/>
<Card
withBorder
w={dimensions.width}
h={dimensions.height}
p={dimensions.height >= 96 ? undefined : 4}
>
<Comp
options={state.options as never}
integrations={state.integrations.map(
(id) => integrationData.find((x) => x.id === id)!,
)}
width={dimensions.width}
height={dimensions.height}
isEditMode={editMode}
/>
</Card>
<Affix bottom={12} right={72}>
<ActionIcon
size={48}
variant="default"
radius="xl"
onClick={() => {
return modalEvents.openManagedModal({
modal: "widgetEditModal",
innerProps: {
kind,
value: state,
onSuccessfulEdit: (value) => {
setState(value);
},
integrationData: integrationData.filter(
(integration) =>
"supportedIntegrations" in currentDefinition &&
(currentDefinition.supportedIntegrations as string[]).some(
(kind) => kind === integration.kind,
),
),
integrationSupport:
"supportedIntegrations" in currentDefinition,
},
});
}}
onClick={openWitgetEditModal}
>
<IconPencil size={24} />
</ActionIcon>
</Affix>
<Affix bottom={12} right={72 + 60}>
<ActionIcon
size={48}
variant="default"
radius="xl"
onClick={toggleEditMode}
>
{editMode ? (
<IconToggleLeft size={24} />
) : (
<IconToggleRight size={24} />
)}
</ActionIcon>
</Affix>
<Affix bottom={12} right={72 + 120}>
<ActionIcon
size={48}
variant="default"
radius="xl"
onClick={openDimensionsModal}
>
<IconDimensions size={24} />
</ActionIcon>
</Affix>
</>
);
};

View File

@@ -0,0 +1,61 @@
"use client";
import type { ManagedModal } from "mantine-modal-manager";
import { useForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";
import { Button, Group, InputWrapper, Slider, Stack } from "@homarr/ui";
interface InnerProps {
dimensions: Dimensions;
setDimensions: (dimensions: Dimensions) => void;
}
export const PreviewDimensionsModal: ManagedModal<InnerProps> = ({
actions,
innerProps,
}) => {
const t = useI18n();
const form = useForm({
initialValues: innerProps.dimensions,
});
const handleSubmit = (values: Dimensions) => {
innerProps.setDimensions(values);
actions.closeModal();
};
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack>
<InputWrapper label={t("item.move.field.width.label")}>
<Slider
min={64}
max={1024}
step={64}
{...form.getInputProps("width")}
/>
</InputWrapper>
<InputWrapper label={t("item.move.field.height.label")}>
<Slider
min={64}
max={1024}
step={64}
{...form.getInputProps("height")}
/>
</InputWrapper>
<Group justify="end">
<Button variant="subtle" color="gray" onClick={actions.closeModal}>
{t("common.action.cancel")}
</Button>
<Button type="submit">{t("common.action.confirm")}</Button>
</Group>
</Stack>
</form>
);
};
export interface Dimensions {
width: number;
height: number;
}