mirror of https://github.com/buster-so/buster.git
chat header options update
This commit is contained in:
parent
4d7ddb3592
commit
fda555211a
|
@ -1,4 +1,5 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useUpdateChatShare } from '@/api/buster_rest/chats';
|
||||
import { useUpdateCollectionShare } from '@/api/buster_rest/collections';
|
||||
import { useUpdateDashboardShare } from '@/api/buster_rest/dashboards';
|
||||
import { useUpdateMetricShare } from '@/api/buster_rest/metrics';
|
||||
|
@ -35,10 +36,16 @@ export const ShareMenuContentPublish: React.FC<ShareMenuContentBodyProps> = Reac
|
|||
const { mutateAsync: onShareCollection, isPending: isPublishingCollection } =
|
||||
useUpdateCollectionShare();
|
||||
const { mutateAsync: onShareReport, isPending: isPublishingReport } = useUpdateReportShare();
|
||||
const { mutateAsync: onShareChat, isPending: isPublishingChat } = useUpdateChatShare();
|
||||
const [isPasswordProtected, setIsPasswordProtected] = useState<boolean>(!!password);
|
||||
const [_password, _setPassword] = React.useState<string>(password || '');
|
||||
|
||||
const isPublishing = isPublishingMetric || isPublishingDashboard || isPublishingCollection;
|
||||
const isPublishing =
|
||||
isPublishingMetric ||
|
||||
isPublishingDashboard ||
|
||||
isPublishingCollection ||
|
||||
isPublishingChat ||
|
||||
isPublishingReport;
|
||||
|
||||
const linkExpiry = useMemo(() => {
|
||||
return publicExpirationDate ? new Date(publicExpirationDate) : null;
|
||||
|
@ -115,7 +122,7 @@ export const ShareMenuContentPublish: React.FC<ShareMenuContentBodyProps> = Reac
|
|||
} else if (assetType === 'report') {
|
||||
await onShareReport(payload);
|
||||
} else if (assetType === 'chat') {
|
||||
console.warn('Chat sharing is not implemented');
|
||||
await onShareChat(payload);
|
||||
} else {
|
||||
const _exhaustiveCheck: never = assetType;
|
||||
}
|
||||
|
@ -143,7 +150,7 @@ export const ShareMenuContentPublish: React.FC<ShareMenuContentBodyProps> = Reac
|
|||
} else if (assetType === 'report') {
|
||||
await onShareReport(payload);
|
||||
} else if (assetType === 'chat') {
|
||||
console.warn('Chat sharing is not implemented');
|
||||
await onShareChat(payload);
|
||||
} else {
|
||||
const _exhaustiveCheck: never = assetType;
|
||||
}
|
||||
|
@ -170,7 +177,7 @@ export const ShareMenuContentPublish: React.FC<ShareMenuContentBodyProps> = Reac
|
|||
} else if (assetType === 'report') {
|
||||
await onShareReport(payload);
|
||||
} else if (assetType === 'chat') {
|
||||
console.warn('Chat sharing is not implemented');
|
||||
await onShareChat(payload);
|
||||
} else {
|
||||
const _exhaustiveCheck: never = assetType;
|
||||
}
|
||||
|
|
|
@ -2,10 +2,17 @@ import type { GetDashboardResponse } from '@buster/server-shared/dashboards';
|
|||
import type { GetMetricResponse } from '@buster/server-shared/metrics';
|
||||
import type { GetReportResponse } from '@buster/server-shared/reports';
|
||||
import type { ShareConfig } from '@buster/server-shared/share';
|
||||
import type { IBusterChat } from '@/api/asset_interfaces/chat';
|
||||
import type { BusterCollection } from '@/api/asset_interfaces/collection';
|
||||
|
||||
export const getShareAssetConfig = (
|
||||
message: GetMetricResponse | GetDashboardResponse | BusterCollection | GetReportResponse | null
|
||||
message:
|
||||
| GetMetricResponse
|
||||
| GetDashboardResponse
|
||||
| BusterCollection
|
||||
| GetReportResponse
|
||||
| IBusterChat
|
||||
| null
|
||||
): ShareConfig | null => {
|
||||
if (!message) return null;
|
||||
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import type React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import type { IBusterChat } from '@/api/asset_interfaces';
|
||||
import { useGetChat } from '@/api/buster_rest/chats';
|
||||
import { Dropdown, type IDropdownItems } from '@/components/ui/dropdown';
|
||||
import { useGetChatId } from '@/context/Chats/useGetChatId';
|
||||
import { getIsEffectiveOwner } from '@/lib/share';
|
||||
import {
|
||||
useDeleteChatSelectMenu,
|
||||
useDuplicateChatSelectMenu,
|
||||
useFavoriteChatSelectMenu,
|
||||
useOpenInNewTabSelectMenu,
|
||||
useRenameChatTitle,
|
||||
useShareMenuSelectMenu,
|
||||
} from './threeDotMenuHooks';
|
||||
|
||||
const stablePermissionSelector = (chat: IBusterChat) => chat.permission;
|
||||
|
||||
export const ChatContainerHeaderDropdown: React.FC<{
|
||||
children: React.ReactNode;
|
||||
}> = ({ children }) => {
|
||||
const chatId = useGetChatId();
|
||||
const { data: permission } = useGetChat(
|
||||
{ id: chatId || '' },
|
||||
{ select: stablePermissionSelector }
|
||||
);
|
||||
const shareMenu = useShareMenuSelectMenu({ chatId });
|
||||
const renameChatTitle = useRenameChatTitle();
|
||||
const favoriteChat = useFavoriteChatSelectMenu({ chatId });
|
||||
const openInNewTab = useOpenInNewTabSelectMenu({ chatId });
|
||||
const duplicateChat = useDuplicateChatSelectMenu({ chatId });
|
||||
const deleteChat = useDeleteChatSelectMenu({ chatId });
|
||||
|
||||
const isOwnerEffective = getIsEffectiveOwner(permission);
|
||||
|
||||
const menuItem: IDropdownItems = useMemo(() => {
|
||||
return [
|
||||
isOwnerEffective && shareMenu,
|
||||
isOwnerEffective && renameChatTitle,
|
||||
favoriteChat,
|
||||
openInNewTab,
|
||||
{ type: 'divider' },
|
||||
duplicateChat,
|
||||
deleteChat,
|
||||
].filter(Boolean) as IDropdownItems;
|
||||
}, [chatId, duplicateChat, deleteChat, duplicateChat]);
|
||||
|
||||
return (
|
||||
<Dropdown align="end" items={menuItem}>
|
||||
{chatId ? children : null}
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
ChatContainerHeaderDropdown.displayName = 'ChatContainerHeaderDropdown';
|
|
@ -0,0 +1,145 @@
|
|||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { useMemo } from 'react';
|
||||
import type { IBusterChat } from '@/api/asset_interfaces';
|
||||
import { useDeleteChat, useDuplicateChat, useGetChat } from '@/api/buster_rest/chats';
|
||||
import { useFavoriteStar } from '@/components/features/favorites';
|
||||
import { createDropdownItem, type IDropdownItems } from '@/components/ui/dropdown';
|
||||
import { ArrowRight, DuplicatePlus, Pencil, ShareRight, Star, Trash } from '@/components/ui/icons';
|
||||
import { Star as StarFilled } from '@/components/ui/icons/NucleoIconFilled';
|
||||
import { useBusterNotifications } from '@/context/BusterNotifications';
|
||||
import { getIsEffectiveOwner } from '@/lib/share';
|
||||
import { timeout } from '@/lib/timeout';
|
||||
import { getShareAssetConfig, ShareMenuContent } from '../ShareMenu';
|
||||
import { CHAT_HEADER_TITLE_ID } from './ChatHeaderTitle';
|
||||
|
||||
const stablePermissionSelector = (chat: IBusterChat) => chat.permission;
|
||||
|
||||
export const useShareMenuSelectMenu = ({ chatId = '' }: { chatId: string | undefined }) => {
|
||||
const { data: shareAssetConfig } = useGetChat({ id: chatId }, { select: getShareAssetConfig });
|
||||
const isEffectiveOwner = getIsEffectiveOwner(shareAssetConfig?.permission);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
label: 'Share',
|
||||
value: 'share-report',
|
||||
icon: <ShareRight />,
|
||||
disabled: !isEffectiveOwner,
|
||||
items:
|
||||
isEffectiveOwner && shareAssetConfig
|
||||
? [
|
||||
<ShareMenuContent
|
||||
key={chatId}
|
||||
shareAssetConfig={shareAssetConfig}
|
||||
assetId={chatId}
|
||||
assetType={'chat'}
|
||||
/>,
|
||||
]
|
||||
: undefined,
|
||||
}),
|
||||
[chatId, shareAssetConfig, isEffectiveOwner]
|
||||
);
|
||||
};
|
||||
|
||||
export const useRenameChatTitle = () => {
|
||||
return useMemo(
|
||||
() =>
|
||||
createDropdownItem({
|
||||
label: 'Rename',
|
||||
value: 'edit-chat-title',
|
||||
icon: <Pencil />,
|
||||
onClick: async () => {
|
||||
const input = document.getElementById(CHAT_HEADER_TITLE_ID) as HTMLInputElement;
|
||||
if (input) {
|
||||
await timeout(25);
|
||||
input.focus();
|
||||
input.select();
|
||||
}
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
const stableChatTitleSelector = (chat: IBusterChat) => chat.title;
|
||||
export const useFavoriteChatSelectMenu = ({ chatId = '' }: { chatId: string | undefined }) => {
|
||||
const { data: chatTitle } = useGetChat(
|
||||
{ id: chatId || '' },
|
||||
{ select: stableChatTitleSelector, enabled: !!chatId }
|
||||
);
|
||||
const { isFavorited, onFavoriteClick } = useFavoriteStar({
|
||||
id: chatId || '',
|
||||
type: 'chat',
|
||||
name: chatTitle || '',
|
||||
});
|
||||
|
||||
return useMemo(() => {
|
||||
return createDropdownItem({
|
||||
label: 'Add to favorites',
|
||||
value: 'add-to-favorites',
|
||||
icon: <Star />,
|
||||
});
|
||||
}, [isFavorited, onFavoriteClick]);
|
||||
};
|
||||
|
||||
export const useOpenInNewTabSelectMenu = ({ chatId = '' }: { chatId: string | undefined }) => {
|
||||
return useMemo(() => {
|
||||
return createDropdownItem({
|
||||
label: 'Open in new tab',
|
||||
value: 'open-in-new-tab',
|
||||
icon: <ArrowRight />,
|
||||
link: {
|
||||
to: '/app/chats/$chatId',
|
||||
params: { chatId: chatId },
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const useDuplicateChatSelectMenu = ({ chatId = '' }: { chatId: string | undefined }) => {
|
||||
const { mutateAsync: duplicateChat, isPending: isDuplicating } = useDuplicateChat();
|
||||
const navigate = useNavigate();
|
||||
const { openSuccessMessage } = useBusterNotifications();
|
||||
|
||||
return useMemo(() => {
|
||||
return createDropdownItem({
|
||||
label: 'Duplicate',
|
||||
value: 'duplicate',
|
||||
icon: <DuplicatePlus />,
|
||||
loading: isDuplicating,
|
||||
onClick: async () => {
|
||||
if (chatId) {
|
||||
const res = await duplicateChat({ id: chatId });
|
||||
await timeout(100);
|
||||
await navigate({ to: '/app/chats/$chatId', params: { chatId: res.id } });
|
||||
openSuccessMessage('Chat duplicated');
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [chatId, isDuplicating, duplicateChat, navigate, openSuccessMessage]);
|
||||
};
|
||||
|
||||
export const useDeleteChatSelectMenu = ({ chatId = '' }: { chatId: string | undefined }) => {
|
||||
const { mutate: deleteChat, isPending: isDeleting } = useDeleteChat();
|
||||
const navigate = useNavigate();
|
||||
const { openSuccessMessage } = useBusterNotifications();
|
||||
|
||||
return useMemo(() => {
|
||||
return createDropdownItem({
|
||||
label: 'Delete',
|
||||
value: 'delete',
|
||||
icon: <Trash />,
|
||||
loading: isDeleting,
|
||||
onClick: () =>
|
||||
chatId &&
|
||||
deleteChat(
|
||||
{ data: [chatId] },
|
||||
{
|
||||
onSuccess: () => {
|
||||
navigate({ to: '/app/chats' });
|
||||
openSuccessMessage('Chat deleted');
|
||||
},
|
||||
}
|
||||
),
|
||||
});
|
||||
}, [chatId, isDeleting, deleteChat, navigate, openSuccessMessage]);
|
||||
};
|
|
@ -39,6 +39,7 @@ import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
|||
import { useIsMac } from '@/hooks/usePlatform';
|
||||
import { useEditorContext } from '@/layouts/AssetContainer/ReportAssetContainer';
|
||||
import { canEdit, getIsEffectiveOwner } from '@/lib/share';
|
||||
import { useShareMenuSelectMenu } from './threeDotMenuHooks';
|
||||
|
||||
export const ReportThreeDotMenu = React.memo(
|
||||
({
|
||||
|
@ -138,35 +139,6 @@ const useEditWithAI = ({ reportId }: { reportId: string }): IDropdownItem => {
|
|||
);
|
||||
};
|
||||
|
||||
const useShareMenuSelectMenu = ({ reportId }: { reportId: string }) => {
|
||||
const { data: shareAssetConfig } = useGetReport(
|
||||
{ id: reportId },
|
||||
{ select: getShareAssetConfig }
|
||||
);
|
||||
const isEffectiveOwner = getIsEffectiveOwner(shareAssetConfig?.permission);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
label: 'Share',
|
||||
value: 'share-report',
|
||||
icon: <ShareRight />,
|
||||
disabled: !isEffectiveOwner,
|
||||
items:
|
||||
isEffectiveOwner && shareAssetConfig
|
||||
? [
|
||||
<ShareMenuContent
|
||||
key={reportId}
|
||||
shareAssetConfig={shareAssetConfig}
|
||||
assetId={reportId}
|
||||
assetType={'report'}
|
||||
/>,
|
||||
]
|
||||
: undefined,
|
||||
}),
|
||||
[reportId, shareAssetConfig, isEffectiveOwner]
|
||||
);
|
||||
};
|
||||
|
||||
const useSaveToLibrary = ({ reportId }: { reportId: string }): IDropdownItem => {
|
||||
const { mutateAsync: saveReportToCollection } = useAddReportToCollection();
|
||||
const { mutateAsync: removeReportFromCollection } = useRemoveReportFromCollection();
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useGetReport } from '@/api/buster_rest/reports';
|
||||
import { createDropdownItem } from '@/components/ui/dropdown';
|
||||
import { ShareRight } from '@/components/ui/icons';
|
||||
import { getIsEffectiveOwner } from '@/lib/share';
|
||||
import { getShareAssetConfig, ShareMenuContent } from '../ShareMenu';
|
||||
|
||||
export const useShareMenuSelectMenu = ({ reportId }: { reportId: string }) => {
|
||||
const { data: shareAssetConfig } = useGetReport(
|
||||
{ id: reportId },
|
||||
{ select: getShareAssetConfig }
|
||||
);
|
||||
const isEffectiveOwner = getIsEffectiveOwner(shareAssetConfig?.permission);
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
createDropdownItem({
|
||||
label: 'Share',
|
||||
value: 'share-report',
|
||||
icon: <ShareRight />,
|
||||
disabled: !isEffectiveOwner,
|
||||
items:
|
||||
isEffectiveOwner && shareAssetConfig
|
||||
? [
|
||||
<ShareMenuContent
|
||||
key={reportId}
|
||||
shareAssetConfig={shareAssetConfig}
|
||||
assetId={reportId}
|
||||
assetType={'report'}
|
||||
/>,
|
||||
]
|
||||
: undefined,
|
||||
}),
|
||||
[reportId, shareAssetConfig, isEffectiveOwner]
|
||||
);
|
||||
};
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import { ChatHeaderOptions } from '@/components/features/chat/ChatHeaderOptions';
|
||||
import { ChatHeaderTitle } from '@/components/features/chat/ChatHeaderTitle';
|
||||
import { useGetActiveChatTitle, useIsStreamingMessage } from '@/context/Chats';
|
||||
import { useGetChatId } from '@/context/Chats/useGetChatId';
|
||||
import { ChatHeaderOptions } from './ChatHeaderOptions';
|
||||
import { ChatHeaderTitle } from './ChatHeaderTitle';
|
||||
|
||||
export const ChatHeader: React.FC = React.memo(() => {
|
||||
const chatId = useGetChatId();
|
||||
|
|
|
@ -1,123 +0,0 @@
|
|||
import { useNavigate } from '@tanstack/react-router';
|
||||
import type React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useDeleteChat, useDuplicateChat, useGetChat } from '@/api/buster_rest/chats';
|
||||
import { useFavoriteStar } from '@/components/features/favorites';
|
||||
import {
|
||||
createDropdownItem,
|
||||
createDropdownItems,
|
||||
Dropdown,
|
||||
type IDropdownItems,
|
||||
} from '@/components/ui/dropdown';
|
||||
import { ArrowRight, DuplicatePlus, Pencil, Star, Trash } from '@/components/ui/icons';
|
||||
import { Star as StarFilled } from '@/components/ui/icons/NucleoIconFilled';
|
||||
import { useBusterNotifications } from '@/context/BusterNotifications';
|
||||
import { useGetChatId } from '@/context/Chats/useGetChatId';
|
||||
import { timeout } from '@/lib/timeout';
|
||||
import { CHAT_HEADER_TITLE_ID } from '../ChatHeaderTitle';
|
||||
|
||||
export const ChatContainerHeaderDropdown: React.FC<{
|
||||
children: React.ReactNode;
|
||||
}> = ({ children }) => {
|
||||
const { openSuccessMessage } = useBusterNotifications();
|
||||
const chatId = useGetChatId();
|
||||
const navigate = useNavigate();
|
||||
const { mutate: deleteChat, isPending: isDeleting } = useDeleteChat();
|
||||
const { mutateAsync: duplicateChat, isPending: isDuplicating } = useDuplicateChat();
|
||||
|
||||
const { data: chatTitle } = useGetChat(
|
||||
{ id: chatId || '' },
|
||||
{ select: (x) => x.title, enabled: !!chatId }
|
||||
);
|
||||
|
||||
const { isFavorited, onFavoriteClick } = useFavoriteStar({
|
||||
id: chatId || '',
|
||||
type: 'chat',
|
||||
name: chatTitle || '',
|
||||
});
|
||||
|
||||
const menuItem: IDropdownItems = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
label: 'Rename',
|
||||
value: 'edit-chat-title',
|
||||
icon: <Pencil />,
|
||||
onClick: async () => {
|
||||
const input = document.getElementById(CHAT_HEADER_TITLE_ID) as HTMLInputElement;
|
||||
if (input) {
|
||||
await timeout(25);
|
||||
input.focus();
|
||||
input.select();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: isFavorited ? 'Remove from favorites' : 'Add to favorites',
|
||||
value: 'add-to-favorites',
|
||||
icon: isFavorited ? <StarFilled /> : <Star />,
|
||||
onClick: () => onFavoriteClick(),
|
||||
},
|
||||
createDropdownItem({
|
||||
label: 'Open in new tab',
|
||||
value: 'open-in-new-tab',
|
||||
icon: <ArrowRight />,
|
||||
link: {
|
||||
to: '/app/chats/$chatId',
|
||||
params: { chatId: chatId || '' },
|
||||
},
|
||||
}),
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
label: 'Duplicate chat',
|
||||
value: 'duplicate',
|
||||
icon: <DuplicatePlus />,
|
||||
loading: isDuplicating,
|
||||
onClick: async () => {
|
||||
if (chatId) {
|
||||
const res = await duplicateChat({ id: chatId });
|
||||
await timeout(100);
|
||||
await navigate({ to: '/app/chats/$chatId', params: { chatId: res.id } });
|
||||
openSuccessMessage('Chat duplicated');
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Delete chat',
|
||||
value: 'delete',
|
||||
icon: <Trash />,
|
||||
loading: isDeleting,
|
||||
onClick: () =>
|
||||
chatId &&
|
||||
deleteChat(
|
||||
{ data: [chatId] },
|
||||
{
|
||||
onSuccess: () => {
|
||||
navigate({ to: '/app/chats' });
|
||||
openSuccessMessage('Chat deleted');
|
||||
},
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [
|
||||
chatId,
|
||||
isDeleting,
|
||||
isDuplicating,
|
||||
deleteChat,
|
||||
duplicateChat,
|
||||
isFavorited,
|
||||
onFavoriteClick,
|
||||
openSuccessMessage,
|
||||
navigate,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Dropdown align="end" items={menuItem}>
|
||||
{chatId ? children : null}
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
ChatContainerHeaderDropdown.displayName = 'ChatContainerHeaderDropdown';
|
|
@ -1 +0,0 @@
|
|||
export * from './ChatHeaderOptions';
|
Loading…
Reference in New Issue