From 1d1291b596516c9f5d9b4dba341ebe78b6c7fb69 Mon Sep 17 00:00:00 2001 From: Abhishekfm Date: Mon, 2 Mar 2026 19:05:40 +0530 Subject: [PATCH 1/4] quill Editor, next and prev fixed, Collabrative description fixed --- .../edit/[id]/publish/Details.tsx | 10 +- .../components/StepNavigation.tsx | 41 +++++- .../[id]/edit/components/EditLayout.tsx | 12 +- .../[id]/edit/components/EditMetadata.tsx | 139 +++++++++++------- .../dataset/[id]/edit/context.tsx | 23 ++- components/RichTextEditor/RichTextEditor.tsx | 8 +- .../RichTextRenderer/RichTextRenderer.tsx | 50 ++++++- 7 files changed, 212 insertions(+), 71 deletions(-) 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]/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 +217,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..1e1dd377 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 { @@ -430,10 +430,10 @@ export function EditMetadata({ id }: { id: string }) { if (res.addUpdateDatasetMetadata.success) { toast('Details updated successfully!'); 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 @@ -532,6 +532,7 @@ export function EditMetadata({ id }: { id: string }) { ) ); const [previousFormData, setPreviousFormData] = useState(formData); + const formDataRef = useRef(formData); useEffect(() => { if (getDatasetMetadata.data?.datasets[0]) { @@ -539,18 +540,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 +584,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 +598,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 +786,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 +813,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/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 (