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:
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user