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..e0d0556c 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,16 +316,6 @@ export default function ConnectPage() { } = useUseCaseEditStatus(); const ownerArgs = { [params.entityType]: params.entitySlug }; - const [formData, setFormData] = useState({ - tags: [], - sdgs: [], - sectors: [], - geographies: [], - }); - const formRef = useRef(formData); - useEffect(() => { - formRef.current = formData; - }); const [userSearch, setUserSearch] = useState(''); const [datasetSearch, setDatasetSearch] = useState(''); const [debouncedDatasetSearch, setDebouncedDatasetSearch] = useState(''); @@ -374,33 +406,22 @@ export default function ConnectPage() { ]); const useCase = useCaseQuery.data?.useCases?.[0]; + const [formData, setFormData] = useState(() => + useCase ? toConnectForm(useCase) : emptyConnectForm + ); + const formRef = useRef(formData); const [syncedUseCase, setSyncedUseCase] = useState(useCase); + const [metadataReady, setMetadataReady] = useState(Boolean(useCase)); if (useCase && useCase !== syncedUseCase) { 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(toConnectForm(useCase)); + setMetadataReady(true); } + useEffect(() => { + formRef.current = formData; + }); + useEffect(() => { const timer = window.setTimeout(() => { setDebouncedDatasetSearch(datasetSearch.trim()); @@ -439,6 +460,7 @@ export default function ConnectPage() { const persistMetadata = useCallback( (next = formRef.current) => { + if (!metadataReady) return Promise.resolve(); return saveMetadata({ id: params.id, metadata: [], @@ -450,7 +472,7 @@ export default function ConnectPage() { ), }).catch(() => undefined); }, - [params.id, saveMetadata] + [metadataReady, params.id, saveMetadata] ); const handleClassChange = ( @@ -828,7 +850,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