Fix optional empty value update

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-10-16 10:32:32 +02:00
parent 6253254335
commit 611b549f08
18 changed files with 516 additions and 497 deletions

View File

@@ -25,6 +25,7 @@ import type { TaskFormDialogFragment$key } from "./__generated__/TaskFormDialogF
import { MeasureSelectField } from "/components/form/MeasureSelectField";
import { Controller } from "react-hook-form";
import { updateStoreCounter } from "/hooks/useMutationWithIncrement";
import { formatDatetime } from "@probo/helpers";
const taskFragment = graphql`
fragment TaskFormDialogFragment on Task {
@@ -74,11 +75,7 @@ const schema = z.object({
timeEstimate: z.string().nullable(),
assignedToId: z.string(),
measureId: z.string(),
deadline: z
.date({
coerce: true,
})
.nullable(),
deadline: z.string().optional(),
});
type Props = {
@@ -113,7 +110,7 @@ export default function TaskFormDialog(props: Props) {
timeEstimate: task?.timeEstimate ?? "",
assignedToId: task?.assignedTo?.id ?? "",
measureId: task?.measure?.id ?? props.measureId ?? "",
deadline: task?.deadline?.split("T")[0] ?? null,
deadline: task?.deadline?.split("T")[0] ?? "",
},
});
@@ -126,7 +123,7 @@ export default function TaskFormDialog(props: Props) {
name: data.name,
description: data.description,
timeEstimate: data.timeEstimate || null,
deadline: data.deadline,
deadline: formatDatetime(data.deadline) ?? null,
},
},
});
@@ -138,7 +135,7 @@ export default function TaskFormDialog(props: Props) {
name: data.name,
description: data.description,
timeEstimate: data.timeEstimate || null,
deadline: data.deadline || null,
deadline: formatDatetime(data.deadline) ?? null,
assignedToId: data.assignedToId,
measureId: data.measureId,
},

View File

@@ -188,8 +188,8 @@ export const useUpdateAudit = () => {
return (input: {
id: string;
name?: string;
validFrom?: string;
validUntil?: string;
validFrom?: string | null;
validUntil?: string | null;
state?: string;
}) => {
if (!input.id) {

View File

@@ -189,7 +189,7 @@ export const useUpdateContinualImprovement = () => {
description?: string;
source?: string;
ownerId?: string;
targetDate?: string;
targetDate?: string | null;
status?: string;
priority?: string;
}) => {

View File

@@ -222,12 +222,12 @@ export const useUpdateNonconformity = () => {
id: string;
referenceId?: string;
description?: string;
dateIdentified?: string;
dateIdentified?: string | null;
rootCause?: string;
correctiveAction?: string;
ownerId?: string;
auditId?: string;
dueDate?: string;
dueDate?: string | null;
status?: string;
effectivenessCheck?: string;
}) => {

View File

@@ -195,8 +195,8 @@ export const useUpdateObligation = () => {
actionsToBeImplemented?: string;
regulator?: string;
ownerId?: string;
lastReviewDate?: string;
dueDate?: string;
lastReviewDate?: string | null;
dueDate?: string | null;
status?: string;
}) => {
if (!input.id) {

View File

@@ -23,12 +23,11 @@ import {
Tr,
useConfirm,
useDialogRef,
useToast,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import type { PreloadedQuery } from "react-relay";
import type { OrganizationGraph_ViewQuery } from "/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql";
import { useFragment, useMutation, usePreloadedQuery, usePaginationFragment } from "react-relay";
import { useFragment, usePreloadedQuery, usePaginationFragment } from "react-relay";
import { organizationViewQuery } from "/hooks/graph/OrganizationGraph";
import { graphql } from "relay-runtime";
import { SortableTable, SortableTh } from "/components/SortableTable";
@@ -45,7 +44,7 @@ import type {
SettingsPageInvitationsFragment$data,
SettingsPageInvitationsFragment$key
} from "./__generated__/SettingsPageInvitationsFragment.graphql";
import { useState, type ChangeEventHandler, useEffect } from "react";
import { useState, type ChangeEventHandler, useEffect, useRef } from "react";
import { sprintf } from "@probo/helpers";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { z } from "zod";
@@ -226,7 +225,6 @@ export default function SettingsPage({ queryRef }: Props) {
organizationViewQuery,
queryRef
).node;
const { toast } = useToast();
const organization = useFragment<SettingsPageFragment$key>(
organizationFragment,
organizationKey
@@ -250,7 +248,13 @@ export default function SettingsPage({ queryRef }: Props) {
invitationsPagination.refetch({}, { fetchPolicy: 'network-only' });
};
const [updateOrganization] = useMutation(updateOrganizationMutation);
const [updateOrganization, isUpdatingOrganization] = useMutationWithToasts(
updateOrganizationMutation,
{
successMessage: __("Organization updated successfully"),
errorMessage: __("Failed to update organization"),
}
);
const [deleteHorizontalLogo, isDeletingHorizontalLogo] = useMutationWithToasts(
deleteHorizontalLogoMutation,
{
@@ -262,8 +266,6 @@ export default function SettingsPage({ queryRef }: Props) {
const memberships = membershipsPagination.data.memberships?.edges.map((edge) => edge.node) || [];
const invitations = invitationsPagination.data.invitations?.edges.map((edge) => edge.node) || [];
const [activeTab, setActiveTab] = useState<"memberships" | "invitations">("memberships");
const [logoFile, setLogoFile] = useState<File | null>(null);
const [horizontalLogoFile, setHorizontalLogoFile] = useState<File | null>(null);
const [logoPreview, setLogoPreview] = useState<string | null>(null);
const [horizontalLogoPreview, setHorizontalLogoPreview] = useState<string | null>(null);
@@ -280,31 +282,45 @@ export default function SettingsPage({ queryRef }: Props) {
}
);
const prevOrgDataRef = useRef({
name: organization.name,
description: organization.description,
websiteUrl: organization.websiteUrl,
email: organization.email,
headquarterAddress: organization.headquarterAddress,
});
useEffect(() => {
reset({
name: organization.name || "",
description: organization.description || "",
websiteUrl: organization.websiteUrl || "",
email: organization.email || "",
headquarterAddress: organization.headquarterAddress || "",
});
setLogoFile(null);
setHorizontalLogoFile(null);
setLogoPreview(null);
setHorizontalLogoPreview(null);
const prev = prevOrgDataRef.current;
const hasFormFieldChanges =
prev.name !== organization.name ||
prev.description !== organization.description ||
prev.websiteUrl !== organization.websiteUrl ||
prev.email !== organization.email ||
prev.headquarterAddress !== organization.headquarterAddress;
if (hasFormFieldChanges) {
reset({
name: organization.name || "",
description: organization.description || "",
websiteUrl: organization.websiteUrl || "",
email: organization.email || "",
headquarterAddress: organization.headquarterAddress || "",
});
setLogoPreview(null);
setHorizontalLogoPreview(null);
prevOrgDataRef.current = {
name: organization.name,
description: organization.description,
websiteUrl: organization.websiteUrl,
email: organization.email,
headquarterAddress: organization.headquarterAddress,
};
}
}, [organization, reset]);
const onSubmit = handleSubmit((data: OrganizationFormData) => {
const uploadables: Record<string, File> = {};
if (logoFile) {
uploadables["input.logo"] = logoFile;
}
if (horizontalLogoFile) {
uploadables["input.horizontalLogoFile"] = horizontalLogoFile;
}
updateOrganization({
variables: {
input: {
@@ -314,27 +330,8 @@ export default function SettingsPage({ queryRef }: Props) {
websiteUrl: data.websiteUrl || undefined,
email: data.email || undefined,
headquarterAddress: data.headquarterAddress || undefined,
logo: logoFile ? null : undefined,
horizontalLogoFile: horizontalLogoFile ? null : undefined,
},
},
uploadables: Object.keys(uploadables).length > 0 ? uploadables : undefined,
onError() {
toast({
title: __("Error"),
description: __("Failed to update organization."),
variant: "error",
});
},
onCompleted() {
toast({
title: __("Organization updated"),
description: __(
"Your organization details have been updated successfully."
),
variant: "success",
});
},
});
});
@@ -343,12 +340,27 @@ export default function SettingsPage({ queryRef }: Props) {
if (!file) {
return;
}
setLogoFile(file);
const reader = new FileReader();
reader.onloadend = () => {
setLogoPreview(reader.result as string);
};
reader.readAsDataURL(file);
updateOrganization({
variables: {
input: {
organizationId: organization.id,
logoFile: null,
},
},
uploadables: {
"input.logoFile": file,
},
onSuccess: () => {
setLogoPreview(null);
},
});
};
const handleHorizontalLogoChange: ChangeEventHandler<HTMLInputElement> = (e) => {
@@ -356,12 +368,27 @@ export default function SettingsPage({ queryRef }: Props) {
if (!file) {
return;
}
setHorizontalLogoFile(file);
const reader = new FileReader();
reader.onloadend = () => {
setHorizontalLogoPreview(reader.result as string);
};
reader.readAsDataURL(file);
updateOrganization({
variables: {
input: {
organizationId: organization.id,
horizontalLogoFile: null,
},
},
uploadables: {
"input.horizontalLogoFile": file,
},
onSuccess: () => {
setHorizontalLogoPreview(null);
},
});
};
const deleteDialogRef = useDialogRef();
@@ -416,13 +443,13 @@ export default function SettingsPage({ queryRef }: Props) {
size="xl"
/>
<FileButton
disabled={formState.isSubmitting}
disabled={formState.isSubmitting || isUpdatingOrganization}
onChange={handleLogoChange}
variant="secondary"
className="ml-auto"
accept="image/png,image/jpeg,image/jpg"
>
{__("Change logo")}
{isUpdatingOrganization ? __("Uploading...") : __("Change logo")}
</FileButton>
</div>
</div>
@@ -442,14 +469,18 @@ export default function SettingsPage({ queryRef }: Props) {
</div>
)}
<FileButton
disabled={formState.isSubmitting}
disabled={formState.isSubmitting || isUpdatingOrganization}
onChange={handleHorizontalLogoChange}
variant="secondary"
accept="image/png,image/jpeg,image/jpg"
>
{(horizontalLogoPreview || organization.horizontalLogoUrl) ? __("Change horizontal logo") : __("Upload horizontal logo")}
{isUpdatingOrganization
? __("Uploading...")
: (horizontalLogoPreview || organization.horizontalLogoUrl)
? __("Change horizontal logo")
: __("Upload horizontal logo")}
</FileButton>
{(organization.horizontalLogoUrl && !horizontalLogoFile) && (
{organization.horizontalLogoUrl && (
<Dialog
ref={deleteDialogRef}
trigger={
@@ -533,10 +564,10 @@ export default function SettingsPage({ queryRef }: Props) {
/>
</div>
{(formState.isDirty || logoFile || horizontalLogoFile) && (
{formState.isDirty && (
<div className="flex justify-end pt-6">
<Button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting
<Button type="submit" disabled={formState.isSubmitting || isUpdatingOrganization}>
{(formState.isSubmitting || isUpdatingOrganization)
? __("Updating...")
: __("Update Organization")}
</Button>

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<ce571f7246b6f7500a132f28c8081f84>>
* @generated SignedSource<<ac93ed76dd8e53876d200e40f5b62edb>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -14,7 +14,7 @@ export type UpdateOrganizationInput = {
email?: string | null | undefined;
headquarterAddress?: string | null | undefined;
horizontalLogoFile?: any | null | undefined;
logo?: any | null | undefined;
logoFile?: any | null | undefined;
name?: string | null | undefined;
organizationId: string;
websiteUrl?: string | null | undefined;

View File

@@ -83,8 +83,8 @@ export default function AuditDetailsPage(props: Props) {
await updateAudit({
id: auditEntry.id,
name: formData.name,
validFrom: formatDatetime(formData.validFrom),
validUntil: formatDatetime(formData.validUntil),
validFrom: formatDatetime(formData.validFrom) ?? null,
validUntil: formatDatetime(formData.validUntil) ?? null,
state: formData.state,
});
reset(formData);

View File

@@ -98,7 +98,7 @@ export default function ContinualImprovementDetailsPage(props: Props) {
referenceId: formData.referenceId,
description: formData.description || undefined,
source: formData.source || undefined,
targetDate: formatDatetime(formData.targetDate),
targetDate: formatDatetime(formData.targetDate) ?? null,
status: formData.status,
priority: formData.priority,
ownerId: formData.ownerId,

View File

@@ -103,8 +103,8 @@ export default function NonconformityDetailsPage(props: Props) {
id: nonconformity.id,
referenceId: formData.referenceId,
description: formData.description,
dateIdentified: formatDatetime(formData.dateIdentified),
dueDate: formatDatetime(formData.dueDate),
dateIdentified: formatDatetime(formData.dateIdentified) ?? null,
dueDate: formatDatetime(formData.dueDate) ?? null,
rootCause: formData.rootCause,
correctiveAction: formData.correctiveAction,
effectivenessCheck: formData.effectivenessCheck,

View File

@@ -109,8 +109,8 @@ export default function ObligationDetailsPage(props: Props) {
requirement: formData.requirement || undefined,
actionsToBeImplemented: formData.actionsToBeImplemented || undefined,
regulator: formData.regulator || undefined,
lastReviewDate: formatDatetime(formData.lastReviewDate),
dueDate: formatDatetime(formData.dueDate),
lastReviewDate: formatDatetime(formData.lastReviewDate) ?? null,
dueDate: formatDatetime(formData.dueDate) ?? null,
status: formData.status,
ownerId: formData.ownerId,
});