Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: attach docs from Raven #1088

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions frontend/src/components/common/LinkField/LinkField.tsx
Original file line number Diff line number Diff line change
@@ -19,18 +19,19 @@ export interface LinkFieldProps {
autofocus?: boolean,
dropdownClass?: string,
required?: boolean,
suggestedItems?: SearchResult[],
}


const LinkField = ({ doctype, filters, label, placeholder, value, required, setValue, disabled, autofocus, dropdownClass }: LinkFieldProps) => {
const LinkField = ({ doctype, filters, label, placeholder, value, required, setValue, disabled, autofocus, dropdownClass, suggestedItems }: LinkFieldProps) => {

const [searchText, setSearchText] = useState(value ?? '')

const isDesktop = useIsDesktop()

const { data } = useSearch(doctype, searchText, filters)

const items: SearchResult[] = data?.message ?? []
const items: SearchResult[] = [...(suggestedItems ?? []), ...(data?.message ?? [])]

const {
isOpen,
@@ -44,6 +45,9 @@ const LinkField = ({ doctype, filters, label, placeholder, value, required, setV
} = useCombobox({
onInputValueChange({ inputValue }) {
setSearchText(inputValue ?? '')
if (!inputValue) {
setValue('')
}
},
items: items,
itemToString(item) {
@@ -54,7 +58,6 @@ const LinkField = ({ doctype, filters, label, placeholder, value, required, setV
setValue(selectedItem?.value ?? '')
},
defaultInputValue: value,
defaultIsOpen: isDesktop && autofocus,
defaultSelectedItem: items.find(item => item.value === value),
})

13 changes: 12 additions & 1 deletion frontend/src/components/feature/CommandMenu/SettingsList.tsx
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom'
import { commandMenuOpenAtom } from './CommandMenu'
import { PiOpenAiLogo } from 'react-icons/pi'
import { LuFunctionSquare } from 'react-icons/lu'
import { AiOutlineApi } from 'react-icons/ai'

type Props = {}

@@ -30,7 +31,7 @@ const SettingsList = (props: Props) => {
<BiGroup size={ICON_SIZE} />
Users
</Command.Item>
<Command.Item value='hr' onSelect={onSelect}>
<Command.Item value='hr' keywords={['hr', 'human resources', 'Frappe HR']} onSelect={onSelect}>
<svg fill="none" viewBox="0 0 32 32" width={18} height={18} xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_2850_17380)">
<path d="M25.5561 0H6.44394C2.88505 0 0 2.88505 0 6.44394V25.5561C0 29.115 2.88505 32 6.44394 32H25.5561C29.115 32 32 29.115 32 25.5561V6.44394C32 2.88505 29.115 0 25.5561 0Z" fill="#A1EEC9"></path>
@@ -46,6 +47,16 @@ const SettingsList = (props: Props) => {
HR
</Command.Item>

<Command.Item value='scheduled-messages' keywords={['scheduled messages']} onSelect={onSelect}>
<BiMessageSquareDots size={ICON_SIZE} />
Scheduled Messages
</Command.Item>

<Command.Item value='webhooks' keywords={['webhooks']} onSelect={onSelect}>
<AiOutlineApi size={ICON_SIZE} />
Webhooks
</Command.Item>

<Command.Item value='bots' onSelect={onSelect}>
<BiBot size={ICON_SIZE} />
Bots
147 changes: 147 additions & 0 deletions frontend/src/components/feature/chat/ChatInput/DocumentLinkButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { Box, Button, Dialog, Flex, IconButton, Tooltip } from '@radix-ui/themes'
import { DEFAULT_BUTTON_STYLE, ICON_PROPS } from './ToolPanel'
import { LuFileBox } from 'react-icons/lu'
import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog'
import { FormProvider, useForm } from 'react-hook-form'
import LinkFormField from '@/components/common/LinkField/LinkFormField'
import { ErrorText } from '@/components/common/Form'
import { useBoolean } from '@/hooks/useBoolean'
import clsx from 'clsx'
import { useFrappeCreateDoc } from 'frappe-react-sdk'
import { Loader } from '@/components/common/Loader'
import { DoctypeLinkRenderer } from '../ChatMessage/Renderers/DoctypeLinkRenderer'
import { RavenMessage } from '@/types/RavenMessaging/RavenMessage'
import { Stack } from '@/components/layout/Stack'
import { ErrorBanner } from '@/components/layout/AlertBanner'
import useRecentlyUsedDocType from '@/hooks/useRecentlyUsedDocType'

type Props = {}

const DocumentLinkButton = ({ channelID }: { channelID: string }) => {

const [open, { off }, setOpen] = useBoolean()

return <Dialog.Root open={open} onOpenChange={setOpen}>
<Tooltip content={`Attach a document from the system`}>
<Dialog.Trigger>
<IconButton
aria-label='Attach a document from the system'
variant='ghost'
className={DEFAULT_BUTTON_STYLE}
size='1'
title='Attach a document from the system'>
<LuFileBox {...ICON_PROPS} />
</IconButton>
</Dialog.Trigger>
</Tooltip>
<Dialog.Content className={clsx(DIALOG_CONTENT_CLASS, 'static')}>
<Dialog.Title className='mb-1'>Send a document</Dialog.Title>
<Dialog.Description size='2'>Choose a document from the system to send.</Dialog.Description>
<DocumentLinkForm channelID={channelID} onClose={off} />
</Dialog.Content>
</Dialog.Root>
}

interface DocumentLinkFormData {
doctype: string
docname: string
}

const DocumentLinkForm = ({ channelID, onClose }: { channelID: string, onClose: () => void }) => {

const { loading, error, createDoc } = useFrappeCreateDoc<RavenMessage>()

const methods = useForm<DocumentLinkFormData>()

const { watch } = methods

const handleClose = () => {
methods.reset()
onClose()
}

const doctype = watch('doctype')
const docname = watch('docname')

const { recentlyUsedDoctypes, addRecentlyUsedDocType } = useRecentlyUsedDocType()

const onSubmit = (data: DocumentLinkFormData) => {

addRecentlyUsedDocType(data.doctype)

createDoc('Raven Message', {
message_type: 'Text',
channel_id: channelID,
link_doctype: data.doctype,
link_document: data.docname
} as RavenMessage)
.then(() => {
handleClose()
})
}

const onDoctypeChange = () => {
// Reset docname when doctype changes
methods.setValue('docname', '')
}

return <form className='pt-2' onSubmit={methods.handleSubmit(onSubmit)}>
<FormProvider {...methods}>
<Stack gap='2'>
{error && <ErrorBanner error={error} />}
<Box width='100%'>
<Flex direction='column' gap='2'>
<LinkFormField
name='doctype'
label='Document Type'
autofocus
suggestedItems={recentlyUsedDoctypes}
required
rules={{
required: 'Document Type is required',
onChange: onDoctypeChange
}}
filters={[["issingle", "=", 0], ["istable", "=", 0]]}
doctype="DocType"
/>
<ErrorText>{methods.formState.errors.doctype?.message}</ErrorText>
</Flex>
</Box>
{doctype &&
<Box width='100%'>
<Flex direction='column' gap='2'>
<LinkFormField
name='docname'
required
label='Document Name'
placeholder="Select a document"
disabled={!doctype}
rules={{ required: 'Document Name is required' }}
doctype={doctype}
/>
<ErrorText>{methods.formState.errors.docname?.message}</ErrorText>
</Flex>
</Box>
}

<div className={clsx('transition-all duration-500', docname ? 'opacity-100' : 'opacity-0')}>
{doctype && docname && <DoctypeLinkRenderer doctype={doctype} docname={docname} />}
</div>

<Flex gap="3" mt="6" justify="end" align='center'>
<Dialog.Close disabled={loading}>
<Button variant="soft" color="gray">Cancel</Button>
</Dialog.Close>
<Button type='button' disabled={loading} onClick={methods.handleSubmit(onSubmit)}
>
{loading && <Loader />}
Send
</Button>
</Flex>
</Stack>
</FormProvider>
</form>

}

export default DocumentLinkButton
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import { useBoolean } from '@/hooks/useBoolean'
import { MdOutlineBarChart } from 'react-icons/md'
import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog'
import AISavedPromptsButton from './AISavedPromptsButton'
import DocumentLinkButton from './DocumentLinkButton'


const EmojiPicker = lazy(() => import('@/components/common/EmojiPicker/EmojiPicker'))
@@ -21,7 +22,9 @@ export type RightToolbarButtonsProps = {
fileProps?: ToolbarFileProps,
sendMessage: (html: string, json: any) => Promise<void>,
messageSending: boolean,
setContent: (content: string) => void
setContent: (content: string) => void,
channelID?: string,
isEdit?: boolean
}
/**
* Component to render the right toolbar buttons:
@@ -34,22 +37,26 @@ export type RightToolbarButtonsProps = {
* @param props
* @returns
*/
export const RightToolbarButtons = ({ fileProps, ...sendProps }: RightToolbarButtonsProps) => {
export const RightToolbarButtons = ({ fileProps, channelID, isEdit, ...sendProps }: RightToolbarButtonsProps) => {
return (
<Flex gap='2' align='center' px='1' py='1'>

<MentionButtons />
<Flex gap='3' align='center'>
{!isEdit && channelID && <DocumentLinkButton channelID={channelID} />}
<MentionButtons />
</Flex>
<Separator orientation='vertical' />
<AISavedPromptsButton />
<CreatePollButton />
<Flex gap='3' align='center'>
<AISavedPromptsButton />
<CreatePollButton />
</Flex>
<Separator orientation='vertical' />
<Flex gap='3' align='center'>
<EmojiPickerButton />
<GIFPickerButton />
{fileProps && <FilePickerButton fileProps={fileProps} />}
<SendButton {...sendProps} />
</Flex>
</Flex>
</Flex >
)
}

9 changes: 6 additions & 3 deletions frontend/src/components/feature/chat/ChatInput/Tiptap.tsx
Original file line number Diff line number Diff line change
@@ -61,7 +61,8 @@ type TiptapEditorProps = {
messageSending: boolean,
defaultText?: string,
replyMessage?: Message | null,
channelMembers?: ChannelMembers
channelMembers?: ChannelMembers,
channelID?: string
}

export const UserMention = Mention.extend({
@@ -84,7 +85,7 @@ export const ChannelMention = Mention.extend({
}
})

const Tiptap = ({ isEdit, slotBefore, fileProps, onMessageSend, channelMembers, replyMessage, clearReplyMessage, placeholder = 'Type a message...', messageSending, sessionStorageKey = 'tiptap-editor', disableSessionStorage = false, defaultText = '' }: TiptapEditorProps) => {
const Tiptap = ({ isEdit, slotBefore, fileProps, onMessageSend, channelMembers, channelID, replyMessage, clearReplyMessage, placeholder = 'Type a message...', messageSending, sessionStorageKey = 'tiptap-editor', disableSessionStorage = false, defaultText = '' }: TiptapEditorProps) => {

const { enabledUsers } = useContext(UserListContext)

@@ -484,7 +485,9 @@ const Tiptap = ({ isEdit, slotBefore, fileProps, onMessageSend, channelMembers,
<EditorContent editor={editor} />
<ToolPanel>
<TextFormattingMenu />
<RightToolbarButtons fileProps={fileProps} setContent={setContent} sendMessage={onMessageSend} messageSending={messageSending} />
<RightToolbarButtons fileProps={fileProps} setContent={setContent} sendMessage={onMessageSend} messageSending={messageSending}
isEdit={isEdit}
channelID={channelID} />
</ToolPanel>
</EditorContext.Provider>
</Box>
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import LinkFormField from "@/components/common/LinkField/LinkFormField"
import { useContext, useState } from "react"
import { FileExtensionIcon } from "@/utils/layout/FileExtIcon"
import { getFileExtension, getFileName } from "@/utils/operations"
import useRecentlyUsedDocType from "@/hooks/useRecentlyUsedDocType"

interface AttachFileToDocumentModalProps {
onClose: () => void,
@@ -36,8 +37,11 @@ const AttachFileToDocumentModal = ({ onClose, message }: AttachFileToDocumentMod
const [loading, setLoading] = useState(false)
const [error, setError] = useState<FrappeError | null>(null)

const { recentlyUsedDoctypes, addRecentlyUsedDocType } = useRecentlyUsedDocType()

const onSubmit = (data: AttachFileToDocumentForm) => {
if ((message as FileMessage).file) {
addRecentlyUsedDocType(data.doctype)
setLoading(true)
call.get('frappe.client.get_value', {
doctype: 'File',
@@ -111,6 +115,7 @@ const AttachFileToDocumentModal = ({ onClose, message }: AttachFileToDocumentMod
name='doctype'
label='Document Type'
autofocus
suggestedItems={recentlyUsedDoctypes}
rules={{
required: 'Document Type is required',
onChange: onDoctypeChange
Original file line number Diff line number Diff line change
@@ -145,6 +145,7 @@ export const MessageItem = ({ message, setDeleteMessage, isHighlighted, onReplyM
transition-colors
px-1
py-1.5
sm:p-1.5
rounded-md`, isHighlighted ? 'bg-yellow-50 hover:bg-yellow-50 dark:bg-yellow-300/20 dark:hover:bg-yellow-300/20' : !isDesktop && isHovered ? 'bg-gray-2 dark:bg-gray-3' : '', isEmojiPickerOpen ? 'bg-gray-2 dark:bg-gray-3' : '')}>
<Flex className='gap-2.5 sm:gap-3 items-start'>
<MessageLeftElement message={message} user={user} isActive={isActive} />
Original file line number Diff line number Diff line change
@@ -101,6 +101,7 @@ export const ChatBoxBody = ({ channelData }: ChatBoxBodyProps) => {
&&
<Tiptap
key={channelData.name}
channelID={channelData.name}
fileProps={{
fileInputRef,
addFile
Original file line number Diff line number Diff line change
@@ -92,6 +92,7 @@ export const ThreadMessages = ({ threadMessage }: { threadMessage: Message }) =>
user={user} />}
{isUserInChannel && <Tiptap
key={threadID}
channelID={threadID}
fileProps={{
fileInputRef,
addFile
29 changes: 29 additions & 0 deletions frontend/src/hooks/useRecentlyUsedDocType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useMemo } from 'react'

const useRecentlyUsedDocType = () => {

const recentlyUsedDoctypes = useMemo(() => {
const existingItems = sessionStorage.getItem(`recently-used-doctype`) ?? '[]'
return JSON.parse(existingItems).map((doctype: string) => ({
value: doctype,
description: "Recently used"
}))
}, [])

const addRecentlyUsedDocType = (doctype: string) => {
// Save recently used doctype to session storage
const existingItems = sessionStorage.getItem(`recently-used-doctype`) ?? '[]'
let recentlyUsedDoctypes = JSON.parse(existingItems)
if (!recentlyUsedDoctypes.includes(doctype)) {
recentlyUsedDoctypes = [doctype, ...recentlyUsedDoctypes].slice(0, 5)
}
sessionStorage.setItem(`recently-used-doctype`, JSON.stringify(recentlyUsedDoctypes))
}

return {
recentlyUsedDoctypes,
addRecentlyUsedDocType
}
}

export default useRecentlyUsedDocType