diff --git a/app/[locale]/(user)/components/Content.tsx b/app/[locale]/(user)/components/Content.tsx index 38c83ec3..777b061d 100644 --- a/app/[locale]/(user)/components/Content.tsx +++ b/app/[locale]/(user)/components/Content.tsx @@ -122,7 +122,7 @@ export const Content = () => { {item.label} diff --git a/app/[locale]/(user)/layout.tsx b/app/[locale]/(user)/layout.tsx index 6cbc4dc2..29f8f885 100644 --- a/app/[locale]/(user)/layout.tsx +++ b/app/[locale]/(user)/layout.tsx @@ -20,11 +20,11 @@ export default function Layout({ children }: UserLayoutProps) { } return ( -
+
- <>{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) => {
- +
@@ -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 (