From 6546b379fc7e72ecfef727fa0919187a1284c4ba Mon Sep 17 00:00:00 2001 From: Nicolas Wormser Date: Fri, 7 Aug 2026 19:05:22 +0200 Subject: [PATCH 1/2] fix(app): make kanban and gallery record expansion update the url Cards opened their record by setting local state only, so the url kept pointing at the view and "copy record url" returned a link without recordId. Expanding now goes through the router, like the grid view. --- .../blocks/view/gallery/components/Card.tsx | 21 ++++++---------- .../view/gallery/context/GalleryContext.ts | 2 -- .../view/gallery/context/GalleryProvider.tsx | 2 -- .../app/blocks/view/hooks/useExpandRecord.ts | 24 +++++++++++++++++++ .../view/kanban/components/KanbanCard.tsx | 11 ++++----- .../view/kanban/context/KanbanContext.ts | 2 -- .../view/kanban/context/KanbanProvider.tsx | 2 -- 7 files changed, 36 insertions(+), 28 deletions(-) create mode 100644 apps/nextjs-app/src/features/app/blocks/view/hooks/useExpandRecord.ts diff --git a/apps/nextjs-app/src/features/app/blocks/view/gallery/components/Card.tsx b/apps/nextjs-app/src/features/app/blocks/view/gallery/components/Card.tsx index 6d7d4b733f..df27bc1bab 100644 --- a/apps/nextjs-app/src/features/app/blocks/view/gallery/components/Card.tsx +++ b/apps/nextjs-app/src/features/app/blocks/view/gallery/components/Card.tsx @@ -19,6 +19,7 @@ import { useTranslation } from 'next-i18next'; import { Fragment, useMemo } from 'react'; import { tableConfig } from '@/features/i18n/table.config'; import { useContextMenu } from '../../hooks/useContextMenu'; +import { useExpandRecord } from '../../hooks/useExpandRecord'; import { useGallery } from '../hooks'; import { CARD_COVER_HEIGHT, CARD_STYLE } from '../utils'; import { CardCarousel } from './CardCarousel'; @@ -33,16 +34,10 @@ export const Card = (props: IKanbanCardProps) => { const viewId = useViewId(); const getFieldStatic = useFieldStaticGetter(); const { t } = useTranslation(tableConfig.i18nNamespaces); - const { - coverField, - primaryField, - displayFields, - permission, - isCoverFit, - isFieldNameHidden, - setExpandRecordId, - } = useGallery(); + const { coverField, primaryField, displayFields, permission, isCoverFit, isFieldNameHidden } = + useGallery(); const { copyRecordUrl, viewRecordHistory, addRecordComment } = useContextMenu(); + const expandRecord = useExpandRecord(); const { cardCreatable, cardDeletable, cardEditable, cardCommentCreatable } = permission; const coverFieldId = coverField?.id; @@ -59,8 +54,8 @@ export const Card = (props: IKanbanCardProps) => { ); }, [card, primaryField, t]); - const onExpand = () => { - setExpandRecordId(card.id); + const onExpand = async () => { + await expandRecord(card.id); }; const onDelete = () => { @@ -87,7 +82,7 @@ export const Card = (props: IKanbanCardProps) => { const record = res.data.records[0]; if (record != null) { - setExpandRecordId(record.id); + await expandRecord(record.id); } }; @@ -96,12 +91,10 @@ export const Card = (props: IKanbanCardProps) => { }; const onViewRecordHistory = async () => { - setExpandRecordId(card.id); await viewRecordHistory(card.id); }; const onAddRecordComment = async () => { - setExpandRecordId(card.id); await addRecordComment(card.id); }; diff --git a/apps/nextjs-app/src/features/app/blocks/view/gallery/context/GalleryContext.ts b/apps/nextjs-app/src/features/app/blocks/view/gallery/context/GalleryContext.ts index 630cfe7be1..6523ed70a4 100644 --- a/apps/nextjs-app/src/features/app/blocks/view/gallery/context/GalleryContext.ts +++ b/apps/nextjs-app/src/features/app/blocks/view/gallery/context/GalleryContext.ts @@ -1,6 +1,5 @@ import type { IGetRecordsRo } from '@teable/openapi'; import type { AttachmentField, IFieldInstance } from '@teable/sdk/model'; -import type { Dispatch, SetStateAction } from 'react'; import { createContext } from 'react'; import type { IGalleryPermission } from '../type'; @@ -12,7 +11,6 @@ export interface IGalleryContext { permission: IGalleryPermission; primaryField: IFieldInstance; displayFields: IFieldInstance[]; - setExpandRecordId: Dispatch>; } export const GalleryContext = createContext(null!); diff --git a/apps/nextjs-app/src/features/app/blocks/view/gallery/context/GalleryProvider.tsx b/apps/nextjs-app/src/features/app/blocks/view/gallery/context/GalleryProvider.tsx index 9d8bca5c33..9aa3647607 100644 --- a/apps/nextjs-app/src/features/app/blocks/view/gallery/context/GalleryProvider.tsx +++ b/apps/nextjs-app/src/features/app/blocks/view/gallery/context/GalleryProvider.tsx @@ -105,7 +105,6 @@ export const GalleryProvider = ({ children }: { children: ReactNode }) => { coverField, primaryField, displayFields, - setExpandRecordId, }; }, [ recordQuery, @@ -115,7 +114,6 @@ export const GalleryProvider = ({ children }: { children: ReactNode }) => { coverField, primaryField, displayFields, - setExpandRecordId, ]); const onClose = () => { diff --git a/apps/nextjs-app/src/features/app/blocks/view/hooks/useExpandRecord.ts b/apps/nextjs-app/src/features/app/blocks/view/hooks/useExpandRecord.ts new file mode 100644 index 0000000000..769f600e05 --- /dev/null +++ b/apps/nextjs-app/src/features/app/blocks/view/hooks/useExpandRecord.ts @@ -0,0 +1,24 @@ +import { useRouter } from 'next/router'; +import { useCallback } from 'react'; + +// expanding a record must go through the url, otherwise the expanded record is +// not linkable (copy record url would return the bare view url) +export const useExpandRecord = () => { + const router = useRouter(); + + return useCallback( + async (recordId: string) => { + await router.push( + { + pathname: router.pathname, + query: { ...router.query, recordId }, + }, + undefined, + { + shallow: true, + } + ); + }, + [router] + ); +}; diff --git a/apps/nextjs-app/src/features/app/blocks/view/kanban/components/KanbanCard.tsx b/apps/nextjs-app/src/features/app/blocks/view/kanban/components/KanbanCard.tsx index 5849df6c95..edd70c9105 100644 --- a/apps/nextjs-app/src/features/app/blocks/view/kanban/components/KanbanCard.tsx +++ b/apps/nextjs-app/src/features/app/blocks/view/kanban/components/KanbanCard.tsx @@ -21,6 +21,7 @@ import { useMemo } from 'react'; import { tableConfig } from '@/features/i18n/table.config'; import { CardCarousel } from '../../gallery/components'; import { useContextMenu } from '../../hooks/useContextMenu'; +import { useExpandRecord } from '../../hooks/useExpandRecord'; import type { IKanbanContext } from '../context'; import { useKanban } from '../hooks'; import type { IStackData } from '../type'; @@ -47,9 +48,9 @@ export const KanbanCard = (props: IKanbanCardProps) => { coverField, isCoverFit, isFieldNameHidden, - setExpandRecordId, } = useKanban() as Required; const { copyRecordUrl, viewRecordHistory, addRecordComment } = useContextMenu(); + const expandRecord = useExpandRecord(); const { cardCreatable, cardDeletable, cardEditable, cardCommentCreatable } = permission; const { id: fieldId } = stackField; @@ -67,8 +68,8 @@ export const KanbanCard = (props: IKanbanCardProps) => { ); }, [card, primaryField, t]); - const onExpand = () => { - setExpandRecordId(card.id); + const onExpand = async () => { + await expandRecord(card.id); }; const onDelete = () => { @@ -100,7 +101,7 @@ export const KanbanCard = (props: IKanbanCardProps) => { const record = res.data.records[0]; if (record != null) { - setExpandRecordId(record.id); + await expandRecord(record.id); } }; @@ -109,12 +110,10 @@ export const KanbanCard = (props: IKanbanCardProps) => { }; const onViewRecordHistory = async () => { - setExpandRecordId(card.id); await viewRecordHistory(card.id); }; const onAddRecordComment = async () => { - setExpandRecordId(card.id); await addRecordComment(card.id); }; diff --git a/apps/nextjs-app/src/features/app/blocks/view/kanban/context/KanbanContext.ts b/apps/nextjs-app/src/features/app/blocks/view/kanban/context/KanbanContext.ts index 6e2d062610..a4b4f17a73 100644 --- a/apps/nextjs-app/src/features/app/blocks/view/kanban/context/KanbanContext.ts +++ b/apps/nextjs-app/src/features/app/blocks/view/kanban/context/KanbanContext.ts @@ -1,6 +1,5 @@ import type { IGetRecordsRo } from '@teable/openapi'; import type { AttachmentField, IFieldInstance } from '@teable/sdk/model'; -import type { Dispatch, SetStateAction } from 'react'; import { createContext } from 'react'; import type { IKanbanPermission, IStackData } from '../type'; @@ -15,7 +14,6 @@ export interface IKanbanContext { permission: IKanbanPermission; primaryField: IFieldInstance; displayFields: IFieldInstance[]; - setExpandRecordId: Dispatch>; } export const KanbanContext = createContext(null!); diff --git a/apps/nextjs-app/src/features/app/blocks/view/kanban/context/KanbanProvider.tsx b/apps/nextjs-app/src/features/app/blocks/view/kanban/context/KanbanProvider.tsx index 3072b7d4c4..8ccfbfb2f5 100644 --- a/apps/nextjs-app/src/features/app/blocks/view/kanban/context/KanbanProvider.tsx +++ b/apps/nextjs-app/src/features/app/blocks/view/kanban/context/KanbanProvider.tsx @@ -270,7 +270,6 @@ export const KanbanProvider = ({ children }: { children: ReactNode }) => { primaryField, displayFields, stackCollection, - setExpandRecordId, }; }, [ recordQuery, @@ -283,7 +282,6 @@ export const KanbanProvider = ({ children }: { children: ReactNode }) => { primaryField, displayFields, stackCollection, - setExpandRecordId, ]); const onClose = () => { From 0a09af72c010e7d3f6d820a29987c1ba7f781148 Mon Sep 17 00:00:00 2001 From: Nicolas Wormser Date: Fri, 7 Aug 2026 19:48:24 +0200 Subject: [PATCH 2/2] test(app): cover useExpandRecord url push --- .../blocks/view/hooks/useExpandRecord.spec.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 apps/nextjs-app/src/features/app/blocks/view/hooks/useExpandRecord.spec.ts diff --git a/apps/nextjs-app/src/features/app/blocks/view/hooks/useExpandRecord.spec.ts b/apps/nextjs-app/src/features/app/blocks/view/hooks/useExpandRecord.spec.ts new file mode 100644 index 0000000000..ca0acd65cb --- /dev/null +++ b/apps/nextjs-app/src/features/app/blocks/view/hooks/useExpandRecord.spec.ts @@ -0,0 +1,31 @@ +import { renderHook } from '@testing-library/react'; +import { useExpandRecord } from './useExpandRecord'; + +const push = vi.fn(); + +vi.mock('next/router', () => ({ + useRouter: () => ({ + pathname: '/base/[baseId]/[tableId]/[viewId]', + query: { baseId: 'bse1', tableId: 'tbl1', viewId: 'viw1' }, + push, + }), +})); + +describe('useExpandRecord', () => { + beforeEach(() => push.mockReset()); + + it('puts the record id in the url so the expanded record stays linkable', async () => { + const { result } = renderHook(() => useExpandRecord()); + + await result.current('rec1'); + + expect(push).toHaveBeenCalledWith( + { + pathname: '/base/[baseId]/[tableId]/[viewId]', + query: { baseId: 'bse1', tableId: 'tbl1', viewId: 'viw1', recordId: 'rec1' }, + }, + undefined, + { shallow: true } + ); + }); +});