feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Outlet, useParams } from 'react-router-dom';
|
||||
|
||||
import { useDestorySpace } from '@coze-common/auth';
|
||||
import { useInitSpaceRole } from '@coze-common/auth-adapter';
|
||||
|
||||
const SpaceIdContainer = ({ spaceId }: { spaceId: string }) => {
|
||||
// 空间组件销毁时,清空对应space数据
|
||||
useDestorySpace(spaceId);
|
||||
|
||||
// 初始化空间权限数据
|
||||
const isCompleted = useInitSpaceRole(spaceId);
|
||||
|
||||
// isCompleted 的 判断条件很重要,确保了在Space空间内能够获取到空间的权限数据。
|
||||
return isCompleted ? <Outlet /> : null;
|
||||
};
|
||||
|
||||
export const SpaceIdLayout = () => {
|
||||
const { space_id: spaceId } = useParams<{
|
||||
space_id: string;
|
||||
}>();
|
||||
|
||||
return spaceId ? <SpaceIdContainer key={spaceId} spaceId={spaceId} /> : null;
|
||||
};
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* 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 classNames from 'classnames';
|
||||
import { cozeMitt } from '@coze-common/coze-mitt';
|
||||
import { reporter } from '@coze-arch/logger';
|
||||
import {
|
||||
type IntelligenceData,
|
||||
IntelligenceType,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozMore } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Space,
|
||||
Avatar,
|
||||
Typography,
|
||||
Popover,
|
||||
Button,
|
||||
} from '@coze-arch/coze-design';
|
||||
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import {
|
||||
ProductEntityType,
|
||||
type FavoriteProductResponse,
|
||||
} from '@coze-arch/bot-api/product_api';
|
||||
import { ProductApi } from '@coze-arch/bot-api';
|
||||
|
||||
const getSubPath = (type: IntelligenceType | undefined) => {
|
||||
if (type === IntelligenceType.Project) {
|
||||
return 'project-ide';
|
||||
}
|
||||
if (type === IntelligenceType.Bot) {
|
||||
//跳转至 Bot编辑页,后续会改成新的URL/space/:spaceId/agent/:agentId
|
||||
return 'bot';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const getIntelligenceNavigateUrl = ({
|
||||
basic_info = {},
|
||||
type,
|
||||
}: Pick<IntelligenceData, 'basic_info' | 'type'>) => {
|
||||
const { space_id, id } = basic_info;
|
||||
return `/space/${space_id}/${getSubPath(type)}/${id}`;
|
||||
};
|
||||
|
||||
export const FavoritesListItem: FC<IntelligenceData> = ({
|
||||
basic_info = {},
|
||||
type,
|
||||
}) => {
|
||||
// 取消收藏
|
||||
const clickToUnfavorite = async () => {
|
||||
try {
|
||||
const res: FavoriteProductResponse =
|
||||
await ProductApi.PublicFavoriteProduct({
|
||||
entity_type:
|
||||
type === IntelligenceType.Project
|
||||
? ProductEntityType.Project
|
||||
: ProductEntityType.Bot,
|
||||
is_cancel: true,
|
||||
entity_id: id,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
// 取消收藏成功,刷新收藏列表
|
||||
cozeMitt.emit('refreshFavList', {
|
||||
id,
|
||||
numDelta: -1,
|
||||
emitPosition: 'favorites-list-item',
|
||||
});
|
||||
} else {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
} catch (error) {
|
||||
reporter.errorEvent({
|
||||
eventName: 'sub_menu_unfavorite_error',
|
||||
error: new CustomError(
|
||||
'sub_menu_unfavorite_error',
|
||||
(error as Error).message,
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
const { icon_url, name, space_id, id } = basic_info;
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'group',
|
||||
'h-[32px] w-full rounded-[8px] cursor-pointer hover:coz-mg-secondary-hovered active:coz-mg-secondary-pressed',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!space_id || !id) {
|
||||
return;
|
||||
}
|
||||
sendTeaEvent(EVENT_NAMES.coze_space_sidenavi_ck, {
|
||||
item: id,
|
||||
category: 'space_favourite',
|
||||
navi_type: 'second',
|
||||
need_login: true,
|
||||
have_access: true,
|
||||
});
|
||||
//跳转至 Bot编辑页,后续会改成新的URL/space/:spaceId/agent/:agentId
|
||||
window.open(getIntelligenceNavigateUrl({ basic_info, type }), '_blank');
|
||||
}}
|
||||
data-testid="workspace.favorites.list.item"
|
||||
>
|
||||
<Space className="h-[32px] px-[8px] w-full" spacing={8}>
|
||||
<Avatar
|
||||
className="h-[16px] w-[16px] rounded-[4px] shrink-0"
|
||||
shape="square"
|
||||
src={icon_url}
|
||||
/>
|
||||
<Typography.Text
|
||||
className="flex-1"
|
||||
ellipsis={{ showTooltip: true, rows: 1 }}
|
||||
>
|
||||
{name}
|
||||
</Typography.Text>
|
||||
<div
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className={classNames(
|
||||
'invisible opacity-0 group-hover:visible group-hover:opacity-100',
|
||||
'h-[16px] w-[16px]',
|
||||
)}
|
||||
>
|
||||
<Popover
|
||||
className="rounded-[8px]"
|
||||
position="bottomRight"
|
||||
mouseLeaveDelay={200}
|
||||
stopPropagation
|
||||
content={
|
||||
<div
|
||||
data-testid="workspace.favorites.list.item.popover"
|
||||
className="w-[112px] h-[32px] pl-[8px] rounded-[8px] flex items-center overflow-hidden relative cursor-pointer hover:coz-mg-secondary-hovered"
|
||||
onClick={clickToUnfavorite}
|
||||
>
|
||||
{I18n.t('navigation_workspace_favourites_cancle')}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
data-testid="workspace.favorites.list.item.popover.button"
|
||||
className={classNames('h-full w-full !flex')}
|
||||
size="mini"
|
||||
color="secondary"
|
||||
icon={<IconCozMore />}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* 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, useRef, useEffect, useMemo } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import classNames from 'classnames';
|
||||
import { useInfiniteScroll } from 'ahooks';
|
||||
import { reporter } from '@coze-arch/logger';
|
||||
import {
|
||||
type Intelligence,
|
||||
IntelligenceStatus,
|
||||
SearchScope,
|
||||
search,
|
||||
} from '@coze-arch/idl/intelligence_api';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type SpaceType } from '@coze-arch/bot-api/developer_api';
|
||||
import { intelligenceApi } from '@coze-arch/bot-api';
|
||||
import { useSpaceStore } from '@coze-foundation/space-store';
|
||||
import { cozeMitt, type RefreshFavListParams } from '@coze-common/coze-mitt';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
import { Space, Loading } from '@coze-arch/coze-design';
|
||||
|
||||
import { FavoritesListItem } from './favorites-list-item';
|
||||
|
||||
interface FEIntelligenceListData {
|
||||
list: Intelligence[];
|
||||
total: number;
|
||||
hasMore: boolean;
|
||||
cursorId?: string;
|
||||
}
|
||||
|
||||
const emptyDraftBotListData: FEIntelligenceListData = {
|
||||
list: [],
|
||||
total: 0,
|
||||
hasMore: false,
|
||||
cursorId: undefined,
|
||||
};
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
const getFavoritesList = async ({
|
||||
spaceId,
|
||||
spaceType,
|
||||
cursorId,
|
||||
pageSize = DEFAULT_PAGE_SIZE,
|
||||
}: {
|
||||
spaceId?: string;
|
||||
spaceType?: SpaceType;
|
||||
cursorId?: string;
|
||||
pageSize?: number;
|
||||
}): Promise<FEIntelligenceListData> => {
|
||||
try {
|
||||
if (spaceId) {
|
||||
const res = await intelligenceApi.GetDraftIntelligenceList({
|
||||
space_id: spaceId,
|
||||
order_by: search.OrderBy.UpdateTime,
|
||||
is_fav: true,
|
||||
status: [
|
||||
IntelligenceStatus.Using,
|
||||
IntelligenceStatus.Banned,
|
||||
IntelligenceStatus.MoveFailed,
|
||||
],
|
||||
size: pageSize,
|
||||
cursor_id: cursorId,
|
||||
search_scope: SearchScope.All,
|
||||
});
|
||||
const resData = res?.data;
|
||||
return {
|
||||
list: resData?.intelligences || [],
|
||||
total: resData?.total ?? 0,
|
||||
hasMore: Boolean(resData?.has_more),
|
||||
cursorId: resData?.next_cursor_id,
|
||||
};
|
||||
} else {
|
||||
return emptyDraftBotListData;
|
||||
}
|
||||
} catch (error) {
|
||||
reporter.errorEvent({
|
||||
eventName: 'get_favorites_list_error',
|
||||
error: new CustomError(
|
||||
'get_favorites_list_error',
|
||||
(error as Error).message,
|
||||
),
|
||||
});
|
||||
return emptyDraftBotListData;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ahooks 的 useInfiniteScroll 返回的对象引用会变,本方法返回一个引用不变的对象,仅此而已,不用关注其声明和实现
|
||||
*/
|
||||
const useInfiniteScrollRef: typeof useInfiniteScroll = <
|
||||
T extends { list: unknown[] },
|
||||
>(
|
||||
...params: Parameters<typeof useInfiniteScroll<T>>
|
||||
) => {
|
||||
const req = useInfiniteScroll<T>(...params);
|
||||
const reqRef = useMemo(() => ({ ...req }), []);
|
||||
return Object.assign(reqRef, req);
|
||||
};
|
||||
|
||||
export const FavoritesList: FC = () => {
|
||||
const { spaceId, spaceType } = useSpaceStore(
|
||||
useShallow(store => ({
|
||||
spaceId: store.space.id,
|
||||
spaceType: store.space.space_type,
|
||||
})),
|
||||
);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 用一个引用不变的 req,便于 effect 中的 handler 拿到最新的 loading 状态
|
||||
// (将 loading 放进 effect 的 deps 中并不能解决问题,因为上一次的闭包中 getFavoritesList 前后的 loading 状态已经固定不会变了,导致上一次执行出错)
|
||||
const req = useInfiniteScrollRef<FEIntelligenceListData>(
|
||||
async dataSource =>
|
||||
await getFavoritesList({
|
||||
spaceId,
|
||||
spaceType,
|
||||
cursorId: dataSource?.cursorId ?? undefined,
|
||||
}),
|
||||
{
|
||||
target: containerRef,
|
||||
reloadDeps: [spaceId, spaceType],
|
||||
isNoMore: dataSource => !dataSource?.hasMore,
|
||||
},
|
||||
);
|
||||
const { loading, data, loadingMore } = req;
|
||||
|
||||
useEffect(() => {
|
||||
const handler = async (refreshFavListParams: RefreshFavListParams) => {
|
||||
if (req.loading || req.loadingMore) {
|
||||
// 处理竞态问题,优先保证列表滚动加载,下同
|
||||
return;
|
||||
}
|
||||
|
||||
const currLength = req.data?.list?.length;
|
||||
const mutateData = await getFavoritesList({
|
||||
spaceId,
|
||||
spaceType,
|
||||
// Q:为什么要专门设置 pageSize
|
||||
// A:useInfiniteScroll 有个 bug/feature,mutate 后不会立即触发高度检测
|
||||
// 这要从它的 loadmore 触发逻辑讲起,正常是监听 scroll 动作,检测高度,从而判断是否需要 loadmore
|
||||
// 但假如首次请求回来的数据就不足一屏高度,没有 overflow 无法触发 scroll 动作,怎么办?
|
||||
// 因此 useInfiniteScroll 会在 run、reload 之类的行为完成后立即做一次高度检测来判断是否要继续 loadmore。
|
||||
// 但是!它却唯独不会在 mutate 后做高度检测,导致 mutate 出来的数据不足一屏,就再也无法 loadmore 了
|
||||
// 因此这里手动计算一下需要 mutate 的数据量。
|
||||
// 如果后续 pageSize 太大有问题,那还可以继续改造一下 useInfiniteScrollRef,使 mutate 动作实际执行 reload,但拦截其 loading 属性返回 false
|
||||
pageSize: Math.max(
|
||||
currLength
|
||||
? currLength + refreshFavListParams.numDelta
|
||||
: DEFAULT_PAGE_SIZE,
|
||||
DEFAULT_PAGE_SIZE,
|
||||
),
|
||||
});
|
||||
if (req.loading || req.loadingMore) {
|
||||
return;
|
||||
}
|
||||
// 使用 mutate 静默加载,直接更新视图,不展示 loading 效果
|
||||
req.mutate(mutateData);
|
||||
};
|
||||
cozeMitt.on('refreshFavList', handler);
|
||||
return () => cozeMitt.off('refreshFavList', handler);
|
||||
}, [spaceId, spaceType]);
|
||||
|
||||
return (
|
||||
// 这里有一个很坑,滚动蒙层如果直接挂在滚动画布元素上会一起滚动,所以这里需要单独包一层(往上方一层)
|
||||
<div className={classNames('w-full h-full flex flex-col')}>
|
||||
<>
|
||||
<Space
|
||||
className="h-[24px] pl-[8px] w-full mb-[4px] flex-none"
|
||||
spacing={4}
|
||||
>
|
||||
<div className="coz-fg-secondary text-[14px] font-[500] leading-[20px]">
|
||||
{I18n.t('navigation_workspace_favourites', {}, 'Favourites')}
|
||||
</div>
|
||||
</Space>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="w-full flex-grow max-h-full overflow-y-auto styled-scrollbar-hidden"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-[200px] w-full">
|
||||
<Loading loading={true} size="mini" />
|
||||
</div>
|
||||
) : (
|
||||
<Space vertical spacing={4} className="w-full">
|
||||
{data?.list?.length && data?.list?.length > 0 ? (
|
||||
data?.list?.map(intelligenceData => (
|
||||
<FavoritesListItem
|
||||
key={intelligenceData.basic_info?.id}
|
||||
{...intelligenceData}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="coz-fg-dim pl-[8px] text-[14px] font-[500] leading-[20px]">
|
||||
<div>{I18n.t('home_favor_desc1')}</div>
|
||||
<div>{I18n.t('home_favor_desc2')}</div>
|
||||
</div>
|
||||
)}
|
||||
{loadingMore ? <Loading loading={true} size="mini" /> : null}
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
/* 定义滚动条默认隐藏样式 styled-scrollbar-hidden */
|
||||
.styled-scrollbar-hidden {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
|
||||
-ms-overflow-style: none; /* IE 10+ */
|
||||
}
|
||||
|
||||
.styled-scrollbar-hidden::-webkit-scrollbar {
|
||||
display: none; /* Safari and Chrome */
|
||||
}
|
||||
|
||||
.styled-scrollbar-hidden:hover {
|
||||
scrollbar-width: auto;
|
||||
}
|
||||
|
||||
.styled-scrollbar-hidden:hover::-webkit-scrollbar {
|
||||
display: inline-block;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.styled-scrollbar-hidden:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(31, 35, 41, 30%);
|
||||
background-clip: padding-box;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 9999px;
|
||||
|
||||
transition: background 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||
}
|
||||
|
||||
.styled-scrollbar-hidden:hover::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(31, 35, 41, 60%);
|
||||
}
|
||||
|
||||
.styled-scrollbar-hidden:hover::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.list-top-mask,
|
||||
.list-bottom-mask {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.list-bottom-mask::after,
|
||||
.list-top-mask::before {
|
||||
pointer-events: none; /* 确保伪元素不阻挡用户交互 */
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.list-top-mask::before {
|
||||
top: 0;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 100%),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.list-bottom-mask::after {
|
||||
bottom: 0;
|
||||
background: linear-gradient(to top, rgba(255, 255, 255, 100%), transparent);
|
||||
}
|
||||
|
||||
.list-top-mask-24::before {
|
||||
top: 24px;
|
||||
}
|
||||
|
||||
.list-bottom-mask-32::after {
|
||||
bottom: 32px;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { type ReactNode, type FC } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import classNames from 'classnames';
|
||||
import { useSpaceStore } from '@coze-foundation/space-store';
|
||||
import { localStorageService } from '@coze-foundation/local-storage';
|
||||
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
|
||||
|
||||
export interface IWorkspaceListItem {
|
||||
icon?: ReactNode;
|
||||
activeIcon?: ReactNode;
|
||||
title?: () => string;
|
||||
path?: string;
|
||||
dataTestId?: string;
|
||||
}
|
||||
|
||||
interface IWorkspaceListItemProps extends IWorkspaceListItem {
|
||||
currentSubMenu?: string;
|
||||
}
|
||||
|
||||
export const WorkspaceListItem: FC<IWorkspaceListItemProps> = ({
|
||||
icon,
|
||||
activeIcon,
|
||||
title,
|
||||
path,
|
||||
currentSubMenu,
|
||||
dataTestId,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { spaceId } = useSpaceStore(
|
||||
useShallow(store => ({
|
||||
spaceId: store.space.id,
|
||||
})),
|
||||
);
|
||||
return spaceId ? (
|
||||
<div
|
||||
onClick={() => {
|
||||
sendTeaEvent(EVENT_NAMES.coze_space_sidenavi_ck, {
|
||||
item: title?.() || 'unknown-workspace-submenu',
|
||||
navi_type: 'second',
|
||||
need_login: true,
|
||||
have_access: true,
|
||||
});
|
||||
localStorageService.setValue('workspace-subMenu', path);
|
||||
navigate(`/space/${spaceId}/${path}`);
|
||||
}}
|
||||
className={classNames(
|
||||
'flex items-center gap-[8px]',
|
||||
'transition-colors',
|
||||
'rounded-[8px]',
|
||||
'h-[32px] w-full',
|
||||
'px-[8px]',
|
||||
'cursor-pointer',
|
||||
'group',
|
||||
'hover:coz-mg-secondary-hovered',
|
||||
{
|
||||
'coz-bg-primary': path === currentSubMenu,
|
||||
'coz-fg-plus': path === currentSubMenu,
|
||||
'coz-fg-primary': path !== currentSubMenu,
|
||||
},
|
||||
)}
|
||||
id={`workspace-submenu-${path}`}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
<div className="text-[14px]">
|
||||
<div className="w-[16px] h-[16px]">
|
||||
{path === currentSubMenu ? activeIcon : icon}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex-1',
|
||||
'text-[14px]',
|
||||
'leading-[20px]',
|
||||
'font-[500]',
|
||||
)}
|
||||
>
|
||||
{title?.()}
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
@@ -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 { Space } from '@coze-arch/coze-design';
|
||||
|
||||
import {
|
||||
WorkspaceListItem,
|
||||
type IWorkspaceListItem,
|
||||
} from './workspace-list-item';
|
||||
|
||||
interface WorkspaceListProps {
|
||||
menus: Array<IWorkspaceListItem>;
|
||||
currentSubMenu?: string;
|
||||
}
|
||||
|
||||
export const WorkspaceList: FC<WorkspaceListProps> = ({
|
||||
menus,
|
||||
currentSubMenu,
|
||||
}: WorkspaceListProps) => (
|
||||
<div className="w-full mt-[16px]">
|
||||
<Space vertical spacing={4} className="w-full">
|
||||
{menus.map((item, index) => (
|
||||
<WorkspaceListItem
|
||||
{...item}
|
||||
key={index}
|
||||
currentSubMenu={currentSubMenu}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
@@ -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 { type ReactNode } from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { Space, Skeleton } from '@coze-arch/coze-design';
|
||||
import { useSpaceStore } from '@coze-foundation/space-store';
|
||||
|
||||
import { type IWorkspaceListItem } from './components/workspace-list-item';
|
||||
import { WorkspaceList } from './components/workspace-list';
|
||||
import { FavoritesList } from './components/favorites-list';
|
||||
|
||||
import './components/list.css';
|
||||
|
||||
interface IWorkspaceSubMenuProps {
|
||||
header: ReactNode;
|
||||
menus: Array<IWorkspaceListItem>;
|
||||
currentSubMenu?: string;
|
||||
}
|
||||
|
||||
export const WorkspaceSubMenu = ({
|
||||
header,
|
||||
menus,
|
||||
currentSubMenu,
|
||||
}: IWorkspaceSubMenuProps) => {
|
||||
const { spaceList, loading } = useSpaceStore(
|
||||
useShallow(state => ({
|
||||
currentSpace: state.space,
|
||||
spaceList: state.spaceList,
|
||||
loading: !!state.loading || !state.inited,
|
||||
})),
|
||||
);
|
||||
|
||||
const hasSpace = spaceList.length > 0;
|
||||
|
||||
return (
|
||||
<Skeleton loading={loading} active placeholder={<Skeleton.Paragraph />}>
|
||||
<Space spacing={4} vertical className="w-full h-full">
|
||||
<div className="flex-none w-full">{header}</div>
|
||||
{hasSpace ? (
|
||||
<>
|
||||
<div className="flex-none w-full">
|
||||
<WorkspaceList menus={menus} currentSubMenu={currentSubMenu} />
|
||||
</div>
|
||||
<div className="flex-grow max-h-full overflow-y-auto w-full mt-[24px]">
|
||||
<FavoritesList />
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</Space>
|
||||
</Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
export { IWorkspaceListItem };
|
||||
Reference in New Issue
Block a user