From 87d922896bfb49dac69a2f13419c587c02be0236 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Mon, 19 Oct 2026 16:27:16 +0800 Subject: [PATCH] fix(web): refresh removed owner state on resubmission Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- .../changes/clarify-skill-ownership-transfer/tasks.md | 10 +++++----- .../pages/dashboard/publish-owner-permissions.test.tsx | 7 ++++--- web/src/pages/dashboard/publish.tsx | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/openspec/changes/clarify-skill-ownership-transfer/tasks.md b/openspec/changes/clarify-skill-ownership-transfer/tasks.md index deb681a3a..e47868790 100644 --- a/openspec/changes/clarify-skill-ownership-transfer/tasks.md +++ b/openspec/changes/clarify-skill-ownership-transfer/tasks.md @@ -5,11 +5,11 @@ ## 2. 服务端与界面 -- [ ] 统一当前所有者读取与成员维护权限,保留管理员独立授权。 -- [ ] 修正移除后写入口及旧旁路,补齐所有者列表、详情和受控下载读取。 -- [ ] 保留已有审核申请与现有账号停用/恢复行为。 -- [ ] 管理页面明确只读状态;如有契约变更,同步 OpenAPI 与生成类型。 -- [ ] 更新现有领域模型和用户文档中过时的成员移除/所有者权限说明,不新增转让或接管功能。 +- [x] 统一当前所有者读取与成员维护权限,保留管理员独立授权。 +- [x] 修正移除后写入口及旧旁路,补齐所有者列表、详情和受控下载读取。 +- [x] 保留已有审核申请与现有账号停用/恢复行为。 +- [x] 管理页面明确只读状态;如有契约变更,同步 OpenAPI 与生成类型。 +- [x] 更新现有领域模型和用户文档中过时的成员移除/所有者权限说明,不新增转让或接管功能。 ## 3. 验证和交付 diff --git a/web/src/pages/dashboard/publish-owner-permissions.test.tsx b/web/src/pages/dashboard/publish-owner-permissions.test.tsx index cbd239758..9ec0b673e 100644 --- a/web/src/pages/dashboard/publish-owner-permissions.test.tsx +++ b/web/src/pages/dashboard/publish-owner-permissions.test.tsx @@ -3,7 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' -const api = vi.hoisted(() => ({ detail: vi.fn(), publish: vi.fn(), navigate: vi.fn() })) +const api = vi.hoisted(() => ({ detail: vi.fn(), publish: vi.fn(), navigate: vi.fn(), search: {} as { namespace?: string; updateSkill?: string; resubmitSkill?: string; resubmitVersion?: string } })) vi.mock('@/api/client', async () => ({ ...await vi.importActual('@/api/client'), fetchJson: api.detail, @@ -14,7 +14,7 @@ vi.mock('@/shared/hooks/use-skill-queries', async () => ({ previewSkillPackage: vi.fn().mockResolvedValue({ name: 'foo', displayName: 'Foo', version: '1.1' }), usePublishSkill: () => ({ mutateAsync: api.publish, isPending: false }), })) -vi.mock('@tanstack/react-router', () => ({ useNavigate: () => api.navigate, useSearch: () => ({ namespace: 'team', updateSkill: 'foo' }) })) +vi.mock('@tanstack/react-router', () => ({ useNavigate: () => api.navigate, useSearch: () => api.search })) vi.mock('react-i18next', async () => ({ ...await vi.importActual('react-i18next'), useTranslation: () => ({ t: (key: string) => key }) })) vi.mock('@/shared/hooks/use-namespace-queries', () => ({ useMyNamespaces: () => ({ data: [{ slug: 'team', displayName: 'Team' }], isLoading: false }) })) vi.mock('@/features/publish/upload-zone', () => ({ UploadZone: ({ onFileSelect }: { onFileSelect: (file: File) => void }) => })) @@ -27,8 +27,9 @@ import { toast } from '@/shared/lib/toast' afterEach(() => { cleanup(); vi.clearAllMocks() }) -describe('package rejection refreshes current update permissions', () => { +describe.each(['update', 'resubmit'])('%s package rejection refreshes current permissions', mode => { it.each([true, false])('uses the server ownerReadOnly=%s response after a rejected package', async ownerReadOnly => { + api.search = mode === 'update' ? { namespace: 'team', updateSkill: 'foo' } : { namespace: 'team', resubmitSkill: 'foo', resubmitVersion: '1.0' } const client = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } } }) api.detail.mockResolvedValueOnce({ ownerReadOnly: false }).mockResolvedValue({ ownerReadOnly }) api.publish.mockRejectedValue(new ApiError('Localized package rejection', 400)) diff --git a/web/src/pages/dashboard/publish.tsx b/web/src/pages/dashboard/publish.tsx index f8cbd1c85..f54249f19 100644 --- a/web/src/pages/dashboard/publish.tsx +++ b/web/src/pages/dashboard/publish.tsx @@ -170,7 +170,7 @@ export function PublishPage() { } catch (error) { // A package rejection may also reflect changed membership. Re-read the // server state instead of inferring permissions from its localized message. - if (isUpdate && !avatarOnly && error instanceof ApiError && error.status === 400) { + if (updateSlug && !avatarOnly && error instanceof ApiError && error.status === 400) { await queryClient.invalidateQueries({ queryKey: getSkillDetailQueryKey(namespaceSlug, updateSlug ?? '') }) } // Avatar-only updates call the API directly rather than a mutation.