diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf36d..05626a1c9 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -22,3 +22,6 @@ dist-ssr *.njsproj *.sln *.sw? + +# Million Lint +.million diff --git a/frontend/package.json b/frontend/package.json index 722e53c59..f0941d8fa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,7 @@ "copy-html-entry": "cp ../raven/public/raven/index.html ../raven/www/raven.html" }, "dependencies": { - "@radix-ui/themes": "^3.1.3", + "@radix-ui/themes": "3.1.4", "@tiptap/extension-code-block-lowlight": "2.5.9", "@tiptap/extension-highlight": "2.5.9", "@tiptap/extension-image": "2.5.9", @@ -33,16 +33,16 @@ "cal-sans": "^1.0.1", "chrono-node": "^2.7.7", "clsx": "^2.1.0", - "cmdk": "^1.0.0", + "cmdk": "^1.0.4", "cva": "npm:class-variance-authority", "dayjs": "^1.11.11", "downshift": "^8.3.1", - "emoji-picker-element": "^1.22.3", + "emoji-picker-element": "^1.25.0", "firebase": "^10.9.0", - "frappe-react-sdk": "^1.8.0", + "frappe-react-sdk": "^1.9.0", "highlight.js": "^11.9.0", "html-react-parser": "^5.1.8", - "jotai": "^2.9.3", + "jotai": "^2.10.3", "js-cookie": "^3.0.5", "lowlight": "^3.1.0", "react": "^18.3.1", @@ -50,18 +50,19 @@ "react-dropzone": "^14.2.3", "react-error-boundary": "^4.0.13", "react-hook-form": "^7.52.2", - "react-icons": "^5.3.0", + "react-icons": "^5.4.0", "react-idle-timer": "^5.7.2", "react-intersection-observer": "^9.10.3", "react-router-dom": "^6.26.1", + "react-virtuoso": "^4.12.3", "react-zoom-pan-pinch": "^3.4.4", - "sonner": "^1.5.0", + "sonner": "^1.7.0", "tailwindcss": "^3.4.10", "tailwindcss-animate": "^1.0.7", "tippy.js": "^6.3.7", "turndown": "^7.2.0", "use-double-tap": "^1.3.6", - "vaul": "^0.9.1", + "vaul": "^1.1.1", "vite": "^4.5.5", "vite-plugin-pwa": "^0.20.0", "vite-plugin-svgr": "^4.2.0" @@ -72,5 +73,8 @@ "@types/react-dom": "^18.2.19", "@types/turndown": "^5.0.4", "typescript": "^5.3.3" + }, + "resolutions": { + "@radix-ui/react-dialog": "1.1.1" } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c7fad569c..7e47d0c46 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,9 +1,8 @@ import { FrappeProvider } from 'frappe-react-sdk' -import { Route, RouterProvider, createBrowserRouter, createRoutesFromElements } from 'react-router-dom' +import { Navigate, Route, RouterProvider, createBrowserRouter, createRoutesFromElements } from 'react-router-dom' import { MainPage } from './pages/MainPage' import { ProtectedRoute } from './utils/auth/ProtectedRoute' import { UserProvider } from './utils/auth/UserProvider' -import { ChannelRedirect } from './utils/channel/ChannelRedirect' import "cal-sans"; import { ThemeProvider } from './ThemeProvider' import { Toaster } from 'sonner' @@ -11,6 +10,8 @@ import { useStickyState } from './hooks/useStickyState' import MobileTabsPage from './pages/MobileTabsPage' import Cookies from 'js-cookie' import ErrorPage from './pages/ErrorPage' +import WorkspaceSwitcher from './pages/WorkspaceSwitcher' +import WorkspaceSwitcherGrid from './components/layout/WorkspaceSwitcherGrid' /** Following keys will not be cached in app cache */ const NO_CACHE_KEYS = [ @@ -19,9 +20,13 @@ const NO_CACHE_KEYS = [ "frappe.model.workflow.get_transitions", "frappe.desk.reportview.get_count", "frappe.core.doctype.server_script.server_script.enabled", - "raven.api.message_actions.get_action_defaults" + "raven.api.message_actions.get_action_defaults", + "raven.api.document_link.get_preview_data" ] +const lastWorkspace = localStorage.getItem('ravenLastWorkspace') ?? '' +const lastChannel = localStorage.getItem('ravenLastChannel') ?? '' + const router = createBrowserRouter( createRoutesFromElements( @@ -31,64 +36,73 @@ const router = createBrowserRouter( import('@/pages/auth/SignUp')} /> import('@/pages/auth/ForgotPassword')} /> } errorElement={}> - }> - } > + }> + : lastWorkspace ? : } /> + } /> + import('./pages/settings/Settings')}> + import('./components/feature/userSettings/UserProfile/UserProfile')} /> + import('./components/feature/userSettings/UserProfile/UserProfile')} /> + import('./pages/settings/Users/UserList')} /> + import('./pages/settings/Appearance')} /> + import('./pages/settings/Integrations/FrappeHR')} /> + + + import('./pages/settings/Workspaces/WorkspaceList')} /> + import('./pages/settings/Workspaces/ViewWorkspace')} /> + + + + import('./pages/settings/AI/BotList')} /> + import('./pages/settings/AI/CreateBot')} /> + import('./pages/settings/AI/ViewBot')} /> + + + + import('./pages/settings/AI/FunctionList')} /> + import('./pages/settings/AI/CreateFunction')} /> + import('./pages/settings/AI/ViewFunction')} /> + + + + + import('./pages/settings/AI/InstructionTemplateList')} /> + import('./pages/settings/AI/CreateInstructionTemplate')} /> + import('./pages/settings/AI/ViewInstructionTemplate')} /> + + + + import('./pages/settings/AI/SavedPromptsList')} /> + import('./pages/settings/AI/CreateSavedPrompt')} /> + import('./pages/settings/AI/ViewSavedPrompt')} /> + + + import('./pages/settings/AI/OpenAISettings')} /> + + + import('./pages/settings/Webhooks/WebhookList')} /> + import('./pages/settings/Webhooks/CreateWebhook')} /> + import('./pages/settings/Webhooks/ViewWebhook')} /> + + + + import('./pages/settings/ServerScripts/SchedulerEvents/SchedulerEvents')} /> + import('./pages/settings/ServerScripts/SchedulerEvents/CreateSchedulerEvent')} /> + import('./pages/settings/ServerScripts/SchedulerEvents/ViewSchedulerEvent')} /> + + + + import('./pages/settings/MessageActions/MessageActionList')} /> + import('./pages/settings/MessageActions/CreateMessageAction')} /> + import('./pages/settings/MessageActions/ViewMessageAction')} /> + + + }> } /> import('./components/feature/threads/Threads')}> import('./components/feature/threads/ThreadDrawer/ThreadDrawer')} /> import('./components/feature/saved-messages/SavedMessages')} /> - import('./pages/settings/Settings')}> - import('./components/feature/userSettings/UserProfile/UserProfile')} /> - import('./components/feature/userSettings/UserProfile/UserProfile')} /> - import('./pages/settings/Users/UserList')} /> - import('./pages/settings/Appearance')} /> - import('./pages/settings/Integrations/FrappeHR')} /> - - import('./pages/settings/AI/BotList')} /> - import('./pages/settings/AI/CreateBot')} /> - import('./pages/settings/AI/ViewBot')} /> - - - - import('./pages/settings/AI/FunctionList')} /> - import('./pages/settings/AI/CreateFunction')} /> - import('./pages/settings/AI/ViewFunction')} /> - - - - - import('./pages/settings/AI/InstructionTemplateList')} /> - import('./pages/settings/AI/CreateInstructionTemplate')} /> - import('./pages/settings/AI/ViewInstructionTemplate')} /> - - - - import('./pages/settings/AI/SavedPromptsList')} /> - import('./pages/settings/AI/CreateSavedPrompt')} /> - import('./pages/settings/AI/ViewSavedPrompt')} /> - - - import('./pages/settings/AI/OpenAISettings')} /> - - - import('./pages/settings/Webhooks/WebhookList')} /> - import('./pages/settings/Webhooks/CreateWebhook')} /> - import('./pages/settings/Webhooks/ViewWebhook')} /> - - - - import('./pages/settings/ServerScripts/SchedulerEvents/SchedulerEvents')} /> - import('./pages/settings/ServerScripts/SchedulerEvents/CreateSchedulerEvent')} /> - import('./pages/settings/ServerScripts/SchedulerEvents/ViewSchedulerEvent')} /> - - - - import('./pages/settings/MessageActions/MessageActionList')} /> - import('./pages/settings/MessageActions/CreateMessageAction')} /> - import('./pages/settings/MessageActions/ViewMessageAction')} /> - - + import('@/pages/ChatSpace')}> import('./components/feature/threads/ThreadDrawer/ThreadDrawer')} /> @@ -123,7 +137,7 @@ function App() { socketPort={import.meta.env.VITE_SOCKET_PORT ? import.meta.env.VITE_SOCKET_PORT : undefined} //@ts-ignore swrConfig={{ - provider: localStorageProvider + errorRetryCount: 2, }} siteName={getSiteName()} > diff --git a/frontend/src/components/common/Callouts/CustomCallout.tsx b/frontend/src/components/common/Callouts/CustomCallout.tsx index 67e40d3ae..de0ac420b 100644 --- a/frontend/src/components/common/Callouts/CustomCallout.tsx +++ b/frontend/src/components/common/Callouts/CustomCallout.tsx @@ -1,9 +1,5 @@ import { Callout } from "@radix-ui/themes"; -import { - CalloutIconProps, - CalloutRootProps, - CalloutTextProps, -} from "@radix-ui/themes/dist/cjs/components/callout"; +import clsx from "clsx"; import { PropsWithChildren } from "react"; export type CalloutObject = { @@ -12,10 +8,10 @@ export type CalloutObject = { } export type CustomCalloutProps = { - rootProps?: CalloutRootProps; - iconProps?: CalloutIconProps; + rootProps?: Callout.RootProps; + iconProps?: Callout.IconProps; iconChildren?: React.ReactNode; - textProps?: CalloutTextProps; + textProps?: Callout.TextProps; textChildren?: React.ReactNode; }; @@ -27,7 +23,7 @@ export const CustomCallout = ({ iconChildren, }: PropsWithChildren) => { return ( - + {iconChildren} {textChildren} diff --git a/frontend/src/components/common/LinkField/LinkField.tsx b/frontend/src/components/common/LinkField/LinkField.tsx index 7219249ee..bd482c68c 100644 --- a/frontend/src/components/common/LinkField/LinkField.tsx +++ b/frontend/src/components/common/LinkField/LinkField.tsx @@ -4,7 +4,7 @@ import { useCombobox } from "downshift"; import { Filter, SearchResult, useSearch } from "frappe-react-sdk"; import { useState } from "react"; import { Label } from "../Form"; -import { Text, TextField } from "@radix-ui/themes"; +import { Text, TextField, VisuallyHidden } from "@radix-ui/themes"; import { useIsDesktop } from "@/hooks/useMediaQuery"; import clsx from "clsx"; @@ -20,10 +20,11 @@ export interface LinkFieldProps { dropdownClass?: string, required?: boolean, suggestedItems?: SearchResult[], + hideLabel?: boolean } -const LinkField = ({ doctype, filters, label, placeholder, value, required, setValue, disabled, autofocus, dropdownClass, suggestedItems }: LinkFieldProps) => { +const LinkField = ({ doctype, filters, hideLabel = false, label, placeholder, value, required, setValue, disabled, autofocus, dropdownClass, suggestedItems }: LinkFieldProps) => { const [searchText, setSearchText] = useState(value ?? '') @@ -63,9 +64,13 @@ const LinkField = ({ doctype, filters, label, placeholder, value, required, setV return
- + {hideLabel ? + + : + + } ( diff --git a/frontend/src/components/common/Loader.tsx b/frontend/src/components/common/Loader.tsx index e5179e9e6..29532f420 100644 --- a/frontend/src/components/common/Loader.tsx +++ b/frontend/src/components/common/Loader.tsx @@ -1,6 +1,8 @@ -export const Loader = () => { +import clsx from "clsx" + +export const Loader = ({ className }: { className?: string }) => { return ( - + diff --git a/frontend/src/components/common/MemberManager.tsx b/frontend/src/components/common/MemberManager.tsx new file mode 100644 index 000000000..7a87f4284 --- /dev/null +++ b/frontend/src/components/common/MemberManager.tsx @@ -0,0 +1,131 @@ +import { HStack, Stack } from "../layout/Stack" +import { Checkbox, Table, Text, TextField } from "@radix-ui/themes" +import { UserFields, UserListContext } from "@/utils/users/UserListProvider" +import { UserAvatar } from "./UserAvatar" +import { useContext, useEffect, useMemo, useState } from "react" +import { BiSearch } from "react-icons/bi" +import { TableVirtuoso } from "react-virtuoso" + +export type MemberObject = { user: string, is_admin?: 0 | 1, is_member?: 0 | 1 } +type Props = { + currentMembers: MemberObject[] + onChange: (members: MemberObject[]) => void +} + +/** + * Common component to manage members of a workspace/channel + */ +const MemberManager = ({ currentMembers, onChange }: Props) => { + + const { users } = useContext(UserListContext) + + const [search, setSearch] = useState('') + + const onMemberChange = (user: string, is_member?: boolean, is_admin?: boolean) => { + // Add the member after removing the existing member + const newMembers = currentMembers.filter((member) => member.user !== user) + newMembers.push({ user, is_admin: is_admin && is_member ? 1 : 0, is_member: is_member ? 1 : 0 }) + onChange(newMembers) + } + + const filteredUsers = useMemo(() => { + return users.filter((user) => user.full_name.toLowerCase().includes(search.toLowerCase())) + }, [users, search]) + + const userMembershipMap: Record = currentMembers.reduce((acc, member) => { + acc[member.user] = { + is_admin: member.is_admin ? true : false, + is_member: member.is_member ? true : false + } + return acc + }, {} as Record) + + return ( + + + + , + TableHead: Table.Header, + TableBody: Table.Body, + TableRow: (props) => , + }} + fixedHeaderContent={() => ( + + User + Member + Admin + + )} + itemContent={(index, user) => { + return + }} + /> + + + ) +} + +const SearchBar = ({ onSearch }: { onSearch: (search: string) => void }) => { + + const [search, setSearch] = useState('') + + useEffect(() => { + // Debounced search + const timeout = setTimeout(() => { + onSearch(search) + }, 250) + return () => clearTimeout(timeout) + }, [search]) + + return ( + setSearch(e.target.value)}> + + + + + ) +} + +interface MemberRowProps { + member: UserFields + membership?: { is_admin: boolean, is_member: boolean } + onMemberChange: (user: string, is_member?: boolean, is_admin?: boolean) => void +} + +const MemberRow = ({ member, onMemberChange, membership }: MemberRowProps) => { + + return ( + <> + onMemberChange(member.name, !membership?.is_member, membership?.is_admin)}> + + + + {member.full_name ?? member.name} + + + + +
+ onMemberChange(member.name, checked ? true : false, membership?.is_admin)} /> +
+ + +
+ onMemberChange(member.name, membership?.is_member, checked ? true : false)} /> +
+
+ + ) +} + +export default MemberManager \ No newline at end of file diff --git a/frontend/src/components/feature/CommandMenu/ChannelItem.tsx b/frontend/src/components/feature/CommandMenu/ChannelItem.tsx index 2402339c6..056c03251 100644 --- a/frontend/src/components/feature/CommandMenu/ChannelItem.tsx +++ b/frontend/src/components/feature/CommandMenu/ChannelItem.tsx @@ -1,10 +1,12 @@ import { ChannelListItem } from "@/utils/channel/ChannelListProvider" import { ChannelIcon } from "@/utils/layout/channelIcon" -import { Badge, Flex } from "@radix-ui/themes" +import { Badge, Flex, Text } from "@radix-ui/themes" import { Command } from "cmdk" import { useNavigate } from "react-router-dom" import { useSetAtom } from 'jotai' import { commandMenuOpenAtom } from "./CommandMenu" +import { BiBuildings } from "react-icons/bi" +import { HStack } from "@/components/layout/Stack" const ChannelItem = ({ channel }: { channel: ChannelListItem }) => { @@ -14,7 +16,7 @@ const ChannelItem = ({ channel }: { channel: ChannelListItem }) => { const onSelect = () => { setOpen(false) - navigate(`/channel/${channel.name}`) + navigate(`/${channel.workspace}/${channel.name}`) } return { {channel.channel_name} + + + {channel.workspace} + + {channel.is_archived ? Archived : null} diff --git a/frontend/src/components/feature/CommandMenu/CommandMenu.tsx b/frontend/src/components/feature/CommandMenu/CommandMenu.tsx index 939198045..fb04c3ec4 100644 --- a/frontend/src/components/feature/CommandMenu/CommandMenu.tsx +++ b/frontend/src/components/feature/CommandMenu/CommandMenu.tsx @@ -1,6 +1,6 @@ import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog' import { Dialog, VisuallyHidden } from '@radix-ui/themes' -import { Command } from 'cmdk' +import { Command, defaultFilter } from 'cmdk' import { useEffect } from 'react' import './commandMenu.styles.css' import ChannelList from './ChannelList' @@ -10,6 +10,7 @@ import { atom, useAtom } from 'jotai' import { useIsDesktop } from '@/hooks/useMediaQuery' import { Drawer, DrawerContent } from '@/components/layout/Drawer' import SettingsList from './SettingsList' +import ToggleThemeCommand from './ToggleThemeCommand' export const commandMenuOpenAtom = atom(false) @@ -52,21 +53,25 @@ const CommandMenu = () => {
- } - - - - - - } export const CommandList = () => { const isDesktop = useIsDesktop() - return + + /** Use a custom filter instead of the default one - ignore very low scores in results */ + const customFilter = (value: string, search: string, keywords?: string[]) => { + const score = defaultFilter ? defaultFilter(value, search, keywords) : 1 + + if (score <= 0.1) { + return 0 + } + return score + } + + return @@ -75,6 +80,9 @@ export const CommandList = () => { + + + {/* TODO: Make these commands work */} {/* diff --git a/frontend/src/components/feature/CommandMenu/SettingsList.tsx b/frontend/src/components/feature/CommandMenu/SettingsList.tsx index b39d3341e..4e819232c 100644 --- a/frontend/src/components/feature/CommandMenu/SettingsList.tsx +++ b/frontend/src/components/feature/CommandMenu/SettingsList.tsx @@ -4,7 +4,7 @@ import { BiBoltCircle, BiBot, BiFile, BiGroup, BiMessageSquareDots, BiTime, BiUs import { useNavigate } from 'react-router-dom' import { commandMenuOpenAtom } from './CommandMenu' import { PiOpenAiLogo } from 'react-icons/pi' -import { LuFunctionSquare } from 'react-icons/lu' +import { LuSquareFunction } from 'react-icons/lu' import { AiOutlineApi } from 'react-icons/ai' type Props = {} @@ -18,7 +18,7 @@ const SettingsList = (props: Props) => { const setOpen = useSetAtom(commandMenuOpenAtom) const onSelect = (value: string) => { - navigate(`/channel/settings/${value}`) + navigate(`/settings/${value}`) setOpen(false) } return ( @@ -68,7 +68,7 @@ const SettingsList = (props: Props) => { - + Functions diff --git a/frontend/src/components/feature/CommandMenu/ToggleThemeCommand.tsx b/frontend/src/components/feature/CommandMenu/ToggleThemeCommand.tsx new file mode 100644 index 000000000..eac3dcf19 --- /dev/null +++ b/frontend/src/components/feature/CommandMenu/ToggleThemeCommand.tsx @@ -0,0 +1,26 @@ +import { useTheme } from '@/ThemeProvider' +import { Command } from 'cmdk' +import { useSetAtom } from 'jotai' +import { BiMoon, BiSun } from 'react-icons/bi' +import { commandMenuOpenAtom } from './CommandMenu' + +const ToggleThemeCommand = () => { + + const { appearance, setAppearance } = useTheme() + + const setOpen = useSetAtom(commandMenuOpenAtom) + + const onSelect = () => { + setAppearance(appearance === 'light' ? 'dark' : 'light') + setOpen(false) + } + + return ( + + {appearance === 'light' ? : } + Toggle Theme + + ) +} + +export default ToggleThemeCommand \ No newline at end of file diff --git a/frontend/src/components/feature/GlobalSearch/ChannelSearch.tsx b/frontend/src/components/feature/GlobalSearch/ChannelSearch.tsx deleted file mode 100644 index 56abc15ba..000000000 --- a/frontend/src/components/feature/GlobalSearch/ChannelSearch.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { BiSearch } from 'react-icons/bi' -import { useFrappeGetCall } from 'frappe-react-sdk' -import { useState } from 'react' -import { useNavigate } from 'react-router-dom' -import { useDebounce } from '../../../hooks/useDebounce' -import { GetChannelSearchResult } from '../../../../../types/Search/Search' -import { ErrorBanner } from '../../layout/AlertBanner/ErrorBanner' -import { EmptyStateForSearch } from '../../layout/EmptyState/EmptyState' -import { ChannelIcon } from '@/utils/layout/channelIcon' -import { Flex, Select, TextField, Box, Checkbox, ScrollArea, Text, Badge } from '@radix-ui/themes' -import { Loader } from '@/components/common/Loader' -interface Props { - onToggleMyChannels: () => void, - isOnlyInMyChannels: boolean, - input: string, - onClose: () => void -} - -export const ChannelSearch = ({ onToggleMyChannels, isOnlyInMyChannels, input, onClose }: Props) => { - - const [searchText, setSearchText] = useState(input) - const debouncedText = useDebounce(searchText) - - const [channelType, setChannelType] = useState() - - const navigate = useNavigate() - - const handleChange = (e: React.ChangeEvent) => { - setSearchText(e.target.value) - } - - const { data, error, isLoading } = useFrappeGetCall<{ message: GetChannelSearchResult[] }>("raven.api.search.get_search_result", { - filter_type: 'Channel', - search_text: debouncedText, - channel_type: channelType === 'any' ? undefined : channelType, - my_channel_only: isOnlyInMyChannels, - }, undefined, { - revalidateOnFocus: false - }) - - return ( - - - - - - - - - {isLoading && } - - - - - - - - Channel Type - - - - 🤷🏻‍♀️ - - Any - - - - - - Public - - - - - - Private - - - - - - Open - - - - - - - - - - - Only in my channels - - - - - - - - {data?.message?.length === 0 && } - {data?.message && data.message.length > 0 ? - - {data.message.map((channel: GetChannelSearchResult) => { - return ( - { - navigate(`/channel/${channel.name}`) - onClose() - }} - key={channel.name}> - - - - {channel.channel_name} - {channel.is_archived ? Archived : null} - - - - ) - })} - : null} - - - ) -} \ No newline at end of file diff --git a/frontend/src/components/feature/GlobalSearch/GlobalSearch.tsx b/frontend/src/components/feature/GlobalSearch/GlobalSearch.tsx index 38f5db99e..1bb642a41 100644 --- a/frontend/src/components/feature/GlobalSearch/GlobalSearch.tsx +++ b/frontend/src/components/feature/GlobalSearch/GlobalSearch.tsx @@ -1,4 +1,3 @@ -import { ChannelSearch } from "./ChannelSearch" import { FileSearch } from "./FileSearch" import { MessageSearch } from "./MessageSearch" import { Dialog, Flex, Tabs, Box } from "@radix-ui/themes" @@ -66,7 +65,6 @@ const GlobalSearchContent = (props: GlobalSearchModalProps) => { Messages Files - Channels @@ -75,9 +73,6 @@ const GlobalSearchContent = (props: GlobalSearchModalProps) => { - - - diff --git a/frontend/src/components/feature/GlobalSearch/MessageBox.tsx b/frontend/src/components/feature/GlobalSearch/MessageBox.tsx index c8e7c535c..997c67582 100644 --- a/frontend/src/components/feature/GlobalSearch/MessageBox.tsx +++ b/frontend/src/components/feature/GlobalSearch/MessageBox.tsx @@ -9,8 +9,8 @@ import { useMemo } from "react" import { DateMonthYear } from "@/utils/dateConversions" type MessageBoxProps = { - message: Message - handleScrollToMessage: (messageName: string, channelID: string) => void + message: Message & { workspace?: string } + handleScrollToMessage: (messageName: string, channelID: string, workspace?: string) => void } export const MessageBox = ({ message, handleScrollToMessage }: MessageBoxProps) => { @@ -44,7 +44,7 @@ export const MessageBox = ({ message, handleScrollToMessage }: MessageBoxProps) - handleScrollToMessage(message.name, channel_id)}> + handleScrollToMessage(message.name, channel_id, message.workspace)}> View in channel diff --git a/frontend/src/components/feature/channel-details/ChannelDetails.tsx b/frontend/src/components/feature/channel-details/ChannelDetails.tsx index 83cd0b018..60934ce31 100644 --- a/frontend/src/components/feature/channel-details/ChannelDetails.tsx +++ b/frontend/src/components/feature/channel-details/ChannelDetails.tsx @@ -1,4 +1,4 @@ -import { useContext } from "react" +import { useContext, useMemo } from "react" import { UserContext } from "../../../utils/auth/UserProvider" import { ChannelListItem } from "@/utils/channel/ChannelListProvider" import { ChannelIcon } from "@/utils/layout/channelIcon" @@ -23,6 +23,14 @@ export const ChannelDetails = ({ channelData, channelMembers, onClose }: Channel const channelOwner = useGetUser(channelData.owner) + const { channelMember, isAdmin } = useMemo(() => { + const channelMember = channelMembers[currentUser] + return { + channelMember, + isAdmin: channelMember?.is_admin == 1 + } + }, [channelMembers, currentUser]) + return ( @@ -40,11 +48,11 @@ export const ChannelDetails = ({ channelData, channelMembers, onClose }: Channel channel_name={channelData.channel_name} className="" channelType={channelData.type} - disabled={channelData.is_archived == 1} /> + disabled={channelData.is_archived == 1 && !isAdmin} /> @@ -56,7 +64,7 @@ export const ChannelDetails = ({ channelData, channelMembers, onClose }: Channel {channelData && channelData.channel_description && channelData.channel_description.length > 0 ? channelData.channel_description : 'No description'} - + @@ -71,7 +79,7 @@ export const ChannelDetails = ({ channelData, channelMembers, onClose }: Channel {/* users can only leave channels they are members of */} {/* users cannot leave open channels */} - {channelMembers[currentUser] && Object.keys(channelMembers).length > 1 && channelData?.type != 'Open' && channelData.is_archived == 0 && + {channelMember && Object.keys(channelMembers).length > 1 && channelData?.type != 'Open' && channelData.is_archived == 0 && <> diff --git a/frontend/src/components/feature/channel-details/edit-channel-description/EditChannelDescriptionModal.tsx b/frontend/src/components/feature/channel-details/edit-channel-description/EditChannelDescriptionModal.tsx index 6ee0dfe34..8a98e87e1 100644 --- a/frontend/src/components/feature/channel-details/edit-channel-description/EditChannelDescriptionModal.tsx +++ b/frontend/src/components/feature/channel-details/edit-channel-description/EditChannelDescriptionModal.tsx @@ -68,7 +68,7 @@ export const EditChannelDescriptionModalContent = ({ channelData, onClose }: Ren diff --git a/frontend/src/components/feature/channel-details/leave-channel/LeaveChannelModal.tsx b/frontend/src/components/feature/channel-details/leave-channel/LeaveChannelModal.tsx index 6d3c9db19..177ca7f5f 100644 --- a/frontend/src/components/feature/channel-details/leave-channel/LeaveChannelModal.tsx +++ b/frontend/src/components/feature/channel-details/leave-channel/LeaveChannelModal.tsx @@ -67,7 +67,7 @@ export const LeaveChannelModal = ({ onClose, channelData, isDrawer, closeDetails diff --git a/frontend/src/components/feature/channel-details/rename-channel/ChannelRenameModal.tsx b/frontend/src/components/feature/channel-details/rename-channel/ChannelRenameModal.tsx index 959389e12..8c79b490f 100644 --- a/frontend/src/components/feature/channel-details/rename-channel/ChannelRenameModal.tsx +++ b/frontend/src/components/feature/channel-details/rename-channel/ChannelRenameModal.tsx @@ -103,7 +103,7 @@ export const RenameChannelModalContent = ({ channelID, channelName, type, onClos diff --git a/frontend/src/components/feature/channel-member-details/add-members/AddChannelMemberModalContent.tsx b/frontend/src/components/feature/channel-member-details/add-members/AddChannelMemberModalContent.tsx index e0bab186b..f018dac37 100644 --- a/frontend/src/components/feature/channel-member-details/add-members/AddChannelMemberModalContent.tsx +++ b/frontend/src/components/feature/channel-member-details/add-members/AddChannelMemberModalContent.tsx @@ -1,5 +1,5 @@ import { Controller, FormProvider, useForm } from 'react-hook-form' -import { useFrappeCreateDoc, useSWRConfig } from 'frappe-react-sdk' +import { useFrappePostCall, useSWRConfig } from 'frappe-react-sdk' import { ErrorBanner } from '@/components/layout/AlertBanner/ErrorBanner' import { Loader } from '@/components/common/Loader' import { Box, Button, Dialog, Flex, Text } from '@radix-ui/themes' @@ -33,7 +33,8 @@ export const AddChannelMembersModalContent = ({ onClose }: AddChannelMemberModal const { mutate } = useSWRConfig() - const { createDoc, error, loading: creatingDoc } = useFrappeCreateDoc() + const { call, error, loading } = useFrappePostCall('raven.api.raven_channel_member.add_channel_members') + const methods = useForm({ defaultValues: { add_members: null @@ -44,14 +45,10 @@ export const AddChannelMembersModalContent = ({ onClose }: AddChannelMemberModal const onSubmit = (data: AddChannelMemberForm) => { if (data.add_members && data.add_members.length > 0) { - const promises = data.add_members.map(async (member) => { - return createDoc('Raven Channel Member', { - channel_id: channelID, - user_id: member.name - }) + call({ + channel_id: channelID, + members: data.add_members.map((member) => member.name) }) - - Promise.all(promises) .then(() => { toast.success("Members added") mutate(["channel_members", channelID]) @@ -66,10 +63,13 @@ export const AddChannelMembersModalContent = ({ onClose }: AddChannelMemberModal
- Add members to  {channel?.channelData.channel_name} + Add members to {channel?.channelData.channel_name} + + New members will be able to see all of {channel?.channelData.channel_name}'s history, including any files that have been shared in the channel. + - + @@ -93,18 +93,17 @@ export const AddChannelMembersModalContent = ({ onClose }: AddChannelMemberModal {methods.formState.errors.add_members?.message} - New members will be able to see all of {channel?.channelData.channel_name}'s history, including any files that have been shared in the channel. - + - diff --git a/frontend/src/components/feature/channel-member-details/add-members/AddChannelMembersModal.tsx b/frontend/src/components/feature/channel-member-details/add-members/AddChannelMembersModal.tsx index 75b8275cb..37340b130 100644 --- a/frontend/src/components/feature/channel-member-details/add-members/AddChannelMembersModal.tsx +++ b/frontend/src/components/feature/channel-member-details/add-members/AddChannelMembersModal.tsx @@ -25,7 +25,8 @@ const AddChannelMembersModal = ({ if (isDesktop) { return ( - + {/* The backdrop is removed in this case because we don't want the backdrop blur to appear in front of the dropdown contents */} + diff --git a/frontend/src/components/feature/channel-member-details/remove-members/RemoveChannelMemberModal.tsx b/frontend/src/components/feature/channel-member-details/remove-members/RemoveChannelMemberModal.tsx index 1fed8c80d..221702eac 100644 --- a/frontend/src/components/feature/channel-member-details/remove-members/RemoveChannelMemberModal.tsx +++ b/frontend/src/components/feature/channel-member-details/remove-members/RemoveChannelMemberModal.tsx @@ -67,7 +67,7 @@ export const RemoveChannelMemberModal = ({ onClose, member }: RemoveChannelMembe @@ -95,7 +95,7 @@ export const RemoveChannelMemberModal = ({ onClose, member }: RemoveChannelMembe Cancel diff --git a/frontend/src/components/feature/channel-settings/archive-channel/ArchiveChannelModal.tsx b/frontend/src/components/feature/channel-settings/archive-channel/ArchiveChannelModal.tsx index d7e3044a3..3d8f31436 100644 --- a/frontend/src/components/feature/channel-settings/archive-channel/ArchiveChannelModal.tsx +++ b/frontend/src/components/feature/channel-settings/archive-channel/ArchiveChannelModal.tsx @@ -61,7 +61,7 @@ export const ArchiveChannelModal = ({ onClose, onCloseViewDetails, channelData, diff --git a/frontend/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeModal.tsx b/frontend/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeModal.tsx index b6fe38cc3..c3cb16f33 100644 --- a/frontend/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeModal.tsx +++ b/frontend/src/components/feature/channel-settings/change-channel-type/ChangeChannelTypeModal.tsx @@ -60,7 +60,7 @@ export const ChangeChannelTypeModal = ({ onClose, channelData, newChannelType }: diff --git a/frontend/src/components/feature/channel-settings/delete-channel/DeleteChannelModal.tsx b/frontend/src/components/feature/channel-settings/delete-channel/DeleteChannelModal.tsx index 82921f38b..880e3106d 100644 --- a/frontend/src/components/feature/channel-settings/delete-channel/DeleteChannelModal.tsx +++ b/frontend/src/components/feature/channel-settings/delete-channel/DeleteChannelModal.tsx @@ -1,7 +1,7 @@ import { ErrorBanner } from '@/components/layout/AlertBanner/ErrorBanner' import { Fragment, useState } from 'react' import { useNavigate } from 'react-router-dom' -import { useFrappeDeleteDoc } from 'frappe-react-sdk' +import { useFrappeDeleteDoc, useSWRConfig } from 'frappe-react-sdk' import { ChannelListItem } from '@/utils/channel/ChannelListProvider' import { AlertDialog, Button, Callout, Checkbox, Dialog, Flex, Text } from '@radix-ui/themes' import { Loader } from '@/components/common/Loader' @@ -17,6 +17,8 @@ type DeleteChannelModalProps = { export const DeleteChannelModal = ({ onClose, onCloseParent, isDrawer, channelData }: DeleteChannelModalProps) => { + const { mutate } = useSWRConfig() + const { deleteDoc, error, loading: deletingDoc, reset } = useFrappeDeleteDoc() const handleClose = () => { @@ -30,6 +32,8 @@ export const DeleteChannelModal = ({ onClose, onCloseParent, isDrawer, channelDa if (channelData?.name) { deleteDoc('Raven Channel', channelData.name) .then(() => { + // Mutate the channel members cache + mutate(["channel_members", channelData.name], undefined, { revalidate: false }) onClose() onCloseParent() localStorage.removeItem('ravenLastChannel') @@ -83,7 +87,7 @@ export const DeleteChannelModal = ({ onClose, onCloseParent, isDrawer, channelDa diff --git a/frontend/src/components/feature/channels/ChannelList.tsx b/frontend/src/components/feature/channels/ChannelList.tsx index c85e3fd32..6c4c893d1 100644 --- a/frontend/src/components/feature/channels/ChannelList.tsx +++ b/frontend/src/components/feature/channels/ChannelList.tsx @@ -61,6 +61,7 @@ export const ChannelList = ({ channels }: ChannelListProps) => { }} >
+ {filteredChannels.length === 0 ? {__("No channels found")} : null} {filteredChannels.map((channel: ChannelWithUnreadCount) => )} diff --git a/frontend/src/components/feature/channels/CreateChannelModal.tsx b/frontend/src/components/feature/channels/CreateChannelModal.tsx index a650fff66..026cc9bbd 100644 --- a/frontend/src/components/feature/channels/CreateChannelModal.tsx +++ b/frontend/src/components/feature/channels/CreateChannelModal.tsx @@ -1,8 +1,8 @@ -import { useFrappeCreateDoc } from 'frappe-react-sdk' +import { useFrappeCreateDoc, useFrappeGetCall, useSWRConfig } from 'frappe-react-sdk' import { ChangeEvent, useCallback, useMemo, useState } from 'react' import { Controller, FormProvider, useForm } from 'react-hook-form' -import { BiGlobe, BiHash, BiLockAlt } from 'react-icons/bi' -import { useNavigate } from 'react-router-dom' +import { BiGlobe, BiHash, BiInfoCircle, BiLockAlt } from 'react-icons/bi' +import { useNavigate, useParams } from 'react-router-dom' import { ErrorBanner } from '@/components/layout/AlertBanner/ErrorBanner' import { Box, Button, Dialog, Flex, IconButton, RadioGroup, Text, TextArea, TextField } from '@radix-ui/themes' import { ErrorText, HelperText, Label } from '@/components/common/Form' @@ -13,6 +13,7 @@ import { FiPlus } from 'react-icons/fi' import { useIsDesktop } from '@/hooks/useMediaQuery' import { Drawer, DrawerContent, DrawerTrigger } from '@/components/layout/Drawer' import { __ } from '@/utils/translations' +import { CustomCallout } from '@/components/common/Callouts/CustomCallout' interface ChannelCreationForm { channel_name: string, @@ -30,7 +31,7 @@ export const CreateChannelButton = ({ updateChannelList }: { updateChannelList: return + className='transition-all ease-ease text-gray-10 bg-transparent hover:bg-gray-3 hover:text-gray-12'> @@ -45,7 +46,7 @@ export const CreateChannelButton = ({ updateChannelList }: { updateChannelList: + className='transition-all ease-ease text-gray-10 bg-transparent hover:bg-gray-3 hover:text-gray-12'> @@ -66,6 +67,12 @@ export const CreateChannelButton = ({ updateChannelList }: { updateChannelList: const CreateChannelContent = ({ updateChannelList, isOpen, setIsOpen }: { updateChannelList: VoidFunction, setIsOpen: (v: boolean) => void, isOpen: boolean }) => { + + const { workspaceID } = useParams() + + const { data: isAdmin } = useFrappeGetCall<{ message: boolean }>('raven.api.workspaces.is_workspace_admin', { workspace: workspaceID }, workspaceID ? undefined : null) + + const { mutate } = useSWRConfig() let navigate = useNavigate() const methods = useForm({ defaultValues: { @@ -79,12 +86,13 @@ const CreateChannelContent = ({ updateChannelList, isOpen, setIsOpen }: { update const { createDoc, error: channelCreationError, loading: creatingChannel, reset: resetCreateHook } = useFrappeCreateDoc() - const onClose = (channel_name?: string) => { + const onClose = (channel_name?: string, workspace?: string) => { if (channel_name) { // Update channel list when name is provided. // Also navigate to new channel updateChannelList() - navigate(`/channel/${channel_name}`) + navigate(`/${workspace}/${channel_name}`) + mutate(["channel_members", channel_name]) } setIsOpen(false) @@ -95,16 +103,17 @@ const CreateChannelContent = ({ updateChannelList, isOpen, setIsOpen }: { update resetCreateHook() resetForm() } - - - const channelType = watch('type') + const onSubmit = (data: ChannelCreationForm) => { - createDoc('Raven Channel', data).then(result => { + createDoc('Raven Channel', { + ...data, + workspace: workspaceID + }).then(result => { if (result) { toast.success(__("Channel created")) - onClose(result.name) + onClose(result.name, workspaceID) } }) } @@ -149,6 +158,11 @@ const CreateChannelContent = ({ updateChannelList, isOpen, setIsOpen }: { update + {!isAdmin?.message && } + rootProps={{ color: 'yellow', variant: 'surface' }} + textChildren={You cannot create a new channel since you are not an admin of this workspace. Ask an admin to create a channel or make you an admin.} + />} @@ -255,8 +269,8 @@ const CreateChannelContent = ({ updateChannelList, isOpen, setIsOpen }: { update {__("Cancel")} - diff --git a/frontend/src/components/feature/chat/ChatInput/AISavedPromptsButton.tsx b/frontend/src/components/feature/chat/ChatInput/AISavedPromptsButton.tsx index 71797fc7e..76ebc75b7 100644 --- a/frontend/src/components/feature/chat/ChatInput/AISavedPromptsButton.tsx +++ b/frontend/src/components/feature/chat/ChatInput/AISavedPromptsButton.tsx @@ -85,7 +85,7 @@ const SavedPrompts = ({ onClose }: { onClose: () => void }) => { + onClick={() => navigate('/settings/commands')}>Create diff --git a/frontend/src/components/feature/chat/ChatInput/DocumentLinkButton.tsx b/frontend/src/components/feature/chat/ChatInput/DocumentLinkButton.tsx index 35315f61c..9d0c8f857 100644 --- a/frontend/src/components/feature/chat/ChatInput/DocumentLinkButton.tsx +++ b/frontend/src/components/feature/chat/ChatInput/DocumentLinkButton.tsx @@ -134,7 +134,7 @@ const DocumentLinkForm = ({ channelID, onClose }: { channelID: string, onClose: diff --git a/frontend/src/components/feature/chat/ChatInput/TextFormattingMenu.tsx b/frontend/src/components/feature/chat/ChatInput/TextFormattingMenu.tsx index c97de0f42..da9e88668 100644 --- a/frontend/src/components/feature/chat/ChatInput/TextFormattingMenu.tsx +++ b/frontend/src/components/feature/chat/ChatInput/TextFormattingMenu.tsx @@ -3,8 +3,9 @@ import { BiBold, BiCodeAlt, BiCodeBlock, BiHighlight, BiItalic, BiListOl, BiList import { DEFAULT_BUTTON_STYLE, ICON_PROPS } from './ToolPanel' import { Box, Flex, IconButton, Separator, Tooltip } from '@radix-ui/themes' import { getKeyboardMetaKeyString } from '@/utils/layout/keyboardKey' +import { memo } from 'react' -export const TextFormattingMenu = () => { +export const TextFormattingMenu = memo(() => { const { editor } = useCurrentEditor() @@ -217,7 +218,7 @@ export const TextFormattingMenu = () => { ) -} +}) const TimestampButton = () => { const { editor } = useCurrentEditor() diff --git a/frontend/src/components/feature/chat/ChatInput/Tiptap.tsx b/frontend/src/components/feature/chat/ChatInput/Tiptap.tsx index e204fed6a..ab1e437a7 100644 --- a/frontend/src/components/feature/chat/ChatInput/Tiptap.tsx +++ b/frontend/src/components/feature/chat/ChatInput/Tiptap.tsx @@ -35,6 +35,7 @@ import { BiPlus } from 'react-icons/bi' import clsx from 'clsx' import { ChannelMembers } from '@/hooks/fetchers/useFetchChannelMembers' import TimestampRenderer from '../ChatMessage/Renderers/TiptapRenderer/TimestampRenderer' +import { useParams } from 'react-router-dom' const MobileInputActions = lazy(() => import('./MobileActions/MobileInputActions')) const lowlight = createLowlight(common) @@ -102,6 +103,8 @@ const Tiptap = ({ isEdit, slotBefore, fileProps, onMessageSend, channelMembers, const { channels } = useContext(ChannelListContext) as ChannelListContextType + const { workspaceID } = useParams() + // this is a dummy extension only to create custom keydown behavior const KeyboardHandler = Extension.create({ name: 'keyboardHandler', @@ -386,7 +389,7 @@ const Tiptap = ({ isEdit, slotBefore, fileProps, onMessageSend, channelMembers, }, suggestion: { items: (query) => { - return channels.filter((channel) => channel.channel_name.toLowerCase().startsWith(query.query.toLowerCase())) + return channels.filter((channel) => channel.workspace === workspaceID && channel.channel_name.toLowerCase().startsWith(query.query.toLowerCase())) .slice(0, 10); }, // char: '#', diff --git a/frontend/src/components/feature/chat/ChatMessage/ActionModals/AttachFileToDocumentModal.tsx b/frontend/src/components/feature/chat/ChatMessage/ActionModals/AttachFileToDocumentModal.tsx index 4b9a40aa5..20e2cdb4f 100644 --- a/frontend/src/components/feature/chat/ChatMessage/ActionModals/AttachFileToDocumentModal.tsx +++ b/frontend/src/components/feature/chat/ChatMessage/ActionModals/AttachFileToDocumentModal.tsx @@ -148,7 +148,7 @@ const AttachFileToDocumentModal = ({ onClose, message }: AttachFileToDocumentMod diff --git a/frontend/src/components/feature/chat/ChatMessage/ActionModals/DeleteMessageModal.tsx b/frontend/src/components/feature/chat/ChatMessage/ActionModals/DeleteMessageModal.tsx index a2806b3ee..447dd2bdb 100644 --- a/frontend/src/components/feature/chat/ChatMessage/ActionModals/DeleteMessageModal.tsx +++ b/frontend/src/components/feature/chat/ChatMessage/ActionModals/DeleteMessageModal.tsx @@ -56,7 +56,7 @@ export const DeleteMessageModal = ({ onClose, message }: DeleteMessageModalProps diff --git a/frontend/src/components/feature/chat/ChatMessage/ActionModals/ForwardMessageModal.tsx b/frontend/src/components/feature/chat/ChatMessage/ActionModals/ForwardMessageModal.tsx index 383901e59..56f9843e0 100644 --- a/frontend/src/components/feature/chat/ChatMessage/ActionModals/ForwardMessageModal.tsx +++ b/frontend/src/components/feature/chat/ChatMessage/ActionModals/ForwardMessageModal.tsx @@ -99,7 +99,7 @@ const ForwardMessageModal = ({ onClose, message }: ForwardMessageModalProps) => diff --git a/frontend/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/CreateThreadButton.tsx b/frontend/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/CreateThreadButton.tsx index cdc2e05e9..ba344bdea 100644 --- a/frontend/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/CreateThreadButton.tsx +++ b/frontend/src/components/feature/chat/ChatMessage/MessageActions/QuickActions/CreateThreadButton.tsx @@ -2,17 +2,19 @@ import { useFrappePostCall } from 'frappe-react-sdk' import { toast } from 'sonner' import { QuickActionButton } from './QuickActionButton' import { BiMessageDetail } from 'react-icons/bi' -import { useNavigate } from 'react-router-dom' +import { useNavigate, useParams } from 'react-router-dom' import { ContextMenu, Flex } from '@radix-ui/themes' const useCreateThread = (messageID: string) => { const navigate = useNavigate() + const { workspaceID } = useParams() + const { call } = useFrappePostCall('raven.api.threads.create_thread') const handleCreateThread = () => { call({ 'message_id': messageID }).then((res) => { toast.success('Thread created successfully!') - navigate(`/channel/${res.message.channel_id}/thread/${res.message.thread_id}`) + navigate(`/${workspaceID}/${res.message.channel_id}/thread/${res.message.thread_id}`) }).catch(() => { toast.error('Failed to create thread') }) diff --git a/frontend/src/components/feature/chat/ChatMessage/Renderers/DateTooltip.tsx b/frontend/src/components/feature/chat/ChatMessage/Renderers/DateTooltip.tsx index 4ed41ca16..27b4ef899 100644 --- a/frontend/src/components/feature/chat/ChatMessage/Renderers/DateTooltip.tsx +++ b/frontend/src/components/feature/chat/ChatMessage/Renderers/DateTooltip.tsx @@ -1,15 +1,29 @@ -import { DateMonthAtHourMinuteAmPm, HourMinuteAmPm } from '@/utils/dateConversions' -import { Tooltip, Text, Link } from '@radix-ui/themes' +import { useMemo } from 'react' +import { Tooltip, Link } from '@radix-ui/themes' +import { getDateObject } from '@/utils/dateConversions/utils' + export const DateTooltip = ({ timestamp }: { timestamp: string }) => { + + const { tooltipContent, time } = useMemo(() => { + + const dateObj = getDateObject(timestamp) + + return { + tooltipContent: dateObj.format("Do MMMM [at] hh:mm A"), + time: dateObj.format("hh:mm A") + } + + }, [timestamp]) + return ( - }> + - + {time} @@ -17,14 +31,26 @@ export const DateTooltip = ({ timestamp }: { timestamp: string }) => { } export const DateTooltipShort = ({ timestamp }: { timestamp: string }) => { + + const { tooltipContent, time } = useMemo(() => { + + const dateObj = getDateObject(timestamp) + + return { + tooltipContent: dateObj.format("Do MMMM [at] hh:mm A"), + time: dateObj.format("hh:mm") + } + + }, [timestamp]) + return ( - }> + - + {time} ) diff --git a/frontend/src/components/feature/chat/ChatMessage/Renderers/DoctypeLinkRenderer.tsx b/frontend/src/components/feature/chat/ChatMessage/Renderers/DoctypeLinkRenderer.tsx index f9b837438..25fd9cb3d 100644 --- a/frontend/src/components/feature/chat/ChatMessage/Renderers/DoctypeLinkRenderer.tsx +++ b/frontend/src/components/feature/chat/ChatMessage/Renderers/DoctypeLinkRenderer.tsx @@ -49,7 +49,7 @@ export const DoctypeLinkRenderer = ({ doctype, docname }: { doctype: string, doc { isLoading ? - : + : error ? diff --git a/frontend/src/components/feature/chat/ChatMessage/Renderers/PollMessage.tsx b/frontend/src/components/feature/chat/ChatMessage/Renderers/PollMessage.tsx index a8e43a845..2faa97794 100644 --- a/frontend/src/components/feature/chat/ChatMessage/Renderers/PollMessage.tsx +++ b/frontend/src/components/feature/chat/ChatMessage/Renderers/PollMessage.tsx @@ -1,17 +1,16 @@ -import { Box, Checkbox, Flex, Text, RadioGroup, Button, Badge } from "@radix-ui/themes" -import { BoxProps } from "@radix-ui/themes/dist/cjs/components/box" +import { Box, Checkbox, Flex, Text, RadioGroup, Button, Badge, BoxProps } from "@radix-ui/themes" import { useEffect, useMemo, useState } from "react" import { UserFields } from "../../../../../utils/users/UserListProvider" import { PollMessage } from "../../../../../../../types/Messaging/Message" import { useFrappeDocumentEventListener, useFrappeGetCall, useFrappePostCall } from "frappe-react-sdk" import { RavenPoll } from "@/types/RavenMessaging/RavenPoll" -import { ErrorBanner } from "@/components/layout/AlertBanner/ErrorBanner" +import { ErrorBanner, getErrorMessage } from "@/components/layout/AlertBanner/ErrorBanner" import { RavenPollOption } from "@/types/RavenMessaging/RavenPollOption" import { ViewPollVotes } from "@/components/feature/polls/ViewPollVotes" import { toast } from "sonner" -interface PollMessageBlockProps extends BoxProps { +type PollMessageBlockProps = BoxProps & { message: PollMessage, user?: UserFields, } @@ -143,6 +142,8 @@ const SingleChoicePoll = ({ data, messageID }: { data: Poll, messageID: string } 'option_id': option.name }).then(() => { toast.success('Your vote has been submitted!') + }).catch((error) => { + toast.error(getErrorMessage(error)) }) } @@ -181,6 +182,8 @@ const MultiChoicePoll = ({ data, messageID }: { data: Poll, messageID: string }) 'option_id': selectedOptions }).then(() => { toast.success('Your vote has been submitted!') + }).catch((error) => { + toast.error(getErrorMessage(error)) }) } diff --git a/frontend/src/components/feature/chat/ChatMessage/Renderers/ThreadMessage.tsx b/frontend/src/components/feature/chat/ChatMessage/Renderers/ThreadMessage.tsx index a382feb01..c79e74187 100644 --- a/frontend/src/components/feature/chat/ChatMessage/Renderers/ThreadMessage.tsx +++ b/frontend/src/components/feature/chat/ChatMessage/Renderers/ThreadMessage.tsx @@ -1,4 +1,4 @@ -import { Link } from "react-router-dom" +import { Link, useParams } from "react-router-dom" import { Message } from "../../../../../../../types/Messaging/Message" import { Button, Flex, Text } from "@radix-ui/themes" import { useFrappeGetDocCount } from "frappe-react-sdk" @@ -8,6 +8,8 @@ import { useFrappeEventListener } from "frappe-react-sdk" export const ThreadMessage = ({ thread }: { thread: Message }) => { + const { workspaceID } = useParams() + return (
@@ -17,7 +19,7 @@ export const ThreadMessage = ({ thread }: { thread: Message }) => { color="gray" variant={'ghost'} className={'not-cal w-fit hover:bg-transparent hover:underline cursor-pointer'}> - View Thread + View Thread
diff --git a/frontend/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/LinkPreview.tsx b/frontend/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/LinkPreview.tsx index fffcc5c65..fd2a53b5b 100644 --- a/frontend/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/LinkPreview.tsx +++ b/frontend/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/LinkPreview.tsx @@ -1,5 +1,5 @@ import { Stack } from '@/components/layout/Stack'; -import { Box, Card, Flex, IconButton, Inset, Link, Text, Tooltip } from '@radix-ui/themes'; +import { Box, Card, IconButton, Text, Tooltip } from '@radix-ui/themes'; import { useCurrentEditor } from "@tiptap/react"; import { useFrappeGetCall, useFrappePostCall } from 'frappe-react-sdk'; import { memo, useMemo } from 'react'; @@ -86,25 +86,28 @@ const LinkPreview = memo(({ messageID }: { messageID: string }) => { if (linkPreview && linkPreview.site_name && linkPreview.description) { const image = linkPreview.absolute_image || linkPreview.image - return + return - - {image && - {linkPreview.title} - } + + {image && {linkPreview.title} + } - + {linkPreview.title} {linkPreview.site_name} - {linkPreview.description} + {linkPreview.description} diff --git a/frontend/src/components/feature/chat/ChatStream/ChatBoxBody.tsx b/frontend/src/components/feature/chat/ChatStream/ChatBoxBody.tsx index f91423d5d..4e49fc265 100644 --- a/frontend/src/components/feature/chat/ChatStream/ChatBoxBody.tsx +++ b/frontend/src/components/feature/chat/ChatStream/ChatBoxBody.tsx @@ -13,7 +13,7 @@ import { ReplyMessageBox } from "../ChatMessage/ReplyMessageBox/ReplyMessageBox" import { BiX } from "react-icons/bi" import ChatStream from "./ChatStream" import Tiptap from "../ChatInput/Tiptap" -import useFetchChannelMembers from "@/hooks/fetchers/useFetchChannelMembers" +import useFetchChannelMembers, { Member } from "@/hooks/fetchers/useFetchChannelMembers" import { useParams } from "react-router-dom" import clsx from "clsx" import { Stack } from "@/components/layout/Stack" @@ -59,11 +59,11 @@ export const ChatBoxBody = ({ channelData }: ChatBoxBodyProps) => { clearSelectedMessage() } - const isUserInChannel = useMemo(() => { + const channelMemberProfile: Member | null = useMemo(() => { if (user && channelMembers) { - return user in channelMembers + return channelMembers[user] ?? null } - return false + return null }, [user, channelMembers]) const { fileInputRef, files, setFiles, removeFile, uploadFiles, addFile, fileUploadProgress } = useFileUpload(channelData.name) @@ -90,14 +90,33 @@ export const ChatBoxBody = ({ channelData }: ChatBoxBodyProps) => { return null } + const { canUserSendMessage, shouldShowJoinBox } = useMemo(() => { + + let canUserSendMessage = false + let shouldShowJoinBox = false + + const isDM = channelData?.is_direct_message === 1 || channelData?.is_self_message === 1 + + + if (channelData.type === 'Open' || channelMemberProfile && channelData.is_archived === 0) { + canUserSendMessage = true + } + + if (channelData.is_archived === 0 && !channelMemberProfile && channelData.type !== 'Open' && !isDM) { + shouldShowJoinBox = true + } + + return { canUserSendMessage, shouldShowJoinBox } + + }, [channelMemberProfile, channelData]) + + - const isDM = channelData?.is_direct_message === 1 || channelData?.is_self_message === 1 const { threadID } = useParams() return ( - - + { channelID={channelData.name} replyToMessage={handleReplyAction} /> - {channelData?.is_archived == 0 && (isUserInChannel || channelData?.type === 'Open') - && + {canUserSendMessage && { /> } - {channelData && !isLoading && <> - {channelData.is_archived == 0 && !isUserInChannel && channelData.type !== 'Open' && !isDM && - } - {channelData.is_archived == 1 && } - } + {shouldShowJoinBox ? + : null} + - + ) +} + +// Separate container to prevent re-rendering when the threadID changes + +const ChatBoxBodyContainer = ({ children }: { children: React.ReactNode }) => { + + const { threadID } = useParams() + + return
+ {children} +
} \ No newline at end of file diff --git a/frontend/src/components/feature/chat/chat-footer/ArchivedChannelBox.tsx b/frontend/src/components/feature/chat/chat-footer/ArchivedChannelBox.tsx index e63c1843f..5bd0e1fec 100644 --- a/frontend/src/components/feature/chat/chat-footer/ArchivedChannelBox.tsx +++ b/frontend/src/components/feature/chat/chat-footer/ArchivedChannelBox.tsx @@ -1,51 +1,66 @@ -import { ErrorBanner } from "@/components/layout/AlertBanner/ErrorBanner" -import { UserContext } from "@/utils/auth/UserProvider" -import { ChannelListItem } from "@/utils/channel/ChannelListProvider" import { useFrappeUpdateDoc } from "frappe-react-sdk" -import { useContext } from "react" -import { Box, Button, Flex, Text } from "@radix-ui/themes" +import { Button, Flex, Text } from "@radix-ui/themes" import { Loader } from "@/components/common/Loader" import { toast } from "sonner" -import { ChannelMembers } from "@/hooks/fetchers/useFetchChannelMembers" +import { Stack } from "@/components/layout/Stack" interface ArchivedChannelBoxProps { - channelData: ChannelListItem, - channelMembers: ChannelMembers + channelID: string, + isArchived?: 0 | 1, + isMemberAdmin?: 0 | 1 } -export const ArchivedChannelBox = ({ channelData, channelMembers }: ArchivedChannelBoxProps) => { +export const ArchivedChannelBox = ({ channelID, isArchived, isMemberAdmin }: ArchivedChannelBoxProps) => { - const { updateDoc, error, loading } = useFrappeUpdateDoc() + if (isArchived === 1) { + return + } + + return null + +} + + +const ArchiveChannelBoxContent = ({ channelID, isMemberAdmin }: { channelID: string, isMemberAdmin?: 0 | 1 }) => { + + return ( + + + This channel has been archived. + {isMemberAdmin === 1 ? : null} + + + ) +} + +const UnArchiveButton = ({ channelID }: { channelID: string }) => { + + const { updateDoc, loading } = useFrappeUpdateDoc() const unArchiveChannel = async () => { - return updateDoc('Raven Channel', channelData.name, { + return updateDoc('Raven Channel', channelID, { is_archived: 0 }).then(() => { toast.success('Channel restored.') + }).catch(err => { + toast.error(err.message) }) } - const { currentUser } = useContext(UserContext) - return ( - - - - - This channel has been archived. - {channelMembers[currentUser]?.is_admin === 1 && } - - - + ) } \ No newline at end of file diff --git a/frontend/src/components/feature/chat/chat-footer/JoinChannelBox.tsx b/frontend/src/components/feature/chat/chat-footer/JoinChannelBox.tsx index 5f9367278..1080e895c 100644 --- a/frontend/src/components/feature/chat/chat-footer/JoinChannelBox.tsx +++ b/frontend/src/components/feature/chat/chat-footer/JoinChannelBox.tsx @@ -7,7 +7,6 @@ import { ChannelMembers } from "@/hooks/fetchers/useFetchChannelMembers" import { useParams } from "react-router-dom" interface JoinChannelBoxProps { channelData?: ChannelListItem, - channelMembers: ChannelMembers, user: string, } @@ -41,7 +40,7 @@ export const JoinChannelBox = ({ channelData, user }: JoinChannelBoxProps) => { onClick={joinChannel} size={channelData ? '2' : '1'} disabled={loading}> - {loading && } + {loading && } {loading ? 'Joining' : Join {channelData ? `${channelData?.channel_name}` : "Conversation"} } diff --git a/frontend/src/components/feature/file-upload/FileDrop.tsx b/frontend/src/components/feature/file-upload/FileDrop.tsx index 7d3feff86..7b52a2794 100644 --- a/frontend/src/components/feature/file-upload/FileDrop.tsx +++ b/frontend/src/components/feature/file-upload/FileDrop.tsx @@ -1,5 +1,4 @@ -import { Flex, Text } from "@radix-ui/themes" -import { FlexProps } from "@radix-ui/themes/dist/cjs/components/flex" +import { Flex, Text, FlexProps } from "@radix-ui/themes" import clsx from "clsx" import { forwardRef, useImperativeHandle, useState } from "react" import { Accept, useDropzone } from "react-dropzone" @@ -11,7 +10,7 @@ export interface CustomFile extends File { uploadProgress?: number } -export interface FileDropProps extends FlexProps { +export type FileDropProps = FlexProps & { /** Array of files */ files: CustomFile[], /** Function to set files in parent */ @@ -95,7 +94,7 @@ export const FileDrop = forwardRef((props: FileDropProps, ref) => { justify='center' className={clsx("fixed top-14 border-2 border-dashed rounded-md border-gray-6 dark:bg-[#171923AA] bg-[#F7FAFCAA]", areaHeight ?? "h-[calc(100vh-72px)]", - width ?? "w-[calc(100vw-var(--sidebar-width)-var(--space-6))]", + width ?? "w-[calc(100vw-var(--sidebar-width)-var(--space-8))]", )} style={{ zIndex: 9999 diff --git a/frontend/src/components/feature/hr/CompanyWorkspaceMapping.tsx b/frontend/src/components/feature/hr/CompanyWorkspaceMapping.tsx new file mode 100644 index 000000000..8cc3b305a --- /dev/null +++ b/frontend/src/components/feature/hr/CompanyWorkspaceMapping.tsx @@ -0,0 +1,123 @@ +import { Box, Button, IconButton, Select, Text, VisuallyHidden } from '@radix-ui/themes' +import { RavenSettings } from '@/types/Raven/RavenSettings' +import { Controller, useFieldArray, useFormContext } from 'react-hook-form' +import { __ } from '@/utils/translations' +import LinkFormField from '@/components/common/LinkField/LinkFormField' +import { ErrorText, Label } from '@/components/common/Form' +import { HStack, Stack } from '@/components/layout/Stack' +import useFetchWorkspaces from '@/hooks/fetchers/useFetchWorkspaces' +import { BiTrashAlt } from 'react-icons/bi' + +type Props = {} + +const CompanyWorkspaceMapping = (props: Props) => { + + const { control, formState: { errors, disabled } } = useFormContext() + + const { fields, append, remove } = useFieldArray({ + control, + name: 'company_workspace_mapping' + }) + + // @ts-ignore + const addRow = () => append({ company: '', raven_workspace: '' }) + + return ( + + + {__("Choose workspaces based on companies")} + + + + + + {__("Company")} + + + {__("Workspace")} + + + {__("Actions")} + + + + {fields.map((field, index) => ( + + + + + {errors.company_workspace_mapping?.[index]?.company?.message} + + + + + + + + + + {errors.company_workspace_mapping?.[index]?.raven_workspace?.message} + + + + + remove(index)}> + + + + + + ))} + + + ) +} + +const WorkspaceDropdown = ({ name }: { name: `company_workspace_mapping.${number}.raven_workspace` }) => { + + const { control } = useFormContext() + + const { data: workspaces } = useFetchWorkspaces() + + return ( + + + + {workspaces?.message.map((workspace) => ( + + {workspace.name} + + ))} + + + )} + /> + +} + +export default CompanyWorkspaceMapping \ No newline at end of file diff --git a/frontend/src/components/feature/integrations/meetings/CreateMeetingForm.tsx b/frontend/src/components/feature/integrations/meetings/CreateMeetingForm.tsx index 9cf23002e..9f41e9ebe 100644 --- a/frontend/src/components/feature/integrations/meetings/CreateMeetingForm.tsx +++ b/frontend/src/components/feature/integrations/meetings/CreateMeetingForm.tsx @@ -168,7 +168,7 @@ const CreateMeetingForm = ({ onClose, channelData }: CreateMeetingFormProps) => diff --git a/frontend/src/components/feature/integrations/webhooks/WebhookForm.tsx b/frontend/src/components/feature/integrations/webhooks/WebhookForm.tsx index 4c9db36d3..2bdad1145 100644 --- a/frontend/src/components/feature/integrations/webhooks/WebhookForm.tsx +++ b/frontend/src/components/feature/integrations/webhooks/WebhookForm.tsx @@ -12,9 +12,9 @@ import { UserAvatar } from '@/components/common/UserAvatar'; import { SidebarIcon } from '@/components/layout/Sidebar/SidebarComp'; import { useGetUser } from '@/hooks/useGetUser'; import { ChannelIcon } from '@/utils/layout/channelIcon'; -import { Stack } from '@/components/layout/Stack'; +import { HStack, Stack } from '@/components/layout/Stack'; import { AiOutlineApi, AiOutlineDatabase } from 'react-icons/ai'; -import { BiCodeCurly } from 'react-icons/bi'; +import { BiBuildings, BiCodeCurly } from 'react-icons/bi'; import { LuWorkflow } from 'react-icons/lu'; const ICON_PROPS = { @@ -392,10 +392,16 @@ export const DirectMessageItem = ({ user }: { user: UserFields }) => { } export const ChannelItem = ({ channel }: { channel: ChannelListItem }) => { - return - - - {channel.channel_name} + return + + + + {channel.channel_name} + - + + + {channel.workspace} + + } diff --git a/frontend/src/components/feature/integrations/webhooks/WebhookItem.tsx b/frontend/src/components/feature/integrations/webhooks/WebhookItem.tsx index ff53b48f4..1b32484e4 100644 --- a/frontend/src/components/feature/integrations/webhooks/WebhookItem.tsx +++ b/frontend/src/components/feature/integrations/webhooks/WebhookItem.tsx @@ -104,7 +104,7 @@ const DeleteWebhookAlertContent = ({ webhhookID, onClose, mutate }: { webhhookID diff --git a/frontend/src/components/feature/message-actions/MessageActionModal.tsx b/frontend/src/components/feature/message-actions/MessageActionModal.tsx index ee969fa71..293c4e700 100644 --- a/frontend/src/components/feature/message-actions/MessageActionModal.tsx +++ b/frontend/src/components/feature/message-actions/MessageActionModal.tsx @@ -99,7 +99,7 @@ const MessageActionForm = ({ action, messageID, defaultValues }: { action: Raven diff --git a/frontend/src/components/feature/saved-messages/SavedMessages.tsx b/frontend/src/components/feature/saved-messages/SavedMessages.tsx index 96b7cb6e2..f3c344780 100644 --- a/frontend/src/components/feature/saved-messages/SavedMessages.tsx +++ b/frontend/src/components/feature/saved-messages/SavedMessages.tsx @@ -1,5 +1,5 @@ import { useFrappeGetCall } from "frappe-react-sdk" -import { Link, useNavigate } from "react-router-dom" +import { Link, useNavigate, useParams } from "react-router-dom" import { Message } from "../../../../../types/Messaging/Message" import { ErrorBanner } from "@/components/layout/AlertBanner/ErrorBanner" import { EmptyStateForSavedMessages } from "@/components/layout/EmptyState/EmptyState" @@ -13,20 +13,28 @@ const SavedMessages = () => { const navigate = useNavigate() - const { data, error } = useFrappeGetCall<{ message: Message[] }>("raven.api.raven_message.get_saved_messages", undefined, undefined, { + const { data, error } = useFrappeGetCall<{ message: (Message & { workspace?: string })[] }>("raven.api.raven_message.get_saved_messages", undefined, undefined, { revalidateOnFocus: false }) - const handleNavigateToChannel = (channelID: string, baseMessage?: string) => { - navigate(`/channel/${channelID}`, { + const { workspaceID } = useParams() + + const handleNavigateToChannel = (channelID: string, workspace?: string, baseMessage?: string) => { + let baseRoute = '' + if (workspace) { + baseRoute = `/${workspace}` + } else { + baseRoute = `/${workspaceID}` + } + navigate(`${baseRoute}/${channelID}`, { state: { baseMessage: baseMessage } }) } - const handleScrollToMessage = (messageName: string, channelID: string) => { - handleNavigateToChannel(channelID, messageName) + const handleScrollToMessage = (messageName: string, channelID: string, workspace?: string) => { + handleNavigateToChannel(channelID, workspace, messageName) } return ( diff --git a/frontend/src/components/feature/selectDropdowns/AddMembersDropdown.tsx b/frontend/src/components/feature/selectDropdowns/AddMembersDropdown.tsx index 149b0440d..27bd547f2 100644 --- a/frontend/src/components/feature/selectDropdowns/AddMembersDropdown.tsx +++ b/frontend/src/components/feature/selectDropdowns/AddMembersDropdown.tsx @@ -1,12 +1,7 @@ -import { useContext, useMemo, useState } from 'react' +import { useCallback, useContext, useMemo } from 'react' import { UserFields, UserListContext } from '@/utils/users/UserListProvider' -import { Text, TextField } from '@radix-ui/themes' -import { UserAvatar } from '@/components/common/UserAvatar' -import { useMultipleSelection, useCombobox } from 'downshift' -import { clsx } from 'clsx' -import { Label } from '@/components/common/Form' +import MultipleUserComboBox from './MultipleUserCombobox' import useFetchChannelMembers from '@/hooks/fetchers/useFetchChannelMembers' -import { useIsDesktop } from '@/hooks/useMediaQuery' interface AddMembersDropdownProps { channelID: string, @@ -23,10 +18,10 @@ const AddMembersDropdown = ({ channelID, label = 'Select users', selectedUsers, const users = useContext(UserListContext) //Options for dropdown - const nonChannelMembers = users.enabledUsers?.filter((m: UserFields) => !channelMembers?.[m.name]) ?? [] + const nonChannelMembers = useMemo(() => users.enabledUsers?.filter((m: UserFields) => !channelMembers?.[m.name]) ?? [], [users.enabledUsers, channelMembers]) /** Function to filter users */ - function getFilteredUsers(selectedUsers: UserFields[], inputValue: string) { + const getFilteredUsers = useCallback((selectedUsers: UserFields[], inputValue: string) => { const lowerCasedInputValue = inputValue.toLowerCase() return nonChannelMembers.filter((user: UserFields) => { @@ -37,172 +32,9 @@ const AddMembersDropdown = ({ channelID, label = 'Select users', selectedUsers, user.name.toLowerCase().includes(lowerCasedInputValue)) ) }) - } + }, [nonChannelMembers]) - function MultipleComboBox({ selectedUsers, setSelectedUsers }: { selectedUsers: UserFields[], setSelectedUsers: (users: UserFields[]) => void }) { - const [inputValue, setInputValue] = useState('') - - const items = useMemo(() => getFilteredUsers(selectedUsers, inputValue), [selectedUsers, inputValue]) - - const { getSelectedItemProps, getDropdownProps, removeSelectedItem } = useMultipleSelection({ - selectedItems: selectedUsers, - onStateChange({ selectedItems: newSelectedItems, type }) { - switch (type) { - case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace: - case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete: - case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace: - case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem: - setSelectedUsers(newSelectedItems ?? []) - break - default: - break - } - } - }) - const { - isOpen, - getLabelProps, - getMenuProps, - getInputProps, - highlightedIndex, - getItemProps, - selectedItem, - } = useCombobox({ - items, - itemToString(item) { - return item ? item.name : '' - }, - defaultHighlightedIndex: 0, // after selection, highlight the first item. - selectedItem: null, - inputValue, - stateReducer(state, actionAndChanges) { - const { changes, type } = actionAndChanges - - switch (type) { - case useCombobox.stateChangeTypes.InputKeyDownEnter: - case useCombobox.stateChangeTypes.ItemClick: - return { - ...changes, - isOpen: true, // keep the menu open after selection. - highlightedIndex: 0, // with the first option highlighted. - } - default: - return changes - } - }, - onStateChange({ - inputValue: newInputValue, - type, - selectedItem: newSelectedItem, - }) { - switch (type) { - case useCombobox.stateChangeTypes.InputKeyDownEnter: - case useCombobox.stateChangeTypes.ItemClick: - case useCombobox.stateChangeTypes.InputBlur: - if (newSelectedItem) { - setSelectedUsers([...selectedUsers, newSelectedItem]) - setInputValue('') - } - break - - case useCombobox.stateChangeTypes.InputChange: - setInputValue(newInputValue ?? '') - - break - default: - break - } - }, - }) - - const isDesktop = useIsDesktop() - - return ( -
-
- - - - - -
- {selectedUsers.map(function renderSelectedItem( - selectedItemForRender, - index, - ) { - return ( - - - - {selectedItemForRender.full_name} - - - { - e.stopPropagation() - removeSelectedItem(selectedItemForRender) - }} - > - ✕ - - - ) - })} -
-
-
    - {isOpen && - items.map((item, index) => ( -
  • - -
    - {item.full_name} - {item.name} -
    - -
  • - ))} -
-
- ) - } - - return + return } diff --git a/frontend/src/components/feature/selectDropdowns/MultipleUserCombobox.tsx b/frontend/src/components/feature/selectDropdowns/MultipleUserCombobox.tsx new file mode 100644 index 000000000..0142fd5be --- /dev/null +++ b/frontend/src/components/feature/selectDropdowns/MultipleUserCombobox.tsx @@ -0,0 +1,188 @@ +import { Label } from "@/components/common/Form" +import { UserAvatar } from "@/components/common/UserAvatar" +import { HStack, Stack } from "@/components/layout/Stack" +import { useIsDesktop } from "@/hooks/useMediaQuery" +import { UserFields } from "@/utils/users/UserListProvider" +import { ScrollArea, Text, TextField } from "@radix-ui/themes" +import clsx from "clsx" +import { useCombobox, useMultipleSelection } from "downshift" +import { useMemo, useState } from "react" +import { FiX } from "react-icons/fi" + +function MultipleUserComboBox({ selectedUsers, setSelectedUsers, getFilteredUsers, label }: { selectedUsers: UserFields[], setSelectedUsers: (users: UserFields[]) => void, getFilteredUsers: (selectedUsers: UserFields[], inputValue: string) => UserFields[], label: string }) { + const [inputValue, setInputValue] = useState('') + + const items = useMemo(() => getFilteredUsers(selectedUsers, inputValue), [selectedUsers, inputValue]) + + const { getSelectedItemProps, getDropdownProps, removeSelectedItem } = useMultipleSelection({ + selectedItems: selectedUsers, + onStateChange({ selectedItems: newSelectedItems, type }) { + switch (type) { + case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace: + case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete: + case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace: + case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem: + setSelectedUsers(newSelectedItems ?? []) + break + default: + break + } + } + }) + const { + isOpen, + getLabelProps, + getMenuProps, + getInputProps, + highlightedIndex, + getItemProps, + selectedItem, + } = useCombobox({ + items, + itemToString(item) { + return item ? item.name : '' + }, + // defaultHighlightedIndex: 0, // after selection, highlight the first item. + // selectedItem: null, + inputValue, + stateReducer(state, actionAndChanges) { + const { changes, type } = actionAndChanges + + switch (type) { + case useCombobox.stateChangeTypes.InputKeyDownEnter: + case useCombobox.stateChangeTypes.ItemClick: + return { + ...changes, + // isOpen: true, // keep the menu open after selection. + // highlightedIndex: 0, // with the first option highlighted. + } + default: + return changes + } + }, + onStateChange({ + inputValue: newInputValue, + type, + selectedItem: newSelectedItem, + }) { + switch (type) { + case useCombobox.stateChangeTypes.InputKeyDownEnter: + case useCombobox.stateChangeTypes.ItemClick: + case useCombobox.stateChangeTypes.InputBlur: + if (newSelectedItem) { + setSelectedUsers([...selectedUsers, newSelectedItem]) + setInputValue('') + } + break + + case useCombobox.stateChangeTypes.InputChange: + setInputValue(newInputValue ?? '') + + break + default: + break + } + }, + }) + + const isDesktop = useIsDesktop() + + return ( +
+
+ + + + +
+
    + {isOpen && + items.map((item, index) => ( +
  • + +
    + {item.full_name} + {item.name} +
    + +
  • + ))} +
+ + + {selectedUsers.map(function renderSelectedItem( + selectedItemForRender, + index, + ) { + return ( + + + + + + {selectedItemForRender.full_name} + + + {selectedItemForRender.name} + + + + + + + { + e.stopPropagation() + removeSelectedItem(selectedItemForRender) + }} + > + + + + ) + })} + +
+ ) +} + +export default MultipleUserComboBox \ No newline at end of file diff --git a/frontend/src/components/feature/selectDropdowns/UsersOrChannelsDropdown.tsx b/frontend/src/components/feature/selectDropdowns/UsersOrChannelsDropdown.tsx index 1ee27ea4d..c0fc6fcb1 100644 --- a/frontend/src/components/feature/selectDropdowns/UsersOrChannelsDropdown.tsx +++ b/frontend/src/components/feature/selectDropdowns/UsersOrChannelsDropdown.tsx @@ -1,5 +1,6 @@ import { Label } from "@/components/common/Form" import { UserAvatar } from "@/components/common/UserAvatar" +import { HStack } from "@/components/layout/Stack" import { useIsDesktop } from "@/hooks/useMediaQuery" import { ChannelListContext, ChannelListContextType, ChannelListItem } from "@/utils/channel/ChannelListProvider" import { ChannelIcon } from "@/utils/layout/channelIcon" @@ -8,6 +9,7 @@ import { TextField, Text } from "@radix-ui/themes" import clsx from "clsx" import { useMultipleSelection, useCombobox } from "downshift" import { useContext, useMemo, useState } from "react" +import { BiBuildings } from "react-icons/bi" interface UsersOrChannelsDropdownProps { label?: string, @@ -214,10 +216,16 @@ const UsersOrChannelsDropdown = ({ selectedOptions, setSelectedOptions, label = )} key={`${item.name}`} {...getItemProps({ item, index })}> {'channel_name' in item ? - <> - - {item.channel_name} - + + + + {item.channel_name} + + + + {item.workspace} + + : <> diff --git a/frontend/src/components/feature/settings/Integrations.tsx b/frontend/src/components/feature/settings/Integrations.tsx index 09b6f4cd9..8b26b6657 100644 --- a/frontend/src/components/feature/settings/Integrations.tsx +++ b/frontend/src/components/feature/settings/Integrations.tsx @@ -1,4 +1,4 @@ -import { SidebarGroup, SidebarGroupItem, SidebarGroupLabel, SidebarGroupList, SidebarItem } from "@/components/layout/Sidebar" +import { SidebarGroup, SidebarGroupItem, SidebarGroupLabel, SidebarGroupList, SidebarItem } from "@/components/layout/Sidebar/SidebarComp" import { hasServerScriptEnabled, isSystemManager } from "@/utils/roles" import { Flex, Text } from "@radix-ui/themes" import { BiPlug } from "react-icons/bi" diff --git a/frontend/src/components/feature/settings/ai/AINotEnabledCallout.tsx b/frontend/src/components/feature/settings/ai/AINotEnabledCallout.tsx index 0cbfb29f2..ef8382f5a 100644 --- a/frontend/src/components/feature/settings/ai/AINotEnabledCallout.tsx +++ b/frontend/src/components/feature/settings/ai/AINotEnabledCallout.tsx @@ -16,7 +16,7 @@ const AINotEnabledCallout = () => { } rootProps={{ color: 'blue', variant: 'surface' }} - textChildren={Raven AI is not enabled. Please enable it in OpenAI Settings} + textChildren={Raven AI is not enabled. Please enable it in OpenAI Settings} /> ) } diff --git a/frontend/src/components/feature/settings/ai/bots/BotDocs.tsx b/frontend/src/components/feature/settings/ai/bots/BotDocs.tsx index ec04f9586..1d0d67195 100644 --- a/frontend/src/components/feature/settings/ai/bots/BotDocs.tsx +++ b/frontend/src/components/feature/settings/ai/bots/BotDocs.tsx @@ -56,7 +56,7 @@ ${botVarName}.send_message(channel_id="channel-name", text="This is a test messa Sending a message to a channel - Bots can be used to send messages to channels with html formatted content. + Bots can be used to send messages to channels with HTML formatted content. { General {isAiBot ? AI : null} {isAiBot ? Instructions : null} - {isAiBot ? Functions : null} + {isAiBot ? Functions : null} {isEdit ? API Docs : null} diff --git a/frontend/src/components/feature/settings/ai/bots/BotFunctionsForm.tsx b/frontend/src/components/feature/settings/ai/bots/BotFunctionsForm.tsx index 735663b55..490a14c2b 100644 --- a/frontend/src/components/feature/settings/ai/bots/BotFunctionsForm.tsx +++ b/frontend/src/components/feature/settings/ai/bots/BotFunctionsForm.tsx @@ -47,7 +47,7 @@ const BotFunctionsForm = (props: Props) => { Add functions that the bot can use to create or update documents in the system.
- Create functions in the function builder and then add them here. + Create functions in the function builder and then add them here.
@@ -83,7 +83,7 @@ const BotFunctionsForm = (props: Props) => { - {field.function} + {field.function} {field.type} diff --git a/frontend/src/components/feature/settings/ai/functions/FunctionForm.tsx b/frontend/src/components/feature/settings/ai/functions/FunctionForm.tsx index fc7d75d88..643f07fb4 100644 --- a/frontend/src/components/feature/settings/ai/functions/FunctionForm.tsx +++ b/frontend/src/components/feature/settings/ai/functions/FunctionForm.tsx @@ -9,7 +9,7 @@ import VariableBuilder from './VariableBuilder' import LinkFormField from '@/components/common/LinkField/LinkFormField' import AINotEnabledCallout from '../AINotEnabledCallout' import DoctypeVariableBuilder from './DoctypeVariableBuilder' -import { LuFunctionSquare, LuVariable } from 'react-icons/lu' +import { LuSquareFunction, LuVariable } from 'react-icons/lu' import { in_list } from '@/utils/validations' const ICON_PROPS = { @@ -25,10 +25,10 @@ const FunctionForm = ({ isEdit }: { isEdit?: boolean }) => { return ( - Details + Details Variables - + @@ -36,7 +36,7 @@ const FunctionForm = ({ isEdit }: { isEdit?: boolean }) => { - + ) diff --git a/frontend/src/components/feature/settings/common/DeleteAlert.tsx b/frontend/src/components/feature/settings/common/DeleteAlert.tsx index 2314f589e..f96b2752b 100644 --- a/frontend/src/components/feature/settings/common/DeleteAlert.tsx +++ b/frontend/src/components/feature/settings/common/DeleteAlert.tsx @@ -92,7 +92,7 @@ export const AlertContent = ({ onClose, onUpdate, doctype, docname, path }: Dele diff --git a/frontend/src/components/feature/settings/scheduler-events/ServerScriptNotEnabledForm.tsx b/frontend/src/components/feature/settings/scheduler-events/ServerScriptNotEnabledForm.tsx index 9852baeb8..d5009b703 100644 --- a/frontend/src/components/feature/settings/scheduler-events/ServerScriptNotEnabledForm.tsx +++ b/frontend/src/components/feature/settings/scheduler-events/ServerScriptNotEnabledForm.tsx @@ -18,7 +18,7 @@ const ServerScriptNotEnabledCallout = () => { return ( } - rootProps={{ color: 'yellow' }} + rootProps={{ color: 'yellow', variant: 'surface' }} textChildren={Server scripts are not enabled on this site. Please view the Frappe documentation for more information.} /> ) diff --git a/frontend/src/components/feature/threads/ThreadDrawer/ThreadMessages.tsx b/frontend/src/components/feature/threads/ThreadDrawer/ThreadMessages.tsx index 4f9687b95..7b2daab98 100644 --- a/frontend/src/components/feature/threads/ThreadDrawer/ThreadMessages.tsx +++ b/frontend/src/components/feature/threads/ThreadDrawer/ThreadMessages.tsx @@ -101,7 +101,6 @@ export const ThreadMessages = ({ threadMessage }: { threadMessage: Message }) => {!isUserInChannel && } {isUserInChannel && diff --git a/frontend/src/components/feature/threads/ThreadPreviewBox.tsx b/frontend/src/components/feature/threads/ThreadPreviewBox.tsx index e160864a3..b6586698b 100644 --- a/frontend/src/components/feature/threads/ThreadPreviewBox.tsx +++ b/frontend/src/components/feature/threads/ThreadPreviewBox.tsx @@ -4,7 +4,7 @@ import { MessageContent, MessageSenderAvatar, UserHoverCard } from '../chat/Chat import { useGetUser } from '@/hooks/useGetUser' import { useCurrentChannelData } from '@/hooks/useCurrentChannelData' import { ChannelIcon } from '@/utils/layout/channelIcon' -import { Link } from 'react-router-dom' +import { Link, useParams } from 'react-router-dom' import { ThreadMessage } from './Threads' import { Message } from '../../../../../types/Messaging/Message' import { ViewThreadParticipants } from './ThreadParticipants' @@ -36,6 +36,10 @@ export const ThreadPreviewBox = ({ thread }: { thread: ThreadMessage }) => { } }, [channelData, users]) + const { workspaceID } = useParams() + + const workspace = thread.workspace ? thread.workspace : workspaceID + return ( - View Thread + View Thread
diff --git a/frontend/src/components/feature/threads/Threads.tsx b/frontend/src/components/feature/threads/Threads.tsx index 13907bb1e..ddd3c9623 100644 --- a/frontend/src/components/feature/threads/Threads.tsx +++ b/frontend/src/components/feature/threads/Threads.tsx @@ -25,6 +25,7 @@ export type ThreadMessage = { text: string, thread_message_id: string, participants: { user_id: string }[], + workspace?: string } const Threads = () => { diff --git a/frontend/src/components/feature/threads/ThreadsList.tsx b/frontend/src/components/feature/threads/ThreadsList.tsx index f30d85af9..77ce350bc 100644 --- a/frontend/src/components/feature/threads/ThreadsList.tsx +++ b/frontend/src/components/feature/threads/ThreadsList.tsx @@ -4,6 +4,7 @@ import { useFrappeGetCall } from 'frappe-react-sdk' import { Flex } from '@radix-ui/themes' import { ErrorBanner } from '@/components/layout/AlertBanner/ErrorBanner' import { EmptyStateForThreads } from '@/components/layout/EmptyState/EmptyState' +import { useParams } from 'react-router-dom' type Props = { aiThreads?: 0 | 1 @@ -11,8 +12,11 @@ type Props = { const ThreadsList = ({ aiThreads }: Props) => { + const { workspaceID } = useParams() + const { data, error } = useFrappeGetCall<{ message: ThreadMessage[] }>("raven.api.threads.get_all_threads", { - is_ai_thread: aiThreads + is_ai_thread: aiThreads, + workspace: workspaceID }, undefined, { revalidateOnFocus: false }) diff --git a/frontend/src/components/feature/userSettings/AvailabilityStatus/SetUserAvailabilityMenu.tsx b/frontend/src/components/feature/userSettings/AvailabilityStatus/SetUserAvailabilityMenu.tsx index c4288dbcd..869d2d868 100644 --- a/frontend/src/components/feature/userSettings/AvailabilityStatus/SetUserAvailabilityMenu.tsx +++ b/frontend/src/components/feature/userSettings/AvailabilityStatus/SetUserAvailabilityMenu.tsx @@ -3,32 +3,33 @@ import { toast } from 'sonner' import { MdWatchLater } from 'react-icons/md' import { FaCircleDot, FaCircleMinus } from 'react-icons/fa6' import { BiSolidCircle } from 'react-icons/bi' -import { useCallback } from 'react' import { DropdownMenu, Flex } from '@radix-ui/themes' -import { useUserData } from '@/hooks/useUserData' import { GrPowerReset } from 'react-icons/gr' import useCurrentRavenUser from '@/hooks/useCurrentRavenUser' import { __ } from '@/utils/translations' +import { getErrorMessage } from '@/components/layout/AlertBanner/ErrorBanner' export type AvailabilityStatus = 'Available' | 'Away' | 'Do not disturb' | 'Invisible' | '' export const SetUserAvailabilityMenu = () => { - - const userData = useUserData() const { myProfile, mutate } = useCurrentRavenUser() - const { call } = useFrappePostCall('frappe.client.set_value') - const setAvailabilityStatus = useCallback((status: AvailabilityStatus) => { + const { call } = useFrappePostCall('raven.api.raven_users.update_raven_user') + const setAvailabilityStatus = (status: AvailabilityStatus) => { call({ - doctype: 'Raven User', - name: userData.name, - fieldname: 'availability_status', - value: status + 'availability_status': status }).then(() => { - toast.success(__("User availability updated")) + toast.success(__("Updated!"), { + duration: 600 + }) mutate() + }).catch((error) => { + toast.error(error.message, { + description: getErrorMessage(error) + }) + console.error(error) }) - }, [userData.name]) + } return ( diff --git a/frontend/src/components/feature/userSettings/CustomStatus/SetCustomStatusContent.tsx b/frontend/src/components/feature/userSettings/CustomStatus/SetCustomStatusContent.tsx index c60560ed1..fd1d464fb 100644 --- a/frontend/src/components/feature/userSettings/CustomStatus/SetCustomStatusContent.tsx +++ b/frontend/src/components/feature/userSettings/CustomStatus/SetCustomStatusContent.tsx @@ -7,7 +7,7 @@ import { useUserData } from '@/hooks/useUserData' import { __ } from '@/utils/translations' import { Button, Dialog, Flex, TextField, IconButton } from '@radix-ui/themes' import { useFrappePostCall } from 'frappe-react-sdk' -import { useCallback, useState } from 'react' +import { useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { BiSmile } from 'react-icons/bi' import { toast } from 'sonner' @@ -24,19 +24,16 @@ const SetCustomStatusContent = ({ onClose }: { onClose: VoidFunction }) => { }) const { register, handleSubmit, formState: { errors } } = methods - const { call, error, loading } = useFrappePostCall('frappe.client.set_value') - const onSubmit = useCallback((data: { custom_status: string }) => { + const { call, loading, error } = useFrappePostCall('raven.api.raven_users.update_raven_user') + const onSubmit = (data: { custom_status: string }) => { call({ - doctype: 'Raven User', - name: userData.name, - fieldname: 'custom_status', - value: data.custom_status + custom_status: data.custom_status }).then(() => { toast.success(__("User status updated")) mutate() onClose() }) - }, [userData.name]) + } const onEmojiSelect = (emoji: string) => { methods.setValue('custom_status', `${methods.getValues('custom_status')} ${emoji}`) @@ -88,7 +85,7 @@ const SetCustomStatusContent = ({ onClose }: { onClose: VoidFunction }) => { diff --git a/frontend/src/components/feature/userSettings/SettingsSidebar.tsx b/frontend/src/components/feature/userSettings/SettingsSidebar.tsx index 254d2f2f4..f866b53ba 100644 --- a/frontend/src/components/feature/userSettings/SettingsSidebar.tsx +++ b/frontend/src/components/feature/userSettings/SettingsSidebar.tsx @@ -5,22 +5,22 @@ import { PropsWithChildren, createElement } from 'react' import { IconType } from 'react-icons' import { BiBot, BiBuildings } from 'react-icons/bi' import { BsBoxes } from 'react-icons/bs' -import { LuUserCircle2 } from 'react-icons/lu' +import { LuCircleUserRound } from 'react-icons/lu' import { NavLink } from 'react-router-dom' export const SettingsSidebar = () => { return ( - + - + + - {/* */} @@ -37,8 +37,8 @@ export const SettingsSidebar = () => { - + diff --git a/frontend/src/components/feature/userSettings/UploadImage/DeleteImageModal.tsx b/frontend/src/components/feature/userSettings/UploadImage/DeleteImageModal.tsx index bc29c44c5..85c32181e 100644 --- a/frontend/src/components/feature/userSettings/UploadImage/DeleteImageModal.tsx +++ b/frontend/src/components/feature/userSettings/UploadImage/DeleteImageModal.tsx @@ -12,17 +12,14 @@ interface DeleteImageModalProps { export const DeleteImageModal = ({ onClose }: DeleteImageModalProps) => { - const { call, error, loading } = useFrappePostCall('frappe.client.set_value') - const { myProfile, mutate } = useCurrentRavenUser() + const { call, loading, error } = useFrappePostCall('raven.api.raven_users.update_raven_user') + const { mutate } = useCurrentRavenUser() const removeImage = () => { call({ - doctype: 'Raven User', - name: myProfile?.name, - fieldname: 'user_image', - value: '' + user_image: '' }).then(() => { - toast.success("User status updated") + toast.success("Profile picture removed.") mutate() onClose() }) diff --git a/frontend/src/components/feature/userSettings/UploadImage/FileUploadBox.tsx b/frontend/src/components/feature/userSettings/UploadImage/FileUploadBox.tsx index a488fc442..dca06a05a 100644 --- a/frontend/src/components/feature/userSettings/UploadImage/FileUploadBox.tsx +++ b/frontend/src/components/feature/userSettings/UploadImage/FileUploadBox.tsx @@ -88,7 +88,7 @@ export const FileUploadBox = forwardRef((props: FileUploadBoxProps, ref) => { {__("Drag and drop your file here or")} - diff --git a/frontend/src/components/feature/userSettings/UploadImage/ImageUploader.tsx b/frontend/src/components/feature/userSettings/UploadImage/ImageUploader.tsx index 35ac527ca..cdb226c0a 100644 --- a/frontend/src/components/feature/userSettings/UploadImage/ImageUploader.tsx +++ b/frontend/src/components/feature/userSettings/UploadImage/ImageUploader.tsx @@ -11,6 +11,7 @@ import { BiSolidTrash } from "react-icons/bi" import { UserAvatar, getInitials } from "@/components/common/UserAvatar" import useCurrentRavenUser from "@/hooks/useCurrentRavenUser" import { __ } from "@/utils/translations" +import { getErrorMessage } from "@/components/layout/AlertBanner/ErrorBanner" interface ImageUploaderProps { /** Takes input MIME type as 'key' & array of extensions as 'value'; empty array - all extensions supported */ @@ -22,45 +23,43 @@ interface ImageUploaderProps { export const ImageUploader = ({ icon, accept = { 'image/*': ['.jpeg', '.jpg', '.png'] }, maxFileSize, ...props }: ImageUploaderProps) => { - const { call, error } = useFrappePostCall('frappe.client.set_value') + const { call } = useFrappePostCall('raven.api.raven_users.update_raven_user') const { myProfile, mutate } = useCurrentRavenUser() + const [isUploadImageModalOpen, setUploadImageModalOpen] = useState(false) + const [isDeleteImageModalOpen, setDeleteImageModalOpen] = useState(false) + const uploadImage = (file: string) => { if (file) { call({ - doctype: 'Raven User', - name: myProfile?.name, - fieldname: 'user_image', - value: file + user_image: file }).then(() => { toast(__("Image uploaded successfully.")) mutate() - }).catch(() => { - toast(`There was an error while uploading the image. ${error ? error.exception ?? error.httpStatusText : null}`) + setUploadImageModalOpen(false) + }).catch((error) => { + toast(`There was an error while uploading the image.`, { + description: getErrorMessage(error) + }) }) } } - const [isUploadImageModalOpen, setUploadImageModalOpen] = useState(false) - const [isDeleteImageModalOpen, setDeleteImageModalOpen] = useState(false) + return ( {myProfile?.user_image ? {'User : } - + {myProfile?.user_image && } ) } -export const UploadImage = ({ open, setOpen, uploadImage }: { open: boolean, setOpen: (open: boolean) => void, uploadImage: (file: string) => void }) => { - - const onClose = () => { - setOpen(false) - } +export const UploadImage = ({ open, setOpen, uploadImage, userID }: { open: boolean, setOpen: (open: boolean) => void, uploadImage: (file: string) => void, userID: string }) => { return ( @@ -73,7 +72,7 @@ export const UploadImage = ({ open, setOpen, uploadImage }: { open: boolean, set
- +
) diff --git a/frontend/src/components/feature/userSettings/UploadImage/UploadImageModal.tsx b/frontend/src/components/feature/userSettings/UploadImage/UploadImageModal.tsx index 0488b60f2..1b2a223ee 100644 --- a/frontend/src/components/feature/userSettings/UploadImage/UploadImageModal.tsx +++ b/frontend/src/components/feature/userSettings/UploadImage/UploadImageModal.tsx @@ -9,11 +9,14 @@ import { FileUploadBox } from "./FileUploadBox" import { __ } from "@/utils/translations" interface UploadImageModalProps { - onClose: () => void, - uploadImage: (file: string) => void + uploadImage: (file: string) => void, + label?: string, + doctype: string, + docname: string, + fieldname: string, } -export const UploadImageModal = ({ onClose, uploadImage }: UploadImageModalProps) => { +export const UploadImageModal = ({ uploadImage, label = 'Upload Image', doctype, docname, fieldname }: UploadImageModalProps) => { const [file, setFile] = useState() const [fileError, setFileError] = useState() @@ -29,13 +32,12 @@ export const UploadImageModal = ({ onClose, uploadImage }: UploadImageModalProps const uploadFiles = async () => { if (file) { return upload(file, { - doctype: 'Raven User', - docname: userData.name, - fieldname: 'user_image', + doctype: doctype, + docname: docname, + fieldname: fieldname, isPrivate: true, }).then((res) => { uploadImage(res.file_url) - onClose() }).catch((e) => { setFileError(e) }) @@ -44,7 +46,7 @@ export const UploadImageModal = ({ onClose, uploadImage }: UploadImageModalProps return ( <> - {__("Upload file")} + {label} @@ -61,7 +63,7 @@ export const UploadImageModal = ({ onClose, uploadImage }: UploadImageModalProps diff --git a/frontend/src/components/feature/userSettings/Users/AddUserDialog.tsx b/frontend/src/components/feature/userSettings/Users/AddUserDialog.tsx index 82de4e6c8..e8f9e71e5 100644 --- a/frontend/src/components/feature/userSettings/Users/AddUserDialog.tsx +++ b/frontend/src/components/feature/userSettings/Users/AddUserDialog.tsx @@ -158,7 +158,7 @@ const UserForm = ({ onClose }: { onClose: VoidFunction }) => { diff --git a/frontend/src/components/feature/workspaces/AddWorkspaceForm.tsx b/frontend/src/components/feature/workspaces/AddWorkspaceForm.tsx new file mode 100644 index 000000000..89d559f72 --- /dev/null +++ b/frontend/src/components/feature/workspaces/AddWorkspaceForm.tsx @@ -0,0 +1,167 @@ +import { ErrorText, HelperText, Label } from '@/components/common/Form' +import { Loader } from '@/components/common/Loader' +import { ErrorBanner } from '@/components/layout/AlertBanner/ErrorBanner' +import { Stack } from '@/components/layout/Stack' +import { useIsDesktop } from '@/hooks/useMediaQuery' +import { RavenWorkspace } from '@/types/Raven/RavenWorkspace' +import { __ } from '@/utils/translations' +import { Box, Button, Dialog, Flex, RadioGroup, Text, TextArea, TextField } from '@radix-ui/themes' +import { useFrappeCreateDoc, useFrappeFileUpload, useFrappeUpdateDoc, useSWRConfig } from 'frappe-react-sdk' +import { useState } from 'react' +import { Controller, FormProvider, useForm } from 'react-hook-form' +import { toast } from 'sonner' +import { FileUploadBox } from '../userSettings/UploadImage/FileUploadBox' +import { CustomFile } from '../file-upload/FileDrop' + +const AddWorkspaceForm = ({ onClose }: { onClose: (workspaceID?: string) => void }) => { + + const { mutate } = useSWRConfig() + + const methods = useForm({ + defaultValues: { + type: "Public" + } + }) + + const [image, setImage] = useState(undefined) + + const { register, handleSubmit, control, formState: { errors } } = methods + + const { createDoc, loading: creatingDoc, error } = useFrappeCreateDoc() + const { updateDoc, loading: updatingDoc } = useFrappeUpdateDoc() + + const { upload, loading: uploadingFile, error: fileError } = useFrappeFileUpload() + + const onSubmit = (data: RavenWorkspace) => { + + createDoc("Raven Workspace", data) + .then(res => { + if (image) { + return upload(image, { + doctype: 'Raven Workspace', + docname: res.name, + fieldname: 'logo', + isPrivate: true, + }).then((fileRes) => { + return updateDoc("Raven Workspace", res.name, { + logo: fileRes.file_url + }) + }) + } + return res + }) + .then((res) => { + // Mutate the workspace list, channel list + mutate("workspaces_list") + mutate("channel_list") + toast.success("Workspace created", { + description: `You can now invite members to ${res.workspace_name}`, + duration: 2000 + }) + onClose(res.name) + }) + } + + const isDesktop = useIsDesktop() + + const loading = creatingDoc || uploadingFile || updatingDoc + + return ( + + + + + + + + + + + + {errors.workspace_name && {errors.workspace_name?.message}} + + + + + +