fix(web): refresh removed owner state on resubmission

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-10-19 16:27:16 +08:00
parent 1826fa6768
commit 87d922896b
3 changed files with 10 additions and 9 deletions

View file

@ -5,11 +5,11 @@
## 2. 服务端与界面
- [ ] 统一当前所有者读取与成员维护权限,保留管理员独立授权。
- [ ] 修正移除后写入口及旧旁路,补齐所有者列表、详情和受控下载读取。
- [ ] 保留已有审核申请与现有账号停用/恢复行为。
- [ ] 管理页面明确只读状态;如有契约变更,同步 OpenAPI 与生成类型。
- [ ] 更新现有领域模型和用户文档中过时的成员移除/所有者权限说明,不新增转让或接管功能。
- [x] 统一当前所有者读取与成员维护权限,保留管理员独立授权。
- [x] 修正移除后写入口及旧旁路,补齐所有者列表、详情和受控下载读取。
- [x] 保留已有审核申请与现有账号停用/恢复行为。
- [x] 管理页面明确只读状态;如有契约变更,同步 OpenAPI 与生成类型。
- [x] 更新现有领域模型和用户文档中过时的成员移除/所有者权限说明,不新增转让或接管功能。
## 3. 验证和交付

View file

@ -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<typeof import('@/api/client')>('@/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<typeof import('react-i18next')>('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 }) => <button onClick={() => onFileSelect(new File(['zip'], 'skill.zip'))}>choose package</button> }))
@ -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))

View file

@ -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.