chore: reconcile grid layer after stack rebase
This commit is contained in:
1 parent
16982aa178
commit
6bfa9c33d0
9 files changed
+54
-166
No files matched your search
@@ -1,5 +1,9 @@
|
||||
# Product
|
||||
|
||||
## Register
|
||||
|
||||
product
|
||||
|
||||
## Users
|
||||
|
||||
Homarr is used by self-hosters and administrators who assemble dashboards for services running across home labs and private infrastructure. They need dense, reliable controls and status at a glance, with enough guidance to configure advanced features without hiding how they work.
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { Group, Menu } from "@mantine/core";
|
||||
import { IconBox, IconBoxAlignTop, IconChevronDown, IconPlug, IconPlus, IconResize } from "@tabler/icons-react";
|
||||
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { AppSelectModal } from "@homarr/modals-collection";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { useItemActions } from "~/components/board/items/item-actions";
|
||||
import { ItemSelectModal } from "~/components/board/items/item-select-modal";
|
||||
import { useCategoryActions } from "~/components/board/sections/category/category-actions";
|
||||
import { CategoryEditModal } from "~/components/board/sections/category/category-edit-modal";
|
||||
import { useDynamicSectionActions } from "~/components/board/sections/dynamic/dynamic-actions";
|
||||
import { IntegrationSelectModal } from "~/components/integration/integration-select-modal";
|
||||
import { HeaderButton } from "~/components/layout/header/button";
|
||||
|
||||
export const BoardAddMenu = () => {
|
||||
const { data: session } = useSession();
|
||||
const board = useRequiredBoard();
|
||||
const { openModal: openCategoryEditModal } = useModalAction(CategoryEditModal);
|
||||
const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal);
|
||||
const { openModal: openAppSelectModal } = useModalAction(AppSelectModal);
|
||||
const { openModal: openIntegrationSelectModal } = useModalAction(IntegrationSelectModal);
|
||||
const { addCategoryToEnd } = useCategoryActions();
|
||||
const { addDynamicSection } = useDynamicSectionActions();
|
||||
const { createItem } = useItemActions();
|
||||
const t = useI18n();
|
||||
|
||||
const handleAddCategory = useCallback(
|
||||
() =>
|
||||
openCategoryEditModal(
|
||||
{
|
||||
category: { id: "new", name: "" },
|
||||
onSuccess({ name }) {
|
||||
addCategoryToEnd({ name });
|
||||
},
|
||||
submitLabel: t("section.category.create.submit"),
|
||||
},
|
||||
{ title: (translate) => translate("section.category.create.title") },
|
||||
),
|
||||
[addCategoryToEnd, openCategoryEditModal, t],
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu position="bottom-end">
|
||||
<Menu.Target>
|
||||
<HeaderButton w="auto" px={4} aria-label={t("board.action.addContent")}>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<IconPlus stroke={1.5} />
|
||||
<IconChevronDown color="gray" size={16} />
|
||||
</Group>
|
||||
</HeaderButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown style={{ transform: "translate(-3px, 0)" }}>
|
||||
<Menu.Item leftSection={<IconResize size={20} />} onClick={() => openItemSelectModal({ boardId: board.id })}>
|
||||
{t("item.action.create")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<IconBox size={20} />}
|
||||
onClick={() =>
|
||||
openAppSelectModal({
|
||||
onSelect: (app) => createItem({ kind: "app", options: { appId: app.id } }),
|
||||
withCreate: session?.user.permissions.includes("app-create") ?? false,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("app.action.add")}
|
||||
</Menu.Item>
|
||||
<Menu.Item leftSection={<IconPlug size={20} />} onClick={() => openIntegrationSelectModal({})}>
|
||||
{t("integration.action.create")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item leftSection={<IconBoxAlignTop size={20} />} onClick={handleAddCategory}>
|
||||
{t("section.category.action.create")}
|
||||
</Menu.Item>
|
||||
<Menu.Item leftSection={<IconResize size={20} />} onClick={addDynamicSection}>
|
||||
{t("section.dynamic.action.create")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { Group, Menu } from "@mantine/core";
|
||||
import { IconBox, IconChevronDown, IconLayoutGridAdd, IconPlug, IconPlus, IconResize } from "@tabler/icons-react";
|
||||
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { AppSelectModal } from "@homarr/modals-collection";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
@@ -20,6 +21,7 @@ export default function BoardEditActions() {
|
||||
|
||||
const AddMenu = () => {
|
||||
const { data: session } = useSession();
|
||||
const board = useRequiredBoard();
|
||||
const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal);
|
||||
const { openModal: openAppSelectModal } = useModalAction(AppSelectModal);
|
||||
const { openModal: openIntegrationSelectModal } = useModalAction(IntegrationSelectModal);
|
||||
@@ -28,7 +30,7 @@ const AddMenu = () => {
|
||||
const t = useI18n();
|
||||
|
||||
const handleSelectItem = () => {
|
||||
openItemSelectModal();
|
||||
openItemSelectModal({ boardId: board.id });
|
||||
};
|
||||
|
||||
const handleSelectApp = () => {
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense, use } from "react";
|
||||
import { Center, Loader } from "@mantine/core";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
|
||||
import { createModal, modalSizeForm } from "@homarr/modals";
|
||||
import { WidgetError } from "@homarr/widgets/errors";
|
||||
|
||||
import type { ItemMoveModalProps } from "./item-move-modal";
|
||||
|
||||
let itemMoveModalPromise: Promise<typeof import("./item-move-modal")> | undefined;
|
||||
|
||||
const loadItemMoveModal = () => {
|
||||
if (itemMoveModalPromise) return itemMoveModalPromise;
|
||||
|
||||
const currentPromise = import("./item-move-modal");
|
||||
itemMoveModalPromise = currentPromise;
|
||||
void currentPromise.catch(() => {
|
||||
if (itemMoveModalPromise === currentPromise) itemMoveModalPromise = undefined;
|
||||
});
|
||||
return currentPromise;
|
||||
};
|
||||
|
||||
export const preloadItemMoveModal = () => void loadItemMoveModal().catch(() => undefined);
|
||||
|
||||
interface LazyItemMoveModalContentProps {
|
||||
actions: { closeModal: () => void };
|
||||
innerProps: ItemMoveModalProps;
|
||||
}
|
||||
|
||||
const LazyItemMoveModalContent = (props: LazyItemMoveModalContentProps) => {
|
||||
const { ItemMoveModal } = use(loadItemMoveModal());
|
||||
const Component = ItemMoveModal.component;
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
||||
export const LazyItemMoveModal = createModal<ItemMoveModalProps>((props) => (
|
||||
<ErrorBoundary
|
||||
fallbackRender={({ error, resetErrorBoundary }) => (
|
||||
<Center py="xl">
|
||||
<WidgetError
|
||||
error={error}
|
||||
resetErrorBoundary={() => {
|
||||
itemMoveModalPromise = undefined;
|
||||
resetErrorBoundary();
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
>
|
||||
<Suspense
|
||||
fallback={
|
||||
<Center py="xl">
|
||||
<Loader size="sm" />
|
||||
</Center>
|
||||
}
|
||||
>
|
||||
<LazyItemMoveModalContent {...props} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)).withOptions({
|
||||
defaultTitle(t) {
|
||||
return t("item.moveResize.title");
|
||||
},
|
||||
size: modalSizeForm,
|
||||
});
|
||||
@@ -5,8 +5,10 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { useIntegrations } from "@homarr/auth/client";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { getRootSectionLane } from "@homarr/definitions";
|
||||
|
||||
import type { ContainerSectionItem, Section } from "~/app/[locale]/boards/_types";
|
||||
@@ -33,7 +35,11 @@ const GridPortalHostContext = createContext<GridPortalHostContextValue | null>(n
|
||||
*/
|
||||
export const BoardGridPortalHost = ({ children }: PropsWithChildren) => {
|
||||
const board = useRequiredBoard();
|
||||
const integrations = useIntegrations();
|
||||
const { data: session } = useSession();
|
||||
const [isEditMode] = useEditMode();
|
||||
const { data: integrations } = clientApi.integration.all.useQuery(undefined, {
|
||||
enabled: Boolean(session) && isEditMode,
|
||||
});
|
||||
const containersRef = useRef<Map<string, HTMLElement>>(new Map());
|
||||
const [containers, setContainers] = useState<Map<string, HTMLElement>>(() => new Map());
|
||||
const [announcement, setAnnouncement] = useState({ id: 0, message: "" });
|
||||
|
||||
@@ -12,8 +12,18 @@ const mocks = vi.hoisted(() => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/api/client", () => ({
|
||||
clientApi: {
|
||||
integration: {
|
||||
all: {
|
||||
useQuery: () => ({ data: undefined }),
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/auth/client", () => ({
|
||||
useIntegrations: () => undefined,
|
||||
useSession: () => ({ data: { user: { id: "user" } } }),
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/boards/context", () => ({
|
||||
@@ -21,6 +31,10 @@ vi.mock("@homarr/boards/context", () => ({
|
||||
useRequiredBoard: () => mocks.board,
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/boards/edit-mode", () => ({
|
||||
useEditMode: () => [true],
|
||||
}));
|
||||
|
||||
describe("board grid portal host", () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { use, useCallback, useMemo } from "react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { WidgetComponentProps } from "@homarr/widgets/definition";
|
||||
import { loadWidgetDefinition, reduceWidgetOptionsWithDefinition } from "@homarr/widgets/manifest";
|
||||
|
||||
import type { Item } from "~/app/[locale]/boards/_types";
|
||||
import { filterByItemKind } from "./filter-by-item-kind";
|
||||
|
||||
interface SectionItemLayout {
|
||||
layoutId: string;
|
||||
@@ -79,10 +80,27 @@ export const useOpenSectionApps = (sectionId: string, enabled: boolean) => {
|
||||
const settings = useSettings();
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const t = useI18n();
|
||||
const appDefinition = use(loadWidgetDefinition("app"));
|
||||
const appIds = useMemo(() => {
|
||||
const items: Item[] = getSectionItemsForLayout(board, sectionId, currentLayoutId);
|
||||
return Array.from(new Set(filterByItemKind(items, settings, "app").map((item) => item.options.appId)));
|
||||
}, [board, currentLayoutId, sectionId, settings]);
|
||||
return Array.from(
|
||||
new Set(
|
||||
items
|
||||
.filter((item) => item.kind === "app")
|
||||
.map(
|
||||
(item) =>
|
||||
(
|
||||
reduceWidgetOptionsWithDefinition(
|
||||
appDefinition,
|
||||
settings,
|
||||
item.options,
|
||||
) as WidgetComponentProps<"app">["options"]
|
||||
).appId,
|
||||
)
|
||||
.filter((appId): appId is string => typeof appId === "string"),
|
||||
),
|
||||
);
|
||||
}, [appDefinition, board, currentLayoutId, sectionId, settings]);
|
||||
const { data: apps = [], isLoading } = clientApi.app.byIds.useQuery(appIds, {
|
||||
enabled: enabled && appIds.length > 0,
|
||||
staleTime: 30_000,
|
||||
|
||||
@@ -5,10 +5,7 @@ interface ResponsiveLayout {
|
||||
breakpoint: number;
|
||||
}
|
||||
|
||||
export const getLayoutIdForViewportWidth = (
|
||||
layouts: readonly ResponsiveLayout[],
|
||||
viewportWidth: number,
|
||||
): string => {
|
||||
export const getLayoutIdForViewportWidth = (layouts: readonly ResponsiveLayout[], viewportWidth: number): string => {
|
||||
if (layouts.length === 0) {
|
||||
throw new Error("Expected the board to contain a layout");
|
||||
}
|
||||
|
||||
@@ -2347,7 +2347,7 @@
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
|
||||
Reference in new issue
Block a user