feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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';
|
||||
Reference in New Issue
Block a user