feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
170
frontend/packages/project-ide/framework/README.md
Normal file
170
frontend/packages/project-ide/framework/README.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# @coze-project-ide/framework
|
||||
|
||||
A ide features package for the Coze Studio monorepo
|
||||
|
||||
## Overview
|
||||
|
||||
This package is part of the Coze Studio monorepo and provides ide features functionality. It includes component, hook, adapter and more.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
Add this package to your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@coze-project-ide/framework": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
rush update
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
import { /* exported functions/components */ } from '@coze-project-ide/framework';
|
||||
|
||||
// Example usage
|
||||
// TODO: Add specific usage examples
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Component
|
||||
- Hook
|
||||
- Adapter
|
||||
- Store
|
||||
- Service
|
||||
- Manager
|
||||
- Modal
|
||||
- Plugin
|
||||
- Sdk
|
||||
|
||||
## API Reference
|
||||
|
||||
### Exports
|
||||
|
||||
- `IDEClient,
|
||||
ReactWidget,
|
||||
LayoutPanelType,
|
||||
URI,
|
||||
definePluginCreator,
|
||||
bindContributions,
|
||||
ViewContribution,
|
||||
LifecycleContribution,
|
||||
Emitter,
|
||||
Event,
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
useIDEService,
|
||||
useNavigation,
|
||||
LabelHandler,
|
||||
CommandContribution,
|
||||
ShortcutsContribution,
|
||||
OpenerService,
|
||||
useCurrentWidget,
|
||||
DISABLE_HANDLE_EVENT,
|
||||
ViewService,
|
||||
LayoutRestorer,
|
||||
ApplicationShell,
|
||||
WidgetManager,
|
||||
ViewRenderer,
|
||||
type PluginCreator,
|
||||
type ViewOptionRegisterService,
|
||||
type BoxPanel,
|
||||
ShortcutsService,
|
||||
CommandRegistry,
|
||||
useIDEContainer,
|
||||
TabBarToolbar,
|
||||
ContextKeyService,
|
||||
type ShortcutsRegistry,
|
||||
SplitWidget,
|
||||
Command,
|
||||
WindowService,
|
||||
type CustomTitleType,`
|
||||
- `useCommitVersion`
|
||||
- `useCurrentWidgetContext,
|
||||
useSpaceId,
|
||||
useProjectId,
|
||||
useProjectIDEServices,
|
||||
useActivateWidgetContext,
|
||||
useIDENavigate,
|
||||
useCurrentModeType,
|
||||
useSplitScreenArea,
|
||||
useTitle,
|
||||
useIDELocation,
|
||||
useIDEParams,
|
||||
useIDEServiceInBiz,
|
||||
useShortcuts,
|
||||
useListenMessageEvent,
|
||||
useWsListener,
|
||||
useSendMessageEvent,
|
||||
useViewService,
|
||||
useGetUIWidgetFromId,`
|
||||
- `IDEGlobalProvider, WidgetContext`
|
||||
- `UI_BUILDER_URI,
|
||||
MAIN_PANEL_DEFAULT_URI,
|
||||
SIDEBAR_URI,
|
||||
URI_SCHEME,
|
||||
SIDEBAR_CONFIG_URI,
|
||||
CONVERSATION_URI,
|
||||
SECONDARY_SIDEBAR_URI,
|
||||
CustomCommand,`
|
||||
- `type TitlePropsType, WidgetRegistry`
|
||||
- `withLazyLoad,
|
||||
getResourceByPathname,
|
||||
getURIByResource,
|
||||
getResourceByURI,
|
||||
getURIPathByPathname,
|
||||
getURLByURI,
|
||||
getURIByPath,
|
||||
getPathnameByURI,
|
||||
compareURI,
|
||||
addPreservedSearchParams,`
|
||||
- `ProjectIDEServices`
|
||||
- `WidgetService`
|
||||
- `ProjectIDEClient,
|
||||
ResourceFolder,
|
||||
mapResourceTree,
|
||||
ResourceTypeEnum,
|
||||
BaseResourceContextMenuBtnType,
|
||||
type CommonRenderProps,
|
||||
type ResourceType,
|
||||
type ResourceMapType,
|
||||
type ResourceFolderRefType,
|
||||
type RightPanelConfigType,
|
||||
type ResourceFolderShortCutContextType,
|
||||
type ResourceFolderProps,
|
||||
type RenderMoreSuffixType,
|
||||
type CreateResourcePropType,
|
||||
RESOURCE_FOLDER_CONTEXT_KEY,
|
||||
ROOT_KEY,
|
||||
type IdType,`
|
||||
|
||||
*And more...*
|
||||
|
||||
For detailed API documentation, please refer to the TypeScript definitions.
|
||||
|
||||
## Development
|
||||
|
||||
This package is built with:
|
||||
|
||||
- TypeScript
|
||||
- Modern JavaScript
|
||||
|
||||
- ESLint for code quality
|
||||
|
||||
## Contributing
|
||||
|
||||
This package is part of the Coze Studio monorepo. Please follow the monorepo contribution guidelines.
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-project.schema.json",
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./lib-ts"]
|
||||
}
|
||||
]
|
||||
}
|
||||
14
frontend/packages/project-ide/framework/eslint.config.js
Normal file
14
frontend/packages/project-ide/framework/eslint.config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {
|
||||
'no-restricted-syntax': 'off',
|
||||
'@typescript-eslint/naming-convention': 'off',
|
||||
'@typescript-eslint/no-magic-numbers': 'off',
|
||||
'@coze-arch/no-batch-import-or-export': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
},
|
||||
});
|
||||
44
frontend/packages/project-ide/framework/package.json
Normal file
44
frontend/packages/project-ide/framework/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@coze-project-ide/framework",
|
||||
"version": "0.0.1",
|
||||
"author": "jiangxujin@bytedance.com",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache --quiet"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-arch/bot-api": "workspace:*",
|
||||
"@coze-arch/bot-flags": "workspace:*",
|
||||
"@coze-arch/bot-utils": "workspace:*",
|
||||
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
|
||||
"@coze-arch/i18n": "workspace:*",
|
||||
"@coze-project-ide/base-adapter": "workspace:*",
|
||||
"@coze-project-ide/base-interface": "workspace:*",
|
||||
"@coze-project-ide/client": "workspace:*",
|
||||
"ahooks": "^3.7.8",
|
||||
"inversify": "^6.0.1",
|
||||
"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/ts-config": "workspace:*",
|
||||
"@rsbuild/core": "1.1.13",
|
||||
"@types/lodash-es": "^4.17.10",
|
||||
"@types/node": "18.18.9",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"react-is": ">= 16.8.0",
|
||||
"react-router-dom": "^6.22.0",
|
||||
"styled-components": ">= 2",
|
||||
"typescript": "~5.8.2",
|
||||
"webpack": "~5.91.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { IDEClient, type IDEClientOptions } from '@coze-project-ide/client';
|
||||
|
||||
import { type ProjectIDEClientProps as PresetPluginOptions } from '../types';
|
||||
import {
|
||||
createPresetPlugin,
|
||||
createCloseConfirmPlugin,
|
||||
createContextMenuPlugin,
|
||||
} from '../plugins';
|
||||
|
||||
interface ProjectIDEClientProps {
|
||||
presetOptions: PresetPluginOptions;
|
||||
plugins?: IDEClientOptions['plugins'];
|
||||
}
|
||||
|
||||
export const ProjectIDEClient: React.FC<
|
||||
React.PropsWithChildren<ProjectIDEClientProps>
|
||||
> = ({ presetOptions, plugins, children }) => {
|
||||
const options = useCallback(() => {
|
||||
const temp: IDEClientOptions = {
|
||||
preferences: {
|
||||
defaultData: {
|
||||
theme: 'light',
|
||||
},
|
||||
},
|
||||
view: {
|
||||
restoreDisabled: true,
|
||||
widgetFactories: [],
|
||||
defaultLayoutData: {},
|
||||
widgetFallbackRender: presetOptions.view.widgetFallbackRender,
|
||||
},
|
||||
plugins: [
|
||||
createPresetPlugin(presetOptions),
|
||||
createCloseConfirmPlugin(),
|
||||
createContextMenuPlugin(),
|
||||
...(plugins || []),
|
||||
],
|
||||
};
|
||||
return temp;
|
||||
}, [presetOptions, plugins]);
|
||||
|
||||
return (
|
||||
<IDEClient
|
||||
options={options}
|
||||
// 兼容 mnt e2e 环境,在 e2e 环境下,高度会被坍缩成 0
|
||||
// 因此需要额外的样式兼容
|
||||
// className={(window as any)._mnt_e2e_testing_ ? 'e2e-flow-container' : ''}
|
||||
className="e2e-flow-container"
|
||||
>
|
||||
{children}
|
||||
</IDEClient>
|
||||
);
|
||||
};
|
||||
@@ -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 { ProjectIDEClient } from './ide-client';
|
||||
export * from './resource-folder';
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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 { ResourceTypeEnum } from './type';
|
||||
import { BaseResourceContextMenuBtnType } from './hooks/use-right-click-panel/constant';
|
||||
|
||||
const ROOT_KEY = '$-ROOT-$';
|
||||
|
||||
const RESOURCE_FOLDER_WRAPPER_CLASS = 'resource-list-right-click-wrapper';
|
||||
|
||||
const ROOT_NODE = {
|
||||
id: ROOT_KEY,
|
||||
type: ResourceTypeEnum.Folder,
|
||||
name: 'root',
|
||||
};
|
||||
|
||||
const ITEM_HEIGHT = 24;
|
||||
|
||||
const HALF_ICON_WIDTH = 5;
|
||||
|
||||
const TAB_SIZE = 8;
|
||||
|
||||
const MAX_DEEP = 5;
|
||||
|
||||
const RESOURCE_FOLDER_CONTEXT_KEY = 'resourceFolderContextKey';
|
||||
|
||||
/**
|
||||
* 乐观 ui 创建文件的 ID 默认前缀
|
||||
*/
|
||||
const OPTIMISM_ID_PREFIX = 'resource-folder-optimism-id-';
|
||||
|
||||
const MOUSEUP_IGNORE_CLASS_NAME = 'mouseup-ignore-class-name';
|
||||
|
||||
const MORE_TOOLS_CLASS_NAME = 'more-tools-class-name';
|
||||
|
||||
enum ItemStatus {
|
||||
Normal = 'normal',
|
||||
Disabled = 'disabled', // 禁止操作
|
||||
}
|
||||
|
||||
const COLOR_CONFIG = {
|
||||
selectedItemBgColor: 'rgba(6, 7, 9, 0.14)',
|
||||
tempSelectedItemBgColor: 'rgba(6, 7, 9, 0.04)',
|
||||
errorItemBgColor: 'rgba(255, 241, 242, 1)',
|
||||
|
||||
dragFolderHoverBgColor: 'rgba(148, 152, 247, 0.44)',
|
||||
|
||||
textErrorColor: 'rgba(var(--blockwise-error-color))',
|
||||
textWarningColor: 'rgba(var(--blockwise-warning-color))',
|
||||
textSelectedColor: 'rgba(6, 7, 9, 0.96)',
|
||||
textNormalColor: 'rgba(6, 7, 9, 0.5)',
|
||||
};
|
||||
|
||||
export {
|
||||
ROOT_KEY,
|
||||
ITEM_HEIGHT,
|
||||
HALF_ICON_WIDTH,
|
||||
TAB_SIZE,
|
||||
MAX_DEEP,
|
||||
ROOT_NODE,
|
||||
ItemStatus,
|
||||
COLOR_CONFIG,
|
||||
OPTIMISM_ID_PREFIX,
|
||||
BaseResourceContextMenuBtnType,
|
||||
RESOURCE_FOLDER_WRAPPER_CLASS,
|
||||
MOUSEUP_IGNORE_CLASS_NAME,
|
||||
RESOURCE_FOLDER_CONTEXT_KEY,
|
||||
MORE_TOOLS_CLASS_NAME,
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import { useStateRef } from './uss-state-ref';
|
||||
|
||||
const useCollapsedMap = ({ _collapsedMap, _setCollapsedMap, resourceMap }) => {
|
||||
const [collapsedMapRef, setCollapsedMap, collapsedState] = useStateRef(
|
||||
_collapsedMap || {},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (_collapsedMap) {
|
||||
setCollapsedMap(_collapsedMap);
|
||||
}
|
||||
}, [_collapsedMap]);
|
||||
|
||||
const setCollapsed = v => {
|
||||
_setCollapsedMap?.(v);
|
||||
if (!_collapsedMap) {
|
||||
setCollapsedMap(v);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCollapse = (id, v) => {
|
||||
if (resourceMap.current?.[id]?.type === 'folder') {
|
||||
setCollapsed({
|
||||
...collapsedMapRef.current,
|
||||
[id]: v,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return { handleCollapse, collapsedMapRef, setCollapsed, collapsedState };
|
||||
};
|
||||
|
||||
export { useCollapsedMap };
|
||||
@@ -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 { useRef } from 'react';
|
||||
|
||||
import { ContextKeyService, useIDEService } from '@coze-project-ide/client';
|
||||
|
||||
import { type ResourceFolderContextType } from '../type';
|
||||
import { RESOURCE_FOLDER_CONTEXT_KEY } from '../constant';
|
||||
|
||||
const useContextChange = (id: string) => {
|
||||
const contextRef = useRef<Partial<ResourceFolderContextType>>({
|
||||
id,
|
||||
});
|
||||
|
||||
const contextService = useIDEService<ContextKeyService>(ContextKeyService);
|
||||
|
||||
const setContext = dispatch => {
|
||||
contextService.setContext(RESOURCE_FOLDER_CONTEXT_KEY, dispatch);
|
||||
};
|
||||
|
||||
const getContext = (): Partial<ResourceFolderContextType> =>
|
||||
contextService.getContext(RESOURCE_FOLDER_CONTEXT_KEY);
|
||||
|
||||
const updateContext = (other: Partial<ResourceFolderContextType>) => {
|
||||
if (getContext()?.id === id) {
|
||||
contextRef.current = {
|
||||
...contextRef.current,
|
||||
...other,
|
||||
};
|
||||
setContext(contextRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const updateId = () => {
|
||||
setContext(contextRef.current);
|
||||
};
|
||||
|
||||
const clearContext = () => {
|
||||
if (getContext()?.id === id) {
|
||||
contextService.setContext(RESOURCE_FOLDER_CONTEXT_KEY, undefined);
|
||||
}
|
||||
contextRef.current = {
|
||||
id,
|
||||
};
|
||||
};
|
||||
|
||||
return { updateContext, clearContext, updateId };
|
||||
};
|
||||
|
||||
export { useContextChange };
|
||||
@@ -0,0 +1,401 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable complexity */
|
||||
/* eslint-disable max-lines-per-function */
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
|
||||
import type React from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Toast } from '@coze-arch/coze-design';
|
||||
|
||||
import { baseValidateNames, getCreateResourceIndex } from '../../utils';
|
||||
import {
|
||||
type EditItemType,
|
||||
type ChangeNameType,
|
||||
type ResourceMapType,
|
||||
type ItemType,
|
||||
type IdType,
|
||||
type CreateResourcePropType,
|
||||
type ResourceType,
|
||||
ResourceTypeEnum,
|
||||
type ValidatorConfigType,
|
||||
type ConfigType,
|
||||
} from '../../type';
|
||||
import {
|
||||
ItemStatus,
|
||||
MAX_DEEP,
|
||||
BaseResourceContextMenuBtnType,
|
||||
ROOT_KEY,
|
||||
} from '../../constant';
|
||||
import { useCustomValidator } from './use-custom-validator';
|
||||
|
||||
const CREATE_RESOURCE_ID = '-1';
|
||||
|
||||
const useCreateEditResource = ({
|
||||
folderEnable,
|
||||
defaultResourceType,
|
||||
tempSelectedMapRef,
|
||||
registerEvent,
|
||||
setCollapsedMap,
|
||||
onChangeName,
|
||||
onCreate,
|
||||
resourceMap,
|
||||
selectedIdRef,
|
||||
resourceTreeRef,
|
||||
onDelete,
|
||||
validateConfig,
|
||||
config,
|
||||
resourceList,
|
||||
}: {
|
||||
folderEnable?: boolean;
|
||||
defaultResourceType?: string;
|
||||
registerEvent: (key: BaseResourceContextMenuBtnType, fn: (e) => void) => void;
|
||||
setCollapsedMap: (id: IdType, v: boolean) => void;
|
||||
resourceTreeRef: React.MutableRefObject<ResourceType>;
|
||||
tempSelectedMapRef: React.MutableRefObject<Record<string, ResourceType>>;
|
||||
disabled: React.MutableRefObject<boolean>;
|
||||
isFocusRef: React.MutableRefObject<boolean>;
|
||||
onChangeName?: (v: ChangeNameType) => void;
|
||||
resourceMap: React.MutableRefObject<ResourceMapType>;
|
||||
onCreate?: (v: CreateResourcePropType) => void;
|
||||
onDelete?: (v: ResourceType[]) => void;
|
||||
selectedIdRef: React.MutableRefObject<string>;
|
||||
validateConfig?: ValidatorConfigType;
|
||||
config?: ConfigType;
|
||||
resourceList: React.MutableRefObject<ResourceType[]>;
|
||||
}): {
|
||||
context: EditItemType;
|
||||
onCreateResource: (type: ItemType) => void;
|
||||
isInEditModeRef: React.MutableRefObject<boolean>;
|
||||
handleRenderList: (
|
||||
list: ResourceType[],
|
||||
createResourceInfo?: EditItemType['createResourceInfo'],
|
||||
) => (typeof CREATE_RESOURCE_ID | ResourceType)[];
|
||||
handleRename: (resourceId: IdType) => void;
|
||||
} => {
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
const errorMsgRef = useRef('');
|
||||
|
||||
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||
const isInEditModeRef = useRef(false);
|
||||
const setInEditMode = (v: boolean) => {
|
||||
isInEditModeRef.current = v;
|
||||
setIsInEditMode(v);
|
||||
};
|
||||
|
||||
const [editResource, setEditResource] = useState<ResourceType | null>(null);
|
||||
const editResourceRef = useRef<ResourceType | null>(null);
|
||||
|
||||
const [createResourceInfo, setCreateResourceInfo] = useState<{
|
||||
/**
|
||||
* 用于定位渲染位置的 index
|
||||
* 资源在文件夹后面,所有资源前面
|
||||
* 文件夹在当前文件夹下最前面
|
||||
*/
|
||||
index: number;
|
||||
parentId: IdType;
|
||||
type: ItemType;
|
||||
} | null>(null);
|
||||
const createResourceInfoRef = useRef<{
|
||||
parentId: IdType;
|
||||
type: ItemType;
|
||||
} | null>(null);
|
||||
|
||||
const preName = useRef<null | string>(null);
|
||||
const nextName = useRef<null | string>(null);
|
||||
|
||||
const reset = () => {
|
||||
setEditResource(null);
|
||||
editResourceRef.current = null;
|
||||
|
||||
setCreateResourceInfo(null);
|
||||
createResourceInfoRef.current = null;
|
||||
|
||||
setInEditMode(false);
|
||||
setErrorMsg('');
|
||||
errorMsgRef.current = '';
|
||||
preName.current = null;
|
||||
nextName.current = null;
|
||||
};
|
||||
|
||||
const handleRename = (resourceId: IdType) => {
|
||||
const target = resourceMap.current[resourceId];
|
||||
if (target && target.status !== ItemStatus.Disabled) {
|
||||
setEditResource(target);
|
||||
editResourceRef.current = target;
|
||||
setInEditMode(true);
|
||||
preName.current = target.name;
|
||||
nextName.current = target.name;
|
||||
}
|
||||
};
|
||||
|
||||
const onEditName = () => {
|
||||
const v = tempSelectedMapRef.current;
|
||||
if (Object.keys(v).filter(key => key !== ROOT_KEY).length === 1) {
|
||||
const tempSelected = Object.values(v)[0];
|
||||
handleRename(tempSelected.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = (_nextValue?: string) => {
|
||||
const nextValue =
|
||||
_nextValue === undefined ? nextName.current || '' : _nextValue;
|
||||
if (editResourceRef.current) {
|
||||
if (editResourceRef.current.id === CREATE_RESOURCE_ID) {
|
||||
if (nextValue !== '' && createResourceInfoRef.current?.parentId) {
|
||||
onCreate?.({
|
||||
parentId: createResourceInfoRef.current?.parentId,
|
||||
name: nextValue,
|
||||
type: createResourceInfoRef.current?.type,
|
||||
path:
|
||||
resourceMap.current?.[createResourceInfoRef.current?.parentId]
|
||||
?.path || [],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
nextValue !== '' &&
|
||||
nextValue !== preName.current &&
|
||||
editResourceRef.current.id
|
||||
) {
|
||||
onChangeName?.({
|
||||
id: editResourceRef.current.id,
|
||||
name: nextValue,
|
||||
type: editResourceRef.current.type,
|
||||
path: resourceMap.current[editResourceRef.current.id].path,
|
||||
resource: resourceMap.current[
|
||||
editResourceRef.current.id
|
||||
] as ResourceType,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
reset();
|
||||
};
|
||||
|
||||
const updateErrorMsg = (error: string) => {
|
||||
if (error) {
|
||||
setErrorMsg(error);
|
||||
errorMsgRef.current = error;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同名检测
|
||||
*/
|
||||
// if (editResourceRef.current) {
|
||||
// const parentFolder = createResourceInfoRef.current
|
||||
// ? getResourceById(
|
||||
// resourceTreeRef.current,
|
||||
// createResourceInfoRef.current.parentId,
|
||||
// )?.resource
|
||||
// : getParentResource(resourceTreeRef.current, editResourceRef.current);
|
||||
|
||||
// if (parentFolder) {
|
||||
// const sameNameValidate = validateSameNameInFolder({
|
||||
// folder: parentFolder,
|
||||
// editResource: {
|
||||
// ...editResourceRef.current,
|
||||
// name: v,
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (sameNameValidate) {
|
||||
// setErrorMsg(sameNameValidate);
|
||||
// errorMsgRef.current = sameNameValidate;
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
setErrorMsg('');
|
||||
errorMsgRef.current = '';
|
||||
};
|
||||
|
||||
const { validateAndUpdate } = useCustomValidator({
|
||||
validator: validateConfig?.customValidator || (baseValidateNames as any),
|
||||
callback: updateErrorMsg,
|
||||
});
|
||||
|
||||
const handleChangeName = (v: null | string) => {
|
||||
nextName.current = v;
|
||||
|
||||
if (createResourceInfoRef?.current) {
|
||||
/**
|
||||
* 新建资源
|
||||
*/
|
||||
const parentPath =
|
||||
resourceMap.current[createResourceInfoRef?.current?.parentId]?.path;
|
||||
|
||||
validateAndUpdate({
|
||||
type: 'create',
|
||||
label: v || '',
|
||||
parentPath: parentPath || [],
|
||||
resourceTree: resourceTreeRef.current,
|
||||
id: CREATE_RESOURCE_ID,
|
||||
});
|
||||
} else if (editResourceRef?.current) {
|
||||
/**
|
||||
* 编辑资源
|
||||
*/
|
||||
const path = editResourceRef?.current?.path || [];
|
||||
const parentPath = path.slice(0, path?.length - 1);
|
||||
|
||||
validateAndUpdate({
|
||||
type: 'edit',
|
||||
label: v || '',
|
||||
parentPath: parentPath || [],
|
||||
resourceTree: resourceTreeRef.current,
|
||||
id: editResourceRef.current.id,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onCreateResource = async (_type: ItemType) => {
|
||||
const type = _type || defaultResourceType;
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!folderEnable && type === ResourceTypeEnum.Folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
let selectResource = Object.values(tempSelectedMapRef.current || {})[0];
|
||||
|
||||
if (!selectResource) {
|
||||
selectResource = resourceMap.current[selectedIdRef.current];
|
||||
}
|
||||
|
||||
if (!selectResource) {
|
||||
selectResource = resourceMap.current[ROOT_KEY];
|
||||
}
|
||||
|
||||
if (selectResource.type !== 'folder' && selectResource?.path) {
|
||||
// 如果不是 folder 则选中父亲 folder
|
||||
selectResource =
|
||||
resourceMap.current[
|
||||
selectResource.path[selectResource.path.length - 2]
|
||||
];
|
||||
}
|
||||
|
||||
if (
|
||||
(selectResource?.path?.length || 0) +
|
||||
(type === ResourceTypeEnum.Folder ? 1 : 0) >
|
||||
(config?.maxDeep || MAX_DEEP)
|
||||
) {
|
||||
Toast.warning(`Can't create ${type}. MaxDeep is 5`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectResource) {
|
||||
let parentId = selectResource.id;
|
||||
if (selectResource.type !== 'folder') {
|
||||
parentId = selectResource.path?.[selectResource.path?.length - 2] || '';
|
||||
}
|
||||
|
||||
preName.current = '';
|
||||
nextName.current = '';
|
||||
|
||||
setCollapsedMap(parentId, false);
|
||||
|
||||
await new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(null);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
editResourceRef.current = {
|
||||
id: CREATE_RESOURCE_ID,
|
||||
name: '',
|
||||
type,
|
||||
};
|
||||
setEditResource(editResourceRef.current);
|
||||
createResourceInfoRef.current = {
|
||||
parentId,
|
||||
type,
|
||||
};
|
||||
const index = getCreateResourceIndex({
|
||||
resourceList: resourceList.current,
|
||||
parentId,
|
||||
type,
|
||||
});
|
||||
setCreateResourceInfo({
|
||||
parentId,
|
||||
type,
|
||||
index,
|
||||
});
|
||||
setInEditMode(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
registerEvent(BaseResourceContextMenuBtnType.EditName, () => {
|
||||
if (!editResourceRef.current) {
|
||||
onEditName();
|
||||
}
|
||||
});
|
||||
registerEvent(BaseResourceContextMenuBtnType.Delete, () => {
|
||||
const selectedResource = Object.values(tempSelectedMapRef.current).filter(
|
||||
v => v.id !== ROOT_KEY,
|
||||
);
|
||||
if (selectedResource.length > 0) {
|
||||
onDelete?.(selectedResource);
|
||||
}
|
||||
});
|
||||
registerEvent(BaseResourceContextMenuBtnType.CreateFolder, () => {
|
||||
onCreateResource(ResourceTypeEnum.Folder);
|
||||
});
|
||||
registerEvent(BaseResourceContextMenuBtnType.CreateResource, type => {
|
||||
onCreateResource(type);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleRenderList = (
|
||||
list: ResourceType[],
|
||||
info?: EditItemType['createResourceInfo'],
|
||||
) => {
|
||||
if (!info) {
|
||||
return list as (typeof CREATE_RESOURCE_ID | ResourceType)[];
|
||||
}
|
||||
const { index } = info;
|
||||
return [
|
||||
...list.slice(0, index),
|
||||
CREATE_RESOURCE_ID,
|
||||
...list.slice(index, list.length),
|
||||
] as (typeof CREATE_RESOURCE_ID | ResourceType)[];
|
||||
};
|
||||
|
||||
return {
|
||||
context: {
|
||||
isInEditMode,
|
||||
editResourceId: editResource?.id,
|
||||
createResourceInfo,
|
||||
handleChangeName,
|
||||
errorMsg,
|
||||
errorMsgRef,
|
||||
handleSave,
|
||||
},
|
||||
onCreateResource,
|
||||
isInEditModeRef,
|
||||
handleRenderList,
|
||||
handleRename,
|
||||
};
|
||||
};
|
||||
|
||||
export { useCreateEditResource, CREATE_RESOURCE_ID };
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
class PromiseController<T, O> {
|
||||
private lastVersion: number;
|
||||
private callbacks: Array<(v: O) => void> = [];
|
||||
private mainFunction: (v: T) => Promise<O>;
|
||||
|
||||
constructor() {
|
||||
this.lastVersion = 0;
|
||||
}
|
||||
|
||||
registerPromiseFn(fn: (v: T) => Promise<O>) {
|
||||
this.mainFunction = fn;
|
||||
return this;
|
||||
}
|
||||
|
||||
registerCallbackFb(cb: (v: O) => void) {
|
||||
this.callbacks.push(cb);
|
||||
return this;
|
||||
}
|
||||
|
||||
async excute(v: T) {
|
||||
if (!this.mainFunction) {
|
||||
return;
|
||||
}
|
||||
this.lastVersion += 1;
|
||||
const currentVersion = this.lastVersion;
|
||||
const res = await this.mainFunction(v);
|
||||
if (this.lastVersion === currentVersion) {
|
||||
this.callbacks.forEach(cb => cb(res));
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.callbacks = [];
|
||||
}
|
||||
}
|
||||
|
||||
export { PromiseController };
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 CustomValidatorPropsType } from '../../type';
|
||||
import { PromiseController } from './promise-controller';
|
||||
|
||||
const useCustomValidator = ({
|
||||
validator,
|
||||
callback,
|
||||
}: {
|
||||
validator: (data: CustomValidatorPropsType) => Promise<string>;
|
||||
callback: (label: string) => void;
|
||||
}) => {
|
||||
const promiseController = useRef(
|
||||
new PromiseController<CustomValidatorPropsType, string>(),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
promiseController.current
|
||||
.registerPromiseFn(validator)
|
||||
.registerCallbackFb(callback);
|
||||
return () => {
|
||||
promiseController.current?.dispose?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const validateAndUpdate = (data: CustomValidatorPropsType) => {
|
||||
promiseController.current?.excute(data);
|
||||
};
|
||||
|
||||
return {
|
||||
validateAndUpdate,
|
||||
};
|
||||
};
|
||||
|
||||
export { useCustomValidator };
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
enum EventKey {
|
||||
MouseDown = 'mouseDown',
|
||||
MouseDownInDiv = 'mouseDownInDiv',
|
||||
MouseUpInDiv = 'mouseUpInDiv',
|
||||
MouseUp = 'mouseUp',
|
||||
MouseMove = 'mouseMove',
|
||||
KeyDown = 'keyDown',
|
||||
}
|
||||
|
||||
const useEvent = () => {
|
||||
const mouseDownRef = useRef<Array<(e) => void>>([]);
|
||||
const mouseDownInDivRef = useRef<Array<(e) => void>>([]);
|
||||
const mouseUpInDivRef = useRef<Array<(e) => void>>([]);
|
||||
const mouseUpRef = useRef<Array<(e) => void>>([]);
|
||||
const mouseMoveRef = useRef<Array<(e) => void>>([]);
|
||||
const keyDownRef = useRef<Array<(e) => void>>([]);
|
||||
|
||||
const addEventListener = (key: EventKey, fn: (e) => void) => {
|
||||
if (key === EventKey.KeyDown) {
|
||||
keyDownRef.current.push(fn);
|
||||
} else if (key === EventKey.MouseDownInDiv) {
|
||||
mouseDownInDivRef.current.push(fn);
|
||||
} else if (key === EventKey.MouseUpInDiv) {
|
||||
mouseUpInDivRef.current.push(fn);
|
||||
} else if (key === EventKey.MouseDown) {
|
||||
mouseDownRef.current.push(fn);
|
||||
} else if (key === EventKey.MouseUp) {
|
||||
mouseUpRef.current.push(fn);
|
||||
} else if (key === EventKey.MouseMove) {
|
||||
mouseMoveRef.current.push(fn);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseDown = e => {
|
||||
mouseDownRef.current.forEach(fn => {
|
||||
fn(e);
|
||||
});
|
||||
};
|
||||
const onMouseDownInDiv = e => {
|
||||
mouseDownInDivRef.current.forEach(fn => {
|
||||
fn(e);
|
||||
});
|
||||
};
|
||||
const onMouseUpInDiv = e => {
|
||||
mouseUpInDivRef.current.forEach(fn => {
|
||||
fn(e);
|
||||
});
|
||||
};
|
||||
const onMouseUp = e => {
|
||||
mouseUpRef.current.forEach(fn => {
|
||||
fn(e);
|
||||
});
|
||||
};
|
||||
const onKeyDown = e => {
|
||||
keyDownRef.current.forEach(fn => {
|
||||
fn(e);
|
||||
});
|
||||
};
|
||||
const onMouseMove = e => {
|
||||
mouseMoveRef.current.forEach(fn => {
|
||||
fn(e);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('mousedown', onMouseDown);
|
||||
window.addEventListener('mouseup', onMouseUp);
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
return () => {
|
||||
window.removeEventListener('mousedown', onMouseDown);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const customEventBox = ({ children }) => (
|
||||
<div
|
||||
className={'resource-list-custom-event-wrapper'}
|
||||
onMouseDownCapture={onMouseDownInDiv}
|
||||
onMouseUp={onMouseUpInDiv}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
return { addEventListener, customEventBox, onMouseDownInDiv, onMouseUpInDiv };
|
||||
};
|
||||
|
||||
export { useEvent, EventKey };
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
import { calcOffsetTopByCollapsedMap } from '../../utils';
|
||||
import {
|
||||
type ResourceType,
|
||||
type ConfigType,
|
||||
type ResourceMapType,
|
||||
} from '../../type';
|
||||
import { ITEM_HEIGHT } from '../../constant';
|
||||
|
||||
const useFocusResource = ({
|
||||
resourceTreeRef,
|
||||
collapsedMapRef,
|
||||
resourceMap,
|
||||
config,
|
||||
}: {
|
||||
config?: ConfigType;
|
||||
resourceTreeRef: React.MutableRefObject<ResourceType>;
|
||||
collapsedMapRef: React.MutableRefObject<Record<string, boolean>>;
|
||||
resourceMap: React.MutableRefObject<ResourceMapType>;
|
||||
}) => {
|
||||
const scrollWrapper = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollEnable = useRef(true);
|
||||
|
||||
const scrollInView = (selectedId: string) => {
|
||||
if (
|
||||
!scrollWrapper.current ||
|
||||
!scrollEnable.current ||
|
||||
!selectedId ||
|
||||
!resourceTreeRef.current ||
|
||||
!resourceMap?.current?.[selectedId]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollTop = calcOffsetTopByCollapsedMap({
|
||||
selectedId,
|
||||
resourceTree: resourceTreeRef.current,
|
||||
collapsedMap: collapsedMapRef.current,
|
||||
itemHeight: config?.itemHeight || ITEM_HEIGHT,
|
||||
});
|
||||
|
||||
// 如果在视图内, 则不滚
|
||||
if (
|
||||
scrollTop > scrollWrapper.current.scrollTop &&
|
||||
scrollTop <
|
||||
scrollWrapper.current.offsetHeight + scrollWrapper.current.scrollTop
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollWrapper.current.scrollTo({
|
||||
top: scrollTop,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
|
||||
const tempDisableScroll = (t?: number) => {
|
||||
scrollEnable.current = false;
|
||||
|
||||
setTimeout(() => {
|
||||
scrollEnable.current = true;
|
||||
}, t || 300);
|
||||
};
|
||||
|
||||
return {
|
||||
scrollInView,
|
||||
scrollWrapper,
|
||||
tempDisableScroll,
|
||||
};
|
||||
};
|
||||
export { useFocusResource };
|
||||
@@ -0,0 +1,584 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable complexity */
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
/* eslint-disable max-lines-per-function */
|
||||
import type React from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { DragService, type URI, useIDEService } from '@coze-project-ide/client';
|
||||
|
||||
import { EventKey } from '../use-custom-event';
|
||||
import { flatTree, getResourceListFromIdToId } from '../../utils';
|
||||
import {
|
||||
type DragAndDropType,
|
||||
type ResourceType,
|
||||
type DragPropType,
|
||||
type ResourceMapType,
|
||||
type IdType,
|
||||
type CommonRenderProps,
|
||||
ResourceTypeEnum,
|
||||
type ConfigType,
|
||||
} from '../../type';
|
||||
import { MORE_TOOLS_CLASS_NAME, ROOT_KEY, ROOT_NODE } from '../../constant';
|
||||
import {
|
||||
CLICK_OUTSIDE,
|
||||
CLICK_TOOL_BAR,
|
||||
type MousePosition,
|
||||
DATASET_PARENT_DATA_KEY_ID,
|
||||
findTargetElement,
|
||||
canStartDrag,
|
||||
getFolderIdFromPath,
|
||||
CLICK_CONTEXT_MENU,
|
||||
validateDrag,
|
||||
getElementByXY,
|
||||
DATASET_RESOURCE_FOLDER_KEY,
|
||||
} from './utils';
|
||||
import { useDragUI } from './use-drag-ui';
|
||||
|
||||
const useMouseEvent = ({
|
||||
draggable,
|
||||
uniqId,
|
||||
updateId,
|
||||
setTempSelectedMap,
|
||||
collapsedMapRef,
|
||||
setCollapsedMap,
|
||||
resourceTreeRef,
|
||||
onDrag,
|
||||
disabled,
|
||||
resourceMap,
|
||||
addEventListener,
|
||||
selectedIdRef,
|
||||
onSelected,
|
||||
tempSelectedMapRef,
|
||||
resourceTreeWrapperRef,
|
||||
iconRender,
|
||||
config,
|
||||
}: {
|
||||
draggable: boolean; // 是否可以拖拽
|
||||
uniqId: string;
|
||||
updateId: () => void;
|
||||
resourceTreeWrapperRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||
setTempSelectedMap: (v: Record<string, ResourceType>) => void;
|
||||
collapsedMapRef: React.MutableRefObject<Record<string, boolean>>;
|
||||
setCollapsedMap: (id: IdType, v: boolean) => void;
|
||||
resourceTreeRef: React.MutableRefObject<ResourceType>;
|
||||
tempSelectedMapRef: React.MutableRefObject<Record<string, ResourceType>>;
|
||||
disabled?: React.MutableRefObject<boolean>;
|
||||
selectedIdRef?: React.MutableRefObject<string>;
|
||||
resourceMap: React.MutableRefObject<ResourceMapType>;
|
||||
onDrag: (v: DragPropType) => void;
|
||||
onSelected?: (id: string | number, resource: ResourceType) => void;
|
||||
addEventListener: (key: EventKey, fn: (e) => void) => void;
|
||||
iconRender?: (v: CommonRenderProps) => React.ReactElement | undefined;
|
||||
config?: ConfigType;
|
||||
}): {
|
||||
context: DragAndDropType;
|
||||
isFocusRef: React.MutableRefObject<boolean>;
|
||||
dragPreview: React.ReactElement;
|
||||
onMouseMove;
|
||||
handleFocus: () => void;
|
||||
handleBlur: () => void;
|
||||
} => {
|
||||
const isMouseDownRef = useRef<MousePosition | null>(null);
|
||||
const [isFocus, setIsFocus] = useState(false);
|
||||
const isFocusRef = useRef(false);
|
||||
const dragService = useIDEService<DragService>(DragService);
|
||||
/**
|
||||
* 存储拖拽过程中是否合法的字段
|
||||
*/
|
||||
const [draggingError, setDraggingError] = useState<string>('');
|
||||
|
||||
const multiModeFirstSelected = useRef<ResourceType | null>(null);
|
||||
|
||||
const { isDragging, isDraggingRef, handleDrag, dragPreview } = useDragUI({
|
||||
iconRender,
|
||||
selectedMap: tempSelectedMapRef.current,
|
||||
addEventListener,
|
||||
config,
|
||||
});
|
||||
|
||||
/**
|
||||
* 用于开启拖拽到 mainPanel 下打开资源的方法
|
||||
*/
|
||||
const dragAndOpenResource = e => {
|
||||
if (!config?.resourceUriHandler) {
|
||||
return;
|
||||
}
|
||||
const uris = Object.values(tempSelectedMapRef.current)
|
||||
.filter(resource => resource.type !== ResourceTypeEnum.Folder)
|
||||
.map(resource => config.resourceUriHandler?.(resource))
|
||||
.filter(Boolean);
|
||||
|
||||
if (!uris.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
dragService?.startDrag?.({
|
||||
uris: uris as URI[],
|
||||
position: {
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
},
|
||||
callback: v => {
|
||||
//
|
||||
},
|
||||
backdropTransform: {
|
||||
/**
|
||||
* 通过边缘检测的方式,阻止 lm-cursor-backdrop 元素进入树组件内
|
||||
*/
|
||||
clientX: (eventX: number) =>
|
||||
Math.max(
|
||||
eventX,
|
||||
(resourceTreeWrapperRef.current?.clientWidth || 0) + 100,
|
||||
),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const startDrag = e => {
|
||||
if (draggable) {
|
||||
dragAndOpenResource(e);
|
||||
setDraggingError('');
|
||||
handleDrag(true);
|
||||
}
|
||||
};
|
||||
|
||||
const stopDrag = () => {
|
||||
setDraggingError('');
|
||||
handleDrag(false);
|
||||
};
|
||||
|
||||
const changeTempSelectedMap = (v: Record<string, ResourceType>) => {
|
||||
const values = Object.values(v);
|
||||
if (values.length === 1) {
|
||||
multiModeFirstSelected.current = values[0];
|
||||
} else if (values.length === 0) {
|
||||
multiModeFirstSelected.current = null;
|
||||
}
|
||||
tempSelectedMapRef.current = v;
|
||||
setTempSelectedMap(v);
|
||||
};
|
||||
|
||||
const [currentHoverItem, setCurrentHoverItem] = useState<ResourceType | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
/**
|
||||
* 用于记录拖拽过程中,当前 hover 元素的 父元素 id。
|
||||
*/
|
||||
const hoverItemParentId = useRef<string | null>(null);
|
||||
/**
|
||||
* 用于记录拖拽过程中,当前 hover 元素的 父元素及其下钻所有节点的id表
|
||||
*/
|
||||
const [highlightItemMap, setHighlightItemMap] = useState<ResourceMapType>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (currentHoverItem?.id) {
|
||||
const parentId = getFolderIdFromPath(currentHoverItem);
|
||||
if (parentId !== hoverItemParentId.current) {
|
||||
const treeList = resourceMap.current[parentId]
|
||||
? flatTree(
|
||||
resourceMap.current[parentId],
|
||||
resourceMap.current,
|
||||
collapsedMapRef.current,
|
||||
)
|
||||
: [];
|
||||
|
||||
setHighlightItemMap(
|
||||
treeList.reduce(
|
||||
(pre, cur, index) => ({
|
||||
...pre,
|
||||
[cur.id]: {
|
||||
...cur,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
);
|
||||
|
||||
hoverItemParentId.current = parentId;
|
||||
}
|
||||
} else {
|
||||
hoverItemParentId.current = null;
|
||||
setHighlightItemMap({});
|
||||
}
|
||||
}, [currentHoverItem?.id]);
|
||||
|
||||
const handleFocus = () => {
|
||||
updateId();
|
||||
setIsFocus(true);
|
||||
isFocusRef.current = true;
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
changeTempSelectedMap({});
|
||||
setIsFocus(false);
|
||||
isFocusRef.current = false;
|
||||
};
|
||||
|
||||
const getCurrentDragResourceList = () => {
|
||||
let resourceList = Object.values(tempSelectedMapRef.current).filter(
|
||||
item => item.id !== ROOT_KEY,
|
||||
);
|
||||
|
||||
/**
|
||||
* 将文件夹下的文件给过滤掉,防止拖拽之后失去层级结构
|
||||
*/
|
||||
resourceList = resourceList.filter(resource => {
|
||||
const { type, path } = resourceMap.current[resource.id];
|
||||
if (type !== ResourceTypeEnum.Folder) {
|
||||
const resourcePath = path || [];
|
||||
return !(resourcePath || [])
|
||||
.slice(0, resourcePath.length - 1)
|
||||
.some(id => id !== ROOT_KEY && tempSelectedMapRef.current[id]);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return resourceList;
|
||||
};
|
||||
|
||||
const onMouseDownInDiv = e => {
|
||||
if (disabled?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = findTargetElement(e.target, uniqId);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleFocus();
|
||||
|
||||
if (typeof target === 'object' && e.button === 0) {
|
||||
isMouseDownRef.current = {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
};
|
||||
}
|
||||
|
||||
// 这里要选中一次是保证拖动前选中正确的 item
|
||||
if (typeof target === 'object' && !e.shiftKey && !e.metaKey) {
|
||||
let currentSelected: ResourceType | null = null;
|
||||
|
||||
if (target.id === ROOT_KEY) {
|
||||
currentSelected = ROOT_NODE;
|
||||
} else {
|
||||
currentSelected = resourceMap?.current?.[String(target?.id)] || {};
|
||||
}
|
||||
|
||||
if (
|
||||
currentSelected?.id &&
|
||||
!tempSelectedMapRef.current?.[currentSelected.id]
|
||||
) {
|
||||
changeTempSelectedMap({ [currentSelected.id]: currentSelected });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseDown = e => {
|
||||
if (disabled?.current || !isFocusRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = findTargetElement(e.target, uniqId);
|
||||
|
||||
stopDrag();
|
||||
|
||||
if (!target || target === CLICK_OUTSIDE) {
|
||||
handleBlur();
|
||||
return;
|
||||
}
|
||||
|
||||
// 点到右键的 panel 中 啥也别操作
|
||||
if (target === CLICK_CONTEXT_MENU) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUpInDiv = e => {
|
||||
if (disabled?.current || isDraggingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = findTargetElement(e.target, uniqId, MORE_TOOLS_CLASS_NAME);
|
||||
|
||||
if (typeof target === 'object' && target !== null) {
|
||||
let currentSelected: ResourceType | null = null;
|
||||
|
||||
if (target.id === ROOT_KEY) {
|
||||
currentSelected = ROOT_NODE;
|
||||
} else {
|
||||
currentSelected = resourceMap?.current?.[String(target?.id)] || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果点击了 more tools 三颗点,那需要先将临时选中表重置为只选中该 item。 用于右键菜单的消费。
|
||||
*/
|
||||
if (target.customTag === MORE_TOOLS_CLASS_NAME) {
|
||||
const nextSelected = { [currentSelected.id]: currentSelected };
|
||||
changeTempSelectedMap(nextSelected);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果右键, 并且点击在已经被选中的资源上,则不再做操作,因为要弹操作栏
|
||||
if (
|
||||
e.ctrlKey ||
|
||||
(e.button === 2 &&
|
||||
currentSelected?.id &&
|
||||
tempSelectedMapRef.current?.[currentSelected.id])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.shiftKey) {
|
||||
const firstSelectedId =
|
||||
multiModeFirstSelected.current?.id || selectedIdRef?.current;
|
||||
if (
|
||||
!firstSelectedId ||
|
||||
!currentSelected?.id ||
|
||||
firstSelectedId === currentSelected?.id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 批量一下子多选
|
||||
let nextSelected: any = getResourceListFromIdToId({
|
||||
resourceTree: resourceTreeRef.current,
|
||||
from: firstSelectedId,
|
||||
to: currentSelected.id,
|
||||
options: { collapsedMap: collapsedMapRef.current },
|
||||
});
|
||||
|
||||
nextSelected = (nextSelected || []).reduce((prev, next) => {
|
||||
const id = typeof next === 'object' ? String(next.id) : String(next);
|
||||
return {
|
||||
...prev,
|
||||
[id]: resourceMap?.current[id],
|
||||
} as any;
|
||||
}, {});
|
||||
|
||||
if (nextSelected[ROOT_KEY]) {
|
||||
delete nextSelected[ROOT_KEY];
|
||||
}
|
||||
changeTempSelectedMap(nextSelected);
|
||||
} else if (e.metaKey) {
|
||||
// 挑着多选
|
||||
let nextSelected = { ...tempSelectedMapRef.current };
|
||||
if (currentSelected?.id) {
|
||||
if (nextSelected[currentSelected.id]) {
|
||||
delete nextSelected[currentSelected.id];
|
||||
} else {
|
||||
nextSelected = {
|
||||
...nextSelected,
|
||||
[currentSelected.id]: currentSelected,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (nextSelected[ROOT_KEY]) {
|
||||
delete nextSelected[ROOT_KEY];
|
||||
}
|
||||
changeTempSelectedMap(nextSelected);
|
||||
} else {
|
||||
if (currentSelected?.type && currentSelected.type !== 'folder') {
|
||||
onSelected?.(currentSelected.id, currentSelected as any);
|
||||
}
|
||||
if (currentSelected?.id) {
|
||||
changeTempSelectedMap({ [currentSelected.id]: currentSelected });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!isDraggingRef.current ||
|
||||
!Object.keys(tempSelectedMapRef.current).length
|
||||
) {
|
||||
if (
|
||||
typeof target === 'object' &&
|
||||
target !== null &&
|
||||
target.id !== ROOT_KEY
|
||||
) {
|
||||
if (e.button === 0 && !e.shiftKey && !e.metaKey) {
|
||||
setCollapsedMap(target.id, !collapsedMapRef.current[target.id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseMove = e => {
|
||||
if (disabled?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isMouseDownRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectMapValue = Object.values(tempSelectedMapRef.current);
|
||||
|
||||
if (selectMapValue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectMapValue.length === 1 && selectMapValue[0].id === ROOT_KEY) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isStartDrag =
|
||||
isDraggingRef.current ||
|
||||
canStartDrag(isMouseDownRef.current, {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
});
|
||||
|
||||
if (!isStartDrag) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始拖拽
|
||||
*/
|
||||
if (!isDraggingRef.current) {
|
||||
startDrag(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resourceTreeWrapperRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = getElementByXY({
|
||||
e,
|
||||
wrapperElm: resourceTreeWrapperRef.current,
|
||||
uniqId,
|
||||
});
|
||||
|
||||
// 点到右键的 panel 中 啥也别操作
|
||||
if (target === CLICK_CONTEXT_MENU) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
setCurrentHoverItem(null);
|
||||
return;
|
||||
}
|
||||
if (target === CLICK_OUTSIDE || target === CLICK_TOOL_BAR) {
|
||||
setCurrentHoverItem(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof target === 'object' && target?.id !== currentHoverItem?.id) {
|
||||
if (target?.id === ROOT_KEY) {
|
||||
setCurrentHoverItem(ROOT_NODE);
|
||||
} else {
|
||||
setCurrentHoverItem(resourceMap.current[String(target?.id)]);
|
||||
const toId = getFolderIdFromPath(resourceMap.current[target.id]);
|
||||
const resourceList = getCurrentDragResourceList();
|
||||
const error = validateDrag(
|
||||
resourceList,
|
||||
resourceMap.current[toId],
|
||||
config,
|
||||
);
|
||||
setDraggingError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = e => {
|
||||
if (disabled?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isDraggingRef.current &&
|
||||
Object.keys(tempSelectedMapRef.current).length &&
|
||||
resourceTreeWrapperRef.current
|
||||
) {
|
||||
const target = getElementByXY({
|
||||
e,
|
||||
wrapperElm: resourceTreeWrapperRef.current,
|
||||
uniqId,
|
||||
});
|
||||
|
||||
// 点到右键的 panel 中 啥也别操作
|
||||
if (target === CLICK_CONTEXT_MENU || !target) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof target === 'object') {
|
||||
const resourceList = getCurrentDragResourceList();
|
||||
|
||||
const toId = getFolderIdFromPath(resourceMap.current[target.id]);
|
||||
|
||||
const error = validateDrag(
|
||||
resourceList,
|
||||
resourceMap.current[toId],
|
||||
config,
|
||||
);
|
||||
|
||||
if (error) {
|
||||
onDrag?.({ errorMsg: error });
|
||||
} else {
|
||||
onDrag?.({
|
||||
resourceList,
|
||||
toId,
|
||||
});
|
||||
setCollapsedMap(toId, false);
|
||||
}
|
||||
}
|
||||
|
||||
stopDrag();
|
||||
}
|
||||
|
||||
isMouseDownRef.current = null;
|
||||
|
||||
setCurrentHoverItem(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
addEventListener(EventKey.MouseDown, onMouseDown);
|
||||
addEventListener(EventKey.MouseDownInDiv, onMouseDownInDiv);
|
||||
addEventListener(EventKey.MouseUpInDiv, onMouseUpInDiv);
|
||||
addEventListener(EventKey.MouseUp, onMouseUp);
|
||||
}, []);
|
||||
|
||||
const dataHandler = (resource: ResourceType) => ({
|
||||
[`data-${DATASET_PARENT_DATA_KEY_ID}`]: resource.id,
|
||||
[`data-${DATASET_RESOURCE_FOLDER_KEY}`]: uniqId,
|
||||
});
|
||||
|
||||
return {
|
||||
context: {
|
||||
isDragging,
|
||||
draggingError,
|
||||
isFocus,
|
||||
dataHandler,
|
||||
tempSelectedMapRef,
|
||||
currentHoverItem,
|
||||
highlightItemMap,
|
||||
},
|
||||
isFocusRef,
|
||||
onMouseMove,
|
||||
dragPreview,
|
||||
handleFocus,
|
||||
handleBlur,
|
||||
};
|
||||
};
|
||||
|
||||
export { useMouseEvent };
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { EventKey } from '../use-custom-event';
|
||||
import {
|
||||
type ConfigType,
|
||||
type CommonRenderProps,
|
||||
type ResourceType,
|
||||
} from '../../type';
|
||||
|
||||
export const useDragUI = ({
|
||||
iconRender,
|
||||
selectedMap,
|
||||
addEventListener,
|
||||
config,
|
||||
}: {
|
||||
iconRender?: (v: CommonRenderProps) => React.ReactElement | undefined;
|
||||
selectedMap: Record<string, ResourceType>;
|
||||
addEventListener: (key: EventKey, fn: (e) => void) => void;
|
||||
config?: ConfigType;
|
||||
}) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const isDraggingRef = useRef(false);
|
||||
|
||||
const [mousePosition, setMousePosition] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
} | null>(null);
|
||||
|
||||
const handleDrag = (v: boolean) => {
|
||||
isDraggingRef.current = v;
|
||||
setIsDragging(v);
|
||||
|
||||
setMousePosition(null);
|
||||
if (v) {
|
||||
document.body.style.cursor = 'grabbing';
|
||||
} else {
|
||||
document.body.style.cursor = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = e => {
|
||||
if (isDraggingRef.current) {
|
||||
setMousePosition({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
addEventListener(EventKey.MouseMove, handleMouseMove);
|
||||
}, []);
|
||||
|
||||
const dragPreview =
|
||||
mousePosition && !config?.dragUi?.disable ? (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
zIndex: 99999,
|
||||
top: 5,
|
||||
left: 5,
|
||||
display: isDragging && mousePosition?.x ? 'block' : 'none',
|
||||
transform: `translate(${mousePosition?.x || 0}px, ${
|
||||
mousePosition?.y || 0
|
||||
}px)`,
|
||||
userSelect: 'none',
|
||||
pointerEvents: 'none',
|
||||
backgroundColor: 'rgba(6, 7, 9, 0.08)',
|
||||
borderRadius: 6,
|
||||
padding: '2px 4px',
|
||||
minWidth: 20,
|
||||
minHeight: 20,
|
||||
...(config?.dragUi?.wrapperStyle || {}),
|
||||
}}
|
||||
className={config?.dragUi?.wrapperClassName || ''}
|
||||
>
|
||||
{Object.values(selectedMap).length > 1 ? (
|
||||
<>{Object.values(selectedMap).length}</>
|
||||
) : (
|
||||
Object.values(selectedMap).map(item => (
|
||||
<div
|
||||
key={item.name}
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
{item?.type ? (
|
||||
<span style={{ marginRight: 4 }}>
|
||||
{iconRender?.({
|
||||
resource: item,
|
||||
})}
|
||||
</span>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
return { handleDrag, isDragging, isDraggingRef, dragPreview };
|
||||
};
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable complexity */
|
||||
import { type ConfigType, type IdType, type ResourceType } from '../../type';
|
||||
import { MAX_DEEP, ROOT_KEY, ROOT_NODE } from '../../constant';
|
||||
|
||||
const DATASET_RESOURCE_FOLDER_KEY = 'resource_folder_key';
|
||||
const DATASET_PARENT_DATA_STOP_TAG = 'resource_folder_drag_and_drop_stop_tag';
|
||||
const DATASET_PARENT_DATA_KEY_ID = 'resource_folder_drag_and_drop_id';
|
||||
|
||||
const TOOL_BAR_CLASS_NAME = 'resource_folder_tool_bar_class_name';
|
||||
const CLICK_TOOL_BAR = 'click_tool_bar';
|
||||
|
||||
const CLICK_OUTSIDE = 'click_outside';
|
||||
|
||||
const CLICK_CONTEXT_MENU = 'click_context_menu';
|
||||
|
||||
const PATH_SPLIT_KEY = '-$$-';
|
||||
|
||||
const START_DRAG_GAP = 10;
|
||||
|
||||
export interface MousePosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
const isPointInRect = (
|
||||
point: { x: number; y: number },
|
||||
rect: { x: number; y: number; width: number; height: number },
|
||||
) =>
|
||||
point.x > rect.x &&
|
||||
point.x < rect.x + rect.width &&
|
||||
point.y > rect.y &&
|
||||
point.y < rect.y + rect.height;
|
||||
|
||||
const BORDER_GAP = 8; // 默认边缘阈值
|
||||
/**
|
||||
* 相对于 findTargetElement ,增加了边缘检测功能。
|
||||
* 当鼠标在 资源目录边缘, 会算作聚焦在 root 节点
|
||||
*/
|
||||
const getElementByXY = ({
|
||||
e,
|
||||
wrapperElm,
|
||||
uniqId,
|
||||
}: {
|
||||
e: MouseEvent;
|
||||
wrapperElm: HTMLDivElement;
|
||||
uniqId: string;
|
||||
}) => {
|
||||
const { pageX, pageY } = e;
|
||||
const { x, y, width, height } = wrapperElm.getBoundingClientRect();
|
||||
|
||||
if (
|
||||
isPointInRect({ x: pageX, y: pageY }, { x, y, width, height }) &&
|
||||
!isPointInRect(
|
||||
{ x: pageX, y: pageY },
|
||||
{
|
||||
x: x + BORDER_GAP,
|
||||
y: y + BORDER_GAP,
|
||||
width: width - BORDER_GAP * 2,
|
||||
height: height - BORDER_GAP * 2,
|
||||
},
|
||||
)
|
||||
) {
|
||||
return ROOT_NODE;
|
||||
}
|
||||
|
||||
return findTargetElement(e.target as HTMLElement, uniqId);
|
||||
};
|
||||
|
||||
type TargetType =
|
||||
| {
|
||||
id: IdType;
|
||||
customTag?: string;
|
||||
}
|
||||
| null
|
||||
| typeof CLICK_OUTSIDE
|
||||
| typeof CLICK_CONTEXT_MENU
|
||||
| typeof CLICK_TOOL_BAR;
|
||||
|
||||
const findTargetElement = (
|
||||
elm: HTMLElement | null,
|
||||
uniqueId: string,
|
||||
/**
|
||||
* 遇到该 className 会进行记录,并且返回 id 的时候会带上该 className
|
||||
*/
|
||||
customClassName?: string,
|
||||
): TargetType | string => {
|
||||
const extraProps: {
|
||||
customTag?: string;
|
||||
} = {};
|
||||
|
||||
if (customClassName && elm?.classList?.contains?.(customClassName)) {
|
||||
extraProps.customTag = customClassName;
|
||||
}
|
||||
if (!elm) {
|
||||
return CLICK_OUTSIDE;
|
||||
}
|
||||
|
||||
if (
|
||||
elm.dataset?.[DATASET_PARENT_DATA_KEY_ID] !== undefined &&
|
||||
elm.dataset?.[DATASET_RESOURCE_FOLDER_KEY] === uniqueId
|
||||
) {
|
||||
return {
|
||||
id: elm.dataset[DATASET_PARENT_DATA_KEY_ID],
|
||||
...extraProps,
|
||||
};
|
||||
} else if (
|
||||
elm.dataset?.[DATASET_PARENT_DATA_STOP_TAG] &&
|
||||
elm.dataset[DATASET_RESOURCE_FOLDER_KEY] === uniqueId
|
||||
) {
|
||||
return {
|
||||
id: ROOT_KEY,
|
||||
...extraProps,
|
||||
};
|
||||
} else if (elm.classList.contains(TOOL_BAR_CLASS_NAME)) {
|
||||
return CLICK_TOOL_BAR;
|
||||
}
|
||||
const result = findTargetElement(
|
||||
elm.parentElement,
|
||||
uniqueId,
|
||||
customClassName,
|
||||
);
|
||||
if (typeof result === 'object' && result !== null) {
|
||||
return {
|
||||
...result,
|
||||
...extraProps,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const getFolderIdFromPath = (resource: ResourceType | null): string => {
|
||||
if (!resource) {
|
||||
return '';
|
||||
}
|
||||
if (resource.type === 'folder') {
|
||||
return String(resource.id);
|
||||
} else {
|
||||
return String(resource.path?.[resource.path.length - 2]);
|
||||
}
|
||||
};
|
||||
|
||||
const canStartDrag = (
|
||||
startPosition: MousePosition,
|
||||
currentPosition: MousePosition,
|
||||
): boolean => {
|
||||
if (
|
||||
Math.abs(currentPosition.x - startPosition.x) > START_DRAG_GAP ||
|
||||
Math.abs(currentPosition.y - startPosition.y) > START_DRAG_GAP
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const validateDrag = (
|
||||
resourceList: ResourceType[],
|
||||
target: ResourceType,
|
||||
config?: ConfigType,
|
||||
) => {
|
||||
const { name, path, id } = target;
|
||||
|
||||
/**
|
||||
* 只要有一个文件是自己层级挪动到自己层级的则 return
|
||||
*/
|
||||
const selfList = resourceList.filter(resource => {
|
||||
const resourcePath = resource.path || [];
|
||||
const parentId = resourcePath[resourcePath.length - 2];
|
||||
return parentId === id;
|
||||
});
|
||||
if (selfList.length) {
|
||||
return `Can't move ${selfList
|
||||
.map(item => item.name)
|
||||
.join(', ')} into ${name}`;
|
||||
}
|
||||
|
||||
// 校验是不是爹移到儿子
|
||||
const notAllowedList = resourceList.filter(resource =>
|
||||
(path || []).includes(String(resource.id)),
|
||||
);
|
||||
|
||||
if (notAllowedList.length) {
|
||||
return `Can't move ${notAllowedList
|
||||
.map(item => item.name)
|
||||
.join(', ')} into ${name}`;
|
||||
}
|
||||
|
||||
// 校验移动之后层级是不是过深
|
||||
const maxDeep = resourceList.reduce(
|
||||
(max, resource) => Math.max(max, (resource?.maxDeep || 0) + 1),
|
||||
0,
|
||||
);
|
||||
const targetDeep = (target.path || []).length - 1;
|
||||
|
||||
if (targetDeep + maxDeep > (config?.maxDeep || MAX_DEEP)) {
|
||||
return `Can't move into ${name}. MaxDeep is ${config?.maxDeep || MAX_DEEP}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export {
|
||||
DATASET_PARENT_DATA_STOP_TAG,
|
||||
DATASET_PARENT_DATA_KEY_ID,
|
||||
DATASET_RESOURCE_FOLDER_KEY,
|
||||
CLICK_CONTEXT_MENU,
|
||||
TOOL_BAR_CLASS_NAME,
|
||||
CLICK_TOOL_BAR,
|
||||
CLICK_OUTSIDE,
|
||||
PATH_SPLIT_KEY,
|
||||
findTargetElement,
|
||||
getElementByXY,
|
||||
getFolderIdFromPath,
|
||||
validateDrag,
|
||||
canStartDrag,
|
||||
};
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
/* eslint-disable max-lines-per-function */
|
||||
import { useState } from 'react';
|
||||
|
||||
import { noop } from 'lodash-es';
|
||||
|
||||
import {
|
||||
getResourceById,
|
||||
findResourceByPath,
|
||||
sortResourceList,
|
||||
} from '../../utils';
|
||||
import {
|
||||
type ChangeNameType,
|
||||
type CreateResourcePropType,
|
||||
type DragPropType,
|
||||
type ResourceType,
|
||||
type ResourceMapType,
|
||||
type IdType,
|
||||
} from '../../type';
|
||||
import { OPTIMISM_ID_PREFIX } from '../../constant';
|
||||
|
||||
export const useOptimismUI = ({
|
||||
enable,
|
||||
onChange = noop,
|
||||
onDrag = noop,
|
||||
onChangeName = noop,
|
||||
onCreate = noop,
|
||||
onDelete = noop,
|
||||
onRevertDelete = noop,
|
||||
changeResourceTree,
|
||||
scrollInView,
|
||||
resourceTreeRef,
|
||||
resourceMap,
|
||||
}: {
|
||||
enable?: boolean;
|
||||
onChange?: (resource: ResourceType[]) => void;
|
||||
onDrag?: (v: DragPropType) => void;
|
||||
onChangeName?: (v: ChangeNameType) => void;
|
||||
onCreate?: (v: CreateResourcePropType) => void;
|
||||
onDelete?: (ids: ResourceType[]) => void;
|
||||
onRevertDelete?: (ids: ResourceType[]) => void;
|
||||
changeResourceTree: (v) => void;
|
||||
scrollInView: (selectedId: string) => void;
|
||||
resourceTreeRef: React.MutableRefObject<ResourceType>;
|
||||
resourceMap: React.MutableRefObject<ResourceMapType>;
|
||||
}) => {
|
||||
const [optimismSavingMap, setOptimismSavingMap] = useState<
|
||||
Record<IdType, true>
|
||||
>({});
|
||||
|
||||
const clearOptimismSavingMap = () => {
|
||||
setOptimismSavingMap({});
|
||||
};
|
||||
|
||||
const addOptimismSavingItems = (_ids: string | string[]) => {
|
||||
const ids = _ids instanceof Array ? _ids : [_ids];
|
||||
setOptimismSavingMap({
|
||||
...optimismSavingMap,
|
||||
...ids.reduce(
|
||||
(pre, cur) => ({
|
||||
[cur]: true,
|
||||
...pre,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const handleDrag = (res: DragPropType) => {
|
||||
const { toId, resourceList } = res;
|
||||
if (!toId || !resourceList) {
|
||||
return;
|
||||
}
|
||||
resourceList.forEach(resource => {
|
||||
const target = findResourceByPath(
|
||||
resourceTreeRef.current,
|
||||
resource.path!.slice(0, resource.path!.length - 1),
|
||||
);
|
||||
if (target?.children) {
|
||||
target.children = target.children.filter(
|
||||
child => child.id !== resource.id,
|
||||
);
|
||||
}
|
||||
});
|
||||
const toTarget = getResourceById(resourceTreeRef.current, toId)?.resource;
|
||||
if (toTarget) {
|
||||
toTarget.children = sortResourceList([
|
||||
...(toTarget.children || []),
|
||||
...resourceList,
|
||||
]);
|
||||
}
|
||||
|
||||
addOptimismSavingItems(resourceList.map(resource => resource.id));
|
||||
|
||||
changeResourceTree(resourceTreeRef.current);
|
||||
onDrag?.(res);
|
||||
onChange?.(resourceTreeRef.current?.children || []);
|
||||
};
|
||||
|
||||
const handleChangeName = (res: ChangeNameType) => {
|
||||
const target = findResourceByPath(resourceTreeRef.current, res.path || []);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
target.name = res.name;
|
||||
|
||||
addOptimismSavingItems(target.id);
|
||||
|
||||
changeResourceTree(resourceTreeRef.current);
|
||||
onChangeName?.(res);
|
||||
onChange?.(resourceTreeRef.current?.children || []);
|
||||
};
|
||||
const handleCreate = (res: CreateResourcePropType) => {
|
||||
const { path, type, name } = res;
|
||||
const target = findResourceByPath(resourceTreeRef.current, path);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
const tempId = `${OPTIMISM_ID_PREFIX}${Number(new Date())}`;
|
||||
const tempFile = {
|
||||
id: tempId,
|
||||
type,
|
||||
name,
|
||||
};
|
||||
|
||||
addOptimismSavingItems(tempId);
|
||||
|
||||
target.children = sortResourceList([...(target.children || []), tempFile]);
|
||||
changeResourceTree(resourceTreeRef.current);
|
||||
onCreate(res);
|
||||
|
||||
resourceMap.current[tempId] = tempFile;
|
||||
scrollInView?.(tempId);
|
||||
onChange?.(resourceTreeRef.current?.children || []);
|
||||
};
|
||||
const handleDelete = (res: ResourceType[]) => {
|
||||
onDelete(res);
|
||||
};
|
||||
const handleRevertDelete = (res: ResourceType[]) => {
|
||||
res.forEach(source => {
|
||||
const target = findResourceByPath(
|
||||
resourceTreeRef.current,
|
||||
source.path || [],
|
||||
);
|
||||
if (target) {
|
||||
target.status = 'normal';
|
||||
}
|
||||
});
|
||||
changeResourceTree(resourceTreeRef.current);
|
||||
onRevertDelete?.(res);
|
||||
onChange?.(resourceTreeRef.current?.children || []);
|
||||
};
|
||||
|
||||
const commonArgs = { optimismSavingMap, clearOptimismSavingMap };
|
||||
|
||||
if (!enable) {
|
||||
return {
|
||||
handleDrag: onDrag,
|
||||
handleChangeName: onChangeName,
|
||||
handleCreate: onCreate,
|
||||
handleDelete: onDelete,
|
||||
handleRevertDelete: onRevertDelete,
|
||||
...commonArgs,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
handleDrag,
|
||||
handleChangeName,
|
||||
handleCreate,
|
||||
handleDelete,
|
||||
handleRevertDelete,
|
||||
...commonArgs,
|
||||
};
|
||||
};
|
||||
@@ -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 { useEffect, useRef } from 'react';
|
||||
|
||||
import { CommandRegistry, useIDEService } from '@coze-project-ide/client';
|
||||
|
||||
import { ContextMenuConfigMap } from '../use-right-click-panel/constant';
|
||||
import {
|
||||
type RightPanelConfigType,
|
||||
type ResourceFolderContextType,
|
||||
type ResourceType,
|
||||
} from '../../type';
|
||||
import { BaseResourceContextMenuBtnType } from '../../constant';
|
||||
|
||||
const useRegisterCommand = ({
|
||||
isFocus,
|
||||
id,
|
||||
updateContext,
|
||||
clearContext,
|
||||
selectedIdRef,
|
||||
tempSelectedMapRef,
|
||||
}: {
|
||||
isFocus: boolean;
|
||||
id: string;
|
||||
updateContext: (v: Partial<ResourceFolderContextType>) => void;
|
||||
clearContext: () => void;
|
||||
selectedIdRef: React.MutableRefObject<string>;
|
||||
tempSelectedMapRef: React.MutableRefObject<Record<string, ResourceType>>;
|
||||
}) => {
|
||||
const cbRef = useRef<Record<string, (v?: any) => void>>({});
|
||||
const commandRegistry = useIDEService<CommandRegistry>(CommandRegistry);
|
||||
|
||||
const dispatchInstance = useRef<ResourceFolderContextType>({
|
||||
onEnter: () => {
|
||||
cbRef.current[BaseResourceContextMenuBtnType.EditName]?.();
|
||||
},
|
||||
onDelete: () => {
|
||||
cbRef.current[BaseResourceContextMenuBtnType.Delete]?.();
|
||||
},
|
||||
onCreateFolder: () => {
|
||||
cbRef.current[BaseResourceContextMenuBtnType.CreateFolder]?.();
|
||||
},
|
||||
onCreateResource: () => {
|
||||
cbRef.current[BaseResourceContextMenuBtnType.CreateResource]?.();
|
||||
},
|
||||
});
|
||||
|
||||
const registerEvent = (type: BaseResourceContextMenuBtnType, cb) => {
|
||||
cbRef.current[type] = cb;
|
||||
};
|
||||
|
||||
const registerCommand = (config: RightPanelConfigType[]) => {
|
||||
config.forEach(command => {
|
||||
if ('type' in command) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ContextMenuConfigMap[command.id]) {
|
||||
if (command.label || command.shortLabel) {
|
||||
commandRegistry.updateCommand(command.id, {
|
||||
...(command.label ? { label: command.label } : {}),
|
||||
...(command.shortLabel ? { shortLabel: command.shortLabel } : {}),
|
||||
});
|
||||
}
|
||||
} else if (command.execute) {
|
||||
// 如果有自定义的 execute 函数才会需要重新注册
|
||||
if (commandRegistry.getCommand(command.id)) {
|
||||
commandRegistry.unregisterCommand(command.id);
|
||||
}
|
||||
commandRegistry.registerCommand(
|
||||
{
|
||||
id: command.id,
|
||||
label: command.label,
|
||||
shortLabel: command.label,
|
||||
},
|
||||
{
|
||||
execute: () => {
|
||||
command.execute?.();
|
||||
},
|
||||
isEnabled: opt => !opt.disabled,
|
||||
isVisible: opt => !opt.isHidden,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocus) {
|
||||
updateContext({
|
||||
...dispatchInstance.current,
|
||||
currentSelectedId: selectedIdRef.current,
|
||||
tempSelectedMap: tempSelectedMapRef.current,
|
||||
id,
|
||||
});
|
||||
} else {
|
||||
clearContext();
|
||||
}
|
||||
}, [isFocus]);
|
||||
|
||||
return {
|
||||
registerEvent,
|
||||
registerCommand,
|
||||
};
|
||||
};
|
||||
|
||||
export { useRegisterCommand };
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 enum BaseResourceContextMenuBtnType {
|
||||
CreateFolder = 'resource-folder-create-folder',
|
||||
CreateResource = 'resource-folder-create-resource',
|
||||
EditName = 'resource-folder-edit-name',
|
||||
Delete = 'resource-folder-delete',
|
||||
}
|
||||
|
||||
export const ContextMenuConfigMap: Record<
|
||||
BaseResourceContextMenuBtnType,
|
||||
{
|
||||
id: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
executeName: string;
|
||||
}
|
||||
> = {
|
||||
[BaseResourceContextMenuBtnType.CreateFolder]: {
|
||||
id: BaseResourceContextMenuBtnType.CreateFolder,
|
||||
label: 'Create Folder',
|
||||
executeName: 'onCreateFolder',
|
||||
},
|
||||
[BaseResourceContextMenuBtnType.CreateResource]: {
|
||||
id: BaseResourceContextMenuBtnType.CreateResource,
|
||||
label: 'Create Resource',
|
||||
executeName: 'onCreateResource',
|
||||
},
|
||||
[BaseResourceContextMenuBtnType.EditName]: {
|
||||
id: BaseResourceContextMenuBtnType.EditName,
|
||||
label: 'Edit Name',
|
||||
executeName: 'onEnter',
|
||||
},
|
||||
[BaseResourceContextMenuBtnType.Delete]: {
|
||||
id: BaseResourceContextMenuBtnType.Delete,
|
||||
label: 'Delete',
|
||||
executeName: 'onDelete',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type React from 'react';
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
import { MenuService, useIDEService } from '@coze-project-ide/client';
|
||||
|
||||
import { createUniqId } from '../../utils';
|
||||
import { type ResourceType, type RightPanelConfigType } from '../../type';
|
||||
import { RESOURCE_FOLDER_WRAPPER_CLASS } from '../../constant';
|
||||
import { handleConfig } from './util';
|
||||
import { ContextMenuConfigMap } from './constant';
|
||||
|
||||
const commandIdStashSet: Set<string> = new Set();
|
||||
|
||||
const RESOURCE_FOLDER_SEPARATOR_KEY = 'resource-folder-separator-key';
|
||||
|
||||
const useRightClickPanel = ({
|
||||
tempSelectedMapRef,
|
||||
contextMenuHandler,
|
||||
registerCommand,
|
||||
id,
|
||||
contextMenuDisabled,
|
||||
onContextMenuVisibleChange,
|
||||
}: {
|
||||
tempSelectedMapRef: React.MutableRefObject<Record<string, ResourceType>>;
|
||||
contextMenuHandler?: (v: ResourceType[]) => RightPanelConfigType[];
|
||||
registerCommand: (config: RightPanelConfigType[]) => void;
|
||||
id: string;
|
||||
contextMenuDisabled?: boolean;
|
||||
onContextMenuVisibleChange?: (v: boolean) => void;
|
||||
}) => {
|
||||
const menuService = useIDEService<MenuService>(MenuService);
|
||||
|
||||
const separatorNum = useRef(0);
|
||||
const menuNum = useRef(0);
|
||||
const rightPanelVisible = useRef(false);
|
||||
const changeRightPanelVisible = (v: boolean) => {
|
||||
if (rightPanelVisible.current !== v) {
|
||||
rightPanelVisible.current = v;
|
||||
onContextMenuVisibleChange?.(rightPanelVisible.current);
|
||||
}
|
||||
};
|
||||
|
||||
const clearMenuItems = () => {
|
||||
menuService.clearMenuItems(
|
||||
[
|
||||
...commandIdStashSet.keys(),
|
||||
...Object.keys(ContextMenuConfigMap),
|
||||
...new Array(separatorNum.current)
|
||||
.fill(null)
|
||||
.map(_ => command => command === RESOURCE_FOLDER_SEPARATOR_KEY),
|
||||
].filter(Boolean),
|
||||
);
|
||||
separatorNum.current = 0;
|
||||
menuNum.current = 0;
|
||||
};
|
||||
|
||||
const dispose = () => {
|
||||
clearMenuItems();
|
||||
(menuService as any)?.contextMenu?.menu?.close?.();
|
||||
changeRightPanelVisible?.(false);
|
||||
};
|
||||
|
||||
const contextMenuCallback = (e, resources?: ResourceType[]) => {
|
||||
const baseConfig = contextMenuHandler
|
||||
? contextMenuHandler(
|
||||
resources || Object.values(tempSelectedMapRef.current),
|
||||
)
|
||||
: [];
|
||||
|
||||
const config = handleConfig(baseConfig);
|
||||
|
||||
registerCommand(config);
|
||||
|
||||
clearMenuItems();
|
||||
|
||||
config.forEach(v => {
|
||||
if ('type' in v) {
|
||||
separatorNum.current = separatorNum.current + 1;
|
||||
menuNum.current = menuNum.current + 1;
|
||||
menuService.addMenuItem({
|
||||
command: RESOURCE_FOLDER_SEPARATOR_KEY,
|
||||
type: 'separator',
|
||||
selector: `.${createUniqId(RESOURCE_FOLDER_WRAPPER_CLASS, id)}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!v.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!commandIdStashSet.has(v.id)) {
|
||||
commandIdStashSet.add(v.id);
|
||||
}
|
||||
if (!contextMenuDisabled) {
|
||||
menuNum.current = menuNum.current + 1;
|
||||
menuService.addMenuItem({
|
||||
command: v.id,
|
||||
selector: `.${createUniqId(RESOURCE_FOLDER_WRAPPER_CLASS, id)}`,
|
||||
args: v,
|
||||
tooltip: v.tooltip,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!contextMenuDisabled && menuNum.current > 0) {
|
||||
menuService.open(e);
|
||||
setTimeout(() => {
|
||||
changeRightPanelVisible?.(true);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return () => {
|
||||
dispose();
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(menuService as any)?.contextMenu?.menu?.aboutToClose?.connect?.(() => {
|
||||
if (rightPanelVisible.current) {
|
||||
changeRightPanelVisible?.(false);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { contextMenuCallback, closeContextMenu: dispose };
|
||||
};
|
||||
|
||||
export { useRightClickPanel };
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type RightPanelConfigType } from '../../type';
|
||||
import { ContextMenuConfigMap } from './constant';
|
||||
|
||||
/**
|
||||
* 主要替换资源树默认支持的 右键菜单配置,
|
||||
* 并且对三方注入的右键菜单的 id 进行包装
|
||||
*/
|
||||
export const handleConfig = (
|
||||
baseConfig: RightPanelConfigType[],
|
||||
): RightPanelConfigType[] =>
|
||||
baseConfig.map(config => {
|
||||
if ('type' in config) {
|
||||
return config;
|
||||
}
|
||||
if (ContextMenuConfigMap[config.id]) {
|
||||
return {
|
||||
...ContextMenuConfigMap[config.id],
|
||||
...config,
|
||||
id: config.id,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
id: config.id,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
const useSelectedChange = ({
|
||||
selected,
|
||||
resourceMap,
|
||||
collapsedMapRef,
|
||||
setCollapsed,
|
||||
tempSelectedMapRef,
|
||||
setTempSelectedMap,
|
||||
scrollInView,
|
||||
updateContext,
|
||||
}) => {
|
||||
const selectedIdRef = useRef<string>(selected || '');
|
||||
|
||||
useEffect(() => {
|
||||
if (!selected) {
|
||||
setTempSelectedMap({});
|
||||
return;
|
||||
}
|
||||
selectedIdRef.current = selected;
|
||||
|
||||
updateContext({ currentSelectedId: selected });
|
||||
|
||||
// 将聚焦的 path 上的文件夹都展开
|
||||
const path = resourceMap.current[selected]?.path || [];
|
||||
path.forEach(pathKey => {
|
||||
delete collapsedMapRef.current[pathKey];
|
||||
});
|
||||
setCollapsed({
|
||||
...collapsedMapRef.current,
|
||||
});
|
||||
|
||||
tempSelectedMapRef.current = {};
|
||||
if (resourceMap.current?.[selected]) {
|
||||
tempSelectedMapRef.current = {
|
||||
[selected]: resourceMap.current[selected],
|
||||
};
|
||||
}
|
||||
setTempSelectedMap(tempSelectedMapRef.current);
|
||||
|
||||
setTimeout(() => {
|
||||
scrollInView(selected);
|
||||
}, 16);
|
||||
}, [selected]);
|
||||
|
||||
return selectedIdRef;
|
||||
};
|
||||
|
||||
export { useSelectedChange };
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
const useStateRef = <T>(
|
||||
v: T,
|
||||
cb?: (v: T) => void,
|
||||
): [React.MutableRefObject<T>, (v: T) => void, T] => {
|
||||
const [state, setState] = useState<T>(v);
|
||||
const ref = useRef<T>(v);
|
||||
|
||||
const onChange = (nextV: T) => {
|
||||
ref.current = nextV;
|
||||
setState(nextV);
|
||||
cb?.(nextV);
|
||||
};
|
||||
|
||||
return [ref, onChange, state];
|
||||
};
|
||||
|
||||
export { useStateRef };
|
||||
@@ -0,0 +1,159 @@
|
||||
.resource-list-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
|
||||
:global{
|
||||
.resource-list-custom-event-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.resource-list-scroll-container {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.resource-list-drag-and-drop-wrapper {
|
||||
height: 100%;
|
||||
|
||||
.item-wrapper {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
|
||||
.item-wrapper-indent-line {
|
||||
position: absolute;
|
||||
border-left-width: 1px;
|
||||
border-left-style: solid;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.base-item-hover-class {
|
||||
&:hover {
|
||||
background-color: rgba(6, 7, 9, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.item-is-temp-selected {
|
||||
background-color: rgba(6, 7, 9, 0.04);
|
||||
border-radius: 0px;
|
||||
}
|
||||
.item-is-in-edit {
|
||||
background-color: rgba(6, 7, 9, 0.04);
|
||||
}
|
||||
.item-is-selected {
|
||||
background-color: rgba(6, 7, 9, 0.14);
|
||||
}
|
||||
|
||||
.dragging-hover-class {
|
||||
background-color: rgba(148, 152, 247, 0.44);
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.file-item-wrapper {
|
||||
}
|
||||
|
||||
.base-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 8px;
|
||||
transition: all 0.1s ease-in-out;
|
||||
|
||||
.base-item-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.base-item-name-input {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.semi-input-wrapper {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.semi-input-wrapper-focus {
|
||||
background-color: white;
|
||||
}
|
||||
[class~='semi-input'] {
|
||||
padding: 0 4px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.base-item-name-input-error-msg-absolute {
|
||||
z-index: 999;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 26px;
|
||||
background-color: rgba(255, 241, 242, 1);
|
||||
border-width: 1px;
|
||||
border-color: rgba(242, 36, 53, 1);
|
||||
border-radius: 6px;
|
||||
border-style: solid;
|
||||
color: rgba(6, 7, 9, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 4px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.base-item-name-input-error {
|
||||
[class~='semi-input-wrapper'] {
|
||||
border-color: rgba(242, 36, 53, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.base-item-more-hover-display-class {
|
||||
display: none;
|
||||
}
|
||||
&:hover {
|
||||
.base-item-more-hover-display-class {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.base-item-more-btn {
|
||||
// 用于覆盖 coze-design 二次封装被固化的样式。。。
|
||||
min-width: 16px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
background-color: transparent;
|
||||
color: rgba(6, 7, 9, 0.96);
|
||||
&:hover {
|
||||
background-color: rgba(6, 7, 9, 0.14);
|
||||
}
|
||||
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.base-radius-class-first {
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
.base-radius-class-last {
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
.base-radius-class-single {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,776 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable complexity */
|
||||
/* eslint-disable max-lines */
|
||||
/* eslint-disable max-lines-per-function */
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import React, {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import { noop } from 'lodash-es';
|
||||
|
||||
import { createUniqId, flatTree, mapResourceTree } from './utils';
|
||||
import {
|
||||
type DragPropType,
|
||||
type ResourceType,
|
||||
type ChangeNameType,
|
||||
type ResourceMapType,
|
||||
type CreateResourcePropType,
|
||||
type CommonRenderProps,
|
||||
type RightPanelConfigType,
|
||||
ResourceTypeEnum,
|
||||
type ValidatorConfigType,
|
||||
type ConfigType,
|
||||
type RenderMoreSuffixType,
|
||||
type IdType,
|
||||
} from './type';
|
||||
import { BaseRender } from './render-components/base-render';
|
||||
import { useStateRef } from './hooks/uss-state-ref';
|
||||
import { useSelectedChange } from './hooks/use-selected-change';
|
||||
import { useRightClickPanel } from './hooks/use-right-click-panel';
|
||||
import { useRegisterCommand } from './hooks/use-register-command';
|
||||
import { useOptimismUI } from './hooks/use-optimism-ui';
|
||||
import {
|
||||
DATASET_PARENT_DATA_STOP_TAG,
|
||||
DATASET_RESOURCE_FOLDER_KEY,
|
||||
} from './hooks/use-mouse-event/utils';
|
||||
import { useMouseEvent } from './hooks/use-mouse-event';
|
||||
import { useFocusResource } from './hooks/use-focus-resource';
|
||||
import { useEvent } from './hooks/use-custom-event';
|
||||
import {
|
||||
CREATE_RESOURCE_ID,
|
||||
useCreateEditResource,
|
||||
} from './hooks/use-create-edit-resource';
|
||||
import { useContextChange } from './hooks/use-context-change';
|
||||
import { useCollapsedMap } from './hooks/use-collapsed-map';
|
||||
import { RESOURCE_FOLDER_WRAPPER_CLASS, ROOT_KEY, ROOT_NODE } from './constant';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface RefType {
|
||||
/**
|
||||
* 创建文件夹
|
||||
*/
|
||||
createFolder: () => void;
|
||||
/**
|
||||
* 创建资源
|
||||
*/
|
||||
createResource: (type: string) => void;
|
||||
/**
|
||||
* 重命名资源
|
||||
*/
|
||||
renameResource: (id: IdType) => void;
|
||||
/**
|
||||
* 手动关闭右键菜单
|
||||
*/
|
||||
closeContextMenu: () => void;
|
||||
/**
|
||||
* 收起所有文件夹
|
||||
*/
|
||||
collapseAll: () => void;
|
||||
/**
|
||||
* 展开所有文件夹
|
||||
*/
|
||||
expandAll: () => void;
|
||||
/**
|
||||
* 手动聚焦
|
||||
*/
|
||||
focus: () => void;
|
||||
/**
|
||||
* 手动失焦
|
||||
*/
|
||||
blur: () => void;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
style?: React.CSSProperties;
|
||||
resourceTree: ResourceType | ResourceType[];
|
||||
resourceMap: ResourceMapType;
|
||||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* 主要的资源类型,非必填。
|
||||
* 主要用于快捷键创建资源的默认类型。
|
||||
*/
|
||||
defaultResourceType?: string;
|
||||
/**
|
||||
* 是否使用乐观 ui;
|
||||
* false 时,onChange 失效;
|
||||
* default = true
|
||||
*
|
||||
* 传入 loadingRender 时,会对乐观保存的 item 尾部增加一个渲染块,由外部控制渲染
|
||||
*/
|
||||
useOptimismUI?:
|
||||
| boolean
|
||||
| {
|
||||
loadingRender?: () => React.ReactElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* 当前选中的资源 id, 受控的
|
||||
*/
|
||||
selected?: string;
|
||||
|
||||
/**
|
||||
* 是否渲染每个 item 末尾的 more 按钮,hover 等同于 右键
|
||||
*/
|
||||
renderMoreSuffix?: RenderMoreSuffixType;
|
||||
|
||||
/**
|
||||
* 用于 name 校验的配置
|
||||
*/
|
||||
validateConfig?: ValidatorConfigType;
|
||||
|
||||
/**
|
||||
* 支持搜索, 高亮展示
|
||||
*/
|
||||
searchConfig?: {
|
||||
searchKey?: string;
|
||||
highlightStyle?: React.CSSProperties;
|
||||
};
|
||||
|
||||
/**
|
||||
* 可选。
|
||||
* 传入则是受控的收起展开树。
|
||||
* 不传则内部自己维护树
|
||||
*/
|
||||
collapsedMap?: Record<string, boolean>;
|
||||
setCollapsedMap?: (v: Record<string, boolean>) => void;
|
||||
|
||||
/**
|
||||
* 树变更的回调函数,依赖 useOptimismUI 为 true。
|
||||
*/
|
||||
onChange?: (resource: ResourceType[]) => void;
|
||||
|
||||
/**
|
||||
* 单击选中资源的回调,仅支持非 folder 类型资源
|
||||
*/
|
||||
onSelected?: (id: string | number, resource: ResourceType) => void;
|
||||
/**
|
||||
* 拖拽完成之后的回调
|
||||
*/
|
||||
onDrag?: (v: DragPropType) => void;
|
||||
/**
|
||||
* 修改 name 之后的回调
|
||||
*/
|
||||
onChangeName?: (v: ChangeNameType) => void;
|
||||
/**
|
||||
* 创建资源的回调
|
||||
*/
|
||||
onCreate?: (v: CreateResourcePropType) => void;
|
||||
/**
|
||||
* 删除的回调。该方法不会被乐观 ui 逻辑改写,最好业务层加一个二次确认逻辑,删除之后走数据更新的方式来更新树组件
|
||||
*/
|
||||
onDelete?: (ids: ResourceType[]) => void;
|
||||
|
||||
/**
|
||||
* 用于自定义配置渲染资源的 icon
|
||||
* @returns react 节点
|
||||
*/
|
||||
iconRender?: (v: CommonRenderProps) => React.ReactElement | undefined;
|
||||
|
||||
/**
|
||||
* 用于自定义配置每一项末尾的元素
|
||||
*/
|
||||
suffixRender?: {
|
||||
width: number; // 用于文本超长的 tooltip 偏移量计算的,是一个必填字段
|
||||
render: (v: CommonRenderProps) => React.ReactElement | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 用于自定义配置每一个资源的文本渲染器。
|
||||
* 如果采用自定义渲染,则需要自己实现搜索高亮能力
|
||||
* @returns react 节点
|
||||
*/
|
||||
textRender?: (v: CommonRenderProps) => React.ReactElement | undefined;
|
||||
|
||||
/**
|
||||
* 右键菜单配置
|
||||
* @param v 当前临时选中的资源列表。 可以通过 id 判断是否是根文件。(ROOT_KEY: 根文件 id)
|
||||
* @returns 组件内置注册好的命令和菜单,详见 BaseResourceContextMenuBtnType 枚举
|
||||
*/
|
||||
contextMenuHandler?: (v: ResourceType[]) => RightPanelConfigType[];
|
||||
|
||||
/**
|
||||
* 右键菜单弹窗展示和隐藏的回调。
|
||||
*/
|
||||
onContextMenuVisibleChange?: (v: boolean) => void;
|
||||
|
||||
/**
|
||||
* 禁用右键菜单,主要是兼容在 popover 的场景内
|
||||
*/
|
||||
contextMenuDisabled?: boolean;
|
||||
|
||||
/**
|
||||
* 一些用于杂七杂八的配置项
|
||||
*/
|
||||
config?: ConfigType;
|
||||
|
||||
/**
|
||||
* 能力黑名单
|
||||
*/
|
||||
powerBlackMap?: {
|
||||
dragAndDrop?: boolean;
|
||||
folder?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 列表为空的渲染组件
|
||||
*/
|
||||
empty?: React.ReactElement;
|
||||
}
|
||||
|
||||
let idIndex = 0;
|
||||
|
||||
const ResourceFolder = forwardRef<RefType, Props>(
|
||||
(
|
||||
{
|
||||
id,
|
||||
resourceTree: _resourceTree,
|
||||
resourceMap: _resourceMap,
|
||||
selected,
|
||||
disabled,
|
||||
searchConfig,
|
||||
defaultResourceType,
|
||||
useOptimismUI: _useOptimismUI = false,
|
||||
style,
|
||||
collapsedMap: _collapsedMap,
|
||||
setCollapsedMap: _setCollapsedMap,
|
||||
validateConfig,
|
||||
onChange = noop,
|
||||
onSelected: _onSelected = noop,
|
||||
onDrag: _onDrag = noop,
|
||||
onChangeName: _onChangeName = noop,
|
||||
onCreate: _onCreate = noop,
|
||||
onDelete: _onDelete = noop,
|
||||
iconRender,
|
||||
suffixRender,
|
||||
textRender,
|
||||
renderMoreSuffix: _renderMoreSuffix = false,
|
||||
contextMenuHandler,
|
||||
onContextMenuVisibleChange,
|
||||
contextMenuDisabled,
|
||||
config,
|
||||
powerBlackMap,
|
||||
empty,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const uniqId = useRef(id ? id : `${idIndex++}`);
|
||||
|
||||
const resourceTreeWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { updateContext, clearContext, updateId } = useContextChange(
|
||||
uniqId.current,
|
||||
);
|
||||
|
||||
const renderMoreSuffix = contextMenuDisabled ? false : _renderMoreSuffix;
|
||||
|
||||
/**
|
||||
* 临时选中的表
|
||||
*/
|
||||
const [tempSelectedMapRef, setTempSelectedMap] = useStateRef<
|
||||
Record<string, ResourceType>
|
||||
>({}, v => {
|
||||
updateContext?.({ tempSelectedMap: v });
|
||||
});
|
||||
|
||||
/**
|
||||
* 打平的树
|
||||
*/
|
||||
const resourceMap = useRef<ResourceMapType>(_resourceMap || {});
|
||||
const changeResourceMap = nextMap => {
|
||||
resourceMap.current = nextMap;
|
||||
// 变更之后维护临时选中表
|
||||
tempSelectedMapRef.current = Object.keys(
|
||||
tempSelectedMapRef.current,
|
||||
).reduce((pre, cur) => {
|
||||
if (resourceMap.current[cur]) {
|
||||
return {
|
||||
...pre,
|
||||
[cur]: resourceMap.current[cur],
|
||||
};
|
||||
}
|
||||
return pre;
|
||||
}, {});
|
||||
setTempSelectedMap(tempSelectedMapRef.current);
|
||||
};
|
||||
useEffect(() => {
|
||||
changeResourceMap(_resourceMap);
|
||||
}, [_resourceMap]);
|
||||
|
||||
/**
|
||||
* 处理一系列收起展开的 hook
|
||||
*/
|
||||
const { collapsedMapRef, handleCollapse, setCollapsed, collapsedState } =
|
||||
useCollapsedMap({
|
||||
_collapsedMap,
|
||||
_setCollapsedMap,
|
||||
resourceMap,
|
||||
});
|
||||
|
||||
/**
|
||||
* 用于渲染的树
|
||||
*/
|
||||
const [resourceTreeRef, setResourceTree] = useStateRef<ResourceType>(
|
||||
{
|
||||
...ROOT_NODE,
|
||||
children:
|
||||
_resourceTree instanceof Array ? _resourceTree : [_resourceTree],
|
||||
} as unknown as ResourceType,
|
||||
v => {
|
||||
setResourceList(
|
||||
flatTree(v, resourceMap.current, collapsedMapRef.current),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const changeResourceTree = v => {
|
||||
resourceTreeRef.current = v;
|
||||
changeResourceMap(mapResourceTree(v?.children || []));
|
||||
setResourceTree(resourceTreeRef.current);
|
||||
};
|
||||
|
||||
const [resourceList, setResourceList] = useStateRef(
|
||||
flatTree(
|
||||
resourceTreeRef.current,
|
||||
resourceMap.current,
|
||||
collapsedMapRef.current,
|
||||
),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setResourceList(
|
||||
flatTree(
|
||||
resourceTreeRef.current,
|
||||
resourceMap.current,
|
||||
collapsedMapRef.current,
|
||||
),
|
||||
);
|
||||
}, [collapsedState]);
|
||||
|
||||
const disabledRef = useRef(!!disabled);
|
||||
useEffect(() => {
|
||||
disabledRef.current = !!disabled;
|
||||
}, [disabled]);
|
||||
|
||||
/**
|
||||
* 用于收敛树组件的滚动,聚焦逻辑的 hook
|
||||
*/
|
||||
const { scrollInView, scrollWrapper, tempDisableScroll } = useFocusResource(
|
||||
{
|
||||
resourceTreeRef,
|
||||
collapsedMapRef,
|
||||
resourceMap,
|
||||
config,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
handleDrag: onDrag,
|
||||
handleChangeName: onChangeName,
|
||||
handleCreate: onCreate,
|
||||
handleDelete: onDelete,
|
||||
optimismSavingMap,
|
||||
clearOptimismSavingMap,
|
||||
} = useOptimismUI({
|
||||
enable: !!_useOptimismUI,
|
||||
onDrag: _onDrag,
|
||||
onChangeName: _onChangeName,
|
||||
onCreate: _onCreate,
|
||||
onDelete: _onDelete,
|
||||
changeResourceTree,
|
||||
scrollInView,
|
||||
resourceTreeRef,
|
||||
resourceMap,
|
||||
onChange,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
resourceTreeRef.current = {
|
||||
...ROOT_NODE,
|
||||
children:
|
||||
_resourceTree instanceof Array ? _resourceTree : [_resourceTree],
|
||||
};
|
||||
setResourceTree(resourceTreeRef.current);
|
||||
clearOptimismSavingMap();
|
||||
}, [_resourceTree]);
|
||||
|
||||
/**
|
||||
* 处理选中的资源变更之后的副作用
|
||||
*/
|
||||
const selectedIdRef = useSelectedChange({
|
||||
selected,
|
||||
resourceMap,
|
||||
collapsedMapRef,
|
||||
setCollapsed,
|
||||
tempSelectedMapRef,
|
||||
setTempSelectedMap,
|
||||
scrollInView,
|
||||
updateContext,
|
||||
});
|
||||
|
||||
const { addEventListener, onMouseDownInDiv, onMouseUpInDiv } = useEvent();
|
||||
|
||||
const {
|
||||
onMouseMove,
|
||||
context: dragAndDropContext,
|
||||
context: { isFocus },
|
||||
isFocusRef,
|
||||
dragPreview,
|
||||
handleFocus,
|
||||
handleBlur,
|
||||
} = useMouseEvent({
|
||||
draggable: !powerBlackMap?.dragAndDrop,
|
||||
uniqId: uniqId.current,
|
||||
updateId,
|
||||
iconRender,
|
||||
resourceTreeWrapperRef,
|
||||
collapsedMapRef,
|
||||
tempSelectedMapRef,
|
||||
setTempSelectedMap,
|
||||
setCollapsedMap: handleCollapse,
|
||||
resourceTreeRef,
|
||||
selectedIdRef,
|
||||
onSelected: (...props) => {
|
||||
_onSelected(...props);
|
||||
tempDisableScroll();
|
||||
},
|
||||
onDrag,
|
||||
addEventListener,
|
||||
disabled: disabledRef,
|
||||
resourceMap,
|
||||
config,
|
||||
});
|
||||
|
||||
const { registerEvent, registerCommand } = useRegisterCommand({
|
||||
isFocus,
|
||||
updateContext,
|
||||
clearContext,
|
||||
id: uniqId.current,
|
||||
selectedIdRef,
|
||||
tempSelectedMapRef,
|
||||
});
|
||||
|
||||
const {
|
||||
context: createEditResourceContext,
|
||||
onCreateResource,
|
||||
isInEditModeRef,
|
||||
handleRenderList,
|
||||
handleRename,
|
||||
} = useCreateEditResource({
|
||||
folderEnable: !powerBlackMap?.folder,
|
||||
defaultResourceType,
|
||||
registerEvent,
|
||||
setCollapsedMap: handleCollapse,
|
||||
resourceTreeRef,
|
||||
tempSelectedMapRef,
|
||||
selectedIdRef,
|
||||
isFocusRef,
|
||||
resourceMap,
|
||||
onChangeName,
|
||||
onCreate,
|
||||
disabled: disabledRef,
|
||||
onDelete,
|
||||
validateConfig,
|
||||
config,
|
||||
resourceList,
|
||||
});
|
||||
|
||||
const { contextMenuCallback, closeContextMenu } = useRightClickPanel({
|
||||
tempSelectedMapRef,
|
||||
contextMenuHandler,
|
||||
registerCommand,
|
||||
id: uniqId.current,
|
||||
contextMenuDisabled,
|
||||
onContextMenuVisibleChange,
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
handleFocus();
|
||||
},
|
||||
blur: () => {
|
||||
handleBlur();
|
||||
},
|
||||
createResource: (type: string) => {
|
||||
if (isInEditModeRef.current || !type) {
|
||||
return;
|
||||
}
|
||||
onCreateResource?.(type);
|
||||
},
|
||||
renameResource: (resourceId: IdType) => {
|
||||
handleRename(resourceId);
|
||||
},
|
||||
createFolder: () => {
|
||||
if (isInEditModeRef.current) {
|
||||
return;
|
||||
}
|
||||
onCreateResource?.(ResourceTypeEnum.Folder);
|
||||
},
|
||||
expandAll: () => {
|
||||
collapsedMapRef.current = {};
|
||||
setCollapsed(collapsedMapRef.current);
|
||||
},
|
||||
collapseAll: () => {
|
||||
collapsedMapRef.current = Object.keys(resourceMap.current).reduce(
|
||||
(pre, cur) => {
|
||||
if (
|
||||
cur !== ROOT_KEY &&
|
||||
resourceMap.current?.[cur]?.type === 'folder'
|
||||
) {
|
||||
return {
|
||||
...pre,
|
||||
[cur]: true,
|
||||
};
|
||||
}
|
||||
return pre;
|
||||
},
|
||||
{},
|
||||
);
|
||||
setCollapsed(collapsedMapRef.current);
|
||||
},
|
||||
closeContextMenu,
|
||||
}));
|
||||
|
||||
const commonProps = {
|
||||
searchConfig,
|
||||
suffixRender,
|
||||
config,
|
||||
renderMoreSuffix,
|
||||
textRender,
|
||||
contextMenuCallback,
|
||||
resourceTreeWrapperRef,
|
||||
iconRender,
|
||||
isDragging: dragAndDropContext.isDragging,
|
||||
draggingError: dragAndDropContext.draggingError,
|
||||
currentHoverItem: dragAndDropContext.currentHoverItem,
|
||||
validateConfig,
|
||||
errorMsg: createEditResourceContext.errorMsg,
|
||||
errorMsgRef: createEditResourceContext.errorMsgRef,
|
||||
editResourceId: createEditResourceContext.editResourceId,
|
||||
handleChangeName: createEditResourceContext.handleChangeName,
|
||||
handleSave: createEditResourceContext.handleSave,
|
||||
useOptimismUI: _useOptimismUI,
|
||||
};
|
||||
|
||||
const createNode = createEditResourceContext?.createResourceInfo ? (
|
||||
<div key={CREATE_RESOURCE_ID}>
|
||||
<BaseRender
|
||||
resource={{
|
||||
id: CREATE_RESOURCE_ID,
|
||||
name: '',
|
||||
type: createEditResourceContext?.createResourceInfo.type,
|
||||
}}
|
||||
path={[
|
||||
...(resourceMap.current?.[
|
||||
createEditResourceContext?.createResourceInfo.parentId
|
||||
]?.path || []),
|
||||
CREATE_RESOURCE_ID,
|
||||
]}
|
||||
isInEdit={
|
||||
CREATE_RESOURCE_ID === createEditResourceContext.editResourceId
|
||||
}
|
||||
{...commonProps}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const renderResourceList = handleRenderList(
|
||||
resourceList.current,
|
||||
createEditResourceContext?.createResourceInfo,
|
||||
);
|
||||
|
||||
const emptyRender = () => {
|
||||
const list = renderResourceList || [];
|
||||
|
||||
/**
|
||||
* 为空数组,或者数组中只有一个 root 节点
|
||||
*/
|
||||
if (
|
||||
(list.length === 0 ||
|
||||
(list.length === 1 &&
|
||||
list[0] !== CREATE_RESOURCE_ID &&
|
||||
list[0].id === ROOT_KEY)) &&
|
||||
empty
|
||||
) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
key={uniqId.current}
|
||||
className={`resource-list-wrapper ${s['resource-list-wrapper']}`}
|
||||
ref={resourceTreeWrapperRef}
|
||||
style={style || {}}
|
||||
>
|
||||
<div
|
||||
{...{
|
||||
[`data-${DATASET_PARENT_DATA_STOP_TAG}`]: true,
|
||||
[`data-${DATASET_RESOURCE_FOLDER_KEY}`]: uniqId.current,
|
||||
}}
|
||||
ref={scrollWrapper}
|
||||
className={`${createUniqId(
|
||||
RESOURCE_FOLDER_WRAPPER_CLASS,
|
||||
uniqId.current,
|
||||
)} resource-list-drag-and-drop-wrapper resource-list-custom-event-wrapper resource-list-scroll-container`}
|
||||
onMouseDown={onMouseDownInDiv}
|
||||
onMouseUp={onMouseUpInDiv}
|
||||
onMouseMove={onMouseMove}
|
||||
onContextMenu={e => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
onContextMenuCapture={contextMenuCallback}
|
||||
>
|
||||
{renderResourceList.map((resource, i) => {
|
||||
if (resource === CREATE_RESOURCE_ID) {
|
||||
return createNode;
|
||||
}
|
||||
|
||||
if (resource.id === ROOT_KEY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!resource) {
|
||||
return <></>;
|
||||
}
|
||||
const isInEdit =
|
||||
String(resource.id) ===
|
||||
String(createEditResourceContext.editResourceId);
|
||||
const isSelected = String(selected) === String(resource.id);
|
||||
const isTempSelected = !!tempSelectedMapRef.current[resource.id];
|
||||
const preItemTempSelected =
|
||||
resourceList.current[i - 1]?.id !== ROOT_KEY &&
|
||||
!!tempSelectedMapRef.current[resourceList.current[i - 1]?.id];
|
||||
const nextItemTempSelected =
|
||||
!!tempSelectedMapRef.current[resourceList.current[i + 1]?.id];
|
||||
|
||||
const isExpand = !collapsedMapRef.current[resource.id];
|
||||
const highlightItem =
|
||||
!powerBlackMap?.folder && // 不支持文件夹则不需支持拖拽时候的高亮
|
||||
!!dragAndDropContext.highlightItemMap[resource.id];
|
||||
const preHighlightItem =
|
||||
resourceList.current[i - 1]?.id !== ROOT_KEY &&
|
||||
!!dragAndDropContext.highlightItemMap[
|
||||
resourceList.current[i - 1]?.id
|
||||
];
|
||||
const nextHighlightItem =
|
||||
!!dragAndDropContext.highlightItemMap[
|
||||
resourceList.current[i + 1]?.id
|
||||
];
|
||||
|
||||
const extraClassName = [
|
||||
/**
|
||||
* 拖拽过程中的样式
|
||||
*/
|
||||
...(highlightItem
|
||||
? [
|
||||
resource.id !== ROOT_KEY ? 'dragging-hover-class' : '',
|
||||
!preHighlightItem && !nextHighlightItem
|
||||
? 'base-radius-class-single'
|
||||
: '',
|
||||
!preHighlightItem ? 'base-radius-class-first' : '',
|
||||
!nextHighlightItem ? 'base-radius-class-last' : '',
|
||||
]
|
||||
: []),
|
||||
isSelected ? 'item-is-selected' : '',
|
||||
/**
|
||||
* 拖拽过程中的 hover 态优先级 大于 临时选中态的优先级
|
||||
*/
|
||||
...(isTempSelected && !highlightItem
|
||||
? [
|
||||
'item-is-temp-selected',
|
||||
!preItemTempSelected && !nextItemTempSelected
|
||||
? 'base-radius-class-single'
|
||||
: '',
|
||||
!preItemTempSelected ? 'base-radius-class-first' : '',
|
||||
!nextItemTempSelected ? 'base-radius-class-last' : '',
|
||||
]
|
||||
: []),
|
||||
isInEdit ? 'item-is-in-edit' : '',
|
||||
dragAndDropContext.isDragging || isSelected
|
||||
? ''
|
||||
: 'base-item-hover-class',
|
||||
].join(' ');
|
||||
|
||||
return (
|
||||
<div
|
||||
key={resource.id}
|
||||
className={`item-wrapper ${extraClassName}`}
|
||||
{...dragAndDropContext.dataHandler(resource)}
|
||||
>
|
||||
<BaseRender
|
||||
resource={resource}
|
||||
path={resource.path || []}
|
||||
isSelected={isSelected}
|
||||
isTempSelected={isTempSelected}
|
||||
isInEdit={isInEdit}
|
||||
isExpand={isExpand}
|
||||
isOptimismSaving={
|
||||
_useOptimismUI && optimismSavingMap[resource.id]
|
||||
}
|
||||
{...commonProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{emptyRender()}
|
||||
{/* 添加 24px 底部间距,标识加载完全 */}
|
||||
<div style={{ padding: 12 }}></div>
|
||||
</div>
|
||||
</div>
|
||||
{ReactDOM.createPortal(dragPreview, document.body)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export {
|
||||
ResourceFolder,
|
||||
ROOT_KEY,
|
||||
type Props as ResourceFolderProps,
|
||||
type RefType as ResourceFolderRefType,
|
||||
mapResourceTree,
|
||||
};
|
||||
|
||||
export {
|
||||
type ResourceType,
|
||||
type ResourceMapType,
|
||||
type CommonRenderProps,
|
||||
type RightPanelConfigType,
|
||||
type RenderMoreSuffixType,
|
||||
ResourceTypeEnum,
|
||||
type ResourceFolderContextType as ResourceFolderShortCutContextType,
|
||||
type CreateResourcePropType,
|
||||
type IdType,
|
||||
} from './type';
|
||||
|
||||
export {
|
||||
BaseResourceContextMenuBtnType,
|
||||
RESOURCE_FOLDER_CONTEXT_KEY,
|
||||
} from './constant';
|
||||
@@ -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 React from 'react';
|
||||
|
||||
import { type CommonComponentProps, ResourceTypeEnum } from '../type';
|
||||
import { FolderRender } from './folder-render';
|
||||
import { FileRender } from './file-render';
|
||||
|
||||
const BaseRender: React.FC<CommonComponentProps> = ({ ...props }) => {
|
||||
const { resource, path } = props;
|
||||
|
||||
const Component =
|
||||
resource.type === ResourceTypeEnum.Folder ? FolderRender : FileRender;
|
||||
if (!Component) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
key={`base-render-${resource.id}`}
|
||||
{...props}
|
||||
path={[...path, resource.id]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { BaseRender };
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { type CommonComponentProps } from '../../type';
|
||||
import { ITEM_HEIGHT, ItemStatus, TAB_SIZE } from '../../constant';
|
||||
import { NameInput } from './name-input';
|
||||
import { MoreTools } from './more-tools';
|
||||
import { MemoText } from './memo-text';
|
||||
|
||||
const ItemRender = ({
|
||||
resource,
|
||||
path,
|
||||
icon,
|
||||
isSelected,
|
||||
isTempSelected,
|
||||
isInEdit,
|
||||
searchConfig,
|
||||
suffixRender,
|
||||
config,
|
||||
renderMoreSuffix,
|
||||
textRender,
|
||||
isDragging,
|
||||
useOptimismUI,
|
||||
isOptimismSaving,
|
||||
contextMenuCallback,
|
||||
resourceTreeWrapperRef,
|
||||
...props
|
||||
}: CommonComponentProps) => {
|
||||
const { name, status } = resource;
|
||||
|
||||
const optimismUILoading = useMemo(() => {
|
||||
if (isOptimismSaving) {
|
||||
if (typeof useOptimismUI === 'object' && useOptimismUI.loadingRender) {
|
||||
return useOptimismUI.loadingRender();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, [isOptimismSaving]);
|
||||
|
||||
const suffix = useMemo(
|
||||
() =>
|
||||
!isInEdit &&
|
||||
suffixRender?.render?.({
|
||||
resource,
|
||||
isSelected,
|
||||
isTempSelected,
|
||||
}),
|
||||
[isSelected, isInEdit, resource, isTempSelected],
|
||||
);
|
||||
|
||||
const moreTools = useMemo(
|
||||
() =>
|
||||
!isInEdit && renderMoreSuffix ? (
|
||||
<MoreTools
|
||||
resource={resource}
|
||||
contextMenuCallback={contextMenuCallback}
|
||||
resourceTreeWrapperRef={resourceTreeWrapperRef}
|
||||
renderMoreSuffix={renderMoreSuffix}
|
||||
/>
|
||||
) : null,
|
||||
[isInEdit, resource, renderMoreSuffix],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid={`agent-ide.resource-item.${resource.type}.${resource.name}`}
|
||||
key={resource.id}
|
||||
className={'base-item'}
|
||||
style={{
|
||||
justifyContent: 'space-between',
|
||||
height: config?.itemHeight || ITEM_HEIGHT,
|
||||
borderRadius: 4,
|
||||
paddingLeft: (path.length - 1) * (config?.tabSize || TAB_SIZE) - 4,
|
||||
...(status === ItemStatus.Disabled
|
||||
? {
|
||||
fontStyle: 'italic',
|
||||
filter: 'opacity(0.5)',
|
||||
cursor: 'not-allowed',
|
||||
textDecoration: 'line-through',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
overflow: isInEdit ? 'visible' : 'hidden',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{icon ? (
|
||||
<span
|
||||
className={'base-item-icon'}
|
||||
style={{
|
||||
color: 'rgba(6, 7, 9, 0.96)',
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
) : null}
|
||||
{isInEdit ? (
|
||||
<NameInput
|
||||
resource={resource}
|
||||
initValue={name}
|
||||
handleSave={props.handleSave}
|
||||
handleChangeName={props.handleChangeName}
|
||||
errorMsg={props.errorMsg}
|
||||
errorMsgRef={props.errorMsgRef}
|
||||
validateConfig={props.validateConfig}
|
||||
config={config}
|
||||
/>
|
||||
) : (
|
||||
<MemoText
|
||||
isSelected={isSelected}
|
||||
resource={resource}
|
||||
name={name}
|
||||
searchConfig={searchConfig}
|
||||
tooltipSpace={
|
||||
(suffixRender?.width || 0) + (renderMoreSuffix ? 26 : 0)
|
||||
}
|
||||
textRender={textRender}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{optimismUILoading}
|
||||
{suffix}
|
||||
{moreTools}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ItemRender };
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { Typography, Highlight } from '@coze-arch/coze-design';
|
||||
|
||||
import {
|
||||
type CommonRenderProps,
|
||||
// ResourceTypeEnum,
|
||||
// type ResourceStatusType,
|
||||
type ResourceType,
|
||||
} from '../../type';
|
||||
import { COLOR_CONFIG } from '../../constant';
|
||||
|
||||
const { Text: BaseText } = Typography;
|
||||
|
||||
// const ResourceBadge = (props: {
|
||||
// resource: ResourceType;
|
||||
// status?: ResourceStatusType;
|
||||
// }) => {
|
||||
// const { resource, status = {} } = props;
|
||||
// const badgeTextArr: string[] = [];
|
||||
|
||||
// if (status.problem?.number) {
|
||||
// badgeTextArr.push(`${status?.problem?.number}`);
|
||||
// }
|
||||
// if (status.draft) {
|
||||
// badgeTextArr.push('M');
|
||||
// }
|
||||
// const badgeText = badgeTextArr.join(', ');
|
||||
// const level =
|
||||
// status.problem?.status && status.problem.status !== 'normal'
|
||||
// ? status.problem.status
|
||||
// : status.draft
|
||||
// ? 'warning'
|
||||
// : '';
|
||||
|
||||
// return badgeText ? (
|
||||
// <span
|
||||
// style={{
|
||||
// marginRight: 4,
|
||||
// opacity: '0.75',
|
||||
// color:
|
||||
// level === 'error'
|
||||
// ? 'rgba(var(--blockwise-error-color))'
|
||||
// : 'rgba(var(--blockwise-warning-color))',
|
||||
// }}
|
||||
// >
|
||||
// {resource.type !== ResourceTypeEnum.Folder ? (
|
||||
// badgeText
|
||||
// ) : (
|
||||
// <Badge countStyle={{ backgroundColor: 'yellow' }} type="mini" />
|
||||
// )}
|
||||
// </span>
|
||||
// ) : (
|
||||
// <></>
|
||||
// );
|
||||
// };
|
||||
|
||||
const Text = ({
|
||||
name,
|
||||
resource,
|
||||
searchConfig,
|
||||
isSelected,
|
||||
tooltipSpace,
|
||||
textRender,
|
||||
}: {
|
||||
name: string;
|
||||
resource: ResourceType;
|
||||
searchConfig?: {
|
||||
searchKey?: string;
|
||||
highlightStyle?: React.CSSProperties;
|
||||
};
|
||||
isSelected?: boolean;
|
||||
tooltipSpace?: number;
|
||||
textRender?: (v: CommonRenderProps) => React.ReactElement | undefined;
|
||||
}) => {
|
||||
const color = (() => {
|
||||
if (resource.problem?.status === 'error') {
|
||||
return COLOR_CONFIG.textErrorColor;
|
||||
} else if (resource.problem?.status === 'warning') {
|
||||
return COLOR_CONFIG.textWarningColor;
|
||||
} else if (isSelected) {
|
||||
return COLOR_CONFIG.textSelectedColor;
|
||||
}
|
||||
return COLOR_CONFIG.textNormalColor;
|
||||
})();
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
overflow: 'hidden',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
<BaseText
|
||||
style={{ flex: 1 }}
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: {
|
||||
content: `${name}`,
|
||||
style: { wordBreak: 'break-all' },
|
||||
position: 'right',
|
||||
spacing: 8 + (tooltipSpace || 0),
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{textRender ? (
|
||||
textRender({ resource, isSelected })
|
||||
) : (
|
||||
<span
|
||||
style={{
|
||||
color,
|
||||
}}
|
||||
>
|
||||
<Highlight
|
||||
sourceString={name}
|
||||
searchWords={[searchConfig?.searchKey || '']}
|
||||
highlightStyle={{
|
||||
...searchConfig?.highlightStyle,
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--semi-color-primary)',
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</BaseText>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const MemoText = memo(Text, (pre, cur) => {
|
||||
if (
|
||||
pre.name !== cur.name ||
|
||||
pre.searchConfig?.searchKey !== cur.searchConfig?.searchKey ||
|
||||
pre.resource !== cur.resource ||
|
||||
pre.isSelected !== cur.isSelected
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
export { MemoText };
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { IconCozMore } from '@coze-arch/coze-design/icons';
|
||||
import { Button, Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
import { type RenderMoreSuffixType, type ResourceType } from '../../type';
|
||||
import { MORE_TOOLS_CLASS_NAME } from '../../constant';
|
||||
|
||||
const MoreTools = ({
|
||||
resource,
|
||||
contextMenuCallback,
|
||||
resourceTreeWrapperRef,
|
||||
renderMoreSuffix,
|
||||
}: {
|
||||
resource: ResourceType;
|
||||
contextMenuCallback: (e: any, resources?: ResourceType[]) => () => void;
|
||||
resourceTreeWrapperRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||
renderMoreSuffix?: RenderMoreSuffixType;
|
||||
}) => {
|
||||
const handleClick = e => {
|
||||
/**
|
||||
* 这里将 event 的 currentTarget 设置成树组件的 wrapper 元素,保证 contextMenu 的 matchItems 方法可以正常遍历。
|
||||
*/
|
||||
e.currentTarget = resourceTreeWrapperRef.current;
|
||||
contextMenuCallback(e, [resource]);
|
||||
};
|
||||
|
||||
const btnElm = (
|
||||
<Button
|
||||
data-testid={`agent-ide.resource-item.${resource.type}.${resource.name}.more-tools`}
|
||||
{...(typeof renderMoreSuffix === 'object' && renderMoreSuffix?.extraProps
|
||||
? renderMoreSuffix?.extraProps
|
||||
: {})}
|
||||
className={`base-item-more-hover-display-class ${MORE_TOOLS_CLASS_NAME} base-item-more-btn ${
|
||||
typeof renderMoreSuffix === 'object' && renderMoreSuffix.className
|
||||
? renderMoreSuffix.className
|
||||
: ''
|
||||
}`}
|
||||
style={
|
||||
typeof renderMoreSuffix === 'object' && renderMoreSuffix.style
|
||||
? renderMoreSuffix.style
|
||||
: {}
|
||||
}
|
||||
icon={<IconCozMore />}
|
||||
theme="borderless"
|
||||
size="small"
|
||||
onMouseUp={handleClick}
|
||||
/>
|
||||
);
|
||||
|
||||
if (typeof renderMoreSuffix === 'object' && renderMoreSuffix.render) {
|
||||
return renderMoreSuffix.render({
|
||||
onActive: handleClick,
|
||||
baseBtn: btnElm,
|
||||
resource,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof renderMoreSuffix === 'object' && renderMoreSuffix.tooltip) {
|
||||
if (typeof renderMoreSuffix.tooltip === 'string') {
|
||||
return <Tooltip content={renderMoreSuffix.tooltip}>{btnElm}</Tooltip>;
|
||||
}
|
||||
return <Tooltip {...renderMoreSuffix.tooltip}>{btnElm}</Tooltip>;
|
||||
}
|
||||
|
||||
return btnElm;
|
||||
};
|
||||
|
||||
export { MoreTools };
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Input } from '@coze-arch/coze-design';
|
||||
|
||||
import { type CommonComponentProps } from '../../type';
|
||||
import { MOUSEUP_IGNORE_CLASS_NAME } from '../../constant';
|
||||
|
||||
const DATASET_PARENT_DATA_KEY_ID = 'name_input_wrapper';
|
||||
|
||||
const isClickOutside = (elm, deep = 0) => {
|
||||
if (!elm || deep > 10) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (elm.dataset?.[DATASET_PARENT_DATA_KEY_ID] !== undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isClickOutside(elm.parentElement, deep + 1);
|
||||
};
|
||||
|
||||
const NameInput = ({
|
||||
resource,
|
||||
initValue,
|
||||
handleSave: onSave,
|
||||
handleChangeName,
|
||||
errorMsg,
|
||||
errorMsgRef,
|
||||
validateConfig,
|
||||
config,
|
||||
}: { initValue: string } & Pick<
|
||||
CommonComponentProps,
|
||||
| 'resource'
|
||||
| 'handleSave'
|
||||
| 'handleChangeName'
|
||||
| 'errorMsg'
|
||||
| 'errorMsgRef'
|
||||
| 'validateConfig'
|
||||
| 'config'
|
||||
>) => {
|
||||
const [value, setValue] = useState(initValue);
|
||||
|
||||
const ref = useRef(null);
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(errorMsgRef?.current ? initValue : undefined);
|
||||
};
|
||||
|
||||
const loaded = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
loaded.current = true;
|
||||
}, 0);
|
||||
|
||||
const handleBlur = (e: MouseEvent) => {
|
||||
const clickOutside = isClickOutside(e.target);
|
||||
if (clickOutside) {
|
||||
handleSave();
|
||||
}
|
||||
};
|
||||
window.addEventListener('mousedown', handleBlur, true);
|
||||
return () => {
|
||||
window.removeEventListener('mousedown', handleBlur, true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...{
|
||||
[`data-${DATASET_PARENT_DATA_KEY_ID}`]: true,
|
||||
}}
|
||||
className={`base-item-name-input ${MOUSEUP_IGNORE_CLASS_NAME} ${
|
||||
errorMsg ? 'base-item-name-input-error' : ''
|
||||
}`}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Input
|
||||
className={config?.input?.className || ''}
|
||||
style={{ padding: 0, ...config?.input?.style }}
|
||||
ref={ref}
|
||||
placeholder={config?.input?.placeholder}
|
||||
onMouseDown={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.code === 'Escape') {
|
||||
onSave('');
|
||||
}
|
||||
}}
|
||||
onEnterPress={e => {
|
||||
if (!loaded.current) {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
}}
|
||||
onChange={v => {
|
||||
setValue(v);
|
||||
handleChangeName(v);
|
||||
}}
|
||||
value={value}
|
||||
autoFocus
|
||||
/>
|
||||
{errorMsg ? (
|
||||
validateConfig?.errorMsgRender ? (
|
||||
validateConfig?.errorMsgRender?.(errorMsg, resource)
|
||||
) : (
|
||||
<div
|
||||
style={validateConfig?.errorMsgStyle || {}}
|
||||
className={`base-item-name-input-error-msg-absolute ${validateConfig?.errorMsgClassName}`}
|
||||
>
|
||||
{errorMsg}
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export { NameInput };
|
||||
@@ -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 React from 'react';
|
||||
|
||||
import { type CommonComponentProps } from '../type';
|
||||
import { ItemRender } from './components/item-render';
|
||||
|
||||
const FileRender: React.FC<CommonComponentProps> = ({
|
||||
resource,
|
||||
path,
|
||||
...props
|
||||
}) => {
|
||||
const { isDragging, draggingError, isSelected, isTempSelected, iconRender } =
|
||||
props;
|
||||
|
||||
const cursor = (() => {
|
||||
if (draggingError) {
|
||||
return 'not-allowed';
|
||||
} else if (isDragging) {
|
||||
return 'grabbing';
|
||||
}
|
||||
return 'pointer';
|
||||
})();
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`file-${resource.id}`}
|
||||
style={{
|
||||
cursor,
|
||||
}}
|
||||
>
|
||||
<ItemRender
|
||||
resource={resource}
|
||||
path={path}
|
||||
icon={
|
||||
resource?.type
|
||||
? iconRender?.({
|
||||
resource,
|
||||
isSelected,
|
||||
isTempSelected,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { FileRender };
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { type CommonComponentProps } from '../type';
|
||||
import { ItemRender } from './components/item-render';
|
||||
|
||||
const FolderRender: React.FC<CommonComponentProps> = ({
|
||||
resource,
|
||||
path,
|
||||
style,
|
||||
...props
|
||||
}) => {
|
||||
const { id } = resource;
|
||||
|
||||
const {
|
||||
iconRender,
|
||||
isSelected,
|
||||
isTempSelected,
|
||||
isExpand,
|
||||
draggingError,
|
||||
isDragging,
|
||||
} = props;
|
||||
|
||||
const cursor = (() => {
|
||||
if (draggingError) {
|
||||
return 'not-allowed';
|
||||
} else if (isDragging) {
|
||||
return 'grabbing';
|
||||
}
|
||||
return 'default';
|
||||
})();
|
||||
|
||||
const isRoot = path.length === 1;
|
||||
|
||||
// const { parentId: createResourceParentId, type: createResourceType } =
|
||||
// createResourceInfo || {};
|
||||
|
||||
// const { renderCreateNode, appendIndex, itemElm } = (() => {
|
||||
// if (String(createResourceParentId) === String(id)) {
|
||||
// return {
|
||||
// renderCreateNode: true,
|
||||
// appendIndex:
|
||||
// createResourceType === ResourceTypeEnum.Folder
|
||||
// ? 0
|
||||
// : children?.findIndex(
|
||||
// child => child.type !== ResourceTypeEnum.Folder,
|
||||
// ),
|
||||
// itemElm: (
|
||||
// <BaseRender
|
||||
// resource={{
|
||||
// id: CREATE_RESOURCE_ID,
|
||||
// name: '',
|
||||
// type: createResourceType,
|
||||
// }}
|
||||
// path={path}
|
||||
// />
|
||||
// ),
|
||||
// };
|
||||
// }
|
||||
|
||||
// return {
|
||||
// renderCreateNode: false,
|
||||
// appendIndex: -1,
|
||||
// itemElm: null,
|
||||
// };
|
||||
// })();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
key={`folder-${id}`}
|
||||
style={{
|
||||
...(style || {}),
|
||||
cursor,
|
||||
}}
|
||||
>
|
||||
{!isRoot && (
|
||||
<ItemRender
|
||||
resource={resource}
|
||||
path={path}
|
||||
icon={
|
||||
iconRender
|
||||
? iconRender({
|
||||
resource,
|
||||
isSelected,
|
||||
isTempSelected,
|
||||
isExpand,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { FolderRender };
|
||||
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type React from 'react';
|
||||
import { type MutableRefObject } from 'react';
|
||||
|
||||
import {
|
||||
type TooltipProps,
|
||||
type ButtonProps,
|
||||
type SemiButton,
|
||||
} from '@coze-arch/coze-design';
|
||||
import { type URI } from '@coze-project-ide/client';
|
||||
|
||||
import { type BaseResourceContextMenuBtnType } from './hooks/use-right-click-panel/constant';
|
||||
import { type CREATE_RESOURCE_ID } from './hooks/use-create-edit-resource';
|
||||
|
||||
export type IdType = string;
|
||||
|
||||
export enum ResourceTypeEnum {
|
||||
Folder = 'folder',
|
||||
}
|
||||
|
||||
export type ItemType = ResourceTypeEnum | string;
|
||||
|
||||
export interface ResourceStatusType {
|
||||
// 是否是草稿态
|
||||
draft?: boolean;
|
||||
// 错误内容 & 个数
|
||||
problem?: {
|
||||
status?: 'normal' | 'error' | 'warning';
|
||||
number?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ResourceType {
|
||||
id: IdType;
|
||||
type?: ItemType;
|
||||
name: string;
|
||||
description?: string;
|
||||
children?: ResourceType[];
|
||||
path?: string[];
|
||||
maxDeep?: number;
|
||||
[T: string]: any;
|
||||
}
|
||||
|
||||
export type ResourceMapType<
|
||||
T = {
|
||||
[T: string]: any;
|
||||
},
|
||||
> = Record<IdType, ResourceType & ResourceStatusType & T>;
|
||||
|
||||
export interface CustomResourceConfigType extends ResourceStatusType {
|
||||
[T: string]: any;
|
||||
}
|
||||
|
||||
export type CustomResourceConfigMapType = Record<
|
||||
IdType,
|
||||
CustomResourceConfigType
|
||||
>;
|
||||
|
||||
export type RenderMoreSuffixType =
|
||||
| boolean
|
||||
| {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
extraProps?: ButtonProps & React.RefAttributes<SemiButton>;
|
||||
render?: (
|
||||
v: {
|
||||
onActive: (e: React.MouseEventHandler<HTMLButtonElement>) => void;
|
||||
baseBtn: React.ReactElement;
|
||||
} & CommonRenderProps,
|
||||
) => React.ReactElement;
|
||||
tooltip?: string | TooltipProps;
|
||||
};
|
||||
|
||||
export interface CommonComponentProps {
|
||||
resource: ResourceType;
|
||||
path: Array<IdType>;
|
||||
style?: React.CSSProperties;
|
||||
isTempSelected?: boolean;
|
||||
isSelected?: boolean;
|
||||
isInEdit?: boolean;
|
||||
isDraggingHover?: boolean;
|
||||
icon?: React.ReactElement;
|
||||
searchConfig?: {
|
||||
searchKey?: string;
|
||||
highlightStyle?: React.CSSProperties;
|
||||
};
|
||||
suffixRender?: {
|
||||
width: number;
|
||||
render: (v: CommonRenderProps) => React.ReactElement | undefined;
|
||||
};
|
||||
config?: ConfigType;
|
||||
renderMoreSuffix?: RenderMoreSuffixType;
|
||||
textRender?: (v: CommonRenderProps) => React.ReactElement | undefined;
|
||||
isDragging: boolean;
|
||||
isExpand?: boolean;
|
||||
/**
|
||||
* 是否处于乐观 ui 的保存阶段
|
||||
*/
|
||||
isOptimismSaving?: boolean;
|
||||
useOptimismUI?:
|
||||
| boolean
|
||||
| {
|
||||
loadingRender?: () => React.ReactElement;
|
||||
};
|
||||
draggingError?: string;
|
||||
contextMenuCallback: (e: any, resources?: ResourceType[]) => () => void;
|
||||
resourceTreeWrapperRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||
iconRender?: (v: CommonRenderProps) => React.ReactElement | undefined;
|
||||
currentHoverItem: ResourceType | null;
|
||||
validateConfig?: ValidatorConfigType;
|
||||
errorMsg?: string;
|
||||
errorMsgRef?: MutableRefObject<string>;
|
||||
editResourceId: IdType | undefined;
|
||||
handleChangeName: (v: string) => void;
|
||||
handleSave: (v?: string) => void;
|
||||
}
|
||||
|
||||
export interface DragPropType {
|
||||
resourceList?: ResourceType[];
|
||||
toId?: IdType;
|
||||
errorMsg?: string;
|
||||
}
|
||||
|
||||
export interface ChangeNameType {
|
||||
id: IdType;
|
||||
name: string;
|
||||
type?: ItemType;
|
||||
path?: IdType[];
|
||||
resource?: ResourceType;
|
||||
}
|
||||
export interface DragAndDropType {
|
||||
isDragging: boolean;
|
||||
draggingError?: string;
|
||||
isFocus: boolean;
|
||||
tempSelectedMapRef: React.MutableRefObject<Record<string, ResourceType>>;
|
||||
dataHandler: (resource: ResourceType) => Record<string, IdType>;
|
||||
currentHoverItem: ResourceType | null;
|
||||
highlightItemMap: ResourceMapType;
|
||||
}
|
||||
|
||||
export interface EditItemType {
|
||||
isInEditMode: boolean;
|
||||
errorMsg?: string;
|
||||
errorMsgRef?: MutableRefObject<string>;
|
||||
editResourceId: IdType | undefined;
|
||||
createResourceInfo: {
|
||||
parentId: IdType;
|
||||
type: ItemType;
|
||||
index: number;
|
||||
} | null;
|
||||
handleChangeName: (v: string) => void;
|
||||
handleSave: (v?: string) => void;
|
||||
}
|
||||
|
||||
export interface CommonRenderProps {
|
||||
resource: ResourceType;
|
||||
isSelected?: boolean;
|
||||
isTempSelected?: boolean;
|
||||
isExpand?: boolean /** 只有 resource.type === folder 的时候才会有该字段 */;
|
||||
}
|
||||
|
||||
export interface ContextType {
|
||||
selected?: IdType;
|
||||
disabled?: boolean;
|
||||
collapsedMapRef: React.MutableRefObject<Record<IdType, boolean> | null>;
|
||||
setCollapsed: (id: IdType, v: boolean) => void;
|
||||
searchConfig?: {
|
||||
searchKey?: string;
|
||||
highlightStyle?: React.CSSProperties;
|
||||
};
|
||||
resourceTreeWrapperRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||
resourceMap: React.MutableRefObject<ResourceMapType>;
|
||||
dragAndDropContext: DragAndDropType;
|
||||
createEditResourceContext: EditItemType;
|
||||
renderMoreSuffix?:
|
||||
| boolean
|
||||
| {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
render?: () => React.ReactElement;
|
||||
};
|
||||
validateConfig?: ValidatorConfigType;
|
||||
contextMenuCallback: (e: any, resources?: ResourceType[]) => () => void;
|
||||
iconRender?: (v: CommonRenderProps) => React.ReactElement | undefined;
|
||||
suffixRender?: {
|
||||
width: number;
|
||||
render: (v: CommonRenderProps) => React.ReactElement | undefined;
|
||||
};
|
||||
textRender?: (v: CommonRenderProps) => React.ReactElement | undefined;
|
||||
config?: ConfigType;
|
||||
optimismSavingMap: Record<IdType, true>;
|
||||
useOptimismUI?:
|
||||
| boolean
|
||||
| {
|
||||
loadingRender?: () => React.ReactElement;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CustomValidatorPropsType {
|
||||
type: 'create' | 'edit';
|
||||
label: string;
|
||||
parentPath: string[];
|
||||
resourceTree: ResourceType;
|
||||
id: IdType | typeof CREATE_RESOURCE_ID;
|
||||
}
|
||||
|
||||
export interface ValidatorConfigType {
|
||||
/**
|
||||
* @param label 当前输入框的文本
|
||||
* @param parentPath 从 root 开始到 父级的路径
|
||||
* @param resourceTree 当前的资源树
|
||||
* @returns Promise<string>
|
||||
*/
|
||||
/** */
|
||||
customValidator?: (
|
||||
data: CustomValidatorPropsType,
|
||||
) => string | Promise<string>;
|
||||
|
||||
/**
|
||||
* 默认: absolute;
|
||||
* absolute: 不占用树的文档流,错误文案覆盖上去;
|
||||
*/
|
||||
errorMsgPosition?: 'absolute';
|
||||
errorMsgStyle?: React.CSSProperties;
|
||||
errorMsgClassName?: string;
|
||||
errorMsgRender?: (msg: string, resource: ResourceType) => React.ReactElement;
|
||||
}
|
||||
|
||||
export interface RightOptionsType {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: React.ReactElement;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
disabledMsg?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateResourcePropType {
|
||||
type: ItemType;
|
||||
parentId: IdType;
|
||||
path: IdType[];
|
||||
name: string;
|
||||
schema?: any;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface CommandOption {
|
||||
id: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
disabledMsg?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
/** 内置的顶部工具栏的按钮组 */
|
||||
export enum BuildInToolOperations {
|
||||
/** 搜索过滤按钮 */
|
||||
CreateFile = 'CreateFile',
|
||||
CreateFolder = 'CreateFolder',
|
||||
/** 展开收起文件夹 */
|
||||
ExpandFolder = 'ExpandFolder',
|
||||
}
|
||||
|
||||
export type RightPanelConfigType =
|
||||
| {
|
||||
// command id
|
||||
id: BaseResourceContextMenuBtnType | string;
|
||||
label?: string;
|
||||
shortLabel?: string;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
|
||||
/**
|
||||
* 这里有两类 命令。
|
||||
* 1. 不在外部插件中提前注册好的,需要组件内部动态注册的。往往是该组件树定制化的菜单项。比如 校验并运行该资源
|
||||
* 那这个 execute 字段是一个必填项
|
||||
* 2. 在外部插件中提前注册好的,往往是需要配合快捷键一起使用的,并且具有一定的普适性的命令。比如创建资源,复制资源等。
|
||||
* 那这个 execute 不需要配置,的回调在 plugin 中注册的地方触发
|
||||
* 上下文可以通过 RESOURCE_FOLDER_CONTEXT_KEY 这个 key 从 ide 上下文中获取
|
||||
*/
|
||||
execute?: () => void;
|
||||
}
|
||||
| {
|
||||
// 分割线
|
||||
type: 'separator';
|
||||
};
|
||||
|
||||
export interface ConfigType {
|
||||
/**
|
||||
* 每个资源的高度
|
||||
*/
|
||||
itemHeight?: number;
|
||||
/**
|
||||
* 半个 icon 的宽度,用于左侧折线的计算。
|
||||
*/
|
||||
halfIconWidth?: number;
|
||||
/**
|
||||
* 每个文件夹下缩进的宽度
|
||||
*/
|
||||
tabSize?: number;
|
||||
|
||||
/**
|
||||
* 文件夹下钻最大的深度
|
||||
*/
|
||||
maxDeep?: number;
|
||||
|
||||
/**
|
||||
* 资源 name 输入框配置
|
||||
*/
|
||||
input?: {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 拖拽过程中的预览框配置项
|
||||
*/
|
||||
dragUi?: {
|
||||
disable?: boolean;
|
||||
wrapperClassName?: string;
|
||||
|
||||
/**
|
||||
* 特别说明: 可以通过配置这里的 top 和 left 来设置相对鼠标的偏移量
|
||||
*/
|
||||
wrapperStyle?: React.CSSProperties;
|
||||
};
|
||||
|
||||
resourceUriHandler?: (resource: ResourceType) => URI | null;
|
||||
}
|
||||
|
||||
export interface ResourceFolderContextType {
|
||||
id?: string; // folder 组件唯一的 id
|
||||
currentSelectedId?: IdType; // 当前选中的资源 id
|
||||
tempSelectedMap?: Record<string, ResourceType>; // 当前临时选中的资源 map
|
||||
onEnter?: () => void;
|
||||
onDelete?: () => void;
|
||||
onCreateFolder?: () => void;
|
||||
onCreateResource?: (type?: ResourceTypeEnum) => void;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
type Any = any;
|
||||
|
||||
type BaseEventParamsType = Record<string, Any>;
|
||||
|
||||
export class BaseEvent<T extends string, O extends BaseEventParamsType> {
|
||||
protected deps: Record<T, ((data?: Any) => Any)[]>;
|
||||
|
||||
constructor() {
|
||||
this.deps = {} as any;
|
||||
}
|
||||
|
||||
on(name: T, func: (data?: O[T]) => Any): void {
|
||||
if (this.deps[name]) {
|
||||
this.deps[name].push(func);
|
||||
} else {
|
||||
this.deps[name] = [func];
|
||||
}
|
||||
}
|
||||
|
||||
once(name: T, func: (data?: O[T]) => Any): void {
|
||||
const f = (data: Any) => {
|
||||
func(data);
|
||||
this.un(name, f);
|
||||
};
|
||||
if (this.deps[name]) {
|
||||
this.deps[name].push(f);
|
||||
} else {
|
||||
this.deps[name] = [f];
|
||||
}
|
||||
}
|
||||
|
||||
un(name?: T, func?: (data?: O[T]) => Any): void {
|
||||
if (!name) {
|
||||
this.deps = {} as any;
|
||||
return;
|
||||
}
|
||||
if (func) {
|
||||
this.deps[name] = this.deps[name].filter(fn => fn !== func);
|
||||
} else {
|
||||
delete this.deps[name];
|
||||
}
|
||||
}
|
||||
|
||||
emit(name: T, data?: O[T]): void {
|
||||
if (this.deps[name]) {
|
||||
this.deps[name].forEach(fn => fn(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* 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 IdType,
|
||||
type ResourceType,
|
||||
type ResourceMapType,
|
||||
ResourceTypeEnum,
|
||||
} from '../type';
|
||||
import { ROOT_KEY, ROOT_NODE } from '../constant';
|
||||
|
||||
export const RESOURCE_FOLDER_COMMAND_PREFIX = 'resource-folder-command-prefix';
|
||||
|
||||
export const createUniqId = (key: string, suffix: string) =>
|
||||
`${RESOURCE_FOLDER_COMMAND_PREFIX}_${key}_${suffix}`;
|
||||
|
||||
export const findResourceByPath = (
|
||||
resourceTree: ResourceType,
|
||||
path: IdType[],
|
||||
): ResourceType | undefined => {
|
||||
let currentIndex = 0;
|
||||
let currentResource: undefined | ResourceType = resourceTree;
|
||||
|
||||
if (String(currentResource.id) !== String(path[currentIndex])) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
currentIndex += 1;
|
||||
|
||||
while (currentIndex < path.length && currentResource) {
|
||||
currentResource = (currentResource.children || []).find(
|
||||
child => String(child.id) === String(path[currentIndex]),
|
||||
);
|
||||
currentIndex += 1;
|
||||
}
|
||||
|
||||
return currentResource;
|
||||
};
|
||||
|
||||
export const getParentResource = (
|
||||
resourceTree: ResourceType,
|
||||
targetResource: ResourceType,
|
||||
) => getResourceById(resourceTree, targetResource.id)?.parent;
|
||||
|
||||
export const getResourceById = (
|
||||
resourceTree: ResourceType,
|
||||
id: IdType,
|
||||
): {
|
||||
resource: ResourceType | null;
|
||||
parent: ResourceType | null;
|
||||
path: IdType[];
|
||||
} | null => {
|
||||
let parent: ResourceType | null = null;
|
||||
|
||||
let result: {
|
||||
resource: ResourceType | null;
|
||||
parent: ResourceType | null;
|
||||
path: IdType[];
|
||||
} | null = null;
|
||||
const dfs = (resource: ResourceType, _path: IdType[]) => {
|
||||
if (result) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (String(resource.id) === String(id)) {
|
||||
result = {
|
||||
resource,
|
||||
parent,
|
||||
path: _path,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (resource.children) {
|
||||
const currentParent = parent;
|
||||
parent = resource;
|
||||
resource.children.forEach(child => {
|
||||
dfs(child, [..._path, child.id]);
|
||||
});
|
||||
parent = currentParent;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
dfs(resourceTree, [resourceTree.id]);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 用 shift 修饰键的时候, 从 from 的到 to 的中间全部选中,包括下钻文件。 dfs
|
||||
*/
|
||||
export const getResourceListFromIdToId = ({
|
||||
resourceTree,
|
||||
from,
|
||||
to,
|
||||
options,
|
||||
}: {
|
||||
resourceTree: ResourceType;
|
||||
from: IdType;
|
||||
to: IdType;
|
||||
options?: { collapsedMap?: Record<string, boolean> };
|
||||
}): ResourceType[] | IdType[] => {
|
||||
const { collapsedMap } = options || {};
|
||||
const result: Array<IdType> = [];
|
||||
let isStart = false;
|
||||
|
||||
const dfs = (resource: ResourceType, _path: IdType[]) => {
|
||||
const shot =
|
||||
String(resource.id) === String(from) ||
|
||||
String(resource.id) === String(to);
|
||||
if (!isStart && shot) {
|
||||
isStart = true;
|
||||
result.push(resource.id);
|
||||
} else if (isStart) {
|
||||
result.push(resource.id);
|
||||
if (shot) {
|
||||
isStart = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (resource.children && !collapsedMap?.[resource.id]) {
|
||||
resource.children.forEach(child => {
|
||||
dfs(child, [..._path, child.id]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
dfs(resourceTree, [resourceTree.id]);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getAllResourcesInFolder = (
|
||||
resourceTree: ResourceType | ResourceType[],
|
||||
) => {
|
||||
const folders = resourceTree instanceof Array ? resourceTree : [resourceTree];
|
||||
|
||||
const result: ResourceType[] = [];
|
||||
|
||||
const dfs = (resource: ResourceType) => {
|
||||
if (!resource) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resource.children) {
|
||||
resource.children.forEach(child => {
|
||||
dfs(child);
|
||||
});
|
||||
}
|
||||
|
||||
if (resource.type !== 'folder') {
|
||||
result.push(resource);
|
||||
}
|
||||
};
|
||||
|
||||
folders.forEach(folder => {
|
||||
dfs(folder);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const sortResourceList = (
|
||||
resourceList: ResourceType[],
|
||||
): ResourceType[] => {
|
||||
const sortFunc = (a, b) => {
|
||||
const leftName = a.name?.toLowerCase?.() || '';
|
||||
const rightName = b.name?.toLowerCase?.() || '';
|
||||
if (leftName < rightName) {
|
||||
return -1;
|
||||
}
|
||||
if (leftName > rightName) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const folderList = resourceList
|
||||
.filter(source => source.type === ResourceTypeEnum.Folder)
|
||||
.sort((a, b) => sortFunc(a, b));
|
||||
const sourceList = resourceList
|
||||
.filter(source => source.type !== ResourceTypeEnum.Folder)
|
||||
.sort((a, b) => sortFunc(a, b));
|
||||
|
||||
return folderList.concat(sourceList) as ResourceType[];
|
||||
};
|
||||
|
||||
// 后续要优化算法的话 ,得在树打平的算法中,记录每个文件夹的高度,这样在 change 的时候不需要重复计算。 packages/api-builder/base/src/utils/resource-folder/index.ts mapResourceTree
|
||||
export const calcOffsetTopByCollapsedMap = (props: {
|
||||
selectedId: string;
|
||||
resourceTree: ResourceType;
|
||||
collapsedMap: Record<IdType, boolean>;
|
||||
itemHeight: number;
|
||||
}) => {
|
||||
const { selectedId, resourceTree, collapsedMap, itemHeight } = props;
|
||||
|
||||
let num = -1; // 因为从 root 开始, root 不展示,所以从 -1 开始算
|
||||
let finish = false;
|
||||
|
||||
const dfs = (resource: ResourceType) => {
|
||||
if (!resource || finish) {
|
||||
return;
|
||||
}
|
||||
|
||||
num += 1;
|
||||
|
||||
if (selectedId === resource.id) {
|
||||
finish = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (resource.children && !collapsedMap[resource.id]) {
|
||||
resource.children.forEach(child => {
|
||||
dfs(child);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
dfs(resourceTree);
|
||||
|
||||
return (num - 1) * itemHeight;
|
||||
};
|
||||
|
||||
export const travelResource = (
|
||||
resource: ResourceType,
|
||||
cb: (item: ResourceType) => boolean,
|
||||
) => {
|
||||
if (resource) {
|
||||
const shouldContinue = cb(resource);
|
||||
|
||||
if (!shouldContinue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resource.children) {
|
||||
resource.children.forEach(child => {
|
||||
travelResource(child, cb);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getResourceTravelIds = (ctx: {
|
||||
resource: ResourceType;
|
||||
resourceMap: Record<string, ResourceType>;
|
||||
collapsedMap: Record<string, boolean>;
|
||||
}): string[] => {
|
||||
const { resource, resourceMap, collapsedMap } = ctx;
|
||||
|
||||
const ids: string[] = [];
|
||||
|
||||
travelResource(resource, item => {
|
||||
const info = resourceMap[item.id];
|
||||
|
||||
// 被删除的资源、文件夹不展示
|
||||
if (!info || info.status === 'deprecated') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 折叠的文件夹折叠后,不遍历只节点
|
||||
if (info.type === ResourceTypeEnum.Folder && collapsedMap[info.id]) {
|
||||
ids.push(item.id);
|
||||
return false;
|
||||
}
|
||||
ids.push(item.id);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return ids.filter(id => id !== ROOT_KEY);
|
||||
};
|
||||
|
||||
export const findLastResource = (ctx: {
|
||||
id: string;
|
||||
resource: ResourceType;
|
||||
resourceMap: Record<string, ResourceType>;
|
||||
collapsedMap: Record<string, boolean>;
|
||||
}) => {
|
||||
const { resource, resourceMap, collapsedMap, id } = ctx;
|
||||
|
||||
const ids = getResourceTravelIds({
|
||||
resource,
|
||||
resourceMap,
|
||||
collapsedMap,
|
||||
});
|
||||
|
||||
const index = ids.findIndex(item => item === id);
|
||||
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const finalIndex = (index - 1 + ids.length) % ids.length;
|
||||
|
||||
return {
|
||||
id: ids[finalIndex],
|
||||
info: resourceMap[ids[finalIndex]],
|
||||
};
|
||||
};
|
||||
|
||||
export const findNextResource = (ctx: {
|
||||
id: string;
|
||||
resource: ResourceType;
|
||||
resourceMap: Record<string, ResourceType>;
|
||||
collapsedMap: Record<string, boolean>;
|
||||
}) => {
|
||||
const { resource, resourceMap, collapsedMap, id } = ctx;
|
||||
|
||||
const ids = getResourceTravelIds({
|
||||
resource,
|
||||
resourceMap,
|
||||
collapsedMap,
|
||||
});
|
||||
|
||||
const index = ids.findIndex(item => item === id);
|
||||
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const final = (index + 1) % ids.length;
|
||||
|
||||
return {
|
||||
id: ids[final],
|
||||
info: resourceMap[ids[final]],
|
||||
};
|
||||
};
|
||||
|
||||
export const validateSameNameInFolder = ({
|
||||
folder,
|
||||
editResource,
|
||||
}: {
|
||||
folder: ResourceType;
|
||||
editResource: ResourceType;
|
||||
}): string => {
|
||||
if (!folder || !editResource) {
|
||||
return '';
|
||||
}
|
||||
const children = (folder.children || []).filter(
|
||||
child => child.id !== editResource.id,
|
||||
);
|
||||
|
||||
const hasSameName = children.some(child => child.name === editResource.name);
|
||||
|
||||
return hasSameName
|
||||
? `有一个文件或文件夹 ${editResource.name} 已经存在在当前位置,请使用一个不同的名称`
|
||||
: '';
|
||||
};
|
||||
|
||||
export const mapResourceTree = (resourceTree): ResourceMapType => {
|
||||
if (!resourceTree) {
|
||||
return {};
|
||||
}
|
||||
const fullTree = {
|
||||
...ROOT_NODE,
|
||||
children: resourceTree instanceof Array ? resourceTree : [resourceTree],
|
||||
};
|
||||
|
||||
const result: ResourceMapType = {};
|
||||
|
||||
const dfs = (
|
||||
resource,
|
||||
path: string[],
|
||||
): { maxDeep: number; editDraft?: boolean } => {
|
||||
if (!resource) {
|
||||
return { maxDeep: path.length - 1 };
|
||||
}
|
||||
|
||||
// 文件夹要加一,因为能加文件
|
||||
let maxDeep = path.length + (resource.type === 'folder' ? 1 : 0);
|
||||
|
||||
// 当前资源是否处于提交状态
|
||||
let editDraft = resource.edit_status === 'draft';
|
||||
|
||||
if (resource.children) {
|
||||
resource.children.forEach(child => {
|
||||
const { maxDeep: deep, editDraft: status } = dfs(child, [
|
||||
...path,
|
||||
child.id,
|
||||
]);
|
||||
maxDeep = Math.max(maxDeep, deep);
|
||||
|
||||
// 文件夹 editDraft 跟随草稿走,只要内部有一个为草稿,本文件夹也为草稿
|
||||
editDraft = !!(editDraft || status);
|
||||
});
|
||||
}
|
||||
|
||||
result[String(resource.id)] = {
|
||||
...resource,
|
||||
path,
|
||||
maxDeep: maxDeep - path.length,
|
||||
/**
|
||||
* 随业务放开
|
||||
*/
|
||||
// draft: editDraft,
|
||||
// problem: {
|
||||
// status: 'warning',
|
||||
// number: 12,
|
||||
// },
|
||||
};
|
||||
|
||||
return { maxDeep, editDraft };
|
||||
};
|
||||
|
||||
dfs(fullTree, [fullTree.id]);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const combineExtraObject = (
|
||||
mainObj: Record<IdType, any>,
|
||||
extraMap: Record<IdType, any>,
|
||||
) =>
|
||||
Object.keys(mainObj).reduce(
|
||||
(pre, cur) => ({
|
||||
...pre,
|
||||
[cur]: {
|
||||
...mainObj[cur],
|
||||
...extraMap[cur],
|
||||
},
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
export const flatTree = (
|
||||
tree: ResourceType,
|
||||
map: ResourceMapType,
|
||||
collapsedMap: Record<string, boolean>,
|
||||
) => {
|
||||
const result: ResourceType[] = [];
|
||||
|
||||
const dfs = (resource: ResourceType) => {
|
||||
result.push(map[resource.id]);
|
||||
|
||||
if (resource.children && !collapsedMap[resource.id]) {
|
||||
resource.children.forEach(child => {
|
||||
dfs(child);
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
dfs(tree);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算当前文件夹下,新建的资源所处的位置。
|
||||
* 文件夹:在当前文件夹下顶部
|
||||
* 资源:在当前文件夹下的文件夹末尾,所有资源顶部
|
||||
*/
|
||||
export const getCreateResourceIndex = ({
|
||||
resourceList,
|
||||
parentId,
|
||||
type,
|
||||
}: {
|
||||
resourceList: ResourceType[];
|
||||
parentId: string;
|
||||
type: ResourceTypeEnum | string;
|
||||
}) => {
|
||||
let i = 0;
|
||||
let inFolder = false;
|
||||
let parentPath: string[] = [];
|
||||
while (i < resourceList.length) {
|
||||
const resource = resourceList[i];
|
||||
|
||||
if (inFolder && (resource.path?.length || 0) <= parentPath.length) {
|
||||
return i;
|
||||
}
|
||||
if (
|
||||
inFolder &&
|
||||
resource.type !== ResourceTypeEnum.Folder &&
|
||||
(resource.path?.length || 0) - 1 === parentPath.length
|
||||
) {
|
||||
return i;
|
||||
}
|
||||
|
||||
if (resource.id === parentId) {
|
||||
inFolder = true;
|
||||
parentPath = resource.path || [];
|
||||
if (type === ResourceTypeEnum.Folder) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
return i;
|
||||
};
|
||||
|
||||
export function baseValidateNames(props: { label: string; nameTitle: string }) {
|
||||
const { label, nameTitle } = props;
|
||||
|
||||
const simple = true;
|
||||
|
||||
// 设定默认值,避免 propExtra 只传入一个配置
|
||||
|
||||
// 检测 name 是否空
|
||||
if (!label) {
|
||||
return simple ? 'Empty Key' : `${nameTitle} name can not be empty`;
|
||||
}
|
||||
|
||||
if (label.length > 64) {
|
||||
return simple ? 'Length exceeds' : `${nameTitle} name length exceeds limit`;
|
||||
}
|
||||
|
||||
// 必须由字母开头
|
||||
if (!/^[A-Za-z]/.test(label)) {
|
||||
return simple
|
||||
? 'Must start with letter'
|
||||
: `${nameTitle} name must start with a letter`;
|
||||
}
|
||||
|
||||
// 检测 name 的命名规则
|
||||
if (!/^[A-Za-z][0-9a-zA-Z_]*$/.test(label)) {
|
||||
return simple
|
||||
? 'only ASCII letters, digits, and _'
|
||||
: `${nameTitle} name can only contain ASCII letters, digits, and _`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.content-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 6px;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
import {
|
||||
ResourceCopyScene,
|
||||
type ResType,
|
||||
} from '@coze-arch/bot-api/plugin_develop';
|
||||
|
||||
import { useNavigateResource } from './use-navigate-resource';
|
||||
|
||||
import styles from './styles.module.less';
|
||||
|
||||
export const SuccessContent = ({
|
||||
spaceId,
|
||||
scene,
|
||||
resourceId,
|
||||
resourceType,
|
||||
}: {
|
||||
spaceId?: string;
|
||||
scene?: ResourceCopyScene;
|
||||
resourceId?: string;
|
||||
resourceType?: ResType;
|
||||
}) => {
|
||||
const handleNavigateResource = useNavigateResource({
|
||||
resourceType,
|
||||
resourceId,
|
||||
spaceId,
|
||||
});
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (scene === ResourceCopyScene.MoveResourceToLibrary) {
|
||||
return I18n.t('resource_toast_move_to_library_success');
|
||||
}
|
||||
return I18n.t('resource_toast_copy_to_library_success');
|
||||
}, [scene]);
|
||||
return (
|
||||
<div className={styles['content-container']}>
|
||||
{content}
|
||||
<Button color="primary" size="small" onClick={handleNavigateResource}>
|
||||
{I18n.t('resource_toast_view_resource')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 { ResType } from '@coze-arch/bot-api/plugin_develop';
|
||||
|
||||
export const useNavigateResource =
|
||||
({
|
||||
resourceId,
|
||||
resourceType,
|
||||
spaceId,
|
||||
}: {
|
||||
resourceId?: string;
|
||||
resourceType?: ResType;
|
||||
spaceId?: string;
|
||||
}) =>
|
||||
() => {
|
||||
switch (resourceType) {
|
||||
case ResType.Plugin:
|
||||
window.open(`/space/${spaceId}/plugin/${resourceId}`);
|
||||
break;
|
||||
case ResType.Workflow:
|
||||
case ResType.Imageflow:
|
||||
window.open(`/work_flow?workflow_id=${resourceId}&space_id=${spaceId}`);
|
||||
break;
|
||||
case ResType.Knowledge:
|
||||
window.open(`/space/${spaceId}/knowledge/${resourceId}`);
|
||||
break;
|
||||
case ResType.UI:
|
||||
window.open(`/space/${spaceId}/widget/${resourceId}`);
|
||||
break;
|
||||
case ResType.Database:
|
||||
window.open(
|
||||
`/space/${spaceId}/database/${resourceId}?page_mode=normal`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 enum CustomCommand {
|
||||
RELOAD = 'reload',
|
||||
}
|
||||
@@ -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 * from './uri';
|
||||
export { CustomCommand } from './commands';
|
||||
38
frontend/packages/project-ide/framework/src/constants/uri.ts
Normal file
38
frontend/packages/project-ide/framework/src/constants/uri.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 { URI } from '@coze-project-ide/client';
|
||||
|
||||
export const URI_SCHEME = 'coze-project';
|
||||
|
||||
export const TOP_BAR_URI = new URI(`${URI_SCHEME}:///top-bar`);
|
||||
export const MAIN_PANEL_DEFAULT_URI = new URI(`${URI_SCHEME}:///default`);
|
||||
|
||||
export const SIDEBAR_URI = new URI(`${URI_SCHEME}:///side-bar`);
|
||||
export const SECONDARY_SIDEBAR_URI = new URI(
|
||||
`${URI_SCHEME}:///secondary-sidebar`,
|
||||
);
|
||||
export const SIDEBAR_RESOURCE_URI = new URI(
|
||||
`${URI_SCHEME}:///side-bar/resource`,
|
||||
);
|
||||
export const SIDEBAR_CONFIG_URI = new URI(`${URI_SCHEME}:///side-bar/config`);
|
||||
|
||||
export const UI_BUILDER_URI = new URI(`${URI_SCHEME}:///ui-builder`);
|
||||
export const UI_BUILDER_CONTENT = new URI(
|
||||
`${URI_SCHEME}:///ui-builder/content`,
|
||||
);
|
||||
|
||||
export const CONVERSATION_URI = new URI(`${URI_SCHEME}:///session`);
|
||||
22
frontend/packages/project-ide/framework/src/context/index.ts
Normal file
22
frontend/packages/project-ide/framework/src/context/index.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.
|
||||
*/
|
||||
|
||||
export {
|
||||
IDEGlobalProvider,
|
||||
useIDEGlobalContext,
|
||||
useIDEGlobalStore,
|
||||
} from '@coze-project-ide/base-adapter';
|
||||
export { WidgetContext } from './widget-context';
|
||||
@@ -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 { type URI } from '@coze-project-ide/client';
|
||||
|
||||
import { type ProjectIDEServices } from '../types';
|
||||
import { type WidgetService } from '../plugins/create-preset-plugin/widget-service';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export interface WidgetContext<T = any> {
|
||||
uri?: URI; // 当前 widget 的 uri
|
||||
store: T; // 当前 widget 的 store
|
||||
widget: WidgetService;
|
||||
services: ProjectIDEServices; // 全局的 ide 服务
|
||||
}
|
||||
|
||||
export const WidgetContext = Symbol('WidgetContext');
|
||||
17
frontend/packages/project-ide/framework/src/global.d.ts
vendored
Normal file
17
frontend/packages/project-ide/framework/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
36
frontend/packages/project-ide/framework/src/hooks/index.ts
Normal file
36
frontend/packages/project-ide/framework/src/hooks/index.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.
|
||||
*/
|
||||
|
||||
export { useSpaceId } from './use-space-id';
|
||||
export { useProjectIDEServices } from './use-project-ide-services';
|
||||
export { useCurrentWidgetContext } from './use-current-widget-context';
|
||||
export { useActivateWidgetContext } from './use-activate-widget-context';
|
||||
export { useIDENavigate } from './use-ide-navigate';
|
||||
export { useCurrentModeType } from './use-current-mode-type';
|
||||
export { useProjectId } from './use-project-id';
|
||||
export { useSplitScreenArea } from './use-current-split-screen';
|
||||
export { useTitle } from './use-title';
|
||||
export { useIDELocation, useIDEParams } from './use-ide-location';
|
||||
export { useIDEServiceInBiz } from './use-ide-service-in-biz';
|
||||
export { useShortcuts } from './use-shortcuts';
|
||||
export { useCommitVersion } from './use-commit-version';
|
||||
export { useWsListener } from './use-ws-listener';
|
||||
export {
|
||||
useSendMessageEvent,
|
||||
useListenMessageEvent,
|
||||
} from './use-message-event';
|
||||
export { useViewService } from './use-view-service';
|
||||
export { useGetUIWidgetFromId } from './use-get-ui-widget-from-id';
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 {
|
||||
useCurrentWidgetFromArea,
|
||||
LayoutPanelType,
|
||||
} from '@coze-project-ide/client';
|
||||
|
||||
import { type ProjectIDEWidget } from '@/widgets/project-ide-widget';
|
||||
import { type WidgetContext } from '@/context/widget-context';
|
||||
|
||||
/**
|
||||
* 用于提供当前 focus 的 widget 上下文
|
||||
*/
|
||||
export const useActivateWidgetContext = (): WidgetContext => {
|
||||
const currentWidget = useCurrentWidgetFromArea(LayoutPanelType.MAIN_PANEL);
|
||||
return (currentWidget as ProjectIDEWidget)?.context;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 { useIDEGlobalStore } from '../context';
|
||||
|
||||
export const useCommitVersion = () => {
|
||||
// 内置了 shallow 操作,无需 useShallow
|
||||
// eslint-disable-next-line @coze-arch/zustand/prefer-shallow
|
||||
const { version, patch } = useIDEGlobalStore(store => ({
|
||||
version: store.version,
|
||||
patch: store.patch,
|
||||
}));
|
||||
|
||||
return {
|
||||
version,
|
||||
patch,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { type ModeType } from '../types';
|
||||
import { UI_BUILDER_URI } from '../constants';
|
||||
|
||||
export const useCurrentModeType = () => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const type: ModeType = useMemo(() => {
|
||||
if (pathname.includes(UI_BUILDER_URI.path.toString())) {
|
||||
return 'ui-builder';
|
||||
}
|
||||
return 'dev';
|
||||
}, [pathname]);
|
||||
|
||||
return type;
|
||||
};
|
||||
@@ -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 { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
ApplicationShell,
|
||||
useIDEService,
|
||||
type URI,
|
||||
type DockLayout,
|
||||
type ReactWidget,
|
||||
type TabBar,
|
||||
type Widget,
|
||||
} from '@coze-project-ide/client';
|
||||
|
||||
import { compareURI } from '@/utils';
|
||||
|
||||
type Area = 'left' | 'right';
|
||||
|
||||
const getTabArea = (shell: ApplicationShell, uri?: URI): Area | undefined => {
|
||||
let currentTabIndex = -1;
|
||||
const area = (shell.mainPanel?.layout as DockLayout)?.saveLayout?.().main;
|
||||
const children = (area as DockLayout.ISplitAreaConfig)?.children || [area];
|
||||
|
||||
children.forEach((child, idx) => {
|
||||
const containCurrent =
|
||||
uri &&
|
||||
((child as DockLayout.ITabAreaConfig)?.widgets || []).some(
|
||||
widget => (widget as ReactWidget).uri?.toString?.() === uri.toString(),
|
||||
);
|
||||
if (containCurrent) {
|
||||
currentTabIndex = idx;
|
||||
}
|
||||
});
|
||||
|
||||
// 右边分屏不展示 hover icon
|
||||
if (children?.length === 1) {
|
||||
return undefined;
|
||||
} else if (currentTabIndex === 1) {
|
||||
return 'right';
|
||||
} else {
|
||||
return 'left';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前 uri 的资源在哪个分屏下
|
||||
* left: 左边分屏
|
||||
* right: 右边分屏
|
||||
* undefined: 未分屏
|
||||
*/
|
||||
export const useSplitScreenArea = (
|
||||
uri?: URI,
|
||||
tabBar?: TabBar<Widget>,
|
||||
): Area | undefined => {
|
||||
const shell = useIDEService<ApplicationShell>(ApplicationShell);
|
||||
|
||||
const [area, setArea] = useState(getTabArea(shell, uri));
|
||||
|
||||
useEffect(() => {
|
||||
setArea(getTabArea(shell, uri));
|
||||
const listener = () => {
|
||||
// 本次 uri 是否在当前 tab,不是不执行
|
||||
// 分屏过程中会出现中间态,布局变更时盲目执行会导致时序异常问题
|
||||
const uriInCurrentTab = tabBar?.titles.some(title =>
|
||||
compareURI((title.owner as ReactWidget)?.uri, uri),
|
||||
);
|
||||
if (uriInCurrentTab) {
|
||||
setArea(getTabArea(shell, uri));
|
||||
}
|
||||
};
|
||||
shell.mainPanel.layoutModified.connect(listener);
|
||||
return () => {
|
||||
shell.mainPanel.layoutModified.disconnect(listener);
|
||||
};
|
||||
}, [uri?.toString?.()]);
|
||||
|
||||
return area;
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCurrentWidget } from '@coze-project-ide/client';
|
||||
|
||||
import { type ProjectIDEWidget } from '@/widgets/project-ide-widget';
|
||||
|
||||
import { type WidgetContext } from '../context/widget-context';
|
||||
|
||||
/**
|
||||
* 获取当前的 WidgetContext
|
||||
* 在 registry 的 renderContent 内调用
|
||||
*/
|
||||
export function useCurrentWidgetContext<T>(): WidgetContext<T> {
|
||||
const currentWidget = useCurrentWidget() as ProjectIDEWidget;
|
||||
if (!currentWidget.context) {
|
||||
throw new Error(
|
||||
'[useWidgetContext] Undefined widgetContext from ide context',
|
||||
);
|
||||
}
|
||||
return currentWidget.context as WidgetContext<T>;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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 { URI, useIDEService, WidgetManager } from '@coze-project-ide/client';
|
||||
|
||||
import { type ProjectIDEWidget } from '../widgets/project-ide-widget';
|
||||
import { URI_SCHEME } from '../constants';
|
||||
|
||||
export const useGetUIWidgetFromId = (
|
||||
value: string,
|
||||
): ProjectIDEWidget | undefined => {
|
||||
const widgetManager = useIDEService<WidgetManager>(WidgetManager);
|
||||
const uri = new URI(`${URI_SCHEME}://${value}`);
|
||||
const widget = widgetManager.getWidgetFromURI(uri) as ProjectIDEWidget;
|
||||
return widget;
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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 { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
import { type URI, useCurrentWidget } from '@coze-project-ide/client';
|
||||
|
||||
import { type ProjectIDEWidget } from '../widgets/project-ide-widget';
|
||||
|
||||
type ActivateCallback = (widget: ProjectIDEWidget) => void;
|
||||
|
||||
interface WidgetLocation {
|
||||
uri: URI;
|
||||
pathname: string;
|
||||
params: { [key: string]: string | undefined };
|
||||
}
|
||||
|
||||
const genLocationByURI = (uri: URI): WidgetLocation => ({
|
||||
uri,
|
||||
pathname: uri.path.toString(),
|
||||
params: uri.queryObject,
|
||||
});
|
||||
|
||||
const useCurrentWidgetActivate = (cb: ActivateCallback) => {
|
||||
const currentWidget = useCurrentWidget() as ProjectIDEWidget;
|
||||
useLayoutEffect(() => {
|
||||
const dispose = currentWidget.onActivate(() => {
|
||||
cb(currentWidget);
|
||||
});
|
||||
return () => dispose.dispose();
|
||||
}, [currentWidget, cb]);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前 widget 的 location
|
||||
*/
|
||||
export const useIDELocation = () => {
|
||||
const currentWidget = useCurrentWidget() as ProjectIDEWidget;
|
||||
const [location, setLocation] = useState(
|
||||
genLocationByURI(currentWidget.uri!),
|
||||
);
|
||||
const uriRef = useRef(currentWidget.uri?.toString());
|
||||
|
||||
const callback = useCallback<ActivateCallback>(
|
||||
widget => {
|
||||
if (uriRef.current !== widget.uri?.toString()) {
|
||||
uriRef.current = widget.uri?.toString();
|
||||
setLocation(genLocationByURI(widget.uri!));
|
||||
}
|
||||
},
|
||||
[setLocation, uriRef],
|
||||
);
|
||||
|
||||
useCurrentWidgetActivate(callback);
|
||||
|
||||
return location;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前 widget 的 query 参数
|
||||
*/
|
||||
export const useIDEParams = () => {
|
||||
const currentWidget = useCurrentWidget() as ProjectIDEWidget;
|
||||
const [params, setParams] = useState(currentWidget.uri?.queryObject || {});
|
||||
const queryRef = useRef(currentWidget.uri?.query);
|
||||
|
||||
const callback = useCallback<ActivateCallback>(
|
||||
widget => {
|
||||
const query = widget.uri?.query;
|
||||
if (queryRef.current !== query) {
|
||||
queryRef.current = query;
|
||||
setParams(widget.uri?.queryObject || {});
|
||||
}
|
||||
},
|
||||
[queryRef, setParams],
|
||||
);
|
||||
|
||||
useCurrentWidgetActivate(callback);
|
||||
|
||||
return params;
|
||||
};
|
||||
@@ -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 { useNavigate, type NavigateOptions } from 'react-router-dom';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { URI } from '@coze-project-ide/client';
|
||||
|
||||
import { addPreservedSearchParams } from '../utils';
|
||||
import { URI_SCHEME, UI_BUILDER_URI } from '../constants';
|
||||
import { useSpaceId } from './use-space-id';
|
||||
import { useProjectIDEServices } from './use-project-ide-services';
|
||||
import { useProjectId } from './use-project-id';
|
||||
|
||||
export const useIDENavigate = () => {
|
||||
const { view } = useProjectIDEServices();
|
||||
const spaceId = useSpaceId();
|
||||
const projectId = useProjectId();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
/**
|
||||
* value(string): /:resourceType/:resourceId?a=a&b=b
|
||||
*/
|
||||
const IDENavigate = useCallback(
|
||||
(value: string, options?: NavigateOptions) => {
|
||||
const url = `/space/${spaceId}/project-ide/${projectId}${value}`;
|
||||
const uri = new URI(`${URI_SCHEME}://${value}`);
|
||||
const isUIBuilder = uri.displayName === UI_BUILDER_URI.displayName;
|
||||
if (value && value !== '/' && !isUIBuilder) {
|
||||
// 调用 openService
|
||||
view.open(uri);
|
||||
} else {
|
||||
// 如果没有要打开的 widget,就只打开主面板
|
||||
view.openPanel(isUIBuilder ? 'ui-builder' : 'dev');
|
||||
}
|
||||
navigate(addPreservedSearchParams(url), options);
|
||||
},
|
||||
[spaceId, projectId, view, navigate],
|
||||
);
|
||||
|
||||
return IDENavigate;
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 interfaces } from 'inversify';
|
||||
import { useIDEContainer } from '@coze-project-ide/client';
|
||||
|
||||
/**
|
||||
* 获取 IDE 的 IOC 模块
|
||||
* 和 flow-ide/client 包内容相同,但可以支持在业务侧如 workflow 内调用
|
||||
* @param identifier
|
||||
*/
|
||||
export function useIDEServiceInBiz<T>(
|
||||
identifier: interfaces.ServiceIdentifier,
|
||||
): T | undefined {
|
||||
const container = useIDEContainer();
|
||||
if (container.isBound(identifier)) {
|
||||
return container.get(identifier) as T;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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 { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { URI, useIDEService } from '@coze-project-ide/client';
|
||||
|
||||
import { getURLByURI } from '../utils';
|
||||
import { MessageEventService, type MessageEvent } from '../services';
|
||||
import { URI_SCHEME } from '../constants';
|
||||
import { useIDENavigate } from './use-ide-navigate';
|
||||
|
||||
export const useMessageEventService = () =>
|
||||
useIDEService<MessageEventService>(MessageEventService);
|
||||
|
||||
/**
|
||||
* 获取向 widget 发送信息函数的 hooks
|
||||
*/
|
||||
export const useSendMessageEvent = () => {
|
||||
const messageEventService = useMessageEventService();
|
||||
const navigate = useIDENavigate();
|
||||
|
||||
/**
|
||||
* 向以 uri 为索引的 widget 发送信息
|
||||
*/
|
||||
const send = useCallback(
|
||||
<T>(target: string | URI, data: MessageEvent<T>) => {
|
||||
const uri =
|
||||
typeof target === 'string'
|
||||
? new URI(`${URI_SCHEME}://${target}`)
|
||||
: target;
|
||||
messageEventService.send(uri, data);
|
||||
},
|
||||
[messageEventService],
|
||||
);
|
||||
|
||||
/**
|
||||
* 向以 uri 为索引的 widget 发送信息,并且打开/激活此 widget
|
||||
* 此函数比较常用
|
||||
*/
|
||||
const sendOpen = useCallback(
|
||||
<T>(target: string | URI, data: MessageEvent<T>) => {
|
||||
const uri =
|
||||
typeof target === 'string'
|
||||
? new URI(`${URI_SCHEME}://${target}`)
|
||||
: target;
|
||||
messageEventService.send(uri, data);
|
||||
navigate(getURLByURI(uri));
|
||||
},
|
||||
[messageEventService, navigate],
|
||||
);
|
||||
|
||||
return { send, sendOpen };
|
||||
};
|
||||
|
||||
/**
|
||||
* 监听向指定 uri 对应的唯一 widget 发送消息的 hook
|
||||
* 监听消息的 widget 一定是知道 this.uri,所以入参无须支持 string
|
||||
* 注:虽然 widget.uri 的值是会变得,但其 withoutQuery().toString() 一定是不变的,所以 uri 可以认定为不变
|
||||
*/
|
||||
export const useListenMessageEvent = (
|
||||
uri: URI,
|
||||
cb: (e: MessageEvent) => void,
|
||||
) => {
|
||||
const messageEventService = useMessageEventService();
|
||||
// 尽管 uri 对应的唯一 key 不会变化,但 uri 内存地址仍然会变化,这里显式的固化 uri 的不变性
|
||||
const uriRef = useRef(uri);
|
||||
|
||||
// 保证 callback 函数的可变性
|
||||
const listener = useMemoizedFn(() => {
|
||||
const queue = messageEventService.on(uri);
|
||||
queue.forEach(cb);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// 组件挂在时去队列中取一次,有可能在组件未挂载前已经被发送了消息
|
||||
listener();
|
||||
|
||||
const disposable = messageEventService.onSend(e => {
|
||||
if (messageEventService.compare(e.uri, uriRef.current)) {
|
||||
listener();
|
||||
}
|
||||
});
|
||||
return () => disposable.dispose();
|
||||
}, [messageEventService, listener, uriRef]);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 { useIDEGlobalContext } from '../context';
|
||||
|
||||
export const useProjectId = () => {
|
||||
const store = useIDEGlobalContext();
|
||||
const projectId = store(state => state.projectId);
|
||||
|
||||
return projectId;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useIDEService } from '@coze-project-ide/client';
|
||||
|
||||
import { ProjectIDEServices } from '../plugins/create-preset-plugin/project-ide-services';
|
||||
|
||||
export const useProjectIDEServices = (): ProjectIDEServices => {
|
||||
const projectIDEServices =
|
||||
useIDEService<ProjectIDEServices>(ProjectIDEServices);
|
||||
|
||||
return projectIDEServices;
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
useIDEService,
|
||||
ShortcutsService,
|
||||
CommandRegistry,
|
||||
} from '@coze-project-ide/client';
|
||||
|
||||
export const useShortcuts = (commandId: string) => {
|
||||
const commandRegistry = useIDEService<CommandRegistry>(CommandRegistry);
|
||||
const shortcutsService = useIDEService<ShortcutsService>(ShortcutsService);
|
||||
|
||||
const shortcut = shortcutsService.getShortcutByCommandId(commandId);
|
||||
const keybinding = shortcut.map(item => item.join(' ')).join('/');
|
||||
const label = commandRegistry.getCommand(commandId)?.label;
|
||||
|
||||
return {
|
||||
keybinding,
|
||||
label,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 { useIDEGlobalContext } from '../context';
|
||||
|
||||
export const useSpaceId = () => {
|
||||
const store = useIDEGlobalContext();
|
||||
const spaceId = store(state => state.spaceId);
|
||||
|
||||
return spaceId;
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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, useState } from 'react';
|
||||
|
||||
import { useCurrentWidgetContext } from './use-current-widget-context';
|
||||
|
||||
export const useTitle = () => {
|
||||
const currentWidgetContext = useCurrentWidgetContext();
|
||||
const { widget } = currentWidgetContext;
|
||||
const [title, setTitle] = useState(widget.getTitle());
|
||||
useEffect(() => {
|
||||
const disposable = widget.onTitleChanged(_title => {
|
||||
setTitle(_title);
|
||||
});
|
||||
return () => {
|
||||
disposable?.dispose?.();
|
||||
};
|
||||
}, []);
|
||||
return title;
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 ViewService } from '@/plugins/create-preset-plugin/view-service';
|
||||
|
||||
import { useProjectIDEServices } from './use-project-ide-services';
|
||||
|
||||
/**
|
||||
* 获取 ProjectIDE 所有视图操作
|
||||
*/
|
||||
export const useViewService = (): ViewService => {
|
||||
const projectIDEServices = useProjectIDEServices();
|
||||
return projectIDEServices.view;
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { useIDEService } from '@coze-project-ide/client';
|
||||
|
||||
import { type WsMessageProps } from '@/types';
|
||||
import { WsService } from '@/services';
|
||||
|
||||
export const useWsListener = (listener: (props: WsMessageProps) => void) => {
|
||||
const wsService = useIDEService<WsService>(WsService);
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = wsService.onMessageSend(listener);
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const send = useCallback(
|
||||
data => {
|
||||
wsService.send(data);
|
||||
},
|
||||
[wsService],
|
||||
);
|
||||
|
||||
return {
|
||||
send,
|
||||
};
|
||||
};
|
||||
141
frontend/packages/project-ide/framework/src/index.ts
Normal file
141
frontend/packages/project-ide/framework/src/index.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** 透传 sdk */
|
||||
export {
|
||||
IDEClient,
|
||||
ReactWidget,
|
||||
LayoutPanelType,
|
||||
URI,
|
||||
definePluginCreator,
|
||||
bindContributions,
|
||||
ViewContribution,
|
||||
LifecycleContribution,
|
||||
Emitter,
|
||||
Event,
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
useIDEService,
|
||||
useNavigation,
|
||||
LabelHandler,
|
||||
CommandContribution,
|
||||
ShortcutsContribution,
|
||||
OpenerService,
|
||||
useCurrentWidget,
|
||||
DISABLE_HANDLE_EVENT,
|
||||
ViewService,
|
||||
LayoutRestorer,
|
||||
ApplicationShell,
|
||||
WidgetManager,
|
||||
ViewRenderer,
|
||||
type PluginCreator,
|
||||
type ViewOptionRegisterService,
|
||||
type BoxPanel,
|
||||
ShortcutsService,
|
||||
CommandRegistry,
|
||||
useIDEContainer,
|
||||
TabBarToolbar,
|
||||
ContextKeyService,
|
||||
type ShortcutsRegistry,
|
||||
SplitWidget,
|
||||
Command,
|
||||
WindowService,
|
||||
type CustomTitleType,
|
||||
} from '@coze-project-ide/client';
|
||||
|
||||
export { useCommitVersion } from '@coze-project-ide/base-adapter';
|
||||
|
||||
export {
|
||||
useCurrentWidgetContext,
|
||||
useSpaceId,
|
||||
useProjectId,
|
||||
useProjectIDEServices,
|
||||
useActivateWidgetContext,
|
||||
useIDENavigate,
|
||||
useCurrentModeType,
|
||||
useSplitScreenArea,
|
||||
useTitle,
|
||||
useIDELocation,
|
||||
useIDEParams,
|
||||
useIDEServiceInBiz,
|
||||
useShortcuts,
|
||||
useListenMessageEvent,
|
||||
useWsListener,
|
||||
useSendMessageEvent,
|
||||
useViewService,
|
||||
useGetUIWidgetFromId,
|
||||
} from './hooks';
|
||||
export { IDEGlobalProvider, WidgetContext } from './context';
|
||||
export {
|
||||
UI_BUILDER_URI,
|
||||
MAIN_PANEL_DEFAULT_URI,
|
||||
SIDEBAR_URI,
|
||||
URI_SCHEME,
|
||||
SIDEBAR_CONFIG_URI,
|
||||
CONVERSATION_URI,
|
||||
SECONDARY_SIDEBAR_URI,
|
||||
CustomCommand,
|
||||
} from './constants';
|
||||
export type { TitlePropsType, WidgetRegistry } from './types';
|
||||
|
||||
export {
|
||||
withLazyLoad,
|
||||
getResourceByPathname,
|
||||
getURIByResource,
|
||||
getResourceByURI,
|
||||
getURIPathByPathname,
|
||||
getURLByURI,
|
||||
getURIByPath,
|
||||
getPathnameByURI,
|
||||
compareURI,
|
||||
addPreservedSearchParams,
|
||||
} from './utils';
|
||||
export { ProjectIDEServices } from './plugins/create-preset-plugin/project-ide-services';
|
||||
export { WidgetService } from './plugins/create-preset-plugin/widget-service';
|
||||
|
||||
export {
|
||||
ProjectIDEClient,
|
||||
ResourceFolder,
|
||||
mapResourceTree,
|
||||
ResourceTypeEnum,
|
||||
BaseResourceContextMenuBtnType,
|
||||
type CommonRenderProps,
|
||||
type ResourceType,
|
||||
type ResourceMapType,
|
||||
type ResourceFolderRefType,
|
||||
type RightPanelConfigType,
|
||||
type ResourceFolderShortCutContextType,
|
||||
type ResourceFolderProps,
|
||||
type RenderMoreSuffixType,
|
||||
type CreateResourcePropType,
|
||||
RESOURCE_FOLDER_CONTEXT_KEY,
|
||||
ROOT_KEY,
|
||||
type IdType,
|
||||
} from './components';
|
||||
|
||||
export { useIDEGlobalStore, useIDEGlobalContext } from './context';
|
||||
|
||||
export { ProjectIDEWidget } from './widgets/project-ide-widget';
|
||||
|
||||
export { CloseConfirmContribution } from './plugins/close-confirm-plugin/close-confirm-contribution';
|
||||
|
||||
export {
|
||||
ModalService,
|
||||
ModalType,
|
||||
OptionsService,
|
||||
ErrorService,
|
||||
type MessageEvent,
|
||||
} from './services';
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 { inject, injectable } from 'inversify';
|
||||
import {
|
||||
type CommandContribution,
|
||||
type CommandRegistry,
|
||||
type CustomTitleType,
|
||||
Command,
|
||||
} from '@coze-project-ide/client';
|
||||
|
||||
import { ModalService, ModalType } from '@/services';
|
||||
|
||||
@injectable()
|
||||
export class CloseConfirmContribution implements CommandContribution {
|
||||
@inject(ModalService) private modalService: ModalService;
|
||||
|
||||
registerCommands(commands: CommandRegistry): void {
|
||||
commands.registerCommand(Command.Default.VIEW_SAVING_WIDGET_CLOSE_CONFIRM, {
|
||||
execute: (titles: CustomTitleType[]) => {
|
||||
const hasUnsaved = titles.some(title => title?.saving);
|
||||
if (hasUnsaved) {
|
||||
this.modalService.onModalVisibleChangeEmitter.fire({
|
||||
type: ModalType.CLOSE_CONFIRM,
|
||||
options: titles,
|
||||
});
|
||||
} else {
|
||||
titles.forEach(title => title?.owner?.close?.());
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 {
|
||||
bindContributions,
|
||||
definePluginCreator,
|
||||
type PluginCreator,
|
||||
CommandContribution,
|
||||
} from '@coze-project-ide/client';
|
||||
|
||||
import { CloseConfirmContribution } from './close-confirm-contribution';
|
||||
|
||||
export const createCloseConfirmPlugin: PluginCreator<void> =
|
||||
definePluginCreator({
|
||||
onBind: ({ bind }) => {
|
||||
bindContributions(bind, CloseConfirmContribution, [CommandContribution]);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
definePluginCreator,
|
||||
type PluginCreator,
|
||||
MenuService,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
ApplicationShell,
|
||||
type FlowDockPanel,
|
||||
TabBar,
|
||||
ShortcutsService,
|
||||
} from '@coze-project-ide/client';
|
||||
|
||||
import { ViewService } from '../create-preset-plugin/view-service';
|
||||
|
||||
const CUSTOM_COMMAND = {
|
||||
// 在左侧分屏打开
|
||||
SPLIT_LEFT: {
|
||||
id: 'view.custom.split-left',
|
||||
label: I18n.t('project_ide_tabs_open_on_left'),
|
||||
},
|
||||
// 在右侧分屏打开
|
||||
SPLIT_RIGHT: {
|
||||
id: 'view.custom.split-right',
|
||||
label: I18n.t('project_ide_tabs_open_on_right'),
|
||||
},
|
||||
REFRESH: {
|
||||
id: 'view.custom.refresh-widget',
|
||||
label: I18n.t('refresh_project_tags'),
|
||||
},
|
||||
};
|
||||
|
||||
function getAllTabsCount(dockPanel: FlowDockPanel): number {
|
||||
let count = 0;
|
||||
|
||||
// 遍历 DockPanel 中的所有小部件
|
||||
Array.from(dockPanel.children()).forEach(widget => {
|
||||
if (widget instanceof TabBar) {
|
||||
// 累计 TabBar 中的所有标签页数
|
||||
count += widget.titles.length;
|
||||
}
|
||||
});
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
export const createContextMenuPlugin: PluginCreator<void> = definePluginCreator(
|
||||
{
|
||||
onInit(ctx) {
|
||||
const menuService = ctx.container.get<MenuService>(MenuService);
|
||||
const command = ctx.container.get<CommandRegistry>(CommandRegistry);
|
||||
const viewService = ctx.container.get<ViewService>(ViewService);
|
||||
const shell = ctx.container.get<ApplicationShell>(ApplicationShell);
|
||||
const shortcutsService =
|
||||
ctx.container.get<ShortcutsService>(ShortcutsService);
|
||||
/**
|
||||
* 更改标题
|
||||
*/
|
||||
// 更新 command 标题 label
|
||||
command.updateCommand(Command.Default.VIEW_CLOSE_CURRENT_WIDGET, {
|
||||
label: I18n.t('project_ide_tabs_close'),
|
||||
});
|
||||
command.updateCommand(Command.Default.VIEW_CLOSE_OTHER_WIDGET, {
|
||||
label: I18n.t('project_ide_tabs_close_other_tabs'),
|
||||
});
|
||||
command.updateCommand(Command.Default.VIEW_CLOSE_ALL_WIDGET, {
|
||||
label: I18n.t('project_ide_tabs_close_all'),
|
||||
});
|
||||
command.registerCommand(CUSTOM_COMMAND.REFRESH, {
|
||||
execute: widget => {
|
||||
widget.refresh();
|
||||
},
|
||||
});
|
||||
shortcutsService.registerHandlers({
|
||||
keybinding: 'alt r',
|
||||
commandId: CUSTOM_COMMAND.REFRESH.id,
|
||||
});
|
||||
command.registerCommand(CUSTOM_COMMAND.SPLIT_LEFT, {
|
||||
execute: widget => {
|
||||
viewService.splitScreen('left', widget);
|
||||
},
|
||||
// 分屏功能在所有 tab 大于 1 时才可以使用
|
||||
isEnabled: () => {
|
||||
const tabCounts = getAllTabsCount(shell.mainPanel);
|
||||
return tabCounts > 1;
|
||||
},
|
||||
});
|
||||
command.registerCommand(CUSTOM_COMMAND.SPLIT_RIGHT, {
|
||||
execute: widget => {
|
||||
viewService.splitScreen('right', widget);
|
||||
},
|
||||
// 分屏功能在所有 tab 大于 1 时才可以使用
|
||||
isEnabled: () => {
|
||||
const tabCounts = getAllTabsCount(shell.mainPanel);
|
||||
return tabCounts > 1;
|
||||
},
|
||||
});
|
||||
/**
|
||||
* 注册 menu
|
||||
*/
|
||||
// 关闭
|
||||
menuService.addMenuItem({
|
||||
command: Command.Default.VIEW_CLOSE_CURRENT_WIDGET,
|
||||
selector: '.lm-TabBar-tab',
|
||||
});
|
||||
// 关闭其他
|
||||
menuService.addMenuItem({
|
||||
command: Command.Default.VIEW_CLOSE_OTHER_WIDGET,
|
||||
selector: '.lm-TabBar-tab',
|
||||
});
|
||||
// 关闭所有
|
||||
menuService.addMenuItem({
|
||||
command: Command.Default.VIEW_CLOSE_ALL_WIDGET,
|
||||
selector: '.lm-TabBar-tab',
|
||||
});
|
||||
// 刷新标签
|
||||
menuService.addMenuItem({
|
||||
command: CUSTOM_COMMAND.REFRESH.id,
|
||||
selector: '.lm-TabBar-tab',
|
||||
});
|
||||
// 分割线
|
||||
menuService.addMenuItem({
|
||||
type: 'separator',
|
||||
selector: '.lm-TabBar-tab',
|
||||
});
|
||||
// 向左分屏
|
||||
menuService.addMenuItem({
|
||||
command: CUSTOM_COMMAND.SPLIT_LEFT.id,
|
||||
selector: '.lm-TabBar-tab',
|
||||
});
|
||||
// 向右分屏
|
||||
menuService.addMenuItem({
|
||||
command: CUSTOM_COMMAND.SPLIT_RIGHT.id,
|
||||
selector: '.lm-TabBar-tab',
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 {
|
||||
LabelHandler,
|
||||
LifecycleContribution,
|
||||
WindowService,
|
||||
bindContributions,
|
||||
} from '@coze-project-ide/client';
|
||||
import {
|
||||
ViewContribution,
|
||||
definePluginCreator,
|
||||
type PluginCreator,
|
||||
} from '@coze-project-ide/client';
|
||||
|
||||
import {
|
||||
ModalService,
|
||||
ErrorService,
|
||||
MessageEventService,
|
||||
WsService,
|
||||
} from '@/services';
|
||||
|
||||
import { ProjectIDEClientProps } from '../../types';
|
||||
import { ViewService } from './view-service';
|
||||
import { TooltipContribution } from './tooltip-contribution';
|
||||
import { ProjectIDEServices } from './project-ide-services';
|
||||
import { PresetContribution } from './preset-contribution';
|
||||
import { LifecycleService } from './lifecycle-service';
|
||||
|
||||
export const createPresetPlugin: PluginCreator<ProjectIDEClientProps> =
|
||||
definePluginCreator({
|
||||
onBind: ({ bind }, opts) => {
|
||||
bind(ProjectIDEClientProps).toConstantValue(opts);
|
||||
bind(LifecycleService).toSelf().inSingletonScope();
|
||||
bind(ViewService).toSelf().inSingletonScope();
|
||||
bind(ModalService).toSelf().inSingletonScope();
|
||||
bind(MessageEventService).toSelf().inSingletonScope();
|
||||
bind(ErrorService).toSelf().inSingletonScope();
|
||||
bind(WsService).toSelf().inSingletonScope();
|
||||
bind(ProjectIDEServices).toSelf().inSingletonScope();
|
||||
bindContributions(bind, PresetContribution, [
|
||||
ViewContribution,
|
||||
LifecycleContribution,
|
||||
]);
|
||||
bindContributions(bind, TooltipContribution, [LabelHandler]);
|
||||
},
|
||||
onStart: ctx => {
|
||||
const windowService = ctx.container.get<WindowService>(WindowService);
|
||||
windowService.onStart();
|
||||
},
|
||||
onDispose: ctx => {
|
||||
const lifecycleService =
|
||||
ctx.container.get<LifecycleService>(LifecycleService);
|
||||
lifecycleService.dispose();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 { inject, injectable, postConstruct } from 'inversify';
|
||||
import {
|
||||
ApplicationShell,
|
||||
DisposableCollection,
|
||||
type Disposable,
|
||||
Emitter,
|
||||
type CustomTitleType,
|
||||
type Event,
|
||||
EventService,
|
||||
MenuService,
|
||||
} from '@coze-project-ide/client';
|
||||
|
||||
@injectable()
|
||||
export class LifecycleService implements Disposable {
|
||||
@inject(ApplicationShell) shell: ApplicationShell;
|
||||
|
||||
@inject(EventService) eventService: EventService;
|
||||
|
||||
@inject(MenuService) menuService: MenuService;
|
||||
|
||||
protected readonly onFocusEmitter = new Emitter<CustomTitleType>();
|
||||
|
||||
readonly onFocus: Event<CustomTitleType> = this.onFocusEmitter.event;
|
||||
|
||||
private disposable = new DisposableCollection(this.onFocusEmitter);
|
||||
|
||||
@postConstruct()
|
||||
init() {
|
||||
this.disposable.push(
|
||||
this.shell.mainPanel.onDidChangeCurrent(title => {
|
||||
if (title) {
|
||||
this.onFocusEmitter.fire(title as CustomTitleType);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* 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 { inject, injectable } from 'inversify';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
type ViewContribution,
|
||||
type ViewOptionRegisterService,
|
||||
type URI,
|
||||
LayoutPanelType,
|
||||
type WidgetFactory,
|
||||
type LifecycleContribution,
|
||||
BoxPanel,
|
||||
ApplicationShell,
|
||||
WidgetManager,
|
||||
CommandRegistry,
|
||||
ShortcutsService,
|
||||
ContextKeyService,
|
||||
ToolbarAlign,
|
||||
Command,
|
||||
ViewRenderer,
|
||||
type ReactWidget,
|
||||
} from '@coze-project-ide/client';
|
||||
|
||||
import { ProjectIDEWidget } from '@/widgets/project-ide-widget';
|
||||
import { PrimarySidebarWidget } from '@/widgets/primary-sidebar-widget';
|
||||
import { ProjectIDEClientProps, type WidgetRegistry } from '@/types';
|
||||
import { WsService } from '@/services';
|
||||
import { WidgetContext } from '@/context/widget-context';
|
||||
import { CustomCommand } from '@/constants';
|
||||
|
||||
import { customLayout } from '../../utils';
|
||||
import {
|
||||
SIDEBAR_URI,
|
||||
UI_BUILDER_URI,
|
||||
UI_BUILDER_CONTENT,
|
||||
TOP_BAR_URI,
|
||||
MAIN_PANEL_DEFAULT_URI,
|
||||
SECONDARY_SIDEBAR_URI,
|
||||
} from '../../constants';
|
||||
import { withRegistryContent } from './with-registry-content';
|
||||
import { WidgetService } from './widget-service';
|
||||
import { ViewService } from './view-service';
|
||||
import { ProjectIDEServices } from './project-ide-services';
|
||||
|
||||
@injectable()
|
||||
export class PresetContribution
|
||||
implements ViewContribution, LifecycleContribution
|
||||
{
|
||||
@inject(ProjectIDEClientProps) props: ProjectIDEClientProps;
|
||||
|
||||
@inject(WidgetManager) widgetManager: WidgetManager;
|
||||
|
||||
@inject(ContextKeyService) contextKeyService: ContextKeyService;
|
||||
|
||||
@inject(ProjectIDEServices) services: ProjectIDEServices;
|
||||
|
||||
@inject(CommandRegistry) commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(ShortcutsService) shortcutsService: ShortcutsService;
|
||||
|
||||
@inject(ApplicationShell) shell: ApplicationShell;
|
||||
|
||||
@inject(ViewService) viewService: ViewService;
|
||||
|
||||
@inject(ViewRenderer) viewRenderer: ViewRenderer;
|
||||
|
||||
@inject(WsService) wsService: WsService;
|
||||
|
||||
onInit() {
|
||||
this.wsService.init();
|
||||
// register command
|
||||
this.props.view.widgetRegistries.forEach(registry => {
|
||||
if (registry.registerCommands) {
|
||||
const commands = registry.registerCommands();
|
||||
commands.forEach(cmd => {
|
||||
const existCmd = this.commandRegistry.getCommand(cmd.id);
|
||||
|
||||
if (!existCmd) {
|
||||
this.commandRegistry.registerCommand(
|
||||
{
|
||||
id: cmd.id,
|
||||
label: cmd.label,
|
||||
},
|
||||
{
|
||||
execute: props => {
|
||||
const currentContext = this.contextKeyService.getContext(
|
||||
'widgetContext',
|
||||
) as WidgetContext;
|
||||
cmd.execute(currentContext, props);
|
||||
},
|
||||
isEnabled: props => {
|
||||
const currentUri = this.contextKeyService.getContext(
|
||||
'widgetFocus',
|
||||
) as URI;
|
||||
const currentContext = this.contextKeyService.getContext(
|
||||
'widgetContext',
|
||||
) as WidgetContext;
|
||||
if (
|
||||
currentUri?.toString?.() &&
|
||||
!registry.match.test(currentUri.toString()) &&
|
||||
cmd.when === 'widgetFocus'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return cmd.isEnable(currentContext, props);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (registry.registerShortcuts) {
|
||||
const shortcuts = registry.registerShortcuts();
|
||||
|
||||
shortcuts.forEach(shortcut => {
|
||||
this.shortcutsService.registerHandlers({
|
||||
commandId: shortcut.commandId,
|
||||
keybinding: shortcut.keybinding,
|
||||
preventDefault: shortcut.preventDefault,
|
||||
});
|
||||
});
|
||||
}
|
||||
if (registry.registerContextMenu) {
|
||||
const menus = registry.registerContextMenu();
|
||||
this.services.contextmenu.registerContextMenu(menus, registry.match);
|
||||
}
|
||||
});
|
||||
// 覆写全屏逻辑
|
||||
this.commandRegistry.unregisterCommand(Command.Default.VIEW_FULL_SCREEN);
|
||||
this.commandRegistry.registerCommand(
|
||||
{
|
||||
id: Command.Default.VIEW_FULL_SCREEN,
|
||||
label: I18n.t('project_ide_maximize'),
|
||||
},
|
||||
{
|
||||
execute: () => {
|
||||
this.viewService.switchFullScreenMode();
|
||||
},
|
||||
},
|
||||
);
|
||||
this.commandRegistry.registerCommand(
|
||||
{
|
||||
id: CustomCommand.RELOAD,
|
||||
label: I18n.t('refresh_project_tags'),
|
||||
},
|
||||
{
|
||||
execute: (widget?: ProjectIDEWidget) => {
|
||||
if (!widget) {
|
||||
const { currentWidget } = this.shell;
|
||||
(currentWidget as ProjectIDEWidget)?.refresh?.();
|
||||
} else {
|
||||
widget.refresh();
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
this.shortcutsService.registerHandlers({
|
||||
commandId: CustomCommand.RELOAD,
|
||||
keybinding: 'alt r',
|
||||
preventDefault: false,
|
||||
});
|
||||
}
|
||||
|
||||
private createLayout(shell: ApplicationShell) {
|
||||
// 设置 panel 存储到 widgetManager
|
||||
const uiBuilderPanel = new BoxPanel();
|
||||
uiBuilderPanel.id = UI_BUILDER_URI.displayName;
|
||||
this.widgetManager.setWidget(UI_BUILDER_URI.toString(), uiBuilderPanel);
|
||||
return customLayout(shell, uiBuilderPanel);
|
||||
}
|
||||
|
||||
private createWidget(factory: WidgetRegistry<any>, uri: URI) {
|
||||
const childContainer = this.widgetManager.containerFactory.createChild();
|
||||
childContainer.bind(ProjectIDEWidget).toSelf().inSingletonScope();
|
||||
const widget = childContainer.get<ProjectIDEWidget>(ProjectIDEWidget);
|
||||
|
||||
const store = factory.createStore?.(uri);
|
||||
|
||||
childContainer.bind(WidgetService).toSelf().inSingletonScope();
|
||||
const widgetService = childContainer.get(WidgetService);
|
||||
widgetService.init(factory, this.props.view.widgetTitleRender);
|
||||
|
||||
const widgetContext: WidgetContext = {
|
||||
uri,
|
||||
store,
|
||||
widget: widgetService,
|
||||
services: this.services,
|
||||
};
|
||||
widget.context = widgetContext;
|
||||
widget.container = childContainer;
|
||||
childContainer.bind(WidgetContext).toConstantValue(widgetContext);
|
||||
widget.render = withRegistryContent(factory);
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
registerView(service: ViewOptionRegisterService): void {
|
||||
const widgetFactories: WidgetFactory[] =
|
||||
this.props.view.widgetRegistries.map(factory => ({
|
||||
area: factory.area || LayoutPanelType.MAIN_PANEL,
|
||||
match: factory.match,
|
||||
createWidget: this.createWidget.bind(this, factory),
|
||||
toolbarItems: this.props.view.preToolbar
|
||||
? [
|
||||
{
|
||||
render: this.props.view.preToolbar as (
|
||||
widget: ReactWidget,
|
||||
) => React.ReactElement<any, any> | null,
|
||||
align: ToolbarAlign.LEADING,
|
||||
},
|
||||
{
|
||||
render: this.props.view.toolbar as (
|
||||
widget: ReactWidget,
|
||||
) => React.ReactElement<any, any> | null,
|
||||
align: ToolbarAlign.TRAILING,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
}));
|
||||
service.register({
|
||||
presetConfig: {
|
||||
disableContextMenu: true,
|
||||
splitScreenConfig: {
|
||||
main: {
|
||||
splitOptions: {
|
||||
maxSplitCount: 2,
|
||||
splitOrientation: 'horizontal', // 只支持水平分屏
|
||||
},
|
||||
dockPanelOptions: {
|
||||
spacing: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
disableFullScreen: true,
|
||||
},
|
||||
widgetFactories: [
|
||||
{
|
||||
area: LayoutPanelType.MAIN_PANEL,
|
||||
canHandle: UI_BUILDER_CONTENT.match.bind(UI_BUILDER_CONTENT),
|
||||
render: this.props.view.uiBuilder,
|
||||
},
|
||||
{
|
||||
area: LayoutPanelType.TOP_BAR,
|
||||
canHandle: TOP_BAR_URI.match.bind(TOP_BAR_URI),
|
||||
render: this.props.view.topBar,
|
||||
},
|
||||
{
|
||||
area: LayoutPanelType.MAIN_PANEL,
|
||||
canHandle: MAIN_PANEL_DEFAULT_URI.match.bind(MAIN_PANEL_DEFAULT_URI),
|
||||
render: this.props.view.widgetDefaultRender,
|
||||
},
|
||||
{
|
||||
area: LayoutPanelType.PRIMARY_SIDEBAR,
|
||||
canHandle: SIDEBAR_URI.match.bind(SIDEBAR_URI),
|
||||
widget: PrimarySidebarWidget,
|
||||
},
|
||||
{
|
||||
area: LayoutPanelType.SECONDARY_SIDEBAR,
|
||||
canHandle: SECONDARY_SIDEBAR_URI.match.bind(SECONDARY_SIDEBAR_URI),
|
||||
render: this.props.view.secondarySidebar,
|
||||
},
|
||||
...widgetFactories,
|
||||
],
|
||||
defaultLayoutData: {
|
||||
defaultWidgets: [TOP_BAR_URI],
|
||||
},
|
||||
customLayout: this.createLayout.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
onDispose() {
|
||||
this.wsService.onDispose();
|
||||
}
|
||||
}
|
||||
@@ -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 { inject, injectable } from 'inversify';
|
||||
import {
|
||||
CommandService,
|
||||
MenuService,
|
||||
type URI,
|
||||
ContextKeyService,
|
||||
} from '@coze-project-ide/client';
|
||||
|
||||
import {
|
||||
type ContextMenuService,
|
||||
type MenuItem,
|
||||
type CommandService as CustomCommandService,
|
||||
} from '@/types/services';
|
||||
|
||||
import { ViewService } from './view-service';
|
||||
|
||||
/**
|
||||
* 获取 service 操作
|
||||
* 全局任意位置均可调用
|
||||
* command:命令系统注册
|
||||
* contextmenu:右键菜单注册
|
||||
* view:视图操作
|
||||
*/
|
||||
@injectable()
|
||||
export class ProjectIDEServices {
|
||||
@inject(CommandService)
|
||||
private commandService: CommandService;
|
||||
|
||||
@inject(ContextKeyService)
|
||||
private contextKeyService: ContextKeyService;
|
||||
|
||||
@inject(MenuService)
|
||||
private menu: MenuService;
|
||||
|
||||
@inject(ViewService)
|
||||
public view: ViewService;
|
||||
|
||||
private registerMenus(options: MenuItem[], match?: RegExp) {
|
||||
const filter = () => {
|
||||
const currentUri = this.contextKeyService.getContext(
|
||||
'widgetFocus',
|
||||
) as URI;
|
||||
return Boolean(match?.test?.(currentUri.toString()));
|
||||
};
|
||||
options.forEach(option => {
|
||||
if (!option.submenu) {
|
||||
this.menu.addMenuItem({
|
||||
command: option.commandId,
|
||||
selector: option.selector,
|
||||
filter,
|
||||
});
|
||||
} else {
|
||||
const submenu = this.menu.createSubMenu();
|
||||
this.menu.addMenuItem({
|
||||
command: option.commandId,
|
||||
selector: option.selector,
|
||||
submenu,
|
||||
filter,
|
||||
});
|
||||
option.submenu.forEach(sub => {
|
||||
submenu.addItem({
|
||||
command: sub.commandId,
|
||||
filter,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public contextmenu: ContextMenuService = {
|
||||
registerContextMenu: (options: MenuItem[], match?: RegExp) => {
|
||||
this.registerMenus(options, match);
|
||||
},
|
||||
open: e => this.menu.open(e),
|
||||
};
|
||||
|
||||
public command: CustomCommandService = {
|
||||
execute: (id, ...args) => {
|
||||
this.commandService.executeCommand(id, ...args);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import {
|
||||
type URI,
|
||||
type LabelHandler,
|
||||
HoverService,
|
||||
} from '@coze-project-ide/client';
|
||||
import { Tooltip } from '@coze-arch/coze-design';
|
||||
|
||||
// 自定义 IDE HoverService 样式
|
||||
@injectable()
|
||||
class TooltipContribution implements LabelHandler {
|
||||
@inject(HoverService) hoverService: HoverService;
|
||||
|
||||
visible = false;
|
||||
|
||||
@postConstruct()
|
||||
init() {
|
||||
this.hoverService.enableCustomHoverHost();
|
||||
}
|
||||
|
||||
canHandle(uri: URI): number {
|
||||
return 500;
|
||||
}
|
||||
|
||||
renderer(uri: URI, opt?: any): React.ReactNode {
|
||||
// 下边的 opacity、width 设置原因:
|
||||
// semi 源码位置:https://github.com/DouyinFE/semi-design/blob/main/packages/semi-foundation/tooltip/foundation.ts#L342
|
||||
// semi 有 trigger 元素判断,本次自定义 semi 组件没有 focus 内部元素。
|
||||
return opt?.content ? (
|
||||
<Tooltip
|
||||
key={opt.content}
|
||||
content={opt.content}
|
||||
position={opt.position}
|
||||
// 覆盖设置重置 foundation opacity,避免 tooltip 跳动
|
||||
style={{ opacity: 1 }}
|
||||
trigger="custom"
|
||||
getPopupContainer={() => document.body}
|
||||
visible={true}
|
||||
>
|
||||
{/* 宽度 0 避免被全局样式影响导致 Tooltip 定位错误 */}
|
||||
<div style={{ width: 0 }}></div>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
}
|
||||
|
||||
onDispose() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export { TooltipContribution };
|
||||
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* 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 { inject, injectable } from 'inversify';
|
||||
import {
|
||||
ApplicationShell,
|
||||
WidgetManager,
|
||||
type URI,
|
||||
type BoxPanel,
|
||||
OpenerService,
|
||||
Emitter,
|
||||
type Event,
|
||||
type DockLayout,
|
||||
type ReactWidget,
|
||||
LayoutPanelType,
|
||||
ViewRenderer,
|
||||
} from '@coze-project-ide/client';
|
||||
|
||||
import { type ProjectIDEWidget } from '@/widgets/project-ide-widget';
|
||||
import { type WidgetContext } from '@/context';
|
||||
import {
|
||||
UI_BUILDER_URI,
|
||||
MAIN_PANEL_DEFAULT_URI,
|
||||
UI_BUILDER_CONTENT,
|
||||
SECONDARY_SIDEBAR_URI,
|
||||
} from '@/constants';
|
||||
|
||||
import { type ModeType } from '../../types';
|
||||
|
||||
@injectable()
|
||||
export class ViewService {
|
||||
@inject(ApplicationShell)
|
||||
public shell: ApplicationShell;
|
||||
|
||||
@inject(WidgetManager)
|
||||
private widgetManager: WidgetManager;
|
||||
|
||||
@inject(OpenerService)
|
||||
openerService: OpenerService;
|
||||
|
||||
@inject(ViewRenderer)
|
||||
private viewRenderer: ViewRenderer;
|
||||
|
||||
public isFullScreenMode = false;
|
||||
|
||||
protected readonly onSidebarVisibleChangeEmitter = new Emitter<boolean>();
|
||||
readonly onSidebarVisibleChange: Event<boolean> =
|
||||
this.onSidebarVisibleChangeEmitter.event;
|
||||
|
||||
protected readonly onSecondarySidebarVisibleChangeEmitter =
|
||||
new Emitter<boolean>();
|
||||
readonly onSecondarySidebarChange: Event<boolean> =
|
||||
this.onSecondarySidebarVisibleChangeEmitter.event;
|
||||
|
||||
protected readonly onFullScreenModeChangeEmitter = new Emitter<boolean>();
|
||||
readonly onFullScreenModeChange: Event<boolean> =
|
||||
this.onFullScreenModeChangeEmitter.event;
|
||||
|
||||
/**
|
||||
* 主侧边栏功能集合
|
||||
*/
|
||||
public primarySidebar = {
|
||||
onSidebarVisibleChange: this.onSidebarVisibleChange,
|
||||
getVisible: () => this.shell.primarySidebar.isVisible,
|
||||
changeVisible: (vis: boolean) => {
|
||||
if (vis) {
|
||||
this.shell.primarySidebar.show();
|
||||
this.onSidebarVisibleChangeEmitter.fire(true);
|
||||
} else {
|
||||
this.shell.primarySidebar.hide();
|
||||
this.onSidebarVisibleChangeEmitter.fire(false);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
public secondarySidebar = {
|
||||
getVisible: () => this.shell.secondarySidebar.isVisible,
|
||||
changeVisible: (vis: boolean) => {
|
||||
if (vis) {
|
||||
// 打开前需要判断面板是否已经注册打开
|
||||
const secondaryPanel = this.widgetManager.getWidgetFromURI(
|
||||
SECONDARY_SIDEBAR_URI,
|
||||
);
|
||||
if (!secondaryPanel) {
|
||||
this.openerService.open(SECONDARY_SIDEBAR_URI);
|
||||
}
|
||||
this.shell.secondarySidebar.show();
|
||||
this.onSecondarySidebarVisibleChangeEmitter.fire(true);
|
||||
} else {
|
||||
this.shell.secondarySidebar.hide();
|
||||
this.onSecondarySidebarVisibleChangeEmitter.fire(false);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
private switchPanel(uri?: URI) {
|
||||
const uiBuilderPanel = this.widgetManager.getWidgetFromURI(UI_BUILDER_URI);
|
||||
if (uri && UI_BUILDER_URI.match(uri)) {
|
||||
// 跳转到 UIBuilder
|
||||
(this.shell.mainPanel.parent?.parent as BoxPanel).hide();
|
||||
uiBuilderPanel?.show();
|
||||
} else {
|
||||
uiBuilderPanel?.hide();
|
||||
(this.shell.mainPanel.parent?.parent as BoxPanel).show();
|
||||
}
|
||||
}
|
||||
|
||||
async uiBuilderReopen() {
|
||||
const uiBuilderWidget =
|
||||
await this.widgetManager.getOrCreateWidgetFromURI(UI_BUILDER_CONTENT);
|
||||
uiBuilderWidget.dispose();
|
||||
this.openPanel('ui-builder');
|
||||
}
|
||||
|
||||
secondarySidebarReOpen() {
|
||||
if (!this.secondarySidebar.getVisible()) {
|
||||
return;
|
||||
}
|
||||
const secondaryPanel = this.widgetManager.getWidgetFromURI(
|
||||
SECONDARY_SIDEBAR_URI,
|
||||
);
|
||||
secondaryPanel?.dispose();
|
||||
this.openerService.open(SECONDARY_SIDEBAR_URI);
|
||||
}
|
||||
|
||||
async open(uri: URI) {
|
||||
this.switchPanel(uri);
|
||||
// openService
|
||||
await this.openerService.open(uri);
|
||||
}
|
||||
|
||||
async openPanel(type?: ModeType) {
|
||||
if (type === 'ui-builder') {
|
||||
this.switchPanel(UI_BUILDER_URI);
|
||||
const factory = this.widgetManager.getFactoryFromURI(UI_BUILDER_CONTENT)!;
|
||||
const uiBuilderWidget = await this.widgetManager.getOrCreateWidgetFromURI(
|
||||
UI_BUILDER_CONTENT,
|
||||
factory,
|
||||
);
|
||||
this.viewRenderer.addReactPortal(uiBuilderWidget);
|
||||
if (!uiBuilderWidget?.isAttached && uiBuilderWidget) {
|
||||
const uiBuilderPanel = this.widgetManager.getWidgetFromURI(
|
||||
UI_BUILDER_URI,
|
||||
) as BoxPanel;
|
||||
uiBuilderPanel?.addWidget?.(uiBuilderWidget);
|
||||
}
|
||||
} else {
|
||||
this.switchPanel();
|
||||
}
|
||||
}
|
||||
// 打开默认页
|
||||
async openDefault() {
|
||||
await this.openerService.open(MAIN_PANEL_DEFAULT_URI, {
|
||||
mode: 'single-document',
|
||||
});
|
||||
}
|
||||
closeWidgetByUri(uri: URI) {
|
||||
const widget = this.widgetManager.getWidgetFromURI(uri);
|
||||
if (widget) {
|
||||
widget.close();
|
||||
}
|
||||
}
|
||||
|
||||
getWidgetContextFromURI<T>(uri: URI): WidgetContext<T> | undefined {
|
||||
const widgetFromURI = this.widgetManager.getWidgetFromURI(
|
||||
uri,
|
||||
) as ProjectIDEWidget;
|
||||
if (widgetFromURI) {
|
||||
return widgetFromURI.context;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 由于最大分屏数量为 2
|
||||
// 因此 children[0] 为左边分屏,children[1] 为右边分屏
|
||||
splitScreen(direction: 'left' | 'right', widget: ReactWidget) {
|
||||
const mode = direction === 'left' ? 'split-left' : 'split-right';
|
||||
const splitScreenIdx = direction === 'left' ? 0 : 1;
|
||||
|
||||
const layoutConfig = (
|
||||
this.shell.mainPanel?.layout as DockLayout
|
||||
)?.saveLayout()?.main;
|
||||
// 未分屏场景,直接打开
|
||||
if ((layoutConfig as DockLayout.ITabAreaConfig)?.type === 'tab-area') {
|
||||
this.shell.mainPanel.addWidget(widget, {
|
||||
mode,
|
||||
});
|
||||
this.shell.mainPanel.activateWidget(widget);
|
||||
} else if (
|
||||
(layoutConfig as DockLayout.ISplitAreaConfig)?.type === 'split-area'
|
||||
) {
|
||||
const { widgets } = (layoutConfig as DockLayout.ISplitAreaConfig)
|
||||
?.children[splitScreenIdx] as DockLayout.ITabAreaConfig;
|
||||
const tabActivateWidget = widgets.find(_widget => _widget.isVisible);
|
||||
// 已分屏场景
|
||||
this.shell.mainPanel.addWidget(widget, {
|
||||
mode: 'tab-after',
|
||||
ref: tabActivateWidget,
|
||||
});
|
||||
this.shell.mainPanel.activateWidget(widget);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全屏模式切换
|
||||
*/
|
||||
switchFullScreenMode() {
|
||||
if (!this.isFullScreenMode) {
|
||||
this.enableFullScreenMode();
|
||||
} else {
|
||||
this.disableFullScreenMode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启全屏模式
|
||||
* 在 CozeProjectIDE 中,全屏模式隐藏侧边栏和顶部导航栏
|
||||
*/
|
||||
enableFullScreenMode() {
|
||||
if (this.isFullScreenMode) {
|
||||
return;
|
||||
}
|
||||
// 隐藏侧边栏
|
||||
this.primarySidebar.changeVisible(false);
|
||||
// 隐藏顶部导航栏
|
||||
const topBar = this.shell.getPanelFromArea(LayoutPanelType.TOP_BAR);
|
||||
topBar.hide();
|
||||
|
||||
this.isFullScreenMode = true;
|
||||
this.onFullScreenModeChangeEmitter.fire(true);
|
||||
}
|
||||
|
||||
disableFullScreenMode() {
|
||||
if (!this.isFullScreenMode) {
|
||||
return;
|
||||
}
|
||||
// 显示侧边栏
|
||||
this.primarySidebar.changeVisible(true);
|
||||
// 显示顶部导航栏
|
||||
const topBar = this.shell.getPanelFromArea(LayoutPanelType.TOP_BAR);
|
||||
topBar.show();
|
||||
|
||||
this.isFullScreenMode = false;
|
||||
this.onFullScreenModeChangeEmitter.fire(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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 { inject, injectable } from 'inversify';
|
||||
import {
|
||||
CommandRegistry,
|
||||
Emitter,
|
||||
type CustomTitleType,
|
||||
type Event,
|
||||
} from '@coze-project-ide/client';
|
||||
|
||||
import { ProjectIDEWidget } from '@/widgets/project-ide-widget';
|
||||
import { type WidgetTitleRender } from '@/types/client';
|
||||
import { type WidgetRegistry, type WidgetUIState } from '@/types';
|
||||
@injectable()
|
||||
export class WidgetService {
|
||||
@inject(ProjectIDEWidget) public widget: ProjectIDEWidget;
|
||||
|
||||
@inject(CommandRegistry) private commandRegistry: CommandRegistry;
|
||||
|
||||
private _uiState: WidgetUIState = 'loading';
|
||||
|
||||
private _title: string;
|
||||
|
||||
private _iconType: string;
|
||||
|
||||
private _widgetTitleRender: WidgetTitleRender;
|
||||
|
||||
private registry: WidgetRegistry;
|
||||
|
||||
readonly onFocusEmitter = new Emitter<void>();
|
||||
|
||||
readonly onFocus: Event<void> = this.onFocusEmitter.event;
|
||||
|
||||
readonly onTitleChangedEmitter = new Emitter<string>();
|
||||
|
||||
readonly onTitleChanged: Event<string> = this.onTitleChangedEmitter.event;
|
||||
|
||||
readonly onIconTypeChangeEmitter = new Emitter<string>();
|
||||
|
||||
readonly onIconTypeChanged: Event<string> =
|
||||
this.onIconTypeChangeEmitter.event;
|
||||
|
||||
init(factory: WidgetRegistry, widgetTitleRender: WidgetTitleRender) {
|
||||
this.registry = factory;
|
||||
this._widgetTitleRender = widgetTitleRender;
|
||||
this.setTitle(this._title);
|
||||
}
|
||||
|
||||
/** 触发重渲染 */
|
||||
update() {
|
||||
(this.widget.title as CustomTitleType).iconLabel = this._widgetTitleRender({
|
||||
commandRegistry: this.commandRegistry,
|
||||
registry: this.registry,
|
||||
uiState: this._uiState,
|
||||
title: this._title,
|
||||
widget: this.widget,
|
||||
}) as any;
|
||||
(this.widget.title as CustomTitleType).saving = this._uiState === 'saving';
|
||||
}
|
||||
|
||||
setTitle(title: string, uiState?: WidgetUIState) {
|
||||
if (this._title !== title) {
|
||||
this.onTitleChangedEmitter.fire(title);
|
||||
}
|
||||
this._title = title;
|
||||
if (uiState) {
|
||||
this._uiState = uiState;
|
||||
}
|
||||
this.update();
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return this._title;
|
||||
}
|
||||
|
||||
getUIState() {
|
||||
return this._uiState;
|
||||
}
|
||||
|
||||
setUIState(uiState: WidgetUIState) {
|
||||
if (this._uiState !== uiState) {
|
||||
this._uiState = uiState;
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
getIconType(): string {
|
||||
return this._iconType;
|
||||
}
|
||||
|
||||
setIconType(iconType: string) {
|
||||
if (this._iconType !== iconType) {
|
||||
this.onIconTypeChangeEmitter.fire(iconType);
|
||||
}
|
||||
this._iconType = iconType;
|
||||
this.update();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.widget.close();
|
||||
}
|
||||
}
|
||||
@@ -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 React from 'react';
|
||||
|
||||
import { Spin } from '@coze-arch/coze-design';
|
||||
import { useCurrentWidget } from '@coze-project-ide/client';
|
||||
|
||||
import { type ProjectIDEWidget } from '@/widgets/project-ide-widget';
|
||||
import { type RegistryHandler } from '@/types';
|
||||
|
||||
import { useMount } from './use-mount';
|
||||
import { useLifeCycle } from './use-lifecycle';
|
||||
|
||||
export const withRegistryContent = (registry: RegistryHandler<any>) => {
|
||||
const WidgetComp = () => {
|
||||
const widget: ProjectIDEWidget = useCurrentWidget();
|
||||
|
||||
const { context } = widget;
|
||||
|
||||
useLifeCycle(registry, context, widget);
|
||||
|
||||
const { loaded, mounted, content } = useMount(registry, widget);
|
||||
|
||||
return loaded && mounted ? content : <Spin />;
|
||||
};
|
||||
return WidgetComp;
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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, useCallback } from 'react';
|
||||
|
||||
import {
|
||||
ApplicationShell,
|
||||
ContextKeyService,
|
||||
type ReactWidget,
|
||||
useIDEService,
|
||||
} from '@coze-project-ide/client';
|
||||
|
||||
import { type ProjectIDEWidget } from '@/widgets/project-ide-widget';
|
||||
import { type RegistryHandler } from '@/types';
|
||||
import { type WidgetContext } from '@/context/widget-context';
|
||||
|
||||
import { LifecycleService } from '../lifecycle-service';
|
||||
|
||||
export const useLifeCycle = (
|
||||
registry: RegistryHandler,
|
||||
widgetContext: WidgetContext,
|
||||
widget?: ReactWidget,
|
||||
) => {
|
||||
const lifecycleService = useIDEService<LifecycleService>(LifecycleService);
|
||||
const contextKeyService = useIDEService<ContextKeyService>(ContextKeyService);
|
||||
const setContextKey = useCallback(() => {
|
||||
registry?.onFocus?.(widgetContext);
|
||||
contextKeyService.setContext('widgetFocus', widget?.uri);
|
||||
contextKeyService.setContext('widgetContext', widgetContext);
|
||||
}, [widgetContext]);
|
||||
const shell = useIDEService<ApplicationShell>(ApplicationShell);
|
||||
// 生命周期管理
|
||||
useEffect(() => {
|
||||
const currentUri = (shell.mainPanel.currentTitle?.owner as ProjectIDEWidget)
|
||||
?.uri;
|
||||
if (currentUri && widget?.uri?.match(currentUri)) {
|
||||
setContextKey();
|
||||
}
|
||||
const listenActivate = lifecycleService.onFocus(title => {
|
||||
if (
|
||||
(title.owner as ReactWidget).uri?.toString() === widget?.uri?.toString()
|
||||
) {
|
||||
setContextKey();
|
||||
}
|
||||
});
|
||||
const listenDispose = widget?.onDispose?.(() => {
|
||||
registry?.onDispose?.(widgetContext);
|
||||
});
|
||||
return () => {
|
||||
listenActivate?.dispose?.();
|
||||
listenDispose?.dispose?.();
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useLayoutEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
|
||||
import { isFunction } from 'lodash-es';
|
||||
|
||||
import { type ProjectIDEWidget } from '@/widgets/project-ide-widget';
|
||||
import { type RegistryHandler } from '@/types';
|
||||
|
||||
export const useMount = (
|
||||
registry: RegistryHandler,
|
||||
widget: ProjectIDEWidget,
|
||||
) => {
|
||||
/**
|
||||
* 是否已经挂载
|
||||
*/
|
||||
const [mounted, setMounted] = useState(widget.isVisible);
|
||||
const [version, setVersion] = useState(0);
|
||||
const mountedRef = useRef(widget.isVisible);
|
||||
|
||||
/**
|
||||
* 是否已加载完成
|
||||
*/
|
||||
const [loaded, setLoaded] = useState(!registry.load);
|
||||
|
||||
/**
|
||||
* renderContent 函数结果缓存
|
||||
* 由于 registry 和 widget 基本不变,可以保证在同一个 widget 中 renderContent 函数只会运行一次
|
||||
* 除非 WidgetComp 组件被卸载 =.=
|
||||
*/
|
||||
const content = useMemo(() => {
|
||||
if (!isFunction(registry.renderContent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return registry.renderContent(widget.context, widget);
|
||||
}, [registry, widget, version]);
|
||||
|
||||
/**
|
||||
* 支持 registry 定义加载函数
|
||||
*/
|
||||
const load = useCallback(async () => {
|
||||
if (!registry.load || !isFunction(registry.load)) {
|
||||
return;
|
||||
}
|
||||
await registry.load(widget.context);
|
||||
setLoaded(true);
|
||||
}, [registry, widget, setLoaded]);
|
||||
|
||||
/**
|
||||
* 监听 widget 的显示隐藏状态,若 widget 显示且未挂载,则需要主动挂载一次
|
||||
*/
|
||||
const watchWidgetStatus = useCallback(
|
||||
(w: ProjectIDEWidget) => {
|
||||
const { isVisible } = w;
|
||||
if (isVisible && !mountedRef.current) {
|
||||
setMounted(true);
|
||||
mountedRef.current = true;
|
||||
}
|
||||
return w.onDidChangeVisibility(visible => {
|
||||
if (visible && !mountedRef.current) {
|
||||
setMounted(true);
|
||||
mountedRef.current = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
[setMounted, mountedRef],
|
||||
);
|
||||
|
||||
/**
|
||||
* 监听器可以较早挂载,避免多渲染一次
|
||||
*/
|
||||
useLayoutEffect(() => {
|
||||
const dispose = watchWidgetStatus(widget);
|
||||
const disposeRefresh = widget.onRefresh(() => {
|
||||
setVersion(prev => prev + 1);
|
||||
});
|
||||
return () => {
|
||||
dispose.dispose();
|
||||
disposeRefresh.dispose();
|
||||
};
|
||||
}, [widget, watchWidgetStatus]);
|
||||
|
||||
/**
|
||||
* 加载函数时机暂无特殊设计,先保持和历史逻辑一致
|
||||
*/
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
return {
|
||||
loaded,
|
||||
mounted,
|
||||
content,
|
||||
};
|
||||
};
|
||||
19
frontend/packages/project-ide/framework/src/plugins/index.ts
Normal file
19
frontend/packages/project-ide/framework/src/plugins/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 { createPresetPlugin } from './create-preset-plugin';
|
||||
export { createContextMenuPlugin } from './create-context-menu-plugin';
|
||||
export { createCloseConfirmPlugin } from './close-confirm-plugin';
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { injectable } from 'inversify';
|
||||
import { Emitter, type Event } from '@coze-project-ide/client';
|
||||
|
||||
@injectable()
|
||||
export class ErrorService {
|
||||
private readonly onErrorEmitter = new Emitter<void>();
|
||||
readonly onError: Event<void> = this.onErrorEmitter.event;
|
||||
|
||||
toErrorPage() {
|
||||
this.onErrorEmitter.fire();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ModalService, ModalType } from './modal-service';
|
||||
export { ErrorService } from './error-service';
|
||||
export {
|
||||
MessageEventService,
|
||||
type MessageEvent,
|
||||
} from './message-event-service';
|
||||
export { WsService, OptionsService } from '@coze-project-ide/base-adapter';
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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 { injectable, inject } from 'inversify';
|
||||
import { Emitter, type URI, WidgetManager } from '@coze-project-ide/client';
|
||||
|
||||
export interface MessageEvent<T = any> {
|
||||
name: string;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* widget 的通信服务
|
||||
*/
|
||||
@injectable()
|
||||
export class MessageEventService {
|
||||
@inject(WidgetManager) widgetManager: WidgetManager;
|
||||
|
||||
/**
|
||||
* 消息队列
|
||||
*/
|
||||
events = new Map<string, MessageEvent[]>();
|
||||
|
||||
onSendEmitter = new Emitter<MessageEvent & { uri: URI }>();
|
||||
onSend = this.onSendEmitter.event;
|
||||
|
||||
private toKey(uri: URI) {
|
||||
// 通过 uri 获取 widget 的唯一索引
|
||||
return this.widgetManager.uriToWidgetID(uri);
|
||||
}
|
||||
|
||||
/** 通过 uri 获取消息队列 */
|
||||
private get(uri: URI): MessageEvent[] {
|
||||
const key = this.toKey(uri);
|
||||
if (this.events.has(key)) {
|
||||
return this.events.get(key)!;
|
||||
}
|
||||
const queue = [];
|
||||
this.events.set(key, queue);
|
||||
return queue;
|
||||
}
|
||||
private delete(uri: URI) {
|
||||
const key = this.toKey(uri);
|
||||
return this.events.delete(key);
|
||||
}
|
||||
|
||||
send(uri: URI, msg: MessageEvent) {
|
||||
this.get(uri).push(msg);
|
||||
this.onSendEmitter.fire({ uri, ...msg });
|
||||
}
|
||||
|
||||
on(uri: URI) {
|
||||
const queue = this.get(uri);
|
||||
this.delete(uri);
|
||||
return queue;
|
||||
}
|
||||
|
||||
compare(uriA: URI, uriB: URI) {
|
||||
return this.toKey(uriA) === this.toKey(uriB);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @coze-arch/use-error-in-catch */
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { Emitter, type Event } from '@coze-project-ide/client';
|
||||
import { OptionsService } from '@coze-project-ide/base-adapter';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { sleep } from '@coze-arch/bot-utils';
|
||||
import {
|
||||
type ResourceCopyDispatchRequest,
|
||||
type ResourceCopyScene,
|
||||
type ResourceCopyTaskDetail,
|
||||
ResType,
|
||||
TaskStatus,
|
||||
} from '@coze-arch/bot-api/plugin_develop';
|
||||
import { PluginDevelopApi } from '@coze-arch/bot-api';
|
||||
|
||||
export enum ModalType {
|
||||
RESOURCE = 'resource',
|
||||
CLOSE_CONFIRM = 'close-confirm',
|
||||
}
|
||||
|
||||
interface EmitterProps {
|
||||
type: ModalType;
|
||||
options?: any;
|
||||
// 不传默认打开窗口
|
||||
visible?: boolean;
|
||||
scene?: ResourceCopyScene;
|
||||
resourceName?: string;
|
||||
}
|
||||
|
||||
const POLLING_DELAY = 1000;
|
||||
|
||||
/**
|
||||
* 弹窗轮询服务
|
||||
* startPolling 开始轮询
|
||||
* retry 重试
|
||||
* onCloseResourceModal 关闭轮询弹窗
|
||||
*/
|
||||
@injectable()
|
||||
export class ModalService {
|
||||
@inject(OptionsService) options: OptionsService;
|
||||
readonly onModalVisibleChangeEmitter = new Emitter<EmitterProps>();
|
||||
readonly onModalVisibleChange: Event<EmitterProps> =
|
||||
this.onModalVisibleChangeEmitter.event;
|
||||
readonly onCancelEmitter: Emitter<void> = new Emitter<void>();
|
||||
readonly onCancel: Event<void> = this.onCancelEmitter.event;
|
||||
protected readonly pollingDelay = 1000;
|
||||
|
||||
readonly onErrorEmitter = new Emitter<boolean | string>();
|
||||
readonly onError: Event<boolean | string> = this.onErrorEmitter.event;
|
||||
|
||||
readonly onSuccessEmitter = new Emitter<ResourceCopyTaskDetail | undefined>();
|
||||
readonly onSuccess: Event<ResourceCopyTaskDetail | undefined> =
|
||||
this.onSuccessEmitter.event;
|
||||
|
||||
protected _stopPolling = false;
|
||||
private _taskId?: string;
|
||||
|
||||
// 开始轮询
|
||||
async startPolling(props: ResourceCopyDispatchRequest) {
|
||||
this._stopPolling = false;
|
||||
|
||||
const resourceName = props.res_name;
|
||||
|
||||
try {
|
||||
// 1. 请求接口,获取 taskId
|
||||
this.onModalVisibleChangeEmitter.fire({
|
||||
type: ModalType.RESOURCE,
|
||||
scene: props.scene,
|
||||
resourceName,
|
||||
});
|
||||
const { task_id, failed_reasons } =
|
||||
await PluginDevelopApi.ResourceCopyDispatch(props);
|
||||
this._taskId = task_id;
|
||||
|
||||
if (failed_reasons?.length) {
|
||||
let errorInfo = '';
|
||||
// workflow 特定文案
|
||||
if (
|
||||
failed_reasons.some(reason => reason.res_type === ResType.Workflow)
|
||||
) {
|
||||
errorInfo = I18n.t('resource_copy_move_notify');
|
||||
} else {
|
||||
errorInfo = failed_reasons.reduce((allInfo, item) => {
|
||||
const currentError = `${item.res_name || ''}${item.reason || ''}`;
|
||||
if (allInfo) {
|
||||
return `${allInfo}\n${currentError}`;
|
||||
} else {
|
||||
return currentError;
|
||||
}
|
||||
}, '');
|
||||
}
|
||||
this.onErrorEmitter.fire(errorInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 轮询接口,根据 taskId 获取任务状态
|
||||
if (task_id) {
|
||||
this.doPolling(task_id);
|
||||
} else {
|
||||
this.onErrorEmitter.fire('no_task_id');
|
||||
}
|
||||
} catch (e) {
|
||||
this.onErrorEmitter.fire(true);
|
||||
}
|
||||
}
|
||||
|
||||
async retry() {
|
||||
this._stopPolling = false;
|
||||
if (this._taskId) {
|
||||
// 1. retry 接口
|
||||
await PluginDevelopApi.ResourceCopyRetry({
|
||||
task_id: this._taskId,
|
||||
});
|
||||
this.onErrorEmitter.fire(false);
|
||||
// 2. 开始轮询
|
||||
this.doPolling(this._taskId);
|
||||
}
|
||||
}
|
||||
|
||||
async doPolling(taskId: string) {
|
||||
this._taskId = taskId;
|
||||
if (this._stopPolling) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const taskInfo = await this.polling();
|
||||
const { status } = taskInfo || {};
|
||||
await sleep(POLLING_DELAY);
|
||||
|
||||
if (this._taskId && !this._stopPolling) {
|
||||
// 更新弹窗内 info 信息
|
||||
this.onModalVisibleChangeEmitter.fire({
|
||||
type: ModalType.RESOURCE,
|
||||
scene: taskInfo?.scene,
|
||||
resourceName: taskInfo?.res_name,
|
||||
});
|
||||
}
|
||||
if (status === TaskStatus.Processing) {
|
||||
this.doPolling(taskId);
|
||||
} else if (status === TaskStatus.Successed) {
|
||||
this._stopPolling = true;
|
||||
this.onModalVisibleChangeEmitter.fire({
|
||||
type: ModalType.RESOURCE,
|
||||
visible: false,
|
||||
});
|
||||
this.onSuccessEmitter.fire(taskInfo);
|
||||
} else {
|
||||
this.onErrorEmitter.fire(true);
|
||||
}
|
||||
} catch (_e) {
|
||||
this._stopPolling = true;
|
||||
this.onErrorEmitter.fire(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询请求接口,返回轮询状态
|
||||
*/
|
||||
private async polling(): Promise<ResourceCopyTaskDetail | undefined> {
|
||||
try {
|
||||
const { task_detail } = await PluginDevelopApi.ResourceCopyDetail({
|
||||
task_id: this._taskId,
|
||||
});
|
||||
|
||||
return task_detail;
|
||||
} catch (e) {
|
||||
this._stopPolling = true;
|
||||
this.onErrorEmitter.fire(true);
|
||||
return {
|
||||
status: TaskStatus.Failed,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async onCloseResourceModal() {
|
||||
this._stopPolling = true;
|
||||
// 关闭 modal
|
||||
this.onModalVisibleChangeEmitter.fire({
|
||||
type: ModalType.RESOURCE,
|
||||
visible: false,
|
||||
});
|
||||
this.onCancelEmitter.fire();
|
||||
if (this._taskId) {
|
||||
// 停止轮询的请求
|
||||
await PluginDevelopApi.ResourceCopyCancel({
|
||||
task_id: this._taskId,
|
||||
});
|
||||
this._taskId = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
92
frontend/packages/project-ide/framework/src/types/client.ts
Normal file
92
frontend/packages/project-ide/framework/src/types/client.ts
Normal file
@@ -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 { type ReactElement } from 'react';
|
||||
|
||||
import {
|
||||
type CommandRegistry,
|
||||
type ReactWidget,
|
||||
} from '@coze-project-ide/client';
|
||||
|
||||
import { type ProjectIDEWidget } from '@/widgets/project-ide-widget';
|
||||
|
||||
import { type WidgetUIState } from './widget';
|
||||
import { type WidgetRegistry } from './registry';
|
||||
|
||||
export const ProjectIDEClientProps = Symbol('ProjectIDEClientProps');
|
||||
|
||||
export interface TitlePropsType {
|
||||
commandRegistry: CommandRegistry;
|
||||
registry: WidgetRegistry;
|
||||
title: string;
|
||||
widget?: ReactWidget;
|
||||
uiState: WidgetUIState;
|
||||
}
|
||||
|
||||
export type WidgetTitleRender = (
|
||||
props: TitlePropsType,
|
||||
) => ReactElement<any, any>;
|
||||
|
||||
export interface ProjectIDEClientProps {
|
||||
view: {
|
||||
/**
|
||||
* 主编辑区域渲染内容
|
||||
*/
|
||||
widgetRegistries: WidgetRegistry[];
|
||||
/**
|
||||
* 默认渲染页
|
||||
*/
|
||||
widgetDefaultRender: () => ReactElement<any, any>;
|
||||
/**
|
||||
* widget 兜底报错,如果 widget 挂掉会渲染该组件,发送埋点
|
||||
*/
|
||||
widgetFallbackRender?: (props: {
|
||||
widget: ReactWidget;
|
||||
}) => ReactElement<any, any>;
|
||||
/**
|
||||
* 统一标题渲染
|
||||
*/
|
||||
widgetTitleRender: WidgetTitleRender;
|
||||
/**
|
||||
* 主侧边栏渲染
|
||||
*/
|
||||
primarySideBar: () => ReactElement<any, any>;
|
||||
/**
|
||||
* 辅助侧边栏渲染
|
||||
*/
|
||||
secondarySidebar?: () => ReactElement<any, any>;
|
||||
/**
|
||||
* 主侧边栏底部分区 configuration 配置渲染
|
||||
*/
|
||||
configuration?: () => ReactElement<any, any>;
|
||||
/**
|
||||
* 前置工具栏渲染
|
||||
*/
|
||||
preToolbar?: () => ReactElement<any, any>;
|
||||
/**
|
||||
* 后置工具栏渲染
|
||||
*/
|
||||
toolbar?: (widget: ProjectIDEWidget) => ReactElement<any, any>;
|
||||
/**
|
||||
* 顶部导航栏
|
||||
*/
|
||||
topBar: () => ReactElement<any, any>;
|
||||
/**
|
||||
* uibuilder
|
||||
*/
|
||||
uiBuilder: () => ReactElement<any, any> | null;
|
||||
};
|
||||
}
|
||||
23
frontend/packages/project-ide/framework/src/types/index.ts
Normal file
23
frontend/packages/project-ide/framework/src/types/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { type WidgetRegistry, RegistryHandler } from './registry';
|
||||
export type { WidgetUIState } from './widget';
|
||||
export { type ProjectIDEServices } from './services';
|
||||
export { ProjectIDEClientProps } from './client';
|
||||
export type { TitlePropsType } from './client';
|
||||
export { type ResourceType, type ModeType } from './resource';
|
||||
export { type WsMessageProps } from '@coze-project-ide/base-interface';
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 URI,
|
||||
type ReactWidget,
|
||||
type LayoutPanelType,
|
||||
} from '@coze-project-ide/client';
|
||||
|
||||
import { type WidgetContext } from '@/context/widget-context';
|
||||
|
||||
import { type CommandItem, type MenuItem, type ShortcutItem } from './services';
|
||||
|
||||
export interface WidgetRegistry<T = any> {
|
||||
// widget 渲染区域
|
||||
area?: LayoutPanelType;
|
||||
// 规则匹配
|
||||
match: RegExp;
|
||||
canClose?: () => boolean;
|
||||
// 数据存储
|
||||
createStore?: (uri?: URI) => T;
|
||||
// 注册
|
||||
registerCommands?: () => CommandItem<T>[];
|
||||
registerShortcuts?: () => ShortcutItem[];
|
||||
registerContextMenu?: () => MenuItem[];
|
||||
renderStatusbar?: (ctx: WidgetContext<T>) => void;
|
||||
renderIcon?: (ctx: WidgetContext<T>) => React.ReactElement<any, any>;
|
||||
renderContent: (
|
||||
ctx: WidgetContext<T>,
|
||||
widget?: ReactWidget,
|
||||
) => React.ReactElement<any, any>;
|
||||
|
||||
// 生命周期
|
||||
load?: (ctx: WidgetContext<T>) => Promise<void>;
|
||||
/**
|
||||
* 注意:分屏场景,如果有一个面板之前未展示,会先 focus 那个面板,然后 focus 当前选中的面板。
|
||||
*/
|
||||
onFocus?: (ctx: WidgetContext<T>) => void;
|
||||
/**
|
||||
* 业务侧销毁逻辑
|
||||
* createStore 的销毁逻辑由业务侧自行处理
|
||||
*/
|
||||
onDispose?: (ctx: WidgetContext<T>) => void;
|
||||
}
|
||||
|
||||
export const RegistryHandler = Symbol('RegistryHandler');
|
||||
|
||||
export type RegistryHandler<T = any> = WidgetRegistry<T>;
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 资源类型
|
||||
*/
|
||||
export type ResourceType = 'workflow' | 'knowledge' | 'ui-builder';
|
||||
/**
|
||||
* 模式类型
|
||||
*/
|
||||
export type ModeType = 'dev' | 'ui-builder';
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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 ViewService } from '@/plugins/create-preset-plugin/view-service';
|
||||
import { type WidgetContext } from '@/context/widget-context';
|
||||
|
||||
export interface CommandItem<T> {
|
||||
id: string;
|
||||
label: string;
|
||||
when?: 'widgetFocus';
|
||||
execute: (ctx?: WidgetContext, props?: T) => void;
|
||||
isEnable: (ctx?: WidgetContext, props?: T) => boolean;
|
||||
}
|
||||
|
||||
export interface ShortcutItem {
|
||||
// 命令系统中绑定的 id
|
||||
commandId: string;
|
||||
// 快捷键
|
||||
keybinding: string;
|
||||
// 是否阻止浏览器原生行为
|
||||
preventDefault: boolean;
|
||||
}
|
||||
|
||||
export interface CommandService {
|
||||
execute: (id: string, ...args: any[]) => void; // 执行命令
|
||||
}
|
||||
|
||||
export interface MenuItem {
|
||||
/**
|
||||
* 使用已经注册的 command 的 id
|
||||
*/
|
||||
commandId: string;
|
||||
/**
|
||||
* 元素选择器
|
||||
* 类:.class
|
||||
* id:#id
|
||||
*/
|
||||
selector: string;
|
||||
/**
|
||||
* 子菜单
|
||||
*/
|
||||
submenu?: MenuItem[];
|
||||
}
|
||||
|
||||
export interface ContextMenuService {
|
||||
open: (e: React.MouseEvent) => boolean; // 没有任何菜单注册项,返回 false
|
||||
registerContextMenu: (options: MenuItem[], match?: RegExp) => void; // 入参形同 widgetRegistry 里的 registerContextMenu
|
||||
}
|
||||
|
||||
export interface ProjectIDEServices {
|
||||
contextmenu: ContextMenuService; // 右键菜单服务
|
||||
command: CommandService; // 命令服务
|
||||
view: ViewService;
|
||||
}
|
||||
17
frontend/packages/project-ide/framework/src/types/widget.ts
Normal file
17
frontend/packages/project-ide/framework/src/types/widget.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type WidgetUIState = 'loading' | 'saving' | 'error' | 'normal';
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/** 保留的查询参数 */
|
||||
const PRESERVED_SEARCH_PARAMS = ['commit_version'];
|
||||
|
||||
/**
|
||||
* 给指定 url 添加特定的 search params
|
||||
* @param url 当前 url
|
||||
* @returns
|
||||
*/
|
||||
export function addPreservedSearchParams(url: string) {
|
||||
if (!url) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const newSearchParams = new URLSearchParams();
|
||||
|
||||
for (const param of PRESERVED_SEARCH_PARAMS) {
|
||||
const value = searchParams.get(param);
|
||||
if (value && !url.includes(`${param}=`)) {
|
||||
newSearchParams.append(param, value);
|
||||
}
|
||||
}
|
||||
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
const qs = newSearchParams.toString();
|
||||
return qs ? `${url}${separator}${qs}` : url;
|
||||
}
|
||||
@@ -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 {
|
||||
type ApplicationShell,
|
||||
createBoxLayout,
|
||||
type BoxLayout,
|
||||
createSplitLayout,
|
||||
SplitPanel,
|
||||
BoxPanel,
|
||||
} from '@coze-project-ide/client';
|
||||
|
||||
export const customLayout = (
|
||||
shell: ApplicationShell,
|
||||
uiBuilderPanel: BoxPanel,
|
||||
): BoxLayout => {
|
||||
const bottomSplitLayout = createSplitLayout([shell.mainPanel], [1], {
|
||||
orientation: 'vertical',
|
||||
spacing: 0,
|
||||
});
|
||||
shell.bottomSplitLayout = bottomSplitLayout;
|
||||
const middleContentPanel = new SplitPanel({ layout: bottomSplitLayout });
|
||||
|
||||
const leftRightSplitLayout = createBoxLayout(
|
||||
[
|
||||
// 左边的不可伸缩 bar
|
||||
shell.primarySidebar,
|
||||
middleContentPanel,
|
||||
],
|
||||
[0, 1],
|
||||
{
|
||||
direction: 'left-to-right',
|
||||
spacing: 6,
|
||||
},
|
||||
);
|
||||
const mainDockPanel = new BoxPanel({ layout: leftRightSplitLayout });
|
||||
|
||||
const centerLayout = createBoxLayout(
|
||||
[mainDockPanel, uiBuilderPanel, shell.secondarySidebar],
|
||||
[1, 0, 0],
|
||||
{
|
||||
direction: 'left-to-right',
|
||||
},
|
||||
);
|
||||
const centerPanel = new BoxPanel({ layout: centerLayout });
|
||||
|
||||
return createBoxLayout([shell.topPanel, centerPanel], [0, 1], {
|
||||
direction: 'top-to-bottom',
|
||||
spacing: 0,
|
||||
});
|
||||
};
|
||||
29
frontend/packages/project-ide/framework/src/utils/index.ts
Normal file
29
frontend/packages/project-ide/framework/src/utils/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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 { customLayout } from './custom-layout';
|
||||
export { withLazyLoad } from './with-lazy-load';
|
||||
export {
|
||||
getResourceByPathname,
|
||||
getResourceByURI,
|
||||
getURIByResource,
|
||||
getURIPathByPathname,
|
||||
getURIByPath,
|
||||
getPathnameByURI,
|
||||
compareURI,
|
||||
getURLByURI,
|
||||
} from './resource-tools';
|
||||
export { addPreservedSearchParams } from './add-preserved-search-params';
|
||||
@@ -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 { URI } from '@coze-project-ide/client';
|
||||
|
||||
import { URI_SCHEME } from '../constants';
|
||||
|
||||
/**
|
||||
* 从给定的 url 字符串中解析出 resourceType 和 resourceId;
|
||||
*/
|
||||
export const getResourceByPathname = (pathname: string) => {
|
||||
let resourceType: undefined | string;
|
||||
let resourceId: undefined | string;
|
||||
|
||||
const regex = /space\/\d+\/project-ide\/\d+\/([^\/]+)(?:\/([^\/]+))?/;
|
||||
const match = pathname.match(regex);
|
||||
|
||||
if (match) {
|
||||
resourceType = match[1];
|
||||
resourceId = match[2];
|
||||
}
|
||||
|
||||
return {
|
||||
resourceType,
|
||||
resourceId,
|
||||
};
|
||||
};
|
||||
|
||||
export const getURIPathByPathname = (pathname: string) => {
|
||||
const match = pathname.match(/space\/[^/]+\/project-ide\/[^/]+\/(.*)/);
|
||||
return match ? match[1] : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 从 uri 上解析 resourceType 和 resourceId
|
||||
*/
|
||||
export const getResourceByURI = (uri: URI) => {
|
||||
/**
|
||||
* TODO: 这样解析有些粗暴了,后面要调整一下
|
||||
*/
|
||||
const resourceType = uri.path.dir.base;
|
||||
const resourceId = uri.path.base;
|
||||
|
||||
return {
|
||||
resourceType,
|
||||
resourceId,
|
||||
};
|
||||
};
|
||||
|
||||
export const getPathnameByURI = (uri: URI) => uri.path.toString();
|
||||
|
||||
/**
|
||||
* 根据 resourceType 和 resourceId 生成 URI
|
||||
*/
|
||||
export const getURIByResource = (
|
||||
resourceType: string,
|
||||
resourceId: string,
|
||||
query?: string,
|
||||
) =>
|
||||
new URI(
|
||||
`${URI_SCHEME}:///${resourceType}/${resourceId}${query ? `?${query}` : ''}`,
|
||||
);
|
||||
|
||||
export const getURIByPath = (path: string) =>
|
||||
new URI(`${URI_SCHEME}:///${path}`);
|
||||
|
||||
/**
|
||||
* 将 uri 转化为 url
|
||||
*/
|
||||
export const getURLByURI = (uri: URI) =>
|
||||
`${uri.path.toString()}${uri.query ? `${uri.query}` : ''}${
|
||||
uri.fragment ? `#${uri.fragment}` : ''
|
||||
}`;
|
||||
|
||||
/**
|
||||
* 执行 URI 比对,完全一致返回 true,否则返回 false
|
||||
*/
|
||||
export const compareURI = (uri1?: URI, uri2?: URI) => {
|
||||
if (!uri1 || !uri2) {
|
||||
return false;
|
||||
}
|
||||
return uri1.toString() === uri2.toString();
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
|
||||
const withLazyLoad = (
|
||||
importFunc: () => Promise<{ default: React.ComponentType<any> }>,
|
||||
fallback?: React.ReactNode,
|
||||
) => {
|
||||
const Component = lazy(importFunc);
|
||||
const LazyComponent = () => (
|
||||
<Suspense fallback={fallback}>
|
||||
<Component />
|
||||
</Suspense>
|
||||
);
|
||||
return LazyComponent;
|
||||
};
|
||||
|
||||
export { withLazyLoad };
|
||||
@@ -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 React from 'react';
|
||||
|
||||
import { inject, injectable } from 'inversify';
|
||||
|
||||
import { ProjectIDEClientProps } from '@/types';
|
||||
|
||||
import { ProjectIDEWidget } from '../project-ide-widget';
|
||||
|
||||
@injectable()
|
||||
export class ConfigWidget extends ProjectIDEWidget {
|
||||
@inject(ProjectIDEClientProps) props: ProjectIDEClientProps;
|
||||
|
||||
render() {
|
||||
const Component = this.props.view.configuration;
|
||||
if (Component) {
|
||||
return <Component />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 { injectable, type interfaces } from 'inversify';
|
||||
import { SplitWidget, type URI } from '@coze-project-ide/client';
|
||||
|
||||
import { type WidgetContext } from '@/context';
|
||||
|
||||
import { SIDEBAR_RESOURCE_URI, SIDEBAR_CONFIG_URI } from '../../constants/uri';
|
||||
import { ResourceWidget } from './resource-widget';
|
||||
import { ConfigWidget } from './config-widget';
|
||||
|
||||
@injectable()
|
||||
export class PrimarySidebarWidget extends SplitWidget {
|
||||
context: WidgetContext;
|
||||
|
||||
container: interfaces.Container;
|
||||
|
||||
render(): any {
|
||||
return null;
|
||||
}
|
||||
|
||||
init(uri: URI) {
|
||||
this.orientation = 'vertical';
|
||||
this.defaultStretch = [0.7, 0.3];
|
||||
this.splitPanels = [
|
||||
{
|
||||
widgetUri: SIDEBAR_RESOURCE_URI,
|
||||
widget: ResourceWidget,
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
widgetUri: SIDEBAR_CONFIG_URI,
|
||||
widget: ConfigWidget,
|
||||
order: 2,
|
||||
},
|
||||
];
|
||||
super.init(uri);
|
||||
}
|
||||
}
|
||||
@@ -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 React from 'react';
|
||||
|
||||
import { inject, injectable } from 'inversify';
|
||||
|
||||
import { ProjectIDEClientProps } from '@/types';
|
||||
|
||||
import { ProjectIDEWidget } from '../project-ide-widget';
|
||||
|
||||
@injectable()
|
||||
export class ResourceWidget extends ProjectIDEWidget {
|
||||
@inject(ProjectIDEClientProps) props: ProjectIDEClientProps;
|
||||
|
||||
render(): any {
|
||||
const Component = this.props.view.primarySideBar;
|
||||
if (Component) {
|
||||
return <Component />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 interfaces } from 'inversify';
|
||||
import { Emitter, ReactWidget } from '@coze-project-ide/client';
|
||||
|
||||
import { type WidgetContext } from '@/context/widget-context';
|
||||
|
||||
export class ProjectIDEWidget extends ReactWidget {
|
||||
context: WidgetContext;
|
||||
|
||||
container: interfaces.Container;
|
||||
|
||||
private onRefreshEmitter = new Emitter<void>();
|
||||
|
||||
onRefresh = this.onRefreshEmitter.event;
|
||||
|
||||
refresh() {
|
||||
this.onRefreshEmitter.fire();
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.scrollOptions = {
|
||||
minScrollbarLength: 35,
|
||||
};
|
||||
}
|
||||
|
||||
render(): any {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
51
frontend/packages/project-ide/framework/tsconfig.build.json
Normal file
51
frontend/packages/project-ide/framework/tsconfig.build.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"types": ["react", "react-dom"],
|
||||
"jsx": "react",
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib-ts",
|
||||
"tsBuildInfoFile": "./lib-ts/tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["./src", "./src/**/*.json"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../arch/bot-api/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-flags/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-typings/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-utils/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/i18n/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../base-adapter/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../base-interface/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../client/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/eslint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/ts-config/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/project-ide/framework/tsconfig.json
Normal file
15
frontend/packages/project-ide/framework/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"exclude": ["**/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user