chat header options update

This commit is contained in:
Nate Kelley 2025-09-11 12:16:42 -06:00
parent 4d7ddb3592
commit fda555211a
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
11 changed files with 258 additions and 160 deletions

View File

@ -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;
}

View File

@ -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;

View File

@ -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';

View File

@ -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]);
};

View File

@ -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();

View File

@ -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]
);
};

View File

@ -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();

View File

@ -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';

View File

@ -1 +0,0 @@
export * from './ChatHeaderOptions';