From 42596aef605ddeab969904b93d945b23b3514576 Mon Sep 17 00:00:00 2001 From: Uday Raj Sahai Date: Tue, 28 Jul 2026 16:21:15 +0530 Subject: [PATCH 1/2] Fixed:Usecase issue --- .../usecases/edit/[id]/dashboards/page.tsx | 49 +- .../usecases/edit/[id]/details/page.tsx | 430 ++++++++++++------ .../usecases/edit/[id]/metadata/page.tsx | 70 ++- 3 files changed, 387 insertions(+), 162 deletions(-) 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 d733559c..4a43daf0 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 @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; import Link from 'next/link'; import { useParams } from 'next/navigation'; import { graphql } from '@/gql'; -import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Button, Icon, Text, TextField, toast } from 'opub-ui'; import { GraphQL } from '@/lib/api'; @@ -76,6 +76,7 @@ const deleteDashboard: any = graphql(` const Dashboard = () => { const params = useParams<{ entityType?: string; entitySlug?: string; id?: string }>(); + const queryClient = useQueryClient(); 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'; @@ -109,24 +110,32 @@ const Dashboard = () => { >([]); const [previousState, setPreviousState] = useState({}); - useQuery( - ['fetch_dashboardData', usecaseId], + const queryKey = ['fetch_dashboardData', usecaseId]; + + const { data: dashboardData }: { data: any } = useQuery( + queryKey, () => GraphQL(dashboardList, ownerArgs || {}, { usecaseId }), { refetchOnMount: true, refetchOnReconnect: true, enabled: isValidId, - onSuccess: (res: any) => { - setDashboards(res.usecaseDashboards || []); - setPreviousState( - Object.fromEntries( - res.usecaseDashboards.map((item: any) => [item.id, { ...item }]) - ) - ); - }, } ); + useEffect(() => { + if (dashboardData?.usecaseDashboards) { + setDashboards(dashboardData.usecaseDashboards); + setPreviousState( + Object.fromEntries( + dashboardData.usecaseDashboards.map((item: any) => [ + item.id, + { ...item }, + ]) + ) + ); + } + }, [dashboardData]); + const { mutate: addDashboard, isLoading: addLoading } = useMutation( ({ usecaseId }: { usecaseId: number }) => GraphQL(AddDashboard, ownerArgs || {}, { usecaseId }), @@ -140,6 +149,10 @@ const Dashboard = () => { [newDashboard.id]: { ...newDashboard }, })); toast.success('Dashboard added', { id: DASHBOARD_ADD_SUCCESS_TOAST_ID }); + + queryClient.setQueryData(queryKey, (old: any) => ({ + usecaseDashboards: [...(old?.usecaseDashboards || []), newDashboard], + })); }, } ); @@ -153,6 +166,14 @@ const Dashboard = () => { ...prev, [updateUsecaseDashboard.data.id]: { ...updateUsecaseDashboard.data }, })); + + queryClient.setQueryData(queryKey, (old: any) => ({ + usecaseDashboards: (old?.usecaseDashboards || []).map((item: any) => + item.id === updateUsecaseDashboard.data.id + ? { ...item, ...updateUsecaseDashboard.data } + : item + ), + })); }, onError: (error: any) => { toast( @@ -169,6 +190,12 @@ const Dashboard = () => { onSuccess: (_, id) => { setDashboards((prev) => prev.filter((d) => d.id !== id.toString())); toast.success('Dashboard deleted', { id: DASHBOARD_DELETE_SUCCESS_TOAST_ID }); + + queryClient.setQueryData(queryKey, (old: any) => ({ + usecaseDashboards: (old?.usecaseDashboards || []).filter( + (item: any) => item.id !== id.toString() + ), + })); }, onError: (error: any) => { toast( 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 09b6ae83..c15c661a 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 @@ -1,14 +1,12 @@ 'use client'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useParams } from 'next/navigation'; import { graphql } from '@/gql'; import { UseCaseInputPartial } from '@/gql/generated/graphql'; -import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { DropZone, Select, TextField, toast } from 'opub-ui'; -// Assuming you are using these components - import { GraphQL } from '@/lib/api'; import { RichTextEditor } from '@/components/RichTextEditor'; import { useEditStatus } from '../../context'; @@ -62,12 +60,62 @@ const FetchUseCase: any = graphql(` } `); +type DetailsFormData = { + title: string; + summary: string; + logo: File | null | any; + website: string; + contactEmail: string; + slug: string; + status: string; + runningStatus: string | null; + startedOn: string | null; + completedOn: string | null; + platformUrl: string; +}; + +const initialFormData: DetailsFormData = { + title: '', + summary: '', + logo: null, + website: '', + contactEmail: '', + slug: '', + status: '', + runningStatus: null, + startedOn: null, + completedOn: null, + platformUrl: '', +}; + +const RUNNING_STATUS_OPTIONS = [ + { label: 'Initiated', value: 'INITIATED' }, + { label: 'On Going', value: 'ON_GOING' }, + { label: 'Completed', value: 'COMPLETED' }, + { label: 'Cancelled', value: 'CANCELLED' }, +]; + +const mapUseCaseToForm = (usecase: any): DetailsFormData => ({ + title: usecase.title || '', + summary: usecase.summary || '', + logo: usecase.logo || null, + website: usecase.website || '', + contactEmail: usecase.contactEmail || '', + slug: usecase.slug || '', + status: usecase.status || '', + runningStatus: usecase.runningStatus || null, + startedOn: usecase.startedOn || '', + completedOn: usecase.completedOn || '', + platformUrl: usecase.platformUrl || '', +}); + const Details = () => { const params = useParams<{ entityType: string; entitySlug: string; id: string; }>(); + const queryClient = useQueryClient(); 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) => @@ -75,8 +123,10 @@ const Details = () => { ? error.message.trim() : fallback; - const UseCaseData: { data: any; isLoading: boolean; refetch: any } = useQuery( - [`fetch_UseCaseData_details`], + const queryKey = ['fetch_UseCaseData_details', params.id]; + + const { data: useCaseQueryData }: { data: any } = useQuery( + queryKey, () => GraphQL( FetchUseCase, @@ -95,66 +145,47 @@ const Details = () => { } ); - const UsecasesData = - UseCaseData?.data?.useCases.length > 0 && UseCaseData?.data?.useCases[0]; - - const initialFormData = { - title: '', - summary: '', - logo: null as File | null, - website: '', - contactEmail: '', - slug: '', - status: '', - runningStatus: null, - startedOn: null, - completedOn: null, - platformUrl: '', - }; + const [formData, setFormData] = useState(initialFormData); + const [previousFormData, setPreviousFormData] = + useState(initialFormData); + const formDataRef = useRef(formData); + formDataRef.current = formData; + const previousFormDataRef = useRef(previousFormData); + previousFormDataRef.current = previousFormData; + const hydratedIdRef = useRef(null); + const lastHydratedSnapshotRef = useRef(null); + // Survives tab unmount — in-memory ref alone is lost when leaving Details + const pendingRunningStatusKey = `usecase-pending-runningStatus-${params.id}`; - const runningStatus = [ - { - label: 'Initiated', - value: 'INITIATED', - }, - { - label: 'On Going', - value: 'ON_GOING', - }, - { - label: 'Completed', - value: 'COMPLETED', - }, - { - label: 'Cancelled', - value: 'CANCELLED', - }, - ]; - - const [formData, setFormData] = useState(initialFormData); + const readPendingRunningStatus = () => { + if (typeof window === 'undefined') return null; + try { + return sessionStorage.getItem(pendingRunningStatusKey); + } catch { + return null; + } + }; - const [previousFormData, setPreviousFormData] = useState(initialFormData); + const writePendingRunningStatus = (value: string | null) => { + if (typeof window === 'undefined') return; + try { + if (value) { + sessionStorage.setItem(pendingRunningStatusKey, value); + } else { + sessionStorage.removeItem(pendingRunningStatusKey); + } + } catch { + // ignore storage errors + } + }; + // Reset local form when switching to a different use case useEffect(() => { - if (UsecasesData) { - // Ensure UsecasesData is available - const updatedData = { - title: UsecasesData.title || '', // Fallback to empty string if undefined - summary: UsecasesData.summary || '', - logo: UsecasesData.logo || null, - website: UsecasesData.website || '', - contactEmail: UsecasesData.contactEmail || '', - slug: UsecasesData.slug || '', - status: UsecasesData.status || '', - runningStatus: UsecasesData.runningStatus || null, - startedOn: UsecasesData.startedOn || '', - completedOn: UsecasesData.completedOn || '', - platformUrl: UsecasesData.platformUrl || '', - }; - setFormData(updatedData); - setPreviousFormData(updatedData); - } - }, [params.id, UsecasesData]); + hydratedIdRef.current = null; + lastHydratedSnapshotRef.current = null; + setFormData(initialFormData); + setPreviousFormData(initialFormData); + }, [params.id]); const { mutate, isLoading: editMutationLoading } = useMutation( (data: { data: UseCaseInputPartial }) => @@ -166,18 +197,83 @@ const Details = () => { data ), { - onSuccess: (res: any) => { + onSuccess: async (res: any, variables: { data: UseCaseInputPartial }) => { toast('Use case updated successfully', { id: USECASE_EDIT_SUCCESS_TOAST_ID, }); - setFormData((prev) => ({ - ...prev, - ...res.updateUseCase, - })); - setPreviousFormData((prev) => ({ - ...prev, - ...res.updateUseCase, - })); + const updated = res.updateUseCase; + const sent = variables?.data || { id: params.id.toString() }; + + await queryClient.cancelQueries({ queryKey }); + + // Only patch fields that were actually sent. Spreading the full response + // was overwriting runningStatus back to INITIATED when summary/url/dates saved. + const patchFromSent = ( + current: DetailsFormData | Record + ): DetailsFormData => { + const next = { ...current } as DetailsFormData; + + if (sent.summary !== undefined) { + next.summary = updated.summary ?? sent.summary ?? ''; + } + if (sent.website !== undefined) { + next.website = updated.website ?? sent.website ?? ''; + } + if (sent.contactEmail !== undefined) { + next.contactEmail = updated.contactEmail ?? sent.contactEmail ?? ''; + } + if (sent.platformUrl !== undefined) { + next.platformUrl = updated.platformUrl ?? sent.platformUrl ?? ''; + } + if (sent.startedOn !== undefined) { + next.startedOn = updated.startedOn ?? sent.startedOn ?? null; + } + if (sent.completedOn !== undefined) { + next.completedOn = updated.completedOn ?? sent.completedOn ?? null; + } + if (sent.logo !== undefined) { + next.logo = updated.logo ?? sent.logo ?? null; + } + if (sent.runningStatus !== undefined) { + // Keep the value we sent; don't let a lagging response/refetch reset it + next.runningStatus = String(sent.runningStatus); + writePendingRunningStatus(null); + } + if (updated.title !== undefined && updated.title !== null) { + next.title = updated.title; + } + if (updated.status !== undefined && updated.status !== null) { + next.status = updated.status; + } + if (updated.slug !== undefined && updated.slug !== null) { + next.slug = updated.slug; + } + + return next; + }; + + setFormData((prev) => { + const merged = patchFromSent(prev); + formDataRef.current = merged; + return merged; + }); + setPreviousFormData((prev) => { + const merged = patchFromSent(prev); + previousFormDataRef.current = merged; + return merged; + }); + + queryClient.setQueryData(queryKey, (old: any) => { + const current = old?.useCases?.[0] || {}; + const merged = patchFromSent(current); + lastHydratedSnapshotRef.current = JSON.stringify( + mapUseCaseToForm(merged) + ); + return { + ...(old || {}), + useCases: [merged, ...(old?.useCases?.slice(1) || [])], + }; + }); }, onError: (error: any) => { toast( @@ -188,7 +284,48 @@ const Details = () => { } ); - const handleChange = useCallback((field: string, value: any) => { + // Hydrate from query/cache. Skip while a save is in flight so a late + // refetch cannot wipe values the user just saved. + useEffect(() => { + if (editMutationLoading) return; + + const usecase = useCaseQueryData?.useCases?.[0]; + if (!usecase) return; + + const mapped = mapUseCaseToForm(usecase); + const pendingStatus = readPendingRunningStatus(); + + // Restore optimistic running status after tab remount + if (pendingStatus) { + if (mapped.runningStatus === pendingStatus) { + writePendingRunningStatus(null); + } else { + mapped.runningStatus = pendingStatus; + } + } + + const mappedSnapshot = JSON.stringify(mapped); + + if (lastHydratedSnapshotRef.current === mappedSnapshot) { + return; + } + + const isFirstHydration = hydratedIdRef.current !== params.id; + const isDirty = + JSON.stringify(formDataRef.current) !== + JSON.stringify(previousFormDataRef.current); + + if (isFirstHydration || !isDirty) { + hydratedIdRef.current = params.id; + lastHydratedSnapshotRef.current = mappedSnapshot; + setFormData(mapped); + setPreviousFormData(mapped); + formDataRef.current = mapped; + previousFormDataRef.current = mapped; + } + }, [useCaseQueryData, editMutationLoading, params.id]); + + const handleChange = useCallback((field: keyof DetailsFormData, value: any) => { setFormData((prevData) => ({ ...prevData, [field]: value, @@ -204,38 +341,96 @@ const Details = () => { }, }); }, - [] + [mutate, params.id] ); - const handleSave = (updatedData: any) => { - const updatedSnapshot = JSON.stringify(updatedData); - setPreviousFormData((prevData) => { - if (JSON.stringify(prevData) === updatedSnapshot) { - return prevData; - } + const applyLocalForm = (next: DetailsFormData) => { + formDataRef.current = next; + previousFormDataRef.current = next; + setFormData(next); + setPreviousFormData(next); + }; - mutate({ - data: { - id: params.id.toString(), - title: updatedData.title, - summary: updatedData.summary, - website: updatedData.website, - contactEmail: updatedData.contactEmail, - runningStatus: updatedData.runningStatus, - startedOn: (updatedData.startedOn as Date) || null, - completedOn: (updatedData.completedOn as Date) || null, - platformUrl: updatedData.platformUrl || '', - }, - }); + const saveRunningStatus = (nextValue: string) => { + if (!nextValue || nextValue === formDataRef.current.runningStatus) { + return; + } + + const next = { + ...formDataRef.current, + runningStatus: nextValue, + }; + + writePendingRunningStatus(nextValue); + applyLocalForm(next); - return updatedData; + // Optimistic cache write so remount/hydration cannot flash back to INITIATED + queryClient.setQueryData(queryKey, (old: any) => { + if (!old?.useCases?.[0]) { + return { useCases: [{ id: params.id, runningStatus: nextValue }] }; + } + const merged = { + ...old.useCases[0], + runningStatus: nextValue, + }; + lastHydratedSnapshotRef.current = JSON.stringify(mapUseCaseToForm(merged)); + return { + ...old, + useCases: [merged, ...old.useCases.slice(1)], + }; }); + + mutate({ + data: { + id: params.id.toString(), + runningStatus: nextValue as UseCaseInputPartial['runningStatus'], + }, + }); + }; + + // Only send changed fields so a wiped/empty local value cannot overwrite + // a previously saved platformUrl / runningStatus on the server. + const handleSave = (updatedData: DetailsFormData) => { + const prev = previousFormDataRef.current; + const changed: UseCaseInputPartial = { + id: params.id.toString(), + }; + + if (updatedData.summary !== prev.summary) { + changed.summary = updatedData.summary; + } + if (updatedData.website !== prev.website) { + changed.website = updatedData.website; + } + if (updatedData.contactEmail !== prev.contactEmail) { + changed.contactEmail = updatedData.contactEmail; + } + // runningStatus is saved through saveRunningStatus — do not include here + if (updatedData.startedOn !== prev.startedOn) { + changed.startedOn = (updatedData.startedOn as any) || null; + } + if (updatedData.completedOn !== prev.completedOn) { + changed.completedOn = (updatedData.completedOn as any) || null; + } + if (updatedData.platformUrl !== prev.platformUrl) { + changed.platformUrl = updatedData.platformUrl || ''; + } + + // Nothing changed + if (Object.keys(changed).length === 1) { + return; + } + + previousFormDataRef.current = updatedData; + setPreviousFormData(updatedData); + mutate({ data: changed }); }; + const { setStatus } = useEditStatus(); useEffect(() => { - setStatus(editMutationLoading ? 'loading' : 'success'); // update based on mutation state - }, [editMutationLoading]); + setStatus(editMutationLoading ? 'loading' : 'success'); + }, [editMutationLoading, setStatus]); return (
@@ -244,7 +439,9 @@ const Details = () => { label="Summary *" value={formData.summary} onChange={(value) => handleChange('summary', value)} - onBlur={(value) => handleSave({ ...formData, summary: value })} + onBlur={(value) => + handleSave({ ...formDataRef.current, summary: value }) + } placeholder="Enter use case summary with rich formatting..." helpText={`Character limit: ${formData?.summary?.length || 0}/10000`} /> @@ -257,21 +454,18 @@ const Details = () => { type="url" value={formData.platformUrl} onChange={(e) => handleChange('platformUrl', e)} - onBlur={() => handleSave(formData)} + onBlur={() => handleSave(formDataRef.current)} />