feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
5
frontend/packages/arch/bot-hooks-base/.stylelintrc.js
Normal file
5
frontend/packages/arch/bot-hooks-base/.stylelintrc.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
16
frontend/packages/arch/bot-hooks-base/README.md
Normal file
16
frontend/packages/arch/bot-hooks-base/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# bot-hooks
|
||||
|
||||
> 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,81 @@
|
||||
/*
|
||||
* 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 { useParams } from 'react-router-dom';
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { messageReportEvent } from '@coze-arch/bot-utils';
|
||||
|
||||
import { useMessageReportEvent } from '../../src/bot/use-message-report-event';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@coze-arch/bot-utils', () => ({
|
||||
messageReportEvent: {
|
||||
start: vi.fn(),
|
||||
interrupt: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useParams: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('useMessageReportEvent', () => {
|
||||
const mockBotId = 'test-bot-id';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
(useParams as any).mockReturnValue({ bot_id: mockBotId });
|
||||
});
|
||||
|
||||
it('should start message report event with correct parameters', () => {
|
||||
renderHook(() => useMessageReportEvent());
|
||||
|
||||
expect(messageReportEvent.start).toHaveBeenCalledWith(mockBotId);
|
||||
});
|
||||
|
||||
it('should not start message report event when bot_id is not available', () => {
|
||||
(useParams as any).mockReturnValue({ bot_id: undefined });
|
||||
|
||||
renderHook(() => useMessageReportEvent());
|
||||
|
||||
expect(messageReportEvent.start).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should interrupt message report event on unmount', () => {
|
||||
const { unmount } = renderHook(() => useMessageReportEvent());
|
||||
|
||||
unmount();
|
||||
|
||||
expect(messageReportEvent.interrupt).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should restart message report event when bot_id changes', () => {
|
||||
const { rerender } = renderHook(() => useMessageReportEvent());
|
||||
|
||||
expect(messageReportEvent.start).toHaveBeenCalledTimes(1);
|
||||
expect(messageReportEvent.start).toHaveBeenCalledWith(mockBotId);
|
||||
|
||||
const newBotId = 'new-bot-id';
|
||||
(useParams as any).mockReturnValue({ bot_id: newBotId });
|
||||
|
||||
rerender();
|
||||
|
||||
expect(messageReportEvent.start).toHaveBeenCalledTimes(2);
|
||||
expect(messageReportEvent.start).toHaveBeenCalledWith(newBotId);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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 } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import {
|
||||
userStoreService,
|
||||
type UserInfo,
|
||||
type UserLabel,
|
||||
} from '@coze-studio/user-store';
|
||||
|
||||
import { useUserSenderInfo } from '../../src/bot/use-user-sender-info';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@coze-studio/user-store', () => ({
|
||||
userStoreService: {
|
||||
useUserLabel: vi.fn(),
|
||||
useUserInfo: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useUserSenderInfo', () => {
|
||||
const mockUserLabel = {
|
||||
id: 'label-1',
|
||||
name: 'Test Label',
|
||||
} as unknown as UserLabel;
|
||||
const mockUserInfo: Partial<UserInfo> = {
|
||||
avatar_url: 'https://example.com/avatar.jpg',
|
||||
name: 'Test User',
|
||||
user_id_str: '12345',
|
||||
app_user_info: {
|
||||
user_unique_name: 'test_user',
|
||||
},
|
||||
};
|
||||
|
||||
it('should return null when userInfo is not available', () => {
|
||||
vi.mocked(userStoreService.useUserLabel).mockReturnValue(mockUserLabel);
|
||||
vi.mocked(userStoreService.useUserInfo).mockReturnValue(null);
|
||||
|
||||
const { result } = renderHook(() => useUserSenderInfo());
|
||||
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
|
||||
it('should return formatted user sender info when userInfo is available', () => {
|
||||
vi.mocked(userStoreService.useUserLabel).mockReturnValue(mockUserLabel);
|
||||
vi.mocked(userStoreService.useUserInfo).mockReturnValue(
|
||||
mockUserInfo as UserInfo,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useUserSenderInfo());
|
||||
|
||||
expect(result.current).toEqual({
|
||||
url: mockUserInfo.avatar_url,
|
||||
nickname: mockUserInfo.name,
|
||||
id: mockUserInfo.user_id_str,
|
||||
userUniqueName: mockUserInfo.app_user_info?.user_unique_name,
|
||||
userLabel: mockUserLabel,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing optional fields', () => {
|
||||
const partialUserInfo: Partial<UserInfo> = {
|
||||
user_id_str: '12345',
|
||||
app_user_info: {},
|
||||
};
|
||||
|
||||
vi.mocked(userStoreService.useUserLabel).mockReturnValue(mockUserLabel);
|
||||
vi.mocked(userStoreService.useUserInfo).mockReturnValue(
|
||||
partialUserInfo as UserInfo,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useUserSenderInfo());
|
||||
|
||||
expect(result.current).toEqual({
|
||||
url: '',
|
||||
nickname: '',
|
||||
id: partialUserInfo.user_id_str,
|
||||
userUniqueName: '',
|
||||
userLabel: mockUserLabel,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import {
|
||||
useLayoutContext,
|
||||
LayoutContext,
|
||||
PlacementEnum,
|
||||
} from '../src/editor-layout';
|
||||
|
||||
describe('editor-layout', () => {
|
||||
const wrapper = ({
|
||||
children,
|
||||
placement = PlacementEnum.CENTER,
|
||||
}: PropsWithChildren<{ placement?: PlacementEnum }>) => (
|
||||
<LayoutContext value={{ placement }}>{children}</LayoutContext>
|
||||
);
|
||||
|
||||
it('should use default center placement', () => {
|
||||
const { result } = renderHook(() => useLayoutContext());
|
||||
expect(result.current.placement).toBe(PlacementEnum.CENTER);
|
||||
});
|
||||
|
||||
it('should use provided placement', () => {
|
||||
const { result } = renderHook(() => useLayoutContext(), {
|
||||
wrapper: ({ children }) =>
|
||||
wrapper({ children, placement: PlacementEnum.LEFT }),
|
||||
});
|
||||
expect(result.current.placement).toBe(PlacementEnum.LEFT);
|
||||
});
|
||||
|
||||
it('should use right placement', () => {
|
||||
const { result } = renderHook(() => useLayoutContext(), {
|
||||
wrapper: ({ children }) =>
|
||||
wrapper({ children, placement: PlacementEnum.RIGHT }),
|
||||
});
|
||||
expect(result.current.placement).toBe(PlacementEnum.RIGHT);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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 { useLocation } from 'react-router-dom';
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import {
|
||||
useResetLocationState,
|
||||
resetAuthLoginDataFromRoute,
|
||||
} from '../../src/router/use-reset-location-state';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useLocation: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('use-reset-location-state', () => {
|
||||
const mockLocation = {
|
||||
state: { someState: 'test' },
|
||||
key: 'default',
|
||||
pathname: '/',
|
||||
search: '',
|
||||
hash: '',
|
||||
};
|
||||
|
||||
const mockReplaceState = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Mock useLocation
|
||||
vi.mocked(useLocation).mockReturnValue(mockLocation);
|
||||
|
||||
// Mock window.history
|
||||
Object.defineProperty(window, 'history', {
|
||||
value: {
|
||||
replaceState: mockReplaceState,
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetAuthLoginDataFromRoute', () => {
|
||||
it('should call history.replaceState with empty state', () => {
|
||||
resetAuthLoginDataFromRoute();
|
||||
|
||||
expect(mockReplaceState).toHaveBeenCalledWith({}, '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useResetLocationState', () => {
|
||||
it('should clear location state and auth login data', () => {
|
||||
const { result } = renderHook(() => useResetLocationState());
|
||||
|
||||
// Call the reset function
|
||||
result.current();
|
||||
|
||||
// Verify location state is cleared
|
||||
expect(mockLocation.state).toEqual({});
|
||||
|
||||
// Verify history state is cleared
|
||||
expect(mockReplaceState).toHaveBeenCalledWith({}, '');
|
||||
});
|
||||
|
||||
it('should handle undefined location state', () => {
|
||||
const mockLocationWithoutState = {} as any;
|
||||
vi.mocked(useLocation).mockReturnValue(mockLocationWithoutState);
|
||||
|
||||
const { result } = renderHook(() => useResetLocationState());
|
||||
|
||||
// Call the reset function
|
||||
result.current();
|
||||
|
||||
// Verify location state is set to empty object
|
||||
expect(mockLocationWithoutState.state).toEqual({});
|
||||
|
||||
// Verify history state is cleared
|
||||
expect(mockReplaceState).toHaveBeenCalledWith({}, '');
|
||||
});
|
||||
|
||||
it('should preserve location reference while clearing state', () => {
|
||||
const { result } = renderHook(() => useResetLocationState());
|
||||
|
||||
const originalLocation = mockLocation;
|
||||
|
||||
// Call the reset function
|
||||
result.current();
|
||||
|
||||
// Verify location reference is preserved
|
||||
expect(mockLocation).toBe(originalLocation);
|
||||
|
||||
// But state is cleared
|
||||
expect(mockLocation.state).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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 } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
|
||||
import { useComponentState } from '../src/use-component-state';
|
||||
|
||||
describe('useComponentState', () => {
|
||||
it('should initialize with the provided state', () => {
|
||||
const initialState = { count: 0, text: 'hello' };
|
||||
const { result } = renderHook(() => useComponentState(initialState));
|
||||
|
||||
expect(result.current.state).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('should perform incremental updates by default', () => {
|
||||
const initialState = { count: 0, text: 'hello' };
|
||||
const { result } = renderHook(() => useComponentState(initialState));
|
||||
|
||||
act(() => {
|
||||
result.current.setState({ count: 1 });
|
||||
});
|
||||
|
||||
expect(result.current.state).toEqual({ count: 1, text: 'hello' });
|
||||
});
|
||||
|
||||
it('should replace entire state when replace flag is true', () => {
|
||||
const initialState = { count: 0, text: 'hello', extra: true };
|
||||
const { result } = renderHook(() => useComponentState(initialState));
|
||||
|
||||
act(() => {
|
||||
result.current.setState({ count: 1, text: 'hello', extra: true }, true);
|
||||
});
|
||||
|
||||
expect(result.current.state).toEqual({
|
||||
count: 1,
|
||||
text: 'hello',
|
||||
extra: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset state to initial value', () => {
|
||||
const initialState = { count: 0, text: 'hello' };
|
||||
const { result } = renderHook(() => useComponentState(initialState));
|
||||
|
||||
act(() => {
|
||||
result.current.setState({ count: 1, text: 'world' });
|
||||
});
|
||||
|
||||
expect(result.current.state).toEqual({ count: 1, text: 'world' });
|
||||
|
||||
act(() => {
|
||||
result.current.resetState();
|
||||
});
|
||||
|
||||
expect(result.current.state).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('should handle multiple updates', () => {
|
||||
const initialState = { count: 0, text: 'hello' };
|
||||
const { result } = renderHook(() => useComponentState(initialState));
|
||||
|
||||
act(() => {
|
||||
result.current.setState({ count: 1 });
|
||||
result.current.setState({ text: 'world' });
|
||||
});
|
||||
|
||||
expect(result.current.state).toEqual({ count: 1, text: 'world' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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 } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@coze-studio/bot-detail-store', () => ({
|
||||
useBotDetailIsReadonly: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@coze-studio/bot-detail-store/page-runtime', () => ({
|
||||
usePageRuntimeStore: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@coze-arch/bot-utils', () => ({
|
||||
skillKeyToApiStatusKeyTransformer: vi.fn(),
|
||||
}));
|
||||
|
||||
import { usePageRuntimeStore } from '@coze-studio/bot-detail-store/page-runtime';
|
||||
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
|
||||
import { skillKeyToApiStatusKeyTransformer } from '@coze-arch/bot-utils';
|
||||
import { TabStatus } from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
import { useDefaultExPandCheck } from '../src/use-default-expand-check';
|
||||
|
||||
describe('useDefaultExPandCheck', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
(useBotDetailIsReadonly as any).mockReturnValue(false);
|
||||
(skillKeyToApiStatusKeyTransformer as any).mockReturnValue(
|
||||
'transformedKey',
|
||||
);
|
||||
(usePageRuntimeStore as any).mockReturnValue({
|
||||
init: true,
|
||||
editable: true,
|
||||
botSkillBlockCollapsibleState: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when when is false', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useDefaultExPandCheck(
|
||||
{
|
||||
blockKey: 'test' as any,
|
||||
configured: true,
|
||||
},
|
||||
false,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.current).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when init is false', () => {
|
||||
(usePageRuntimeStore as any).mockReturnValue({
|
||||
init: false,
|
||||
editable: true,
|
||||
botSkillBlockCollapsibleState: {},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useDefaultExPandCheck({
|
||||
blockKey: 'test' as any,
|
||||
configured: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when botSkillBlockCollapsibleState is empty', () => {
|
||||
(usePageRuntimeStore as any).mockReturnValue({
|
||||
init: true,
|
||||
editable: true,
|
||||
botSkillBlockCollapsibleState: {},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useDefaultExPandCheck({
|
||||
blockKey: 'test' as any,
|
||||
configured: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return true when state is Open', () => {
|
||||
(usePageRuntimeStore as any).mockReturnValue({
|
||||
init: true,
|
||||
editable: true,
|
||||
botSkillBlockCollapsibleState: {
|
||||
transformedKey: TabStatus.Open,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useDefaultExPandCheck({
|
||||
blockKey: 'test' as any,
|
||||
configured: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when state is Close', () => {
|
||||
(usePageRuntimeStore as any).mockReturnValue({
|
||||
init: true,
|
||||
editable: true,
|
||||
botSkillBlockCollapsibleState: {
|
||||
transformedKey: TabStatus.Close,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useDefaultExPandCheck({
|
||||
blockKey: 'test' as any,
|
||||
configured: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should return configured value when readonly', () => {
|
||||
(useBotDetailIsReadonly as any).mockReturnValue(true);
|
||||
(usePageRuntimeStore as any).mockReturnValue({
|
||||
init: true,
|
||||
editable: true,
|
||||
botSkillBlockCollapsibleState: {
|
||||
transformedKey: TabStatus.Open,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useDefaultExPandCheck({
|
||||
blockKey: 'test' as any,
|
||||
configured: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should return configured value when not editable', () => {
|
||||
(usePageRuntimeStore as any).mockReturnValue({
|
||||
init: true,
|
||||
editable: false,
|
||||
botSkillBlockCollapsibleState: {
|
||||
transformedKey: TabStatus.Open,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useDefaultExPandCheck({
|
||||
blockKey: 'test' as any,
|
||||
configured: false,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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 } from 'vitest';
|
||||
|
||||
import {
|
||||
getFileListByDragOrPaste,
|
||||
formatTypeFileListToTypeArray,
|
||||
} from '../../../src/use-drag-and-paste-upload/helper/get-file-list-by-drag';
|
||||
|
||||
describe('getFileListByDragOrPaste', () => {
|
||||
it('should handle drag event with files', () => {
|
||||
const file1 = new File(['content1'], 'file1.txt');
|
||||
const file2 = new File(['content2'], 'file2.txt');
|
||||
const fileList = {
|
||||
0: file1,
|
||||
1: file2,
|
||||
length: 2,
|
||||
item: (index: number) => (index === 0 ? file1 : file2),
|
||||
};
|
||||
|
||||
const dragEvent = {
|
||||
dataTransfer: {
|
||||
files: fileList,
|
||||
},
|
||||
} as unknown as DragEvent;
|
||||
|
||||
const result = getFileListByDragOrPaste(dragEvent);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toBe(file1);
|
||||
expect(result[1]).toBe(file2);
|
||||
});
|
||||
|
||||
it('should handle paste event with files', () => {
|
||||
const file1 = new File(['content1'], 'file1.txt');
|
||||
const fileList = {
|
||||
0: file1,
|
||||
length: 1,
|
||||
item: (index: number) => (index === 0 ? file1 : null),
|
||||
};
|
||||
|
||||
const pasteEvent = {
|
||||
clipboardData: {
|
||||
files: fileList,
|
||||
},
|
||||
} as unknown as ClipboardEvent;
|
||||
|
||||
const result = getFileListByDragOrPaste(pasteEvent);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe(file1);
|
||||
});
|
||||
|
||||
it('should return empty array when no files are present', () => {
|
||||
const dragEvent = {
|
||||
dataTransfer: {
|
||||
files: undefined,
|
||||
},
|
||||
} as unknown as DragEvent;
|
||||
|
||||
const result = getFileListByDragOrPaste(dragEvent);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatTypeFileListToTypeArray', () => {
|
||||
it('should convert FileList to array of Files', () => {
|
||||
const file1 = new File(['content1'], 'file1.txt');
|
||||
const file2 = new File(['content2'], 'file2.txt');
|
||||
const fileList = {
|
||||
0: file1,
|
||||
1: file2,
|
||||
length: 2,
|
||||
item: (index: number) => (index === 0 ? file1 : file2),
|
||||
};
|
||||
|
||||
const result = formatTypeFileListToTypeArray(
|
||||
fileList as unknown as FileList,
|
||||
);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toBe(file1);
|
||||
expect(result[1]).toBe(file2);
|
||||
});
|
||||
|
||||
it('should filter out null items', () => {
|
||||
const file1 = new File(['content1'], 'file1.txt');
|
||||
const fileList = {
|
||||
0: file1,
|
||||
1: null,
|
||||
length: 2,
|
||||
item: (index: number) => (index === 0 ? file1 : null),
|
||||
};
|
||||
|
||||
const result = formatTypeFileListToTypeArray(
|
||||
fileList as unknown as FileList,
|
||||
);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBe(file1);
|
||||
});
|
||||
|
||||
it('should handle empty FileList', () => {
|
||||
const fileList = {
|
||||
length: 0,
|
||||
item: () => null,
|
||||
};
|
||||
|
||||
const result = formatTypeFileListToTypeArray(
|
||||
fileList as unknown as FileList,
|
||||
);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 } from 'vitest';
|
||||
|
||||
import { isHasFileByDrag } from '../../../src/use-drag-and-paste-upload/helper/is-has-file-by-drag';
|
||||
|
||||
describe('isHasFileByDrag', () => {
|
||||
it('should return true when Files type is present', () => {
|
||||
const dragEvent = {
|
||||
dataTransfer: {
|
||||
types: ['Files', 'text/plain'],
|
||||
},
|
||||
} as unknown as DragEvent;
|
||||
|
||||
expect(isHasFileByDrag(dragEvent)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when Files type is not present', () => {
|
||||
const dragEvent = {
|
||||
dataTransfer: {
|
||||
types: ['text/plain', 'text/html'],
|
||||
},
|
||||
} as unknown as DragEvent;
|
||||
|
||||
expect(isHasFileByDrag(dragEvent)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when dataTransfer is null', () => {
|
||||
const dragEvent = {
|
||||
dataTransfer: null,
|
||||
} as unknown as DragEvent;
|
||||
|
||||
expect(isHasFileByDrag(dragEvent)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when types is undefined', () => {
|
||||
const dragEvent = {
|
||||
dataTransfer: {
|
||||
types: [],
|
||||
},
|
||||
} as unknown as DragEvent;
|
||||
|
||||
expect(isHasFileByDrag(dragEvent)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* 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 { renderHook, act } from '@testing-library/react';
|
||||
import { Toast } from '@coze-arch/bot-semi';
|
||||
|
||||
import { useDragAndPasteUpload } from '../../src/use-drag-and-paste-upload';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@coze-arch/bot-semi', () => ({
|
||||
Toast: {
|
||||
warning: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useDragAndPasteUpload', () => {
|
||||
const mockRef = {
|
||||
current: document.createElement('div'),
|
||||
};
|
||||
|
||||
const mockProps = {
|
||||
ref: mockRef,
|
||||
onUpload: vi.fn(),
|
||||
disableDrag: false,
|
||||
disablePaste: false,
|
||||
fileLimit: 5,
|
||||
maxFileSize: 1024 * 1024, // 1MB
|
||||
isFileFormatValid: (file: File) => file.type.startsWith('image/'),
|
||||
getExistingFileCount: () => 0,
|
||||
closeDelay: 100,
|
||||
invalidFormatMessage: 'Invalid format',
|
||||
invalidSizeMessage: 'File too large',
|
||||
fileExceedsMessage: 'Too many files',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should handle drag events correctly', () => {
|
||||
const { result } = renderHook(() => useDragAndPasteUpload(mockProps));
|
||||
|
||||
// Initial state
|
||||
expect(result.current.isDragOver).toBe(false);
|
||||
|
||||
// Simulate dragover
|
||||
act(() => {
|
||||
const dragOverEvent = new Event('dragover') as DragEvent;
|
||||
Object.defineProperty(dragOverEvent, 'dataTransfer', {
|
||||
value: {
|
||||
types: ['Files'],
|
||||
},
|
||||
});
|
||||
mockRef.current.dispatchEvent(dragOverEvent);
|
||||
});
|
||||
|
||||
expect(result.current.isDragOver).toBe(true);
|
||||
|
||||
// Simulate dragleave
|
||||
act(() => {
|
||||
const dragLeaveEvent = new Event('dragleave') as DragEvent;
|
||||
mockRef.current.dispatchEvent(dragLeaveEvent);
|
||||
});
|
||||
|
||||
// Wait for closeDelay
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(mockProps.closeDelay);
|
||||
});
|
||||
expect(result.current.isDragOver).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle file drop correctly', () => {
|
||||
const validFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
|
||||
renderHook(() => useDragAndPasteUpload(mockProps));
|
||||
|
||||
// Simulate drop
|
||||
act(() => {
|
||||
const dropEvent = new Event('drop') as DragEvent;
|
||||
Object.defineProperty(dropEvent, 'dataTransfer', {
|
||||
value: {
|
||||
types: ['Files'],
|
||||
files: {
|
||||
0: validFile,
|
||||
length: 1,
|
||||
item: (index: number) => (index === 0 ? validFile : null),
|
||||
},
|
||||
},
|
||||
});
|
||||
mockRef.current.dispatchEvent(dropEvent);
|
||||
});
|
||||
|
||||
expect(mockProps.onUpload).toHaveBeenCalledWith([validFile]);
|
||||
});
|
||||
|
||||
it('should handle paste events correctly', () => {
|
||||
const validFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
|
||||
renderHook(() => useDragAndPasteUpload(mockProps));
|
||||
|
||||
// Simulate paste
|
||||
act(() => {
|
||||
const pasteEvent = new Event('paste') as ClipboardEvent;
|
||||
Object.defineProperty(pasteEvent, 'clipboardData', {
|
||||
value: {
|
||||
files: {
|
||||
0: validFile,
|
||||
length: 1,
|
||||
item: (index: number) => (index === 0 ? validFile : null),
|
||||
},
|
||||
},
|
||||
});
|
||||
mockRef.current.dispatchEvent(pasteEvent);
|
||||
});
|
||||
|
||||
expect(mockProps.onUpload).toHaveBeenCalledWith([validFile]);
|
||||
});
|
||||
|
||||
it('should validate file format', () => {
|
||||
const invalidFile = new File(['content'], 'test.txt', {
|
||||
type: 'text/plain',
|
||||
});
|
||||
renderHook(() => useDragAndPasteUpload(mockProps));
|
||||
|
||||
// Simulate drop with invalid file
|
||||
act(() => {
|
||||
const dropEvent = new Event('drop') as DragEvent;
|
||||
Object.defineProperty(dropEvent, 'dataTransfer', {
|
||||
value: {
|
||||
types: ['Files'],
|
||||
files: {
|
||||
0: invalidFile,
|
||||
length: 1,
|
||||
item: (index: number) => (index === 0 ? invalidFile : null),
|
||||
},
|
||||
},
|
||||
});
|
||||
mockRef.current.dispatchEvent(dropEvent);
|
||||
});
|
||||
|
||||
expect(Toast.warning).toHaveBeenCalledWith({
|
||||
content: mockProps.invalidFormatMessage,
|
||||
showClose: false,
|
||||
});
|
||||
expect(mockProps.onUpload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should validate file size', () => {
|
||||
const largeFile = new File(['content'.repeat(1000000)], 'large.jpg', {
|
||||
type: 'image/jpeg',
|
||||
});
|
||||
renderHook(() => useDragAndPasteUpload(mockProps));
|
||||
|
||||
// Simulate drop with large file
|
||||
act(() => {
|
||||
const dropEvent = new Event('drop') as DragEvent;
|
||||
Object.defineProperty(dropEvent, 'dataTransfer', {
|
||||
value: {
|
||||
types: ['Files'],
|
||||
files: {
|
||||
0: largeFile,
|
||||
length: 1,
|
||||
item: (index: number) => (index === 0 ? largeFile : null),
|
||||
},
|
||||
},
|
||||
});
|
||||
mockRef.current.dispatchEvent(dropEvent);
|
||||
});
|
||||
|
||||
expect(Toast.warning).toHaveBeenCalledWith({
|
||||
content: mockProps.invalidSizeMessage,
|
||||
showClose: false,
|
||||
});
|
||||
expect(mockProps.onUpload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should validate file count', () => {
|
||||
const mockPropsWithExistingFiles = {
|
||||
...mockProps,
|
||||
getExistingFileCount: () => 4,
|
||||
};
|
||||
|
||||
const validFiles = [
|
||||
new File(['content1'], 'test1.jpg', { type: 'image/jpeg' }),
|
||||
new File(['content2'], 'test2.jpg', { type: 'image/jpeg' }),
|
||||
];
|
||||
|
||||
renderHook(() => useDragAndPasteUpload(mockPropsWithExistingFiles));
|
||||
|
||||
// Simulate drop with too many files
|
||||
act(() => {
|
||||
const dropEvent = new Event('drop') as DragEvent;
|
||||
Object.defineProperty(dropEvent, 'dataTransfer', {
|
||||
value: {
|
||||
types: ['Files'],
|
||||
files: {
|
||||
0: validFiles[0],
|
||||
1: validFiles[1],
|
||||
length: 2,
|
||||
item: (index: number) => validFiles[index] || null,
|
||||
},
|
||||
},
|
||||
});
|
||||
mockRef.current.dispatchEvent(dropEvent);
|
||||
});
|
||||
|
||||
expect(Toast.warning).toHaveBeenCalledWith({
|
||||
content: mockProps.fileExceedsMessage,
|
||||
showClose: false,
|
||||
});
|
||||
expect(mockProps.onUpload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should respect disableDrag prop', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useDragAndPasteUpload({ ...mockProps, disableDrag: true }),
|
||||
);
|
||||
|
||||
// Simulate dragover
|
||||
act(() => {
|
||||
const dragOverEvent = new Event('dragover') as DragEvent;
|
||||
Object.defineProperty(dragOverEvent, 'dataTransfer', {
|
||||
value: {
|
||||
types: ['Files'],
|
||||
},
|
||||
});
|
||||
mockRef.current.dispatchEvent(dragOverEvent);
|
||||
});
|
||||
|
||||
expect(result.current.isDragOver).toBe(false);
|
||||
});
|
||||
|
||||
it('should respect disablePaste prop', () => {
|
||||
const validFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
|
||||
renderHook(() =>
|
||||
useDragAndPasteUpload({ ...mockProps, disablePaste: true }),
|
||||
);
|
||||
|
||||
// Simulate paste
|
||||
act(() => {
|
||||
const pasteEvent = new Event('paste') as ClipboardEvent;
|
||||
Object.defineProperty(pasteEvent, 'clipboardData', {
|
||||
value: {
|
||||
files: {
|
||||
0: validFile,
|
||||
length: 1,
|
||||
item: (index: number) => (index === 0 ? validFile : null),
|
||||
},
|
||||
},
|
||||
});
|
||||
mockRef.current.dispatchEvent(pasteEvent);
|
||||
});
|
||||
|
||||
expect(mockProps.onUpload).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -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 { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { useDragAndPasteUpload } from '../src/use-drag-and-paste-upload';
|
||||
|
||||
describe('useDragAndPasteUpload', () => {
|
||||
it('return correctly', () => {
|
||||
const ref = { current: null };
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() =>
|
||||
useDragAndPasteUpload({
|
||||
ref,
|
||||
disableDrag: false,
|
||||
disablePaste: false,
|
||||
onUpload: () => 0,
|
||||
fileLimit: 3,
|
||||
isFileFormatValid: () => true,
|
||||
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
closeDelay: undefined,
|
||||
invalidFormatMessage: '不支持的文件类型',
|
||||
invalidSizeMessage: '不支持文件大小超过 10MB',
|
||||
fileExceedsMessage: '最多上传 3 个文件',
|
||||
getExistingFileCount: () => 0,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(current).toMatchObject({
|
||||
isDragOver: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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 } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useExposure } from '../src/use-exposure';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('ahooks', () => ({
|
||||
useInViewport: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@coze-arch/bot-tea', () => ({
|
||||
sendTeaEvent: vi.fn(),
|
||||
EVENT_NAMES: {
|
||||
page_view: 'page_view',
|
||||
},
|
||||
}));
|
||||
|
||||
import { useInViewport } from 'ahooks';
|
||||
import { sendTeaEvent, EVENT_NAMES } from '@coze-arch/bot-tea';
|
||||
|
||||
describe('useExposure', () => {
|
||||
const mockTarget = { current: document.createElement('div') };
|
||||
const mockEventName = EVENT_NAMES.page_view;
|
||||
const mockReportParams = { key: 'value' };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should report when element is in view and needReport is true', () => {
|
||||
(useInViewport as any).mockReturnValue([true]);
|
||||
|
||||
renderHook(() =>
|
||||
useExposure({
|
||||
target: mockTarget,
|
||||
eventName: mockEventName,
|
||||
reportParams: mockReportParams,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(sendTeaEvent).toHaveBeenCalledWith(mockEventName, mockReportParams);
|
||||
});
|
||||
|
||||
it('should not report when element is not in view', () => {
|
||||
(useInViewport as any).mockReturnValue([false]);
|
||||
|
||||
renderHook(() =>
|
||||
useExposure({
|
||||
target: mockTarget,
|
||||
eventName: mockEventName,
|
||||
reportParams: mockReportParams,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(sendTeaEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not report when needReport is false', () => {
|
||||
(useInViewport as any).mockReturnValue([true]);
|
||||
|
||||
renderHook(() =>
|
||||
useExposure({
|
||||
target: mockTarget,
|
||||
eventName: mockEventName,
|
||||
reportParams: mockReportParams,
|
||||
needReport: false,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(sendTeaEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should report only once when isReportOnce is true', () => {
|
||||
(useInViewport as any).mockReturnValue([true]);
|
||||
|
||||
const { rerender } = renderHook(() =>
|
||||
useExposure({
|
||||
target: mockTarget,
|
||||
eventName: mockEventName,
|
||||
reportParams: mockReportParams,
|
||||
isReportOnce: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(sendTeaEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Rerender should not trigger another report
|
||||
rerender();
|
||||
expect(sendTeaEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should report multiple times when isReportOnce is false', () => {
|
||||
(useInViewport as any).mockReturnValue([true]);
|
||||
|
||||
const { rerender } = renderHook(() =>
|
||||
useExposure({
|
||||
target: mockTarget,
|
||||
eventName: mockEventName,
|
||||
reportParams: mockReportParams,
|
||||
isReportOnce: false,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(sendTeaEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Rerender should not trigger another report
|
||||
rerender();
|
||||
expect(sendTeaEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should pass options to useInViewport', () => {
|
||||
const mockOptions = { threshold: 0.5 };
|
||||
(useInViewport as any).mockReturnValue([true]);
|
||||
|
||||
renderHook(() =>
|
||||
useExposure({
|
||||
target: mockTarget,
|
||||
eventName: mockEventName,
|
||||
reportParams: mockReportParams,
|
||||
options: mockOptions,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(useInViewport).toHaveBeenCalledWith(mockTarget, mockOptions);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useInitialValue } from '../src/use-initial-value';
|
||||
|
||||
describe('useInitialValue', () => {
|
||||
it('should return the initial value', () => {
|
||||
const initialValue = 'test';
|
||||
const { result } = renderHook(() => useInitialValue(initialValue));
|
||||
expect(result.current).toBe(initialValue);
|
||||
});
|
||||
|
||||
it('should maintain the initial value even if input changes', () => {
|
||||
let value = 'initial';
|
||||
const { result, rerender } = renderHook(() => useInitialValue(value));
|
||||
expect(result.current).toBe('initial');
|
||||
|
||||
value = 'changed';
|
||||
rerender();
|
||||
expect(result.current).toBe('initial');
|
||||
});
|
||||
|
||||
it('should work with different types', () => {
|
||||
const numberValue = 42;
|
||||
const { result: numberResult } = renderHook(() =>
|
||||
useInitialValue(numberValue),
|
||||
);
|
||||
expect(numberResult.current).toBe(42);
|
||||
|
||||
const objectValue = { key: 'value' };
|
||||
const { result: objectResult } = renderHook(() =>
|
||||
useInitialValue(objectValue),
|
||||
);
|
||||
expect(objectResult.current).toEqual({ key: 'value' });
|
||||
|
||||
const arrayValue = [1, 2, 3];
|
||||
const { result: arrayResult } = renderHook(() =>
|
||||
useInitialValue(arrayValue),
|
||||
);
|
||||
expect(arrayResult.current).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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 { renderHook, act } from '@testing-library/react';
|
||||
|
||||
import { useLineClamp } from '../src/use-line-clamp';
|
||||
|
||||
describe('useLineClamp', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should return contentRef and isClamped', () => {
|
||||
const { result } = renderHook(() => useLineClamp());
|
||||
|
||||
expect(result.current.contentRef).toBeDefined();
|
||||
expect(result.current.isClamped).toBe(false);
|
||||
});
|
||||
|
||||
it('should add and remove resize event listener', () => {
|
||||
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
|
||||
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
|
||||
|
||||
const { unmount } = renderHook(() => useLineClamp());
|
||||
|
||||
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
||||
'resize',
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(removeEventListenerSpy).toHaveBeenCalledWith(
|
||||
'resize',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should update isClamped when content height changes', () => {
|
||||
const mockDiv = document.createElement('div');
|
||||
Object.defineProperties(mockDiv, {
|
||||
scrollHeight: {
|
||||
configurable: true,
|
||||
get: () => 100,
|
||||
},
|
||||
clientHeight: {
|
||||
configurable: true,
|
||||
get: () => 50,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useLineClamp());
|
||||
|
||||
// 使用 vi.spyOn 来模拟 contentRef.current
|
||||
vi.spyOn(result.current.contentRef, 'current', 'get').mockReturnValue(
|
||||
mockDiv,
|
||||
);
|
||||
|
||||
// 使用 act 包装异步操作
|
||||
act(() => {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
|
||||
expect(result.current.isClamped).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle null contentRef', () => {
|
||||
const { result } = renderHook(() => useLineClamp());
|
||||
|
||||
// 使用 vi.spyOn 来模拟 contentRef.current 为 null
|
||||
vi.spyOn(result.current.contentRef, 'current', 'get').mockReturnValue(null);
|
||||
|
||||
// 使用 act 包装异步操作
|
||||
act(() => {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
|
||||
expect(result.current.isClamped).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -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 { describe, it, expect, vi } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useLoggedIn } from '../src/use-loggedin';
|
||||
|
||||
// Mock userStoreService
|
||||
vi.mock('@coze-studio/user-store', () => ({
|
||||
userStoreService: {
|
||||
useIsLogined: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { userStoreService } from '@coze-studio/user-store';
|
||||
|
||||
describe('useLoggedIn', () => {
|
||||
it('should return true when user is logged in', () => {
|
||||
(userStoreService.useIsLogined as any).mockReturnValue(true);
|
||||
const { result } = renderHook(() => useLoggedIn());
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when user is not logged in', () => {
|
||||
(userStoreService.useIsLogined as any).mockReturnValue(false);
|
||||
const { result } = renderHook(() => useLoggedIn());
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should call userStoreService.useIsLogined', () => {
|
||||
renderHook(() => useLoggedIn());
|
||||
expect(userStoreService.useIsLogined).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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 } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { ScreenRange, useMediaQuery } from '@coze-arch/responsive-kit';
|
||||
|
||||
import { useIsResponsiveByRouteConfig } from '../src/use-responsive';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@coze-arch/responsive-kit', () => ({
|
||||
useMediaQuery: vi.fn(),
|
||||
ScreenRange: {
|
||||
LG: 'lg',
|
||||
MD: 'md',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useLocation: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../src/use-route-config', () => ({
|
||||
useRouteConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
import { useRouteConfig } from '../src/use-route-config';
|
||||
|
||||
describe('useIsResponsiveByRouteConfig', () => {
|
||||
it('should handle responsive=true case', () => {
|
||||
(useRouteConfig as any).mockReturnValue({ responsive: true });
|
||||
(useMediaQuery as any).mockReturnValue(false);
|
||||
|
||||
const { result } = renderHook(() => useIsResponsiveByRouteConfig());
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle custom responsive config with include=true', () => {
|
||||
(useRouteConfig as any).mockReturnValue({
|
||||
responsive: {
|
||||
rangeMax: ScreenRange.LG,
|
||||
include: true,
|
||||
},
|
||||
});
|
||||
(useMediaQuery as any).mockReturnValue(true);
|
||||
|
||||
const { result } = renderHook(() => useIsResponsiveByRouteConfig());
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle custom responsive config with include=false', () => {
|
||||
(useRouteConfig as any).mockReturnValue({
|
||||
responsive: {
|
||||
rangeMax: ScreenRange.LG,
|
||||
include: false,
|
||||
},
|
||||
});
|
||||
(useMediaQuery as any).mockReturnValue(false);
|
||||
|
||||
const { result } = renderHook(() => useIsResponsiveByRouteConfig());
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when responsive is undefined', () => {
|
||||
(useRouteConfig as any).mockReturnValue({});
|
||||
(useMediaQuery as any).mockReturnValue(true);
|
||||
|
||||
const { result } = renderHook(() => useIsResponsiveByRouteConfig());
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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, type Mock } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { ScreenRange } from '@coze-arch/responsive-kit';
|
||||
|
||||
import { useRouteConfig } from '../src/use-route-config';
|
||||
|
||||
// Mock useMatches hook
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useMatches: vi.fn(),
|
||||
}));
|
||||
|
||||
import { useMatches } from 'react-router-dom';
|
||||
|
||||
describe('useRouteConfig', () => {
|
||||
it('should return default config when no matches', () => {
|
||||
const mockUseMatches = useMatches as Mock;
|
||||
mockUseMatches.mockReturnValue([]);
|
||||
|
||||
const defaults = {
|
||||
hasSider: true,
|
||||
showMobileTips: false,
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useRouteConfig(defaults));
|
||||
|
||||
expect(result.current).toEqual(defaults);
|
||||
});
|
||||
|
||||
it('should merge configs from matched routes', () => {
|
||||
const mockUseMatches = useMatches as Mock;
|
||||
mockUseMatches.mockReturnValue([
|
||||
{
|
||||
handle: {
|
||||
hasSider: true,
|
||||
showMobileTips: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
handle: {
|
||||
showMobileTips: true,
|
||||
requireAuth: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const defaults = {
|
||||
hasSider: false,
|
||||
showAssistant: true,
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useRouteConfig(defaults));
|
||||
|
||||
expect(result.current).toEqual({
|
||||
hasSider: true,
|
||||
showMobileTips: true,
|
||||
requireAuth: true,
|
||||
showAssistant: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle responsive config', () => {
|
||||
const mockUseMatches = useMatches as Mock;
|
||||
mockUseMatches.mockReturnValue([
|
||||
{
|
||||
handle: {
|
||||
responsive: {
|
||||
rangeMax: ScreenRange.LG,
|
||||
include: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { result } = renderHook(() => useRouteConfig());
|
||||
|
||||
expect(result.current).toEqual({
|
||||
responsive: {
|
||||
rangeMax: ScreenRange.LG,
|
||||
include: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty defaults', () => {
|
||||
const mockUseMatches = useMatches as Mock;
|
||||
mockUseMatches.mockReturnValue([]);
|
||||
|
||||
const { result } = renderHook(() => useRouteConfig());
|
||||
|
||||
expect(result.current).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -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/arch/bot-hooks-base/eslint.config.js
Normal file
7
frontend/packages/arch/bot-hooks-base/eslint.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
});
|
||||
62
frontend/packages/arch/bot-hooks-base/package.json
Normal file
62
frontend/packages/arch/bot-hooks-base/package.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "@coze-arch/bot-hooks-base",
|
||||
"version": "0.0.1",
|
||||
"description": "hooks for bot studio",
|
||||
"license": "Apache-2.0",
|
||||
"author": "zhanghaochen.z@bytedance.com",
|
||||
"maintainers": [],
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-agent-ide/tool-config": "workspace:*",
|
||||
"@coze-arch/bot-api": "workspace:*",
|
||||
"@coze-arch/bot-semi": "workspace:*",
|
||||
"@coze-arch/bot-tea": "workspace:*",
|
||||
"@coze-arch/bot-utils": "workspace:*",
|
||||
"@coze-arch/responsive-kit": "workspace:*",
|
||||
"@coze-common/chat-area": "workspace:*",
|
||||
"@coze-studio/bot-detail-store": "workspace:*",
|
||||
"@coze-studio/user-store": "workspace:*",
|
||||
"ahooks": "^3.7.8",
|
||||
"lodash-es": "^4.17.21",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@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-common/assets": "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/lodash-es": "^4.17.10",
|
||||
"@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",
|
||||
"react-router-dom": "^6.22.0",
|
||||
"styled-components": ">= 2",
|
||||
"stylelint": "^15.11.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vitest": "~3.0.5",
|
||||
"webpack": "~5.91.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.2.0",
|
||||
"react-dom": ">=18.2.0"
|
||||
},
|
||||
"// deps": "immer@^10.0.3 为脚本自动补齐,请勿改动"
|
||||
}
|
||||
|
||||
21
frontend/packages/arch/bot-hooks-base/setup/index.ts
Normal file
21
frontend/packages/arch/bot-hooks-base/setup/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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('@coze-arch/i18n', () => ({
|
||||
I18n: {
|
||||
t: vi.fn(),
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 { useParams } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { messageReportEvent } from '@coze-arch/bot-utils';
|
||||
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
|
||||
|
||||
export const useMessageReportEvent = () => {
|
||||
const params = useParams<DynamicParams>();
|
||||
useEffect(() => {
|
||||
if (params.bot_id) {
|
||||
messageReportEvent.start(params.bot_id);
|
||||
}
|
||||
return () => {
|
||||
messageReportEvent.interrupt();
|
||||
};
|
||||
}, [params.bot_id]);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 { userStoreService } from '@coze-studio/user-store';
|
||||
import { type UserSenderInfo } from '@coze-common/chat-area';
|
||||
|
||||
export const useUserSenderInfo = () => {
|
||||
const userLabel = userStoreService.useUserLabel();
|
||||
const userInfo = userStoreService.useUserInfo();
|
||||
if (!userInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userSenderInfo: UserSenderInfo = {
|
||||
url: userInfo?.avatar_url || '',
|
||||
nickname: userInfo?.name || '',
|
||||
id: userInfo?.user_id_str || '',
|
||||
userUniqueName: userInfo?.app_user_info?.user_unique_name || '',
|
||||
userLabel,
|
||||
};
|
||||
|
||||
return userSenderInfo;
|
||||
};
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @description `LayoutContext`用于跨组件传递布局相关信息
|
||||
* @since 2024.03.05
|
||||
*/
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export enum PlacementEnum {
|
||||
LEFT = 'left',
|
||||
CENTER = 'center',
|
||||
RIGHT = 'right',
|
||||
}
|
||||
|
||||
interface ILayoutContext {
|
||||
placement: PlacementEnum;
|
||||
}
|
||||
|
||||
const context = createContext<ILayoutContext>({
|
||||
placement: PlacementEnum.CENTER,
|
||||
});
|
||||
|
||||
export const useLayoutContext = () => useContext(context);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const LayoutContext = context.Provider;
|
||||
25
frontend/packages/arch/bot-hooks-base/src/global.d.ts
vendored
Normal file
25
frontend/packages/arch/bot-hooks-base/src/global.d.ts
vendored
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.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
|
||||
declare const ENABLE_COVERAGE: boolean;
|
||||
interface Window {
|
||||
/**
|
||||
* tea 实例
|
||||
*/
|
||||
Tea?: any;
|
||||
}
|
||||
40
frontend/packages/arch/bot-hooks-base/src/index.ts
Normal file
40
frontend/packages/arch/bot-hooks-base/src/index.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.
|
||||
*/
|
||||
|
||||
export { useRouteConfig, TRouteConfigGlobal } from './use-route-config';
|
||||
export { useIsResponsiveByRouteConfig } from './use-responsive';
|
||||
export { useLoggedIn } from './use-loggedin';
|
||||
export { useLineClamp } from './use-line-clamp';
|
||||
export { useInitialValue } from './use-initial-value';
|
||||
export { useExposure, UseExposureParams } from './use-exposure';
|
||||
export {
|
||||
useComponentState,
|
||||
ComponentStateUpdateFunc,
|
||||
} from './use-component-state';
|
||||
export {
|
||||
useDragAndPasteUpload,
|
||||
UseDragAndPasteUploadParam,
|
||||
} from './use-drag-and-paste-upload';
|
||||
export { useDefaultExPandCheck } from './use-default-expand-check';
|
||||
export { useResetLocationState } from './router/use-reset-location-state';
|
||||
export {
|
||||
PlacementEnum,
|
||||
useLayoutContext,
|
||||
LayoutContext,
|
||||
} from './editor-layout';
|
||||
export { usePageState, PageStateUpdateFunc } from './use-page-state';
|
||||
export { useUserSenderInfo } from './bot/use-user-sender-info';
|
||||
export { useMessageReportEvent } from './bot/use-message-report-event';
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 { useLocation } from 'react-router-dom';
|
||||
|
||||
/** 清空认证数据的路由参数 */
|
||||
export const resetAuthLoginDataFromRoute = () => {
|
||||
window.history.replaceState({}, '');
|
||||
};
|
||||
export function useResetLocationState() {
|
||||
const location = useLocation();
|
||||
return () => {
|
||||
// 清空location的state
|
||||
location.state = {};
|
||||
resetAuthLoginDataFromRoute();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 { useState } from 'react';
|
||||
|
||||
import { type Obj } from '@coze-arch/bot-typings/common';
|
||||
|
||||
export interface ComponentStateUpdateFunc<State extends Obj> {
|
||||
(freshState: State, replace: true): void;
|
||||
(freshState: Partial<State>): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对 state 一层封装,用途:
|
||||
* 1. 默认增量更新
|
||||
* 2. 支持重置
|
||||
*
|
||||
* @example
|
||||
* const { state, resetState, setState } = useComponentState({ a: 1, b: 2 });
|
||||
* console.log(state); // { a: 1, b: 2 }
|
||||
* setState({ b: 3 }); // { a: 1, b: 3 }
|
||||
*
|
||||
* setState({ a: 2 }, true); // { a: 2 }
|
||||
*
|
||||
* resetState(); // { a: 1, b: 2 }
|
||||
*
|
||||
* @author lengfangbing
|
||||
* @docs by zhanghaochen
|
||||
*/
|
||||
export function useComponentState<State extends Obj>(initState: State) {
|
||||
const [state, customSetState] = useState(initState);
|
||||
|
||||
function setState(freshState: State, replace: true): void;
|
||||
function setState(freshState: Partial<State>): void;
|
||||
function setState(freshState: Partial<State> | State, replace?: true) {
|
||||
if (replace) {
|
||||
customSetState(freshState as State);
|
||||
}
|
||||
customSetState(prev => ({ ...prev, ...freshState }));
|
||||
}
|
||||
|
||||
const resetState = () => {
|
||||
customSetState(initState);
|
||||
};
|
||||
|
||||
return {
|
||||
state,
|
||||
resetState,
|
||||
setState: setState as ComponentStateUpdateFunc<State>,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { size } from 'lodash-es';
|
||||
import { type SkillKeyEnum } from '@coze-agent-ide/tool-config';
|
||||
import { usePageRuntimeStore } from '@coze-studio/bot-detail-store/page-runtime';
|
||||
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
|
||||
import { skillKeyToApiStatusKeyTransformer } from '@coze-arch/bot-utils';
|
||||
import {
|
||||
TabStatus,
|
||||
type TabDisplayItems,
|
||||
} from '@coze-arch/bot-api/developer_api';
|
||||
|
||||
/**
|
||||
* 用于校验当前模块默认展开收起状态
|
||||
* @deprecated 改属性已经废弃不维护,请更换@coze-agent-ide/tool中的useToolContentBlockDefaultExpand
|
||||
* @param blockKey 主键
|
||||
* @param configured 是否有配置内容
|
||||
* @param when 是否校验
|
||||
*
|
||||
*/
|
||||
const useDefaultExPandCheck = (
|
||||
$params: {
|
||||
blockKey: SkillKeyEnum;
|
||||
configured: boolean;
|
||||
},
|
||||
$when = true,
|
||||
) => {
|
||||
const { blockKey, configured = false } = $params;
|
||||
const isReadonly = useBotDetailIsReadonly();
|
||||
const { init, editable, botSkillBlockCollapsibleState } = usePageRuntimeStore(
|
||||
useShallow(store => ({
|
||||
init: store.init,
|
||||
editable: store.editable,
|
||||
botSkillBlockCollapsibleState: store.botSkillBlockCollapsibleState,
|
||||
})),
|
||||
);
|
||||
return useMemo(() => {
|
||||
// 不做校验
|
||||
if (!$when) {
|
||||
return undefined;
|
||||
// 状态机未就绪
|
||||
} else if (!init || size(botSkillBlockCollapsibleState) === 0) {
|
||||
return undefined;
|
||||
/**
|
||||
* @description 仅在满足以下条件时用户行为记录才能生效
|
||||
*
|
||||
* 1. 用户有编辑权限
|
||||
* 2. 不能是历史预览环境
|
||||
* 3. 必须已配置
|
||||
*/
|
||||
} else if (editable && !isReadonly && configured) {
|
||||
const transformerBlockKey = skillKeyToApiStatusKeyTransformer(blockKey);
|
||||
const collapsibleState =
|
||||
botSkillBlockCollapsibleState[
|
||||
transformerBlockKey as keyof TabDisplayItems
|
||||
];
|
||||
if (collapsibleState === TabStatus.Open) {
|
||||
return true;
|
||||
} else if (collapsibleState === TabStatus.Close) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return configured;
|
||||
}, [
|
||||
$when,
|
||||
blockKey,
|
||||
configured,
|
||||
init,
|
||||
isReadonly,
|
||||
editable,
|
||||
botSkillBlockCollapsibleState,
|
||||
]);
|
||||
};
|
||||
|
||||
export { useDefaultExPandCheck };
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 const getFileListByDragOrPaste = (
|
||||
e: HTMLElementEventMap['drop'] | HTMLElementEventMap['paste'],
|
||||
): File[] => {
|
||||
let fileList: FileList | undefined;
|
||||
if ('dataTransfer' in e) {
|
||||
fileList = e.dataTransfer?.files;
|
||||
} else {
|
||||
fileList = e.clipboardData?.files;
|
||||
}
|
||||
if (!fileList) {
|
||||
return [];
|
||||
}
|
||||
return formatTypeFileListToTypeArray(fileList);
|
||||
};
|
||||
|
||||
export const formatTypeFileListToTypeArray = (fileList: FileList) => {
|
||||
const fileLength = fileList.length;
|
||||
const fileArray: (File | null)[] = [];
|
||||
for (let i = 0; i < fileLength; i++) {
|
||||
fileArray.push(fileList.item(i));
|
||||
}
|
||||
return fileArray.filter((file): file is File => Boolean(file));
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 const isHasFileByDrag = (e: HTMLElementEventMap['drag']) =>
|
||||
Boolean(e.dataTransfer?.types.includes('Files'));
|
||||
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* 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, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Toast } from '@coze-arch/bot-semi';
|
||||
|
||||
import { isHasFileByDrag } from './helper/is-has-file-by-drag';
|
||||
import { getFileListByDragOrPaste } from './helper/get-file-list-by-drag';
|
||||
|
||||
export interface UseDragAndPasteUploadParam {
|
||||
ref: RefObject<HTMLDivElement>;
|
||||
/**
|
||||
* 触发上传的回调
|
||||
*/
|
||||
onUpload: (fileList: File[]) => void;
|
||||
/**
|
||||
* 是否禁用拖拽上传
|
||||
*/
|
||||
disableDrag: boolean;
|
||||
/**
|
||||
* 是否禁用粘贴上传
|
||||
*/
|
||||
disablePaste: boolean;
|
||||
/**
|
||||
* 最大上传的文件数量
|
||||
*/
|
||||
fileLimit: number;
|
||||
/**
|
||||
* 文件大小, eg: 10MB = 10 * 1024 * 1024
|
||||
*/
|
||||
maxFileSize: number;
|
||||
invalidSizeMessage: string | undefined;
|
||||
invalidFormatMessage: string | undefined;
|
||||
fileExceedsMessage: string | undefined;
|
||||
/**
|
||||
* 文件格式是否合法
|
||||
*/
|
||||
isFileFormatValid: (file: File) => boolean;
|
||||
/**
|
||||
* @returns 已存在文件的数量
|
||||
*/
|
||||
getExistingFileCount: () => number;
|
||||
/**
|
||||
* 用户离开拖拽区域时, state 变化的延迟
|
||||
* @default 100
|
||||
*/
|
||||
closeDelay: number | undefined;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function, @coze-arch/max-line-per-function -- drag callback
|
||||
export const useDragAndPasteUpload = ({
|
||||
onUpload,
|
||||
disableDrag,
|
||||
disablePaste,
|
||||
fileLimit,
|
||||
isFileFormatValid,
|
||||
maxFileSize,
|
||||
getExistingFileCount,
|
||||
closeDelay = 100,
|
||||
invalidFormatMessage,
|
||||
invalidSizeMessage,
|
||||
fileExceedsMessage,
|
||||
ref,
|
||||
}: UseDragAndPasteUploadParam) => {
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
|
||||
/**
|
||||
* drag 时, 指针从 parent dom 进入到 child dom 时会快速连续触发 onDragEnter onDragLeave 导致状态流转错误
|
||||
* 在 onLeave 时给状态流转加上延时能够避免流转问题
|
||||
* 触发 dragEnter dragLeave 时, event.target 不一定指向 parent dom, 所以也无法通过 target 来判断
|
||||
*/
|
||||
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const clearTimer = () => {
|
||||
if (!timer.current) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer.current);
|
||||
timer.current = null;
|
||||
};
|
||||
|
||||
const handleDropOrPaste = (
|
||||
e: HTMLElementEventMap['paste'] | HTMLElementEventMap['drop'],
|
||||
) => getFileListByDragOrPaste(e);
|
||||
|
||||
const handleUpload = (fileList: File[]) => {
|
||||
if (!fileList.some(isFileFormatValid)) {
|
||||
Toast.warning({
|
||||
content: invalidFormatMessage,
|
||||
showClose: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fileList.some(file => file.size <= maxFileSize)) {
|
||||
Toast.warning({
|
||||
content: invalidSizeMessage,
|
||||
showClose: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const remainingCount = fileLimit - getExistingFileCount();
|
||||
|
||||
if (fileList.length > remainingCount) {
|
||||
Toast.warning({
|
||||
content: fileExceedsMessage,
|
||||
showClose: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onUpload(fileList);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const target = ref.current;
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
if (disableDrag) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onDragEnter = (e: HTMLElementEventMap['dragenter']) => {
|
||||
clearTimer();
|
||||
if (!isHasFileByDrag(e)) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onDragOver = (e: HTMLElementEventMap['dragover']) => {
|
||||
/**
|
||||
* {@link https://segmentfault.com/q/1010000011746669}
|
||||
* 原理:
|
||||
* 这里阻止的默认行为是开启可编辑模式,具体就是document.designMode属性,
|
||||
* 该属性默认是off关闭的,当开启之后就可以对网页进行编辑
|
||||
* 开启的方式就是document.designMode = "on"; 开启之后就不用在监听dragover事件中阻止默认了
|
||||
*/
|
||||
e.preventDefault();
|
||||
clearTimer();
|
||||
if (!isHasFileByDrag(e)) {
|
||||
return;
|
||||
}
|
||||
setIsDragOver(true);
|
||||
};
|
||||
const onDragLeave = (e: HTMLElementEventMap['dragleave']) => {
|
||||
clearTimer();
|
||||
|
||||
timer.current = setTimeout(() => {
|
||||
setIsDragOver(false);
|
||||
}, closeDelay);
|
||||
};
|
||||
const onDragDrop = (e: HTMLElementEventMap['drop']) => {
|
||||
clearTimer();
|
||||
|
||||
if (!isHasFileByDrag(e)) {
|
||||
return;
|
||||
}
|
||||
setIsDragOver(false);
|
||||
e.preventDefault();
|
||||
handleUpload(handleDropOrPaste(e));
|
||||
};
|
||||
target.addEventListener('dragenter', onDragEnter);
|
||||
target.addEventListener('dragover', onDragOver);
|
||||
target.addEventListener('dragleave', onDragLeave);
|
||||
target.addEventListener('drop', onDragDrop);
|
||||
|
||||
return () => {
|
||||
clearTimer();
|
||||
target.removeEventListener('dragenter', onDragEnter);
|
||||
target.removeEventListener('dragover', onDragOver);
|
||||
target.removeEventListener('dragleave', onDragLeave);
|
||||
target.removeEventListener('drop', onDragDrop);
|
||||
};
|
||||
}, [ref.current, disableDrag]);
|
||||
|
||||
useEffect(() => {
|
||||
const target = ref.current;
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onPaste = (e: HTMLElementEventMap['paste']) => {
|
||||
const fileList = handleDropOrPaste(e);
|
||||
|
||||
if (!fileList.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
if (disablePaste) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleUpload(fileList);
|
||||
};
|
||||
target.addEventListener('paste', onPaste);
|
||||
|
||||
return () => {
|
||||
target.removeEventListener('paste', onPaste);
|
||||
};
|
||||
}, [ref.current, disablePaste]);
|
||||
|
||||
return { isDragOver };
|
||||
};
|
||||
60
frontend/packages/arch/bot-hooks-base/src/use-exposure.ts
Normal file
60
frontend/packages/arch/bot-hooks-base/src/use-exposure.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 { useEffect, useRef } from 'react';
|
||||
|
||||
import { type BasicTarget } from 'ahooks/lib/utils/domTarget';
|
||||
import { type Options } from 'ahooks/lib/useInViewport';
|
||||
import { useInViewport } from 'ahooks';
|
||||
import { type EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
|
||||
|
||||
export interface UseExposureParams {
|
||||
/** 曝光元素 */
|
||||
target: BasicTarget;
|
||||
/** Intersection observer参数 */
|
||||
options?: Options;
|
||||
/** 上报事件名称 */
|
||||
eventName?: EVENT_NAMES;
|
||||
/** 上报参数 */
|
||||
reportParams?: Record<string, unknown>;
|
||||
/** 是否进行上报 默认为true */
|
||||
needReport?: boolean;
|
||||
isReportOnce?: boolean;
|
||||
}
|
||||
|
||||
/** 曝光埋点上报 */
|
||||
export const useExposure = ({
|
||||
target,
|
||||
options,
|
||||
eventName,
|
||||
reportParams,
|
||||
needReport = true,
|
||||
isReportOnce = false,
|
||||
}: UseExposureParams) => {
|
||||
const [isInView] = useInViewport(target, options);
|
||||
const refHasReport = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isReportOnce && refHasReport.current) {
|
||||
//已上报过数据,就直接返回
|
||||
return;
|
||||
}
|
||||
if (needReport && isInView) {
|
||||
sendTeaEvent(eventName, reportParams);
|
||||
refHasReport.current = true;
|
||||
}
|
||||
}, [needReport, isInView, isReportOnce]);
|
||||
};
|
||||
@@ -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 { useRef } from 'react';
|
||||
|
||||
export function useInitialValue<T>(value: T): T {
|
||||
const ref = useRef<T>(value);
|
||||
return ref.current;
|
||||
}
|
||||
41
frontend/packages/arch/bot-hooks-base/src/use-line-clamp.ts
Normal file
41
frontend/packages/arch/bot-hooks-base/src/use-line-clamp.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 { useState, useEffect, useRef } from 'react';
|
||||
|
||||
export function useLineClamp() {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [isClamped, setIsClamped] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkClamped = () => {
|
||||
if (contentRef.current) {
|
||||
setIsClamped(
|
||||
contentRef.current.scrollHeight > contentRef.current.clientHeight,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
checkClamped();
|
||||
window.addEventListener('resize', checkClamped);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', checkClamped);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { contentRef, isClamped };
|
||||
}
|
||||
22
frontend/packages/arch/bot-hooks-base/src/use-loggedin.ts
Normal file
22
frontend/packages/arch/bot-hooks-base/src/use-loggedin.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 { userStoreService } from '@coze-studio/user-store';
|
||||
|
||||
/**
|
||||
* 判断当前用户是否处于登陆状态
|
||||
*/
|
||||
export const useLoggedIn = () => userStoreService.useIsLogined();
|
||||
68
frontend/packages/arch/bot-hooks-base/src/use-page-state.ts
Normal file
68
frontend/packages/arch/bot-hooks-base/src/use-page-state.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export interface PageStateUpdateFunc<State extends object = object> {
|
||||
(freshState: State, replace: true): void;
|
||||
(freshState: Partial<State>): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对state一层封装,包含更新state、重置state
|
||||
*
|
||||
* @deprecated 请使用 bot-hooks 的 useComponentStates
|
||||
*/
|
||||
export function usePageState<State extends object = object>(
|
||||
initState: State,
|
||||
autoResetWhenDestroy = false,
|
||||
) {
|
||||
const [state, customSetState] = useState(initState);
|
||||
const destroyRef = useRef(false);
|
||||
|
||||
function setState(freshState: State, replace: true): void;
|
||||
function setState(freshState: Partial<State>): void;
|
||||
function setState(freshState: Partial<State> | State, replace?: true) {
|
||||
if (replace) {
|
||||
customSetState(freshState as State);
|
||||
}
|
||||
customSetState(prev => ({ ...prev, ...freshState }));
|
||||
}
|
||||
|
||||
const resetState = () => {
|
||||
customSetState(initState);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
destroyRef.current = autoResetWhenDestroy;
|
||||
}, [autoResetWhenDestroy]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
// 自动重置状态
|
||||
if (destroyRef.current) {
|
||||
resetState();
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
state,
|
||||
resetState,
|
||||
setState: setState as PageStateUpdateFunc<State>,
|
||||
};
|
||||
}
|
||||
39
frontend/packages/arch/bot-hooks-base/src/use-responsive.ts
Normal file
39
frontend/packages/arch/bot-hooks-base/src/use-responsive.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 { useMediaQuery, ScreenRange } from '@coze-arch/responsive-kit';
|
||||
|
||||
import { useRouteConfig } from './use-route-config';
|
||||
|
||||
export const useIsResponsiveByRouteConfig = () => {
|
||||
const { responsive } = useRouteConfig();
|
||||
const shouldResponsive = responsive !== undefined;
|
||||
const { rangeMax, include = false } =
|
||||
responsive === true
|
||||
? { rangeMax: ScreenRange.LG, include: false }
|
||||
: responsive || {};
|
||||
const matches = useMediaQuery(
|
||||
include
|
||||
? {
|
||||
rangeMax,
|
||||
}
|
||||
: {
|
||||
rangeMin: rangeMax,
|
||||
},
|
||||
);
|
||||
const isResponsive = include ? matches : !matches;
|
||||
return shouldResponsive && isResponsive;
|
||||
};
|
||||
121
frontend/packages/arch/bot-hooks-base/src/use-route-config.ts
Normal file
121
frontend/packages/arch/bot-hooks-base/src/use-route-config.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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 { useMatches, type NavigateFunction } from 'react-router-dom';
|
||||
import { type FC, useMemo } from 'react';
|
||||
|
||||
import { type ScreenRange } from '@coze-arch/responsive-kit';
|
||||
|
||||
export interface TRouteConfigGlobal {
|
||||
/**
|
||||
* 展示小助手
|
||||
* @default true
|
||||
* @import 社区版不支持该字段
|
||||
*/
|
||||
showAssistant?: boolean;
|
||||
/**
|
||||
* 展示小助手引导提示
|
||||
* @default false
|
||||
* @import 社区版不支持该字段
|
||||
*/
|
||||
showAssistantGuideTip?: boolean;
|
||||
/**
|
||||
* 当企业ID发生变化时的回调函数。
|
||||
* @import 社区版不支持该字段
|
||||
* @param enterpriseId - 变化后的企业ID。
|
||||
* @param params - 包含导航函数和当前路径名的对象。
|
||||
*/
|
||||
onEnterpriseChange?: (
|
||||
enterpriseId: string,
|
||||
params: {
|
||||
navigate: NavigateFunction; // 导航函数,用于路由跳转。
|
||||
pathname: string; // 当前路径名,用于构建新的路径。
|
||||
},
|
||||
) => void;
|
||||
/**
|
||||
* 是否展示侧边栏
|
||||
* @default false
|
||||
*/
|
||||
hasSider?: boolean;
|
||||
/**
|
||||
* 展示移动端不适配提示文案
|
||||
* @default false
|
||||
*/
|
||||
showMobileTips?: boolean;
|
||||
/**
|
||||
* 是否需要身份验证
|
||||
* @default false
|
||||
*/
|
||||
requireAuth?: boolean;
|
||||
/**
|
||||
* 登录失效时的回退地址
|
||||
* @default /sign
|
||||
*/
|
||||
loginFallbackPath?: string;
|
||||
/**
|
||||
* @deprecated
|
||||
* 是否允许身份验证为可选
|
||||
* @default false
|
||||
*/
|
||||
requireAuthOptional?: boolean;
|
||||
/**
|
||||
* 设置为 true 时自动应用缺省值 { rangeMax: ScreenRange.LG, include: false } 对应之前绝大多数支持响应式路由的配置
|
||||
* @default false
|
||||
*/
|
||||
responsive?: { rangeMax: ScreenRange; include?: boolean } | true;
|
||||
/**
|
||||
* 子菜单组件
|
||||
* @default undefined
|
||||
*/
|
||||
subMenu?: FC<Record<string, never>>;
|
||||
/**
|
||||
* 一级导航菜单项 key
|
||||
* @default undefined
|
||||
*/
|
||||
menuKey?: string;
|
||||
/**
|
||||
* 二级导航菜单项 key
|
||||
* @default undefined
|
||||
*/
|
||||
subMenuKey?: string;
|
||||
/**
|
||||
* 控制是否根据 query 中的 page_mode 字段判断页面模式: 默认侧边导航模式 or 全屏popover模式
|
||||
* @default false
|
||||
*/
|
||||
pageModeByQuery?: boolean;
|
||||
}
|
||||
|
||||
export const useRouteConfig = <
|
||||
TConfig extends TRouteConfigGlobal = TRouteConfigGlobal,
|
||||
>(
|
||||
defaults?: TConfig,
|
||||
// 强制所有字段可能为空
|
||||
): Partial<TConfig> => {
|
||||
const matches = useMatches();
|
||||
|
||||
return useMemo<Partial<TConfig>>(
|
||||
() =>
|
||||
matches.reduce(
|
||||
(res, matchedRoute) => ({
|
||||
...res,
|
||||
...(matchedRoute.handle as Partial<TConfig>),
|
||||
...(matchedRoute.data as Partial<TConfig>),
|
||||
}),
|
||||
defaults ?? {},
|
||||
),
|
||||
[matches],
|
||||
);
|
||||
};
|
||||
60
frontend/packages/arch/bot-hooks-base/tsconfig.build.json
Normal file
60
frontend/packages/arch/bot-hooks-base/tsconfig.build.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"compilerOptions": {
|
||||
"types": [],
|
||||
"strictNullChecks": false,
|
||||
"noImplicitAny": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../agent-ide/tool-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../bot-api/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../bot-tea/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../bot-typings/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../bot-utils/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../common/assets/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../common/chat-area/chat-area/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../components/bot-semi/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"
|
||||
},
|
||||
{
|
||||
"path": "../responsive-kit/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../studio/stores/bot-detail/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../studio/user-store/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/arch/bot-hooks-base/tsconfig.json
Normal file
15
frontend/packages/arch/bot-hooks-base/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": ["**/*"]
|
||||
}
|
||||
24
frontend/packages/arch/bot-hooks-base/tsconfig.misc.json
Normal file
24
frontend/packages/arch/bot-hooks-base/tsconfig.misc.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"include": [
|
||||
"__tests__",
|
||||
"stories",
|
||||
"vitest.config.ts",
|
||||
"tailwind.config.ts",
|
||||
"setup"
|
||||
],
|
||||
"exclude": ["./dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"rootDir": "./",
|
||||
"outDir": "./dist",
|
||||
"types": ["vitest/globals"],
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true
|
||||
}
|
||||
}
|
||||
36
frontend/packages/arch/bot-hooks-base/vitest.config.ts
Normal file
36
frontend/packages/arch/bot-hooks-base/vitest.config.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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'],
|
||||
includeSource: ['./src'],
|
||||
coverage: {
|
||||
all: true,
|
||||
include: ['src'],
|
||||
exclude: ['src/index.ts', 'src/global.d.ts', 'src/page-jump/config.ts'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fixSemi: true,
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user