diff --git a/src/core/i18n/en/translation.en.json b/src/core/i18n/en/translation.en.json index 7718c51c43..273f7cc3bc 100644 --- a/src/core/i18n/en/translation.en.json +++ b/src/core/i18n/en/translation.en.json @@ -3168,6 +3168,15 @@ "initialPosts": "Initial Posts", "delete": "Delete post" }, + "documents": { + "title": "Or add documents to your Knowledge Subspace.", + "tooltip": "Supported file formats: Text on websites, .pdf, .doc(x), .xls(x). Maximum file size: 20mb", + "referenceTitle": "Reference Title", + "referenceUrl": "Url", + "addAnother": "Add another document", + "remove": "Remove document", + "initialDocuments": "Initial Links and Documents" + }, "submitDisabled": "Add at least one Post to continue" }, "externalAI": { diff --git a/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent.tsx b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent.tsx deleted file mode 100644 index dd16c5ea52..0000000000 --- a/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import React, { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Formik } from 'formik'; -import * as yup from 'yup'; -import { LoadingButton } from '@mui/lab'; -import { Box, Button, DialogContent, IconButton, Tooltip } from '@mui/material'; -import AddIcon from '@mui/icons-material/Add'; -import DialogHeader from '@/core/ui/dialog/DialogHeader'; -import { Caption } from '@/core/ui/typography'; -import Gutters from '@/core/ui/grid/Gutters'; -import FormikInputField from '@/core/ui/forms/FormikInputField/FormikInputField'; -import FormikMarkdownField from '@/core/ui/forms/MarkdownInput/FormikMarkdownField'; -import { Actions } from '@/core/ui/actions/Actions'; -import { LONG_MARKDOWN_TEXT_LENGTH, SMALL_TEXT_LENGTH } from '@/core/ui/forms/field-length.constants'; -import CancelDialog from './CancelDialog'; -import MarkdownValidator from '@/core/ui/forms/MarkdownInput/MarkdownValidator'; -import { pullAt } from 'lodash'; -import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; -import { gutters } from '@/core/ui/grid/utils'; -import { MessageWithPayload } from '@/domain/shared/i18n/ValidationMessageTranslation'; - -type AddContentProps = { - onClose: () => void; - onCreateVC: (posts: PostsFormValues) => Promise; -}; - -export interface PostValues { - title: string; - description: string; -} - -export interface PostsFormValues { - posts: PostValues[]; -} - -const MAX__POSTS = 25; -const MIN__POSTS = 1; - -const AddContent = ({ onClose, onCreateVC }: AddContentProps) => { - const { t } = useTranslation(); - const [dialogOpen, setDialogOpen] = useState(false); - - const validationSchema = yup.object().shape({ - posts: yup - .array() - .of( - yup.object().shape({ - title: yup - .string() - .min(3, MessageWithPayload('forms.validations.minLength')) - .max(SMALL_TEXT_LENGTH, MessageWithPayload('forms.validations.maxLength')) - .required(MessageWithPayload('forms.validations.requiredField')), - description: MarkdownValidator(LONG_MARKDOWN_TEXT_LENGTH), - }) - ) - .min(MIN__POSTS, MessageWithPayload('forms.validations.minLength')), - }); - - const initialValues: PostsFormValues = { - posts: [ - { - title: t('createVirtualContributorWizard.addContent.post.exampleTitle'), - description: t('createVirtualContributorWizard.addContent.post.exampleDescription'), - }, - ], - }; - - const newPost = () => ({ - title: '', - description: '', - }); - - const onCancel = () => { - setDialogOpen(true); - }; - - return ( - <> - {t('createVirtualContributorWizard.addContent.title')} - - - {t('createVirtualContributorWizard.addContent.description')} - {t('createVirtualContributorWizard.addContent.descriptionBold')} - - {({ values: { posts }, isValid, setFieldValue }) => { - const handleAdd = () => { - const newArray = [...posts, newPost()]; - setFieldValue('posts', newArray); - }; - const handleDelete = (index: number) => { - const nextPosts = [...posts]; - pullAt(nextPosts, index); - setFieldValue('posts', nextPosts); - }; - return ( - <> - {posts.map((post, index) => ( - - - - - {posts.length > MIN__POSTS && ( - - handleDelete(index)} - size="large" - aria-label={t('createVirtualContributorWizard.addContent.post.delete')} - sx={{ marginTop: gutters(-1), alignSelf: 'flex-end' }} - > - - - - )} - - - ))} - - = MAX__POSTS - ? t('createVirtualContributorWizard.addContent.post.tooltip') - : t('createVirtualContributorWizard.addContent.post.addAnotherPost') - } - placement={'bottom-start'} - > - - - - - - - - - - - onCreateVC({ posts })}> - {t('buttons.continue')} - - - - - - - ); - }} - - - - setDialogOpen(false)} onConfirm={onClose} /> - - ); -}; - -export default AddContent; diff --git a/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent/AddContent.tsx b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent/AddContent.tsx new file mode 100644 index 0000000000..cc64ff645f --- /dev/null +++ b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent/AddContent.tsx @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { DialogContent } from '@mui/material'; +import DialogHeader from '@/core/ui/dialog/DialogHeader'; +import Gutters from '@/core/ui/grid/Gutters'; +import { Caption } from '@/core/ui/typography'; +import { gutters } from '@/core/ui/grid/utils'; +import { AddContentForm } from './AddContentForm'; +import { AddContentProps } from './AddContentProps'; +import CancelDialog from '../CancelDialog'; +import { StorageConfigContextProvider } from '@/domain/storage/StorageBucket/StorageConfigContext'; + +const AddContent = ({ onClose, onCreateVC, spaceId }: AddContentProps) => { + const { t } = useTranslation(); + const [dialogOpen, setDialogOpen] = useState(false); + + const onCancel = () => { + setDialogOpen(true); + }; + + return ( + + {t('createVirtualContributorWizard.addContent.title')} + + + {t('createVirtualContributorWizard.addContent.description')} + {t('createVirtualContributorWizard.addContent.descriptionBold')} + + + + setDialogOpen(false)} onConfirm={onClose} /> + + ); +}; + +export default AddContent; diff --git a/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent/AddContentForm.tsx b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent/AddContentForm.tsx new file mode 100644 index 0000000000..6e5bf8e08a --- /dev/null +++ b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent/AddContentForm.tsx @@ -0,0 +1,190 @@ +import { Formik } from 'formik'; +import { Trans, useTranslation } from 'react-i18next'; +import { pullAt } from 'lodash'; +import * as yup from 'yup'; +import { Box, Button, Tooltip } from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import { gutters } from '@/core/ui/grid/utils'; +import { LoadingButton } from '@mui/lab'; +import { LONG_MARKDOWN_TEXT_LENGTH, SMALL_TEXT_LENGTH } from '@/core/ui/forms/field-length.constants'; +import { Actions } from '@/core/ui/actions/Actions'; +import { MessageWithPayload } from '@/domain/shared/i18n/ValidationMessageTranslation'; +import MarkdownValidator from '@/core/ui/forms/MarkdownInput/MarkdownValidator'; +import { BoKCalloutsFormValues } from './AddContentProps'; +import { PostItem } from './PostItem'; +import { Caption } from '@/core/ui/typography'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import { DocumentItem } from '@/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent/DocumentItem'; + +const MAX_POSTS = 25; + +const newPost = () => ({ + title: '', + description: '', +}); + +const newDocument = () => ({ + name: '', + url: '', +}); + +export const AddContentForm = ({ + onSubmit, + onCancel, +}: { + onCancel: () => void; + onSubmit: (values: BoKCalloutsFormValues) => void; +}) => { + const { t } = useTranslation(); + + const validationSchema = yup.object().shape({ + posts: yup + .array() + .of( + yup.object().shape({ + title: yup + .string() + .min(3, MessageWithPayload('forms.validations.minLength')) + .max(SMALL_TEXT_LENGTH, MessageWithPayload('forms.validations.maxLength')) + .required(MessageWithPayload('forms.validations.requiredField')), + description: MarkdownValidator(LONG_MARKDOWN_TEXT_LENGTH), + }) + ) + .min(1, MessageWithPayload('forms.validations.minLength')), + }); + + const initialValues: BoKCalloutsFormValues = { + posts: [ + { + title: t('createVirtualContributorWizard.addContent.post.exampleTitle'), + description: t('createVirtualContributorWizard.addContent.post.exampleDescription'), + }, + ], + documents: [ + { + name: '', + url: '', + }, + ], + }; + + return ( + + {({ values: { posts, documents }, isValid, setFieldValue }) => { + const moreThanOnePost = posts.length > 1; + const maxPostsReached = posts.length >= MAX_POSTS; + + const handleAddPost = () => { + const newArray = [...posts, newPost()]; + setFieldValue('posts', newArray); + }; + const handleDeletePost = (index: number) => { + const nextPosts = [...posts]; + pullAt(nextPosts, index); + setFieldValue('posts', nextPosts); + }; + const handleAddDocument = () => { + const newArrayDocuments = [...documents, newDocument()]; + setFieldValue('documents', newArrayDocuments); + }; + const handleDeleteDocument = (index: number) => { + const nextDocuments = [...documents]; + pullAt(nextDocuments, index); + setFieldValue('documents', nextDocuments); + }; + + return ( + <> + {posts.map((post, index) => ( + + ))} + + + + + + + + + , + tooltip: ( + + <> + + ), + }} + /> + + {documents?.map((document, index) => ( + + ))} + + + + + + + + + + + + onSubmit({ posts, documents })} + > + {t('buttons.continue')} + + + + + + + ); + }} + + ); +}; diff --git a/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent/AddContentProps.ts b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent/AddContentProps.ts new file mode 100644 index 0000000000..88c742f8ea --- /dev/null +++ b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent/AddContentProps.ts @@ -0,0 +1,20 @@ +export type PostValues = { + title: string; + description: string; +}; + +export interface DocumentValues { + name: string; + url: string; +} + +export interface BoKCalloutsFormValues { + posts: PostValues[]; + documents: DocumentValues[]; +} + +export type AddContentProps = { + onClose: () => void; + onCreateVC: (values: BoKCalloutsFormValues) => Promise; + spaceId: string; +}; diff --git a/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent/DocumentItem.tsx b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent/DocumentItem.tsx new file mode 100644 index 0000000000..f2ce59a502 --- /dev/null +++ b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent/DocumentItem.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Box, Tooltip, IconButton, Theme, useMediaQuery, Link } from '@mui/material'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import FormikInputField from '@/core/ui/forms/FormikInputField/FormikInputField'; +import Gutters from '@/core/ui/grid/Gutters'; +import { DocumentValues } from './AddContentProps'; +import FormikFileInput from '@/core/ui/forms/FormikFileInput/FormikFileInput'; +import { useConfig } from '@/domain/platform/config/useConfig'; +import { TranslateWithElements } from '@/domain/shared/i18n/TranslateWithElements'; + +interface DocumentItemProps { + document: DocumentValues; + index: number; + onDelete: (index: number) => void; +} + +export const DocumentItem = ({ document, index, onDelete }: DocumentItemProps) => { + const { t } = useTranslation(); + const { locations } = useConfig(); + const tLinks = TranslateWithElements(); + + const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm')); + + return ( + + + + + + + + onDelete(index)} size="large" color="primary"> + + + + + + + + ); +}; diff --git a/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent/PostItem.tsx b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent/PostItem.tsx new file mode 100644 index 0000000000..ba5521b85e --- /dev/null +++ b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/AddContent/PostItem.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Box, Tooltip, IconButton } from '@mui/material'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import { LONG_MARKDOWN_TEXT_LENGTH } from '@/core/ui/forms/field-length.constants'; +import FormikInputField from '@/core/ui/forms/FormikInputField/FormikInputField'; +import FormikMarkdownField from '@/core/ui/forms/MarkdownInput/FormikMarkdownField'; +import Gutters from '@/core/ui/grid/Gutters'; +import { gutters } from '@/core/ui/grid/utils'; +import { PostValues } from './AddContentProps'; + +interface PostItemProps { + post: PostValues; + index: number; + onDelete: (index: number) => void; + hasDelete: boolean; +} + +export const PostItem = ({ post, index, onDelete, hasDelete }: PostItemProps) => { + const { t } = useTranslation(); + + return ( + + + + + {hasDelete && ( + + onDelete(index)} + size="large" + aria-label={t('createVirtualContributorWizard.addContent.post.delete')} + sx={{ marginTop: gutters(-1), alignSelf: 'flex-end' }} + > + + + + )} + + + ); +}; diff --git a/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/useNewVirtualContributorWizard.tsx b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/useNewVirtualContributorWizard.tsx index 81cd78d06a..8b391afa6f 100644 --- a/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/useNewVirtualContributorWizard.tsx +++ b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/useNewVirtualContributorWizard.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { refetchSubspacesInSpaceQuery, useCreateSpaceMutation, @@ -13,6 +13,7 @@ import { useSubspaceCommunityAndRoleSetIdLazyQuery, useAssignRoleToVirtualContributorMutation, refetchDashboardWithMembershipsQuery, + useCreateLinkOnCalloutMutation, } from '@/core/apollo/generated/apollo-hooks'; import { AiPersonaBodyOfKnowledgeType, @@ -28,7 +29,8 @@ import { } from '@/core/apollo/generated/graphql-schema'; import CreateNewVirtualContributor, { VirtualContributorFromProps } from './CreateNewVirtualContributor'; import LoadingState from './LoadingState'; -import AddContent, { PostsFormValues, PostValues } from './AddContent'; +import AddContent from './AddContent/AddContent'; +import { BoKCalloutsFormValues, DocumentValues, PostValues } from './AddContent/AddContentProps'; import ExistingSpace, { SelectableKnowledgeProps } from './ExistingSpace'; import { useTranslation } from 'react-i18next'; import { useNotification } from '@/core/ui/notifications/useNotification'; @@ -88,8 +90,9 @@ const useNewVirtualContributorWizard = (): useNewVirtualContributorWizardProvide const [boKParentRoleSetId, setBoKParentRoleSetId] = useState(); const [creationIndex, setCreationIndex] = useState(0); // used in case of space deletion const [virtualContributorInput, setVirtualContributorInput] = useState(); - const [calloutPostData, setCalloutPostData] = useState(); + const [calloutData, setCalloutData] = useState(); const [tryCreateCallout, setTryCreateCallout] = useState(false); + const calloutId = useRef(); const startWizard = (initAccount: UserAccountProps | undefined) => { setTargetAccount(initAccount); @@ -102,8 +105,8 @@ const useNewVirtualContributorWizard = (): useNewVirtualContributorWizardProvide setStep(step); }; - const handleAddContent = async (posts: PostsFormValues) => { - setCalloutPostData(posts); + const handleAddContent = async (values: BoKCalloutsFormValues) => { + setCalloutData(values); // trigger useEffect to create the callout once canCreateCallout is true setTryCreateCallout(true); }; @@ -386,6 +389,23 @@ const useNewVirtualContributorWizard = (): useNewVirtualContributorWizardProvide sendNotification: false, }; + const calloutDocumentsDetails: CalloutCreationType = { + framing: { + profile: { + displayName: t('createVirtualContributorWizard.addContent.documents.initialDocuments'), + description: '', + referencesData: [], + }, + }, + type: CalloutType.LinkCollection, + contributionPolicy: { + state: CalloutState.Open, + }, + groupName: CalloutGroupName.Home, + visibility: CalloutVisibility.Published, + sendNotification: false, + }; + // no need of 'update' implementation as the callout is on another page // where CalloutDetails will refetch the details const [createPost] = useCreatePostFromContributeTabMutation(); @@ -405,23 +425,58 @@ const useNewVirtualContributorWizard = (): useNewVirtualContributorWizardProvide }); }; + const [createLinkOnCallout] = useCreateLinkOnCalloutMutation(); + const onCreateLink = async (document: DocumentValues, calloutId: string) => { + await createLinkOnCallout({ + variables: { + input: { + calloutID: calloutId, + link: { + uri: document.url, + profile: { + displayName: document.name, + }, + }, + }, + }, + }); + }; + const createCalloutContent = async () => { setStep('loadingVCSetup'); // create collection of posts - if (calloutPostData?.posts && calloutPostData?.posts.length > 0 && bokId) { - const callout = await handleCreateCallout(calloutDetails); + if (calloutData?.posts && calloutData?.posts.length > 0 && bokId) { + const postCallout = await handleCreateCallout(calloutDetails); + const postCalloutId = postCallout?.id; + calloutId.current = postCalloutId; // add posts to collection - if (callout?.id) { - for (const post of calloutPostData?.posts) { - await onCreatePost(post, callout.id); + if (postCalloutId) { + const postsArray = calloutData?.posts ?? []; + for (const post of postsArray) { + await onCreatePost(post, postCalloutId); } } + } - setTryCreateCallout(false); + // create collection of docs & links + if (calloutData?.documents && calloutData?.documents.length > 0 && bokId) { + const documentsCallout = await handleCreateCallout(calloutDocumentsDetails); + const documentsCalloutId = documentsCallout?.id; + calloutId.current = documentsCalloutId; + + // add documents to collection + if (documentsCalloutId) { + const documentsArray = calloutData?.documents ?? []; + for (const document of documentsArray) { + await onCreateLink(document, documentsCalloutId); + } + } } + setTryCreateCallout(false); + // create VC if (virtualContributorInput && myAccountId && bokId && bokRoleSetId) { const creationSuccess = await handleCreateVirtualContributor({ @@ -606,7 +661,11 @@ const useNewVirtualContributorWizard = (): useNewVirtualContributorWizardProvide )} {step === 'addKnowledge' && virtualContributorInput && ( - + )} {step === 'existingKnowledge' && (