- <>{children}>
+
{children}
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/details/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/details/page.tsx
index 783ed0b9..5aceb7e1 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/details/page.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/details/page.tsx
@@ -1,8 +1,9 @@
'use client';
+import { useEffect, useState } from 'react';
+import { useParams } from 'next/navigation';
import { graphql } from '@/gql';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { useParams } from 'next/navigation';
import {
Checkbox,
Combobox,
@@ -14,7 +15,6 @@ import {
TextField,
toast,
} from 'opub-ui';
-import { useEffect, useState } from 'react';
import { GraphQL } from '@/lib/api';
import RichTextEditor from '@/components/RichTextEditor/RichTextEditor';
@@ -124,6 +124,8 @@ export default function AIModelDetailsPage() {
});
const [isTagsListUpdated, setIsTagsListUpdated] = useState(false);
+ const SAVE_SUCCESS_TOAST_ID = 'ai-model-details-save-success';
+ const SAVE_ERROR_TOAST_ID = 'ai-model-details-save-error';
const isValidHttpUrl = (value: string) => {
try {
const parsed = new URL(value);
@@ -213,7 +215,7 @@ export default function AIModelDetailsPage() {
),
{
onSuccess: () => {
- toast('AI Model updated successfully');
+ toast('AI Model updated successfully', { id: SAVE_SUCCESS_TOAST_ID });
setStatus('saved');
if (isTagsListUpdated) {
getTagsList.refetch();
@@ -223,7 +225,11 @@ export default function AIModelDetailsPage() {
queryClient.invalidateQueries([`fetch_AIModelForPublish_${params.id}`]);
},
onError: (error: any) => {
- toast(`Error: ${error.message}`);
+ const errorMessage =
+ typeof error?.message === 'string' && error.message.trim()
+ ? error.message.trim()
+ : 'Unable to update AI Model right now. Please try again.';
+ toast(`Error: ${errorMessage}`, { id: SAVE_ERROR_TOAST_ID });
setStatus('unsaved');
},
}
@@ -312,14 +318,14 @@ export default function AIModelDetailsPage() {
const handleSave = (overrideData?: any) => {
setStatus('saving');
const dataToUse = overrideData || formData;
-
+
// Ensure access type is always 'open' (required field)
if (dataToUse.accessType !== 'open') {
toast('Open access is required for all models');
setStatus('unsaved');
return;
}
-
+
const updateData: any = {
description: dataToUse.description,
modelType: dataToUse.modelType,
@@ -634,9 +640,7 @@ export default function AIModelDetailsPage() {
>
Open Access
-
- Model can be viewed and used by everyone
-
+ Model can be viewed and used by everyone
-
- Restricted Access
-
+
Restricted Access
Users would require to request access to the model.
Recommended for sensitive models.
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/publish/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/publish/page.tsx
index 59f49e04..1d38d131 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/publish/page.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/publish/page.tsx
@@ -1,25 +1,25 @@
'use client';
+import { useParams, useRouter } from 'next/navigation';
import { graphql } from '@/gql';
import { useMutation, useQuery } from '@tanstack/react-query';
-import { useParams, useRouter } from 'next/navigation';
import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
- Button,
- Icon,
- Spinner,
- Table,
- Tag,
- Text,
- toast,
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+ Button,
+ Icon,
+ Spinner,
+ Table,
+ Tag,
+ Text,
+ toast,
} from 'opub-ui';
+import { GraphQL } from '@/lib/api';
import { Icons } from '@/components/icons';
import { RichTextRenderer } from '@/components/RichTextRenderer';
-import { GraphQL } from '@/lib/api';
import { useEditStatus } from '../../context';
const FetchAIModelForPublish: any = graphql(`
@@ -173,6 +173,8 @@ export default function PublishPage() {
const versions = model?.versions || [];
const primaryVersion = versions.find((v: any) => v.isLatest) || versions[0];
const hasProviders = versions.some((v: any) => v.providers?.length > 0);
+ const PUBLISH_SUCCESS_TOAST_ID = 'publish-ai-model-success';
+ const PUBLISH_ERROR_TOAST_ID = 'publish-ai-model-error';
const { mutate, isLoading: updateLoading } = useMutation(
(mutationData: any) =>
@@ -188,17 +190,7 @@ export default function PublishPage() {
},
}
),
- {
- onSuccess: () => {
- toast('Model status updated successfully');
- setStatus('saved');
- refetch();
- },
- onError: (error: any) => {
- toast(`Error: ${error.message}`);
- setStatus('unsaved');
- },
- }
+ {}
);
const handlePublish = () => {
@@ -211,8 +203,22 @@ export default function PublishPage() {
},
{
onSuccess: () => {
- toast('Model published successfully');
- router.push(`/dashboard/${params.entityType}/${params.entitySlug}/aimodels`);
+ toast('Model published successfully', {
+ id: PUBLISH_SUCCESS_TOAST_ID,
+ });
+ setStatus('saved');
+ refetch();
+ router.push(
+ `/dashboard/${params.entityType}/${params.entitySlug}/aimodels`
+ );
+ },
+ onError: (error: any) => {
+ const errorMessage =
+ typeof error?.message === 'string' && error.message.trim()
+ ? error.message.trim()
+ : 'Unable to publish model right now. Please try again.';
+ toast(`Error: ${errorMessage}`, { id: PUBLISH_ERROR_TOAST_ID });
+ setStatus('unsaved');
},
}
);
@@ -224,14 +230,15 @@ export default function PublishPage() {
if (!model?.tags?.length) metadataErrors.push('Tags');
if (!model?.sectors?.length) metadataErrors.push('Sectors');
if (!model?.geographies?.length) metadataErrors.push('Geographies');
-
+
// Check required fields from metadata
const metadata = model?.metadata || {};
if (!metadata.targetUsers) metadataErrors.push('Target Users');
if (!metadata.intendedUse) metadataErrors.push('Intended Use');
if (!metadata.modelWebsite) metadataErrors.push('Model Website');
if (!model?.maxTokens) metadataErrors.push('Maximum Tokens');
- if (!model?.supportedLanguages?.length) metadataErrors.push('Supported Languages');
+ if (!model?.supportedLanguages?.length)
+ metadataErrors.push('Supported Languages');
if (!model?.modelType) metadataErrors.push('Model Type');
const versionErrors = [];
@@ -271,7 +278,9 @@ export default function PublishPage() {
version: v.version,
lifecycleStage: lifecycleLabels[v.lifecycleStage] || v.lifecycleStage,
providers: v.providers?.length
- ? v.providers.map((p: any) => providerLabels[p.provider] || p.provider).join(', ')
+ ? v.providers
+ .map((p: any) => providerLabels[p.provider] || p.provider)
+ .join(', ')
: 'None',
primary: v.isLatest ? 'Yes' : 'No',
}));
@@ -288,7 +297,7 @@ export default function PublishPage() {
},
{
label: 'Domain',
- value: model?.domain ? (domainLabels[model.domain] || model.domain) : '',
+ value: model?.domain ? domainLabels[model.domain] || model.domain : '',
},
{
label: 'Target Users',
@@ -308,7 +317,9 @@ export default function PublishPage() {
},
{
label: 'Supported Languages',
- value: model?.supportedLanguages?.length ? model.supportedLanguages.join(', ') : '',
+ value: model?.supportedLanguages?.length
+ ? model.supportedLanguages.join(', ')
+ : '',
},
];
@@ -391,10 +402,7 @@ export default function PublishPage() {
{model?.description && (
-
+
Description:
@@ -411,11 +419,9 @@ export default function PublishPage() {
{model?.sectors?.length > 0 ? (
- model.sectors.map(
- (s: any, idx: number) => (
-
{s.name}
- )
- )
+ model.sectors.map((s: any, idx: number) => (
+
{s.name}
+ ))
) : (
None
@@ -430,11 +436,9 @@ export default function PublishPage() {
{model?.tags?.length > 0 ? (
- model.tags.map(
- (t: any, idx: number) => (
-
{t.value}
- )
- )
+ model.tags.map((t: any, idx: number) => (
+
{t.value}
+ ))
) : (
None
@@ -472,7 +476,11 @@ export default function PublishPage() {
hideFooter
/>
) : (
-
+
No versions found
)}
@@ -486,27 +494,27 @@ export default function PublishPage() {
{/* Publication Status */}
{isPublished ? (
-
+
Model is Published and Active
-
+
Your AI model is now publicly accessible and can be
discovered by other users.
) : (
-
+
Model is not published
-
+
{!isPublishDisabled
? 'All checklist items are complete. You can now publish your model.'
: 'Complete all required fields before publishing your model.'}
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Details.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Details.tsx
index d97de6ac..24784999 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Details.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Details.tsx
@@ -4,6 +4,7 @@ import Link from 'next/link';
import { Text } from 'opub-ui';
import { getWebsiteTitle } from '@/lib/utils';
+import { RichTextRenderer } from '@/components/RichTextRenderer';
const Details = ({ data }: { data: any }) => {
const [platformTitle, setPlatformTitle] = useState(null);
@@ -60,7 +61,14 @@ const Details = ({ data }: { data: any }) => {
{item.label}:
- {item.value}
+ {item.label === 'Summary' ? (
+
+ ) : (
+ {item.value}
+ )}
)
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/page.tsx
index 9b5bffe2..366b8bf6 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/page.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/page.tsx
@@ -148,6 +148,8 @@ const Publish = () => {
}
);
const router = useRouter();
+ const PUBLISH_SUCCESS_TOAST_ID = 'collaborative-publish-success';
+ const PUBLISH_ERROR_TOAST_ID = 'collaborative-publish-error';
const { mutate, isLoading: mutationLoading } = useMutation(
() => GraphQL(publishCollaborativeMutation, {
@@ -155,13 +157,19 @@ const Publish = () => {
}, { collaborativeId: params.id }),
{
onSuccess: (data: any) => {
- toast('Collaborative Published Successfully');
+ toast('Collaborative Published Successfully', {
+ id: PUBLISH_SUCCESS_TOAST_ID,
+ });
router.push(
`/dashboard/${params.entityType}/${params.entitySlug}/collaboratives`
);
},
onError: (err: any) => {
- toast(`Received ${err} on dataset publish `);
+ const errorMessage =
+ typeof err?.message === 'string' && err.message.trim()
+ ? err.message.trim()
+ : 'Unable to publish collaborative right now. Please try again.';
+ toast(`Error: ${errorMessage}`, { id: PUBLISH_ERROR_TOAST_ID });
},
}
);
@@ -289,6 +297,7 @@ const Publish = () => {
className="m-auto w-fit"
onClick={() => mutate()}
disabled={isPublishDisabled(CollaborativeData?.data?.collaboratives[0])}
+ loading={mutationLoading}
>
Publish
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/components/StepNavigation.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/components/StepNavigation.tsx
index 18c1f9bc..9e8e663d 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/components/StepNavigation.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/components/StepNavigation.tsx
@@ -1,15 +1,46 @@
import { usePathname, useRouter } from 'next/navigation';
import { Button, Icon, Text } from 'opub-ui';
+import { useEffect, useState } from 'react';
import { Icons } from '@/components/icons';
interface StepNavigationProps {
steps: string[]; // Array of steps (e.g., ['metadata', 'details', 'publish'])
+ onBeforeNavigate?: () => Promise
| void;
}
-const StepNavigation = ({ steps }: StepNavigationProps) => {
+const StepNavigation = ({ steps, onBeforeNavigate }: StepNavigationProps) => {
const pathname = usePathname(); // Get the current URL path
const router = useRouter();
+ const [isNavigating, setIsNavigating] = useState(false);
+
+ useEffect(() => {
+ // Route has changed (or initial mount), re-enable navigation controls.
+ setIsNavigating(false);
+ }, [pathname]);
+
+ const navigateWithBlur = async (newPath: string) => {
+ setIsNavigating(true);
+
+ const activeElement = document.activeElement as HTMLElement | null;
+ if (activeElement && typeof activeElement.blur === 'function') {
+ activeElement.blur();
+ }
+
+ if (onBeforeNavigate) {
+ try {
+ await onBeforeNavigate();
+ } catch {
+ // Preserve current behavior: navigation should still continue.
+ }
+ }
+
+ try {
+ router.push(newPath);
+ } catch {
+ setIsNavigating(false);
+ }
+ };
// Find the current step's index based on the pathname (without query params)
const currentIndex = steps.findIndex((step) =>
@@ -26,7 +57,7 @@ const StepNavigation = ({ steps }: StepNavigationProps) => {
steps[currentIndex],
steps[currentIndex - 1]
);
- router.push(newPath); // Update the URL to the previous step
+ void navigateWithBlur(newPath); // Update the URL to the previous step
}
};
@@ -36,7 +67,7 @@ const StepNavigation = ({ steps }: StepNavigationProps) => {
steps[currentIndex],
steps[currentIndex + 1]
);
- router.push(newPath); // Update the URL to the next step
+ void navigateWithBlur(newPath); // Update the URL to the next step
}
};
@@ -44,7 +75,7 @@ const StepNavigation = ({ steps }: StepNavigationProps) => {
@@ -63,7 +94,7 @@ const StepNavigation = ({ steps }: StepNavigationProps) => {
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelForm.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelForm.tsx
index dbff118a..fb3ffb35 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelForm.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelForm.tsx
@@ -95,6 +95,15 @@ const AccessModelForm: React.FC = ({
setAccessModelId,
accessModelId,
}) => {
+ const ACCESS_MODEL_SAVE_ERROR_TOAST_ID = 'dataset-access-model-save-error';
+ const getErrorMessage = (
+ err: any,
+ fallback: string
+ ) =>
+ typeof err?.message === 'string' && err.message.trim()
+ ? err.message.trim()
+ : fallback;
+
useEffect(() => {
setList(false);
}, []);
@@ -326,7 +335,10 @@ const AccessModelForm: React.FC = ({
setPreviousAccessModelData(accessModelData);
},
onError: (err: any) => {
- toast(`Received ${err} during access model saving`);
+ toast(
+ `Error: ${getErrorMessage(err, 'Unable to save access model right now.')}`,
+ { id: ACCESS_MODEL_SAVE_ERROR_TOAST_ID }
+ );
},
}
);
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelList.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelList.tsx
index 3fe19f0a..dca7ebdd 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelList.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelList.tsx
@@ -48,6 +48,15 @@ const AccessModelList: React.FC = ({
list,
setAccessModelId,
}) => {
+ const ACCESS_MODEL_DELETE_ERROR_TOAST_ID = 'dataset-access-model-delete-error';
+ const getErrorMessage = (
+ err: any,
+ fallback: string
+ ) =>
+ typeof err?.message === 'string' && err.message.trim()
+ ? err.message.trim()
+ : fallback;
+
const params = useParams<{
entityType: string;
entitySlug: string;
@@ -96,7 +105,10 @@ const AccessModelList: React.FC = ({
refetch();
},
onError: (err: any) => {
- toast(`Received ${err} while deleting Access model `);
+ toast(
+ `Error: ${getErrorMessage(err, 'Unable to delete access model right now.')}`,
+ { id: ACCESS_MODEL_DELETE_ERROR_TOAST_ID }
+ );
},
}
);
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditLayout.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditLayout.tsx
index 1041d216..a7a8c891 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditLayout.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditLayout.tsx
@@ -50,6 +50,15 @@ interface LayoutProps {
const layoutList = ['metadata', 'resources', 'publish'];
export function EditLayout({ children, params }: LayoutProps) {
+ const DATASET_TITLE_SAVE_ERROR_TOAST_ID = 'dataset-title-save-error';
+ const getErrorMessage = (
+ err: any,
+ fallback: string
+ ) =>
+ typeof err?.message === 'string' && err.message.trim()
+ ? err.message.trim()
+ : fallback;
+
// const { data } = useQuery([`dataset_layout_${params.id}`], () =>
// GraphQL(datasetQueryDoc, { dataset_id: Number(params.id) })
// );
@@ -98,7 +107,9 @@ export function EditLayout({ children, params }: LayoutProps) {
getDatasetTitleRes.refetch();
},
onError: (err: any) => {
- toast(err.message.split(':')[0]);
+ toast(getErrorMessage(err, 'Unable to update dataset title right now.'), {
+ id: DATASET_TITLE_SAVE_ERROR_TOAST_ID,
+ });
},
}
);
@@ -107,7 +118,7 @@ export function EditLayout({ children, params }: LayoutProps) {
return pathName.indexOf(v) >= 0;
});
- const { status, setStatus } = useDatasetEditStatus();
+ const { status, setStatus, runBeforeNavigateHandler } = useDatasetEditStatus();
// if not from the layoutList, return children
if (!pathItem) {
@@ -150,7 +161,10 @@ export function EditLayout({ children, params }: LayoutProps) {
{children}
-
+
@@ -214,11 +228,12 @@ const Navigation = ({
const handleTabClick = (item: {
label: string;
+ id: string;
url: string;
// selected: boolean;
}) => {
- if (item.label !== selectedTab) {
- setSelectedTab(item.label);
+ if (item.id !== selectedTab) {
+ setSelectedTab(item.id);
router.replace(item.url);
}
};
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditMetadata.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditMetadata.tsx
index e84660df..680f8615 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditMetadata.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditMetadata.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { useParams } from 'next/navigation';
import { graphql } from '@/gql';
import {
@@ -238,6 +238,17 @@ export function EditMetadata({ id }: { id: string }) {
}>();
const queryClient = useQueryClient();
+ const PROMPT_METADATA_SUCCESS_TOAST_ID = 'dataset-prompt-metadata-success';
+ const PROMPT_METADATA_ERROR_TOAST_ID = 'dataset-prompt-metadata-error';
+ const DATASET_METADATA_SUCCESS_TOAST_ID = 'dataset-metadata-save-success';
+ const DATASET_METADATA_ERROR_TOAST_ID = 'dataset-metadata-save-error';
+ const getErrorMessage = (
+ err: any,
+ fallback: string
+ ) =>
+ typeof err?.message === 'string' && err.message.trim()
+ ? err.message.trim()
+ : fallback;
const getDatasetMetadata: {
data: any;
@@ -380,22 +391,28 @@ export function EditMetadata({ id }: { id: string }) {
{
onSuccess: (res: any) => {
if (res.updatePromptMetadata.success) {
- toast('Prompt metadata updated successfully!');
+ toast('Prompt metadata updated successfully!', {
+ id: PROMPT_METADATA_SUCCESS_TOAST_ID,
+ });
queryClient.invalidateQueries({
queryKey: [`metadata_values_query_${params.id}`],
});
} else {
+ const responseError =
+ res.updatePromptMetadata?.errors?.fieldErrors?.[0]?.messages?.[0] ||
+ res.updatePromptMetadata?.errors?.nonFieldErrors?.[0] ||
+ 'Unable to update prompt metadata right now. Please try again.';
toast(
- 'Error: ' +
- (res.updatePromptMetadata?.errors?.fieldErrors
- ? res.updatePromptMetadata?.errors?.fieldErrors[0]?.messages[0]
- : res.updatePromptMetadata?.errors?.nonFieldErrors?.[0] ||
- 'Unknown error')
+ `Error: ${responseError}`,
+ { id: PROMPT_METADATA_ERROR_TOAST_ID }
);
}
},
onError: (err: any) => {
- toast('Error: ' + err.message);
+ toast(
+ `Error: ${getErrorMessage(err, 'Unable to update prompt metadata right now. Please try again.')}`,
+ { id: PROMPT_METADATA_ERROR_TOAST_ID }
+ );
},
}
);
@@ -428,12 +445,14 @@ export function EditMetadata({ id }: { id: string }) {
{
onSuccess: (res: any) => {
if (res.addUpdateDatasetMetadata.success) {
- toast('Details updated successfully!');
+ toast('Details updated successfully!', {
+ id: DATASET_METADATA_SUCCESS_TOAST_ID,
+ });
queryClient.invalidateQueries({
- queryKey: [
- `metadata_values_query_${params.id}`,
- `metadata_fields_list_${id}`,
- ],
+ queryKey: [`metadata_values_query_${params.id}`],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [`metadata_fields_list_${id}`],
});
const updatedData = defaultValuesPrepFn(
res.addUpdateDatasetMetadata.data
@@ -445,15 +464,22 @@ export function EditMetadata({ id }: { id: string }) {
setFormData(updatedData);
setPreviousFormData(updatedData);
} else {
+ const responseError =
+ res.addUpdateDatasetMetadata?.errors?.fieldErrors?.[0]?.messages?.[0] ||
+ res.addUpdateDatasetMetadata?.errors?.nonFieldErrors?.[0] ||
+ 'Unable to update details right now. Please try again.';
toast(
- 'Error: ' +
- (res.addUpdateDatasetMetadata?.errors?.fieldErrors
- ? res.addUpdateDatasetMetadata?.errors?.fieldErrors[0]
- ?.messages[0]
- : res.addUpdateDatasetMetadata?.errors?.nonFieldErrors[0])
+ `Error: ${responseError}`,
+ { id: DATASET_METADATA_ERROR_TOAST_ID }
);
}
},
+ onError: (err: any) => {
+ toast(
+ `Error: ${getErrorMessage(err, 'Unable to update details right now. Please try again.')}`,
+ { id: DATASET_METADATA_ERROR_TOAST_ID }
+ );
+ },
}
);
@@ -532,6 +558,7 @@ export function EditMetadata({ id }: { id: string }) {
)
);
const [previousFormData, setPreviousFormData] = useState(formData);
+ const formDataRef = useRef(formData);
useEffect(() => {
if (getDatasetMetadata.data?.datasets[0]) {
@@ -539,18 +566,27 @@ export function EditMetadata({ id }: { id: string }) {
getDatasetMetadata.data.datasets[0]
);
setFormData(updatedData);
+ formDataRef.current = updatedData;
setPreviousFormData(updatedData);
}
}, [getDatasetMetadata.data]);
const handleChange = (field: string, value: any) => {
- setFormData((prevData) => ({
- ...prevData,
+ formDataRef.current = {
+ ...formDataRef.current,
[field]: value,
- }));
+ };
+
+ setFormData((prevData) => {
+ const nextData = {
+ ...prevData,
+ [field]: value,
+ };
+ return nextData;
+ });
};
- const handleSave = (updatedData: any) => {
+ const getUpdateInput = (updatedData: any): UpdateMetadataInput | null => {
const changedFields: any = {};
for (const key in updatedData) {
@@ -574,10 +610,7 @@ export function EditMetadata({ id }: { id: string }) {
}
}
- // Exit early if nothing changed
- if (Object.keys(changedFields).length === 0) return;
-
- setPreviousFormData(updatedData); // Update local copy
+ if (Object.keys(changedFields).length === 0) return null;
const transformedValues = Object.keys(changedFields).reduce(
(acc: any, key) => {
@@ -591,47 +624,71 @@ export function EditMetadata({ id }: { id: string }) {
{}
);
- updateMetadataMutation.mutate({
- UpdateMetadataInput: {
- dataset: id,
- metadata: Object.keys(transformedValues)
- .filter(
- (key) =>
- ![
- 'sectors',
- 'description',
- 'tags',
- 'geographies',
- 'isPublic',
- 'license',
- ].includes(key) && transformedValues[key] !== ''
- )
- .map((key) => ({
- id: key,
- value: transformedValues[key],
- })),
- ...(changedFields.license && { license: changedFields.license }),
- ...(changedFields.accessType && {
- accessType: changedFields.accessType,
- }),
- ...(changedFields.description !== undefined && {
- description: changedFields.description,
- }),
- ...(changedFields.tags && {
- tags: changedFields.tags.map((item: any) => item.label),
- }),
- ...(changedFields.sectors && {
- sectors: changedFields.sectors.map((item: any) => item.value),
- }),
- ...(changedFields.geographies && {
- geographies: changedFields.geographies.map((item: any) =>
- parseInt(item.value, 10)
- ),
- }),
- },
+ return {
+ dataset: id,
+ metadata: Object.keys(transformedValues)
+ .filter(
+ (key) =>
+ ![
+ 'sectors',
+ 'description',
+ 'tags',
+ 'geographies',
+ 'isPublic',
+ 'license',
+ ].includes(key) && transformedValues[key] !== ''
+ )
+ .map((key) => ({
+ id: key,
+ value: transformedValues[key],
+ })),
+ ...(changedFields.license && { license: changedFields.license }),
+ ...(changedFields.accessType && {
+ accessType: changedFields.accessType,
+ }),
+ ...(changedFields.description !== undefined && {
+ description: changedFields.description,
+ }),
+ ...(changedFields.tags && {
+ tags: changedFields.tags.map((item: any) => item.label),
+ }),
+ ...(changedFields.sectors && {
+ sectors: changedFields.sectors.map((item: any) => item.value),
+ }),
+ ...(changedFields.geographies && {
+ geographies: changedFields.geographies.map((item: any) =>
+ parseInt(item.value, 10)
+ ),
+ }),
+ };
+ };
+
+ const handleSave = (updatedData: any) => {
+ const updateInput = getUpdateInput(updatedData);
+ if (!updateInput) return;
+
+ updateMetadataMutation.mutate({ UpdateMetadataInput: updateInput });
+ };
+
+ const handleSaveAsync = async (updatedData: any) => {
+ const updateInput = getUpdateInput(updatedData);
+ if (!updateInput) return;
+
+ await updateMetadataMutation.mutateAsync({
+ UpdateMetadataInput: updateInput,
});
};
+ const { setStatus, registerBeforeNavigateHandler } = useDatasetEditStatus();
+
+ useEffect(() => {
+ registerBeforeNavigateHandler(() => handleSaveAsync(formDataRef.current));
+
+ return () => {
+ registerBeforeNavigateHandler(null);
+ };
+ }, [previousFormData, registerBeforeNavigateHandler]);
+
function renderInputField(metadataFormItem: any) {
if (metadataFormItem.dataType === 'STRING') {
return (
@@ -755,8 +812,6 @@ export function EditMetadata({ id }: { id: string }) {
},
];
- const { setStatus } = useDatasetEditStatus();
-
useEffect(() => {
setStatus(updateMetadataMutation.isLoading ? 'loading' : 'success'); // update based on mutation state
}, [updateMetadataMutation.isLoading]);
@@ -784,7 +839,9 @@ export function EditMetadata({ id }: { id: string }) {
label="Description *"
value={formData.description}
onChange={(value) => handleChange('description', value)}
- onBlur={() => handleSave(formData)}
+ onBlur={(value) =>
+ handleSave({ ...formData, description: value })
+ }
placeholder="Enter dataset description..."
helpText={`Character limit: ${formData?.description?.length || 0}/10000`}
/>
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/context.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/context.tsx
index 540b546c..b048b420 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/context.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/context.tsx
@@ -1,18 +1,37 @@
'use client';
-import { createContext, useContext, useState } from 'react';
+import { createContext, useContext, useRef, useState } from 'react';
type StatusType = 'loading' | 'success';
+type BeforeNavigateHandler = (() => Promise | void) | null;
const DatasetEditStatusContext = createContext<{
status: StatusType;
setStatus: (status: StatusType) => void;
+ registerBeforeNavigateHandler: (handler: BeforeNavigateHandler) => void;
+ runBeforeNavigateHandler: () => Promise;
} | null>(null);
export const DatasetEditStatusProvider = ({ children }: { children: React.ReactNode }) => {
const [status, setStatus] = useState('success');
+ const beforeNavigateHandlerRef = useRef(null);
+
+ const registerBeforeNavigateHandler = (handler: BeforeNavigateHandler) => {
+ beforeNavigateHandlerRef.current = handler;
+ };
+
+ const runBeforeNavigateHandler = async () => {
+ await beforeNavigateHandlerRef.current?.();
+ };
return (
-
+
{children}
);
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/publish/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/publish/page.tsx
index 0f90565a..c9c39733 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/publish/page.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/publish/page.tsx
@@ -262,6 +262,8 @@ const Page = () => {
},
];
const router = useRouter();
+ const PUBLISH_SUCCESS_TOAST_ID = 'dataset-publish-success';
+ const PUBLISH_ERROR_TOAST_ID = 'dataset-publish-error';
const { mutate, isLoading: mutationLoading } = useMutation(
() =>
@@ -274,13 +276,17 @@ const Page = () => {
),
{
onSuccess: (data: any) => {
- toast('Dataset Published Successfully');
+ toast('Dataset Published Successfully', { id: PUBLISH_SUCCESS_TOAST_ID });
router.push(
`/dashboard/${params.entityType}/${params.entitySlug}/dataset`
);
},
onError: (err: any) => {
- toast(`Received ${err} on dataset publish `);
+ const errorMessage =
+ typeof err?.message === 'string' && err.message.trim()
+ ? err.message.trim()
+ : 'Unable to publish dataset right now. Please try again.';
+ toast(`Error: ${errorMessage}`, { id: PUBLISH_ERROR_TOAST_ID });
},
}
);
@@ -528,6 +534,7 @@ const Page = () => {
getDatasetsSummary.data?.datasets[0]
)}
onClick={() => mutate()}
+ loading={mutationLoading}
>
Publish
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/resources/components/EditResource.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/resources/components/EditResource.tsx
index dcd1070f..1c392fe2 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/resources/components/EditResource.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/resources/components/EditResource.tsx
@@ -208,6 +208,18 @@ export const EditResource = ({
allResources,
isPromptDataset = false,
}: EditProps) => {
+ const UPDATE_RESOURCE_ERROR_TOAST_ID = 'dataset-resource-update-error';
+ const UPDATE_SCHEMA_ERROR_TOAST_ID = 'dataset-schema-update-error';
+ const CREATE_RESOURCE_ERROR_TOAST_ID = 'dataset-resource-create-error';
+ const PROMPT_RESOURCE_ERROR_TOAST_ID = 'dataset-prompt-resource-error';
+ const getErrorMessage = (
+ err: any,
+ fallback: string
+ ) =>
+ typeof err?.message === 'string' && err.message.trim()
+ ? err.message.trim()
+ : fallback;
+
const params = useParams<{
entityType: string;
entitySlug: string;
@@ -268,7 +280,9 @@ export const EditResource = ({
resourceDetailsQuery.refetch();
},
onError: (err: any) => {
- toast(err.message || String(err));
+ toast(getErrorMessage(err, 'Unable to save file changes right now.'), {
+ id: UPDATE_RESOURCE_ERROR_TOAST_ID,
+ });
setFile([]);
},
}
@@ -293,7 +307,9 @@ export const EditResource = ({
});
},
onError: (err: any) => {
- toast('Error ::: ', err);
+ toast(`Error: ${getErrorMessage(err, 'Unable to update schema right now.')}`, {
+ id: UPDATE_SCHEMA_ERROR_TOAST_ID,
+ });
},
}
);
@@ -320,7 +336,8 @@ export const EditResource = ({
resourceDetailsQuery.refetch();
},
onError: (err: any) => {
- toast(err.message, {
+ toast(getErrorMessage(err, 'Unable to add resource right now.'), {
+ id: CREATE_RESOURCE_ERROR_TOAST_ID,
action: {
label: 'Dismiss',
onClick: () => {},
@@ -397,7 +414,10 @@ export const EditResource = ({
}
},
onError: (err: any) => {
- toast('Error: ' + err.message);
+ toast(
+ `Error: ${getErrorMessage(err, 'Unable to update prompt metadata right now.')}`,
+ { id: PROMPT_RESOURCE_ERROR_TOAST_ID }
+ );
},
}
);
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/resources/components/ResourceDropzone.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/resources/components/ResourceDropzone.tsx
index 9d3a9a3a..64be1572 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/resources/components/ResourceDropzone.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/resources/components/ResourceDropzone.tsx
@@ -10,6 +10,14 @@ import { createResourceFilesDoc } from './query';
export const ResourceDropzone = ({ reload }: { reload: () => void }) => {
const fileTypes = ['CSV', 'JSON', 'PDF', 'XLS', 'XLSX', 'XML', 'ZIP'];
+ const RESOURCE_UPLOAD_ERROR_TOAST_ID = 'dataset-resource-upload-error';
+ const getErrorMessage = (
+ err: any,
+ fallback: string
+ ) =>
+ typeof err?.message === 'string' && err.message.trim()
+ ? err.message.trim()
+ : fallback;
const params = useParams<{
entityType: string;
entitySlug: string;
@@ -34,7 +42,9 @@ export const ResourceDropzone = ({ reload }: { reload: () => void }) => {
setResourceId(data.createFileResources[0].id);
},
onError: (err: any) => {
- toast(err.message);
+ toast(getErrorMessage(err, 'Unable to upload resource right now.'), {
+ id: RESOURCE_UPLOAD_ERROR_TOAST_ID,
+ });
setFile([]);
},
}
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/resources/components/ResourceListView.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/resources/components/ResourceListView.tsx
index 0732e4ba..b95a7227 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/resources/components/ResourceListView.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/resources/components/ResourceListView.tsx
@@ -36,6 +36,15 @@ type ResourceListProps = {
export const ResourceListView = ({ data, refetch, isPromptDataset = false }: ResourceListProps) => {
const fileLabel = isPromptDataset ? 'Prompt Files' : 'Data Files';
const fileButtonLabel = isPromptDataset ? 'ADD NEW PROMPT FILE' : 'ADD NEW DATA FILE';
+ const RESOURCE_DELETE_ERROR_TOAST_ID = 'dataset-resource-delete-error';
+ const RESOURCE_ADD_ERROR_TOAST_ID = 'dataset-resource-add-error';
+ const getErrorMessage = (
+ err: any,
+ fallback: string
+ ) =>
+ typeof err?.message === 'string' && err.message.trim()
+ ? err.message.trim()
+ : fallback;
const [resourceId, setResourceId] = useQueryState('id', parseAsString);
const [file, setFile] = React.useState([]);
@@ -74,7 +83,9 @@ export const ResourceListView = ({ data, refetch, isPromptDataset = false }: Res
});
},
onError: (err: any) => {
- toast(err);
+ toast(getErrorMessage(err, 'Unable to delete resource right now.'), {
+ id: RESOURCE_DELETE_ERROR_TOAST_ID,
+ });
},
}
);
@@ -116,7 +127,8 @@ export const ResourceListView = ({ data, refetch, isPromptDataset = false }: Res
);
},
onError: (err: any) => {
- toast(err.message, {
+ toast(getErrorMessage(err, 'Unable to add resource right now.'), {
+ id: RESOURCE_ADD_ERROR_TOAST_ID,
action: {
label: 'Dismiss',
onClick: () => {},
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/assign/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/assign/page.tsx
index 964168d7..5f47c842 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/assign/page.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/assign/page.tsx
@@ -47,6 +47,12 @@ const Assign = () => {
entitySlug: string;
id: string;
}>();
+ const USECASE_ASSIGN_SUCCESS_TOAST_ID = 'usecase-assign-datasets-success';
+ const USECASE_ASSIGN_ERROR_TOAST_ID = 'usecase-assign-datasets-error';
+ const getErrorMessage = (error: any, fallback: string) =>
+ typeof error?.message === 'string' && error.message.trim()
+ ? error.message.trim()
+ : fallback;
const router = useRouter();
const [data, setData] = useState([]); // Ensure `data` is an array
@@ -126,14 +132,19 @@ const Assign = () => {
),
{
onSuccess: () => {
- toast('Dataset Assigned Successfully');
+ toast('Dataset Assigned Successfully', {
+ id: USECASE_ASSIGN_SUCCESS_TOAST_ID,
+ });
UseCaseDetails.refetch();
router.push(
`/dashboard/${params.entityType}/${params.entitySlug}/usecases/edit/${params.id}/dashboards`
);
},
onError: (err: any) => {
- toast(`Received ${err} on dataset publish `);
+ toast(
+ `Error: ${getErrorMessage(err, 'Unable to assign datasets right now. Please try again.')}`,
+ { id: USECASE_ASSIGN_ERROR_TOAST_ID }
+ );
},
}
);
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/page.tsx
index 7b58c8f9..f9bcf5a8 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/page.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/page.tsx
@@ -26,6 +26,22 @@ import {
const Details = () => {
const params = useParams<{ entityType: string; entitySlug: string; id: string }>();
+ const CONTRIBUTORS_ADD_SUCCESS_TOAST_ID = 'usecase-contributor-add-success';
+ const CONTRIBUTORS_ADD_ERROR_TOAST_ID = 'usecase-contributor-add-error';
+ const CONTRIBUTORS_REMOVE_SUCCESS_TOAST_ID = 'usecase-contributor-remove-success';
+ const CONTRIBUTORS_REMOVE_ERROR_TOAST_ID = 'usecase-contributor-remove-error';
+ const SUPPORTER_ADD_SUCCESS_TOAST_ID = 'usecase-supporter-add-success';
+ const SUPPORTER_ADD_ERROR_TOAST_ID = 'usecase-supporter-add-error';
+ const SUPPORTER_REMOVE_SUCCESS_TOAST_ID = 'usecase-supporter-remove-success';
+ const SUPPORTER_REMOVE_ERROR_TOAST_ID = 'usecase-supporter-remove-error';
+ const PARTNER_ADD_SUCCESS_TOAST_ID = 'usecase-partner-add-success';
+ const PARTNER_ADD_ERROR_TOAST_ID = 'usecase-partner-add-error';
+ const PARTNER_REMOVE_SUCCESS_TOAST_ID = 'usecase-partner-remove-success';
+ const PARTNER_REMOVE_ERROR_TOAST_ID = 'usecase-partner-remove-error';
+ const getErrorMessage = (error: any, fallback: string) =>
+ typeof error?.message === 'string' && error.message.trim()
+ ? error.message.trim()
+ : fallback;
const [searchValue, setSearchValue] = useState('');
const [formData, setFormData] = useState({
contributors: [] as { label: string; value: string }[],
@@ -111,11 +127,16 @@ const Details = () => {
}, input),
{
onSuccess: () => {
- toast('Contributor added successfully');
+ toast('Contributor added successfully', {
+ id: CONTRIBUTORS_ADD_SUCCESS_TOAST_ID,
+ });
UseCaseData.refetch();
},
onError: (error: any) => {
- toast(`Error: ${error.message}`);
+ toast(
+ `Error: ${getErrorMessage(error, 'Unable to add contributor right now. Please try again.')}`,
+ { id: CONTRIBUTORS_ADD_ERROR_TOAST_ID }
+ );
},
}
);
@@ -128,10 +149,15 @@ const Details = () => {
}, input),
{
onSuccess: () => {
- toast('Contributor removed successfully');
+ toast('Contributor removed successfully', {
+ id: CONTRIBUTORS_REMOVE_SUCCESS_TOAST_ID,
+ });
},
onError: (error: any) => {
- toast(`Error: ${error.message}`);
+ toast(
+ `Error: ${getErrorMessage(error, 'Unable to remove contributor right now. Please try again.')}`,
+ { id: CONTRIBUTORS_REMOVE_ERROR_TOAST_ID }
+ );
},
}
);
@@ -143,11 +169,16 @@ const Details = () => {
}, input),
{
onSuccess: () => {
- toast('Supporter added successfully');
+ toast('Supporter added successfully', {
+ id: SUPPORTER_ADD_SUCCESS_TOAST_ID,
+ });
UseCaseData.refetch();
},
onError: (error: any) => {
- toast(`Error: ${error.message}`);
+ toast(
+ `Error: ${getErrorMessage(error, 'Unable to add supporter right now. Please try again.')}`,
+ { id: SUPPORTER_ADD_ERROR_TOAST_ID }
+ );
},
}
);
@@ -160,10 +191,15 @@ const Details = () => {
}, input),
{
onSuccess: () => {
- toast('Supporter removed successfully');
+ toast('Supporter removed successfully', {
+ id: SUPPORTER_REMOVE_SUCCESS_TOAST_ID,
+ });
},
onError: (error: any) => {
- toast(`Error: ${error.message}`);
+ toast(
+ `Error: ${getErrorMessage(error, 'Unable to remove supporter right now. Please try again.')}`,
+ { id: SUPPORTER_REMOVE_ERROR_TOAST_ID }
+ );
},
}
);
@@ -175,11 +211,14 @@ const Details = () => {
}, input),
{
onSuccess: () => {
- toast('Partner added successfully');
+ toast('Partner added successfully', { id: PARTNER_ADD_SUCCESS_TOAST_ID });
UseCaseData.refetch();
},
onError: (error: any) => {
- toast(`Error: ${error.message}`);
+ toast(
+ `Error: ${getErrorMessage(error, 'Unable to add partner right now. Please try again.')}`,
+ { id: PARTNER_ADD_ERROR_TOAST_ID }
+ );
},
}
);
@@ -192,10 +231,15 @@ const Details = () => {
}, input),
{
onSuccess: () => {
- toast('Partner removed successfully');
+ toast('Partner removed successfully', {
+ id: PARTNER_REMOVE_SUCCESS_TOAST_ID,
+ });
},
onError: (error: any) => {
- toast(`Error: ${error.message}`);
+ toast(
+ `Error: ${getErrorMessage(error, 'Unable to remove partner right now. Please try again.')}`,
+ { id: PARTNER_REMOVE_ERROR_TOAST_ID }
+ );
},
}
);
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/dashboards/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/dashboards/page.tsx
index 6105375a..d733559c 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/dashboards/page.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/dashboards/page.tsx
@@ -76,6 +76,15 @@ const deleteDashboard: any = graphql(`
const Dashboard = () => {
const params = useParams<{ entityType?: string; entitySlug?: string; id?: string }>();
+ const DASHBOARD_ADD_SUCCESS_TOAST_ID = 'usecase-dashboard-add-success';
+ const DASHBOARD_SAVE_SUCCESS_TOAST_ID = 'usecase-dashboard-save-success';
+ const DASHBOARD_DELETE_SUCCESS_TOAST_ID = 'usecase-dashboard-delete-success';
+ const DASHBOARD_SAVE_ERROR_TOAST_ID = 'usecase-dashboard-save-error';
+ const DASHBOARD_DELETE_ERROR_TOAST_ID = 'usecase-dashboard-delete-error';
+ const getErrorMessage = (error: any, fallback: string) =>
+ typeof error?.message === 'string' && error.message.trim()
+ ? error.message.trim()
+ : fallback;
const entityType = params?.entityType;
const entitySlug = params?.entitySlug;
const idParam = params?.id;
@@ -130,7 +139,7 @@ const Dashboard = () => {
...prev,
[newDashboard.id]: { ...newDashboard },
}));
- toast.success('Dashboard added');
+ toast.success('Dashboard added', { id: DASHBOARD_ADD_SUCCESS_TOAST_ID });
},
}
);
@@ -139,14 +148,17 @@ const Dashboard = () => {
GraphQL(updateDashboard, ownerArgs || {}, { id, name, link }),
{
onSuccess: ({ updateUsecaseDashboard }: any) => {
- toast.success('Changes saved');
+ toast.success('Changes saved', { id: DASHBOARD_SAVE_SUCCESS_TOAST_ID });
setPreviousState((prev: any) => ({
...prev,
[updateUsecaseDashboard.data.id]: { ...updateUsecaseDashboard.data },
}));
},
onError: (error: any) => {
- toast(`Error: ${error.message}`);
+ toast(
+ `Error: ${getErrorMessage(error, 'Unable to save dashboard changes right now. Please try again.')}`,
+ { id: DASHBOARD_SAVE_ERROR_TOAST_ID }
+ );
},
}
);
@@ -156,10 +168,13 @@ const Dashboard = () => {
{
onSuccess: (_, id) => {
setDashboards((prev) => prev.filter((d) => d.id !== id.toString()));
- toast.success('Dashboard deleted');
+ toast.success('Dashboard deleted', { id: DASHBOARD_DELETE_SUCCESS_TOAST_ID });
},
onError: (error: any) => {
- toast(`Error: ${error.message}`);
+ toast(
+ `Error: ${getErrorMessage(error, 'Unable to delete dashboard right now. Please try again.')}`,
+ { id: DASHBOARD_DELETE_ERROR_TOAST_ID }
+ );
},
}
);
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/details/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/details/page.tsx
index 964364a2..0e1adc22 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/details/page.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/details/page.tsx
@@ -68,6 +68,12 @@ const Details = () => {
entitySlug: string;
id: string;
}>();
+ const USECASE_EDIT_SUCCESS_TOAST_ID = 'usecase-edit-save-success';
+ const USECASE_DETAILS_ERROR_TOAST_ID = 'usecase-details-save-error';
+ const getErrorMessage = (error: any, fallback: string) =>
+ typeof error?.message === 'string' && error.message.trim()
+ ? error.message.trim()
+ : fallback;
const UseCaseData: { data: any; isLoading: boolean; refetch: any } = useQuery(
@@ -162,7 +168,9 @@ const Details = () => {
),
{
onSuccess: (res: any) => {
- toast('Use case updated successfully');
+ toast('Use case updated successfully', {
+ id: USECASE_EDIT_SUCCESS_TOAST_ID,
+ });
setFormData((prev) => ({
...prev,
...res.updateUseCase,
@@ -173,7 +181,10 @@ const Details = () => {
}));
},
onError: (error: any) => {
- toast(`Error: ${error.message}`);
+ toast(
+ `Error: ${getErrorMessage(error, 'Unable to update use case right now. Please try again.')}`,
+ { id: USECASE_DETAILS_ERROR_TOAST_ID }
+ );
},
}
);
@@ -198,8 +209,11 @@ const Details = () => {
);
const handleSave = (updatedData: any) => {
- if (JSON.stringify(updatedData) !== JSON.stringify(previousFormData)) {
- setPreviousFormData(updatedData);
+ const updatedSnapshot = JSON.stringify(updatedData);
+ setPreviousFormData((prevData) => {
+ if (JSON.stringify(prevData) === updatedSnapshot) {
+ return prevData;
+ }
mutate({
data: {
@@ -214,7 +228,9 @@ const Details = () => {
platformUrl: updatedData.platformUrl || '',
},
});
- }
+
+ return updatedData;
+ });
};
const { setStatus } = useEditStatus();
@@ -229,7 +245,7 @@ const Details = () => {
label="Summary *"
value={formData.summary}
onChange={(value) => handleChange('summary', value)}
- onBlur={() => handleSave(formData)}
+ onBlur={(value) => handleSave({ ...formData, summary: value })}
placeholder="Enter use case summary with rich formatting..."
helpText={`Character limit: ${formData?.summary?.length || 0}/10000`}
/>
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/metadata/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/metadata/page.tsx
index 3fcae8cc..9cb03849 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/metadata/page.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/metadata/page.tsx
@@ -159,6 +159,12 @@ const Metadata = () => {
entitySlug: string;
id: string;
}>();
+ const USECASE_EDIT_SUCCESS_TOAST_ID = 'usecase-edit-save-success';
+ const USECASE_METADATA_ERROR_TOAST_ID = 'usecase-metadata-save-error';
+ const getErrorMessage = (error: any, fallback: string) =>
+ typeof error?.message === 'string' && error.message.trim()
+ ? error.message.trim()
+ : fallback;
const { setStatus } = useEditStatus();
@@ -331,7 +337,9 @@ const Metadata = () => {
}, data),
{
onSuccess: (res: any) => {
- toast('Use case updated successfully');
+ toast('Use case updated successfully', {
+ id: USECASE_EDIT_SUCCESS_TOAST_ID,
+ });
const updatedData = defaultValuesPrepFn(res.addUpdateUsecaseMetadata);
if (isTagsListUpdated) {
getTagsList.refetch();
@@ -341,7 +349,10 @@ const Metadata = () => {
setPreviousFormData(updatedData);
},
onError: (error: any) => {
- toast(`Error: ${error.message}`);
+ toast(
+ `Error: ${getErrorMessage(error, 'Unable to update use case metadata right now. Please try again.')}`,
+ { id: USECASE_METADATA_ERROR_TOAST_ID }
+ );
},
}
);
@@ -358,9 +369,12 @@ const Metadata = () => {
};
const handleSave = (updatedData: any) => {
- if (JSON.stringify(updatedData) !== JSON.stringify(previousFormData)) {
- // Ensure metadata exists before mapping
- setPreviousFormData(updatedData);
+ const updatedSnapshot = JSON.stringify(updatedData);
+ setPreviousFormData((prevData) => {
+ if (JSON.stringify(prevData) === updatedSnapshot) {
+ return prevData;
+ }
+
const transformedValues = Object.keys(updatedData)?.reduce(
(acc: any, key) => {
acc[key] = Array.isArray(updatedData[key])
@@ -391,10 +405,15 @@ const Metadata = () => {
sectors: updatedData.sectors?.map((item: any) => item.value) || [],
tags: updatedData.tags?.map((item: any) => item.label) || [],
sdgs: updatedData.sdgs?.map((item: any) => item.value) || [],
- geographies: updatedData.geographies?.map((item: any) => parseInt(item.value, 10)) || [],
+ geographies:
+ updatedData.geographies?.map((item: any) =>
+ parseInt(item.value, 10)
+ ) || [],
},
});
- }
+
+ return updatedData;
+ });
};
if (
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/page.tsx
index dc7aaeef..df69d052 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/page.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/page.tsx
@@ -147,6 +147,8 @@ const Publish = () => {
}
);
const router = useRouter();
+ const PUBLISH_SUCCESS_TOAST_ID = 'usecase-publish-success';
+ const PUBLISH_ERROR_TOAST_ID = 'usecase-publish-error';
const { mutate, isLoading: mutationLoading } = useMutation(
() => GraphQL(publishUseCaseMutation, {
@@ -154,13 +156,17 @@ const Publish = () => {
}, { useCaseId: params.id }),
{
onSuccess: () => {
- toast('UseCase Published Successfully');
+ toast('UseCase Published Successfully', { id: PUBLISH_SUCCESS_TOAST_ID });
router.push(
`/dashboard/${params.entityType}/${params.entitySlug}/usecases`
);
},
onError: (err: any) => {
- toast(`Received ${err} on dataset publish `);
+ const errorMessage =
+ typeof err?.message === 'string' && err.message.trim()
+ ? err.message.trim()
+ : 'Unable to publish use case right now. Please try again.';
+ toast(`Error: ${errorMessage}`, { id: PUBLISH_ERROR_TOAST_ID });
},
}
);
@@ -287,6 +293,7 @@ const Publish = () => {
className="m-auto w-fit"
onClick={() => mutate()}
disabled={isPublishDisabled(UseCaseData?.data?.useCases[0])}
+ loading={mutationLoading}
>
Publish
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/layout.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/layout.tsx
index dfc05b94..cfdb5a48 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/layout.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/layout.tsx
@@ -38,6 +38,12 @@ const TabsAndChildren = ({ children }: { children: React.ReactNode }) => {
entitySlug: string;
id: string;
}>();
+ const USECASE_TITLE_SUCCESS_TOAST_ID = 'usecase-title-save-success';
+ const USECASE_TITLE_ERROR_TOAST_ID = 'usecase-title-save-error';
+ const getErrorMessage = (error: any, fallback: string) =>
+ typeof error?.message === 'string' && error.message.trim()
+ ? error.message.trim()
+ : fallback;
const layoutList = [
'details',
@@ -78,12 +84,17 @@ const TabsAndChildren = ({ children }: { children: React.ReactNode }) => {
}, data),
{
onSuccess: () => {
- toast('Use case updated successfully');
+ toast('Use case updated successfully', {
+ id: USECASE_TITLE_SUCCESS_TOAST_ID,
+ });
// Optionally, reset form or perform other actions
UseCaseData.refetch();
},
onError: (error: any) => {
- toast(`Error: ${error.message}`);
+ toast(
+ `Error: ${getErrorMessage(error, 'Unable to update use case title right now. Please try again.')}`,
+ { id: USECASE_TITLE_ERROR_TOAST_ID }
+ );
},
}
);
diff --git a/app/[locale]/dashboard/layout.tsx b/app/[locale]/dashboard/layout.tsx
index 84eadcd8..992fa398 100644
--- a/app/[locale]/dashboard/layout.tsx
+++ b/app/[locale]/dashboard/layout.tsx
@@ -11,11 +11,11 @@ interface DashboardLayoutProps {
export default function Layout({ children }: DashboardLayoutProps) {
return (
-
+
- <>{children}>
+
{children}
diff --git a/components/RichTextEditor/RichTextEditor.tsx b/components/RichTextEditor/RichTextEditor.tsx
index a9020599..cd55daf9 100644
--- a/components/RichTextEditor/RichTextEditor.tsx
+++ b/components/RichTextEditor/RichTextEditor.tsx
@@ -7,7 +7,7 @@ import 'react-quill-new/dist/quill.snow.css';
interface RichTextEditorProps {
value: string;
onChange: (value: string) => void;
- onBlur?: () => void;
+ onBlur?: (value: string) => void;
placeholder?: string;
label?: string;
helpText?: string;
@@ -128,7 +128,11 @@ const RichTextEditor: React.FC
= ({
const stripped = content.replace(/<(.|\n)*?>/g, '').trim();
onChange(stripped === '' ? '' : content);
}}
- onBlur={onBlur}
+ onBlur={(_range: any, _source: any, editor: any) => {
+ const html = editor?.getHTML?.() || value || '';
+ const stripped = html.replace(/<(.|\n)*?>/g, '').trim();
+ onBlur?.(stripped === '' ? '' : html);
+ }}
modules={modules}
formats={formats}
placeholder={placeholder}
diff --git a/components/RichTextRenderer/RichTextRenderer.tsx b/components/RichTextRenderer/RichTextRenderer.tsx
index 49b04df5..2a28a16f 100644
--- a/components/RichTextRenderer/RichTextRenderer.tsx
+++ b/components/RichTextRenderer/RichTextRenderer.tsx
@@ -12,14 +12,29 @@ const RichTextRenderer: React.FC = ({
content,
className = '',
}) => {
+ const rawContent = content || '';
+
+ // Normalize non-breaking spaces only when we detect overflow-prone content
+ // (e.g. very long runs of nbsp that prevent wrapping).
+ const hasOverflowRiskNbsp =
+ /(?: |\u00A0){6,}/.test(rawContent) ||
+ /(?:\w(?: |\u00A0)){12,}\w/i.test(rawContent);
+
+ const normalizedContent = hasOverflowRiskNbsp
+ ? rawContent.replace(/ /g, ' ').replace(/\u00A0/g, ' ')
+ : rawContent;
+
return (