feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
31
frontend/packages/studio/autosave/.storybook/main.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { mergeConfig } from 'vite';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
|
||||
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
||||
const config = {
|
||||
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.tsx'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-onboarding',
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
viteFinal: config =>
|
||||
mergeConfig(config, {
|
||||
plugins: [
|
||||
svgr({
|
||||
svgrOptions: {
|
||||
native: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
};
|
||||
export default config;
|
||||
14
frontend/packages/studio/autosave/.storybook/preview.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/** @type { import('@storybook/react').Preview } */
|
||||
const preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
5
frontend/packages/studio/autosave/.stylelintrc.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
16
frontend/packages/studio/autosave/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# @coze-studio/autosave
|
||||
|
||||
> Project template for react component with storybook and supports publish independently.
|
||||
|
||||
## Features
|
||||
|
||||
- [x] eslint & ts
|
||||
- [x] esm bundle
|
||||
- [x] umd bundle
|
||||
- [x] storybook
|
||||
|
||||
## Commands
|
||||
|
||||
- init: `rush update`
|
||||
- dev: `npm run dev`
|
||||
- build: `npm run build`
|
||||
72
frontend/packages/studio/autosave/__mocks__/zustand.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @coze-arch/no-batch-import-or-export
|
||||
import type * as zustand from 'zustand';
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
const { create: actualCreate, createStore: actualCreateStore } =
|
||||
await vi.importActual<typeof zustand>('zustand');
|
||||
|
||||
// a variable to hold reset functions for all stores declared in the app
|
||||
export const storeResetFns = new Set<() => void>();
|
||||
|
||||
const createUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
|
||||
const store = actualCreate(stateCreator);
|
||||
const initialState = store.getState();
|
||||
storeResetFns.add(() => {
|
||||
store.setState(initialState, true);
|
||||
});
|
||||
return store;
|
||||
};
|
||||
|
||||
// when creating a store, we get its initial state, create a reset function and add it in the set
|
||||
export const create = (<T>(stateCreator: zustand.StateCreator<T>) => {
|
||||
console.log('zustand create mock');
|
||||
|
||||
// to support curried version of create
|
||||
return typeof stateCreator === 'function'
|
||||
? createUncurried(stateCreator)
|
||||
: createUncurried;
|
||||
}) as typeof zustand.create;
|
||||
|
||||
const createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
|
||||
const store = actualCreateStore(stateCreator);
|
||||
const initialState = store.getState();
|
||||
storeResetFns.add(() => {
|
||||
store.setState(initialState, true);
|
||||
});
|
||||
return store;
|
||||
};
|
||||
|
||||
// when creating a store, we get its initial state, create a reset function and add it in the set
|
||||
export const createStore = (<T>(stateCreator: zustand.StateCreator<T>) => {
|
||||
console.log('zustand createStore mock');
|
||||
|
||||
// to support curried version of createStore
|
||||
return typeof stateCreator === 'function'
|
||||
? createStoreUncurried(stateCreator)
|
||||
: createStoreUncurried;
|
||||
}) as typeof zustand.createStore;
|
||||
|
||||
// reset all stores after each test run
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
storeResetFns.forEach(resetFn => {
|
||||
resetFn();
|
||||
});
|
||||
});
|
||||
});
|
||||
234
frontend/packages/studio/autosave/__tests__/core/manager.test.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type StoreApi, type UseBoundStore } from 'zustand';
|
||||
import { describe, beforeEach, afterEach, vi, expect } from 'vitest';
|
||||
|
||||
import { DebounceTime } from '../../src/type';
|
||||
import { AutosaveManager } from '../../src/core/manager';
|
||||
|
||||
vi.mock('lodash-es', () => ({
|
||||
debounce: vi.fn((func, wait) => {
|
||||
let timeout;
|
||||
const debounced = (...args) => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => func(...args), wait);
|
||||
};
|
||||
debounced.cancel = vi.fn(() => clearTimeout(timeout));
|
||||
debounced.flush = vi.fn(() => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
func();
|
||||
}
|
||||
});
|
||||
return debounced;
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockStore = {
|
||||
subscribe: vi.fn().mockReturnValue(() => {
|
||||
console.log('Unsubscribed');
|
||||
}),
|
||||
} as unknown as UseBoundStore<StoreApi<any>>;
|
||||
|
||||
describe('AutosaveManager', () => {
|
||||
let manager;
|
||||
const saveRequest = vi.fn().mockResolvedValue(Promise.resolve());
|
||||
|
||||
const registers = [
|
||||
{
|
||||
key: 'testKey',
|
||||
debounce: DebounceTime.Immediate,
|
||||
selector: state => state,
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new AutosaveManager({
|
||||
store: mockStore,
|
||||
registers,
|
||||
saveRequest,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should initialize and set initial values correctly', () => {
|
||||
expect(manager.observerList.length).toBe(0);
|
||||
expect(manager.configList).toMatchObject(
|
||||
registers.map(r => ({
|
||||
...r,
|
||||
eventCallBacks: undefined,
|
||||
saveRequest,
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
it('should start observers correctly', () => {
|
||||
manager.start();
|
||||
|
||||
expect(manager.observerList.length).toBe(1);
|
||||
const observer = manager.observerList[0];
|
||||
expect(observer.config.key).toBe('testKey');
|
||||
expect(observer.config.saveRequest).toBe(saveRequest);
|
||||
});
|
||||
|
||||
it('should not start observers if already started', () => {
|
||||
manager.start();
|
||||
expect(manager.observerList.length).toBe(1);
|
||||
|
||||
manager.start(); // 再次调用 start
|
||||
expect(manager.observerList.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle manualSave correctly when config is undefined', async () => {
|
||||
await manager.manualSave('undefinedKey', { value: 'test' });
|
||||
|
||||
expect(saveRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should close observers correctly', () => {
|
||||
manager.start();
|
||||
const observer = manager.observerList[0];
|
||||
const closeSpy = vi.spyOn(observer, 'close');
|
||||
|
||||
manager.close();
|
||||
|
||||
expect(closeSpy).toHaveBeenCalled();
|
||||
expect(manager.observerList.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle manualSave correctly', async () => {
|
||||
const params = { value: 'test' };
|
||||
|
||||
await manager.manualSave('testKey', params);
|
||||
|
||||
expect(saveRequest).toHaveBeenCalledWith(params, 'testKey', []);
|
||||
});
|
||||
|
||||
it('call middleware correctly', async () => {
|
||||
const mockOnBeforeSave = vi.fn().mockResolvedValue({ value: 'before' });
|
||||
const mockOnAfterSave = vi.fn().mockResolvedValue({ value: 'after' });
|
||||
|
||||
const registersWithMiddleware = [
|
||||
{
|
||||
key: 'testWithMiddlewareKey',
|
||||
debounce: DebounceTime.Immediate,
|
||||
selector: state => state,
|
||||
middleware: {
|
||||
onBeforeSave: mockOnBeforeSave,
|
||||
onAfterSave: mockOnAfterSave,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
manager = new AutosaveManager({
|
||||
store: mockStore,
|
||||
registers: registersWithMiddleware,
|
||||
saveRequest,
|
||||
});
|
||||
|
||||
const params = { value: 'test' };
|
||||
|
||||
await manager.manualSave('testWithMiddlewareKey', params);
|
||||
|
||||
expect(mockOnBeforeSave).toHaveBeenCalledWith(params);
|
||||
expect(saveRequest).toHaveBeenCalledWith(
|
||||
{ value: 'before' },
|
||||
'testWithMiddlewareKey',
|
||||
[],
|
||||
);
|
||||
expect(mockOnAfterSave).toHaveBeenCalledWith(params);
|
||||
});
|
||||
|
||||
it('should call eventCallBacks', async () => {
|
||||
const onBeforeSaveCallback = vi.fn();
|
||||
const onAfterSaveCallback = vi.fn();
|
||||
const eventCallBacks = {
|
||||
onBeforeSave: onBeforeSaveCallback,
|
||||
onAfterSave: onAfterSaveCallback,
|
||||
};
|
||||
manager = new AutosaveManager({
|
||||
store: mockStore,
|
||||
registers,
|
||||
saveRequest,
|
||||
eventCallBacks,
|
||||
});
|
||||
|
||||
await manager.manualSave('testKey', { value: 'test' });
|
||||
|
||||
expect(onBeforeSaveCallback).toHaveBeenCalledWith({ value: 'test' });
|
||||
expect(onAfterSaveCallback).toHaveBeenCalledWith({ value: 'test' });
|
||||
});
|
||||
|
||||
it('should save without auto save', async () => {
|
||||
const handler = vi.fn().mockResolvedValue(undefined);
|
||||
manager.start();
|
||||
|
||||
await manager.handleWithoutAutosave({
|
||||
key: 'testKey',
|
||||
handler,
|
||||
});
|
||||
|
||||
const observer = manager.getObserver('testKey');
|
||||
// 确保所有异步操作完成
|
||||
await Promise.resolve();
|
||||
|
||||
expect(observer.lock).toBe(false);
|
||||
expect(handler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should save flush correctly', async () => {
|
||||
manager.start();
|
||||
|
||||
vi.spyOn(manager, 'getObserver').mockReturnValue({
|
||||
debouncedSaveFunc: {
|
||||
flush: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
await manager.saveFlush('testKey');
|
||||
|
||||
const observer = manager.getObserver('testKey');
|
||||
expect(observer.debouncedSaveFunc.flush).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should save flush all correctly', async () => {
|
||||
manager.start();
|
||||
const observer = manager.getObserver('testKey');
|
||||
const nextState = { value: 'next' };
|
||||
const prevState = { value: 'prev' };
|
||||
|
||||
vi.spyOn(observer, 'getMemoizeSelector').mockReturnValue(() => nextState);
|
||||
vi.spyOn(observer, 'getTriggerDelayTime').mockReturnValue(1000);
|
||||
|
||||
await observer.subscribeCallback(nextState, prevState);
|
||||
|
||||
manager.observerList.forEach(ob => {
|
||||
ob.debouncedSaveFunc.flush = vi.fn();
|
||||
});
|
||||
|
||||
manager.saveFlushAll();
|
||||
|
||||
manager.observerList.forEach(ob => {
|
||||
expect(ob.debouncedSaveFunc.flush).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,354 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type StoreApi, type UseBoundStore } from 'zustand';
|
||||
import { describe, beforeEach, afterEach, vi, expect } from 'vitest';
|
||||
import { diff, type Diff } from 'deep-diff';
|
||||
|
||||
import { DebounceTime, type AutosaveObserverConfig } from '../../src/type';
|
||||
import { AutosaveObserver } from '../../src/core/observer';
|
||||
|
||||
const debounceMock = vi.fn((func, wait) => {
|
||||
let timeout;
|
||||
const debounced = (...args) => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => func(...args), wait);
|
||||
};
|
||||
debounced.cancel = vi.fn(() => clearTimeout(timeout));
|
||||
debounced.flush = vi.fn(() => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
func();
|
||||
}
|
||||
});
|
||||
return debounced;
|
||||
});
|
||||
|
||||
vi.mock('lodash-es/debounce', () => {
|
||||
const originalModule = vi.importActual('lodash-es/debounce');
|
||||
return {
|
||||
__esModule: true,
|
||||
default: debounceMock,
|
||||
...originalModule,
|
||||
};
|
||||
});
|
||||
|
||||
const mockStore = {
|
||||
subscribe: vi.fn(),
|
||||
} as unknown as UseBoundStore<StoreApi<any>>;
|
||||
|
||||
describe('AutosaveObserver', () => {
|
||||
let observer;
|
||||
const saveRequest = vi.fn().mockResolvedValue(Promise.resolve());
|
||||
|
||||
const config: AutosaveObserverConfig<any, any, any> = {
|
||||
key: 'testKey',
|
||||
debounce: DebounceTime.Immediate,
|
||||
selector: state => state,
|
||||
saveRequest,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers(); // 使用假定时器
|
||||
|
||||
observer = new AutosaveObserver({
|
||||
store: mockStore,
|
||||
...config,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.useRealTimers(); // 恢复真实的定时器
|
||||
});
|
||||
|
||||
it('should initialize and set initial values correctly', () => {
|
||||
expect(observer.lock).toBe(false);
|
||||
expect(observer.config).toStrictEqual(config);
|
||||
expect(observer.store).toBe(mockStore);
|
||||
expect(mockStore.subscribe).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should trigger callback and parsedSaveFunc', async () => {
|
||||
const nextState = { value: 'next' };
|
||||
const prevState = { value: 'prev' };
|
||||
const diffChange: Diff<any, any>[] = [
|
||||
{
|
||||
kind: 'E',
|
||||
path: ['value'],
|
||||
lhs: 'prev',
|
||||
rhs: 'next',
|
||||
},
|
||||
];
|
||||
|
||||
vi.spyOn(observer, 'getMemoizeSelector').mockReturnValue(() => nextState);
|
||||
vi.spyOn(observer, 'getTriggerDelayTime').mockReturnValue(0);
|
||||
|
||||
await observer.subscribeCallback(nextState, prevState);
|
||||
|
||||
expect(observer.nextState).toBe(nextState);
|
||||
expect(observer.prevState).toBe(prevState);
|
||||
expect(observer.diff).toEqual(diffChange);
|
||||
expect(saveRequest).toHaveBeenCalledWith(nextState, 'testKey', diffChange);
|
||||
});
|
||||
|
||||
it('should handle array diff change', async () => {
|
||||
const nextState = { value: [1, 2, 3] };
|
||||
const prevState = { value: [1, 2] };
|
||||
const diffChange = diff(prevState, nextState);
|
||||
|
||||
vi.spyOn(observer, 'getMemoizeSelector').mockReturnValue(() => nextState);
|
||||
vi.spyOn(observer, 'getTriggerDelayTime').mockReturnValue(500);
|
||||
|
||||
await observer.subscribeCallback(nextState, prevState);
|
||||
|
||||
expect(observer.debouncedSaveFunc).toBeInstanceOf(Function);
|
||||
|
||||
vi.runAllTimers(); // 手动推进定时器时间以触发防抖函数
|
||||
|
||||
await vi.runAllTimersAsync(); // 确保所有异步操作完成
|
||||
|
||||
expect(saveRequest).toHaveBeenCalledWith(nextState, 'testKey', diffChange);
|
||||
});
|
||||
|
||||
it('should cancel and unsubscribe correctly', async () => {
|
||||
const prevState = {
|
||||
value: {
|
||||
a: 1,
|
||||
},
|
||||
};
|
||||
const nextState = {
|
||||
value: {
|
||||
a: 2,
|
||||
},
|
||||
};
|
||||
vi.spyOn(observer, 'getMemoizeSelector').mockReturnValue(() => nextState);
|
||||
vi.spyOn(observer, 'getTriggerDelayTime').mockReturnValue(1000);
|
||||
|
||||
await observer.subscribeCallback(nextState, prevState);
|
||||
|
||||
const cancelSpy = vi.spyOn(observer.debouncedSaveFunc, 'flush');
|
||||
const unsubscribeSpy = vi.spyOn(observer, 'unsubscribe');
|
||||
|
||||
observer.close();
|
||||
|
||||
expect(cancelSpy).toHaveBeenCalled();
|
||||
expect(unsubscribeSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('selector with deps', () => {
|
||||
const deps = [state => state.value];
|
||||
const transformer = value => value * 2;
|
||||
|
||||
const observerConfig: AutosaveObserverConfig<any, any, any> = {
|
||||
...config,
|
||||
selector: { deps, transformer },
|
||||
};
|
||||
|
||||
observer = new AutosaveObserver({
|
||||
store: mockStore,
|
||||
...observerConfig,
|
||||
});
|
||||
|
||||
const selector = observer.getMemoizeSelector();
|
||||
|
||||
const state = { value: 3 };
|
||||
const result = selector(state);
|
||||
|
||||
expect(result).toBe(transformer(3));
|
||||
});
|
||||
|
||||
it('should return in callback if lock is true', async () => {
|
||||
observer.lock = true;
|
||||
const nextState = { value: 'next' };
|
||||
const prevState = { value: 'prev' };
|
||||
|
||||
const getTriggerDelayTimeSpy = vi.spyOn(observer, 'getTriggerDelayTime');
|
||||
const parsedSaveFuncSpy = vi.spyOn(observer, 'parsedSaveFunc');
|
||||
|
||||
await observer.subscribeCallback(nextState, prevState);
|
||||
|
||||
expect(observer.nextState).toBe(nextState);
|
||||
expect(observer.prevState).toBe(prevState);
|
||||
expect(getTriggerDelayTimeSpy).not.toHaveBeenCalled();
|
||||
expect(parsedSaveFuncSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return in callback if diffChange is undefined', async () => {
|
||||
const nextState = { value: 'prev' };
|
||||
const prevState = { value: 'prev' };
|
||||
|
||||
const getTriggerDelayTimeSpy = vi.spyOn(observer, 'getTriggerDelayTime');
|
||||
const parsedSaveFuncSpy = vi.spyOn(observer, 'parsedSaveFunc');
|
||||
|
||||
await observer.subscribeCallback(nextState, prevState);
|
||||
|
||||
expect(observer.nextState).toBe(nextState);
|
||||
expect(observer.prevState).toBe(prevState);
|
||||
expect(getTriggerDelayTimeSpy).not.toHaveBeenCalled();
|
||||
expect(parsedSaveFuncSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onBeforeSave lifecycle callback', async () => {
|
||||
const onBeforeSave = vi.fn();
|
||||
observer.config.eventCallBacks = { onBeforeSave };
|
||||
observer.nextState = { value: 'next' };
|
||||
|
||||
await observer.parsedSaveFunc();
|
||||
|
||||
expect(onBeforeSave).toHaveBeenCalledWith({
|
||||
key: observer.config.key,
|
||||
data: observer.nextState,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onAfterSave lifecycle callback', async () => {
|
||||
const onAfterSave = vi.fn();
|
||||
observer.config.eventCallBacks = { onAfterSave };
|
||||
observer.nextState = { value: 'next' };
|
||||
|
||||
await observer.parsedSaveFunc();
|
||||
|
||||
expect(onAfterSave).toHaveBeenCalledWith({
|
||||
key: observer.config.key,
|
||||
data: observer.nextState,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onError lifecycle method on error', async () => {
|
||||
const onError = vi.fn();
|
||||
observer.config.eventCallBacks = { onError };
|
||||
|
||||
saveRequest.mockRejectedValueOnce(new Error('Failed request'));
|
||||
observer.nextState = { value: 'next' };
|
||||
|
||||
await observer.parsedSaveFunc();
|
||||
|
||||
expect(onError).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
key: observer.config.key,
|
||||
error: expect.any(Error),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the correct delay time based on debounce configuration', () => {
|
||||
const prevState = { value: 'prev' };
|
||||
const diffChange = [{ kind: 'E', path: ['value'] }];
|
||||
|
||||
observer.config.debounce = () => 200;
|
||||
expect(observer.getTriggerDelayTime(prevState, diffChange)).toBe(200);
|
||||
|
||||
observer.config.debounce = { default: 300 };
|
||||
expect(observer.getTriggerDelayTime(prevState, diffChange)).toBe(300);
|
||||
|
||||
observer.config.debounce = 100;
|
||||
expect(observer.getTriggerDelayTime(prevState, diffChange)).toBe(100);
|
||||
|
||||
observer.config.debounce = null;
|
||||
expect(observer.getTriggerDelayTime(prevState, diffChange)).toBe(
|
||||
DebounceTime.Immediate,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return default debounce time when diffChange is empty or undefined', () => {
|
||||
const prevState = {};
|
||||
const diffChange = undefined;
|
||||
|
||||
const delayTime = observer.getTriggerDelayTime(prevState, diffChange);
|
||||
expect(delayTime).toBe(DebounceTime.Immediate);
|
||||
});
|
||||
|
||||
it('should push default debounce time when changePath does not exist or is a number', () => {
|
||||
const prevState = { value: 'prev' };
|
||||
const diffChange: Diff<any, any>[] = [
|
||||
{
|
||||
kind: 'E',
|
||||
path: [123],
|
||||
lhs: 'prev',
|
||||
rhs: 'next',
|
||||
},
|
||||
];
|
||||
|
||||
const delayTime = observer.getTriggerDelayTime(prevState, diffChange);
|
||||
expect(delayTime).toBe(DebounceTime.Immediate);
|
||||
});
|
||||
|
||||
it('should push action delay time when DebounceConfig.action is not an object', () => {
|
||||
const prevState = { value: 'prev' };
|
||||
const diffChange = [
|
||||
{
|
||||
kind: 'E',
|
||||
path: ['value'],
|
||||
lhs: 'prev',
|
||||
rhs: 'next',
|
||||
},
|
||||
];
|
||||
|
||||
const debounceActionTime = DebounceTime.Long;
|
||||
|
||||
observer.config.debounce = {
|
||||
default: DebounceTime.Long,
|
||||
value: {
|
||||
action: debounceActionTime,
|
||||
},
|
||||
};
|
||||
|
||||
const delayTime = observer.getTriggerDelayTime(prevState, diffChange);
|
||||
expect(delayTime).toBe(debounceActionTime);
|
||||
});
|
||||
|
||||
it('should return an empty string if changePath does not exist', () => {
|
||||
const changePath = undefined;
|
||||
const debouncePath = observer.getdebouncePath(changePath);
|
||||
expect(debouncePath).toBe('');
|
||||
});
|
||||
|
||||
it('should return the first element of changePath', () => {
|
||||
const changePath = ['value', 0, 'key'];
|
||||
const debouncePath = observer.getdebouncePath(changePath);
|
||||
expect(debouncePath).toBe('value');
|
||||
});
|
||||
|
||||
it('should return default debounce time when diffChange is undefined or empty', () => {
|
||||
const prevState = {};
|
||||
let diffChange: Diff<any, any>[] | undefined = undefined;
|
||||
|
||||
let delayTime = observer.getTriggerDelayTime(prevState, diffChange);
|
||||
expect(delayTime).toBe(DebounceTime.Immediate);
|
||||
|
||||
diffChange = [];
|
||||
delayTime = observer.getTriggerDelayTime(prevState, diffChange);
|
||||
expect(delayTime).toBe(DebounceTime.Immediate);
|
||||
});
|
||||
|
||||
it('should push default debounce time when changePath does not exist or has conditions', () => {
|
||||
const prevState = { value: 'prev' };
|
||||
observer.config.debounce = { default: DebounceTime.Long };
|
||||
const diffChange = [
|
||||
{ kind: 'E', path: undefined },
|
||||
{ kind: 'E', path: ['nonexistentPath'] },
|
||||
{ kind: 'E', path: [123] },
|
||||
];
|
||||
|
||||
vi.spyOn(observer, 'getdebouncePath').mockReturnValue(123);
|
||||
|
||||
const delayTime = observer.getTriggerDelayTime(prevState, diffChange);
|
||||
expect(delayTime).toBe(DebounceTime.Long);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
isFunction,
|
||||
isObject,
|
||||
getPayloadByFormatter,
|
||||
} from '../../src/utils/index';
|
||||
import { type DebounceConfig, DebounceTime } from '../../src/type/index';
|
||||
|
||||
describe('isFunction', () => {
|
||||
const fn: DebounceConfig = () => DebounceTime.Immediate;
|
||||
|
||||
const ob: DebounceConfig = {
|
||||
default: DebounceTime.Immediate,
|
||||
};
|
||||
|
||||
it('isFunction should return true when the input is function', () => {
|
||||
const result = isFunction(fn);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('isFunction should return false when the input is object', () => {
|
||||
const result = isFunction(ob);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('isObject should return true when the input is object', () => {
|
||||
const result = isObject(ob);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPayloadByFormatter', () => {
|
||||
it('should return state directly if formatter is not provided', async () => {
|
||||
const state = { key: 'value' };
|
||||
|
||||
const result = await getPayloadByFormatter(state);
|
||||
|
||||
expect(result).toEqual(state);
|
||||
});
|
||||
|
||||
it('should call formatter and return its result if formatter is provided', async () => {
|
||||
const state = { key: 'value' };
|
||||
const formatter = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ formattedKey: 'formattedValue' });
|
||||
|
||||
const result = await getPayloadByFormatter(state, formatter);
|
||||
|
||||
expect(formatter).toHaveBeenCalledWith(state);
|
||||
expect(result).toEqual({ formattedKey: 'formattedValue' });
|
||||
});
|
||||
});
|
||||
12
frontend/packages/studio/autosave/config/rush-project.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
7
frontend/packages/studio/autosave/eslint.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
});
|
||||
59
frontend/packages/studio/autosave/package.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "@coze-studio/autosave",
|
||||
"version": "0.0.1",
|
||||
"description": "autosave",
|
||||
"license": "Apache-2.0",
|
||||
"author": "zhangxiang.01@bytedance.com",
|
||||
"maintainers": [],
|
||||
"main": "src/index.ts",
|
||||
"unpkg": "./dist/umd/index.js",
|
||||
"types": "./src/index.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-arch/bot-api": "workspace:*",
|
||||
"@coze-arch/logger": "workspace:*",
|
||||
"ahooks": "^3.7.8",
|
||||
"dayjs": "^1.11.7",
|
||||
"deep-diff": "~1.0.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"reselect": "^5.1.1",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/eslint-config": "workspace:*",
|
||||
"@coze-arch/stylelint-config": "workspace:*",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@coze-arch/vitest-config": "workspace:*",
|
||||
"@swc/core": "^1.3.35",
|
||||
"@swc/helpers": "^0.4.12",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/deep-diff": "^1.0.5",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"@vitest/coverage-v8": "~3.0.5",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"less-loader": "~11.1.3",
|
||||
"postcss": "^8.4.32",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"stylelint": "^15.11.0",
|
||||
"vite-plugin-svgr": "~3.3.0",
|
||||
"vitest": "~3.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.2.0",
|
||||
"react-dom": ">=18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
17
frontend/packages/studio/autosave/setup-vitest.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
vi.mock('zustand');
|
||||
179
frontend/packages/studio/autosave/src/core/manager.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type StoreApi, type UseBoundStore } from 'zustand';
|
||||
|
||||
import {
|
||||
type AutosaveObserverConfig,
|
||||
type HostedObserverConfig,
|
||||
type EventCallBacks,
|
||||
type SaveRequest,
|
||||
} from '../type';
|
||||
import { AutosaveObserver } from './observer';
|
||||
|
||||
export interface AutosaveManagerProps<StoreType, ScopeKey, ScopeStateType> {
|
||||
store: UseBoundStore<StoreApi<StoreType>>;
|
||||
registers: HostedObserverConfig<StoreType, ScopeKey, ScopeStateType>[];
|
||||
saveRequest: SaveRequest<ScopeStateType, ScopeKey>;
|
||||
eventCallBacks?: EventCallBacks<ScopeStateType, ScopeKey>;
|
||||
}
|
||||
|
||||
export class AutosaveManager<StoreType, ScopeKey, ScopeStateType> {
|
||||
private configList: AutosaveObserverConfig<
|
||||
StoreType,
|
||||
ScopeKey,
|
||||
ScopeStateType
|
||||
>[];
|
||||
private observerList: AutosaveObserver<StoreType, ScopeKey, ScopeStateType>[];
|
||||
private store: UseBoundStore<StoreApi<StoreType>>;
|
||||
|
||||
private eventCallBacks?: EventCallBacks<ScopeStateType, ScopeKey>;
|
||||
private saveRequest: SaveRequest<ScopeStateType, ScopeKey>;
|
||||
|
||||
constructor(
|
||||
props: AutosaveManagerProps<StoreType, ScopeKey, ScopeStateType>,
|
||||
) {
|
||||
this.configList = [];
|
||||
this.observerList = [];
|
||||
this.saveRequest = props.saveRequest;
|
||||
this.eventCallBacks = props.eventCallBacks;
|
||||
this.store = props.store;
|
||||
|
||||
this.register(props.registers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册数据源和定义对应的 Observer 配置
|
||||
* @param _config
|
||||
*/
|
||||
public register = (
|
||||
registers: HostedObserverConfig<StoreType, ScopeKey, ScopeStateType>[],
|
||||
) => {
|
||||
this.close();
|
||||
this.configList = [];
|
||||
|
||||
registers.forEach(register => {
|
||||
const config: AutosaveObserverConfig<
|
||||
StoreType,
|
||||
ScopeKey,
|
||||
ScopeStateType
|
||||
> = {
|
||||
...register,
|
||||
eventCallBacks: this.eventCallBacks,
|
||||
saveRequest: this.saveRequest,
|
||||
};
|
||||
this.configList.push(config);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 启动 Manager 模块
|
||||
*/
|
||||
public start = () => {
|
||||
if (this.observerList.length > 0) {
|
||||
return;
|
||||
}
|
||||
this.observerList = this.configList.map(
|
||||
config =>
|
||||
new AutosaveObserver({
|
||||
store: this.store,
|
||||
...config,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭 Manager 模块下的所有属性监听
|
||||
*/
|
||||
public close = () => {
|
||||
this.observerList.forEach(observer => observer.close());
|
||||
this.observerList = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* 手动保存
|
||||
* @param params
|
||||
*/
|
||||
public manualSave = async (key: ScopeKey, params: ScopeStateType) => {
|
||||
const config = this.getConfig(key);
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
const { middleware, eventCallBacks, saveRequest } = config;
|
||||
const beforeSavePayload = middleware?.onBeforeSave
|
||||
? await middleware?.onBeforeSave(params)
|
||||
: params;
|
||||
eventCallBacks?.onBeforeSave?.(beforeSavePayload);
|
||||
|
||||
await saveRequest(beforeSavePayload as ScopeStateType, key, []);
|
||||
|
||||
const afterSavePayload = middleware?.onAfterSave
|
||||
? await middleware?.onAfterSave(params)
|
||||
: params;
|
||||
eventCallBacks?.onAfterSave?.(afterSavePayload);
|
||||
};
|
||||
|
||||
/**
|
||||
* 回调过程中关闭自动保存
|
||||
* @param params
|
||||
*/
|
||||
public handleWithoutAutosave = async (params: {
|
||||
key: ScopeKey;
|
||||
handler: () => Promise<void>;
|
||||
}) => {
|
||||
const { key, handler } = params;
|
||||
|
||||
const observers = this.observerList.filter(o => o.config.key === key);
|
||||
if (observers.length) {
|
||||
observers.forEach(o => (o.lock = true));
|
||||
await handler();
|
||||
observers.forEach(o => (o.lock = false));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 立即触发保存
|
||||
* @param key
|
||||
*/
|
||||
public saveFlush = (key: ScopeKey) => {
|
||||
const observer = this.getObserver(key);
|
||||
observer?.debouncedSaveFunc?.flush?.();
|
||||
};
|
||||
|
||||
/**
|
||||
* 立即触发所有保存
|
||||
* @param key
|
||||
*/
|
||||
public saveFlushAll = () => {
|
||||
this.observerList.forEach(observer =>
|
||||
observer?.debouncedSaveFunc?.flush?.(),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取目标 observer 配置
|
||||
* @param key
|
||||
*/
|
||||
private getObserver = (key: ScopeKey) =>
|
||||
this.observerList.find(i => i.config.key === key);
|
||||
|
||||
/**
|
||||
* 获取目标配置项
|
||||
* @param key
|
||||
*/
|
||||
private getConfig = (key: ScopeKey) =>
|
||||
this.configList.find(i => i.key === key);
|
||||
}
|
||||
253
frontend/packages/studio/autosave/src/core/observer.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type StoreApi, type UseBoundStore } from 'zustand';
|
||||
import { createSelector } from 'reselect';
|
||||
import { debounce, has, get, type DebouncedFunc } from 'lodash-es';
|
||||
import diff, { type Diff } from 'deep-diff';
|
||||
|
||||
import { isFunction, isObject, getPayloadByFormatter } from '../utils';
|
||||
import {
|
||||
DebounceTime,
|
||||
type AutosaveObserverConfig,
|
||||
type UseStoreType,
|
||||
type PathType,
|
||||
type AutosaveObserverProps,
|
||||
} from '../type/index';
|
||||
|
||||
export class AutosaveObserver<StoreType, ScopeKey, ScopeStateType> {
|
||||
private store: UseBoundStore<StoreApi<StoreType>>;
|
||||
|
||||
public lock: boolean;
|
||||
public debouncedSaveFunc: DebouncedFunc<() => Promise<void>>;
|
||||
public nextState!: ScopeStateType;
|
||||
public prevState!: ScopeStateType;
|
||||
private diff!: Diff<ScopeStateType, ScopeStateType>[];
|
||||
|
||||
private unobserver?: () => void;
|
||||
private unsubscribe!: () => void;
|
||||
public config: AutosaveObserverConfig<StoreType, ScopeKey, ScopeStateType>;
|
||||
|
||||
constructor(
|
||||
props: AutosaveObserverProps<StoreType, ScopeKey, ScopeStateType>,
|
||||
) {
|
||||
const { store, ...config } = props;
|
||||
this.store = store;
|
||||
this.lock = false;
|
||||
this.config = config;
|
||||
|
||||
// 订阅字段初始化
|
||||
this.initSubscribe();
|
||||
}
|
||||
|
||||
private initSubscribe = () => {
|
||||
const memoizeSelector = this.getMemoizeSelector();
|
||||
|
||||
this.unsubscribe = (
|
||||
this.store as unknown as UseStoreType<StoreType, ScopeStateType>
|
||||
).subscribe(memoizeSelector, this.subscribeCallback);
|
||||
};
|
||||
|
||||
private getMemoizeSelector = () => {
|
||||
if (typeof this.config.selector === 'function') {
|
||||
return this.config.selector;
|
||||
} else {
|
||||
// 使用createSelector创建可记忆化的选择器
|
||||
const { deps, transformer } = this.config.selector;
|
||||
return createSelector(deps, transformer);
|
||||
}
|
||||
};
|
||||
|
||||
private subscribeCallback = async (nextState, prevState) => {
|
||||
console.log('nextState :>> ', nextState);
|
||||
console.log('prevState :>> ', prevState);
|
||||
|
||||
// selector 返回的 state
|
||||
this.nextState = nextState;
|
||||
this.prevState = prevState;
|
||||
|
||||
if (this.lock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const diffChange: Diff<ScopeStateType, ScopeStateType>[] | undefined = diff(
|
||||
prevState,
|
||||
nextState,
|
||||
);
|
||||
|
||||
console.log('diffChange:>>', diffChange);
|
||||
if (!diffChange) {
|
||||
return;
|
||||
}
|
||||
this.debouncedSaveFunc?.cancel?.();
|
||||
|
||||
this.diff = diffChange;
|
||||
|
||||
const delayTime = this.getTriggerDelayTime(prevState, diffChange);
|
||||
|
||||
console.log('delayTime:>>>>>', delayTime);
|
||||
|
||||
if (delayTime === 0 || this.config.immediate) {
|
||||
await this.parsedSaveFunc();
|
||||
return;
|
||||
}
|
||||
this.debouncedSaveFunc = debounce(this.parsedSaveFunc, delayTime);
|
||||
|
||||
await this.debouncedSaveFunc();
|
||||
};
|
||||
|
||||
private parsedSaveFunc = async () => {
|
||||
// 中间件-保存前
|
||||
const beforeSavePayload = await getPayloadByFormatter<ScopeStateType>(
|
||||
this.nextState,
|
||||
this.config?.middleware?.onBeforeSave,
|
||||
);
|
||||
// 生命周期-保存前
|
||||
await this.config?.eventCallBacks?.onBeforeSave?.({
|
||||
key: this.config.key,
|
||||
data: beforeSavePayload,
|
||||
});
|
||||
|
||||
console.log('beforeSavePayload:>>', beforeSavePayload);
|
||||
try {
|
||||
await this.config.saveRequest(
|
||||
beforeSavePayload,
|
||||
this.config.key,
|
||||
this.diff,
|
||||
);
|
||||
|
||||
// 中间件-保存后
|
||||
const afterSavePayload = await getPayloadByFormatter<ScopeStateType>(
|
||||
this.nextState,
|
||||
this.config?.middleware?.onAfterSave,
|
||||
);
|
||||
console.log('afterSavePayload:>>', afterSavePayload);
|
||||
|
||||
// 生命周期-保存后
|
||||
await this.config?.eventCallBacks?.onAfterSave?.({
|
||||
key: this.config.key,
|
||||
data: afterSavePayload,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('error:>>', error);
|
||||
// 生命周期-异常
|
||||
this.config?.eventCallBacks?.onError?.({
|
||||
key: this.config.key,
|
||||
error: error as Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 取消订阅
|
||||
*/
|
||||
public close = () => {
|
||||
this.debouncedSaveFunc?.flush();
|
||||
this.unsubscribe();
|
||||
this.unobserver?.();
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取状态变更带来的触发延时时间
|
||||
* @param prevState selector 选择的 store 的内容
|
||||
* @param diffChange 前后比对的diff
|
||||
* @returns 延时时间
|
||||
*/
|
||||
private getTriggerDelayTime = (
|
||||
prevState?: ScopeStateType,
|
||||
diffChange?: Diff<ScopeStateType, ScopeStateType>[],
|
||||
) => {
|
||||
const configDebounce = this.config.debounce;
|
||||
|
||||
if (!configDebounce) {
|
||||
return DebounceTime.Immediate;
|
||||
}
|
||||
|
||||
if (isFunction(configDebounce)) {
|
||||
return configDebounce();
|
||||
}
|
||||
|
||||
if (!isObject(configDebounce)) {
|
||||
return configDebounce;
|
||||
}
|
||||
|
||||
if (!diffChange || diffChange.length === 0) {
|
||||
return configDebounce.default;
|
||||
}
|
||||
|
||||
const targetDelayTimes: number[] = [];
|
||||
for (const change of diffChange) {
|
||||
const changePath = change.path;
|
||||
const debouncePath = this.getdebouncePath(changePath);
|
||||
|
||||
if (
|
||||
!changePath ||
|
||||
!has(prevState, changePath) ||
|
||||
typeof debouncePath === 'number'
|
||||
) {
|
||||
targetDelayTimes.push(configDebounce.default);
|
||||
continue;
|
||||
}
|
||||
const debounceType = get(
|
||||
configDebounce,
|
||||
debouncePath,
|
||||
configDebounce.default,
|
||||
);
|
||||
if (!isObject(debounceType)) {
|
||||
targetDelayTimes.push(debounceType);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!debounceType.arrayType) {
|
||||
targetDelayTimes.push(configDebounce.default);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isObject(debounceType.action as DebounceTime)) {
|
||||
targetDelayTimes.push(debounceType.action as DebounceTime);
|
||||
} else {
|
||||
const kind =
|
||||
change.kind === 'A' && change.item?.kind
|
||||
? change.item?.kind
|
||||
: change.kind;
|
||||
const triggerKind = debounceType.action[kind];
|
||||
targetDelayTimes.push(triggerKind);
|
||||
}
|
||||
}
|
||||
|
||||
return Math.min(...targetDelayTimes);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取变更与 trigger 声明配置对应的 key
|
||||
* @param changePath diff path
|
||||
* @returns path key
|
||||
*/
|
||||
private getdebouncePath = (changePath?: PathType[]) => {
|
||||
if (!changePath) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const indexPath = path => typeof path === 'number';
|
||||
const isArrayPath = changePath.some(indexPath);
|
||||
|
||||
if (isArrayPath) {
|
||||
return changePath[0];
|
||||
}
|
||||
|
||||
return changePath.join('.');
|
||||
};
|
||||
}
|
||||
25
frontend/packages/studio/autosave/src/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { AutosaveManager } from './core/manager';
|
||||
export {
|
||||
DebounceTime,
|
||||
EventCallBacks,
|
||||
SaveRequest,
|
||||
HostedObserverConfig,
|
||||
} from './type';
|
||||
|
||||
export type { Diff } from 'deep-diff';
|
||||
152
frontend/packages/studio/autosave/src/type/index.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { type StoreApi, type UseBoundStore } from 'zustand';
|
||||
import type { Diff } from 'deep-diff';
|
||||
|
||||
/**
|
||||
* 防抖延迟时间
|
||||
* @readonly
|
||||
* @enum {number}
|
||||
*/
|
||||
export enum DebounceTime {
|
||||
/** 用于需要立即响应的保存操作,如按钮或下拉选择等操作 */
|
||||
Immediate = 0,
|
||||
/** 用于需要较短时间响应的保存操作,如拖拽等操作 */
|
||||
Medium = 500,
|
||||
/** 适合于文本输入等操作 */
|
||||
Long = 1000,
|
||||
}
|
||||
|
||||
/** trigger 配置声明时的函数形式,用于在运行时指定字段触发时机 */
|
||||
export type FunctionDebounceTime = () => DebounceTime;
|
||||
|
||||
/** trigger 配置声明时的数组形式,用于分别指定数组内容变化时的触发时机 */
|
||||
export interface ArrayDebounceTime {
|
||||
arrayType: boolean;
|
||||
action:
|
||||
| DebounceTime
|
||||
| {
|
||||
N?: DebounceTime;
|
||||
D?: DebounceTime;
|
||||
E?: DebounceTime;
|
||||
};
|
||||
}
|
||||
|
||||
/** trigger 配置声明时的对象形式,用于分别指定多字段触发时机 */
|
||||
export interface ObjectDebounceTime {
|
||||
default: DebounceTime;
|
||||
[index: string]: DebounceTime | ArrayDebounceTime;
|
||||
}
|
||||
|
||||
export type DebounceConfig =
|
||||
| DebounceTime
|
||||
| ObjectDebounceTime
|
||||
| FunctionDebounceTime;
|
||||
|
||||
export type FlexibleState<T> = T | any;
|
||||
|
||||
export type HostedObserverConfig<StoreType, ScopeKey, ScopeStateType> = Omit<
|
||||
AutosaveObserverConfig<StoreType, ScopeKey, ScopeStateType>,
|
||||
'saveRequest' | 'eventCallBacks' | 'unobserver' | 'immediate'
|
||||
>;
|
||||
|
||||
export type AutosaveObserverProps<StoreType, ScopeKey, ScopeStateType> =
|
||||
AutosaveObserverConfig<StoreType, ScopeKey, ScopeStateType> & {
|
||||
store: UseBoundStore<StoreApi<StoreType>>;
|
||||
};
|
||||
|
||||
export type SelectorType<StoreType, ScopeStateType> =
|
||||
| ((store: StoreType) => ScopeStateType)
|
||||
| {
|
||||
deps: ((store: StoreType) => ScopeStateType)[];
|
||||
transformer: (...args: ScopeStateType[]) => FlexibleState<ScopeStateType>;
|
||||
};
|
||||
|
||||
export interface AutosaveObserverConfig<StoreType, ScopeKey, ScopeStateType> {
|
||||
/** 被托管的数据字段的类型 */
|
||||
key: ScopeKey;
|
||||
/** 防抖延迟时间 */
|
||||
debounce?: DebounceConfig;
|
||||
/** store 需要被监听的属性选择器,支持配置依赖 */
|
||||
selector: SelectorType<StoreType, ScopeStateType>;
|
||||
/** 中间件 支持业务链式处理监听数据 */
|
||||
middleware?: MiddlewareHanderMap<ScopeStateType>;
|
||||
/** 是否立即保存当前字段 */
|
||||
immediate?: boolean;
|
||||
/** 保存的请求 */
|
||||
saveRequest: SaveRequest<ScopeStateType, ScopeKey>;
|
||||
/** 被托管的数据取消订阅时进行的回调 */
|
||||
unobserver?: () => void;
|
||||
/** 生命周期 */
|
||||
eventCallBacks?: EventCallBacks<ScopeStateType, ScopeKey>;
|
||||
}
|
||||
|
||||
export type SaveRequest<ScopeStateType, ScopeKey> = (
|
||||
payload: FlexibleState<ScopeStateType>,
|
||||
key: ScopeKey,
|
||||
diff: Diff<ScopeStateType, ScopeStateType>[],
|
||||
) => Promise<FlexibleState<ScopeStateType>>;
|
||||
|
||||
export type SaveMiddlewareHander<ScopeStateType> = (
|
||||
data: FlexibleState<ScopeStateType>,
|
||||
) => Promise<FlexibleState<ScopeStateType>> | FlexibleState<ScopeStateType>;
|
||||
|
||||
export interface MiddlewareHanderMap<ScopeStateType> {
|
||||
/** 生命周期-检测变更后 */
|
||||
onBeforeSave?: SaveMiddlewareHander<ScopeStateType>;
|
||||
/** 生命周期-成功保存后 */
|
||||
onAfterSave?: SaveMiddlewareHander<ScopeStateType>;
|
||||
}
|
||||
|
||||
export interface EventCallBacks<ScopeStateType, ScopeKey> {
|
||||
/** 生命周期-检测变更后 */
|
||||
onBeforeSave?: (params: {
|
||||
data: FlexibleState<ScopeStateType>;
|
||||
key: ScopeKey;
|
||||
}) => void | Promise<void>;
|
||||
/** 生命周期-成功保存后 */
|
||||
onAfterSave?: (params: {
|
||||
data: FlexibleState<ScopeStateType>;
|
||||
key: ScopeKey;
|
||||
}) => void | Promise<void>;
|
||||
/** 生命周期-异常 */
|
||||
onError?: (params: { error: Error; key: ScopeKey }) => void;
|
||||
}
|
||||
|
||||
// 比对出来的被变化的 key 的 path, number 形式对应数组
|
||||
export type PathType = string | number;
|
||||
|
||||
export interface UseStoreType<StoreType, ScopeStateType> {
|
||||
subscribe: {
|
||||
(
|
||||
listener: (
|
||||
selectedState: ScopeStateType,
|
||||
previousSelectedState: ScopeStateType,
|
||||
) => void,
|
||||
): () => void;
|
||||
<U>(
|
||||
selector: (state: StoreType) => U,
|
||||
listener: (selectedState: U, previousSelectedState: U) => void,
|
||||
options?: {
|
||||
equalityFn?: (a: U, b: U) => boolean;
|
||||
fireImmediately?: boolean;
|
||||
},
|
||||
): () => void;
|
||||
};
|
||||
}
|
||||
50
frontend/packages/studio/autosave/src/utils/index.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
isObject as isObjectBase,
|
||||
isFunction as isFunctionBase,
|
||||
} from 'lodash-es';
|
||||
|
||||
import {
|
||||
type DebounceConfig,
|
||||
type ObjectDebounceTime,
|
||||
type SaveMiddlewareHander,
|
||||
type FunctionDebounceTime,
|
||||
} from '../type/index';
|
||||
|
||||
export function isFunction(
|
||||
value: DebounceConfig,
|
||||
): value is FunctionDebounceTime {
|
||||
return isFunctionBase(value);
|
||||
}
|
||||
|
||||
export function isObject(value: DebounceConfig): value is ObjectDebounceTime {
|
||||
return isObjectBase(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取保存接口调用时候需要的参数
|
||||
*/
|
||||
export const getPayloadByFormatter = async <T>(
|
||||
state: T,
|
||||
formatter?: SaveMiddlewareHander<T>,
|
||||
) => {
|
||||
if (formatter) {
|
||||
return await formatter(state);
|
||||
}
|
||||
return state;
|
||||
};
|
||||
33
frontend/packages/studio/autosave/tsconfig.build.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"types": [],
|
||||
"module": "ESNext",
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../arch/bot-api/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/logger/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/eslint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/stylelint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/ts-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/vitest-config/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/studio/autosave/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
],
|
||||
"exclude": ["**/*"]
|
||||
}
|
||||
18
frontend/packages/studio/autosave/tsconfig.misc.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"include": ["__tests__", "__mocks__", "vitest.config.ts", "setup-vitest.ts"],
|
||||
"exclude": ["./dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"rootDir": "./",
|
||||
"outDir": "./dist",
|
||||
"types": ["vitest/globals"],
|
||||
"module": "ESNext"
|
||||
}
|
||||
}
|
||||
25
frontend/packages/studio/autosave/vitest.config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { defineConfig } from '@coze-arch/vitest-config';
|
||||
|
||||
export default defineConfig({
|
||||
dirname: __dirname,
|
||||
preset: 'web',
|
||||
test: {
|
||||
setupFiles: ['./setup-vitest.ts'],
|
||||
},
|
||||
});
|
||||
31
frontend/packages/studio/bot-utils/.storybook/main.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { mergeConfig } from 'vite';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
|
||||
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
||||
const config = {
|
||||
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.tsx'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-onboarding',
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
viteFinal: config =>
|
||||
mergeConfig(config, {
|
||||
plugins: [
|
||||
svgr({
|
||||
svgrOptions: {
|
||||
native: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
};
|
||||
export default config;
|
||||
14
frontend/packages/studio/bot-utils/.storybook/preview.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/** @type { import('@storybook/react').Preview } */
|
||||
const preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
5
frontend/packages/studio/bot-utils/.stylelintrc.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
16
frontend/packages/studio/bot-utils/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# @coze-studio/bot-utils
|
||||
|
||||
> Project template for react component with storybook.
|
||||
|
||||
## Features
|
||||
|
||||
- [x] eslint & ts
|
||||
- [x] esm bundle
|
||||
- [x] umd bundle
|
||||
- [x] storybook
|
||||
|
||||
## Commands
|
||||
|
||||
- init: `rush update`
|
||||
- dev: `npm run dev`
|
||||
- build: `npm run build`
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { getSlardarInstance } from '@coze-arch/logger';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Toast } from '@coze-arch/coze-design';
|
||||
|
||||
import { withSlardarIdButton } from '../src/with-slardar-id-button';
|
||||
|
||||
const mockSlardarInstance = {
|
||||
config: vi.fn(() => ({ sessionId: 'test-session-id' })),
|
||||
};
|
||||
|
||||
vi.mock('@coze-arch/logger', () => ({
|
||||
getSlardarInstance: vi.fn(() => mockSlardarInstance),
|
||||
}));
|
||||
|
||||
// 模拟依赖
|
||||
vi.mock('copy-to-clipboard', () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@coze-arch/coze-design', () => ({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
Button: ({ children, onClick, className, size, color }: any) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
data-size={size}
|
||||
data-color={color}
|
||||
data-testid="button"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
Toast: {
|
||||
success: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@coze-arch/i18n', () => ({
|
||||
I18n: {
|
||||
t: vi.fn(key => {
|
||||
if (key === 'copy_session_id') {
|
||||
return '复制会话ID';
|
||||
}
|
||||
if (key === 'error_id_copy_success') {
|
||||
return '复制成功';
|
||||
}
|
||||
return key;
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('withSlardarIdButton', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('应该正确渲染传入的节点和按钮', () => {
|
||||
const testNode = <div data-testid="test-node">测试节点</div>;
|
||||
|
||||
render(withSlardarIdButton(testNode));
|
||||
|
||||
expect(screen.getByTestId('test-node')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('test-node')).toHaveTextContent('测试节点');
|
||||
expect(screen.getByTestId('button')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('button')).toHaveTextContent('复制会话ID');
|
||||
});
|
||||
|
||||
it('按钮应该有正确的属性', () => {
|
||||
render(withSlardarIdButton(<div>测试</div>));
|
||||
|
||||
const button = screen.getByTestId('button');
|
||||
expect(button).toHaveAttribute('data-size', 'small');
|
||||
expect(button).toHaveAttribute('data-color', 'primary');
|
||||
expect(button).toHaveAttribute('class', 'ml-[8px]');
|
||||
});
|
||||
|
||||
it('点击按钮时应该复制会话ID并显示成功提示', () => {
|
||||
render(withSlardarIdButton(<div>测试</div>));
|
||||
|
||||
const button = screen.getByTestId('button');
|
||||
fireEvent.click(button);
|
||||
|
||||
// 验证 slardar.config 被调用
|
||||
expect(getSlardarInstance).toHaveBeenCalled();
|
||||
expect(mockSlardarInstance.config).toHaveBeenCalled();
|
||||
|
||||
// 验证 copy 被调用,且参数正确
|
||||
expect(copy).toHaveBeenCalledWith('test-session-id');
|
||||
|
||||
// 验证 Toast.success 被调用,且参数正确
|
||||
expect(Toast.success).toHaveBeenCalledWith('复制成功');
|
||||
});
|
||||
|
||||
it('当 sessionId 为空时应该复制空字符串', () => {
|
||||
// 模拟 sessionId 为 undefined
|
||||
vi.mocked(mockSlardarInstance.config).mockReturnValueOnce({
|
||||
sessionId: undefined,
|
||||
});
|
||||
|
||||
render(withSlardarIdButton(<div>测试</div>));
|
||||
|
||||
const button = screen.getByTestId('button');
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(copy).toHaveBeenCalledWith('');
|
||||
});
|
||||
|
||||
it('应该使用正确的 i18n 键获取文本', () => {
|
||||
render(withSlardarIdButton(<div>测试</div>));
|
||||
|
||||
fireEvent.click(screen.getByTestId('button'));
|
||||
|
||||
expect(I18n.t).toHaveBeenCalledWith('copy_session_id');
|
||||
expect(I18n.t).toHaveBeenCalledWith('error_id_copy_success');
|
||||
});
|
||||
});
|
||||
12
frontend/packages/studio/bot-utils/config/rush-project.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"codecov": {
|
||||
"coverage": 0,
|
||||
"incrementCoverage": 0
|
||||
}
|
||||
}
|
||||
7
frontend/packages/studio/bot-utils/eslint.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
});
|
||||
52
frontend/packages/studio/bot-utils/package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "@coze-studio/bot-utils",
|
||||
"version": "0.0.1",
|
||||
"description": "utils",
|
||||
"license": "Apache-2.0",
|
||||
"author": "liushuoyan@bytedance.com",
|
||||
"maintainers": [],
|
||||
"main": "src/index.tsx",
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
|
||||
"@coze-arch/i18n": "workspace:*",
|
||||
"@coze-arch/logger": "workspace:*",
|
||||
"classnames": "^2.3.2",
|
||||
"copy-to-clipboard": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/eslint-config": "workspace:*",
|
||||
"@coze-arch/stylelint-config": "workspace:*",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@coze-arch/vitest-config": "workspace:*",
|
||||
"@rsbuild/core": "1.1.13",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/node": "18.18.9",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"@vitest/coverage-v8": "~3.0.5",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"react-is": ">= 16.8.0",
|
||||
"styled-components": ">= 2",
|
||||
"stylelint": "^15.11.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-svgr": "~3.3.0",
|
||||
"vitest": "~3.0.5",
|
||||
"webpack": "~5.91.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.2.0",
|
||||
"react-dom": ">=18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
17
frontend/packages/studio/bot-utils/src/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { withSlardarIdButton } from './with-slardar-id-button';
|
||||
17
frontend/packages/studio/bot-utils/src/typings.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { getSlardarInstance } from '@coze-arch/logger';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Button, Toast } from '@coze-arch/coze-design';
|
||||
|
||||
export const withSlardarIdButton = (node: ReactNode) => {
|
||||
const copySlardarId = () => {
|
||||
const id = getSlardarInstance()?.config()?.sessionId;
|
||||
copy(id ?? '');
|
||||
Toast.success(I18n.t('error_id_copy_success'));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row justify-center items-center">
|
||||
{node}
|
||||
<Button
|
||||
className="ml-[8px]"
|
||||
onClick={copySlardarId}
|
||||
size="small"
|
||||
color="primary"
|
||||
>
|
||||
{I18n.t('copy_session_id')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
36
frontend/packages/studio/bot-utils/tsconfig.build.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"compilerOptions": {
|
||||
"types": [],
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../arch/bot-typings/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/i18n/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/logger/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/eslint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/stylelint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/ts-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/vitest-config/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/studio/bot-utils/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
],
|
||||
"exclude": ["**/*"]
|
||||
}
|
||||
18
frontend/packages/studio/bot-utils/tsconfig.misc.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"include": ["__tests__", "stories", "vitest.config.ts", "tailwind.config.ts"],
|
||||
"exclude": ["./dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"rootDir": "./",
|
||||
"outDir": "./dist",
|
||||
"types": ["vitest/globals"],
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true
|
||||
}
|
||||
}
|
||||
23
frontend/packages/studio/bot-utils/vitest.config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { defineConfig } from '@coze-arch/vitest-config';
|
||||
|
||||
export default defineConfig({
|
||||
dirname: __dirname,
|
||||
preset: 'web',
|
||||
test: {},
|
||||
});
|
||||
5
frontend/packages/studio/common/file-kit/.stylelintrc.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
16
frontend/packages/studio/common/file-kit/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# file-kit
|
||||
|
||||
> Project template for react component with storybook.
|
||||
|
||||
## Features
|
||||
|
||||
- [x] eslint & ts
|
||||
- [x] esm bundle
|
||||
- [x] umd bundle
|
||||
- [x] storybook
|
||||
|
||||
## Commands
|
||||
|
||||
- init: `rush update`
|
||||
- dev: `npm run dev`
|
||||
- build: `npm run build`
|
||||
158
frontend/packages/studio/common/file-kit/__tests__/util.test.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getFileInfo } from '../src/util';
|
||||
import { FileTypeEnum } from '../src/const';
|
||||
|
||||
// 创建模拟的 File 对象
|
||||
function createMockFile(name: string, type: string): File {
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
size: 1024,
|
||||
lastModified: Date.now(),
|
||||
slice: () => new Blob(),
|
||||
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
|
||||
stream: () => new ReadableStream(),
|
||||
text: () => Promise.resolve(''),
|
||||
} as File;
|
||||
}
|
||||
|
||||
describe('getFileInfo', () => {
|
||||
it('应该根据文件类型识别图片文件', () => {
|
||||
const file = createMockFile('test.jpg', 'image/jpeg');
|
||||
const fileInfo = getFileInfo(file);
|
||||
|
||||
expect(fileInfo).not.toBeNull();
|
||||
expect(fileInfo?.fileType).toBe(FileTypeEnum.IMAGE);
|
||||
});
|
||||
|
||||
it('应该根据文件类型识别音频文件', () => {
|
||||
const file = createMockFile('test.mp3', 'audio/mpeg');
|
||||
const fileInfo = getFileInfo(file);
|
||||
|
||||
expect(fileInfo).not.toBeNull();
|
||||
expect(fileInfo?.fileType).toBe(FileTypeEnum.AUDIO);
|
||||
});
|
||||
|
||||
it('应该根据文件类型识别视频文件', () => {
|
||||
const file = createMockFile('test.mp4', 'video/mp4');
|
||||
const fileInfo = getFileInfo(file);
|
||||
|
||||
expect(fileInfo).not.toBeNull();
|
||||
expect(fileInfo?.fileType).toBe(FileTypeEnum.VIDEO);
|
||||
});
|
||||
|
||||
it('应该根据文件扩展名识别 PDF 文件', () => {
|
||||
const file = createMockFile('document.pdf', 'application/pdf');
|
||||
const fileInfo = getFileInfo(file);
|
||||
|
||||
expect(fileInfo).not.toBeNull();
|
||||
expect(fileInfo?.fileType).toBe(FileTypeEnum.PDF);
|
||||
});
|
||||
|
||||
it('应该根据文件扩展名识别 DOCX 文件', () => {
|
||||
const file = createMockFile(
|
||||
'document.docx',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
);
|
||||
const fileInfo = getFileInfo(file);
|
||||
|
||||
expect(fileInfo).not.toBeNull();
|
||||
expect(fileInfo?.fileType).toBe(FileTypeEnum.DOCX);
|
||||
});
|
||||
|
||||
it('应该根据文件扩展名识别 EXCEL 文件', () => {
|
||||
const file = createMockFile(
|
||||
'spreadsheet.xlsx',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
);
|
||||
const fileInfo = getFileInfo(file);
|
||||
|
||||
expect(fileInfo).not.toBeNull();
|
||||
expect(fileInfo?.fileType).toBe(FileTypeEnum.EXCEL);
|
||||
});
|
||||
|
||||
it('应该根据文件扩展名识别 CSV 文件', () => {
|
||||
const file = createMockFile('data.csv', 'text/csv');
|
||||
const fileInfo = getFileInfo(file);
|
||||
|
||||
expect(fileInfo).not.toBeNull();
|
||||
expect(fileInfo?.fileType).toBe(FileTypeEnum.CSV);
|
||||
});
|
||||
|
||||
it('应该根据文件扩展名识别压缩文件', () => {
|
||||
const file = createMockFile('archive.zip', 'application/zip');
|
||||
const fileInfo = getFileInfo(file);
|
||||
|
||||
expect(fileInfo).not.toBeNull();
|
||||
expect(fileInfo?.fileType).toBe(FileTypeEnum.ARCHIVE);
|
||||
});
|
||||
|
||||
it('应该根据文件扩展名识别代码文件', () => {
|
||||
const file = createMockFile('script.js', 'text/javascript');
|
||||
const fileInfo = getFileInfo(file);
|
||||
|
||||
expect(fileInfo).not.toBeNull();
|
||||
expect(fileInfo?.fileType).toBe(FileTypeEnum.CODE);
|
||||
});
|
||||
|
||||
it('应该根据文件扩展名识别文本文件', () => {
|
||||
const file = createMockFile('notes.txt', 'text/plain');
|
||||
const fileInfo = getFileInfo(file);
|
||||
|
||||
expect(fileInfo).not.toBeNull();
|
||||
expect(fileInfo?.fileType).toBe(FileTypeEnum.TXT);
|
||||
});
|
||||
|
||||
it('应该根据文件扩展名识别 PPT 文件', () => {
|
||||
const file = createMockFile(
|
||||
'presentation.pptx',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
);
|
||||
const fileInfo = getFileInfo(file);
|
||||
|
||||
expect(fileInfo).not.toBeNull();
|
||||
expect(fileInfo?.fileType).toBe(FileTypeEnum.PPT);
|
||||
});
|
||||
|
||||
it('应该对未知文件类型返回默认类型', () => {
|
||||
const file = createMockFile('unknown.xyz', 'application/octet-stream');
|
||||
const fileInfo = getFileInfo(file);
|
||||
|
||||
expect(fileInfo).not.toBeNull();
|
||||
expect(fileInfo?.fileType).toBe(FileTypeEnum.DEFAULT_UNKNOWN);
|
||||
});
|
||||
|
||||
it('当文件类型和扩展名不匹配时,应该优先使用文件类型判断', () => {
|
||||
// 文件名是 .txt 但 MIME 类型是图片
|
||||
const file = createMockFile('image.txt', 'image/jpeg');
|
||||
const fileInfo = getFileInfo(file);
|
||||
|
||||
expect(fileInfo).not.toBeNull();
|
||||
expect(fileInfo?.fileType).toBe(FileTypeEnum.IMAGE);
|
||||
});
|
||||
|
||||
it('当文件没有 MIME 类型时,应该使用扩展名判断', () => {
|
||||
const file = createMockFile('document.docx', '');
|
||||
const fileInfo = getFileInfo(file);
|
||||
|
||||
expect(fileInfo).not.toBeNull();
|
||||
expect(fileInfo?.fileType).toBe(FileTypeEnum.DOCX);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"codecov": {
|
||||
"coverage": 0,
|
||||
"incrementCoverage": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
});
|
||||
50
frontend/packages/studio/common/file-kit/package.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@coze-studio/file-kit",
|
||||
"version": "0.0.1",
|
||||
"description": "file kit ",
|
||||
"license": "Apache-2.0",
|
||||
"author": "liuyuhang.0@bytedance.com",
|
||||
"maintainers": [],
|
||||
"exports": {
|
||||
"./config": "./src/exports/config.ts",
|
||||
"./logic": "./src/exports/logic.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"logic": [
|
||||
"./src/exports/logic.ts"
|
||||
],
|
||||
"config": [
|
||||
"./src/exports/config.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-arch/i18n": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/eslint-config": "workspace:*",
|
||||
"@coze-arch/stylelint-config": "workspace:*",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@coze-arch/vitest-config": "workspace:*",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"@vitest/coverage-v8": "~3.0.5",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"stylelint": "^15.11.0",
|
||||
"vite-plugin-svgr": "~3.3.0",
|
||||
"vitest": "~3.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
86
frontend/packages/studio/common/file-kit/src/accept.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import {
|
||||
ZipIcon,
|
||||
VideoIcon,
|
||||
TextIcon as TxtIcon,
|
||||
ImageIcon,
|
||||
AudioIcon,
|
||||
CodeIcon,
|
||||
PptIcon,
|
||||
DocxIcon as DocIcon,
|
||||
XlsxIcon as TableIcon,
|
||||
UnknownIcon,
|
||||
} from './icon';
|
||||
import { FileTypeEnum } from './const';
|
||||
|
||||
const uploadTableConfig = {
|
||||
label: I18n.t('shortcut_modal_upload_component_file_format_table'),
|
||||
icon: TableIcon,
|
||||
};
|
||||
|
||||
const uploadDocConfig = {
|
||||
label: I18n.t('shortcut_modal_upload_component_file_format_doc'),
|
||||
icon: DocIcon,
|
||||
};
|
||||
|
||||
export const ACCEPT_UPLOAD_TYPES: Record<
|
||||
FileTypeEnum,
|
||||
{
|
||||
label: string;
|
||||
icon: string;
|
||||
}
|
||||
> = {
|
||||
[FileTypeEnum.IMAGE]: {
|
||||
label: I18n.t('shortcut_modal_upload_component_file_format_img'),
|
||||
icon: ImageIcon,
|
||||
},
|
||||
[FileTypeEnum.EXCEL]: uploadTableConfig,
|
||||
[FileTypeEnum.CSV]: uploadTableConfig,
|
||||
[FileTypeEnum.PDF]: uploadDocConfig,
|
||||
[FileTypeEnum.DOCX]: uploadDocConfig,
|
||||
[FileTypeEnum.DEFAULT_UNKNOWN]: {
|
||||
label: I18n.t('plugin_file_unknown'),
|
||||
icon: UnknownIcon,
|
||||
},
|
||||
[FileTypeEnum.AUDIO]: {
|
||||
label: I18n.t('shortcut_modal_upload_component_file_format_audio'),
|
||||
icon: AudioIcon,
|
||||
},
|
||||
[FileTypeEnum.CODE]: {
|
||||
label: I18n.t('shortcut_modal_upload_component_file_format_code'),
|
||||
icon: CodeIcon,
|
||||
},
|
||||
[FileTypeEnum.ARCHIVE]: {
|
||||
label: I18n.t('shortcut_modal_upload_component_file_format_zip'),
|
||||
icon: ZipIcon,
|
||||
},
|
||||
[FileTypeEnum.PPT]: {
|
||||
label: I18n.t('shortcut_modal_upload_component_file_format_ppt'),
|
||||
icon: PptIcon,
|
||||
},
|
||||
[FileTypeEnum.VIDEO]: {
|
||||
label: I18n.t('shortcut_modal_upload_component_file_format_video'),
|
||||
icon: VideoIcon,
|
||||
},
|
||||
[FileTypeEnum.TXT]: {
|
||||
label: I18n.t('shortcut_modal_upload_component_file_format_txt'),
|
||||
icon: TxtIcon,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon">
|
||||
<path id="Rectangle 2526" d="M1 4.99996C1 3.15901 2.49238 1.66663 4.33333 1.66663H20.3096C20.7517 1.66663 21.1756 1.84222 21.4882 2.15478L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99996Z" fill="#32A645"/>
|
||||
<path id="Rectangle 2527" opacity="0.9" d="M21 2.27018C21 2.04745 21.2693 1.93591 21.4268 2.0934L30.5732 11.2398C30.7307 11.3973 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1742 21 8.33329V2.27018Z" fill="#258832"/>
|
||||
<path id="music_note" d="M13.0833 31.25C12.1667 31.25 11.3819 30.9236 10.7292 30.2708C10.0764 29.6181 9.75 28.8333 9.75 27.9167C9.75 27 10.0764 26.2153 10.7292 25.5625C11.3819 24.9097 12.1667 24.5833 13.0833 24.5833C13.4028 24.5833 13.6979 24.6215 13.9688 24.6979C14.2396 24.7743 14.5 24.8889 14.75 25.0417V17.5C14.75 16.8096 15.3096 16.25 16 16.25H21V18.75H17.0417C16.6965 18.75 16.4167 19.0298 16.4167 19.375V27.9167C16.4167 28.8333 16.0903 29.6181 15.4375 30.2708C14.7847 30.9236 14 31.25 13.0833 31.25Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon">
|
||||
<path id="Rectangle 2528" d="M1 5.00008C1 3.15913 2.49238 1.66675 4.33333 1.66675H20.3096C20.7517 1.66675 21.1756 1.84234 21.4882 2.1549L30.5118 11.1786C30.8244 11.4912 31 11.9151 31 12.3571V35.0001C31 36.841 29.5076 38.3334 27.6667 38.3334H4.33333C2.49238 38.3334 1 36.841 1 35.0001V5.00008Z" fill="#336DF4"/>
|
||||
<path id="Rectangle 2529" opacity="0.7" d="M21 2.2703C21 2.04757 21.2693 1.93603 21.4268 2.09352L30.5732 11.24C30.7307 11.3975 30.6192 11.6667 30.3964 11.6667H24.3333C22.4924 11.6667 21 10.1744 21 8.33341V2.2703Z" fill="#0442D2"/>
|
||||
<path id="icon_file_code_nor" d="M13.1583 19.1273L8.94653 23.5605L13.1583 27.9936C13.3094 28.1527 13.3067 28.4078 13.1523 28.5635L13.1506 28.5652L12.5848 29.1289C12.4301 29.2831 12.1836 29.2796 12.033 29.1211L7.01788 23.8423C6.86904 23.6857 6.86904 23.4352 7.01788 23.2786L12.033 17.9998C12.1836 17.8414 12.4301 17.8379 12.5848 17.992L13.1506 18.5558C13.306 18.7105 13.3101 18.9657 13.1598 19.1257L13.1583 19.1273ZM23.1935 23.5605L19.2826 19.1273C19.1423 18.9683 19.1448 18.7131 19.2882 18.5574L19.2897 18.5558L19.8152 17.992C19.9588 17.8379 20.1877 17.8414 20.3275 17.9998L24.9844 23.2786C25.1226 23.4352 25.1226 23.6857 24.9844 23.8423L20.3275 29.1211C20.1877 29.2796 19.9588 29.2831 19.8152 29.1289L19.2897 28.5652C19.1455 28.4104 19.1416 28.1552 19.2812 27.9952L19.2826 27.9936L23.1935 23.5605ZM17.0448 15.1539L17.8645 15.2418C18.0882 15.2658 18.2487 15.4536 18.2228 15.6611L16.2662 31.1805C16.2401 31.3879 16.0378 31.5364 15.8143 31.5125L14.9946 31.4246C14.7708 31.4006 14.6104 31.2128 14.6363 31.0052L16.5928 15.4858C16.619 15.2785 16.8212 15.1299 17.0448 15.1539Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon">
|
||||
<path id="Rectangle 2528" d="M4 4.99997C4 3.15902 5.19391 1.66664 6.66667 1.66664H19.4477C19.8013 1.66664 20.1405 1.84224 20.3905 2.1548L27.6095 11.1785C27.8595 11.491 28 11.915 28 12.357V35C28 36.8409 26.8061 38.3333 25.3333 38.3333H6.66667C5.19391 38.3333 4 36.8409 4 35V4.99997Z" fill="#336DF4"/>
|
||||
<path id="Rectangle 2530" d="M1 4.99997C1 3.15902 2.49238 1.66664 4.33333 1.66664H20.3096C20.7517 1.66664 21.1756 1.84224 21.4882 2.1548L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99997Z" fill="#336DF4"/>
|
||||
<path id="Rectangle 2531" opacity="0.7" d="M21 2.27019C21 2.04747 21.2693 1.93593 21.4268 2.09342L30.5732 11.2399C30.7307 11.3974 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1743 21 8.33331V2.27019Z" fill="#0442D2"/>
|
||||
<path id="icon_file_word_nor" d="M16.0074 20.4158L13.5499 29.51C13.52 29.6204 13.4199 29.697 13.3056 29.697H11.8581C11.7449 29.697 11.6455 29.6219 11.6147 29.513L8.2821 17.7463C8.24402 17.6119 8.32215 17.472 8.45662 17.4339C8.47905 17.4275 8.50226 17.4243 8.52557 17.4243H9.97719C10.0926 17.4243 10.1933 17.5023 10.2222 17.614L12.5937 26.7863L15.0564 17.6118C15.086 17.5012 15.1863 17.4243 15.3008 17.4243H16.7153C16.8299 17.4243 16.9302 17.5014 16.9598 17.6121L19.4067 26.784L21.7776 17.614C21.8064 17.5023 21.9072 17.4243 22.0226 17.4243H23.4742C23.6139 17.4243 23.7272 17.5376 23.7272 17.6774C23.7272 17.7007 23.724 17.7239 23.7177 17.7463L20.3851 29.513C20.3542 29.6219 20.2548 29.697 20.1416 29.697H18.6942C18.5797 29.697 18.4795 29.6202 18.4498 29.5096L16.0074 20.4158Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon">
|
||||
<path id="Rectangle 2528" d="M1 4.99996C1 3.15901 2.49238 1.66663 4.33333 1.66663H20.3096C20.7517 1.66663 21.1756 1.84222 21.4882 2.15478L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99996Z" fill="#FFC60A"/>
|
||||
<path id="Rectangle 2529" opacity="0.8" d="M21 2.27018C21 2.04745 21.2693 1.93591 21.4268 2.0934L30.5732 11.2398C30.7307 11.3973 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1742 21 8.33329V2.27018Z" fill="#D99904"/>
|
||||
<g id="形状结合">
|
||||
<path d="M9.95312 16.6666C9.03265 16.6666 8.28646 17.4128 8.28646 18.3333V18.6363C8.28646 19.5568 9.03265 20.303 9.95312 20.303H10.2562C11.1766 20.303 11.9228 19.5568 11.9228 18.6363V18.3333C11.9228 17.4128 11.1766 16.6666 10.2562 16.6666H9.95312Z" fill="white"/>
|
||||
<path d="M23.8254 21.2663C24.4434 20.5989 25.5592 21.0362 25.5592 21.9457V30.8333C25.5592 31.2935 25.1861 31.6666 24.7259 31.6666L8.33494 31.6666C7.76879 31.6666 7.46033 31.0055 7.82405 30.5716L12.7 24.7554C13.3661 23.9609 14.5883 23.9609 15.2544 24.7554L17.792 27.7824L23.8254 21.2663Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon">
|
||||
<path id="Rectangle 2526" d="M1 4.99997C1 3.15902 2.49238 1.66664 4.33333 1.66664H20.3096C20.7517 1.66664 21.1756 1.84224 21.4882 2.1548L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99997Z" fill="#F54A45"/>
|
||||
<path id="Rectangle 2527" d="M21 2.27019C21 2.04747 21.2693 1.93593 21.4268 2.09342L30.5732 11.2399C30.7307 11.3974 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1743 21 8.33331V2.27019Z" fill="#C02A26"/>
|
||||
<path id="icon_file_pdf_nor" d="M25.2516 25.9184C24.8379 25.4341 23.9896 25.1986 22.6583 25.1986C21.8845 25.1986 20.8185 25.2181 19.7472 25.3771C16.821 23.2676 16.1352 21.0015 16.1352 21.0015C16.1352 21.0015 16.635 19.7471 16.6667 17.6982C16.6868 16.403 16.4814 15.437 15.9578 15.0218C15.7347 14.8449 15.4108 14.697 15.0848 14.697C14.8303 14.697 14.592 14.7707 14.3967 14.9115C12.8756 16.009 14.5363 21.1828 14.5808 21.3207C13.8629 23.0626 12.9586 24.9081 12.0279 26.5297C11.7255 27.0565 11.7255 27.0667 11.5223 27.3059C11.5223 27.3059 8.85954 28.6265 7.61092 30.0907C6.90544 30.9181 6.88254 31.4861 6.92 31.9118C6.98036 32.4223 7.63116 32.8788 8.28633 32.8788C8.3135 32.8788 8.34084 32.878 8.36762 32.8763C9.03349 32.8358 9.77415 32.6526 10.5983 31.873C11.195 31.3086 11.866 29.7758 12.7286 28.276C15.2034 27.5818 17.3814 27.0874 19.2066 26.8055C20.5451 27.5157 22.5366 28.3201 23.8922 28.3201C24.3469 28.3201 24.7128 28.2287 24.9795 28.0484C25.2986 27.8328 25.4341 27.5641 25.5183 27.0667C25.6025 26.5693 25.4853 26.1921 25.2516 25.9184ZM22.342 26.697C23.578 26.697 24.2472 26.9151 24.591 27.098C24.697 27.1544 24.7742 27.2089 24.8289 27.2543C24.732 27.3294 24.5414 27.4243 24.1971 27.4243C23.626 27.4243 22.8765 27.1823 21.9623 26.7036C22.0924 26.6992 22.219 26.697 22.342 26.697ZM15.2432 15.7931L15.2453 15.7879C15.4383 15.93 15.5284 16.9281 15.5103 17.5068C15.486 18.2834 15.4802 18.5835 15.3827 19.0606C15.1185 18.0659 15.0997 16.2779 15.2432 15.7931ZM15.3042 23.0606C15.907 24.0536 16.6715 25.0591 17.5328 25.827C15.8516 26.1874 14.4556 26.5181 13.4512 26.8696C14.5278 25.005 15.2335 23.2454 15.3042 23.0606ZM8.51592 31.4738C8.66804 31.2488 9.08367 30.8128 10.1377 29.9697C9.57203 31.2728 8.93672 31.4738 8.34612 31.7878C8.39069 31.6845 8.44678 31.5761 8.51592 31.4738Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon">
|
||||
<path id="Rectangle 2530" d="M1 5.00008C1 3.15913 2.49238 1.66675 4.33333 1.66675H20.3096C20.7517 1.66675 21.1756 1.84234 21.4882 2.1549L30.5118 11.1786C30.8244 11.4912 31 11.9151 31 12.3571V35.0001C31 36.841 29.5076 38.3334 27.6667 38.3334H4.33333C2.49238 38.3334 1 36.841 1 35.0001V5.00008Z" fill="#FF811A"/>
|
||||
<path id="Rectangle 2531" opacity="0.8" d="M21 2.2703C21 2.04757 21.2693 1.93603 21.4268 2.09352L30.5732 11.24C30.7307 11.3975 30.6192 11.6667 30.3964 11.6667H24.3333C22.4924 11.6667 21 10.1744 21 8.33341V2.2703Z" fill="#ED6D0C"/>
|
||||
<path id="Subtract" d="M13.3038 30.8251V24.8477H16.1402C16.8218 24.8477 17.5001 24.7878 18.1749 24.668C18.8621 24.546 19.479 24.3316 20.0243 24.0244C20.5793 23.7117 21.032 23.2837 21.3799 22.7429C21.7332 22.1938 21.9077 21.5053 21.9077 20.6814C21.9077 20.1383 21.8345 19.6179 21.688 19.1206C21.5371 18.6083 21.2705 18.1546 20.8902 17.7622C20.5082 17.3681 19.9947 17.0617 19.3529 16.8413C18.717 16.6229 17.92 16.5151 16.9605 16.5151H11.6886C11.5586 16.5151 11.4531 16.6206 11.4531 16.7506V30.8251C11.4531 30.9552 11.5586 31.0606 11.6886 31.0606H13.0683C13.1983 31.0606 13.3038 30.9552 13.3038 30.8251ZM17.8627 23.1217C17.3692 23.1978 16.8082 23.236 16.1801 23.236H13.3037V18.1268H16.5803C17.8243 18.1268 18.7172 18.3424 19.2601 18.7594C19.7912 19.1673 20.057 19.7679 20.057 20.5837C20.057 21.1479 19.9675 21.5968 19.7935 21.9308C19.6202 22.2634 19.3773 22.5236 19.062 22.7154C18.738 22.9123 18.3387 23.0483 17.8627 23.1217Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon">
|
||||
<path id="Rectangle 2526" d="M1 4.99997C1 3.15902 2.49238 1.66664 4.33333 1.66664H20.3096C20.7517 1.66664 21.1756 1.84224 21.4882 2.1548L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99997Z" fill="#336DF4"/>
|
||||
<path id="Rectangle 2527" opacity="0.7" d="M21 2.27019C21 2.04747 21.2693 1.93593 21.4268 2.09342L30.5732 11.2399C30.7307 11.3974 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1743 21 8.33331V2.27019Z" fill="#0442D2"/>
|
||||
<path id="Union" d="M16.9089 17.8787V30.3787C16.9089 30.5043 16.8072 30.606 16.6816 30.606H15.318C15.1925 30.606 15.0907 30.5043 15.0907 30.3787V17.8787H9.40891C9.28339 17.8787 9.18164 17.777 9.18164 17.6515V16.2878C9.18164 16.1623 9.28339 16.0606 9.40891 16.0606H22.5907C22.7163 16.0606 22.818 16.1623 22.818 16.2878V17.6515C22.818 17.777 22.7163 17.8787 22.5907 17.8787H16.9089Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon">
|
||||
<path id="Rectangle 2526" d="M1 5.00008C1 3.15913 2.49238 1.66675 4.33333 1.66675H20.3096C20.7517 1.66675 21.1756 1.84234 21.4882 2.1549L30.5118 11.1786C30.8244 11.4912 31 11.9151 31 12.3571V35.0001C31 36.841 29.5076 38.3334 27.6667 38.3334H4.33333C2.49238 38.3334 1 36.841 1 35.0001V5.00008Z" fill="#336DF4"/>
|
||||
<path id="Rectangle 2527" opacity="0.7" d="M21 2.2703C21 2.04757 21.2693 1.93603 21.4268 2.09352L30.5732 11.24C30.7307 11.3975 30.6192 11.6667 30.3964 11.6667H24.3333C22.4924 11.6667 21 10.1744 21 8.33341V2.2703Z" fill="#0442D2"/>
|
||||
<path id="Union" d="M16.907 17.8787V30.3787C16.907 30.5042 16.8052 30.606 16.6797 30.606H15.3161C15.1905 30.606 15.0888 30.5042 15.0888 30.3787V17.8787H9.40696C9.28144 17.8787 9.17969 17.777 9.17969 17.6515V16.2878C9.17969 16.1623 9.28144 16.0605 9.40696 16.0605H22.5888C22.7143 16.0605 22.8161 16.1623 22.8161 16.2878V17.6515C22.8161 17.777 22.7143 17.8787 22.5888 17.8787H16.907Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon">
|
||||
<path id="Rectangle 2530" opacity="0.9" d="M1 4.99997C1 3.15902 2.49238 1.66664 4.33333 1.66664H20.3096C20.7517 1.66664 21.1756 1.84224 21.4882 2.1548L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99997Z" fill="#8F959E"/>
|
||||
<path id="Rectangle 2531" opacity="0.6" d="M21 2.27019C21 2.04747 21.2693 1.93593 21.4268 2.09342L30.5732 11.2399C30.7307 11.3974 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1743 21 8.33331V2.27019Z" fill="#646A73"/>
|
||||
<path id="?" d="M19.8136 16.4407C18.904 15.579 17.7103 15.1514 16.2258 15.1514C14.5581 15.1514 13.2511 15.686 12.3042 16.775C11.5021 17.7108 11.07 18.6077 11.0082 20.0217C11.0067 20.055 11.0055 20.1162 11.0044 20.187C11.001 20.4179 11.1884 20.6059 11.4195 20.6059H12.4045C12.6337 20.6059 12.8192 20.4209 12.823 20.1918C12.8242 20.1228 12.8255 20.0633 12.8271 20.0315C12.8755 19.0547 13.1303 18.5436 13.585 17.9752C14.1592 17.2098 15.0014 16.8332 16.1576 16.8332C17.1893 16.8332 17.9629 17.0963 18.4969 17.6303C19.0058 18.1605 19.2712 18.8823 19.2712 19.7877C19.2712 20.4058 19.0472 20.9945 18.5918 21.58C18.4467 21.7613 18.2378 21.977 17.565 22.6498C16.5022 23.579 15.8458 24.3349 15.4991 25.078C15.214 25.648 15.0687 26.2959 15.0687 26.9923V27.462C15.0687 27.6921 15.2552 27.8786 15.4853 27.8786H16.4929C16.723 27.8786 16.9096 27.6921 16.9096 27.462V26.9923C16.9096 26.4021 17.0549 25.8728 17.3559 25.3568C17.5813 24.9736 17.8708 24.673 18.3754 24.2315C19.3872 23.3161 19.9485 22.7708 20.1999 22.4509C20.8038 21.6538 21.1121 20.7409 21.1121 19.7423C21.1121 18.3641 20.6794 17.2584 19.8136 16.4407ZM15.5081 29.6968C15.2779 29.6968 15.0914 29.8834 15.0914 30.1135V31.0983C15.0914 31.3285 15.2779 31.515 15.5081 31.515H16.4929C16.723 31.515 16.9096 31.3285 16.9096 31.0983V30.1135C16.9096 29.8834 16.723 29.6968 16.4929 29.6968H15.5081Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon">
|
||||
<path id="Rectangle 2528" d="M1 5.00008C1 3.15913 2.49238 1.66675 4.33333 1.66675H20.3096C20.7517 1.66675 21.1756 1.84234 21.4882 2.1549L30.5118 11.1786C30.8244 11.4912 31 11.9151 31 12.3571V35.0001C31 36.841 29.5076 38.3334 27.6667 38.3334H4.33333C2.49238 38.3334 1 36.841 1 35.0001V5.00008Z" fill="#336DF4"/>
|
||||
<path id="Rectangle 2529" opacity="0.7" d="M21 2.2703C21 2.04757 21.2693 1.93603 21.4268 2.09352L30.5732 11.24C30.7307 11.3975 30.6192 11.6667 30.3964 11.6667H24.3333C22.4924 11.6667 21 10.1744 21 8.33341V2.2703Z" fill="#0442D2"/>
|
||||
<path id="形状" d="M7.66406 17.9165C7.66406 17.2261 8.22371 16.6665 8.91406 16.6665H17.6641C18.3544 16.6665 18.9141 17.2261 18.9141 17.9165V19.4009L20.8012 18.3393C21.6345 17.8706 22.6641 18.4728 22.6641 19.4288V25.1542C22.6641 26.1102 21.6345 26.7124 20.8012 26.2437L18.9141 25.1821V26.6665C18.9141 27.3569 18.3544 27.9165 17.6641 27.9165H8.91406C8.22371 27.9165 7.66406 27.3569 7.66406 26.6665V17.9165ZM11.4141 21.6665C12.1044 21.6665 12.6641 21.1069 12.6641 20.4165C12.6641 19.7261 12.1044 19.1665 11.4141 19.1665C10.7237 19.1665 10.1641 19.7261 10.1641 20.4165C10.1641 21.1069 10.7237 21.6665 11.4141 21.6665Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon">
|
||||
<path id="Rectangle 2526" d="M1 4.99996C1 3.15901 2.49238 1.66663 4.33333 1.66663H20.3096C20.7517 1.66663 21.1756 1.84222 21.4882 2.15478L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99996Z" fill="#32A645"/>
|
||||
<path id="Rectangle 2527" opacity="0.9" d="M21 2.27018C21 2.04745 21.2693 1.93591 21.4268 2.0934L30.5732 11.2398C30.7307 11.3973 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1742 21 8.33329V2.27018Z" fill="#258832"/>
|
||||
<path id="icon_file_excel_nor" d="M10.2443 16.5151H12.064C12.1447 16.5151 12.2202 16.5548 12.266 16.6212L15.7637 21.6898L19.2798 16.6207C19.3256 16.5546 19.401 16.5151 19.4815 16.5151H21.3009C21.4365 16.5151 21.5464 16.625 21.5464 16.7606C21.5464 16.8117 21.5304 16.8615 21.5008 16.9031L16.9458 23.2925L21.8632 30.2185C21.9416 30.329 21.9156 30.4823 21.8051 30.5607C21.7636 30.5902 21.7139 30.606 21.663 30.606H19.8434C19.7628 30.606 19.6874 30.5665 19.6416 30.5002L15.7636 24.8954L11.9042 30.4998C11.8584 30.5663 11.7828 30.606 11.7021 30.606H9.88217C9.74661 30.606 9.63672 30.4962 9.63672 30.3606C9.63672 30.31 9.65237 30.2606 9.68153 30.2192L14.5623 23.2925L10.0438 16.9023C9.96558 16.7916 9.99186 16.6384 10.1025 16.5602C10.144 16.5309 10.1935 16.5151 10.2443 16.5151Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon">
|
||||
<path id="Rectangle 2528" d="M1 4.99997C1 3.15902 2.49238 1.66664 4.33333 1.66664H20.3096C20.7517 1.66664 21.1756 1.84224 21.4882 2.1548L30.5118 11.1785C30.8244 11.491 31 11.915 31 12.357V35C31 36.8409 29.5076 38.3333 27.6667 38.3333H4.33333C2.49238 38.3333 1 36.8409 1 35V4.99997Z" fill="#336DF4"/>
|
||||
<path id="Rectangle 2529" opacity="0.7" d="M21 2.27019C21 2.04747 21.2693 1.93593 21.4268 2.09342L30.5732 11.2399C30.7307 11.3974 30.6192 11.6666 30.3964 11.6666H24.3333C22.4924 11.6666 21 10.1743 21 8.33331V2.27019Z" fill="#0442D2"/>
|
||||
<path id="合并形状" d="M21.714 15.8334C22.2399 15.8334 22.6663 16.2531 22.6663 16.7709V29.8959C22.6663 30.4136 22.2399 30.8334 21.714 30.8334H10.2854C9.7594 30.8334 9.33301 30.4136 9.33301 29.8959V16.7709C9.33301 16.2531 9.7594 15.8334 10.2854 15.8334H21.714ZM17.9044 26.1459H14.0949V29.4271H17.9044V26.1459ZM16.9521 27.0834V28.1682H15.0473V27.0834H16.9521ZM17.9484 19.5834H15.9997V21.4584H14.0949V23.3334H15.9997V25.2084H17.9484V23.3334H16.0436V21.4584H17.9484V19.5834ZM15.9997 17.7084H14.0949V19.5834H15.9997V17.7084Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
40
frontend/packages/studio/common/file-kit/src/const.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type EnumToUnion } from './types/util';
|
||||
|
||||
export const enum FileTypeEnum {
|
||||
PDF = 'pdf',
|
||||
DOCX = 'docx',
|
||||
EXCEL = 'excel',
|
||||
CSV = 'csv',
|
||||
IMAGE = 'image',
|
||||
AUDIO = 'audio',
|
||||
VIDEO = 'video',
|
||||
ARCHIVE = 'archive',
|
||||
CODE = 'code',
|
||||
TXT = 'txt',
|
||||
PPT = 'ppt',
|
||||
DEFAULT_UNKNOWN = 'default_unknown',
|
||||
}
|
||||
|
||||
export type FileType = EnumToUnion<typeof FileTypeEnum>;
|
||||
|
||||
export interface TFileTypeConfig {
|
||||
fileType: FileTypeEnum;
|
||||
accept: string[];
|
||||
judge?: (file: Pick<File, 'type'>) => boolean;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ACCEPT_UPLOAD_TYPES } from '../accept';
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { FileTypeEnum } from '../const';
|
||||
export { FILE_TYPE_CONFIG } from '../file-type';
|
||||
export { getFileInfo } from '../util';
|
||||
|
||||
// types
|
||||
export type { FileType, TFileTypeConfig } from '../const';
|
||||
90
frontend/packages/studio/common/file-kit/src/file-type.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FileTypeEnum, type TFileTypeConfig } from './const';
|
||||
|
||||
/**
|
||||
* 文件类型
|
||||
* {@link
|
||||
* {@link https://www.iana.org/assignments/media-types/media-types.xhtml#image}
|
||||
*/
|
||||
export const FILE_TYPE_CONFIG: readonly TFileTypeConfig[] = [
|
||||
{
|
||||
fileType: FileTypeEnum.IMAGE,
|
||||
accept: ['image/*'],
|
||||
judge: file => file.type.startsWith('image/'),
|
||||
},
|
||||
{
|
||||
fileType: FileTypeEnum.AUDIO,
|
||||
accept: [
|
||||
'.mp3',
|
||||
'.wav',
|
||||
'.aac',
|
||||
'.flac',
|
||||
'.ogg',
|
||||
'.wma',
|
||||
'.alac',
|
||||
// .midi 和 .mid 都是MIDI(Musical Instrument Digital Interface)文件的扩展名 - GPT
|
||||
'.mid',
|
||||
'.midi',
|
||||
'.ac3',
|
||||
'.dsd',
|
||||
],
|
||||
judge: file => file.type.startsWith('audio/'),
|
||||
},
|
||||
{
|
||||
fileType: FileTypeEnum.PDF,
|
||||
accept: ['.pdf'],
|
||||
},
|
||||
{
|
||||
fileType: FileTypeEnum.DOCX,
|
||||
accept: ['.docx', '.doc'],
|
||||
},
|
||||
{
|
||||
fileType: FileTypeEnum.EXCEL,
|
||||
accept: ['.xls', '.xlsx'],
|
||||
},
|
||||
{
|
||||
fileType: FileTypeEnum.CSV,
|
||||
accept: ['.csv'],
|
||||
},
|
||||
{
|
||||
fileType: FileTypeEnum.VIDEO,
|
||||
accept: ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv'],
|
||||
judge: file => file.type.startsWith('video/'),
|
||||
},
|
||||
{
|
||||
fileType: FileTypeEnum.ARCHIVE,
|
||||
accept: ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2'],
|
||||
},
|
||||
{
|
||||
fileType: FileTypeEnum.CODE,
|
||||
accept: ['.py', '.java', '.c', '.cpp', '.js', '.html', '.css'],
|
||||
},
|
||||
{
|
||||
fileType: FileTypeEnum.TXT,
|
||||
accept: ['.txt'],
|
||||
},
|
||||
{
|
||||
fileType: FileTypeEnum.PPT,
|
||||
accept: ['.ppt', '.pptx'],
|
||||
},
|
||||
{
|
||||
fileType: FileTypeEnum.DEFAULT_UNKNOWN,
|
||||
judge: () => true,
|
||||
accept: ['*'],
|
||||
},
|
||||
];
|
||||
26
frontend/packages/studio/common/file-kit/src/icon/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { default as ZipIcon } from '../assets/file/zip-success.svg';
|
||||
export { default as XlsxIcon } from '../assets/file/xlsx-success.svg';
|
||||
export { default as VideoIcon } from '../assets/file/video-success.svg';
|
||||
export { default as TextIcon } from '../assets/file/txt-success.svg';
|
||||
export { default as PptIcon } from '../assets/file/ppt-success.svg';
|
||||
export { default as ImageIcon } from '../assets/file/image-success.svg';
|
||||
export { default as DocxIcon } from '../assets/file/docx-success.svg';
|
||||
export { default as CodeIcon } from '../assets/file/code-success.svg';
|
||||
export { default as AudioIcon } from '../assets/file/audio-success.svg';
|
||||
export { default as UnknownIcon } from '../assets/file/unknown-success.svg';
|
||||
17
frontend/packages/studio/common/file-kit/src/types/util.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type EnumToUnion<T extends Record<string, string>> = T[keyof T];
|
||||
17
frontend/packages/studio/common/file-kit/src/typings.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
28
frontend/packages/studio/common/file-kit/src/util.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FILE_TYPE_CONFIG } from './file-type';
|
||||
|
||||
// 获取文件信息
|
||||
export const getFileInfo = (file: File) => {
|
||||
const fileInfo = FILE_TYPE_CONFIG.find(({ judge, accept }) =>
|
||||
judge ? judge(file) : accept.some(ext => file.name.endsWith(ext)),
|
||||
);
|
||||
if (!fileInfo) {
|
||||
return null;
|
||||
}
|
||||
return fileInfo;
|
||||
};
|
||||
33
frontend/packages/studio/common/file-kit/tsconfig.build.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"compilerOptions": {
|
||||
"types": [],
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../arch/bot-typings/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../arch/i18n/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../../config/eslint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../../config/stylelint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../../config/ts-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../../config/vitest-config/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/studio/common/file-kit/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
],
|
||||
"exclude": ["**/*"]
|
||||
}
|
||||
18
frontend/packages/studio/common/file-kit/tsconfig.misc.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"include": ["__tests__", "stories", "vitest.config.ts", "tailwind.config.ts"],
|
||||
"exclude": ["./dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"rootDir": "./",
|
||||
"outDir": "./dist",
|
||||
"types": ["vitest/globals"],
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true
|
||||
}
|
||||
}
|
||||
22
frontend/packages/studio/common/file-kit/vitest.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { defineConfig } from '@coze-arch/vitest-config';
|
||||
|
||||
export default defineConfig({
|
||||
dirname: __dirname,
|
||||
preset: 'web',
|
||||
});
|
||||
5
frontend/packages/studio/components/.stylelintrc.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
16
frontend/packages/studio/components/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# @coze-studio/components
|
||||
|
||||
> Project template for react component with storybook.
|
||||
|
||||
## Features
|
||||
|
||||
- [x] eslint & ts
|
||||
- [x] esm bundle
|
||||
- [x] umd bundle
|
||||
- [x] storybook
|
||||
|
||||
## Commands
|
||||
|
||||
- init: `rush update`
|
||||
- dev: `npm run dev`
|
||||
- build: `npm run build`
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { AvatarName } from '../src/avatar-name';
|
||||
|
||||
describe('AvatarName', () => {
|
||||
it('should one image and @username', () => {
|
||||
const wrapper = render(
|
||||
<AvatarName
|
||||
name="BotNickName"
|
||||
username="BotUserName"
|
||||
avatar="https://sf-coze-web-cdn.coze.com/obj/coze-web-sg/obric/coze/favicon.1970.png"
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.getAllByRole('img').length).toBe(1);
|
||||
expect(wrapper.getByText(/^@BotUserName/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should two image', () => {
|
||||
const wrapper = render(
|
||||
<AvatarName
|
||||
name="BotNickName"
|
||||
username="BotUserName"
|
||||
avatar="https://sf-coze-web-cdn.coze.com/obj/coze-web-sg/obric/coze/favicon.1970.png"
|
||||
label={{
|
||||
icon: 'https://sf-coze-web-cdn.coze.com/obj/coze-web-sg/obric/coze/favicon.1970.png',
|
||||
name: 'test',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.getAllByRole('img').length).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render, fireEvent, act } from '@testing-library/react';
|
||||
|
||||
import { SelectSpaceModal } from '../src/select-space-modal';
|
||||
|
||||
const spaces = [
|
||||
{
|
||||
id: 'space0',
|
||||
name: 'space0',
|
||||
hide_operation: false,
|
||||
},
|
||||
{
|
||||
id: 'space1',
|
||||
name: 'space1',
|
||||
hide_operation: true,
|
||||
},
|
||||
{
|
||||
id: 'space2',
|
||||
name: 'space2',
|
||||
hide_operation: false,
|
||||
},
|
||||
];
|
||||
|
||||
vi.mock('@coze-arch/bot-studio-store', () => ({
|
||||
useSpaceStore: () => ({
|
||||
space: { ...spaces[0], id: spaces[0].id },
|
||||
spaces: {
|
||||
bot_space_list: spaces,
|
||||
},
|
||||
getState: () => ({
|
||||
getPersonalSpaceID: () => 'personal-space-id',
|
||||
}),
|
||||
}),
|
||||
useSpaceList: () => ({ spaces, loading: false }),
|
||||
}));
|
||||
|
||||
vi.mock('@coze-studio/bot-detail-store/page-runtime', () => ({
|
||||
usePageRuntimeStore: () => ({
|
||||
pageFrom: 'test',
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@coze-studio/bot-detail-store/bot-skill', () => ({
|
||||
useBotSkillStore: () => ({
|
||||
hasWorkflow: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('SelectSpaceModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should render botName and spaces', () => {
|
||||
const wrapper = render(<SelectSpaceModal visible botName="mockBot" />);
|
||||
expect(wrapper.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(
|
||||
wrapper.getByDisplayValue('mockBot(duplicate_rename_copy)'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// 检查表单是否存在
|
||||
expect(wrapper.getByRole('form')).toBeInTheDocument();
|
||||
|
||||
// 检查确定和取消按钮
|
||||
expect(
|
||||
wrapper.getByRole('button', { name: 'confirm' }),
|
||||
).toBeInTheDocument();
|
||||
expect(wrapper.getByRole('button', { name: 'cancel' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should fire events', async () => {
|
||||
const mockCancel = vi.fn();
|
||||
const mockConfirm = vi.fn();
|
||||
render(
|
||||
<SelectSpaceModal
|
||||
visible
|
||||
botName="mockBot"
|
||||
onCancel={mockCancel}
|
||||
onConfirm={mockConfirm}
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(document.body.querySelector('[aria-label="cancel"]')!);
|
||||
expect(mockCancel).toHaveBeenCalled();
|
||||
await act(async () => {
|
||||
await fireEvent.click(
|
||||
document.body.querySelector('[aria-label="confirm"]')!,
|
||||
);
|
||||
});
|
||||
expect(mockConfirm).toHaveBeenCalledWith(
|
||||
'space0',
|
||||
'mockBot(duplicate_rename_copy)',
|
||||
);
|
||||
});
|
||||
});
|
||||
BIN
frontend/packages/studio/components/assets/avatar_default.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
d="M6.76033 11.9922L3.91012 11.9854C3.79967 11.9851 3.71034 11.8954 3.7106 11.7849C3.71073 11.732 3.73179 11.6814 3.76918 11.644L6.52438 8.88878C6.62852 8.78464 6.79737 8.78464 6.90151 8.88878L8.17289 10.1602L11.5444 6.78861C11.6486 6.68447 11.8174 6.68447 11.9216 6.78861C11.9716 6.83862 11.9997 6.90645 11.9997 6.97717V11.8C11.9997 11.9105 11.9101 12 11.7997 12H6.81585C6.79659 12 6.77796 11.9973 6.76033 11.9922ZM2.66634 14.6667C1.93301 14.6667 1.33301 14.0667 1.33301 13.3334V2.66671C1.33301 1.93337 1.93301 1.33337 2.66634 1.33337H13.333C14.0663 1.33337 14.6663 1.93337 14.6663 2.66671V13.3334C14.6663 14.0667 14.0663 14.6667 13.333 14.6667H2.66634ZM2.66634 13.3334H13.333V2.66671H2.66634V13.3334ZM3.99967 4.00004H5.99967V6.00004H3.99967V4.00004Z"
|
||||
fill="currentColor" fill-opacity="0.96" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 917 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
d="M6 8.66671H10C11.841 8.66671 14 9.95241 14 12.2667V13.3334C14 14.0667 13.4 14.6667 12.6667 14.6667H3.33333C2.6 14.6667 2 14.0667 2 13.3334V12.2667C2 9.95436 4.15905 8.66671 6 8.66671ZM12.6667 13.3334V12.2223C12.6667 10.8213 11.2473 10 10 10H6C4.77998 10 3.33333 10.7812 3.33333 12.2223V13.3334H12.6667ZM8 8.00004C6.15905 8.00004 4.66667 6.50766 4.66667 4.66671C4.66667 2.82576 6.15905 1.33337 8 1.33337C9.84095 1.33337 11.3333 2.82576 11.3333 4.66671C11.3333 6.50766 9.84095 8.00004 8 8.00004ZM8 6.66671C9.10457 6.66671 10 5.77128 10 4.66671C10 3.56214 9.10457 2.66671 8 2.66671C6.89543 2.66671 6 3.56214 6 4.66671C6 5.77128 6.89543 6.66671 8 6.66671Z"
|
||||
fill="currentColor" fill-opacity="0.96" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 820 B |
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
7
frontend/packages/studio/components/eslint.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
});
|
||||
141
frontend/packages/studio/components/package.json
Normal file
@@ -0,0 +1,141 @@
|
||||
{
|
||||
"name": "@coze-studio/components",
|
||||
"version": "0.0.1",
|
||||
"description": "biz components extract from apps/bot/src/components",
|
||||
"license": "Apache-2.0",
|
||||
"author": "fanwenjie.fe@bytedance.com",
|
||||
"maintainers": [],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./coze-brand": "./src/coze-brand/index.tsx",
|
||||
"./search-no-result": "./src/search-no-result/index.tsx",
|
||||
"./sortable-list": "./src/sortable-list/index.tsx",
|
||||
"./dnd-provider": "./src/dnd-provider/index.tsx",
|
||||
"./sortable-list-hooks": "./src/sortable-list/hooks.ts",
|
||||
"./generate-gif": "./src/generate-gif/index.tsx",
|
||||
"./markdown-editor": "./src/markdown-editor/index.tsx",
|
||||
"./parameters-popover": "./src/plugins/parameters-popover/index.tsx",
|
||||
"./collapsible-role-list": "./src/social-scene/collapsible-role-list/index.tsx",
|
||||
"./monetize": "./src/monetize/index.ts",
|
||||
"./collapsible-icon-button": "./src/collapsible-icon-button/index.tsx",
|
||||
"./table-select-all-popover": "./src/table-select-all-popover/index.tsx"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"coze-brand": [
|
||||
"./src/coze-brand/index.tsx"
|
||||
],
|
||||
"search-no-result": [
|
||||
"./src/search-no-result/index.tsx"
|
||||
],
|
||||
"sortable-list": [
|
||||
"./src/sortable-list/index.tsx"
|
||||
],
|
||||
"dnd-provider": [
|
||||
"./src/dnd-provider/index.tsx"
|
||||
],
|
||||
"sortable-list-hooks": [
|
||||
"./src/sortable-list/hooks.ts"
|
||||
],
|
||||
"generate-gif": [
|
||||
"./src/generate-gif/index.tsx"
|
||||
],
|
||||
"markdown-editor": [
|
||||
"./src/markdown-editor/index.tsx"
|
||||
],
|
||||
"parameters-popover": [
|
||||
"./src/plugins/parameters-popover/index.tsx"
|
||||
],
|
||||
"collapsible-role-list": [
|
||||
"./src/social-scene/collapsible-role-list/index.tsx"
|
||||
],
|
||||
"monetize": [
|
||||
"./src/monetize/index.ts"
|
||||
],
|
||||
"collapsible-icon-button": [
|
||||
"./src/collapsible-icon-button/index.tsx"
|
||||
],
|
||||
"table-select-all-popover": [
|
||||
"./src/table-select-all-popover/index.tsx"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "exit 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "^5.1.5",
|
||||
"@coze-agent-ide/bot-input-length-limit": "workspace:*",
|
||||
"@coze-arch/bot-api": "workspace:*",
|
||||
"@coze-arch/bot-error": "workspace:*",
|
||||
"@coze-arch/bot-flags": "workspace:*",
|
||||
"@coze-arch/bot-hooks": "workspace:*",
|
||||
"@coze-arch/bot-icons": "workspace:*",
|
||||
"@coze-arch/bot-md-box-adapter": "workspace:*",
|
||||
"@coze-arch/bot-semi": "workspace:*",
|
||||
"@coze-arch/bot-studio-store": "workspace:*",
|
||||
"@coze-arch/bot-tea": "workspace:*",
|
||||
"@coze-arch/bot-utils": "workspace:*",
|
||||
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
|
||||
"@coze-arch/i18n": "workspace:*",
|
||||
"@coze-arch/logger": "workspace:*",
|
||||
"@coze-arch/report-events": "workspace:*",
|
||||
"@coze-arch/web-context": "workspace:*",
|
||||
"@coze-common/assets": "workspace:*",
|
||||
"@coze-common/biz-components": "workspace:*",
|
||||
"@coze-common/websocket-manager-adapter": "workspace:*",
|
||||
"@coze-data/e2e": "workspace:*",
|
||||
"@coze-foundation/space-store": "workspace:*",
|
||||
"@coze-studio/bot-utils": "workspace:*",
|
||||
"@douyinfe/semi-icons": "^2.36.0",
|
||||
"ahooks": "^3.7.8",
|
||||
"axios": "^1.4.0",
|
||||
"classnames": "^2.3.2",
|
||||
"dayjs": "^1.11.7",
|
||||
"immer": "^10.0.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react-dnd": "16.0.1",
|
||||
"react-dnd-html5-backend": "16.0.1",
|
||||
"react-markdown": "^8.0.3",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/bot-space-api": "workspace:*",
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/eslint-config": "workspace:*",
|
||||
"@coze-arch/stylelint-config": "workspace:*",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@coze-arch/vitest-config": "workspace:*",
|
||||
"@coze-studio/bot-detail-store": "workspace:*",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/lodash-es": "^4.17.10",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"@vitest/coverage-v8": "~3.0.5",
|
||||
"debug": "^4.3.4",
|
||||
"nanoid": "^4.0.2",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"react-router-dom": "^6.22.0",
|
||||
"stylelint": "^15.11.0",
|
||||
"use-event-callback": "~0.1.0",
|
||||
"utility-types": "^3.10.0",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-svgr": "~3.3.0",
|
||||
"vitest": "~3.0.5",
|
||||
"webpack": "~5.91.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.2.0",
|
||||
"react-dom": ">=18.2.0",
|
||||
"utility-types": "^3.10.0"
|
||||
},
|
||||
"// deps": "immer@^10.0.3 为脚本自动补齐,请勿改动"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
.ctn {
|
||||
cursor: pointer;
|
||||
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
|
||||
box-sizing: content-box;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
background-color: var(--coz-fg-white, #fff);
|
||||
border: 1.5px solid var(--coz-fg-white, #fff);
|
||||
border-radius: 50%;
|
||||
|
||||
&.loading {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
padding: 2px;
|
||||
|
||||
font-size: 8px;
|
||||
|
||||
background-color: var(--coz-mg-hglt-plus-green, #00B83E);
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
|
||||
&.icon-generating {
|
||||
color: var(--coz-fg-white, #fff);
|
||||
}
|
||||
|
||||
&.icon-success {
|
||||
color: var(--coz-mg-color-plus-emerald, #00B83E);
|
||||
}
|
||||
|
||||
&.icon-fail {
|
||||
color: var(--coz-mg-color-plus-orange, #FF811A);
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.icon-icon-coz_loading.icon-icon-loading {
|
||||
animation: semi-animation-rotate .6s linear infinite;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { DotStatus } from '@coze-studio/bot-detail-store';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
IconCozCheckMarkCircleFillPalette,
|
||||
IconCozLoading,
|
||||
IconCozWarningCircleFillPalette,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface AvatarBackgroundNoticeDotProps {
|
||||
status: DotStatus;
|
||||
}
|
||||
|
||||
export const AvatarBackgroundNoticeDot: React.FC<
|
||||
AvatarBackgroundNoticeDotProps
|
||||
> = ({ status }) => {
|
||||
if (status === DotStatus.None || status === DotStatus.Cancel) {
|
||||
return null;
|
||||
}
|
||||
const dot = {
|
||||
[DotStatus.Generating]: (
|
||||
<Tooltip content={I18n.t('profilepicture_hover_generating')}>
|
||||
<IconCozLoading
|
||||
className={classNames(s.icon, s['icon-generating'])}
|
||||
spin={true}
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
[DotStatus.Success]: (
|
||||
<Tooltip content={I18n.t('profilepicture_hover_generated')}>
|
||||
<IconCozCheckMarkCircleFillPalette
|
||||
className={classNames(s.icon, s['icon-success'])}
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
[DotStatus.Fail]: (
|
||||
<Tooltip content={I18n.t('profilepicture_hover_failed')}>
|
||||
<IconCozWarningCircleFillPalette
|
||||
className={classNames(s.icon, s['icon-fail'])}
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
s.ctn,
|
||||
status === DotStatus.Generating ? s.loading : undefined,
|
||||
)}
|
||||
>
|
||||
{dot[status]}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
.container {
|
||||
flex-shrink: 0;
|
||||
max-width: 100%;
|
||||
height: 18px;
|
||||
|
||||
.avatar {
|
||||
overflow: hidden;
|
||||
border-radius: 0;
|
||||
border-radius: 12px;
|
||||
|
||||
img {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.label-icon {
|
||||
cursor: pointer;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
|
||||
.txt {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
|
||||
@apply coz-fg-dim;
|
||||
|
||||
&.name {
|
||||
@apply coz-fg-secondary;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
&.light {
|
||||
.txt {
|
||||
color: rgba(255, 255, 255, 39%);
|
||||
|
||||
&.name {
|
||||
color: rgba(255, 255, 255, 79%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.white {
|
||||
.txt {
|
||||
color: #FFF;
|
||||
|
||||
&.name {
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.middle {
|
||||
height: 20px;
|
||||
|
||||
.txt {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
|
||||
&.username {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
height: 20px;
|
||||
|
||||
.label-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.txt {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
117
frontend/packages/studio/components/src/avatar-name/index.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Space, Typography, Tooltip } from '@coze-arch/coze-design';
|
||||
import { Image } from '@coze-arch/bot-semi';
|
||||
|
||||
import AvatarDefault from '../../assets/avatar_default.png';
|
||||
|
||||
import s from './index.module.less';
|
||||
const { Text } = Typography;
|
||||
interface AvatarNameProps {
|
||||
avatar?: string;
|
||||
username?: string;
|
||||
name?: string;
|
||||
label?: {
|
||||
name?: string;
|
||||
icon?: string;
|
||||
href?: string;
|
||||
};
|
||||
theme?: 'default' | 'light' | 'white';
|
||||
className?: string;
|
||||
nameMaxWidth?: number;
|
||||
size?: 'default' | 'large' | 'small';
|
||||
renderCenterSlot?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AvatarSizeMap = {
|
||||
small: 12,
|
||||
default: 14,
|
||||
large: 16,
|
||||
};
|
||||
|
||||
export const AvatarName = ({
|
||||
avatar,
|
||||
username,
|
||||
name,
|
||||
label,
|
||||
theme,
|
||||
className,
|
||||
nameMaxWidth,
|
||||
size = 'default',
|
||||
renderCenterSlot = null,
|
||||
}: AvatarNameProps) => (
|
||||
<Space
|
||||
spacing={4}
|
||||
className={classNames(
|
||||
s.container,
|
||||
theme && s[theme],
|
||||
{ [s.large]: size === 'large' },
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
width={AvatarSizeMap[size]}
|
||||
height={AvatarSizeMap[size]}
|
||||
src={avatar || AvatarDefault}
|
||||
fallback={<img src={AvatarDefault} width={'100%'} height={'100%'} />}
|
||||
preview={false}
|
||||
className={s.avatar}
|
||||
/>
|
||||
<Space spacing={2}>
|
||||
<Text
|
||||
className={classNames(s.txt, s.name)}
|
||||
ellipsis={{ showTooltip: false, rows: 1 }}
|
||||
style={
|
||||
typeof nameMaxWidth === 'number' ? { maxWidth: nameMaxWidth } : {}
|
||||
}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
{label?.icon ? (
|
||||
<Tooltip
|
||||
showArrow
|
||||
content={label?.name}
|
||||
position={'top'}
|
||||
trigger={label?.name ? 'hover' : 'custom'}
|
||||
>
|
||||
<img
|
||||
src={label?.icon}
|
||||
className={s['label-icon']}
|
||||
tabIndex={-1}
|
||||
onMouseDown={event => {
|
||||
if (label?.href) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
window.open(label.href, '_blank');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Space>
|
||||
{renderCenterSlot}
|
||||
{username ? (
|
||||
<Text
|
||||
className={classNames(s.txt, s.username)}
|
||||
ellipsis={{ showTooltip: false, rows: 1 }}
|
||||
>
|
||||
@{username}
|
||||
</Text>
|
||||
) : null}
|
||||
</Space>
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
.popover-content {
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.popover-card-title {
|
||||
margin-bottom: 24px;
|
||||
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
color: #1C1D24;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.popover-card-img {
|
||||
max-width: 360px;
|
||||
border: 1px solid #E5E5E5;
|
||||
border-radius: 8px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-card-icon {
|
||||
color: rgba(29, 28, 35, 35%)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type PropsWithChildren, useRef, useCallback } from 'react';
|
||||
|
||||
import { get } from 'lodash-es';
|
||||
import cls from 'classnames';
|
||||
import { type PopoverProps } from '@coze-arch/bot-semi/Popover';
|
||||
import { type ImageProps } from '@coze-arch/bot-semi/Image';
|
||||
import { Popover, Image } from '@coze-arch/bot-semi';
|
||||
import { IconGroupCardOutlined } from '@coze-arch/bot-icons';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface CardThumbnailPopoverProps extends PopoverProps {
|
||||
title?: string;
|
||||
url?: string;
|
||||
className?: string;
|
||||
imgProps?: ImageProps;
|
||||
}
|
||||
|
||||
export const CardThumbnailPopover: React.FC<
|
||||
PropsWithChildren<CardThumbnailPopoverProps>
|
||||
> = ({ children, url, title = '卡片预览', className, imgProps, ...props }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const popoverRef = useRef<any>();
|
||||
|
||||
const onImageLoad = useCallback(() => {
|
||||
const calcPosition = get(
|
||||
popoverRef.current,
|
||||
'tooltipRef.current.foundation.calcPosition',
|
||||
);
|
||||
if (typeof calcPosition === 'function') {
|
||||
calcPosition?.();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
position="top"
|
||||
showArrow
|
||||
ref={popoverRef}
|
||||
content={
|
||||
<div className={s['popover-content']}>
|
||||
<div className={s['popover-card-title']}>{title}</div>
|
||||
{url && (
|
||||
<div className={s['popover-card-img']}>
|
||||
<Image src={url} {...imgProps} onLoad={onImageLoad} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{children || (
|
||||
<IconGroupCardOutlined
|
||||
className={cls(className, s['popover-card-icon'])}
|
||||
/>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
.carousel-item {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface CarouselItemProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const CarouselItem: React.FC<CarouselItemProps> = props => {
|
||||
const { children, className } = props;
|
||||
return (
|
||||
<div className={cls(styles['carousel-item'], className, 'carousel-item')}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
.carousel {
|
||||
position: relative;
|
||||
|
||||
.arrow-container {
|
||||
&::before {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
|
||||
width: 52px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.left {
|
||||
&::before {
|
||||
left: 0;
|
||||
background: linear-gradient(90deg,
|
||||
var(--coz-bg-max) 7.46%,
|
||||
rgba(255, 255, 255, 0%) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&.right {
|
||||
&::before {
|
||||
right: 0;
|
||||
background: linear-gradient(270deg,
|
||||
var(--coz-bg-max) 7.46%,
|
||||
rgba(255, 255, 255, 0%) 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
cursor: pointer;
|
||||
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
font-size: 20px;
|
||||
|
||||
background: var(--coz-mg-primary);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 16px 0 #0000001a;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&.no-border {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.left-arrow {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.right-arrow {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&-content {
|
||||
overflow: auto;
|
||||
flex: 1 0 auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
243
frontend/packages/studio/components/src/carousel/index.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
import { chunk } from 'lodash-es';
|
||||
import cls from 'classnames';
|
||||
import {
|
||||
IconCozArrowRight,
|
||||
IconCozArrowLeft,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
|
||||
import { CarouselItem } from './carousel-item';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface CarouselProps {
|
||||
/** 元素布局行数默认为1 */
|
||||
rows?: number;
|
||||
/** 元素布局列数,默认为均分数组 */
|
||||
column?: number;
|
||||
/** 每次点击箭头滚动的百分比,0~1. 默认值为0.5 */
|
||||
scrollStep?: number;
|
||||
/** 滚动回调 */
|
||||
onScroll?: () => void;
|
||||
/** 箭头是否显示边框 */
|
||||
enableArrowBorder?: boolean;
|
||||
/** 箭头是否显示阴影渐变 */
|
||||
enableArrowShalldow?: boolean;
|
||||
/** 子元素样式 */
|
||||
itemClassName?: string;
|
||||
/** 左箭头样式 */
|
||||
leftArrowClassName?: string;
|
||||
/** 右箭头样式 */
|
||||
rightArrowClassName?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface ArrowProps {
|
||||
className?: string;
|
||||
enableArrowBorder?: boolean;
|
||||
enableArrowShalldow?: boolean;
|
||||
onClick: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const LeftArrow = ({
|
||||
enableArrowBorder,
|
||||
enableArrowShalldow,
|
||||
className,
|
||||
onClick,
|
||||
}: ArrowProps) => (
|
||||
<div
|
||||
className={cls(
|
||||
styles['arrow-container'],
|
||||
{ [styles.left]: enableArrowShalldow },
|
||||
'arrow-container-left',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={cls(
|
||||
className,
|
||||
styles['left-arrow'],
|
||||
styles.arrow,
|
||||
'left-arrow',
|
||||
{
|
||||
[styles['no-border']]: !enableArrowBorder,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<IconCozArrowLeft />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const RightArrow = ({
|
||||
enableArrowBorder,
|
||||
enableArrowShalldow,
|
||||
className,
|
||||
onClick,
|
||||
}: ArrowProps) => (
|
||||
<div
|
||||
className={cls(
|
||||
styles['arrow-container'],
|
||||
{ [styles.right]: enableArrowShalldow },
|
||||
'arrow-container-right',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={cls(
|
||||
className,
|
||||
styles['right-arrow'],
|
||||
styles.arrow,
|
||||
'right-arrow',
|
||||
{
|
||||
[styles['no-border']]: !enableArrowBorder,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<IconCozArrowRight />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Carousel: React.FC<CarouselProps> = ({
|
||||
rows = 1,
|
||||
column,
|
||||
itemClassName = '',
|
||||
leftArrowClassName = '',
|
||||
rightArrowClassName = '',
|
||||
children,
|
||||
enableArrowShalldow = true,
|
||||
scrollStep = 0.5,
|
||||
enableArrowBorder = true,
|
||||
onScroll,
|
||||
}) => {
|
||||
const itemsContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [leftArrowVisible, setLeftArrowVisible] = useState(false);
|
||||
const [rightArrowVisible, setRightArrowVisible] = useState(false);
|
||||
if (!children) {
|
||||
return null;
|
||||
}
|
||||
const carouselItems = React.Children.map(
|
||||
children,
|
||||
(child: React.ReactNode, idx: number) => (
|
||||
<CarouselItem className={itemClassName} key={idx}>
|
||||
{child}
|
||||
</CarouselItem>
|
||||
),
|
||||
);
|
||||
const chunkedCarouselItems: React.ReactNode[][] = chunk(
|
||||
carouselItems,
|
||||
column ?? Math.ceil((carouselItems?.length || 0) / rows),
|
||||
);
|
||||
const rowItems = Array.from(Array(rows).fill(null)).map((_row, idx) => (
|
||||
<div className={cls(styles['carousel-row'], 'carousel-row')} key={idx}>
|
||||
{chunkedCarouselItems[idx]}
|
||||
</div>
|
||||
));
|
||||
const handleScrollLeft = () => {
|
||||
if (
|
||||
itemsContainerRef?.current?.scrollLeft !== undefined &&
|
||||
itemsContainerRef?.current?.clientWidth
|
||||
) {
|
||||
// 部分浏览器不支持 scrollTo 方法
|
||||
itemsContainerRef.current.scrollTo?.({
|
||||
left: Math.max(
|
||||
itemsContainerRef.current.scrollLeft -
|
||||
itemsContainerRef.current.clientWidth * scrollStep,
|
||||
0,
|
||||
),
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleScrollRight = () => {
|
||||
const containWidth = itemsContainerRef?.current?.clientWidth ?? 0;
|
||||
if (itemsContainerRef?.current?.scrollLeft !== undefined && containWidth) {
|
||||
const scrollLeftMax =
|
||||
(itemsContainerRef?.current?.scrollWidth ?? 0) -
|
||||
(itemsContainerRef?.current?.clientWidth ?? 0);
|
||||
itemsContainerRef.current.scrollTo?.({
|
||||
left: Math.min(
|
||||
itemsContainerRef.current.scrollLeft + containWidth * scrollStep,
|
||||
scrollLeftMax,
|
||||
),
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks -- linter-disable-autofix
|
||||
useEffect(() => {
|
||||
const updateArrowVisible = () => {
|
||||
const scrollLeft = Math.ceil(itemsContainerRef?.current?.scrollLeft ?? 0);
|
||||
const scrollRight =
|
||||
(itemsContainerRef?.current?.scrollWidth ?? 0) -
|
||||
(itemsContainerRef?.current?.clientWidth ?? 0) -
|
||||
scrollLeft;
|
||||
|
||||
const shouldShowArrowLeft = scrollLeft > 0;
|
||||
// 极端场景下存在 1px 偏差
|
||||
const shouldShowArrowRight = Math.abs(scrollRight) > 2;
|
||||
|
||||
setLeftArrowVisible(shouldShowArrowLeft);
|
||||
setRightArrowVisible(shouldShowArrowRight);
|
||||
};
|
||||
const scrollEvent = () => {
|
||||
onScroll?.();
|
||||
updateArrowVisible();
|
||||
};
|
||||
|
||||
// 初始化时判读一次是否显示箭头
|
||||
updateArrowVisible();
|
||||
itemsContainerRef?.current?.addEventListener('scroll', scrollEvent);
|
||||
window?.addEventListener('resize', updateArrowVisible);
|
||||
return () => {
|
||||
itemsContainerRef?.current?.removeEventListener('scroll', scrollEvent);
|
||||
window?.removeEventListener('resize', updateArrowVisible);
|
||||
};
|
||||
}, [children]);
|
||||
|
||||
return (
|
||||
<div className={cls(styles.carousel, 'carousel')}>
|
||||
{leftArrowVisible ? (
|
||||
<LeftArrow
|
||||
onClick={handleScrollLeft}
|
||||
enableArrowBorder={enableArrowBorder}
|
||||
enableArrowShalldow={enableArrowShalldow}
|
||||
className={leftArrowClassName}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className={cls(styles['carousel-content'], 'carousel-content')}
|
||||
ref={itemsContainerRef}
|
||||
>
|
||||
{rowItems}
|
||||
</div>
|
||||
{rightArrowVisible ? (
|
||||
<RightArrow
|
||||
onClick={handleScrollRight}
|
||||
enableArrowBorder={enableArrowBorder}
|
||||
enableArrowShalldow={enableArrowShalldow}
|
||||
className={rightArrowClassName}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type RefObject,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
type SetStateAction,
|
||||
type Dispatch,
|
||||
useState,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import { noop, omit } from 'lodash-es';
|
||||
import { useSize } from 'ahooks';
|
||||
|
||||
interface TConfigItem {
|
||||
width?: number;
|
||||
}
|
||||
|
||||
type ContextItems = Record<symbol, TConfigItem>;
|
||||
|
||||
interface CollapsibleIconButtonContextValue {
|
||||
showText: boolean;
|
||||
setItems: Dispatch<SetStateAction<ContextItems | undefined>>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- 这是 context
|
||||
export const CollapsibleIconButtonContext =
|
||||
createContext<CollapsibleIconButtonContextValue>({
|
||||
showText: true,
|
||||
setItems: noop,
|
||||
});
|
||||
|
||||
export const useItem = (key: symbol, ref: RefObject<HTMLElement>) => {
|
||||
const { showText, setItems } = useContext(CollapsibleIconButtonContext);
|
||||
const size = useSize(ref);
|
||||
useEffect(() => {
|
||||
setItems(items => ({
|
||||
...items,
|
||||
[key]: { width: size?.width ?? 0 },
|
||||
}));
|
||||
}, [size?.width]);
|
||||
|
||||
// 组件销毁后移除
|
||||
useEffect(
|
||||
() => () => {
|
||||
setItems(items => omit(items, key));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return showText;
|
||||
};
|
||||
|
||||
export const useWrapper = (ref: RefObject<HTMLElement>, gap = 0) => {
|
||||
const [items, setItems] = useState<ContextItems>();
|
||||
const size = useSize(ref);
|
||||
|
||||
const totalWidth = Object.getOwnPropertySymbols(items || {}).reduce<number>(
|
||||
(res, key, index) =>
|
||||
res + (items?.[key]?.width ?? 0) + (index > 0 ? gap : 0),
|
||||
0,
|
||||
);
|
||||
const showText = !!size?.width && size.width >= totalWidth;
|
||||
|
||||
const contextValue = useMemo<CollapsibleIconButtonContextValue>(
|
||||
() => ({
|
||||
showText,
|
||||
setItems,
|
||||
}),
|
||||
[showText],
|
||||
);
|
||||
|
||||
return contextValue;
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
type FC,
|
||||
useRef,
|
||||
forwardRef,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import { omit } from 'lodash-es';
|
||||
import {
|
||||
Button,
|
||||
type ButtonProps,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import { CollapsibleIconButtonContext, useWrapper, useItem } from './context';
|
||||
|
||||
/** 能让 Group 内的所有 CollapsibleIconButton 根据空余宽度自动展开(露出文案)收起(隐藏文案只剩图标) */
|
||||
export const CollapsibleIconButtonGroup: FC<
|
||||
PropsWithChildren<{
|
||||
/** @default 12 */
|
||||
gap?: number;
|
||||
}>
|
||||
> = ({ children, gap = 12 }) => {
|
||||
const wrapperDomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const contextValue = useWrapper(wrapperDomRef, gap);
|
||||
return (
|
||||
<div
|
||||
ref={wrapperDomRef}
|
||||
className="flex items-center justify-end flex-1 overflow-hidden"
|
||||
style={{ gap }}
|
||||
>
|
||||
<CollapsibleIconButtonContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</CollapsibleIconButtonContext.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CollapsibleIconButton = forwardRef<
|
||||
HTMLSpanElement,
|
||||
{
|
||||
itemKey: symbol;
|
||||
text: string;
|
||||
} & ButtonProps
|
||||
>(({ itemKey, text, ...rest }, ref) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const showText = useItem(itemKey, contentRef);
|
||||
|
||||
return (
|
||||
<span ref={ref}>
|
||||
{/* 不可见时渲染到屏幕外侧,用于获取宽度 */}
|
||||
<div className={showText ? '' : 'fixed left-[-999px]'} ref={contentRef}>
|
||||
<Button
|
||||
size="default"
|
||||
color="secondary"
|
||||
// 不可见时不附带 testid,避免对 E2E 产生影响
|
||||
{...(showText ? rest : omit(rest, 'data-testid'))}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
</div>
|
||||
{!showText && (
|
||||
<Tooltip content={text}>
|
||||
<IconButton size="default" color="secondary" {...rest} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
/** 更为通用的版本 */
|
||||
export const Collapsible = forwardRef<
|
||||
HTMLSpanElement,
|
||||
{
|
||||
itemKey: symbol;
|
||||
collapsedContent: ReactNode;
|
||||
fullContent: ReactNode;
|
||||
collapsedTooltip?: ReactNode;
|
||||
}
|
||||
>(({ itemKey, fullContent, collapsedContent, collapsedTooltip }, ref) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const showFull = useItem(itemKey, contentRef);
|
||||
|
||||
return (
|
||||
<span ref={ref}>
|
||||
{/* 不可见时渲染到屏幕外侧,用于获取宽度 */}
|
||||
<div className={showFull ? '' : 'fixed left-[-999px]'} ref={contentRef}>
|
||||
{fullContent}
|
||||
</div>
|
||||
{!showFull && (
|
||||
<Tooltip
|
||||
trigger={collapsedTooltip ? 'hover' : 'custom'}
|
||||
content={collapsedTooltip}
|
||||
>
|
||||
<span>{collapsedContent}</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
/** 不会折叠,但参与宽度计算的元素 */
|
||||
export function PlaceholderContainer({
|
||||
itemKey,
|
||||
children,
|
||||
}: PropsWithChildren<{ itemKey: symbol }>) {
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
useItem(itemKey, ref);
|
||||
|
||||
return <span ref={ref}>{children}</span>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.coze-brand {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
& > svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
71
frontend/packages/studio/components/src/coze-brand/index.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
IconBrandCnWhiteRow,
|
||||
IconBrandCnBlackRow,
|
||||
IconBrandEnBlackRow,
|
||||
} from '@coze-arch/bot-icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export interface CozeBrandProps {
|
||||
isOversea: boolean;
|
||||
isWhite?: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export function CozeBrand({
|
||||
isOversea,
|
||||
isWhite,
|
||||
className,
|
||||
style,
|
||||
}: CozeBrandProps) {
|
||||
const navigate = useNavigate();
|
||||
const navBack = () => {
|
||||
navigate('/');
|
||||
};
|
||||
if (isOversea) {
|
||||
return (
|
||||
<IconBrandEnBlackRow
|
||||
onClick={navBack}
|
||||
className={classNames(styles['coze-brand'], className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isWhite) {
|
||||
return (
|
||||
<IconBrandCnWhiteRow
|
||||
onClick={navBack}
|
||||
className={classNames(styles['coze-brand'], className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<IconBrandCnBlackRow
|
||||
onClick={navBack}
|
||||
className={classNames(styles['coze-brand'], className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { DndProvider as Provider } from 'react-dnd';
|
||||
import { type ReactNode, createContext, useContext } from 'react';
|
||||
const DnDContext = createContext<{
|
||||
isInProvider: boolean;
|
||||
}>({
|
||||
isInProvider: false,
|
||||
});
|
||||
export const DndProvider = ({ children }: { children: ReactNode }) => {
|
||||
const context = useContext(DnDContext);
|
||||
return (
|
||||
<DnDContext.Provider
|
||||
value={{
|
||||
isInProvider: true,
|
||||
}}
|
||||
>
|
||||
{context.isInProvider ? (
|
||||
children
|
||||
) : (
|
||||
<Provider backend={HTML5Backend} context={window}>
|
||||
{children}
|
||||
</Provider>
|
||||
)}
|
||||
</DnDContext.Provider>
|
||||
);
|
||||
};
|
||||
338
frontend/packages/studio/components/src/duplicate-bot/index.tsx
Normal file
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC, useState } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { usePageRuntimeStore } from '@coze-studio/bot-detail-store/page-runtime';
|
||||
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
|
||||
import {
|
||||
REPORT_EVENTS as ReportEventNames,
|
||||
createReportEvent,
|
||||
} from '@coze-arch/report-events';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
import { openNewWindow, getParamsFromQuery } from '@coze-arch/bot-utils';
|
||||
import { BotPageFromEnum } from '@coze-arch/bot-typings/common';
|
||||
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
|
||||
import { useSpaceList, useSpaceStore } from '@coze-arch/bot-studio-store';
|
||||
import { SpaceApi } from '@coze-arch/bot-space-api';
|
||||
import { type Size } from '@coze-arch/bot-semi/Button';
|
||||
import { UIButton, Toast } from '@coze-arch/bot-semi';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import {
|
||||
ProductEntityType,
|
||||
type ProductMetaInfo,
|
||||
} from '@coze-arch/bot-api/product_api';
|
||||
import { DeveloperApi, PlaygroundApi, ProductApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { SelectSpaceModal } from '../select-space-modal';
|
||||
|
||||
const botDuplicateEvent = createReportEvent({
|
||||
eventName: ReportEventNames.botDuplicate,
|
||||
});
|
||||
|
||||
interface DuplicateBotProps {
|
||||
storeCategory?: ProductMetaInfo['category'];
|
||||
botName?: string;
|
||||
botID?: string;
|
||||
isDisabled?: boolean;
|
||||
btnTxt?: string;
|
||||
pageFrom?: BotPageFromEnum;
|
||||
version?: string;
|
||||
buttonSize?: Size;
|
||||
enableCozeDesign?: boolean;
|
||||
/**
|
||||
* cozeDesign 的情况下才生效
|
||||
*/
|
||||
isBlock?: boolean;
|
||||
eventCallbacks?: Partial<{
|
||||
clickButton: () => void;
|
||||
duplicateFinished: ({ newBotId }: { newBotId: string }) => void;
|
||||
}>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line -- Needs to be refactored
|
||||
export const DuplicateBot: FC<DuplicateBotProps> = ({
|
||||
storeCategory,
|
||||
botName,
|
||||
botID,
|
||||
isDisabled,
|
||||
btnTxt,
|
||||
pageFrom,
|
||||
version,
|
||||
buttonSize,
|
||||
enableCozeDesign,
|
||||
isBlock,
|
||||
eventCallbacks,
|
||||
}) => {
|
||||
const {
|
||||
space: { hide_operation, id: spaceID },
|
||||
getPersonalSpaceID,
|
||||
} = useSpaceStore();
|
||||
const { spaces: list = [] } = useSpaceList();
|
||||
|
||||
const { pageFromFromStore } = usePageRuntimeStore(
|
||||
useShallow(state => ({
|
||||
pageFromFromStore: state.pageFrom,
|
||||
})),
|
||||
);
|
||||
const { botIdFromStore, botNameFromStore } = useBotInfoStore(
|
||||
useShallow(state => ({
|
||||
botIdFromStore: state.botId,
|
||||
botNameFromStore: state.name,
|
||||
})),
|
||||
);
|
||||
const [showSpaceModal, setShowSpaceModal] = useState(false);
|
||||
|
||||
const { runAsync: copyAndOpenBot } = useRequest(
|
||||
// eslint-disable-next-line complexity
|
||||
async (targetSpaceId?: string, name?: string): Promise<string> => {
|
||||
botDuplicateEvent.start();
|
||||
|
||||
let resp: {
|
||||
code?: string | number;
|
||||
msg?: string;
|
||||
data?: { bot_id?: string };
|
||||
};
|
||||
if (
|
||||
(pageFrom === BotPageFromEnum.Store ||
|
||||
pageFrom === BotPageFromEnum.Template) &&
|
||||
botID &&
|
||||
version &&
|
||||
targetSpaceId
|
||||
) {
|
||||
if (pageFrom === BotPageFromEnum.Template) {
|
||||
const {
|
||||
code,
|
||||
message,
|
||||
data: { new_entity_id: newBotId } = {},
|
||||
} = await ProductApi.PublicDuplicateProduct({
|
||||
product_id: botID,
|
||||
entity_type: ProductEntityType.BotTemplate,
|
||||
space_id: targetSpaceId,
|
||||
name: name ?? '',
|
||||
});
|
||||
resp = {
|
||||
code,
|
||||
msg: message,
|
||||
data: {
|
||||
bot_id: newBotId,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
resp = await PlaygroundApi.DuplicateBotVersionToSpace({
|
||||
bot_id: botID,
|
||||
version,
|
||||
target_space_id: targetSpaceId,
|
||||
name: name ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
//复制完成,关闭空间弹窗
|
||||
setShowSpaceModal(false);
|
||||
} else if (pageFromFromStore === BotPageFromEnum.Explore) {
|
||||
//explore时可以复制到某个空间下
|
||||
resp = await DeveloperApi.DuplicateBotToSpace({
|
||||
draft_bot_id: botIdFromStore,
|
||||
target_space_id: targetSpaceId || '',
|
||||
name,
|
||||
});
|
||||
|
||||
//复制完成,关闭空间弹窗
|
||||
setShowSpaceModal(false);
|
||||
} else {
|
||||
resp = await SpaceApi.DuplicateDraftBot({
|
||||
bot_id: botIdFromStore,
|
||||
});
|
||||
}
|
||||
|
||||
eventCallbacks?.duplicateFinished?.({
|
||||
newBotId: resp.data?.bot_id ?? '',
|
||||
});
|
||||
|
||||
const botTeaparams = {
|
||||
bot_type:
|
||||
pageFromFromStore === BotPageFromEnum.Explore ||
|
||||
pageFromFromStore === BotPageFromEnum.Store
|
||||
? 'store_bot'
|
||||
: 'team_bot',
|
||||
bot_id: botID ?? botIdFromStore,
|
||||
workspace_type:
|
||||
pageFromFromStore === BotPageFromEnum.Store
|
||||
? 'store_workspace'
|
||||
: getPersonalSpaceID() === targetSpaceId
|
||||
? 'personal_workspace'
|
||||
: 'team_workspace',
|
||||
bot_name: botName ?? botNameFromStore ?? '',
|
||||
};
|
||||
if (resp.code === 0) {
|
||||
sendTeaEvent(EVENT_NAMES.bot_duplicate_result, {
|
||||
...botTeaparams,
|
||||
result: 'success',
|
||||
});
|
||||
} else {
|
||||
sendTeaEvent(EVENT_NAMES.bot_duplicate_result, {
|
||||
...botTeaparams,
|
||||
result: 'failed',
|
||||
error_code: resp.code,
|
||||
error_message: resp.msg,
|
||||
});
|
||||
}
|
||||
|
||||
const respData = resp.data;
|
||||
|
||||
if (!respData) {
|
||||
throw new CustomError(
|
||||
ReportEventNames.botDuplicate,
|
||||
I18n.t('bot_copy_info_error'),
|
||||
);
|
||||
}
|
||||
const { bot_id: botId } = respData;
|
||||
if (!botID && !botIdFromStore) {
|
||||
throw new CustomError(
|
||||
ReportEventNames.botDuplicate,
|
||||
I18n.t('bot_copy_id_error'),
|
||||
);
|
||||
}
|
||||
|
||||
const url = `${location.origin}/space/${
|
||||
targetSpaceId || spaceID
|
||||
}/bot/${botId}?from=copy`;
|
||||
|
||||
return url;
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: () => {
|
||||
botDuplicateEvent.success();
|
||||
},
|
||||
onError: e => {
|
||||
botDuplicateEvent.error({ error: e, reason: e.message });
|
||||
setShowSpaceModal(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const beforeCopyClick = () => {
|
||||
eventCallbacks?.clickButton?.();
|
||||
sendTeaEvent(EVENT_NAMES.bot_duplicate_click, {
|
||||
bot_type:
|
||||
pageFromFromStore === BotPageFromEnum.Bot ? 'team_bot' : 'store_bot',
|
||||
});
|
||||
|
||||
if (pageFrom === BotPageFromEnum.Store) {
|
||||
sendTeaEvent(EVENT_NAMES.bot_duplicate_click_front, {
|
||||
bot_type: 'store_bot',
|
||||
bot_id: botID,
|
||||
bot_name: botName,
|
||||
category_id: storeCategory?.id,
|
||||
category_name: storeCategory?.name,
|
||||
source: 'bots_store',
|
||||
from: getParamsFromQuery({ key: 'from' }),
|
||||
});
|
||||
setShowSpaceModal(true);
|
||||
} else if (pageFromFromStore === BotPageFromEnum.Explore) {
|
||||
sendTeaEvent(EVENT_NAMES.bot_duplicate_click_front, {
|
||||
bot_type: 'store_bot',
|
||||
bot_id: botNameFromStore,
|
||||
bot_name: botNameFromStore,
|
||||
source: 'explore_bot_detailpage',
|
||||
from: 'explore_card',
|
||||
});
|
||||
sendTeaEvent(EVENT_NAMES.click_bot_duplicate, {
|
||||
bot_id: botIdFromStore,
|
||||
bot_name: botNameFromStore,
|
||||
from: 'explore_card',
|
||||
source: 'explore_bot_detailpage',
|
||||
});
|
||||
//探索页面来源:team>1时选择copy 空间,否则copy到个人空间
|
||||
if (list.length === 1) {
|
||||
openNewWindow(() => copyAndOpenBot(list?.[0].id));
|
||||
} else {
|
||||
setShowSpaceModal(true);
|
||||
}
|
||||
} else if (pageFrom === BotPageFromEnum.Template) {
|
||||
//探索页面来源:team>1时选择copy 空间,否则copy到个人空间
|
||||
if (list.length === 1) {
|
||||
openNewWindow(() => copyAndOpenBot(list?.[0].id));
|
||||
} else {
|
||||
setShowSpaceModal(true);
|
||||
}
|
||||
} else {
|
||||
sendTeaEvent(EVENT_NAMES.bot_duplicate_click_front, {
|
||||
bot_type: 'team_bot',
|
||||
bot_id: botIdFromStore,
|
||||
bot_name: botNameFromStore,
|
||||
source: 'bots_detailpage',
|
||||
from: 'bots_card',
|
||||
});
|
||||
// bot页面来源:若有操作权限直接copy到当前空间下
|
||||
if (hide_operation) {
|
||||
Toast.warning('Bot in public space cannot duplicate');
|
||||
return;
|
||||
} else {
|
||||
openNewWindow(copyAndOpenBot);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{enableCozeDesign ? (
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
size={buttonSize}
|
||||
onClick={beforeCopyClick}
|
||||
disabled={isDisabled}
|
||||
block={isBlock}
|
||||
>
|
||||
{btnTxt || I18n.t('duplicate')}
|
||||
</Button>
|
||||
) : (
|
||||
<UIButton
|
||||
type="primary"
|
||||
theme="solid"
|
||||
size={buttonSize}
|
||||
onClick={beforeCopyClick}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{btnTxt || I18n.t('duplicate')}
|
||||
</UIButton>
|
||||
)}
|
||||
|
||||
{/* 选择空间弹窗 */}
|
||||
<SelectSpaceModal
|
||||
botName={botName ?? botNameFromStore}
|
||||
visible={showSpaceModal}
|
||||
onCancel={() => {
|
||||
setShowSpaceModal(false);
|
||||
}}
|
||||
onConfirm={(id, name) => {
|
||||
sendTeaEvent(EVENT_NAMES.click_create_bot_confirm, {
|
||||
click: 'success',
|
||||
create_type: 'duplicate',
|
||||
from: 'explore_card',
|
||||
source: 'explore_bot_detailpage',
|
||||
});
|
||||
openNewWindow(() => copyAndOpenBot(id, name));
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||