ci(workshop): add combined deployment image

This commit is contained in:
Thomas Camlong committed 2026-07-14 19:05:13 +02:00
1 parent 89260b8754
commit fd28289f40
39 files changed
+636 -436

No files matched your search

@@ -11,7 +11,7 @@ import { IconInfoCircle } from "@tabler/icons-react";
import { useForm } from "@homarr/form";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { InstallCssFromStoreButton } from "~/components/store-install/install-from-store-buttons";
import { InstallCssFromWorkshopButton } from "~/components/workshop-install/install-from-workshop-buttons";
import type { Board } from "../../_types";
import { useSavePartialSettingsMutation } from "./_shared";
import classes from "./customcss.module.css";
@@ -47,7 +47,7 @@ export const CustomCssSettingsContent = ({ board }: Props) => {
</Alert>
<Group justify="space-between">
<InstallCssFromStoreButton onSelect={(css) => form.setFieldValue("customCss", css)} />
<InstallCssFromWorkshopButton onSelect={(css) => form.setFieldValue("customCss", css)} />
<Button type="submit" loading={isPending}>
{t("common.action.saveChanges")}
</Button>
@@ -9,7 +9,7 @@ import { Link } from "@homarr/ui";
import { ManagePageLayout } from "~/components/manage/manage-page-layout";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
import { InstallWidgetFromStoreButton } from "~/components/store-install/install-from-store-buttons";
import { InstallWidgetFromWorkshopButton } from "~/components/workshop-install/install-from-workshop-buttons";
import { CustomWidgetBetaBanner } from "./_beta-banner";
import { ImportCustomWidgetButton } from "./_custom-widget-actions";
import { CustomWidgetList } from "./_custom-widget-list";
@@ -29,7 +29,7 @@ export default async function CustomWidgetsPage() {
title={t("page.list.title")}
primaryAction={
<Group gap="xs">
<InstallWidgetFromStoreButton />
<InstallWidgetFromWorkshopButton />
<ImportCustomWidgetButton />
<MobileAffixButton component={Link} href="/manage/custom-widgets/new" leftSection={<IconPlus size={16} />}>
{t("action.create")}
@@ -3,7 +3,7 @@
import { useState } from "react";
import { Button } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { IconBuildingStore } from "@tabler/icons-react";
import { IconUsersGroup } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
@@ -12,7 +12,7 @@ import { useScopedI18n } from "@homarr/translation/client";
import { customWidgetImportSchema } from "@homarr/validation/custom-widget";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
import { StoreBrowserModal } from "./store-browser-modal";
import { WorkshopBrowserModal } from "./workshop-browser-modal";
function parseWidgetContent(content: string) {
let parsed: unknown;
@@ -28,7 +28,7 @@ function parseWidgetContent(content: string) {
return { ok: true as const, data: result.data };
}
export const InstallWidgetFromStoreButton = () => {
export const InstallWidgetFromWorkshopButton = () => {
const t = useScopedI18n("customWidget.workshop");
const [opened, { open, close }] = useDisclosure(false);
const [pendingId, setPendingId] = useState<string | null>(null);
@@ -59,10 +59,10 @@ export const InstallWidgetFromStoreButton = () => {
return (
<>
<MobileAffixButton variant="default" leftSection={<IconBuildingStore size={16} />} onClick={open}>
<MobileAffixButton variant="default" leftSection={<IconUsersGroup size={16} />} onClick={open}>
{t("action.installFromWorkshop")}
</MobileAffixButton>
<StoreBrowserModal
<WorkshopBrowserModal
type="widget"
opened={opened}
onClose={close}
@@ -74,16 +74,16 @@ export const InstallWidgetFromStoreButton = () => {
);
};
export const InstallCssFromStoreButton = ({ onSelect }: { onSelect: (css: string) => void }) => {
export const InstallCssFromWorkshopButton = ({ onSelect }: { onSelect: (css: string) => void }) => {
const t = useScopedI18n("customWidget.workshop");
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Button variant="default" leftSection={<IconBuildingStore size={16} />} onClick={open}>
<Button variant="default" leftSection={<IconUsersGroup size={16} />} onClick={open}>
{t("action.installFromWorkshop")}
</Button>
<StoreBrowserModal
<WorkshopBrowserModal
type="css"
opened={opened}
onClose={close}
@@ -1,6 +1,6 @@
export const STORE_URL = process.env.NEXT_PUBLIC_WIDGET_STORE_URL || "https://store.homarr.dev";
export const WORKSHOP_API_URL = process.env.NEXT_PUBLIC_WORKSHOP_API_URL || "https://workshop.homarr.dev";
export interface StoreSubmission {
export interface WorkshopSubmission {
id: string;
collectionId: string;
collectionName: string;
@@ -24,13 +24,13 @@ interface PBListResponse {
perPage: number;
totalPages: number;
totalItems: number;
items: StoreSubmission[];
items: WorkshopSubmission[];
}
export const getStoreFileUrl = (submission: StoreSubmission, filename: string) =>
`${STORE_URL}/api/files/submissions/${submission.id}/${encodeURIComponent(filename)}`;
export const getWorkshopFileUrl = (submission: WorkshopSubmission, filename: string) =>
`${WORKSHOP_API_URL}/api/files/submissions/${submission.id}/${encodeURIComponent(filename)}`;
export const fetchStoreSubmissions = async (
export const fetchWorkshopSubmissions = async (
type: "css" | "widget",
page = 1,
perPage = 50,
@@ -41,7 +41,7 @@ export const fetchStoreSubmissions = async (
sort: "-upvotes",
filter: `(type='${type}')`,
});
const response = await fetch(`${STORE_URL}/api/collections/marketplace/records?${params}`);
if (!response.ok) throw new Error(`Store request failed (${response.status})`);
const response = await fetch(`${WORKSHOP_API_URL}/api/collections/marketplace/records?${params}`);
if (!response.ok) throw new Error(`Workshop request failed (${response.status})`);
return response.json() as Promise<PBListResponse>;
};
@@ -23,8 +23,8 @@ import {
import { IconChevronLeft, IconChevronRight, IconExternalLink } from "@tabler/icons-react";
import { useQuery } from "@tanstack/react-query";
import type { StoreSubmission } from "./store-api";
import { fetchStoreSubmissions, getStoreFileUrl } from "./store-api";
import type { WorkshopSubmission } from "./workshop-api";
import { fetchWorkshopSubmissions, getWorkshopFileUrl } from "./workshop-api";
const WORKSHOP_URL = process.env.NEXT_PUBLIC_WORKSHOP_URL || "https://homarr.dev/workshop";
@@ -32,22 +32,22 @@ interface Props {
type: "css" | "widget";
opened: boolean;
onClose: () => void;
onSelect: (submission: StoreSubmission) => void;
onSelect: (submission: WorkshopSubmission) => void;
actionLabel: string;
pendingId?: string | null;
}
const displayName = (submission: StoreSubmission) => submission.authorName;
const displayName = (submission: WorkshopSubmission) => submission.authorName;
export const StoreBrowserModal = ({ type, opened, onClose, onSelect, actionLabel, pendingId }: Props) => {
export const WorkshopBrowserModal = ({ type, opened, onClose, onSelect, actionLabel, pendingId }: Props) => {
const [page, setPage] = useState(1);
const handleClose = () => {
setPage(1);
onClose();
};
const { data, isLoading, error } = useQuery({
queryKey: ["store-submissions", type, page],
queryFn: () => fetchStoreSubmissions(type, page),
queryKey: ["workshop-submissions", type, page],
queryFn: () => fetchWorkshopSubmissions(type, page),
enabled: opened,
});
@@ -121,7 +121,7 @@ const SubmissionCard = ({
pending,
onSelect,
}: {
submission: StoreSubmission;
submission: WorkshopSubmission;
actionLabel: string;
pending: boolean;
onSelect: () => void;
@@ -136,7 +136,7 @@ const SubmissionCard = ({
{screenshots.length > 0 && (
<Box pos="relative">
<Image
src={screenshots[imgIdx] ? getStoreFileUrl(submission, screenshots[imgIdx]) : undefined}
src={screenshots[imgIdx] ? getWorkshopFileUrl(submission, screenshots[imgIdx]) : undefined}
alt={submission.title}
h={180}
fit="cover"