fix(web): sync parent user directory after admin account changes
AdminSettingsModal previously left onRefreshUsers as a dead prop, so newly created users never reached App allUsers/localStorage and Create Project member candidates stayed stale (UI-01). Wire syncAdminUserDirectories, surface refresh errors, and document the residual cross-device directory gap. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: "管理员开通账号后立项成员候选缺失(UI-01)"
|
||||
status: done
|
||||
created: 2026-09-13
|
||||
updated: 2026-09-13
|
||||
severity: high
|
||||
---
|
||||
|
||||
# 管理员开通账号后立项成员候选缺失(UI-01)
|
||||
|
||||
## 现象
|
||||
|
||||
Antigravity 真实 UI:UI-00 管理员入口已 PASS。UI-01 产品失败——管理员经 UI 成功创建账号 B,但 tech-lead A 打开「新建项目」时成员候选列表中无 B。
|
||||
|
||||
## 根因分析
|
||||
|
||||
1. `AdminSettingsModalProps` 已声明 `onRefreshUsers`;`App` 已传入 `getAdminUsers → setAllUsers`。
|
||||
2. 组件 **destructuring 漏掉** `onRefreshUsers`,成为死 prop。
|
||||
3. 模态内 `refreshUsers()` 仅 `setAdminUsers`,创建/角色/状态成功后从未同步父级 `allUsers`。
|
||||
4. `CreateProjectModal` / `ProjectMembersModal` 的 `availableUsers` 来自 App `allUsers`(并经 `saveUsers` 写入 localStorage),故同会话内立项候选仍是旧目录。
|
||||
5. 附带:App 原 `onRefreshUsers` 用 `.catch(() => {})` 静默吞错,即使接线也会掩盖同步失败。
|
||||
|
||||
## 严重度
|
||||
|
||||
`high`(同浏览器管理员开通后立项立刻缺人;阻断 UI-01)
|
||||
|
||||
## 本轮修复(本任务范围内)
|
||||
|
||||
- 抽出可测 helper:`web/src/lib/adminUserDirectorySync.ts`(缺 `onRefreshUsers` / 父级抛错均 fail-closed)
|
||||
- `AdminSettingsModal` 正确 destructure;创建/角色/状态/审批/重置后 `await refreshUsers()`;同步失败对用户可见
|
||||
- `App` `onRefreshUsers` 改为 async 上抛,禁止静默
|
||||
|
||||
## 残留审计风险(本任务不越权补齐)
|
||||
|
||||
| 风险 ID | 说明 | 为何本轮不修 |
|
||||
|---------|------|--------------|
|
||||
| **DOCREV-RT-USER-DIR-CROSS-DEVICE** | 立项成员目录仍以浏览器 `allUsers`/localStorage 为事实源。tech-lead 在**新设备 / 清缓存 / 从未在本机跑过管理员同步**的会话中,即使服务端已有 B,本地候选仍可能只有 MOCK/`loadUsers` 缓存。非管理员无 `GET /api/v1/admin/users` 权限。 | 安全补齐需**非 admin 的成员目录/搜索 API**(最小权限、按项目或组织范围),属授权面扩展;禁止把 admin users API 下放给 tech-lead。 |
|
||||
|
||||
相关代码锚点:`App.tsx` `availableUsers={allUsers}`;`storage.loadUsers`/`saveUsers`;服务端仅 `GET /api/v1/admin/users`。
|
||||
|
||||
## 关联整改
|
||||
|
||||
- Short-term: [../ops/short-term/2026-09-13-document-revision-runtime.md](../ops/short-term/2026-09-13-document-revision-runtime.md)
|
||||
- Checklist: [../ops/checklists/2026-09-13-document-revision-runtime-ui-acceptance.md](../ops/checklists/2026-09-13-document-revision-runtime-ui-acceptance.md)
|
||||
- 前序 auth 映射: [2026-09-13-auth-useraccount-snake-case-mapping.md](./2026-09-13-auth-useraccount-snake-case-mapping.md)
|
||||
|
||||
## 证据
|
||||
|
||||
- 实战:UI-00 PASS;UI-01 创建 B 后 A 立项候选无 B
|
||||
- 代码:`AdminSettingsModal` 原未解构 `onRefreshUsers`;App 静默 catch
|
||||
- 单测:`web/src/lib/adminUserDirectorySync.test.ts`(死 prop / 父级错误上浮)
|
||||
|
||||
## 开放问题
|
||||
|
||||
- 跨设备成员目录 API(见上表)→ 记入 short-term blocker,勿用 admin API workaround
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
# 当前焦点
|
||||
|
||||
**更新**: 2026-09-13
|
||||
**production_ready**: no(DOCREV 集成槽 e2e;UI-00 暴露 auth snake_case 映射缺陷已在 fix 分支修复,待重跑真实 UI)
|
||||
**production_ready**: no(DOCREV 集成槽 e2e;UI-00 PASS;UI-01 目录死 prop 已在 fix 分支修复,待重跑真实 UI;跨设备成员目录仍为 residual blocker)
|
||||
|
||||
## 焦点
|
||||
|
||||
1. **DOCREV-RT**: [运行时整改收口](short-term/2026-09-13-document-revision-runtime.md)(`fix/document-revision-runtime`:上传注册 + auth UserAccount 映射;占槽真实 UI E2E;不 push)
|
||||
1. **DOCREV-RT**: [运行时整改收口](short-term/2026-09-13-document-revision-runtime.md)(`fix/document-revision-runtime`:上传注册 + auth 映射 + 管理员用户目录同步;占槽真实 UI E2E;不 push)
|
||||
2. **CHAT**: [刷新恢复整改已实施](short-term/2026-09-13-chat-history-refresh-remediation.md)(待合 dev + PI E2E 补录)
|
||||
3. **CI**: `TEST_DATABASE_URL` + 真实 PI E2E 实录;INTENT 发布 blocker 见 conversation-intent short-term
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ updated: 2026-09-13
|
||||
| Agent | 真实 `pi-agent` + 运行中的 `MiniMax-M2.7-highspeed` 容器(禁止 stub) |
|
||||
| 账号 | ≥2 名项目成员(编制人 A、协作者 B);禁止共用 token 串号;**管理员凭据仅来自 `ADMIN_USERNAME`/`ADMIN_PASSWORD` 环境变量(禁止 spec 硬编码)** |
|
||||
| 认证契约 | 真实 login/me 返回 snake_case `role_id` 等;前端须映射为 `roleId`,否则管理员入口隐藏(UI-00 已踩坑,见 audit) |
|
||||
| 用户目录 | 管理员开通 A/B 后,同浏览器立项成员候选须含新账号(`onRefreshUsers` 不得死 prop);跨设备缺口见 [../../audit/2026-09-13-admin-user-directory-sync-ui01.md](../../audit/2026-09-13-admin-user-directory-sync-ui01.md) |
|
||||
| 证据根 | `.doc/test-reports/assets/2026-09-13-document-revision-runtime-ui/` |
|
||||
| 报告 | `.doc/test-reports/YYYY-MM-DD-document-revision-runtime-ui-test-report.md` |
|
||||
|
||||
@@ -87,7 +88,7 @@ RUN_UI_ACCEPTANCE=1 BASE_URL=http://127.0.0.1:8080 \
|
||||
|
||||
| ID | 步骤(仅 UI) | 期望页面现象 | 证据 | 判定 |
|
||||
|----|---------------|--------------|------|------|
|
||||
| UI-01 | 登录 → **页面创建**项目(或进入空项目后创建) | 项目工作台三栏可见;URL/标题含新项目 | 截图 `01-project.png` | |
|
||||
| UI-01 | 管理员开通 B 后,tech-lead A **页面创建**项目并勾选成员 | 立项成员候选含 B;项目工作台三栏可见 | 截图 `01-project.png` | |
|
||||
| UI-02 | 对话输入框发起真实任务(触发真实小队编排) | 助手流式回复;任务进度/文档 busy 可见;网络可见真实 agent 请求 | 截图 + trace | |
|
||||
| UI-03 | 等待小队**首轮**完成,打开生成的受控文档 Tab | 右侧出现文档;版本工具条 `Vn`/`r*`(非「账本未就绪」) | 截图 `03-first-doc.png` | |
|
||||
| UI-04 | 打开变更报告:工具条「变更报告」/「待确认报告」或版本历史「报告」 | **首轮**可见:摘要、**风险条目**、项目影响内容、各文档影响原因/建议、开放问题;加载失败显式错误非永久加载中;`fallback` 不得宣称为完整 PI | 截图 `04-first-report.png` | |
|
||||
|
||||
@@ -64,6 +64,8 @@ cd web && npm test && npm run build
|
||||
|
||||
本轮门禁(2026-09-13 10:11 UTC 独立复跑):`go vet ./...` PASS;`go test -short ./... -count=1` PASS(含 Materialize 首/二轮 change_report_id、PATCH 顺序、Materialize 失败透出、Assert 先职责后租约、SaveDocumentContent 审计、pending 事实源不越权、同步/异步待确认文案、完成活动仅 committed、pending 活动标明待确认、MarkProposalApplied 失败显式可重试);`web npm test` 199+27 PASS;`npm run build` PASS。使用 dev PostgreSQL(schema 28)执行 `TestPGPersistSurvivesNewServiceInstance` PASS,证明新 Service 实例可恢复账本。上述均为工程门禁,不能替代 UI 验收。
|
||||
|
||||
UI-01 目录同步补丁门禁(2026-09-13):`node --test adminUserDirectorySync.test.ts` 3 PASS;`web npm test` 219+27 PASS;`npm run build` PASS;`go vet ./...` + `go test -short ./...` PASS。未提交 `web/e2e/*` 与 `test-reports/assets/`。
|
||||
|
||||
复核补强(同日):
|
||||
- A:`MaterializeAgentOutput` 错误写入 `TaskRunResult.Success=false` + `ErrorReason`,不再空白忽略
|
||||
- B:cand 非空且 report invalid/unassessed 时禁止自动提交(候选标 needs_confirmation)
|
||||
@@ -79,16 +81,18 @@ cd web && npm test && npm run build
|
||||
- L:**Playwright 骨架**:`web` 增加 `@playwright/test` + `playwright.config.ts` + `e2e/document-revision-runtime.spec.ts`(`RUN_UI_ACCEPTANCE=1` 才跑;无 mock/route/API/DB fixture)
|
||||
- M:**上传注册事实源**:发送前 `await registerUploadAttachments` → `POST /api/v1/freeform-documents`(`client_key` 幂等);失败 UI 显式错误且不发消息、不生成本地 `doc-upload` orphan;成功使用 `doc-free-*` 写入 deliverable/fileStore 并挂 toolbar
|
||||
- N:**Auth 契约映射(UI-00 实战)**:Go `UserAccount` 为 snake_case;前端在 API 边界 `mapUserAccount`/`mapLoginResult`/`mapMeResult` 统一转 camelCase(login/me/admin users/create/role/status);`BootstrapAdminFromConfig` 的 `UpdateUserRole` 错误不再静默。详见 [../../audit/2026-09-13-auth-useraccount-snake-case-mapping.md](../../audit/2026-09-13-auth-useraccount-snake-case-mapping.md)
|
||||
- O:**全局用户目录同步(UI-01 实战)**:管理员创建/角色/状态成功后必须经 `syncAdminUserDirectories` 调用父级 `onRefreshUsers`(禁止死 prop / 静默 catch);同浏览器立项候选可立即看到新账号。跨设备/清缓存缺口见 audit 残留风险,**不**下放 admin users API。详见 [../../audit/2026-09-13-admin-user-directory-sync-ui01.md](../../audit/2026-09-13-admin-user-directory-sync-ui01.md)
|
||||
|
||||
### 用户 UI 实战验收
|
||||
|
||||
见扩展 checklist(UI-01…UI-16)。工具条已加 `revisionRefreshKey`(保存/hydrate 后刷新 r)与「变更报告」入口(`change_report_id`)。**骨架已就绪,本轮未执行真实 UI 实录**(需 `RUN_UI_ACCEPTANCE=1` + 占槽)。
|
||||
见扩展 checklist(UI-01…UI-16)。工具条已加 `revisionRefreshKey`(保存/hydrate 后刷新 r)与「变更报告」入口(`change_report_id`)。**骨架已就绪**;Antigravity:UI-00 真实 PASS;UI-01 曾因目录死 prop 产品失败(本轮工程修复,待重跑 UI)。
|
||||
|
||||
### Blockers
|
||||
|
||||
- **DOCREV-RT-PI-NARRATIVE**:真实 PI 业务叙事摘要/风险解释(确定性 fallback 已可见,不得宣称为完整 PI)
|
||||
- **DOCREV-RT-UPLOAD-ORPHAN**:仅 IndexedDB 上传、无服务端正文 → 禁止假 V1
|
||||
- **DOCREV-RT-UI-E2E**:需占集成槽 + 真实小队 + `npm run test:ui-acceptance` 无头 Playwright 实录
|
||||
- **DOCREV-RT-USER-DIR-CROSS-DEVICE**:立项 `availableUsers` 仍依赖本机 `allUsers`/localStorage;非 admin 无成员目录 API;跨设备/新浏览器可能仍缺 B(见 [../../audit/2026-09-13-admin-user-directory-sync-ui01.md](../../audit/2026-09-13-admin-user-directory-sync-ui01.md))
|
||||
|
||||
### Commits(历史)
|
||||
|
||||
|
||||
+3
-7
@@ -4126,13 +4126,9 @@ export default function App() {
|
||||
}}
|
||||
systemConfig={systemAuthConfig}
|
||||
onUpdateSystemConfig={(cfg) => setSystemAuthConfig((prev) => ({ ...prev, ...cfg }))}
|
||||
onRefreshUsers={() => {
|
||||
authClient
|
||||
.getAdminUsers()
|
||||
.then((users) => {
|
||||
setAllUsers(users);
|
||||
})
|
||||
.catch(() => {});
|
||||
onRefreshUsers={async () => {
|
||||
const users = await authClient.getAdminUsers();
|
||||
setAllUsers(users);
|
||||
}}
|
||||
env={systemEnv}
|
||||
/>
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
WorkflowDefinitionSummary,
|
||||
WorkflowVersionSummary,
|
||||
} from '../../api/workflowAdmin';
|
||||
import { syncAdminUserDirectories } from '../../lib/adminUserDirectorySync';
|
||||
|
||||
export interface AdminSettingsModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -38,7 +39,7 @@ export interface AdminSettingsModalProps {
|
||||
onSaveRoles: (roles: RoleDefinition[]) => void;
|
||||
allUsers: UserProfile[];
|
||||
onUpdateUserRole: (userId: number, roleId: string) => void;
|
||||
onRefreshUsers?: () => void;
|
||||
onRefreshUsers: () => void | Promise<void>;
|
||||
systemConfig: SystemAuthConfig;
|
||||
onUpdateSystemConfig: (config: Partial<SystemAuthConfig>) => void;
|
||||
env?: string;
|
||||
@@ -102,6 +103,7 @@ export const AdminSettingsModal: React.FC<AdminSettingsModalProps> = ({
|
||||
onSaveRoles,
|
||||
allUsers: _allUsers,
|
||||
onUpdateUserRole,
|
||||
onRefreshUsers,
|
||||
systemConfig,
|
||||
onUpdateSystemConfig,
|
||||
env = 'dev',
|
||||
@@ -142,13 +144,20 @@ export const AdminSettingsModal: React.FC<AdminSettingsModalProps> = ({
|
||||
const [adminTemplates, setAdminTemplates] = useState<Array<{ asset_key: string; name: string }>>([]);
|
||||
const [validationMsg, setValidationMsg] = useState<string>('');
|
||||
|
||||
// 刷新全量用户列表
|
||||
// 刷新全量用户列表,并同步父级 App allUsers(立项成员候选依赖此目录)
|
||||
const refreshUsers = async () => {
|
||||
try {
|
||||
const list = await authClient.listAllUsers();
|
||||
setAdminUsers(list);
|
||||
} catch {
|
||||
// 容错降级
|
||||
await syncAdminUserDirectories({
|
||||
reloadLocalUsers: async () => {
|
||||
const list = await authClient.listAllUsers();
|
||||
setAdminUsers(list);
|
||||
},
|
||||
onRefreshUsers,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : '刷新/同步用户目录失败';
|
||||
setActionTip({ type: 'error', msg });
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -209,7 +218,9 @@ export const AdminSettingsModal: React.FC<AdminSettingsModalProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
refreshUsers();
|
||||
void refreshUsers().catch(() => {
|
||||
// 错误已写入 actionTip;打开时失败不阻断模态
|
||||
});
|
||||
refreshRoles();
|
||||
refreshWorkflows();
|
||||
setAllowReg(systemConfig.allowRegistration);
|
||||
@@ -230,10 +241,15 @@ export const AdminSettingsModal: React.FC<AdminSettingsModalProps> = ({
|
||||
try {
|
||||
await authClient.updateUserRole(userId, roleId, role?.name || '方案工程师');
|
||||
onUpdateUserRole(userId, roleId);
|
||||
refreshUsers();
|
||||
showNotification('success', '已成功变更该成员的所属职位与系统角色');
|
||||
} catch (err: any) {
|
||||
showNotification('error', err.message || '更新角色失败');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await refreshUsers();
|
||||
showNotification('success', '已成功变更该成员的所属职位与系统角色');
|
||||
} catch {
|
||||
showNotification('error', '角色已更新,但全局用户目录同步失败,请刷新后重试');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -242,10 +258,15 @@ export const AdminSettingsModal: React.FC<AdminSettingsModalProps> = ({
|
||||
const nextStatus = user.status === 'active' ? 'disabled' : 'active';
|
||||
try {
|
||||
await authClient.updateUserStatus(user.id, nextStatus);
|
||||
refreshUsers();
|
||||
showNotification('success', `已将账号【${user.name}】状态切换为:${nextStatus === 'active' ? '启用' : '停用'}`);
|
||||
} catch (err: any) {
|
||||
showNotification('error', err.message || '切换状态失败');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await refreshUsers();
|
||||
showNotification('success', `已将账号【${user.name}】状态切换为:${nextStatus === 'active' ? '启用' : '停用'}`);
|
||||
} catch {
|
||||
showNotification('error', '账号状态已更新,但全局用户目录同步失败,请刷新后重试');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -254,10 +275,15 @@ export const AdminSettingsModal: React.FC<AdminSettingsModalProps> = ({
|
||||
const role = editRoles.find((r) => r.id === targetRoleId);
|
||||
try {
|
||||
await authClient.updateUserStatus(user.id, 'active', targetRoleId, role?.name || '方案工程师');
|
||||
refreshUsers();
|
||||
showNotification('success', `已批准【${user.name}】注册申请,已正式授予【${role?.name}】职位`);
|
||||
} catch (err: any) {
|
||||
showNotification('error', err.message || '审批操作失败');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await refreshUsers();
|
||||
showNotification('success', `已批准【${user.name}】注册申请,已正式授予【${role?.name}】职位`);
|
||||
} catch {
|
||||
showNotification('error', '审批已生效,但全局用户目录同步失败,请刷新后重试');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -265,10 +291,15 @@ export const AdminSettingsModal: React.FC<AdminSettingsModalProps> = ({
|
||||
if (!window.confirm(`确定要驳回【${user.name}】的注册申请吗?该账号将被置为禁用。`)) return;
|
||||
try {
|
||||
await authClient.updateUserStatus(user.id, 'disabled');
|
||||
refreshUsers();
|
||||
showNotification('success', `已驳回【${user.name}】的注册申请`);
|
||||
} catch (err: any) {
|
||||
showNotification('error', err.message || '驳回操作失败');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await refreshUsers();
|
||||
showNotification('success', `已驳回【${user.name}】的注册申请`);
|
||||
} catch {
|
||||
showNotification('error', '驳回已生效,但全局用户目录同步失败,请刷新后重试');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -278,10 +309,15 @@ export const AdminSettingsModal: React.FC<AdminSettingsModalProps> = ({
|
||||
if (!window.confirm(`确定要重置【${user.name}】的密码吗?重置后初始密码为 ${tempPass},该用户下次登录将被强制改密。`)) return;
|
||||
try {
|
||||
await authClient.adminResetPassword(user.id, tempPass);
|
||||
refreshUsers();
|
||||
showNotification('success', `已重置【${user.name}】密码为 ${tempPass},已标记首次登录强制改密`);
|
||||
} catch (err: any) {
|
||||
showNotification('error', err.message || '重置密码失败');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await refreshUsers();
|
||||
showNotification('success', `已重置【${user.name}】密码为 ${tempPass},已标记首次登录强制改密`);
|
||||
} catch {
|
||||
showNotification('error', '密码已重置,但全局用户目录同步失败,请刷新后重试');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -304,8 +340,15 @@ export const AdminSettingsModal: React.FC<AdminSettingsModalProps> = ({
|
||||
setShowCreateUserModal(false);
|
||||
setNewUname('');
|
||||
setNewRealName('');
|
||||
refreshUsers();
|
||||
showNotification('success', `已成功开通受控账号【${newRealName}】,已自动标记首次登录强制改密`);
|
||||
try {
|
||||
await refreshUsers();
|
||||
showNotification('success', `已成功开通受控账号【${newRealName}】,已自动标记首次登录强制改密`);
|
||||
} catch {
|
||||
showNotification(
|
||||
'error',
|
||||
`账号【${newRealName}】已开通,但全局用户目录同步失败,立项成员候选可能暂不可见该账号`,
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
showNotification('error', err.message || '新建员工账号失败');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { syncAdminUserDirectories } from './adminUserDirectorySync';
|
||||
|
||||
test('syncAdminUserDirectories calls local reload then parent onRefreshUsers', async () => {
|
||||
const order: string[] = [];
|
||||
await syncAdminUserDirectories({
|
||||
reloadLocalUsers: async () => {
|
||||
order.push('local');
|
||||
},
|
||||
onRefreshUsers: async () => {
|
||||
order.push('parent');
|
||||
},
|
||||
});
|
||||
assert.deepEqual(order, ['local', 'parent']);
|
||||
});
|
||||
|
||||
test('syncAdminUserDirectories fails closed when onRefreshUsers is a dead/missing prop', async () => {
|
||||
let localCalled = false;
|
||||
await assert.rejects(
|
||||
async () =>
|
||||
syncAdminUserDirectories({
|
||||
reloadLocalUsers: async () => {
|
||||
localCalled = true;
|
||||
},
|
||||
}),
|
||||
/onRefreshUsers 未绑定/,
|
||||
);
|
||||
assert.equal(localCalled, true);
|
||||
});
|
||||
|
||||
test('syncAdminUserDirectories surfaces parent refresh errors (no silent swallow)', async () => {
|
||||
await assert.rejects(
|
||||
async () =>
|
||||
syncAdminUserDirectories({
|
||||
reloadLocalUsers: async () => undefined,
|
||||
onRefreshUsers: async () => {
|
||||
throw new Error('getAdminUsers failed');
|
||||
},
|
||||
}),
|
||||
/getAdminUsers failed/,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 管理员账号变更后,同步父级全局用户目录(App allUsers / localStorage)。
|
||||
* 若漏接 onRefreshUsers,立项成员候选会看不到新开通账号(UI-01 实战缺陷)。
|
||||
*/
|
||||
|
||||
export type ParentUserDirectoryRefresh = () => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* 先刷新模态内部列表,再强制调用父级 onRefreshUsers。
|
||||
* onRefreshUsers 缺失或抛错时必须让调用方可见,禁止静默成功。
|
||||
*/
|
||||
export async function syncAdminUserDirectories(params: {
|
||||
reloadLocalUsers: () => Promise<void>;
|
||||
onRefreshUsers?: ParentUserDirectoryRefresh;
|
||||
}): Promise<void> {
|
||||
await params.reloadLocalUsers();
|
||||
if (!params.onRefreshUsers) {
|
||||
throw new Error(
|
||||
'onRefreshUsers 未绑定:全局用户目录不会更新,立项成员候选可能缺少新开通账号',
|
||||
);
|
||||
}
|
||||
await Promise.resolve(params.onRefreshUsers());
|
||||
}
|
||||
Reference in New Issue
Block a user