From c07f123a29ac06d978aac4a6da2f2ef133fdf3e7 Mon Sep 17 00:00:00 2001 From: Abhishekfm Date: Thu, 12 Nov 2026 13:57:20 +0530 Subject: [PATCH 1/2] Enhance AI model details page to prevent form data loss during refetches by introducing hydration logic. Update user profile component to utilize NextAuth for password changes, improving user experience. Refactor use case components to include summary validation and improve content handling. Introduce empty connect form initialization and streamline form data synchronization. Update error handling in connect page for better user feedback. --- .../aimodels/edit/[id]/details/page.tsx | 24 +++--- .../[entitySlug]/profile/userProfile.tsx | 16 ++-- .../usecases/edit/[id]/builder/page.tsx | 31 ++++++- .../usecases/edit/[id]/connect/page.tsx | 81 ++++++++++++------- .../usecases/edit/components/EditLayout.tsx | 14 +++- .../usecases/edit/usecase-summary.ts | 7 +- app/api/auth/change-password/route.ts | 18 ----- 7 files changed, 119 insertions(+), 72 deletions(-) delete mode 100644 app/api/auth/change-password/route.ts 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 16d03a23..b614d1c6 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 @@ -368,19 +368,25 @@ export default function AIModelDetailsPage() { ); const [prevId, setPrevId] = useState(params.id); - if (params.id !== prevId) { + // Fill the form when this model is first loaded, not on every refetch. + // A refetch is a new object for the same id and was wiping in-progress text. + const [hydratedModelId, setHydratedModelId] = useState(null); + const modelId = model?.id != null ? String(model.id) : null; + const routeChanged = params.id !== prevId; + + if (routeChanged) { setPrevId(params.id); + setHydratedModelId(null); setFormData(emptyAIModelForm()); } - const [prevModel, setPrevModel] = useState( - undefined - ); - if (model !== prevModel) { - setPrevModel(model); - if (model) { - setFormData(formDataFromModel(model)); - } + if ( + model && + modelId === params.id && + modelId !== (routeChanged ? null : hydratedModelId) + ) { + setHydratedModelId(modelId); + setFormData(formDataFromModel(model)); } const handleInputChange = ( diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/profile/userProfile.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/profile/userProfile.tsx index 7c9b8ee7..618f1156 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/profile/userProfile.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/profile/userProfile.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useParams } from 'next/navigation'; +import { signIn } from 'next-auth/react'; import { graphql } from '@/gql'; import { SectorInfoDocument, @@ -306,13 +307,13 @@ const UserProfile = () => { const openChangePassword = async () => { try { - const response = await fetch('/api/auth/change-password'); - const data = (await response.json()) as { url?: string }; - if (!data.url) { - toast.error('Could not open the Keycloak password page.'); - return; - } - window.location.assign(data.url); + // kc_action skips the account "Signing in" page and opens the + // update-password form. NextAuth adds state so the user returns here. + await signIn( + 'keycloak', + { callbackUrl: window.location.href }, + { kc_action: 'UPDATE_PASSWORD' } + ); } catch { toast.error('Could not open the Keycloak password page.'); } @@ -539,7 +540,6 @@ const UserProfile = () => { kind="tertiary" size="slim" onClick={openChangePassword} - disabled icon={} > Change password diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/builder/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/builder/page.tsx index 73756239..9688fee9 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/builder/page.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/builder/page.tsx @@ -22,6 +22,7 @@ import MediaImage from '@/components/MediaImage'; import { ContentEditor } from '../../components/ContentEditor'; import { extractEmbedUrl, + hasContentBlocks, parseUseCaseContent, serializeUseCaseContent, type UseCaseContentDocument, @@ -346,8 +347,14 @@ export default function BuilderPage() { }, [savingUseCase, addingDashboard, savingDashboard, setStatus]); useEffect(() => { - setBuilderCompleted(isUseCaseBuilderComplete({ title, logo })); - }, [title, logo, setBuilderCompleted]); + setBuilderCompleted( + isUseCaseBuilderComplete({ + title, + logo, + summary: serializeUseCaseContent(contentDoc), + }) + ); + }, [title, logo, contentDoc, setBuilderCompleted]); const handleSaveDashboard = () => { const link = extractEmbedUrl(embedCode); @@ -434,6 +441,10 @@ export default function BuilderPage() { stepShowErrors && !title.trim() ? 'Enter a use case title' : undefined; const thumbnailError = stepShowErrors && !logo ? 'Upload a thumbnail image' : undefined; + const contentError = + stepShowErrors && !hasContentBlocks(contentDoc) + ? 'Add at least one content block' + : undefined; const extractedDashboardLink = extractEmbedUrl(embedCode); const dashboardLinkLooksLikeUrl = Boolean( extractedDashboardLink && @@ -563,7 +574,14 @@ export default function BuilderPage() { + Content{' '} + + * + + + } description="Add the main information that explains this content." className={styles.overflowVisible} > @@ -589,6 +607,13 @@ export default function BuilderPage() { persistBuilder({ document }); }} /> + {contentError ? ( +
+ + {contentError} + +
+ ) : null}
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/connect/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/connect/page.tsx index 32043a5a..b3d44ba2 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/connect/page.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/connect/page.tsx @@ -259,6 +259,48 @@ function sdgLabel(item: { return `${num}. ${item.name ?? ''}`; } +const emptyConnectForm: ConnectFormData = { + tags: [], + sdgs: [], + sectors: [], + geographies: [], +}; + +function toConnectForm(useCase: { + tags?: Array<{ id: string; value?: string | null }> | null; + sdgs?: Array<{ + id: string; + number?: number | null; + code?: string | null; + name?: string | null; + }> | null; + sectors?: Array<{ id: string; name?: string | null }> | null; + geographies?: Array<{ id: string; name?: string | null }> | null; +}): ConnectFormData { + return { + tags: + useCase.tags?.map((item) => ({ + label: item.value ?? '', + value: item.id, + })) ?? [], + sdgs: + useCase.sdgs?.map((item) => ({ + label: sdgLabel(item), + value: item.id, + })) ?? [], + sectors: + useCase.sectors?.map((item) => ({ + label: item.name ?? '', + value: item.id, + })) ?? [], + geographies: + useCase.geographies?.map((item) => ({ + label: item.name ?? '', + value: item.id, + })) ?? [], + }; +} + export default function ConnectPage() { const params = useParams<{ entityType: string; @@ -274,13 +316,9 @@ export default function ConnectPage() { } = useUseCaseEditStatus(); const ownerArgs = { [params.entityType]: params.entitySlug }; - const [formData, setFormData] = useState({ - tags: [], - sdgs: [], - sectors: [], - geographies: [], - }); + const [formData, setFormData] = useState(emptyConnectForm); const formRef = useRef(formData); + const metadataReadyRef = useRef(false); useEffect(() => { formRef.current = formData; }); @@ -375,30 +413,12 @@ export default function ConnectPage() { const useCase = useCaseQuery.data?.useCases?.[0]; const [syncedUseCase, setSyncedUseCase] = useState(useCase); - if (useCase && useCase !== syncedUseCase) { + if (useCase && (useCase !== syncedUseCase || !metadataReadyRef.current)) { + const next = toConnectForm(useCase); setSyncedUseCase(useCase); - setFormData({ - tags: - useCase.tags?.map((item) => ({ - label: item.value ?? '', - value: item.id, - })) ?? [], - sdgs: - useCase.sdgs?.map((item) => ({ - label: sdgLabel(item), - value: item.id, - })) ?? [], - sectors: - useCase.sectors?.map((item) => ({ - label: item.name ?? '', - value: item.id, - })) ?? [], - geographies: - useCase.geographies?.map((item) => ({ - label: item.name ?? '', - value: item.id, - })) ?? [], - }); + setFormData(next); + formRef.current = next; + metadataReadyRef.current = true; } useEffect(() => { @@ -439,6 +459,7 @@ export default function ConnectPage() { const persistMetadata = useCallback( (next = formRef.current) => { + if (!metadataReadyRef.current) return Promise.resolve(); return saveMetadata({ id: params.id, metadata: [], @@ -828,7 +849,7 @@ export default function ConnectPage() { }} /> {datasetError ? ( -
+
{datasetError} diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/components/EditLayout.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/components/EditLayout.tsx index 59bec6ff..9452568a 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/components/EditLayout.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/components/EditLayout.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ReactNode, useEffect } from 'react'; +import { ReactNode, useEffect, useRef } from 'react'; import { useParams, usePathname, useRouter } from 'next/navigation'; import { graphql } from '@/gql'; import { UseCaseInputPartial } from '@/gql/generated/graphql'; @@ -54,13 +54,21 @@ const UpdateUseCaseTitleMutation = graphql(` function StepErrorSync() { const { showErrors } = useStepperStep(); - const { setStepShowErrors } = useUseCaseEditStatus(); + const { stepShowErrors, setStepShowErrors } = useUseCaseEditStatus(); + const markerRef = useRef(null); useEffect(() => { setStepShowErrors(showErrors); }, [showErrors, setStepShowErrors]); - return null; + useEffect(() => { + if (!stepShowErrors) return; + markerRef.current?.parentElement + ?.querySelector('[id$="-error"]') + ?.scrollIntoView({ block: 'center' }); + }, [stepShowErrors]); + + return