diff --git a/apps/nextjs/src/app/[locale]/manage/medias/_actions/upload-media.tsx b/apps/nextjs/src/app/[locale]/manage/medias/_actions/upload-media.tsx
index 72b880fc9..f8164cdac 100644
--- a/apps/nextjs/src/app/[locale]/manage/medias/_actions/upload-media.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/medias/_actions/upload-media.tsx
@@ -1,15 +1,40 @@
"use client";
+import type { JSX } from "react";
import { Button, FileButton } from "@mantine/core";
import { IconUpload } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
+import type { MaybePromise } from "@homarr/common/types";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { supportedMediaUploadFormats } from "@homarr/validation";
-export const UploadMedia = () => {
+export const UploadMediaButton = () => {
+ const t = useI18n();
+ const onSettledAsync = async () => {
+ await revalidatePathActionAsync("/manage/medias");
+ };
+
+ return (
+
+ {({ onClick, loading }) => (
+ }>
+ {t("media.action.upload.label")}
+
+ )}
+
+ );
+};
+
+interface UploadMediaProps {
+ children: (props: { onClick: () => void; loading: boolean }) => JSX.Element;
+ onSettled?: () => MaybePromise;
+ onSuccess?: (media: { id: string; url: string }) => MaybePromise;
+}
+
+export const UploadMedia = ({ children, onSettled, onSuccess }: UploadMediaProps) => {
const t = useI18n();
const { mutateAsync, isPending } = clientApi.media.uploadMedia.useMutation();
@@ -18,10 +43,14 @@ export const UploadMedia = () => {
const formData = new FormData();
formData.append("file", file);
await mutateAsync(formData, {
- onSuccess() {
+ async onSuccess(mediaId) {
showSuccessNotification({
message: t("media.action.upload.notification.success.message"),
});
+ await onSuccess?.({
+ id: mediaId,
+ url: `/api/user-medias/${mediaId}`,
+ });
},
onError() {
showErrorNotification({
@@ -29,18 +58,14 @@ export const UploadMedia = () => {
});
},
async onSettled() {
- await revalidatePathActionAsync("/manage/medias");
+ await onSettled?.();
},
});
};
return (
- {({ onClick }) => (
- }>
- {t("media.action.upload.label")}
-
- )}
+ {({ onClick }) => children({ onClick, loading: isPending })}
);
};
diff --git a/apps/nextjs/src/app/[locale]/manage/medias/page.tsx b/apps/nextjs/src/app/[locale]/manage/medias/page.tsx
index 17c01f519..ed50a18ad 100644
--- a/apps/nextjs/src/app/[locale]/manage/medias/page.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/medias/page.tsx
@@ -16,7 +16,7 @@ import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { CopyMedia } from "./_actions/copy-media";
import { DeleteMedia } from "./_actions/delete-media";
import { IncludeFromAllUsersSwitch } from "./_actions/show-all";
-import { UploadMedia } from "./_actions/upload-media";
+import { UploadMediaButton } from "./_actions/upload-media";
const searchParamsSchema = z.object({
search: z.string().optional(),
@@ -61,7 +61,7 @@ export default async function GroupsListPage(props: MediaListPageProps) {
)}
- {session.user.permissions.includes("media-upload") && }
+ {session.user.permissions.includes("media-upload") && }
diff --git a/apps/nextjs/src/components/icons/picker/icon-picker.tsx b/apps/nextjs/src/components/icons/picker/icon-picker.tsx
index 432142dbf..d0758a66e 100644
--- a/apps/nextjs/src/components/icons/picker/icon-picker.tsx
+++ b/apps/nextjs/src/components/icons/picker/icon-picker.tsx
@@ -1,10 +1,12 @@
import type { FocusEventHandler } from "react";
import { startTransition, useState } from "react";
import {
+ ActionIcon,
Box,
Card,
Combobox,
Flex,
+ Group,
Image,
Indicator,
InputBase,
@@ -16,10 +18,13 @@ import {
useCombobox,
} from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
+import { IconUpload } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
+import { useSession } from "@homarr/auth/client";
import { useScopedI18n } from "@homarr/translation/client";
+import { UploadMedia } from "~/app/[locale]/manage/medias/_actions/upload-media";
import classes from "./icon-picker.module.css";
interface IconPickerProps {
@@ -34,6 +39,7 @@ export const IconPicker = ({ initialValue, onChange, error, onFocus, onBlur }: I
const [value, setValue] = useState(initialValue ?? "");
const [search, setSearch] = useState(initialValue ?? "");
const [previewUrl, setPreviewUrl] = useState(initialValue ?? null);
+ const { data: session } = useSession();
const tCommon = useScopedI18n("common");
@@ -105,40 +111,61 @@ export const IconPicker = ({ initialValue, onChange, error, onFocus, onBlur }: I
return (
- }
- leftSection={
- previewUrl ? (
- // eslint-disable-next-line @next/next/no-img-element
-
- ) : null
- }
- value={search}
- onChange={(event) => {
- combobox.openDropdown();
- combobox.updateSelectedOptionIndex();
- setSearch(event.currentTarget.value);
- setValue(event.currentTarget.value);
- setPreviewUrl(null);
- onChange(event.currentTarget.value);
- }}
- onClick={() => combobox.openDropdown()}
- onFocus={(event) => {
- onFocus?.(event);
- combobox.openDropdown();
- }}
- onBlur={(event) => {
- onBlur?.(event);
- combobox.closeDropdown();
- setPreviewUrl(value);
- setSearch(value || "");
- }}
- rightSectionPointerEvents="none"
- withAsterisk
- error={error}
- label={tCommon("iconPicker.label")}
- placeholder={tCommon("iconPicker.header", { countIcons: data?.countIcons ?? 0 })}
- />
+
+ }
+ leftSection={
+ previewUrl ? (
+ // eslint-disable-next-line @next/next/no-img-element
+
+ ) : null
+ }
+ value={search}
+ onChange={(event) => {
+ combobox.openDropdown();
+ combobox.updateSelectedOptionIndex();
+ setSearch(event.currentTarget.value);
+ setValue(event.currentTarget.value);
+ setPreviewUrl(null);
+ onChange(event.currentTarget.value);
+ }}
+ onClick={() => combobox.openDropdown()}
+ onFocus={(event) => {
+ onFocus?.(event);
+ combobox.openDropdown();
+ }}
+ onBlur={(event) => {
+ onBlur?.(event);
+ combobox.closeDropdown();
+ setPreviewUrl(value);
+ setSearch(value || "");
+ }}
+ rightSectionPointerEvents="none"
+ withAsterisk
+ error={error}
+ label={tCommon("iconPicker.label")}
+ placeholder={tCommon("iconPicker.header", { countIcons: data?.countIcons ?? 0 })}
+ />
+ {session?.user.permissions.includes("media-upload") && (
+ {
+ startTransition(() => {
+ setValue(url);
+ setPreviewUrl(url);
+ setSearch(url);
+ onChange(url);
+ });
+ }}
+ >
+ {({ onClick, loading }) => (
+
+
+
+ )}
+
+ )}
+