feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
import { mergeConfig } from 'vite';
import svgr from 'vite-plugin-svgr';
/** @type { import('@storybook/react-vite').StorybookConfig } */
const config = {
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.tsx'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
viteFinal: config =>
mergeConfig(config, {
plugins: [
svgr({
svgrOptions: {
native: false,
},
}),
],
}),
};
export default config;

View File

@@ -0,0 +1,14 @@
/** @type { import('@storybook/react').Preview } */
const preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;

View File

@@ -0,0 +1,5 @@
const { defineConfig } = require('@coze-arch/stylelint-config');
module.exports = defineConfig({
extends: [],
});

View File

@@ -0,0 +1,16 @@
# @coze-project-ide/biz-components
> Project template for react component with storybook.
## Features
- [x] eslint & ts
- [x] esm bundle
- [x] umd bundle
- [x] storybook
## Commands
- init: `rush update`
- dev: `npm run dev`
- build: `npm run build`

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View 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',
},
});

View File

@@ -0,0 +1,55 @@
{
"name": "@coze-project-ide/biz-components",
"version": "0.0.1",
"description": "project ide business components",
"license": "Apache-2.0",
"author": "zhangchaoyang.805@bytedance.com",
"maintainers": [],
"main": "src/index.ts",
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@coze-arch/bot-api": "workspace:*",
"@coze-arch/bot-flags": "workspace:*",
"@coze-arch/bot-icons": "workspace:*",
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
"@coze-arch/i18n": "workspace:*",
"@coze-project-ide/framework": "workspace:*",
"@coze-studio/user-store": "workspace:*",
"@coze-workflow/base": "workspace:*",
"classnames": "^2.3.2",
"inversify": "^6.0.1",
"lodash-es": "^4.17.21",
"zustand": "^4.4.7"
},
"devDependencies": {
"@coze-arch/bot-typings": "workspace:*",
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/stylelint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/react-hooks": "^8.0.1",
"@types/lodash-es": "^4.17.10",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@vitest/coverage-v8": "~3.0.5",
"react": "~18.2.0",
"react-dom": "~18.2.0",
"react-router-dom": "^6.22.0",
"stylelint": "^15.11.0",
"typescript": "~5.8.2",
"vite-plugin-svgr": "~3.3.0",
"vitest": "~3.0.5"
},
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0"
}
}

View 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 { useResourceList } from './use-resource-list';
export { useOpenResource } from './use-open-resource';
export { useResourceCopyDispatch } from './use-resource-copy-dispatch';

View File

@@ -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 { useIDENavigate } from '@coze-project-ide/framework';
import { type BizResourceTypeEnum } from '@/resource-folder-coze/type';
export const useOpenResource = () => {
const navigate = useIDENavigate();
return ({
resourceId,
resourceType,
}: {
resourceType?: BizResourceTypeEnum;
resourceId?: string;
}) => navigate(`/${resourceType}/${resourceId}`);
};

View File

@@ -0,0 +1,172 @@
/*
* 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 { I18n } from '@coze-arch/i18n';
import { Button, Space, Toast, Typography } from '@coze-arch/coze-design';
import {
type ResourceCopyDispatchRequest,
ResourceCopyScene,
type ResourceCopyTaskDetail,
} from '@coze-arch/bot-api/plugin_develop';
import {
DisposableCollection,
getURIByResource,
ModalService,
useIDEService,
useProjectIDEServices,
useSpaceId,
} from '@coze-project-ide/framework';
import { resTypeDTOToVO } from '@/utils';
import { usePrimarySidebarStore } from '@/stores';
import { BizResourceTypeEnum } from '@/resource-folder-coze';
import { useOpenResource } from './use-open-resource';
export const useResourceCopyDispatch = () => {
const modalService = useIDEService<ModalService>(ModalService);
const refetch = usePrimarySidebarStore(state => state.refetch);
const projectIDEServices = useProjectIDEServices();
const openResource = useOpenResource();
const spaceId = useSpaceId();
const viewResource = ({
scene,
taskInfo,
}: {
scene?: ResourceCopyScene;
taskInfo?: ResourceCopyTaskDetail;
}) => {
if (!taskInfo?.res_type || !taskInfo?.res_id || !scene) {
return;
}
const openInIDE =
scene &&
[
ResourceCopyScene.CopyProjectResource,
ResourceCopyScene.CopyResourceFromLibrary,
].includes(scene);
const resourceType = resTypeDTOToVO(taskInfo.res_type);
const resId = taskInfo.res_id;
if (openInIDE) {
openResource({
resourceType: resTypeDTOToVO(taskInfo.res_type),
resourceId: taskInfo.res_id,
});
} else {
if (resourceType === BizResourceTypeEnum.Workflow) {
window.open(`/work_flow?space_id=${spaceId}&workflow_id=${resId}`);
} else {
window.open(
`/space/${spaceId}/${resourceType}/${resId}?from=project`,
'_blank',
);
}
}
};
const showToast = ({
scene,
taskInfo,
hideViewBtn,
}: {
scene?: ResourceCopyScene;
taskInfo?: ResourceCopyTaskDetail;
hideViewBtn?: boolean;
}) => {
let message = '';
switch (scene) {
case ResourceCopyScene.CopyProjectResource:
message = I18n.t('project_toast_copy_successful');
break;
case ResourceCopyScene.CopyResourceToLibrary:
message = I18n.t('resource_toast_copy_to_library_success');
break;
case ResourceCopyScene.MoveResourceToLibrary:
message = I18n.t('resource_toast_move_to_library_success');
break;
case ResourceCopyScene.CopyResourceFromLibrary:
message = I18n.t('project_toast_successfully_imported_from_library');
break;
default:
break;
}
Toast.success({
content: (
<Space spacing={6}>
<Typography.Text>{message}</Typography.Text>
{hideViewBtn ? null : (
<Button
color="primary"
size="small"
onClick={() => viewResource({ scene, taskInfo })}
>
{I18n.t('resource_toast_view_resource')}
</Button>
)}
</Space>
),
});
};
return async (
props: ResourceCopyDispatchRequest,
): Promise<ResourceCopyTaskDetail | undefined> => {
const disposables = new DisposableCollection();
try {
const taskInfo = await new Promise<ResourceCopyTaskDetail | undefined>(
(resolve, reject) => {
modalService.startPolling(props);
disposables.pushAll([
modalService.onSuccess(_taskInfo => {
disposables.dispose();
refetch();
resolve(_taskInfo);
}),
modalService.onError(err => {
disposables.dispose();
reject(err);
}),
modalService.onCancel(() => {
disposables.dispose();
reject(new Error('cancelled'));
}),
]);
},
);
const hideViewBtn =
props.scene === ResourceCopyScene.CopyResourceFromLibrary;
showToast({ scene: props.scene, taskInfo, hideViewBtn });
if (hideViewBtn) {
viewResource({ scene: props.scene, taskInfo });
}
if (props.scene === ResourceCopyScene.MoveResourceToLibrary) {
const uri = getURIByResource(
resTypeDTOToVO(props.res_type) ?? '',
props.res_id ?? '',
);
projectIDEServices.view.closeWidgetByUri(uri);
}
return taskInfo;
} catch (err) {
console.error('error dispatch resource', err);
}
};
};

View File

@@ -0,0 +1,63 @@
/*
* 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 { ProjectResourceGroupType } from '@coze-arch/bot-api/plugin_develop';
import { usePrimarySidebarStore } from '@/stores';
import { type BizResourceType } from '@/resource-folder-coze';
export const useResourceList = (): {
workflowResource: BizResourceType[];
pluginResource: BizResourceType[];
dataResource: BizResourceType[];
initLoaded?: boolean;
isFetching?: boolean;
} => {
const resourceTree = usePrimarySidebarStore(state => state.resourceTree);
const isFetching = usePrimarySidebarStore(state => state.isFetching);
const initLoaded = usePrimarySidebarStore(state => state.initLoaded);
const workflowResource = useMemo<BizResourceType[]>(
() =>
resourceTree.find(
group => group.groupType === ProjectResourceGroupType.Workflow,
)?.resourceList || [],
[resourceTree],
);
const pluginResource = useMemo<BizResourceType[]>(
() =>
resourceTree.find(
group => group.groupType === ProjectResourceGroupType.Plugin,
)?.resourceList || [],
[resourceTree],
);
const dataResource = useMemo<BizResourceType[]>(
() =>
resourceTree.find(
group => group.groupType === ProjectResourceGroupType.Data,
)?.resourceList || [],
[resourceTree],
);
return {
workflowResource,
pluginResource,
dataResource,
initLoaded,
isFetching,
};
};

View File

@@ -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.
*/
export {
ResourceFolderCoze,
type BizResourceType,
BizResourceTypeEnum,
type ResourceFolderCozeProps,
createResourceFolderPlugin,
BizResourceContextMenuBtnType,
type BizResourceTree,
VARIABLE_RESOURCE_ID,
CustomResourceFolderShortcutService,
} from './resource-folder-coze';
export { usePrimarySidebarStore } from './stores';
export * from './hooks';
export { ProjectResourceGroupType } from '@coze-arch/bot-api/plugin_develop';
export { validateNameConflict } from './resource-folder-coze/utils';

View File

@@ -0,0 +1,106 @@
/*
* 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 { I18n } from '@coze-arch/i18n';
import {
IconCozWorkflow,
IconCozPlugin,
IconCozDocument,
IconCozVariables,
} from '@coze-arch/coze-design/icons';
import {
ProjectResourceActionKey,
ProjectResourceGroupType,
} from '@coze-arch/bot-api/plugin_develop';
import { ResourceTypeEnum } from '@coze-project-ide/framework';
import { BizResourceContextMenuBtnType, BizResourceTypeEnum } from './type';
export const iconFolder = (
<i style={{ fontSize: 14 }} className={'codicon codicon-folder'} />
);
export const iconFolderOpended = (
<i style={{ fontSize: 14 }} className={'codicon codicon-folder-opened'} />
);
export const VARIABLE_RESOURCE_ID = 'variables';
/* 资源文件名最大字符数 */
export const RESOURCE_NAME_MAX_LEN = 50;
export const resourceIconMap = {
[BizResourceTypeEnum.Workflow]: <IconCozWorkflow />,
[BizResourceTypeEnum.Plugin]: <IconCozPlugin />,
[BizResourceTypeEnum.Knowledge]: <IconCozDocument />,
[BizResourceTypeEnum.Variable]: <IconCozVariables />,
};
export const resourceTitleMap = {
[ProjectResourceGroupType.Workflow]: I18n.t('library_resource_type_workflow'),
[ProjectResourceGroupType.Plugin]: I18n.t('library_resource_type_plugin'),
[ProjectResourceGroupType.Data]: I18n.t('dataide001'),
};
export const createResourceLabelMap = {
[ResourceTypeEnum.Folder]: I18n.t(
'project_resource_sidebar_create_new_folder',
),
[ProjectResourceGroupType.Workflow]: I18n.t(
'project_resource_sidebar_create_new_resource',
{ resource: I18n.t('library_resource_type_workflow') },
),
[ProjectResourceGroupType.Plugin]: I18n.t(
'project_resource_sidebar_create_new_resource',
{ resource: I18n.t('library_resource_type_plugin') },
),
[ProjectResourceGroupType.Data]: I18n.t(
'project_resource_sidebar_create_new_resource',
{ resource: I18n.t('project_resource_sidebar_data_section') },
),
};
export const createResourceIconMap = {
[ProjectResourceGroupType.Workflow]: <IconCozWorkflow />,
[ProjectResourceGroupType.Plugin]: <IconCozPlugin />,
[ProjectResourceGroupType.Data]: <IconCozDocument />,
};
export const contextMenuDTOToVOMap = {
[ProjectResourceActionKey.Rename]: BizResourceContextMenuBtnType.Rename,
[ProjectResourceActionKey.Copy]:
BizResourceContextMenuBtnType.DuplicateResource,
[ProjectResourceActionKey.Delete]: BizResourceContextMenuBtnType.Delete,
[ProjectResourceActionKey.CopyToLibrary]:
BizResourceContextMenuBtnType.CopyToLibrary,
[ProjectResourceActionKey.MoveToLibrary]:
BizResourceContextMenuBtnType.MoveToLibrary,
[ProjectResourceActionKey.Enable]:
BizResourceContextMenuBtnType.EnableKnowledge,
[ProjectResourceActionKey.Disable]:
BizResourceContextMenuBtnType.DisableKnowledge,
[ProjectResourceActionKey.SwitchToChatflow]:
BizResourceContextMenuBtnType.SwitchToChatflow,
[ProjectResourceActionKey.SwitchToFuncflow]:
BizResourceContextMenuBtnType.SwitchToWorkflow,
[ProjectResourceActionKey.UpdateDesc]:
BizResourceContextMenuBtnType.UpdateDesc,
};
export const MAX_DEEP = 6;
export const TAB_SIZE = 14;
export const ITEM_HEIGHT = 28;
/* 禁用文件夹功能 */
export const DISABLE_FOLDER = true;

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type FC, useMemo } from 'react';
import { I18n } from '@coze-arch/i18n';
import { ProjectResourceGroupType } from '@coze-arch/bot-api/plugin_develop';
import styles from './styles.module.less';
export interface EmptyProps {
type: ProjectResourceGroupType;
}
export const Empty: FC<EmptyProps> = ({ type }) => {
const title = useMemo(() => {
switch (type) {
case ProjectResourceGroupType.Workflow:
return I18n.t('project_resource_sidebar_resource_not_added', {
resource: I18n.t('library_resource_type_workflow'),
});
case ProjectResourceGroupType.Plugin:
return I18n.t('project_resource_sidebar_resource_not_added', {
resource: I18n.t('library_resource_type_plugin'),
});
case ProjectResourceGroupType.Data:
return I18n.t('project_resource_sidebar_resource_not_added', {
resource: I18n.t('project_resource_sidebar_data_section'),
});
default:
return '';
}
}, [type]);
return (
<div className={styles.empty}>
<div className={styles['empty-card']}>
<div className={styles['empty-icon']} />
<div className={styles['empty-skeleton']}>
<span />
<span />
</div>
</div>
<div className={styles['empty-title']}>{title}</div>
</div>
);
};

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { IconCozArrowRightFill } from '@coze-arch/coze-design/icons';
export const ExpandableArrow = ({ expand }: { expand?: boolean }) => (
<IconCozArrowRightFill
className="text-[10px] coz-fg-secondary transition-transform"
style={expand ? { transform: 'rotate(90deg)' } : undefined}
/>
);

View File

@@ -0,0 +1,20 @@
/*
* 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 { useDeleteModal } from './use-delete-modal';
export { useResourceFolderConfig } from './use-resource-folder-config';
export { useResourceOpen } from './use-resource-open';
export { withRenameSync } from './with-rename-sync';

View File

@@ -0,0 +1,123 @@
/*
* 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, { type ReactNode, useMemo, useState } from 'react';
import classnames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { Modal } from '@coze-arch/coze-design';
import {
getURIByResource,
type ResourceFolderProps,
type ResourceType,
type URI,
useProjectIDEServices,
} from '@coze-project-ide/framework';
import { VARIABLE_RESOURCE_ID } from '@/resource-folder-coze/constants';
import { getResourceIconByResource } from '../utils';
import styles from '../styles.module.less';
export const useDeleteModal = ({
onDelete,
}: {
onDelete?: (resources: ResourceType[]) => Promise<void> | void;
}): {
node: ReactNode;
handleDeleteResource: ResourceFolderProps['onDelete'];
} => {
const projectIDEServices = useProjectIDEServices();
const [resources, setResources] = useState<ResourceType[]>([]);
const [visible, setVisible] = useState(false);
const open = () => setVisible(true);
const close = () => {
setResources([]);
setVisible(false);
};
const content = useMemo(() => {
if (!resources.length) {
return <></>;
}
let contentText = '';
if (resources.length > 1) {
contentText = I18n.t('project_resource_sidebar_confirm_batch_delete', {
count: resources.length,
});
} else {
contentText = I18n.t('project_resource_sidebar_confirm_delete', {
resourceName: resources[0].name,
});
}
return (
<div className="break-all">
<span className="coz-fg-secondary">{contentText}</span>
{resources.length > 1 ? (
<div className={styles['file-list-wrapper']}>
<div className={styles['file-list']}>
{resources.map(r => (
<span className={styles['file-item']}>
<span className={classnames(styles['file-icon'])}>
{getResourceIconByResource(r)}
</span>
<span className={styles['file-name']}>{r.name}</span>
</span>
))}
</div>
</div>
) : null}
</div>
);
}, [resources]);
const modal = (
<Modal
type="dialog"
okButtonColor="red"
title={
<span className="text-[16px] font-medium coz-fg-plus">
{I18n.t('project_resource_sidebar_delete')}
</span>
}
visible={visible}
okText={I18n.t('Delete')}
cancelText={I18n.t('Cancel')}
autoLoading={true}
onOk={async () => {
await onDelete?.(resources);
resources
.map(resource =>
resource.type ? getURIByResource(resource.type, resource.id) : null,
)
.filter((uri): uri is URI => Boolean(uri))
.forEach(uri => projectIDEServices.view.closeWidgetByUri(uri));
close();
}}
onCancel={() => close()}
>
{content}
</Modal>
);
const handleDeleteResource = (res: ResourceType[]) => {
console.log('[ResourceFolder]on delete resource>>>', res);
const resourceToDelete = res.filter(r => r.id !== VARIABLE_RESOURCE_ID);
if (!resourceToDelete.length) {
return;
}
setResources(resourceToDelete);
open();
};
return { node: modal, handleDeleteResource };
};

View File

@@ -0,0 +1,297 @@
/*
* 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 React, { useCallback, useMemo } from 'react';
import classNames from 'classnames';
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
import { IconCozWarningCircleFill } from '@coze-arch/coze-design/icons';
import { Loading, Tooltip } from '@coze-arch/coze-design';
import { useFlags } from '@coze-arch/bot-flags';
import {
type ProjectResourceAction,
ProjectResourceActionKey,
type ProjectResourceGroupType,
} from '@coze-arch/bot-api/plugin_develop';
import {
type CommonRenderProps,
getURIByResource,
type RenderMoreSuffixType,
type ResourceFolderProps,
ResourceTypeEnum,
type RightPanelConfigType,
ROOT_KEY,
} from '@coze-project-ide/framework';
import { usePrimarySidebarStore } from '@/stores';
import {
getContextMenuLabel,
getResourceIconByResource,
isDynamicAction,
isResourceActionEnabled,
validateNameBasic,
validateNameConflict,
} from '../utils';
import {
type BizGroupTypeWithFolder,
BizResourceContextMenuBtnType,
type BizResourceType,
BizResourceTypeEnum,
type ResourceFolderCozeProps,
type ResourceSubType,
} from '../type';
import { Empty } from '../empty';
import {
contextMenuDTOToVOMap,
createResourceLabelMap,
DISABLE_FOLDER,
ITEM_HEIGHT,
MAX_DEEP,
TAB_SIZE,
VARIABLE_RESOURCE_ID,
} from '../constants';
export type UseResourceFolderConfigProps = {
groupType: ProjectResourceGroupType;
iconRender?: ResourceFolderProps['iconRender'];
onAction?: (
action: BizResourceContextMenuBtnType,
resource?: BizResourceType,
) => void;
onCreateSubTypeResource?: (
resourceType: BizGroupTypeWithFolder,
subType?: ResourceSubType,
) => void;
createResourceConfig?: ResourceFolderCozeProps['createResourceConfig'];
/**
* 隐藏更多菜单按钮
*/
hideMoreBtn?: boolean;
} & Pick<ResourceFolderProps, 'validateConfig'>;
// eslint-disable-next-line @coze-arch/max-line-per-function
export const useResourceFolderConfig = ({
groupType,
iconRender,
onAction,
onCreateSubTypeResource,
createResourceConfig,
validateConfig,
hideMoreBtn,
}: UseResourceFolderConfigProps): Partial<ResourceFolderProps> => {
const textRender = useCallback(
({ resource, isSelected }: CommonRenderProps) => (
<span
className={classNames('text-[14px]', isSelected ? 'font-medium' : '')}
>
{resource.name}
</span>
),
[],
);
const [FLAGS] = useFlags();
const contextMenuHandler = useCallback(
(selectedResources: BizResourceType[]): RightPanelConfigType[] => {
if (!selectedResources.length || hideMoreBtn) {
return [];
}
if (selectedResources.length === 1) {
const resource = selectedResources[0];
if (resource.id === VARIABLE_RESOURCE_ID) {
return [];
}
if (resource.id === ROOT_KEY) {
// 新建文件夹,新建文件,引入资源库
const createHandlers = createResourceConfig
? createResourceConfig.map(({ label, subType }) => ({
id: `${BizResourceContextMenuBtnType.CreateResource}-${subType}`,
execute: () => onCreateSubTypeResource?.(groupType, subType),
label,
}))
: [
{
id: BizResourceContextMenuBtnType.CreateResource,
label: createResourceLabelMap[groupType],
},
];
return [
DISABLE_FOLDER
? null
: {
id: BizResourceContextMenuBtnType.CreateFolder,
label: createResourceLabelMap[ResourceTypeEnum.Folder],
},
...createHandlers,
{
id: BizResourceContextMenuBtnType.ImportLibraryResource,
label: getContextMenuLabel(
BizResourceContextMenuBtnType.ImportLibraryResource,
),
execute: () =>
onAction?.(BizResourceContextMenuBtnType.ImportLibraryResource),
},
].filter(Boolean) as RightPanelConfigType[];
}
const mapFunc = ({
key,
enable,
hint,
}: ProjectResourceAction): RightPanelConfigType | null => {
const menuType = contextMenuDTOToVOMap[key];
if (!menuType) {
return null;
}
return {
id: menuType,
disabled: !enable,
tooltip: I18n.t((hint || '') as I18nKeysNoOptionsType, {}, hint),
label: getContextMenuLabel(menuType, resource),
execute: isDynamicAction(menuType)
? () => onAction?.(menuType, resource)
: undefined,
};
};
const noDeleteActions = resource.actions?.filter(
action => action.key !== ProjectResourceActionKey.Delete,
);
const deleteAction = resource.actions?.filter(
action => action.key === ProjectResourceActionKey.Delete,
);
const noDeleteHandlers =
noDeleteActions
?.map<RightPanelConfigType | null>(mapFunc)
?.filter((c): c is RightPanelConfigType => Boolean(c)) || [];
const deleteHandler =
deleteAction
?.map<RightPanelConfigType | null>(mapFunc)
?.filter((c): c is RightPanelConfigType => Boolean(c)) || [];
if (!noDeleteHandlers?.length || !deleteHandler?.length) {
return [...noDeleteHandlers, ...deleteHandler];
}
return [...noDeleteHandlers, { type: 'separator' }, ...deleteHandler];
}
if (
selectedResources.every(resource =>
isResourceActionEnabled(resource, ProjectResourceActionKey.Delete),
)
) {
return [
{
id: BizResourceContextMenuBtnType.Delete,
label: getContextMenuLabel(BizResourceContextMenuBtnType.Delete),
},
];
}
return [];
},
[
createResourceConfig,
groupType,
onAction,
onCreateSubTypeResource,
FLAGS,
hideMoreBtn,
],
);
const setCanClosePopover = usePrimarySidebarStore(
state => state.setCanClosePopover,
);
return {
textRender,
iconRender: useCallback(
(renderProps: CommonRenderProps): React.ReactElement | undefined => {
const { resource, isExpand } = renderProps;
const icon =
iconRender?.(renderProps) ||
getResourceIconByResource(resource, isExpand);
return (
<span className="inline-flex coz-fg-secondary text-[14px]">
{icon}
</span>
);
},
[iconRender],
),
useOptimismUI: {
loadingRender: () => (
<Loading className="relative mr-1 top-0.5" loading={true} size="mini" />
),
},
contextMenuHandler,
validateConfig: useMemo(
() => ({
customValidator: params =>
validateNameBasic(params) || validateNameConflict(params),
errorMsgRender: msg => (
<Tooltip theme={'dark'} position="right" content={msg}>
<IconCozWarningCircleFill className="coz-fg-hglt-red absolute right-1 text-[13px]" />
</Tooltip>
),
...validateConfig,
}),
[validateConfig],
),
config: useMemo(
() => ({
itemHeight: ITEM_HEIGHT,
maxDeep: MAX_DEEP,
tabSize: TAB_SIZE,
input: {
placeholder: I18n.t('project_resource_sidebar_please_enter'),
style: { borderRadius: 'var(--coze-4)' },
},
resourceUriHandler: resource =>
resource.type
? getURIByResource(resource.type as string, resource.id)
: null,
}),
[],
),
renderMoreSuffix: useMemo<RenderMoreSuffixType>(
() =>
hideMoreBtn
? false
: {
style: { borderRadius: 'var(--coze-4)' },
render: ({ baseBtn, resource }) => {
if (
resource.type === BizResourceTypeEnum.Variable ||
((resource as BizResourceType).actions || []).length === 0
) {
return <></>;
}
return baseBtn;
},
},
[hideMoreBtn],
),
empty: <Empty type={groupType} />,
powerBlackMap: {
dragAndDrop: false,
folder: DISABLE_FOLDER,
},
onContextMenuVisibleChange: (visible: boolean) =>
setCanClosePopover(!visible),
};
};

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useLocation } from 'react-router-dom';
import { useEffect } from 'react';
import { useShallow } from 'zustand/react/shallow';
import {
getResourceByPathname,
type ResourceType,
ResourceTypeEnum,
useIDENavigate,
} from '@coze-project-ide/framework';
import { usePrimarySidebarStore } from '@/stores';
import { BizResourceTypeEnum } from '@/resource-folder-coze/type';
export const useResourceOpen = () => {
const { selectedResource, setSelectedResource } = usePrimarySidebarStore(
useShallow(store => ({
selectedResource: store.selectedResource,
setSelectedResource: store.setSelectedResource,
})),
);
const location = useLocation();
const navigate = useIDENavigate();
const handleOpenResource = (
resourceId: string | number,
resource: ResourceType,
) => {
if (resource.type === ResourceTypeEnum.Folder) {
return;
}
if (resource.type === BizResourceTypeEnum.Variable) {
navigate(`/${resource.type}`);
return;
}
navigate(`/${resource.type}/${resourceId}`);
};
useEffect(() => {
if (location) {
const { resourceType, resourceId } = getResourceByPathname(
location.pathname,
);
if (resourceType === BizResourceTypeEnum.Variable) {
setSelectedResource(resourceType);
} else {
setSelectedResource(resourceId);
}
}
}, [location]);
return { selectedResource, handleOpenResource };
};

View File

@@ -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 { type FC } from 'react';
import React from 'react';
import {
getURIByResource,
type ResourceFolderProps,
ResourceTypeEnum,
useProjectIDEServices,
} from '@coze-project-ide/framework';
import { type ResourceFolderCozeProps } from '../type';
export const withRenameSync =
(Comp: FC<ResourceFolderCozeProps>): FC<ResourceFolderCozeProps> =>
({ onChangeName, ...props }) => {
const { view } = useProjectIDEServices();
const wrappedChangeName: ResourceFolderProps['onChangeName'] =
async event => {
await onChangeName?.(event);
if (
event.resource?.type &&
event.resource?.id &&
event.resource.type !== ResourceTypeEnum.Folder
) {
const uri = getURIByResource(event.resource.type, event.resource.id);
const widgetContext = view.getWidgetContextFromURI(uri);
widgetContext?.widget?.setTitle(event.name);
}
};
return <Comp {...props} onChangeName={wrappedChangeName} />;
};

View 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 { ResourceFolderCoze } from './resource-folder-coze';
export {
BizResourceTypeEnum,
type BizResourceType,
type ResourceFolderCozeProps,
type BizResourceTree,
BizResourceContextMenuBtnType,
} from './type';
export {
createResourceFolderPlugin,
CustomResourceFolderShortcutService,
} from './plugins';
export { VARIABLE_RESOURCE_ID } from './constants';

View File

@@ -0,0 +1,37 @@
/*
* 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 {
definePluginCreator,
type PluginCreator,
bindContributions,
CommandContribution,
ShortcutsContribution,
} from '@coze-project-ide/framework';
import { CustomResourceFolderShortcutService } from './shortcut-service';
import { ResourceFolderContribution } from './resource-folder-contribution';
export { CustomResourceFolderShortcutService };
export const createResourceFolderPlugin: PluginCreator<void> =
definePluginCreator({
onBind({ bind }) {
bind(CustomResourceFolderShortcutService).toSelf().inSingletonScope();
bindContributions(bind, ResourceFolderContribution, [
CommandContribution,
ShortcutsContribution,
]);
},
});

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { createResourceFolderPlugin } from './create-resource-folder-plugin';
export { CustomResourceFolderShortcutService } from './shortcut-service';

View File

@@ -0,0 +1,166 @@
/*
* 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 { I18n } from '@coze-arch/i18n';
import {
type CommandContribution,
type CommandRegistry,
ContextKeyService,
RESOURCE_FOLDER_CONTEXT_KEY,
type ResourceFolderShortCutContextType,
type ShortcutsContribution,
type ShortcutsRegistry,
} from '@coze-project-ide/framework';
import { BizResourceContextMenuBtnType } from '../type';
import { CustomResourceFolderShortcutService } from './shortcut-service';
const SHORTCUT_HANDLER_RESOURCE = 'resourceFolder';
@injectable()
export class ResourceFolderContribution
implements CommandContribution, ShortcutsContribution
{
@inject(CustomResourceFolderShortcutService)
protected readonly shortcutService: CustomResourceFolderShortcutService;
@inject(ContextKeyService)
protected readonly contextKey: ContextKeyService;
@postConstruct()
init() {
this.contextKey.setContext(RESOURCE_FOLDER_CONTEXT_KEY, undefined);
}
registerShortcuts(registry: ShortcutsRegistry): void {
// 重命名
registry.registerHandlers({
commandId: BizResourceContextMenuBtnType.Rename,
keybinding: 'enter',
preventDefault: false,
source: SHORTCUT_HANDLER_RESOURCE,
when: RESOURCE_FOLDER_CONTEXT_KEY,
});
// 删除
registry.registerHandlers({
commandId: BizResourceContextMenuBtnType.Delete,
keybinding: 'meta backspace',
preventDefault: false,
source: SHORTCUT_HANDLER_RESOURCE,
when: RESOURCE_FOLDER_CONTEXT_KEY,
});
// 创建文件夹
registry.registerHandlers({
commandId: BizResourceContextMenuBtnType.CreateFolder,
keybinding: 'alt shift n',
preventDefault: false,
source: SHORTCUT_HANDLER_RESOURCE,
when: RESOURCE_FOLDER_CONTEXT_KEY,
});
// 创建资源
registry.registerHandlers({
commandId: BizResourceContextMenuBtnType.CreateResource,
keybinding: 'alt n',
preventDefault: false,
source: SHORTCUT_HANDLER_RESOURCE,
when: RESOURCE_FOLDER_CONTEXT_KEY,
});
// 创建副本
registry.registerHandlers({
commandId: BizResourceContextMenuBtnType.DuplicateResource,
keybinding: 'alt d',
preventDefault: false,
source: SHORTCUT_HANDLER_RESOURCE,
when: RESOURCE_FOLDER_CONTEXT_KEY,
});
}
registerCommands(commands: CommandRegistry): void {
// 重命名 command
commands.registerCommand(
{
id: BizResourceContextMenuBtnType.Rename,
label: I18n.t('project_resource_sidebar_rename'),
},
{
execute: () => this.shortcutService.renameResource(),
isEnabled: opt => !opt?.disabled,
isVisible: opt => !opt?.isHidden,
},
);
// 删除 command
commands.registerCommand(
{
id: BizResourceContextMenuBtnType.Delete,
},
{
execute: () => this.shortcutService.deleteResource(),
isEnabled: opt => !opt?.disabled,
isVisible: opt => !opt?.isHidden,
},
);
// 新建文件夹 command
commands.registerCommand(
{
id: BizResourceContextMenuBtnType.CreateFolder,
label: I18n.t('project_resource_sidebar_create_new_folder'),
},
{
execute: () => {
const resourceFolderDispatch =
this.contextKey.getContext<ResourceFolderShortCutContextType>(
RESOURCE_FOLDER_CONTEXT_KEY,
);
resourceFolderDispatch?.onCreateFolder?.();
},
// 禁用文件夹创建
isEnabled: opt => false, //!opt?.disabled,
isVisible: opt => !opt?.isHidden,
},
);
// 新建资源 command
commands.registerCommand(
{
id: BizResourceContextMenuBtnType.CreateResource,
label: 'Create Resource',
shortLabel: 'Create Resource',
},
{
execute: () => this.shortcutService.createResource(),
isEnabled: opt => !opt?.disabled,
isVisible: opt => !opt?.isHidden,
},
);
// 新建副本 command
commands.registerCommand(
{
id: BizResourceContextMenuBtnType.DuplicateResource,
label: I18n.t('project_resource_sidebar_copy'),
},
{
execute: () => this.shortcutService.duplicateResource(),
isEnabled: opt => !opt?.disabled,
isVisible: opt => !opt?.isHidden,
},
);
}
}

View File

@@ -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 { ProjectResourceActionKey } from '@coze-arch/bot-api/plugin_develop';
import {
ContextKeyService,
Emitter,
type Event,
RESOURCE_FOLDER_CONTEXT_KEY,
type ResourceFolderShortCutContextType,
type IdType,
} from '@coze-project-ide/framework';
import { isResourceActionEnabled } from '../utils';
import { type BizResourceType } from '../type';
export type DuplicateEvent = Pick<
ResourceFolderShortCutContextType,
'id' | 'tempSelectedMap'
>;
export interface RenameEvent {
id: IdType;
}
export type CreateResourceEvent = Pick<ResourceFolderShortCutContextType, 'id'>;
@injectable()
export class CustomResourceFolderShortcutService {
private onDuplicateEmitter = new Emitter<DuplicateEvent>();
private onCreateResourceEmitter = new Emitter<CreateResourceEvent>();
@inject(ContextKeyService)
protected readonly contextKey: ContextKeyService;
public onDuplicateEvent: Event<DuplicateEvent> =
this.onDuplicateEmitter.event;
public onCreateResourceEvent: Event<CreateResourceEvent> =
this.onCreateResourceEmitter.event;
private onRenameResourceEmitter = new Emitter<RenameEvent>();
public onRenameResource = this.onRenameResourceEmitter.event;
public isResourceActionEnabled(action: ProjectResourceActionKey): boolean {
return Object.values(
this.resourceFolderDispatch?.tempSelectedMap || {},
).every(resource =>
isResourceActionEnabled(resource as BizResourceType, action),
);
}
public renameResource(id?: IdType) {
if (id) {
this.onRenameResourceEmitter.fire({ id });
return;
}
if (this.isResourceActionEnabled(ProjectResourceActionKey.Rename)) {
this.resourceFolderDispatch?.onEnter?.();
}
}
public deleteResource() {
if (this.isResourceActionEnabled(ProjectResourceActionKey.Delete)) {
this.resourceFolderDispatch?.onDelete?.();
}
}
public duplicateResource() {
if (this.isResourceActionEnabled(ProjectResourceActionKey.Copy)) {
this.onDuplicateEmitter.fire({
id: this.resourceFolderDispatch.id,
tempSelectedMap: this.resourceFolderDispatch.tempSelectedMap,
});
}
}
private get resourceFolderDispatch() {
return this.contextKey.getContext<ResourceFolderShortCutContextType>(
RESOURCE_FOLDER_CONTEXT_KEY,
);
}
public createResource() {
this.onCreateResourceEmitter.fire({
id: this.resourceFolderDispatch.id,
});
}
}

View File

@@ -0,0 +1,247 @@
/*
* 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, {
type FC,
type RefObject,
useEffect,
useMemo,
useRef,
} from 'react';
import { type ProjectResourceGroupType } from '@coze-arch/bot-api/plugin_develop';
import {
mapResourceTree,
ResourceFolder,
type ResourceFolderProps,
type ResourceFolderRefType,
ResourceTypeEnum,
useIDEService,
} from '@coze-project-ide/framework';
import { usePrimarySidebarStore } from '@/stores';
import {
type BizGroupTypeWithFolder,
BizResourceContextMenuBtnType,
type ResourceFolderCozeProps,
type ResourceSubType,
} from './type';
import { ResourceGroupActions } from './resource-group-actions';
import { ResourceGroup } from './resource-group';
import { CustomResourceFolderShortcutService } from './plugins/shortcut-service';
import {
useDeleteModal,
useResourceFolderConfig,
useResourceOpen,
withRenameSync,
} from './hooks';
import { resourceTitleMap } from './constants';
import styles from './styles.module.less';
// eslint-disable-next-line @coze-arch/max-line-per-function
const ResourceFolderCozeImpl: FC<ResourceFolderCozeProps> = ({
groupType,
resourceTree,
onDelete,
onAction,
onCreate,
onCustomCreate,
canCreate,
iconRender,
initLoaded,
createResourceConfig,
defaultResourceType,
validateConfig,
hideMoreBtn,
...props
}) => {
const resourceMap = useMemo(
() => mapResourceTree(resourceTree),
[resourceTree],
);
const ref = useRef<ResourceFolderRefType>();
const handleFocusResourceFolder = (visible: boolean) => {
if (visible) {
ref.current?.focus();
ref.current?.closeContextMenu();
}
};
const groupExpandMap = usePrimarySidebarStore(state => state.groupExpandMap);
const updateGroupExpand = usePrimarySidebarStore(
state => state.updateGroupExpand,
);
const handleExpandChange = (_expand: boolean) => {
ref.current?.focus();
updateGroupExpand?.(groupType, _expand);
};
const shortcutService = useIDEService<CustomResourceFolderShortcutService>(
CustomResourceFolderShortcutService,
);
useEffect(() => {
const disposable1 = shortcutService.onDuplicateEvent(event => {
if (event.id !== props.id) {
return;
}
const selectResources = Object.values(event.tempSelectedMap || {}).filter(
item => item.type !== ResourceTypeEnum.Folder,
);
if (!selectResources?.length) {
return;
}
if (selectResources.length === 1) {
onAction?.(
BizResourceContextMenuBtnType.DuplicateResource,
selectResources[0],
);
}
});
const disposable2 = shortcutService.onCreateResourceEvent(event => {
if (!canCreate) {
return;
}
if (event.id !== props.id) {
return;
}
// 多个创建资源菜单,不使用快捷键
if (createResourceConfig) {
return;
}
// 快捷键触发的创建资源
handleCreateResource(groupType);
});
return () => {
disposable1.dispose();
disposable2.dispose();
};
}, [
defaultResourceType,
groupType,
onCustomCreate,
onAction,
shortcutService,
canCreate,
createResourceConfig,
]);
useEffect(() => {
const disposable = shortcutService.onRenameResource(event => {
ref.current?.renameResource(event.id);
});
return () => {
disposable.dispose();
};
}, [shortcutService]);
const creatingResourceSubTypeRef = useRef<ResourceSubType>();
const handleCreateResource = (
_groupType: BizGroupTypeWithFolder,
subType?: ResourceSubType,
) => {
if (!canCreate) {
return;
}
handleExpandChange(true);
if (_groupType === ResourceTypeEnum.Folder) {
ref.current?.createFolder();
} else {
if (onCustomCreate) {
onCustomCreate(_groupType, subType);
} else if (defaultResourceType) {
creatingResourceSubTypeRef.current = subType;
ref.current?.createResource(defaultResourceType);
} else {
console.error(
'[ResourceFolderCoze]must specify defaultResourceType when use props onCreate creating resource',
);
}
}
};
const handleDefaultCreateResource: ResourceFolderProps['onCreate'] =
createEvent => onCreate?.(createEvent, creatingResourceSubTypeRef.current);
const handleImportResource = (_groupType: ProjectResourceGroupType) => {
if (!canCreate) {
return;
}
handleExpandChange(true);
onAction?.(BizResourceContextMenuBtnType.ImportLibraryResource);
};
const { handleDeleteResource, node: deleteModal } = useDeleteModal({
onDelete,
});
const configProps = useResourceFolderConfig({
groupType,
iconRender,
onAction,
createResourceConfig,
validateConfig,
onCreateSubTypeResource: handleCreateResource,
hideMoreBtn,
});
const { selectedResource, handleOpenResource } = useResourceOpen();
return (
<>
<ResourceGroup
className={styles['resource-folder-coze']}
title={resourceTitleMap[groupType]}
content={
initLoaded ? (
<ResourceFolder
ref={ref as RefObject<ResourceFolderRefType>}
resourceTree={resourceTree}
resourceMap={resourceMap}
onDelete={handleDeleteResource}
onCreate={handleDefaultCreateResource}
selected={selectedResource}
onSelected={handleOpenResource}
defaultResourceType={defaultResourceType}
{...configProps}
{...props}
/>
) : null
}
expand={groupExpandMap[groupType]}
onExpandChange={handleExpandChange}
actions={
canCreate ? (
<ResourceGroupActions
createResourceConfig={createResourceConfig}
groupType={groupType}
onCreateResource={handleCreateResource}
onImportResource={handleImportResource}
onActionVisibleChange={handleFocusResourceFolder}
/>
) : null
}
/>
{deleteModal}
</>
);
};
export const ResourceFolderCoze = withRenameSync(ResourceFolderCozeImpl);

View File

@@ -0,0 +1,168 @@
/*
* 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 {
ResourceTypeEnum,
ShortcutsService,
useIDEService,
} from '@coze-project-ide/framework';
import { I18n } from '@coze-arch/i18n';
import {
IconCozPlus,
IconCozFolder,
IconCozTray,
} from '@coze-arch/coze-design/icons';
import { IconButton, Menu, Tooltip } from '@coze-arch/coze-design';
import { ProjectResourceGroupType } from '@coze-arch/bot-api/plugin_develop';
import {
type BizGroupTypeWithFolder,
BizResourceContextMenuBtnType,
type ResourceFolderCozeProps,
type ResourceSubType,
} from './type';
import {
createResourceIconMap,
createResourceLabelMap,
DISABLE_FOLDER,
} from './constants';
import styles from './styles.module.less';
const ProjectResourceStringType = {
[ProjectResourceGroupType.Workflow]: 'workflow',
[ProjectResourceGroupType.Plugin]: 'plugin',
[ProjectResourceGroupType.Data]: 'data',
};
interface ResourceGroupActionsProps {
groupType: ProjectResourceGroupType;
onActionVisibleChange?: (visible: boolean) => void;
onCreateResource?: (
groupType: BizGroupTypeWithFolder,
subType?: ResourceSubType,
) => void;
onImportResource?: (groupType: ProjectResourceGroupType) => void;
createResourceConfig: ResourceFolderCozeProps['createResourceConfig'];
}
export const ResourceGroupActions: React.FC<ResourceGroupActionsProps> = ({
groupType,
onActionVisibleChange,
onCreateResource,
onImportResource,
createResourceConfig,
}) => {
const shortcutService = useIDEService<ShortcutsService>(ShortcutsService);
const keybindingContent = useMemo(() => {
const keybindings = shortcutService.getShortcutByCommandId(
BizResourceContextMenuBtnType.CreateResource,
);
return keybindings?.map(k => k.join(' ')).join(' / ') || '';
}, [shortcutService]);
const createResourceNode = Array.isArray(createResourceConfig) ? (
createResourceConfig.map(({ icon, label, tooltip, subType }) => {
const children = (
<Menu.Item
data-testid={`project-ide.resource-group.actions.menu-item.${subType}`}
onClick={(value, event) => {
event.stopPropagation();
onCreateResource?.(groupType, subType);
}}
icon={icon}
>
{label}
</Menu.Item>
);
if (tooltip) {
return (
<Tooltip
trigger="hover"
position="rightTop"
key={subType}
showArrow={false}
content={tooltip}
style={{ width: 208, padding: 4, borderRadius: 'var(--coze-8)' }}
>
{children}
</Tooltip>
);
}
return children;
})
) : (
<Menu.Item
suffix={<span className={styles.shortcut}>{keybindingContent}</span>}
onClick={(value, event) => {
event.stopPropagation();
onCreateResource?.(groupType);
}}
icon={createResourceIconMap[groupType]}
>
{createResourceLabelMap[groupType]}
</Menu.Item>
);
const menuWidth = useMemo(() => (IS_OVERSEA ? 260 : 198), []);
return (
<Menu
trigger="hover"
position="bottomLeft"
onVisibleChange={visible => onActionVisibleChange?.(visible)}
render={
<div onClick={e => e.stopPropagation()}>
<Menu.SubMenu
className={'w-[198px]'}
mode="menu"
style={{ width: menuWidth }}
>
{DISABLE_FOLDER ? null : (
<Menu.Item
onClick={(value, event) => {
event.stopPropagation();
onCreateResource?.(ResourceTypeEnum.Folder);
}}
icon={<IconCozFolder />}
>
{I18n.t('project_resource_sidebar_create_new_folder')}
</Menu.Item>
)}
{createResourceNode}
<Menu.Item
onClick={(value, event) => {
event.stopPropagation();
onImportResource?.(groupType);
}}
icon={<IconCozTray />}
>
{I18n.t('project_resource_sidebar_import_from_library')}
</Menu.Item>
</Menu.SubMenu>
</div>
}
>
<IconButton
data-testid={`project-${ProjectResourceStringType[groupType]}-add-resource`}
color="secondary"
size="small"
icon={<IconCozPlus className="coz-fg-primary" />}
onClick={e => e.stopPropagation()}
/>
</Menu>
);
};

View File

@@ -0,0 +1,34 @@
.resource-group {
.resource-group-header {
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
height: 36px;
padding: 0 14px 0 17px;
.header-left {
display: flex;
gap: 4px;
align-items: center;
}
.header-title {
font-size: 14px;
font-weight: 500;
line-height: 20px;
}
.action-group {
display: flex;
gap: 2px;
align-items: center;
}
}
.resource-group-content {
padding: 0 6px;
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import { ExpandableArrow } from './expandable-arrow';
import styles from './resource-group.module.less';
export interface ResourceGroupProps {
title: string;
expand?: boolean;
className?: string;
onExpandChange?: (expand: boolean) => void;
actions?: React.ReactNode;
content?: React.ReactNode;
}
export const ResourceGroup = ({
title,
actions,
content,
expand,
onExpandChange,
className,
}: ResourceGroupProps) => (
<div className={classNames(className, styles['resource-group'])}>
<div
className={styles['resource-group-header']}
onClick={() => onExpandChange?.(!expand)}
>
<div className={styles['header-left']}>
<ExpandableArrow expand={expand} />
<span className={styles['header-title']}>{title}</span>
</div>
{actions ? <div className={styles['action-group']}>{actions}</div> : null}
</div>
<div
className={styles['resource-group-content']}
style={expand ? undefined : { display: 'none' }}
>
{content}
</div>
</div>
);

View File

@@ -0,0 +1,159 @@
.resource-list {
width: 100%;
height: 100%;
}
.resource-folder-coze {
:global {
.resource-list-wrapper .resource-list-drag-and-drop-wrapper {
.item-is-selected {
background-color: var(--coz-mg-primary);
&:hover {
background-color: var(--coz-mg-secondary-hovered);
}
}
.item-is-temp-selected {
&.item-is-selected {
background-color: var(--coz-mg-primary);
}
}
.base-item-hover-class:hover {
background-color: var(--coz-mg-secondary-hovered);
}
}
}
}
.shortcut {
font-family: Inter, sans-serif;
font-size: 12px;
font-weight: 700;
color: var(--coz-fg-dim);
}
.file-list-wrapper {
overflow: auto;
max-height: 124px;
margin-top: 4px;
border: 1px solid var(--coz-stroke-primary);
border-radius: var(--coze-8);
&::-webkit-scrollbar {
width: 0;
height: 10px;
background: transparent;
}
&::-webkit-scrollbar:hover {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: transparent;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
}
.file-list {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 6px 0;
.file-item {
overflow: hidden;
width: 100%;
height: 22px;
padding: 0 8px;
text-overflow: ellipsis;
white-space: nowrap;
.file-icon {
position: relative;
top: 2px;
margin-right: 4px;
color: var(--coz-fg-secondary);
}
.file-name {
font-size: 14px;
color: var(--coz-fg-primary);
}
}
}
.empty {
display: flex;
flex-direction: column;
gap: 12px;
align-items: center;
justify-content: center;
width: 232px;
height: 80px;
margin: 0 auto;
.empty-card {
display: flex;
flex-direction: row;
gap: 4px;
align-items: center;
justify-content: flex-start;
padding: 4.5px;
background-color: var(--coz-bg-max);
border: 0.5px solid rgb(240, 240, 240);
border-radius: var(--coze-4);
}
.empty-icon {
width: 15px;
height: 15px;
background-color: var(--coz-mg-primary);
border-radius: var(--coze-3);
}
.empty-skeleton {
display: flex;
flex-direction: column;
gap: 2.5px;
align-items: flex-start;
opacity: 0.12;
span {
height: 4.5px;
background-color: var(--coz-fg-secondary);
border-radius: var(--coze-2);
&:first-child {
width: 20px;
}
&:last-child {
width: 33px;
}
}
}
.empty-title {
font-size: 12px;
font-weight: 400;
line-height: 16px;
color: var(--coz-fg-dim);
}
}

View File

@@ -0,0 +1,153 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ReactNode } from 'react';
import {
type ProjectResourceGroupType,
type ProjectResourceInfo,
} from '@coze-arch/bot-api/plugin_develop';
import {
type CreateResourcePropType,
type ResourceFolderProps,
type ResourceType,
type ResourceTypeEnum,
} from '@coze-project-ide/framework';
/**
* 用于 ResourceType['type'] 里指定资源类型
*/
export enum BizResourceTypeEnum {
Workflow = 'workflow',
Plugin = 'plugin',
Knowledge = 'knowledge',
Database = 'database',
Variable = 'variables',
}
export interface BizResourceTree {
groupType?: ProjectResourceGroupType;
resourceList: BizResourceType[];
}
export enum BizResourceContextMenuBtnType {
/* 创建资源 */
CreateResource = 'resource-folder-create-resource',
/* 创建文件夹 */
CreateFolder = 'resource-folder-create-folder',
/* 重命名 */
Rename = 'resource-folder-rename',
/* 删除 */
Delete = 'resource-folder-delete',
/* ----------------- 下面的事件会通过 onAction 进行回调 ----------------- */
/* 创建副本 */
DuplicateResource = 'resource-folder-duplicate-resource',
/* 引入资源库文件 */
ImportLibraryResource = 'resource-folder-import-library-resource',
/* 移动到资源库 */
MoveToLibrary = 'resource-folder-move-to-library',
/* 复制到资源库 */
CopyToLibrary = 'resource-folder-copy-to-library',
/* 启用知识库 */
EnableKnowledge = 'resource-folder-enable-knowledge',
/* 禁用知识库 */
DisableKnowledge = 'resource-folder-disable-knowledge',
/* 切换为 chatflow */
SwitchToChatflow = 'resource-folder-switch-to-chatflow',
/* 切换为 workflow */
SwitchToWorkflow = 'resource-folder-switch-to-workflow',
/* 修改描述 */
UpdateDesc = 'resource-folder-update-desc',
}
export type BizGroupTypeWithFolder =
| ProjectResourceGroupType
| ResourceTypeEnum.Folder;
export type Validator = Required<
Required<ResourceFolderProps>['validateConfig']
>['customValidator'];
export type BizResourceType = ResourceType & ProjectResourceInfo;
export type ResourceSubType = string | number;
export type ResourceFolderCozeProps = {
/** 资源类型 */
groupType: ProjectResourceGroupType;
/** 后端资源列表数据 */
resourceTree: BizResourceType[];
/** 自定义事件回调,点击右键菜单/上下文菜单会触发,由业务方实现自定义事件的资源更新逻辑逻辑 */
onAction?: (
action: BizResourceContextMenuBtnType,
resource?: BizResourceType,
) => void;
/**
* 自定义资源创建流程时使用这个 props需要业务自己展示资源创建表单然后更新 resource
* 如果没有自定义逻辑,使用 onCreate
*/
onCustomCreate?: (
groupType: ProjectResourceGroupType,
subType?: ResourceSubType,
) => void;
/**
* 使用默认创建资源的方式,创建资源后的回调,在这里调用业务的创建资源接口
* 配置了 onCustomCreate 会使 onCreate 失效
* 默认创建资源方式:点击新建菜单后,资源列表增加新资源 input 输入框,输入资源名称回车完成创建,触发 onCreate 回调
* @param createEvent
* @param subType 如果配置了 createResourceConfig 数组,会有多个创建资源的菜单,通过 subType 区分子类型
*/
onCreate?: (
createEvent: CreateResourcePropType,
subType?: ResourceSubType,
) => void;
/**
* 是否可以创建资源
*/
canCreate?: boolean;
/**
* 完成初始加载,用于判断显示空状态
*/
initLoaded?: boolean;
/**
* 创建资源的 UI 配置
* 如果资源有多个子类型,可以配置为数组
* 触发 onCreateResource 会传第二个参数 subType
*/
createResourceConfig?: Array<{
icon: ReactNode;
label: string;
subType: number | string;
tooltip: ReactNode;
}>;
/**
* 主要用于快捷键创建资源的默认类型。
*/
defaultResourceType?: BizResourceTypeEnum;
/**
* 隐藏更多菜单按钮
*/
hideMoreBtn?: boolean;
} & Pick<
ResourceFolderProps,
| 'id'
| 'onChangeName'
| 'onDelete'
| 'iconRender'
| 'onContextMenuVisibleChange'
| 'validateConfig'
>;

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode } from 'react';
import {
WORKFLOW_NAME_MAX_LEN,
WORKFLOW_NAME_REGEX,
} from '@coze-workflow/base';
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
import { type ProjectResourceActionKey } from '@coze-arch/bot-api/plugin_develop';
import {
type ResourceType,
ResourceTypeEnum,
} from '@coze-project-ide/framework';
import {
BizResourceContextMenuBtnType,
type BizResourceType,
BizResourceTypeEnum,
type Validator,
} from './type';
import { iconFolder, iconFolderOpended, resourceIconMap } from './constants';
export const validateNameBasic: Validator = ({ label }) => {
// 检测 name 是否空
if (!label) {
return I18n.t('project_resource_sidebar_warning_empty_key');
}
if (label.length > WORKFLOW_NAME_MAX_LEN) {
return I18n.t('project_resource_sidebar_warning_length_exceeds');
}
// 检测 name 的命名规则
if (!WORKFLOW_NAME_REGEX.test(label)) {
return I18n.t('workflow_list_create_modal_name_rule_reg');
}
return '';
};
export const validateNameConflict: Validator = ({
label,
parentPath,
id,
resourceTree,
}) => {
const currentParentPath: Array<ResourceType['id']> = [];
const checkSubTree = (subTree: ResourceType) => {
currentParentPath.push(subTree.id);
if (currentParentPath.join('/') === parentPath.join('/')) {
return subTree.children?.some(child => {
const isSelf = id === child.id && id !== '-1';
return !isSelf && child.name === label;
});
} else {
for (const child of subTree.children || []) {
const conflict = checkSubTree(child);
if (conflict) {
return true;
}
}
}
currentParentPath.pop();
};
const nameConflict = checkSubTree(resourceTree);
if (nameConflict) {
return I18n.t('project_resource_sidebar_warning_label_exists', { label });
}
return '';
};
export const getResourceIconByResource = (
resource: ResourceType,
isExpand?: boolean,
) => {
let icon: ReactNode;
switch (resource.type) {
case ResourceTypeEnum.Folder:
icon = isExpand ? iconFolderOpended : iconFolder;
break;
default:
icon = resourceIconMap[resource.type as BizResourceTypeEnum] || '';
break;
}
return icon;
};
export const isResourceActionEnabled = (
resource: BizResourceType,
action: ProjectResourceActionKey,
): boolean =>
resource.actions?.find(item => item.key === action)?.enable || false;
// eslint-disable-next-line complexity
export const getContextMenuLabel = (
contextMenuType: BizResourceContextMenuBtnType,
resource?: ResourceType,
): string => {
let resourceLabelKey: I18nKeysNoOptionsType | '' = '';
switch (resource?.type) {
case BizResourceTypeEnum.Workflow:
resourceLabelKey = 'library_resource_type_workflow';
break;
case BizResourceTypeEnum.Plugin:
resourceLabelKey = 'library_resource_type_plugin';
break;
case BizResourceTypeEnum.Knowledge:
resourceLabelKey = 'library_resource_type_knowledge';
break;
case BizResourceTypeEnum.Database:
resourceLabelKey = 'db_table_entry';
break;
default:
break;
}
switch (contextMenuType) {
case BizResourceContextMenuBtnType.Rename:
return I18n.t('workflow_detail_node_rename');
case BizResourceContextMenuBtnType.DuplicateResource:
return I18n.t('workflow_add_list_copy');
case BizResourceContextMenuBtnType.Delete:
return resource?.type === ResourceTypeEnum.Folder
? I18n.t('filebox_0042')
: I18n.t('project_resource_sidebar_delete');
case BizResourceContextMenuBtnType.CopyToLibrary:
return I18n.t('project_resource_sidebar_copy_to_library');
case BizResourceContextMenuBtnType.MoveToLibrary:
return I18n.t('project_resource_sidebar_move_to_library');
case BizResourceContextMenuBtnType.EnableKnowledge: {
return I18n.t('project_resource_sidebar_enable_resource', {
resource: I18n.t(resourceLabelKey as I18nKeysNoOptionsType),
});
}
case BizResourceContextMenuBtnType.DisableKnowledge:
return I18n.t('project_resource_sidebar_disable_resource', {
resource: I18n.t(resourceLabelKey as I18nKeysNoOptionsType),
});
case BizResourceContextMenuBtnType.ImportLibraryResource:
return I18n.t('project_resource_sidebar_import_from_library', {}, '');
case BizResourceContextMenuBtnType.UpdateDesc:
return I18n.t('project_241115', {}, '修改描述');
case BizResourceContextMenuBtnType.SwitchToChatflow:
return I18n.t('wf_chatflow_121', { flowMode: I18n.t('wf_chatflow_76') });
case BizResourceContextMenuBtnType.SwitchToWorkflow:
return I18n.t('wf_chatflow_121', { flowMode: I18n.t('Workflow') });
default:
return '';
}
};
/**
* 是否动态注册的 action
* @param action
*/
export const isDynamicAction = (action: BizResourceContextMenuBtnType) =>
![
BizResourceContextMenuBtnType.CreateResource,
BizResourceContextMenuBtnType.CreateFolder,
BizResourceContextMenuBtnType.Rename,
BizResourceContextMenuBtnType.Delete,
BizResourceContextMenuBtnType.DuplicateResource,
].includes(action);

View 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 { usePrimarySidebarStore } from './primary-sidebar-store';

View File

@@ -0,0 +1,151 @@
/*
* 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 { devtools } from 'zustand/middleware';
import { create } from 'zustand';
import { ProjectResourceGroupType } from '@coze-arch/bot-api/plugin_develop';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import { resTypeDTOToVO } from '@/utils';
import { type BizResourceTree } from '@/resource-folder-coze/type';
export interface PrimarySidebarActions {
updateGroupExpand: (
groupType: ProjectResourceGroupType,
expand: boolean,
) => void;
isFetching?: boolean;
initLoaded?: boolean;
setSelectedResource: (resourceId?: string) => void;
/**
* 调用 store 里的 refetch 方法通知资源列表刷新
* @param resourceType 刷新的资源类型,可选,不传时刷新所有类型的资源
*/
refetch: (callback?: (tree?: BizResourceTree[]) => void) => Promise<void>;
/**
* 设置资源列表的刷新方法到 store 里,供其他 widget 里调用 store.refetch(xxx) 刷新资源列表
* @param refetchFunc 刷新方法,刷新方法里内部需要处理资源列表的 setResource
*/
fetchResource: (
spaceId: string,
projectId: string,
version?: string,
callback?: (tree?: BizResourceTree[]) => void,
) => Promise<void>;
setCanClosePopover: (canClose: boolean) => void;
}
export interface PrimarySidebarState {
projectId?: string;
spaceId?: string;
version?: string;
resourceTree: BizResourceTree[];
/**
* 资源分组展开状态,按资源分组类型分开记录
*/
groupExpandMap: Record<ProjectResourceGroupType, boolean>;
/**
* 当前选中的资源
*/
selectedResource?: string;
/**
* 是否可以关闭 popover sidebar右键菜单打开时不允许关闭供 ResourceFolderCoze 组件消费
*/
canClosePopover?: boolean;
}
const defaultState: PrimarySidebarState = {
resourceTree: [],
canClosePopover: true,
groupExpandMap: {
[ProjectResourceGroupType.Workflow]: true,
[ProjectResourceGroupType.Plugin]: true,
[ProjectResourceGroupType.Data]: true,
},
};
export const usePrimarySidebarStore = create<
PrimarySidebarState & PrimarySidebarActions
>()(
devtools(
(set, get) => ({
...defaultState,
updateGroupExpand: (
groupType: ProjectResourceGroupType,
expand: boolean,
) => {
set({
groupExpandMap: {
...get().groupExpandMap,
[groupType]: expand,
},
});
},
setSelectedResource: (resourceId?: string) => {
set({
selectedResource: resourceId,
});
},
// eslint-disable-next-line max-params
fetchResource: async (spaceId, projectId, version, callback) => {
set({
isFetching: true,
spaceId,
projectId,
version,
});
const res = await PluginDevelopApi.ProjectResourceList({
project_id: projectId ?? '',
space_id: spaceId,
project_version: version,
});
const resourceTree = res.resource_groups?.map<BizResourceTree>(
group => ({
groupType: group.group_type,
resourceList:
group.resource_list?.map(resourceInfo => ({
id: String(resourceInfo.res_id ?? ''),
type: resTypeDTOToVO(resourceInfo.res_type),
name: resourceInfo.name ?? '',
...resourceInfo,
})) || [],
}),
);
callback?.(resourceTree);
set({
resourceTree,
isFetching: false,
initLoaded: true,
});
},
refetch: async callback => {
const { spaceId, projectId, version } = get();
if (!spaceId || !projectId) {
return;
}
// 服务端数据同步延迟
await new Promise<void>(resolve => setTimeout(() => resolve(), 700));
return get().fetchResource(spaceId, projectId, version, callback);
},
setCanClosePopover: (canClose: boolean) => {
set({ canClosePopover: canClose });
},
}),
{
name: 'projectIDE.primarySidebar',
enabled: IS_DEV_MODE,
},
),
);

View 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' />

View File

@@ -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 { ResType } from '@coze-arch/bot-api/plugin_develop';
import { BizResourceTypeEnum } from '@/resource-folder-coze/type';
export const resTypeDTOToVO = (
resType?: ResType,
): BizResourceTypeEnum | undefined => {
if (!resType) {
return;
}
switch (resType) {
case ResType.Imageflow:
case ResType.Workflow:
return BizResourceTypeEnum.Workflow;
case ResType.Knowledge:
return BizResourceTypeEnum.Knowledge;
case ResType.Plugin:
return BizResourceTypeEnum.Plugin;
case ResType.Variable:
return BizResourceTypeEnum.Variable;
case ResType.Database:
return BizResourceTypeEnum.Database;
default:
return BizResourceTypeEnum.Workflow;
}
};

View File

@@ -0,0 +1,37 @@
/*
* 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 { DemoComponent } from '../src';
export default {
title: 'Example/Demo',
component: DemoComponent,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
argTypes: {},
};
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Base = {
args: {
name: 'tecvan',
},
};

View File

@@ -0,0 +1,34 @@
import { Meta } from "@storybook/blocks";
<Meta title="Hello world" />
<div className="sb-container">
<div className='sb-section-title'>
# Hello world
Hello world
</div>
</div>
<style>
{`
.sb-container {
margin-bottom: 48px;
}
.sb-section {
width: 100%;
display: flex;
flex-direction: row;
gap: 20px;
}
img {
object-fit: cover;
}
.sb-section-title {
margin-bottom: 32px;
}
`}
</style>

View File

@@ -0,0 +1,57 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"compilerOptions": {
"baseUrl": "./",
"types": [],
"jsx": "react",
"isolatedModules": true,
"strictNullChecks": true,
"strictPropertyInitialization": false,
"paths": {
"@/*": ["./src/*"]
},
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/tsconfig.build.tsbuildinfo"
},
"include": ["src"],
"references": [
{
"path": "../../arch/bot-api/tsconfig.build.json"
},
{
"path": "../../arch/bot-flags/tsconfig.build.json"
},
{
"path": "../../arch/bot-typings/tsconfig.build.json"
},
{
"path": "../../arch/i18n/tsconfig.build.json"
},
{
"path": "../../components/bot-icons/tsconfig.build.json"
},
{
"path": "../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../config/stylelint-config/tsconfig.build.json"
},
{
"path": "../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../config/vitest-config/tsconfig.build.json"
},
{
"path": "../framework/tsconfig.build.json"
},
{
"path": "../../studio/user-store/tsconfig.build.json"
},
{
"path": "../../workflow/base/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"exclude": ["**/*"],
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
]
}

View File

@@ -0,0 +1,24 @@
{
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["__tests__", "stories", "vitest.config.ts", "tailwind.config.ts"],
"exclude": ["**/node_modules", "./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
],
"compilerOptions": {
"baseUrl": "./",
"types": ["react", "react-dom"],
"jsx": "react",
"isolatedModules": true,
"strictNullChecks": true,
"strictPropertyInitialization": false,
"paths": {
"@/*": ["./src/*"]
},
"rootDir": "./",
"outDir": "./dist"
}
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig({
dirname: __dirname,
preset: 'web',
});