chore(release): automatic release v1.45.3

This commit is contained in:
homarr-releases[bot]
2025-12-08 16:10:20 +00:00
committed by GitHub
49 changed files with 416 additions and 336 deletions

View File

@@ -11,7 +11,7 @@ jobs:
if: github.actor_id == 158783068 || github.actor_id == 190541745 || github.actor_id == 210161987 # Id of renovate bot and crowdin bot see https://api.github.com/users/homarr-renovate%5Bbot%5D and https://api.github.com/users/homarr-crowdin%5Bbot%5D and https://api.github.com/users/homarr-update-contributors%5Bbot%5D
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Obtain token
id: obtainToken
uses: actions/create-github-app-token@v2

View File

@@ -24,7 +24,7 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
@@ -39,7 +39,7 @@ jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
@@ -50,7 +50,7 @@ jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
@@ -61,7 +61,7 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
@@ -79,7 +79,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
- name: Build docker image
@@ -102,7 +102,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
- name: Copy env

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Obtain token
id: obtainToken

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Upload Crowdin translations
uses: crowdin/github-action@v2

View File

@@ -55,7 +55,7 @@ jobs:
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to commit package.json & changelog changes, merge them to dev and publish the release
- uses: actions/checkout@v5
- uses: actions/checkout@v6
if: env.SKIP_RELEASE == 'false'
with:
persist-credentials: false
@@ -115,7 +115,7 @@ jobs:
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ needs.release.outputs.git_ref }}
@@ -154,7 +154,7 @@ jobs:
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ needs.release.outputs.git_ref }}

View File

@@ -27,7 +27,7 @@ jobs:
with:
args: "Automatic release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get Next Version

View File

@@ -12,7 +12,7 @@ jobs:
renovate-validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- run: |
npx --yes --package renovate -- \
renovate-config-validator --strict .github/renovate.json5

View File

@@ -49,7 +49,7 @@ jobs:
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
token: ${{ steps.obtainToken.outputs.token }}
- name: Setup

View File

@@ -28,7 +28,7 @@ jobs:
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}

View File

@@ -27,7 +27,7 @@ jobs:
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
- name: Setup

View File

@@ -74,9 +74,9 @@
"dayjs": "^1.11.19",
"dotenv": "^17.2.3",
"flag-icons": "^7.5.0",
"glob": "^12.0.0",
"glob": "^13.0.0",
"isomorphic-dompurify": "^2.33.0",
"jotai": "^2.15.1",
"jotai": "^2.15.2",
"mantine-react-table": "2.0.0-beta.9",
"next": "16.0.7",
"postcss-preset-mantine": "^1.18.0",
@@ -104,7 +104,7 @@
"concurrently": "^9.2.1",
"eslint": "^9.39.1",
"node-loader": "^2.1.0",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"typescript": "^5.9.3"
}
}

View File

@@ -12,7 +12,11 @@ import { groupCreateSchema } from "@homarr/validation/group";
export const InitGroup = () => {
const t = useI18n();
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation();
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/init");
},
});
const form = useZodForm(groupCreateSchema, {
initialValues: {
name: "",
@@ -21,9 +25,6 @@ export const InitGroup = () => {
const handleSubmitAsync = async (values: z.infer<typeof groupCreateSchema>) => {
await mutateAsync(values, {
async onSuccess() {
await revalidatePathActionAsync("/init");
},
onError(error) {
if (error.data?.code === "CONFLICT") {
form.setErrors({ name: t("common.zod.errors.custom.groupNameTaken") });

View File

@@ -17,7 +17,11 @@ import { settingsInitSchema } from "@homarr/validation/settings";
export const InitSettings = () => {
const tSection = useScopedI18n("management.page.settings.section");
const t = useI18n();
const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation();
const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/init");
},
});
const form = useZodForm(settingsInitSchema, { initialValues: defaultServerSettings });
form.watch("analytics.enableGeneral", ({ value }) => {
@@ -31,11 +35,7 @@ export const InitSettings = () => {
});
const handleSubmitAsync = async (values: z.infer<typeof settingsInitSchema>) => {
await mutateAsync(values, {
async onSuccess() {
await revalidatePathActionAsync("/init");
},
});
await mutateAsync(values);
};
return (

View File

@@ -26,19 +26,19 @@ export const InitUserForm = () => {
const handleSubmitAsync = async (values: FormType) => {
await mutateAsync(values, {
async onSuccess() {
onSuccess() {
showSuccessNotification({
title: tUser("notification.success.title"),
message: tUser("notification.success.message"),
});
await signIn("credentials", {
void signIn("credentials", {
name: values.username,
password: values.password,
redirect: false,
}).then(async () => {
await revalidatePathActionAsync("/init");
});
await revalidatePathActionAsync("/init");
},
onError: (error) => {
showErrorNotification({

View File

@@ -84,9 +84,6 @@ export const BoardCardMenuDropdown = ({ board }: BoardCardMenuDropdownProps) =>
id: board.id,
name: board.name,
},
onSuccess: async () => {
await revalidatePathActionAsync("/manage/boards");
},
});
}, [board.id, board.name, openDuplicateModal]);

View File

@@ -86,7 +86,11 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
},
});
const { mutateAsync: createIntegrationAsync, isPending } = clientApi.integration.create.useMutation();
const { mutateAsync: createIntegrationAsync, isPending } = clientApi.integration.create.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/manage/integrations");
},
});
const [error, setError] = useState<null | AnyMappedTestConnectionError>(null);
const handleSubmitAsync = async ({ appId, appHref, hasApp, ...values }: FormType) => {
@@ -116,7 +120,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
app,
},
{
async onSuccess(data) {
onSuccess(data) {
// We do it this way as we are unable to send a typesafe error through onError
if (data?.error) {
setError(data.error);
@@ -132,7 +136,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
message: t("integration.page.create.notification.success.message"),
});
await revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
router.push("/manage/integrations");
},
onError: () => {
showErrorNotification({

View File

@@ -15,7 +15,11 @@ interface RemoveCertificateProps {
export const RemoveCertificate = ({ fileName }: RemoveCertificateProps) => {
const { openConfirmModal } = useConfirmModal();
const { mutateAsync } = clientApi.certificates.removeCertificate.useMutation();
const { mutateAsync } = clientApi.certificates.removeCertificate.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/manage/tools/certificates");
},
});
const t = useI18n();
const handleClick = () => {
@@ -27,12 +31,11 @@ export const RemoveCertificate = ({ fileName }: RemoveCertificateProps) => {
await mutateAsync(
{ fileName },
{
async onSuccess() {
onSuccess() {
showSuccessNotification({
title: t("certificate.action.remove.notification.success.title"),
message: t("certificate.action.remove.notification.success.message"),
});
await revalidatePathActionAsync("/manage/tools/certificates");
},
onError() {
showErrorNotification({

View File

@@ -15,7 +15,11 @@ interface RemoveHostnameActionIconProps {
}
export const RemoveHostnameActionIcon = (input: RemoveHostnameActionIconProps) => {
const { mutateAsync } = clientApi.certificates.removeTrustedHostname.useMutation();
const { mutateAsync } = clientApi.certificates.removeTrustedHostname.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/manage/tools/certificates/hostnames");
},
});
const { openConfirmModal } = useConfirmModal();
const t = useI18n();
@@ -26,8 +30,7 @@ export const RemoveHostnameActionIcon = (input: RemoveHostnameActionIconProps) =
// eslint-disable-next-line no-restricted-syntax
async onConfirm() {
await mutateAsync(input, {
async onSuccess() {
await revalidatePathActionAsync("/manage/tools/certificates/hostnames");
onSuccess() {
showSuccessNotification({
title: t("certificate.action.removeHostname.notification.success.title"),
message: t("certificate.action.removeHostname.notification.success.message"),

View File

@@ -206,15 +206,16 @@ const ContainerActionBarButton = (props: ContainerActionBarButtonProps) => {
const t = useScopedI18n("docker.action");
const utils = clientApi.useUtils();
const { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation();
const { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation({
async onSettled() {
await utils.docker.getContainers.invalidate();
},
});
const handleClickAsync = async () => {
await mutateAsync(
{ ids: props.selectedIds },
{
async onSettled() {
await utils.docker.getContainers.invalidate();
},
onSuccess() {
showSuccessNotification({
title: t(`${props.action}.notification.success.title`),

View File

@@ -18,7 +18,12 @@ interface UserProfileAvatarForm {
}
export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
const { mutate } = clientApi.user.setProfileImage.useMutation();
const { mutate } = clientApi.user.setProfileImage.useMutation({
async onSuccess() {
// Revalidate all as the avatar is used in multiple places
await revalidatePathActionAsync("/");
},
});
const [opened, { toggle }] = useDisclosure(false);
const { openConfirmModal } = useConfirmModal();
const t = useI18n();
@@ -38,9 +43,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
image: base64Url,
},
{
async onSuccess() {
// Revalidate all as the avatar is used in multiple places
await revalidatePathActionAsync("/");
onSuccess() {
showSuccessNotification({
message: tManageAvatar("changeImage.notification.success.message"),
});
@@ -74,9 +77,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
image: null,
},
{
async onSuccess() {
// Revalidate all as the avatar is used in multiple places
await revalidatePathActionAsync("/");
onSuccess() {
showSuccessNotification({
message: tManageAvatar("removeImage.notification.success.message"),
});

View File

@@ -25,7 +25,11 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
const [innerOwnerId, setInnerOwnerId] = useState(group.ownerId);
const { openModal } = useModalAction(UserSelectModal);
const { openConfirmModal } = useConfirmModal();
const { mutateAsync } = clientApi.group.transferOwnership.useMutation();
const { mutateAsync } = clientApi.group.transferOwnership.useMutation({
async onSuccess() {
await revalidatePathActionAsync(`/manage/users/groups/${group.id}`);
},
});
const handleTransfer = useCallback(() => {
openModal(
@@ -47,7 +51,7 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
userId: id,
},
{
async onSuccess() {
onSuccess() {
setInnerOwnerId(id);
showSuccessNotification({
title: tRoot("common.notification.transfer.success"),
@@ -56,7 +60,6 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
user: name,
}),
});
await revalidatePathActionAsync(`/manage/users/groups/${group.id}`);
},
onError() {
showErrorNotification({

View File

@@ -56,16 +56,19 @@ export const GroupsTable = ({ groups, initialGroupIds, hasFilter }: GroupsTableP
() => initialGroupIds.some((groupId, index) => groupIds.indexOf(groupId) !== index),
[groupIds, initialGroupIds],
);
const { mutateAsync, isPending } = clientApi.group.savePositions.useMutation();
const { mutateAsync, isPending } = clientApi.group.savePositions.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/manage/users/groups");
},
});
const handleSavePositionsAsync = async () => {
await mutateAsync(
{ positions: groupIds },
{
async onSuccess() {
onSuccess() {
showSuccessNotification({
message: t("group.action.changePosition.notification.success.message"),
});
await revalidatePathActionAsync("/manage/users/groups");
},
onError() {
showSuccessNotification({

View File

@@ -51,7 +51,7 @@
"dotenv-cli": "^11.0.0",
"esbuild": "^0.27.0",
"eslint": "^9.39.1",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"tsx": "4.20.4",
"typescript": "^5.9.3"
}

View File

@@ -36,7 +36,7 @@
"@types/ws": "^8.18.1",
"esbuild": "^0.27.0",
"eslint": "^9.39.1",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"typescript": "^5.9.3"
}
}

View File

@@ -45,19 +45,19 @@
"@testcontainers/redis": "^11.9.0",
"@turbo/gen": "^2.6.1",
"@vitejs/plugin-react": "^5.1.1",
"@vitest/coverage-v8": "^4.0.14",
"@vitest/ui": "^4.0.14",
"@vitest/coverage-v8": "^4.0.15",
"@vitest/ui": "^4.0.15",
"conventional-changelog-conventionalcommits": "^9.1.0",
"cross-env": "^10.1.0",
"jsdom": "^27.2.0",
"json5": "^2.2.3",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"semantic-release": "^25.0.2",
"testcontainers": "^11.9.0",
"turbo": "^2.6.1",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^4.0.14"
"vitest": "^4.0.15"
},
"packageManager": "pnpm@10.24.0",
"engines": {
@@ -95,7 +95,7 @@
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
"tmp@<=0.2.3": ">=0.2.5",
"vite@>=5.0.0 <=5.4.18": ">=7.2.4"
"vite@>=5.0.0 <=5.4.18": ">=7.2.6"
},
"patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch",

View File

@@ -61,7 +61,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.39.1",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"typescript": "^5.9.3"
}
}

View File

@@ -48,7 +48,7 @@
"@types/bcrypt": "6.0.0",
"@types/cookies": "0.9.2",
"eslint": "^9.39.1",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"typescript": "^5.9.3"
}
}

View File

@@ -6,14 +6,14 @@ import type { Database, InferInsertModel } from "@homarr/db";
import { and, eq } from "@homarr/db";
import { users } from "@homarr/db/schema";
import { logger } from "@homarr/log";
import type { userSignInSchema } from "@homarr/validation/user";
import type { ldapSignInSchema } from "@homarr/validation/user";
import { env } from "../../../env";
import { LdapClient } from "../ldap-client";
export const authorizeWithLdapCredentialsAsync = async (
db: Database,
credentials: z.infer<typeof userSignInSchema>,
credentials: z.infer<typeof ldapSignInSchema>,
) => {
logger.info(`user ${credentials.name} is trying to log in using LDAP. Connecting to LDAP server...`);
const client = new LdapClient();
@@ -38,7 +38,14 @@ export const authorizeWithLdapCredentialsAsync = async (
attributes: [env.AUTH_LDAP_USERNAME_ATTRIBUTE, env.AUTH_LDAP_USER_MAIL_ATTRIBUTE],
},
})
.then((entries) => entries.at(0));
.then((entries) => {
if (entries.length > 1) {
logger.warn(`Multiple LDAP users found for ${credentials.name}, expected only one.`);
throw new CredentialsSignin();
}
return entries.at(0);
});
if (!ldapUser) {
logger.warn(`User ${credentials.name} not found in LDAP`);

View File

@@ -1,7 +1,7 @@
import type Credentials from "@auth/core/providers/credentials";
import type { Database } from "@homarr/db";
import { userSignInSchema } from "@homarr/validation/user";
import { ldapSignInSchema, userSignInSchema } from "@homarr/validation/user";
import { authorizeWithBasicCredentialsAsync } from "./authorization/basic-authorization";
import { authorizeWithLdapCredentialsAsync } from "./authorization/ldap-authorization";
@@ -28,7 +28,7 @@ export const createLdapConfiguration = (db: Database) =>
name: "Ldap",
// eslint-disable-next-line no-restricted-syntax
async authorize(credentials) {
const data = await userSignInSchema.parseAsync(credentials);
const data = await ldapSignInSchema.parseAsync(credentials);
return await authorizeWithLdapCredentialsAsync(db, data).catch(() => null);
},
}) satisfies CredentialsConfiguration;

View File

@@ -71,7 +71,7 @@
"dotenv-cli": "^11.0.0",
"esbuild": "^0.27.0",
"eslint": "^9.39.1",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"tsx": "4.20.4",
"typescript": "^5.9.3"
}

View File

@@ -1,7 +1,9 @@
import type { ChangeEvent, FocusEvent } from "react";
export interface InputPropsFor<T, TOnChangeArg, TComponent extends HTMLElement = HTMLInputElement>
extends BasePropsFor<TOnChangeArg, TComponent> {
export interface InputPropsFor<T, TOnChangeArg, TComponent extends HTMLElement = HTMLInputElement> extends BasePropsFor<
TOnChangeArg,
TComponent
> {
value?: T;
defaultValue?: T;
}

View File

@@ -16,7 +16,19 @@ interface UploadMediaProps {
export const UploadMedia = ({ children, onSettled, onSuccess, multiple = false }: UploadMediaProps) => {
const t = useI18n();
const { mutateAsync, isPending } = clientApi.media.uploadMedia.useMutation();
const { mutateAsync, isPending } = clientApi.media.uploadMedia.useMutation({
async onSuccess(mediaIds) {
await onSuccess?.(
mediaIds.map((id) => ({
id,
url: `/api/user-medias/${id}`,
})),
);
},
async onSettled() {
await onSettled?.();
},
});
const handleFileUploadAsync = async (files: File[] | File | null) => {
if (!files || (Array.isArray(files) && files.length === 0)) return;
@@ -24,25 +36,16 @@ export const UploadMedia = ({ children, onSettled, onSuccess, multiple = false }
const formData = new FormData();
filesArray.forEach((file) => formData.append("files", file));
await mutateAsync(formData, {
async onSuccess(mediaIds) {
onSuccess() {
showSuccessNotification({
message: t("media.action.upload.notification.success.message"),
});
await onSuccess?.(
mediaIds.map((id) => ({
id,
url: `/api/user-medias/${id}`,
})),
);
},
onError() {
showErrorNotification({
message: t("media.action.upload.notification.error.message"),
});
},
async onSettled() {
await onSettled?.();
},
});
};

View File

@@ -2,7 +2,6 @@ import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import type { MaybePromise } from "@homarr/common/types";
import { AppForm } from "@homarr/forms-collection";
import { createModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
@@ -10,7 +9,7 @@ import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { appManageSchema } from "@homarr/validation/app";
interface QuickAddAppModalProps {
onClose: (createdApp: Omit<RouterOutputs["app"]["create"], "appId">) => MaybePromise<void>;
onClose: (createdApp: Omit<RouterOutputs["app"]["create"], "appId">) => void;
}
export const QuickAddAppModal = createModal<QuickAddAppModalProps>(({ actions, innerProps }) => {
@@ -28,13 +27,13 @@ export const QuickAddAppModal = createModal<QuickAddAppModalProps>(({ actions, i
const handleSubmit = (values: z.infer<typeof appManageSchema>) => {
mutate(values, {
async onSuccess(app) {
onSuccess(app) {
showSuccessNotification({
title: tScoped("success.title"),
message: tScoped("success.message"),
});
await innerProps.onClose(app);
innerProps.onClose(app);
actions.closeModal();
},
});

View File

@@ -1,7 +1,7 @@
import { Button, Group, Stack, Text, TextInput } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import type { MaybePromise } from "@homarr/common/types";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
@@ -15,7 +15,6 @@ interface InnerProps {
id: string;
name: string;
};
onSuccess: () => MaybePromise<void>;
}
export const DuplicateBoardModal = createModal<InnerProps>(({ actions, innerProps }) => {
@@ -27,7 +26,11 @@ export const DuplicateBoardModal = createModal<InnerProps>(({ actions, innerProp
},
});
const boardNameStatus = useBoardNameStatus(form.values.name);
const { mutateAsync, isPending } = clientApi.board.duplicateBoard.useMutation();
const { mutateAsync, isPending } = clientApi.board.duplicateBoard.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/manage/boards");
},
});
return (
<form
@@ -40,13 +43,12 @@ export const DuplicateBoardModal = createModal<InnerProps>(({ actions, innerProp
id: innerProps.board.id,
},
{
async onSuccess() {
onSuccess() {
actions.closeModal();
showSuccessNotification({
title: t("board.action.duplicate.notification.success.title"),
message: t("board.action.duplicate.notification.success.message"),
});
await innerProps.onSuccess();
},
onError() {
showErrorNotification({

View File

@@ -65,7 +65,11 @@ export const ImportBoardModal = createModal(({ actions }) => {
},
);
const { mutateAsync, isPending } = clientApi.board.importOldmarrConfig.useMutation();
const { mutateAsync, isPending } = clientApi.board.importOldmarrConfig.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/manage/boards");
},
});
const boardNameStatus = useBoardNameStatus(form.values.configuration.name);
const handleSubmitAsync = async (values: { file: File; configuration: OldmarrImportConfiguration }) => {
@@ -74,9 +78,8 @@ export const ImportBoardModal = createModal(({ actions }) => {
formData.set("configuration", JSON.stringify(values.configuration));
await mutateAsync(formData, {
async onSuccess() {
onSuccess() {
actions.closeModal();
await revalidatePathActionAsync("/manage/boards");
showSuccessNotification({
title: tOldImport("notification.success.title"),
message: tOldImport("notification.success.message"),

View File

@@ -27,7 +27,11 @@ export const AddCertificateModal = createModal<InnerProps>(({ actions, innerProp
},
},
);
const { mutateAsync } = clientApi.certificates.addCertificate.useMutation();
const { mutateAsync } = clientApi.certificates.addCertificate.useMutation({
async onSuccess() {
await innerProps.onSuccess?.();
},
});
return (
<form
@@ -35,12 +39,11 @@ export const AddCertificateModal = createModal<InnerProps>(({ actions, innerProp
const formData = new FormData();
formData.set("file", values.file);
await mutateAsync(formData, {
async onSuccess() {
onSuccess() {
showSuccessNotification({
title: t("certificate.action.create.notification.success.title"),
message: t("certificate.action.create.notification.success.message"),
});
await innerProps.onSuccess?.();
actions.closeModal();
},
onError() {

View File

@@ -29,10 +29,10 @@ export const ImportTokenModal = createModal<InnerProps>(({ actions, innerProps }
mutate(
{ checksum: innerProps.checksum, token: values.token },
{
async onSuccess(isValid) {
onSuccess(isValid) {
if (isValid) {
actions.closeModal();
await innerProps.onSuccessAsync(values.token);
void innerProps.onSuccessAsync(values.token);
} else {
showErrorNotification({
title: tTokenModal("notification.error.title"),

View File

@@ -37,7 +37,7 @@
"@mantine/hooks": "^8.3.9",
"@mantine/spotlight": "^8.3.9",
"@tabler/icons-react": "^3.35.0",
"jotai": "^2.15.1",
"jotai": "^2.15.2",
"next": "16.0.7",
"react": "19.2.0",
"react-dom": "19.2.0",

View File

@@ -33,7 +33,7 @@
"deepmerge": "4.3.1",
"mantine-react-table": "2.0.0-beta.9",
"next": "16.0.7",
"next-intl": "4.5.6",
"next-intl": "4.5.8",
"react": "19.2.0",
"react-dom": "19.2.0"
},

View File

@@ -2232,7 +2232,7 @@
"unknown": "Ukendt",
"pending": "Afventende",
"processing": "Behandler",
"requested": "",
"requested": "Anmodet",
"partiallyAvailable": "Delvis",
"available": "Tilgængelig",
"blacklisted": "Blacklistet",

View File

@@ -747,7 +747,7 @@
},
"statusCode": {
"title": "Antwoord fout",
"description": "",
"description": "Onverwachte {statusCode} ({reason}) reactie van <url></url>. Controleer of de URL wijst naar de basis-URL van de integratie.",
"otherDescription": "",
"reason": {
"badRequest": "Onjuist verzoek",
@@ -2232,7 +2232,7 @@
"unknown": "Onbekend",
"pending": "In afwachting",
"processing": "Verwerken",
"requested": "",
"requested": "Aangevraagd",
"partiallyAvailable": "Gedeeltelijk",
"available": "Beschikbaar",
"blacklisted": "Geblacklist",

View File

@@ -10,8 +10,10 @@ interface BaseSelectItem {
label: string;
}
export interface SelectWithCustomItemsProps<TSelectItem extends BaseSelectItem>
extends Pick<SelectProps, "label" | "error" | "defaultValue" | "value" | "onChange" | "placeholder" | "clearable"> {
export interface SelectWithCustomItemsProps<TSelectItem extends BaseSelectItem> extends Pick<
SelectProps,
"label" | "error" | "defaultValue" | "value" | "onChange" | "placeholder" | "clearable"
> {
data: TSelectItem[];
description?: string;
withAsterisk?: boolean;

View File

@@ -69,6 +69,17 @@ export const userSignInSchema = z.object({
password: z.string().min(1),
});
export const ldapSignInSchema = z.object({
name: z
.string()
.min(1)
// Prevent special characters that could lead to LDAP injection attacks
.regex(/^[^\\,+<>;"=)(*|!&]+$/, {
message: "Invalid characters in ldap username",
}),
password: z.string().min(1),
});
export const userRegistrationSchema = addConfirmPasswordRefinement(
z.object({
username: usernameSchema,

View File

@@ -69,10 +69,10 @@ export const WidgetAppInput = ({ property, kind }: CommonWidgetInputProps<"app">
variant="default"
onClick={() =>
openModal({
// eslint-disable-next-line no-restricted-syntax
async onClose(createdAppId) {
await refetch();
form.setFieldValue(`options.${property}`, createdAppId);
onClose(createdAppId) {
void refetch().then(() => {
form.setFieldValue(`options.${property}`, createdAppId);
});
},
})
}

View File

@@ -111,9 +111,13 @@ const createColumns = (
header: t("action.title"),
Cell({ row }) {
const utils = clientApi.useUtils();
const { mutateAsync: startContainer } = clientApi.docker.startAll.useMutation();
const { mutateAsync: stopContainer } = clientApi.docker.stopAll.useMutation();
const { mutateAsync: restartContainer } = clientApi.docker.restartAll.useMutation();
// eslint-disable-next-line no-restricted-syntax
const onSettled = async () => {
await utils.docker.getContainers.invalidate();
};
const { mutateAsync: startContainer } = clientApi.docker.startAll.useMutation({ onSettled });
const { mutateAsync: stopContainer } = clientApi.docker.stopAll.useMutation({ onSettled });
const { mutateAsync: restartContainer } = clientApi.docker.restartAll.useMutation({ onSettled });
const handleActionAsync = async (action: "start" | "stop" | "restart") => {
const mutation = action === "start" ? startContainer : action === "stop" ? stopContainer : restartContainer;
@@ -121,9 +125,6 @@ const createColumns = (
await mutation(
{ ids: [row.original.id] },
{
async onSettled() {
await utils.docker.getContainers.invalidate();
},
onSuccess() {
showSuccessNotification({
title: t(`action.${action}.notification.success.title`),

View File

@@ -18,14 +18,17 @@ interface TextInput extends CommonInput<string> {
validate?: z.ZodType<string>;
}
interface MultiSelectInput<TOptions extends SelectOption[]>
extends CommonInput<inferSelectOptionValue<TOptions[number]>[]> {
interface MultiSelectInput<TOptions extends SelectOption[]> extends CommonInput<
inferSelectOptionValue<TOptions[number]>[]
> {
options: TOptions;
searchable?: boolean;
}
export interface SortableItemListInput<TItem, TOptionValue extends UniqueIdentifier>
extends Omit<CommonInput<TOptionValue[]>, "withDescription"> {
export interface SortableItemListInput<TItem, TOptionValue extends UniqueIdentifier> extends Omit<
CommonInput<TOptionValue[]>,
"withDescription"
> {
AddButton: (props: { addItem: (item: TItem) => void; values: TOptionValue[] }) => React.ReactNode;
ItemComponent: (props: {
item: TItem;
@@ -37,8 +40,9 @@ export interface SortableItemListInput<TItem, TOptionValue extends UniqueIdentif
useData: (values: TOptionValue[]) => { data: TItem[] | undefined; isLoading: boolean; error: unknown };
}
interface SelectInput<TOptions extends readonly SelectOption[]>
extends CommonInput<inferSelectOptionValue<TOptions[number]>> {
interface SelectInput<TOptions extends readonly SelectOption[]> extends CommonInput<
inferSelectOptionValue<TOptions[number]>
> {
options: TOptions;
searchable?: boolean;
}

433
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^6.1.1",
"typescript-eslint": "^8.46.2"
"typescript-eslint": "^8.48.1"
},
"devDependencies": {
"@homarr/prettier-config": "workspace:^0.1.0",

View File

@@ -11,7 +11,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
"prettier": "^3.6.2"
"prettier": "^3.7.4"
},
"devDependencies": {
"@homarr/tsconfig": "workspace:^0.1.0",