feat: 完成分享、单会话与发布审批改造
- 增加工作流协作分享与知识库卡片分享入口,统一低版本浏览器复制反馈 - Web 新登录替换旧会话,并保持 API Key 会话隔离 - 发布审批增加必填说明并在审批详情展示 - 账号重置与导入改用可配置默认强密码
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { copyTextWithFeedback } from '../clipboard-feedback';
|
||||
|
||||
const { copyTextToClipboard, error, success } = vi.hoisted(() => ({
|
||||
copyTextToClipboard: vi.fn(),
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@easyflow/utils', () => ({
|
||||
copyTextToClipboard,
|
||||
}));
|
||||
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
error,
|
||||
success,
|
||||
},
|
||||
}));
|
||||
|
||||
describe('copyTextWithFeedback', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('copies through the shared Chrome 90 compatible helper', async () => {
|
||||
copyTextToClipboard.mockResolvedValue('exec-command');
|
||||
|
||||
await expect(
|
||||
copyTextWithFeedback('https://example.test/share', '复制成功'),
|
||||
).resolves.toBe(true);
|
||||
|
||||
expect(copyTextToClipboard).toHaveBeenCalledWith(
|
||||
'https://example.test/share',
|
||||
);
|
||||
expect(success).toHaveBeenCalledWith('复制成功');
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reports a recoverable error when copying fails', async () => {
|
||||
copyTextToClipboard.mockRejectedValue(new Error('copy denied'));
|
||||
|
||||
await expect(
|
||||
copyTextWithFeedback('content', '复制成功', '复制失败,请手动复制'),
|
||||
).resolves.toBe(false);
|
||||
|
||||
expect(error).toHaveBeenCalledWith('复制失败,请手动复制');
|
||||
expect(success).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
26
easyflow-ui-admin/app/src/utils/clipboard-feedback.ts
Normal file
26
easyflow-ui-admin/app/src/utils/clipboard-feedback.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { copyTextToClipboard } from '@easyflow/utils';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
/**
|
||||
* 复制文本并统一展示成功或失败反馈。
|
||||
*
|
||||
* @param text 待复制文本
|
||||
* @param successMessage 成功提示
|
||||
* @param failureMessage 失败提示
|
||||
* @returns 是否复制成功
|
||||
*/
|
||||
export async function copyTextWithFeedback(
|
||||
text: string,
|
||||
successMessage: string,
|
||||
failureMessage = '复制失败,请手动复制',
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await copyTextToClipboard(text);
|
||||
ElMessage.success(successMessage);
|
||||
return true;
|
||||
} catch {
|
||||
ElMessage.error(failureMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
88
easyflow-ui-admin/app/src/utils/workflow-share-context.ts
Normal file
88
easyflow-ui-admin/app/src/utils/workflow-share-context.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 工作流协作分享请求头。
|
||||
*/
|
||||
export const WORKFLOW_SHARE_HEADER = 'X-Workflow-Share-Key';
|
||||
|
||||
interface WorkflowShareResolutionOptions<T> {
|
||||
currentWorkflowId?: null | T;
|
||||
onFailure: (error: unknown) => Promise<void> | void;
|
||||
resolve: () => Promise<null | T | undefined>;
|
||||
shareKey?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从页面地址读取工作流分享密钥,兼容 history 与 hash 路由。
|
||||
*/
|
||||
export function readWorkflowShareKey(url?: string): null | string {
|
||||
const currentUrl =
|
||||
url || (typeof window === 'undefined' ? '' : window.location.href);
|
||||
if (!currentUrl) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(
|
||||
currentUrl,
|
||||
typeof window === 'undefined'
|
||||
? 'http://localhost'
|
||||
: window.location.origin,
|
||||
);
|
||||
const historyKey = parsed.searchParams.get('shareKey')?.trim();
|
||||
if (historyKey) {
|
||||
return historyKey;
|
||||
}
|
||||
const queryIndex = parsed.hash.indexOf('?');
|
||||
if (queryIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
new URLSearchParams(parsed.hash.slice(queryIndex + 1))
|
||||
.get('shareKey')
|
||||
?.trim() || null
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在保留现有请求头的基础上附加工作流分享密钥。
|
||||
*/
|
||||
export function withWorkflowShareHeader(
|
||||
headers: Record<string, string>,
|
||||
url?: string,
|
||||
): Record<string, string> {
|
||||
const shareKey = readWorkflowShareKey(url);
|
||||
if (!shareKey) {
|
||||
return headers;
|
||||
}
|
||||
return {
|
||||
...headers,
|
||||
[WORKFLOW_SHARE_HEADER]: shareKey,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析分享地址对应的工作流,并在链接失效时统一收口异常。
|
||||
*/
|
||||
export async function resolveWorkflowShareWorkflowId<T>({
|
||||
currentWorkflowId,
|
||||
onFailure,
|
||||
resolve,
|
||||
shareKey,
|
||||
}: WorkflowShareResolutionOptions<T>): Promise<null | T> {
|
||||
if (currentWorkflowId !== null && currentWorkflowId !== undefined) {
|
||||
return currentWorkflowId;
|
||||
}
|
||||
const normalizedShareKey = Array.isArray(shareKey)
|
||||
? shareKey.find((value) => String(value || '').trim())
|
||||
: shareKey;
|
||||
if (!String(normalizedShareKey || '').trim()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return (await resolve()) ?? null;
|
||||
} catch (error) {
|
||||
await onFailure(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user