From 2640e6e710af91587de68e31c34a0f7eee2bde49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=91=A8=E6=B6=9B?= Date: Sun, 19 Jul 2026 09:56:02 -0700 Subject: [PATCH 1/2] fix(plugin-gantt): temporarily disable the mobile QR context-menu item MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comment out the 移动端二维码 item in the gantt row context menu. While it is off, taskUrl alone no longer justifies opening the menu (it would be an empty popover). The taskUrl prop, QR dialog code and the original tests stay in place (commented / describe.skip) behind TODO(qr-menu) markers so the feature can be restored by uncommenting. Co-Authored-By: Claude Fable 5 --- .changeset/gantt-hide-qr-context-menu.md | 11 ++++++++ .../src/GanttView.enhancements.test.tsx | 25 ++++++++++++++++++- packages/plugin-gantt/src/GanttView.tsx | 20 +++++++++++---- 3 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 .changeset/gantt-hide-qr-context-menu.md diff --git a/.changeset/gantt-hide-qr-context-menu.md b/.changeset/gantt-hide-qr-context-menu.md new file mode 100644 index 000000000..7b6e3cdde --- /dev/null +++ b/.changeset/gantt-hide-qr-context-menu.md @@ -0,0 +1,11 @@ +--- +"@object-ui/plugin-gantt": patch +--- + +fix(plugin-gantt): temporarily disable the 移动端二维码 (mobile QR share) context-menu item + +The QR item in the gantt row context menu is commented out for now: right-click +no longer offers "Mobile QR code", and `taskUrl` alone no longer opens a menu +(it would be empty without the QR item). The `taskUrl` prop, the QR dialog code, +and the original tests are kept in place (commented / skipped) so the feature +can be restored by uncommenting the marked TODO(qr-menu) blocks. diff --git a/packages/plugin-gantt/src/GanttView.enhancements.test.tsx b/packages/plugin-gantt/src/GanttView.enhancements.test.tsx index 9ab3bce75..045a6b8e9 100644 --- a/packages/plugin-gantt/src/GanttView.enhancements.test.tsx +++ b/packages/plugin-gantt/src/GanttView.enhancements.test.tsx @@ -201,7 +201,30 @@ describe('GanttView locked-row tooltip hint (无编辑权限)', () => { }); }); -describe('GanttView 移动端二维码 (context-menu QR share)', () => { +// TODO(qr-menu): the 移动端二维码 context-menu item is temporarily disabled in +// GanttView.tsx (commented out). While it is off, the QR item must never render +// and taskUrl alone must not open the menu — asserted by the active describe +// below. Un-skip this block (and delete the "disabled" one) when restoring. +describe('GanttView 移动端二维码 (temporarily disabled)', () => { + const URL_A = 'https://app.example.com/app/x/rec/record/a'; + const ctxMenu = () => document.querySelector('[data-testid="gantt-context-menu"]'); + const qrItem = () => document.querySelector('[data-testid="gantt-context-menu-qrcode"]'); + + it('renders no QR item even when taskUrl yields a URL', () => { + const { container } = renderView([A()], { taskUrl: () => URL_A, onTaskClick: vi.fn() }); + fireEvent.contextMenu(container.querySelector('[data-testid="gantt-task-bar-a"]')!, { clientX: 10, clientY: 10 }); + expect(ctxMenu()).toBeTruthy(); + expect(qrItem()).toBeNull(); + }); + + it('opens no menu at all when taskUrl is the only would-be action', () => { + const { container } = renderView([A()], { taskUrl: () => URL_A }); + fireEvent.contextMenu(container.querySelector('[data-testid="gantt-task-bar-a"]')!, { clientX: 10, clientY: 10 }); + expect(ctxMenu()).toBeNull(); + }); +}); + +describe.skip('GanttView 移动端二维码 (context-menu QR share)', () => { const URL_A = 'https://app.example.com/app/x/rec/record/a'; const ctxMenu = () => document.querySelector('[data-testid="gantt-context-menu"]'); const qrItem = () => document.querySelector('[data-testid="gantt-context-menu-qrcode"]'); diff --git a/packages/plugin-gantt/src/GanttView.tsx b/packages/plugin-gantt/src/GanttView.tsx index 9d00766ac..4a9cc45c0 100644 --- a/packages/plugin-gantt/src/GanttView.tsx +++ b/packages/plugin-gantt/src/GanttView.tsx @@ -660,7 +660,8 @@ export function GanttView({ endDate: endDateProp, markers: markersProp, onTaskClick, - taskUrl, + // TODO(qr-menu): re-enable together with the 移动端二维码 context-menu item below. + // taskUrl, onTaskUpdate: onTaskUpdateProp, onTaskDelete: onTaskDeleteProp, onViewChange, @@ -1535,11 +1536,14 @@ export function GanttView({ e.preventDefault(); e.stopPropagation(); setSelectedTaskId(task.id); - // 移动端二维码 alone justifies a menu — but only when this row actually - // yields a URL (synthetic group rows return null), so no empty menu opens. - if (!hasTaskMenuActions && !(taskUrl && taskUrl(task))) return; + // TODO(qr-menu): the 移动端二维码 menu item is temporarily disabled (see the + // commented-out block in the context menu below). While it is off, taskUrl + // alone must NOT justify opening the menu — it would render an empty + // popover. Restore the `taskUrl && taskUrl(task)` escape hatch together + // with the menu item. + if (!hasTaskMenuActions) return; setCtxMenu({ x: e.clientX, y: e.clientY, taskId: task.id }); - }, [hasTaskMenuActions, taskUrl]); + }, [hasTaskMenuActions]); const openLinkContextMenu = React.useCallback( (sourceId: string | number, targetId: string | number, type: GanttLinkType, e: React.MouseEvent) => { @@ -4708,6 +4712,11 @@ export function GanttView({ {t('gantt.menu.view')} )} + {/* TODO(qr-menu): 移动端二维码 menu item temporarily disabled. To restore, + uncomment this block, re-enable the `taskUrl` prop destructuring above, + and put the `taskUrl && taskUrl(task)` escape hatch back into + openContextMenu (plus un-skip the QR tests in + GanttView.enhancements.test.tsx). {taskUrl && (() => { const url = taskUrl(task); if (!url) return null; @@ -4723,6 +4732,7 @@ export function GanttView({ ); })()} + */} {inlineEdit && onTaskUpdate && row && !row.isSummary && !task.locked && ( )} - {/* TODO(qr-menu): 移动端二维码 menu item temporarily disabled. To restore, - uncomment this block, re-enable the `taskUrl` prop destructuring above, - and put the `taskUrl && taskUrl(task)` escape hatch back into - openContextMenu (plus un-skip the QR tests in - GanttView.enhancements.test.tsx). - {taskUrl && (() => { - const url = taskUrl(task); - if (!url) return null; - return ( - - ); - })()} - */} {inlineEdit && onTaskUpdate && row && !row.isSummary && !task.locked && ( - - - - - )} - {/* Dependency link context menu (类型选择 + 移除) — fixed-position. */} {linkCtxMenu && (() => { const source = tasks.find((tk) => String(tk.id) === String(linkCtxMenu.sourceId)); diff --git a/packages/plugin-gantt/src/ObjectGantt.persistfilters.test.tsx b/packages/plugin-gantt/src/ObjectGantt.persistfilters.test.tsx index 0ad68302a..074836c64 100644 --- a/packages/plugin-gantt/src/ObjectGantt.persistfilters.test.tsx +++ b/packages/plugin-gantt/src/ObjectGantt.persistfilters.test.tsx @@ -1,8 +1,7 @@ /** * ObjectGantt-side tests for the #2460 batch: quick-filter persistence riding * on 保存布局 (sibling localStorage key + restore on mount), per-level tooltip - * fields skipping empty values (悬浮分层字段), and the taskUrl passed down for - * the 移动端二维码 item (null for synthetic rows without an objectField value). + * fields skipping empty values (悬浮分层字段). * * GanttView is mocked to a thin shell exposing what ObjectGantt feeds it. */ @@ -12,14 +11,13 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { ObjectGantt } from './ObjectGantt'; vi.mock('./GanttView', () => ({ - GanttView: ({ tasks, onLayoutChange, taskUrl }: any) => ( + GanttView: ({ tasks, onLayoutChange }: any) => (
t.id).join(',')}> {tasks.map((t: any) => (
{(t.fields ?? []).map((f: any) => `${f.label}=${f.value}`).join('|')} - {taskUrl ? String(taskUrl(t)) : 'no-fn'}
))} {onLayoutChange && ( @@ -110,15 +108,3 @@ describe('ObjectGantt tooltip fields skip empty values (悬浮分层字段)', () }); }); -describe('ObjectGantt taskUrl for 移动端二维码', () => { - it('returns null for synthetic rows when objectField is configured but empty', async () => { - const items = [ - { id: 'grp', name: '项目组', start: '2024-01-01', end: '2024-02-10', object_name: '' }, - { id: 'r1', name: '计划', start: '2024-01-01', end: '2024-01-05', object_name: 'plan_obj' }, - ]; - const s = schema({ data: { provider: 'value', items }, quickFilters: undefined, objectField: 'object_name' }); - const { container, getByTestId } = render(); - await waitFor(() => expect(gv(container).getAttribute('data-count')).toBe('2')); - expect(getByTestId('gv-url-grp').textContent).toBe('null'); - }); -}); diff --git a/packages/plugin-gantt/src/ObjectGantt.tsx b/packages/plugin-gantt/src/ObjectGantt.tsx index 09d6eb2a8..6617ff664 100644 --- a/packages/plugin-gantt/src/ObjectGantt.tsx +++ b/packages/plugin-gantt/src/ObjectGantt.tsx @@ -1019,11 +1019,10 @@ export const ObjectGantt: React.FC = ({ // row opened under the bound object's route otherwise builds a 404 URL. // deriveRecordPageHref needs the routed object's segment in the current path // (a foreign row object never appears there), so derive from the routed - // object and swap the segment. Shared by the drawer's 整页 link and the - // context menu's 移动端二维码. + // object and swap the segment. Used by the drawer's 整页 link. // With objectField configured, a row without a value is a synthetic group // header composed by the endpoint (its id isn't a real record id) — no - // detail page, drawer or QR link exists for it. + // detail page or drawer exists for it. const isSyntheticRow = useCallback( (rec: Record | undefined): boolean => !!ganttConfig?.objectField && !String(rec?.[ganttConfig.objectField] ?? '').trim(), @@ -1411,15 +1410,6 @@ export const ObjectGantt: React.FC = ({ } onTaskClick?.(task.data); }} - taskUrl={(task) => { - const rec = task.data as Record | undefined; - if (!rec || isSyntheticRow(rec)) return null; - const href = recordDetailHref(rec)?.href; - if (!href) return null; - // Absolute URL: the QR is scanned on another device, so a - // path-relative href would be useless there. - return typeof window === 'undefined' ? href : new URL(href, window.location.href).toString(); - }} onTaskUpdate={handleTaskUpdateDefault} onTaskDelete={requestDelete} onDependencyCreate={ganttConfig?.dependenciesField ? handleDependencyCreate : undefined} diff --git a/packages/plugin-gantt/src/useGanttTranslation.ts b/packages/plugin-gantt/src/useGanttTranslation.ts index ec9db7565..17638863e 100644 --- a/packages/plugin-gantt/src/useGanttTranslation.ts +++ b/packages/plugin-gantt/src/useGanttTranslation.ts @@ -50,12 +50,6 @@ export const GANTT_DEFAULT_TRANSLATIONS: Record = { 'gantt.aria.taskList': 'Task list', 'gantt.tooltip.days': 'd', 'gantt.menu.view': 'View details', - 'gantt.menu.qrcode': 'Mobile QR code', - 'gantt.qr.title': 'Open on mobile', - 'gantt.qr.hint': 'Scan with a phone to open the details', - 'gantt.qr.copy': 'Copy link', - 'gantt.qr.copied': 'Copied', - 'gantt.qr.close': 'Close', 'gantt.menu.edit': 'Edit inline', 'gantt.menu.delete': 'Delete', 'gantt.menu.addPredecessor': 'Add predecessor…', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64540ddb5..14a2f3c9e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1898,9 +1898,6 @@ importers: lucide-react: specifier: ^1.24.0 version: 1.24.0(react@19.2.7) - qrcode: - specifier: ^1.5.4 - version: 1.5.4 react: specifier: 19.2.7 version: 19.2.7 @@ -1911,9 +1908,6 @@ importers: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) devDependencies: - '@types/qrcode': - specifier: ^1.5.6 - version: 1.5.6 '@types/react': specifier: 19.2.17 version: 19.2.17 @@ -7899,8 +7893,8 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - isbot@5.2.1: - resolution: {integrity: sha512-dJ+LpKyClQZ7NG+j3OensC/mAZkGpukE9YUrgPYvAZj2doVL0edfDgywTUh5CXa0o+nW9a1V9e5+CJTX8+SxRw==} + isbot@5.2.0: + resolution: {integrity: sha512-gbZiGCb4B5xaoxg9mS7koAyRdvJnArk10VLSHOgz6rtBG93/pi1xOFaVvXMKZ7JXgyZ8zAbNRK5uIBdIUTFSqw==} engines: {node: '>=18'} isexe@2.0.0: @@ -13358,7 +13352,7 @@ snapshots: '@tanstack/history': 1.162.0 '@tanstack/react-store': 0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tanstack/router-core': 1.171.6 - isbot: 5.2.1 + isbot: 5.2.0 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optional: true @@ -16282,7 +16276,7 @@ snapshots: isarray@1.0.0: {} - isbot@5.2.1: + isbot@5.2.0: optional: true isexe@2.0.0: {}