fix release v2 recovery flows

This commit is contained in:
ajnart committed 2026-08-14 02:18:46 +02:00
1 parent ca856f9f54
commit 4905ea6738
44 files changed
+871 -201

No files matched your search

@@ -40,7 +40,7 @@ const BoardItemMenuInner = ({ item, definition, resetErrorBoundary }: BoardItemM
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { updateItemOptions, updateItemAdvancedOptions, updateItemIntegrations, duplicateItem, removeItem } =
useItemActions();
const { integrations: integrationData = [], section } = useSectionContext();
const { integrations: integrationData, section } = useSectionContext();
const settings = useSettings();
const label = item.advancedOptions.title?.trim() || t(`widget.${item.kind}.name`);
@@ -77,8 +77,10 @@ const BoardItemMenuInner = ({ item, definition, resetErrorBoundary }: BoardItemM
});
refResetErrorBoundaryOnNextRender.current = true;
},
integrationData: integrationData.filter(
onIntegrationSaved: resetErrorBoundary,
integrationData: (integrationData ?? []).filter(
(integration) =>
integration.permissions.hasUseAccess &&
"supportedIntegrations" in definition &&
(definition.supportedIntegrations as string[]).some((kind) => kind === integration.kind),
),
@@ -83,4 +83,20 @@ describe("resolveMatchingIntegrationsAsync", () => {
).resolves.toEqual([]);
expect(ensureDataAsync).not.toHaveBeenCalled();
});
it("excludes matching integrations the user cannot use", async () => {
const currentData = [
{ id: "allowed", kind: "sonarr" as const, permissions: { hasUseAccess: true } },
{ id: "blocked", kind: "sonarr" as const, permissions: { hasUseAccess: false } },
];
await expect(
resolveMatchingIntegrationsAsync({
hasIntegrationSupport: true,
supportedIntegrations: ["sonarr"],
currentData,
ensureDataAsync: vi.fn(),
}),
).resolves.toEqual([currentData[0]]);
});
});
@@ -26,7 +26,9 @@ export const unlockSelection = (lock: { current: boolean }) => {
lock.current = false;
};
export const resolveMatchingIntegrationsAsync = async <TIntegration extends { kind: IntegrationKind }>({
export const resolveMatchingIntegrationsAsync = async <
TIntegration extends { kind: IntegrationKind; permissions?: { hasUseAccess: boolean } },
>({
hasIntegrationSupport,
supportedIntegrations,
currentData,
@@ -40,5 +42,8 @@ export const resolveMatchingIntegrationsAsync = async <TIntegration extends { ki
if (!hasIntegrationSupport) return [];
const integrations = currentData ?? (await ensureDataAsync());
return integrations.filter((integration) => supportedIntegrations.includes(integration.kind));
return integrations.filter(
(integration) =>
integration.permissions?.hasUseAccess !== false && supportedIntegrations.includes(integration.kind),
);
};
@@ -104,7 +104,10 @@ const ItemSelectModalContent = ({
const trackSetup = useSetupAnalytics();
const flowStartedAt = useRef<number | null>(null);
const availableKinds = useMemo(() => new Set((integrationData ?? []).map((i) => i.kind)), [integrationData]);
const availableKinds = useMemo(
() => new Set((integrationData ?? []).filter(({ permissions }) => permissions.hasUseAccess).map((i) => i.kind)),
[integrationData],
);
const items = useMemo(
() =>
@@ -244,13 +247,16 @@ const ItemSelectModalContent = ({
ensureDataAsync: ensureIntegrationDataAsync,
});
const integrationIds = matchingIntegrations.map((i) => i.id);
const maxIntegrations = (definition as { maxIntegrations?: number }).maxIntegrations ?? Infinity;
const integrationIds = matchingIntegrations.slice(0, maxIntegrations).map((i) => i.id);
const itemId = createId();
const defaultOptions = reduceWidgetOptionsWithDefinition(definition, settings);
const openEditor = (
availableIntegrations: { id: string; name: string; url: string; kind: IntegrationKind }[],
) => {
const selectedIntegrationIds = availableIntegrations.map((integration) => integration.id);
const selectedIntegrationIds = availableIntegrations
.slice(0, maxIntegrations)
.map((integration) => integration.id);
openEditModal(
{
kind,
@@ -6,10 +6,15 @@ import { ErrorBoundary } from "react-error-boundary";
import type { WidgetKind } from "@homarr/definitions";
import { createModal, modalSizeForm } from "@homarr/modals";
import { useModalAction } from "@homarr/modals";
import { WidgetError } from "@homarr/widgets/errors";
import type * as WidgetModalsModule from "@homarr/widgets/modals";
import type { WidgetEditModalProps } from "@homarr/widgets/modals";
let widgetEditModalPromise: Promise<typeof import("@homarr/widgets/modals")> | undefined;
import type * as IntegrationEditModalModule from "~/components/integration/embedded-integration-edit-modal";
import type { EmbeddedIntegrationEditModalProps } from "~/components/integration/embedded-integration-edit-modal";
let widgetEditModalPromise: Promise<typeof WidgetModalsModule> | undefined;
export const loadWidgetEditModal = () => {
if (widgetEditModalPromise) return widgetEditModalPromise;
@@ -24,6 +29,59 @@ export const loadWidgetEditModal = () => {
export const preloadWidgetEditModal = () => void loadWidgetEditModal().catch(() => undefined);
let integrationEditModalPromise: Promise<typeof IntegrationEditModalModule> | undefined;
const loadIntegrationEditModal = () => {
if (integrationEditModalPromise) return integrationEditModalPromise;
const currentPromise = import("~/components/integration/embedded-integration-edit-modal");
integrationEditModalPromise = currentPromise;
void currentPromise.catch(() => {
if (integrationEditModalPromise === currentPromise) integrationEditModalPromise = undefined;
});
return currentPromise;
};
const LazyIntegrationEditModalContent = (props: {
actions: { closeModal: () => void };
innerProps: EmbeddedIntegrationEditModalProps;
}) => {
const { EmbeddedIntegrationEditModal } = use(loadIntegrationEditModal());
const Component = EmbeddedIntegrationEditModal.component;
return <Component {...props} />;
};
const LazyIntegrationEditModal = createModal<EmbeddedIntegrationEditModalProps>((props) => (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<Center py="xl">
<WidgetError
error={error}
resetErrorBoundary={() => {
integrationEditModalPromise = undefined;
resetErrorBoundary();
}}
/>
</Center>
)}
>
<Suspense
fallback={
<Center py="xl">
<Loader size="sm" />
</Center>
}
>
<LazyIntegrationEditModalContent {...props} />
</Suspense>
</ErrorBoundary>
)).withOptions({
defaultTitle: (t) => t("item.edit.tab.integration"),
size: modalSizeForm,
presentation: "inspector",
closeOnClickOutside: false,
});
interface LazyWidgetEditModalContentProps {
actions: { closeModal: () => void };
innerProps: WidgetEditModalProps<WidgetKind>;
@@ -32,7 +90,20 @@ interface LazyWidgetEditModalContentProps {
const LazyWidgetEditModalContent = (props: LazyWidgetEditModalContentProps) => {
const { WidgetEditModal } = use(loadWidgetEditModal());
const Component = WidgetEditModal.component;
return <Component {...props} />;
const { openModal: openIntegrationEditModal } = useModalAction(LazyIntegrationEditModal);
return (
<Component
{...props}
innerProps={{
...props.innerProps,
onEditIntegration: (integrationId) =>
openIntegrationEditModal({
integrationId,
onSuccess: props.innerProps.onIntegrationSaved,
}),
}}
/>
);
};
export const LazyWidgetEditModal = createModal<WidgetEditModalProps<WidgetKind>>((props) => (
@@ -178,8 +178,11 @@ export const WidgetContextMenu = ({
),
}));
},
integrationData: integrationData.filter((integration) =>
definition.supportedIntegrations?.some((kind) => kind === integration.kind),
onIntegrationSaved: handleRefetch,
integrationData: integrationData.filter(
(integration) =>
definition.supportedIntegrations?.some((kind) => kind === integration.kind) &&
integration.permissions.hasUseAccess,
),
integrationSupport: definition.supportedIntegrations !== undefined,
settings,
@@ -192,6 +195,7 @@ export const WidgetContextMenu = ({
definition,
integrationData,
item,
handleRefetch,
openModal,
persistBoard,
settings,