diff --git a/src/pages/OldCommon/interfaces/CreateCommonPayload.ts b/src/pages/OldCommon/interfaces/CreateCommonPayload.ts index b23d19dc16..6f955ee066 100644 --- a/src/pages/OldCommon/interfaces/CreateCommonPayload.ts +++ b/src/pages/OldCommon/interfaces/CreateCommonPayload.ts @@ -1,5 +1,10 @@ import { UploadFile } from "@/shared/interfaces"; -import { BaseRule, CommonLink, Roles } from "@/shared/models"; +import { + BaseRule, + CommonLink, + NotionIntegrationIntermediate, + Roles, +} from "@/shared/models"; import { MemberAdmittanceLimitations } from "@/shared/models/governance/proposals"; import { TextEditorValue } from "@/shared/ui-kit/TextEditor/types"; @@ -59,6 +64,7 @@ export interface IntermediateUpdateCommonData { gallery?: UploadFile[]; links?: CommonLink[]; roles?: Roles; + notion?: NotionIntegrationIntermediate; } export interface UpdateCommonData { diff --git a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx index 5e2244434a..e8be9b8a28 100644 --- a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx +++ b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx @@ -23,6 +23,7 @@ import { Common, CommonFeed, CommonMember, + CommonNotion, DirectParent, Governance, PredefinedTypes, @@ -52,6 +53,7 @@ interface DiscussionFeedCardProps { commonId?: string; commonName: string; commonImage: string; + commonNotion?: CommonNotion; pinnedFeedItems?: Common["pinnedFeedItems"]; commonMember?: CommonMember | null; isProject: boolean; @@ -79,6 +81,7 @@ const DiscussionFeedCard = forwardRef( commonId, commonName, commonImage, + commonNotion, pinnedFeedItems, commonMember, isProject, @@ -283,6 +286,7 @@ const DiscussionFeedCard = forwardRef( { onHover(true); @@ -334,6 +338,7 @@ const DiscussionFeedCard = forwardRef( seen={feedItemUserMetadata?.seen ?? !isFeedItemUserMetadataFetched} ownerId={item.userId} discussionPredefinedType={discussion?.predefinedType} + notion={discussion?.notion && commonNotion} hasUnseenMention={ isFeedItemUserMetadataFetched && feedItemUserMetadata?.hasUnseenMention diff --git a/src/pages/common/components/FeedCard/FeedCard.tsx b/src/pages/common/components/FeedCard/FeedCard.tsx index f351559388..05a97ed22c 100644 --- a/src/pages/common/components/FeedCard/FeedCard.tsx +++ b/src/pages/common/components/FeedCard/FeedCard.tsx @@ -11,7 +11,7 @@ import classNames from "classnames"; import { useFeedItemContext } from "@/pages/common"; import { useIsTabletView } from "@/shared/hooks/viewport"; import { ContextMenuItem } from "@/shared/interfaces"; -import { CommonFeedType, PredefinedTypes } from "@/shared/models"; +import { CommonFeedType, CommonNotion, PredefinedTypes } from "@/shared/models"; import { Loader, TextEditorValue } from "@/shared/ui-kit"; import { CommonCard } from "../CommonCard"; import { FeedCardRef } from "./types"; @@ -47,6 +47,7 @@ type FeedCardProps = PropsWithChildren<{ hasFiles?: boolean; hasImages?: boolean; hasUnseenMention?: boolean; + notion?: CommonNotion; }>; const MOBILE_HEADER_HEIGHT = 52; @@ -87,6 +88,7 @@ export const FeedCard = forwardRef((props, ref) => { discussionPredefinedType, hasImages, hasFiles, + notion, } = props; const scrollTimeoutRef = useRef | null>(null); const isTabletView = useIsTabletView(); @@ -210,6 +212,7 @@ export const FeedCard = forwardRef((props, ref) => { hasFiles, hasImages, hasUnseenMention, + notion, })} )} diff --git a/src/pages/common/components/FeedCard/components/FeedCardContent/FeedCardContent.module.scss b/src/pages/common/components/FeedCard/components/FeedCardContent/FeedCardContent.module.scss index 03c749a157..ada8665362 100644 --- a/src/pages/common/components/FeedCard/components/FeedCardContent/FeedCardContent.module.scss +++ b/src/pages/common/components/FeedCard/components/FeedCardContent/FeedCardContent.module.scss @@ -3,6 +3,7 @@ .container { display: flex; flex-direction: column; + gap: 10px; min-height: 4.5rem; justify-content: space-between; width: 100%; diff --git a/src/pages/common/components/FeedCard/components/FeedCardContent/FeedCardContent.tsx b/src/pages/common/components/FeedCard/components/FeedCardContent/FeedCardContent.tsx index 55b2c87fb5..65363a100c 100644 --- a/src/pages/common/components/FeedCard/components/FeedCardContent/FeedCardContent.tsx +++ b/src/pages/common/components/FeedCard/components/FeedCardContent/FeedCardContent.tsx @@ -1,12 +1,14 @@ import React, { ReactNode } from "react"; -import { CommonLink } from "@/shared/models"; +import { CommonLink, DiscussionNotion } from "@/shared/models"; import { FeedGeneralInfo } from "../FeedGeneralInfo"; +import { FeedNotionInfo } from "../FeedNotionInfo"; import styles from "./FeedCardContent.module.scss"; export type FeedCardContentProps = JSX.IntrinsicElements["div"] & { subtitle?: ReactNode; description?: string; images?: CommonLink[]; + notion?: DiscussionNotion; onClick: () => void; }; @@ -16,6 +18,7 @@ export const FeedCardContent: React.FC = (props) => { description, subtitle, images, + notion, onClick, onMouseEnter, onMouseLeave, @@ -28,6 +31,7 @@ export const FeedCardContent: React.FC = (props) => { onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} > + {!!notion && } = (props) => { isLoading = false, shouldHideBottomContent = false, hasUnseenMention, + notion, } = props; const contextMenuRef = useRef(null); const [isLongPressing, setIsLongPressing] = useState(false); @@ -109,15 +114,28 @@ export const FeedItemBaseContent: FC = (props) => { {renderLeftContent?.()}
-

- {isLoading || !title ? "Loading..." : title} -

+ {isLoading || !title ? "Loading..." : title} + {Boolean(notion) && ( + + +
+ +
+
+ + Notion sync + Database: {notion?.title} + +
+ )} +

= (props) => { + const { notion } = props; + + const linkToNotionPageEl = ( + + Notion pageID: {notion?.pageId} + + ); + + return ( +

+ + {linkToNotionPageEl} +
+ ); +}; diff --git a/src/pages/common/components/FeedCard/components/FeedNotionInfo/index.ts b/src/pages/common/components/FeedCard/components/FeedNotionInfo/index.ts new file mode 100644 index 0000000000..10a32c9c46 --- /dev/null +++ b/src/pages/common/components/FeedCard/components/FeedNotionInfo/index.ts @@ -0,0 +1 @@ +export * from "./FeedNotionInfo"; diff --git a/src/pages/common/components/FeedCard/components/index.ts b/src/pages/common/components/FeedCard/components/index.ts index 9103982ecd..809bc3b307 100644 --- a/src/pages/common/components/FeedCard/components/index.ts +++ b/src/pages/common/components/FeedCard/components/index.ts @@ -7,3 +7,4 @@ export * from "./FeedGeneralInfo"; export * from "./FeedInfoHeader"; export * from "./FeedItemBaseContent"; export * from "./FeedCardTags"; +export * from "./FeedNotionInfo"; diff --git a/src/pages/common/components/FeedItem/FeedItem.tsx b/src/pages/common/components/FeedItem/FeedItem.tsx index 6936c90e34..946782d22e 100644 --- a/src/pages/common/components/FeedItem/FeedItem.tsx +++ b/src/pages/common/components/FeedItem/FeedItem.tsx @@ -8,6 +8,7 @@ import { CommonFeed, CommonFeedType, CommonMember, + CommonNotion, DirectParent, } from "@/shared/models"; import { checkIsItemVisibleForUser } from "@/shared/utils"; @@ -23,6 +24,7 @@ interface FeedItemProps { commonName: string; commonMember?: (CommonMember & CirclesPermissions) | null; commonImage: string; + commonNotion?: CommonNotion; pinnedFeedItems?: Common["pinnedFeedItems"]; isProject?: boolean; isPinned?: boolean; @@ -48,6 +50,7 @@ const FeedItem = forwardRef((props, ref) => { commonId, commonName, commonImage, + commonNotion, pinnedFeedItems, commonMember, isProject = false, @@ -112,6 +115,7 @@ const FeedItem = forwardRef((props, ref) => { commonId, commonName, commonImage, + commonNotion, pinnedFeedItems, isActive, isExpanded, diff --git a/src/pages/common/components/FeedItem/context.ts b/src/pages/common/components/FeedItem/context.ts index 0a23aa1ad7..bc4db7e5c2 100644 --- a/src/pages/common/components/FeedItem/context.ts +++ b/src/pages/common/components/FeedItem/context.ts @@ -3,6 +3,7 @@ import { ContextMenuItem } from "@/shared/interfaces"; import { CommonFeed, CommonFeedType, + CommonNotion, Discussion, PredefinedTypes, } from "@/shared/models"; @@ -45,6 +46,7 @@ export interface FeedItemBaseContentProps { shouldHideBottomContent?: boolean; dmUserId?: string; hasUnseenMention?: boolean; + notion?: CommonNotion; } export interface GetLastMessageOptions { diff --git a/src/pages/common/components/FeedItems/FeedItems.tsx b/src/pages/common/components/FeedItems/FeedItems.tsx index c2119d721a..9f1b6a3741 100644 --- a/src/pages/common/components/FeedItems/FeedItems.tsx +++ b/src/pages/common/components/FeedItems/FeedItems.tsx @@ -74,6 +74,7 @@ const FeedItems: FC = (props) => { commonId={common.id} commonName={common.name} commonImage={common.image} + commonNotion={common.notion} pinnedFeedItems={common.pinnedFeedItems} isPinned={isPinned} isProject={checkIsProject(common)} diff --git a/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx b/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx index b29526fd31..ea636ab1f3 100644 --- a/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx +++ b/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx @@ -23,6 +23,7 @@ import { FeedLayoutItemChangeData } from "@/shared/interfaces"; import { Common, CommonFeed, + CommonNotion, Governance, PredefinedTypes, ResolutionType, @@ -68,6 +69,7 @@ interface ProposalFeedCardProps { commonId?: string; commonName: string; commonImage: string; + commonNotion?: CommonNotion; pinnedFeedItems?: Common["pinnedFeedItems"]; isProject: boolean; isPinned: boolean; @@ -91,6 +93,7 @@ const ProposalFeedCard = forwardRef( commonId, commonName, commonImage, + commonNotion, pinnedFeedItems, isProject, isPinned, @@ -371,6 +374,7 @@ const ProposalFeedCard = forwardRef( proposal.data.args.description, proposal.type, )} + notion={discussion?.notion} images={discussion?.images} onClick={handleOpenChat} onMouseEnter={() => { @@ -459,6 +463,7 @@ const ProposalFeedCard = forwardRef( menuItems={menuItems} ownerId={item.userId} commonId={commonId} + notion={discussion?.notion && commonNotion} hasUnseenMention={ isFeedItemUserMetadataFetched && feedItemUserMetadata?.hasUnseenMention diff --git a/src/pages/commonCreation/components/CreationForm/components/Item/Item.tsx b/src/pages/commonCreation/components/CreationForm/components/Item/Item.tsx index c829a54160..c65baf8700 100644 --- a/src/pages/commonCreation/components/CreationForm/components/Item/Item.tsx +++ b/src/pages/commonCreation/components/CreationForm/components/Item/Item.tsx @@ -6,6 +6,7 @@ import { TextField, UploadFiles, RolesArrayWrapper, + NotionIntegration, } from "@/shared/components/Form/Formik"; import { CreationFormItemType } from "../../constants"; import { CreationFormItem } from "../../types"; @@ -79,6 +80,8 @@ const Item: FC = (props) => { disabled={disabled ?? item.props.disabled} /> ); + case CreationFormItemType.NotionIntegration: + return ; default: return null; } diff --git a/src/pages/commonCreation/components/CreationForm/constants/creationFormItemType.ts b/src/pages/commonCreation/components/CreationForm/constants/creationFormItemType.ts index 68613e563c..845719ec7c 100644 --- a/src/pages/commonCreation/components/CreationForm/constants/creationFormItemType.ts +++ b/src/pages/commonCreation/components/CreationForm/constants/creationFormItemType.ts @@ -4,4 +4,5 @@ export enum CreationFormItemType { UploadFiles = "UploadFiles", Links = "Links", Roles = "Roles", + NotionIntegration = "NotionIntegration", } diff --git a/src/pages/commonCreation/components/CreationForm/types.ts b/src/pages/commonCreation/components/CreationForm/types.ts index c86330cd93..180a28e0f6 100644 --- a/src/pages/commonCreation/components/CreationForm/types.ts +++ b/src/pages/commonCreation/components/CreationForm/types.ts @@ -4,6 +4,7 @@ import { TextEditorProps, UploadFilesProps, RolesArrayWrapperProps, + NotionIntegrationProps, } from "@/shared/components/Form/Formik"; import { CreationFormItemType } from "./constants"; @@ -61,9 +62,15 @@ export interface RolesFormItem extends BaseFormItem { validation?: Pick; } +export interface NotionIntegrationFormItem + extends BaseFormItem { + type: CreationFormItemType.NotionIntegration; +} + export type CreationFormItem = | TextFieldFormItem | TextEditorFormItem | UploadFilesFormItem | LinksFormItem - | RolesFormItem; + | RolesFormItem + | NotionIntegrationFormItem; diff --git a/src/pages/commonCreation/components/CreationForm/utils/generateValidationSchema.ts b/src/pages/commonCreation/components/CreationForm/utils/generateValidationSchema.ts index 6e4eee070b..6e2c3c8d41 100644 --- a/src/pages/commonCreation/components/CreationForm/utils/generateValidationSchema.ts +++ b/src/pages/commonCreation/components/CreationForm/utils/generateValidationSchema.ts @@ -99,6 +99,24 @@ const getValidationSchemaForRolesItem = ({ ); }; +const getValidationSchemaForNotionIntegrationItem = (): Schema => { + return Yup.object().shape({ + isEnabled: Yup.boolean(), + token: Yup.string().when("isEnabled", (isEnabled: boolean) => { + if (isEnabled) { + return Yup.string().required( + "Please enter Notion's internal integration secret", + ); + } + }), + databaseId: Yup.string().when("isEnabled", (isEnabled: boolean) => { + if (isEnabled) { + return Yup.string().required("Please enter Notion database ID"); + } + }), + }); +}; + export const generateValidationSchema = ( items: CreationFormItem[], ): Yup.ObjectSchema => { @@ -117,6 +135,9 @@ export const generateValidationSchema = ( if (item.type === CreationFormItemType.Roles) { schema = getValidationSchemaForRolesItem(item); } + if (item.type === CreationFormItemType.NotionIntegration) { + schema = getValidationSchemaForNotionIntegrationItem(); + } return schema ? { diff --git a/src/pages/commonCreation/components/ProjectCreation/ProjectCreation.tsx b/src/pages/commonCreation/components/ProjectCreation/ProjectCreation.tsx index dae48e32af..1e8cab874b 100644 --- a/src/pages/commonCreation/components/ProjectCreation/ProjectCreation.tsx +++ b/src/pages/commonCreation/components/ProjectCreation/ProjectCreation.tsx @@ -171,8 +171,8 @@ const ProjectCreation: FC = (props) => { : `Create a new space in ${parentCommon.name}`}

- Space serves a certain group in the common to organize together and - achieve more focused goals. + Spaces are specific areas of collaboration which contain more focused + subspaces and single-topic streams.

= (props) => { error: updateProjectError, updateCommon: updateProject, } = useCommonUpdate(initialCommon?.id); + const { + isNotionIntegrationUpdated, + notionIntegrationErrorModalState, + disconnectNotionModalState, + setNotionIntegrationFormData, + } = useNotionIntegration({ + projectId: project?.id || updatedProject?.id, + isNotionIntegrationEnabled: Boolean(initialCommon?.notion), + }); const isLoading = isProjectCreationLoading || isCommonUpdateLoading; const error = createProjectError || updateProjectError; const nonProjectCircles = useMemo( @@ -121,6 +138,7 @@ const ProjectCreationForm: FC = (props) => { ); const handleProjectCreate = (values: ProjectCreationFormValues) => { + setNotionIntegrationFormData(values.notion); createProject(parentCommonId, values); }; @@ -132,6 +150,7 @@ const ProjectCreationForm: FC = (props) => { const [image] = values.projectImages; + setNotionIntegrationFormData(values.notion); updateProject({ ...values, image, @@ -158,13 +177,13 @@ const ProjectCreationForm: FC = (props) => { useEffect(() => { const finalProject = project || updatedProject; - if (finalProject && governance) { + if (finalProject && governance && isNotionIntegrationUpdated) { onFinish({ project: finalProject, governance, }); } - }, [project, updatedProject, governance]); + }, [project, updatedProject, governance, isNotionIntegrationUpdated]); return ( <> @@ -190,6 +209,26 @@ const ProjectCreationForm: FC = (props) => { error={error} /> + + Oops, our attempt to integrate the space with Notion hit a bump. Recheck + your settings, and don't hesitate to ask for help if needed! + + + Are you sure you want to remove the Notion integration? + ); }; diff --git a/src/pages/commonCreation/components/ProjectCreation/components/ProjectCreationForm/configuration.ts b/src/pages/commonCreation/components/ProjectCreation/components/ProjectCreationForm/configuration.ts index 85064ac204..b4cd117b37 100644 --- a/src/pages/commonCreation/components/ProjectCreation/components/ProjectCreationForm/configuration.ts +++ b/src/pages/commonCreation/components/ProjectCreation/components/ProjectCreationForm/configuration.ts @@ -143,5 +143,26 @@ export const getConfiguration = (options: Options): CreationFormItem[] => { }); } + if (isProject) { + items.push({ + type: CreationFormItemType.NotionIntegration, + props: { + name: "notion", + isEnabled: { + name: "notion.isEnabled", + label: "Notion database integration", + }, + token: { + name: "notion.token", + label: "Notion's Internal Integration Secret", + }, + databaseId: { + name: "notion.databaseId", + label: "Notion database ID", + }, + }, + }); + } + return items; }; diff --git a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx index 7adc25a741..e045a84dbc 100644 --- a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx +++ b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx @@ -694,6 +694,7 @@ const FeedLayout: ForwardRefRenderFunction = ( commonId={commonData?.id} commonName={commonData?.name || ""} commonImage={commonData?.image || ""} + commonNotion={outerCommon?.notion} pinnedFeedItems={outerCommon?.pinnedFeedItems} isProject={commonData?.isProject} isPinned={isPinned} diff --git a/src/pages/commonFeed/components/HeaderContent/HeaderContent.tsx b/src/pages/commonFeed/components/HeaderContent/HeaderContent.tsx index 15c4f63693..40ed79eeef 100644 --- a/src/pages/commonFeed/components/HeaderContent/HeaderContent.tsx +++ b/src/pages/commonFeed/components/HeaderContent/HeaderContent.tsx @@ -39,6 +39,7 @@ const HeaderContent: FC = (props) => { commonId={common.id} commonName={common.name} commonImage={common.image} + notion={common.notion} isProject={checkIsProject(common)} memberCount={common.memberCount} showFollowIcon={showFollowIcon} diff --git a/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.module.scss b/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.module.scss index bb14f30f7e..3add8e89c3 100644 --- a/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.module.scss +++ b/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.module.scss @@ -110,3 +110,14 @@ font-size: $xxsmall-2; } } + +.tooltipTriggerContainer { + display: inline-flex; +} + +.tooltipContent { + display: flex; + flex-direction: column; + max-width: 20rem; + z-index: 3; +} diff --git a/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.tsx b/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.tsx index a043b52554..d0443964e7 100644 --- a/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.tsx +++ b/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.tsx @@ -1,7 +1,14 @@ import React, { FC } from "react"; import classNames from "classnames"; -import { SidebarIcon, StarIcon } from "@/shared/icons"; -import { CommonAvatar, TopNavigationOpenSidenavButton } from "@/shared/ui-kit"; +import { NotionIcon, SidebarIcon, StarIcon } from "@/shared/icons"; +import { + CommonAvatar, + Tooltip, + TooltipContent, + TooltipTrigger, + TopNavigationOpenSidenavButton, +} from "@/shared/ui-kit"; +import { CommonNotion } from "@/shared/models"; import { getPluralEnding } from "@/shared/utils"; import { ContentWrapper } from "./components"; import styles from "./HeaderCommonContent.module.scss"; @@ -10,6 +17,7 @@ interface HeaderCommonContentProps { commonId: string; commonName: string; commonImage: string; + notion?: CommonNotion; isProject: boolean; memberCount: number; showFollowIcon?: boolean; @@ -20,6 +28,7 @@ const HeaderCommonContent: FC = (props) => { commonId, commonName, commonImage, + notion, isProject, memberCount, showFollowIcon = false, @@ -44,6 +53,19 @@ const HeaderCommonContent: FC = (props) => {

{commonName}

+ {Boolean(notion) && ( + + +
+ +
+
+ + Notion sync + Database: {notion?.title} + +
+ )} {showFollowIcon && }

diff --git a/src/services/Notion.ts b/src/services/Notion.ts new file mode 100644 index 0000000000..15aa442206 --- /dev/null +++ b/src/services/Notion.ts @@ -0,0 +1,21 @@ +import { ApiEndpoint } from "@/shared/constants"; +import { NotionIntegration } from "@/shared/models"; +import Api from "./Api"; + +class NotionService { + public setupIntegration = async ( + commonId: string, + notion: NotionIntegration, + ): Promise => { + await Api.post(ApiEndpoint.AddNotionIntegration, { + ...notion, + commonId, + }); + }; + + public removeIntegration = async (commonId: string): Promise => { + await Api.post(ApiEndpoint.RemoveNotionIntegration, { commonId }); + }; +} + +export default new NotionService(); diff --git a/src/services/index.ts b/src/services/index.ts index aa61e3f5f7..4baa44fd2f 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -19,3 +19,4 @@ export { default as ProjectService } from "./Project"; export { default as ProposalService } from "./Proposal"; export { default as UserService } from "./User"; export { default as DiscussionMessageService } from "./DiscussionMessage"; +export { default as NotionService } from "./Notion"; diff --git a/src/shared/components/ConfirmationModal/ConfirmationModal.module.scss b/src/shared/components/ConfirmationModal/ConfirmationModal.module.scss new file mode 100644 index 0000000000..755e087166 --- /dev/null +++ b/src/shared/components/ConfirmationModal/ConfirmationModal.module.scss @@ -0,0 +1,45 @@ +@import "../../../constants"; +@import "../../../styles/mixins"; + +.modal { + max-width: 31.875rem; + + .modalHeader { + margin-top: 1rem; + justify-content: flex-start; + } + + .modalContent { + padding: 0 2.5rem 2.5rem 2.5rem; + } +} + +.modalTitle { + color: var(--primary-text); + font-family: Poppins, sans-serif; + font-size: 20px; + line-height: 120%; +} + +.description { + margin: 0; + color: var(--primary-text); + font-family: Poppins, sans-serif; + font-size: 16px; + line-height: 20px; +} + +.buttonsContainer { + margin-top: 2.5rem; +} + +.buttonsWrapper { + @include flex-list-with-gap(1.375rem); + + justify-content: flex-end; +} + +.button { + max-width: 9.5rem; + width: 100%; +} diff --git a/src/shared/components/ConfirmationModal/ConfirmationModal.tsx b/src/shared/components/ConfirmationModal/ConfirmationModal.tsx new file mode 100644 index 0000000000..f67ec0fa1c --- /dev/null +++ b/src/shared/components/ConfirmationModal/ConfirmationModal.tsx @@ -0,0 +1,70 @@ +import React, { FC } from "react"; +import { Modal } from "@/shared/components/Modal"; +import { ModalProps } from "@/shared/interfaces"; +import { Button, ButtonVariant } from "@/shared/ui-kit"; +import styles from "./ConfirmationModal.module.scss"; + +export interface ConfirmationModalState { + isShowing: boolean; + onClose: () => void; + onConfirm: () => void; +} + +interface ConfirmationModalProps + extends ConfirmationModalState, + Pick { + title: string; + closeText?: string; + confirmText: string; +} + +const ConfirmationModal: FC = (props) => { + const { + isShowing, + styles: outerStyles, + title, + confirmText, + closeText, + onConfirm, + onClose, + children, + } = props; + + return ( + {title}} + styles={{ + header: styles.modalHeader, + content: styles.modalContent, + ...outerStyles, + }} + > +

{children}

+
+
+ {closeText && ( + + )} + +
+
+ + ); +}; + +export default ConfirmationModal; diff --git a/src/shared/components/ConfirmationModal/index.ts b/src/shared/components/ConfirmationModal/index.ts new file mode 100644 index 0000000000..a59886c656 --- /dev/null +++ b/src/shared/components/ConfirmationModal/index.ts @@ -0,0 +1,2 @@ +export { default as ConfirmationModal } from "./ConfirmationModal"; +export type { ConfirmationModalState } from "./ConfirmationModal"; diff --git a/src/shared/components/Form/Formik/NotionIntegration/NotionIntegration.module.scss b/src/shared/components/Form/Formik/NotionIntegration/NotionIntegration.module.scss new file mode 100644 index 0000000000..2f69954561 --- /dev/null +++ b/src/shared/components/Form/Formik/NotionIntegration/NotionIntegration.module.scss @@ -0,0 +1,32 @@ +@import "../../../../../constants.scss"; + +.integrationGuidanceText { + color: $c-gray-60; + font-family: PoppinsSans, sans-serif; + font-size: 14px; + font-weight: 400; +} + +.integrationGuidanceSteps { + padding-left: 1.25rem; +} + +.integrationGuidanceStep { + margin-bottom: 1rem; +} + +.link { + text-decoration: none; + color: var(--primary-fill); + + &:hover { + cursor: pointer; + opacity: 0.9; + } +} + +.textFieldDisabled { + &:disabled { + background-color: var(--gentle-stroke); + } +} diff --git a/src/shared/components/Form/Formik/NotionIntegration/NotionIntegration.tsx b/src/shared/components/Form/Formik/NotionIntegration/NotionIntegration.tsx new file mode 100644 index 0000000000..d79dfa6499 --- /dev/null +++ b/src/shared/components/Form/Formik/NotionIntegration/NotionIntegration.tsx @@ -0,0 +1,134 @@ +import React, { FC } from "react"; +import classNames from "classnames"; +import { useField } from "formik"; +import { CheckboxProps } from "../../Checkbox"; +import { Checkbox } from "../Checkbox"; +import { TextField, TextFieldProps } from "../TextField"; +import styles from "./NotionIntegration.module.scss"; + +export interface NotionIntegrationProps { + name: string; + className?: string; + isEnabled: CheckboxProps; + token: TextFieldProps; + databaseId: TextFieldProps; +} + +const NotionIntegration: FC = (props) => { + const { className: outerClassName, isEnabled, token, databaseId } = props; + const [ + { value: isNotionIntegrationEnabled }, + { initialValue: initialIsNotionIntegrationEnabled }, + ] = useField(isEnabled.name); + + const linkToNotionIntegrationsEl = ( + + https://www.notion.so/my-integrations + + ); + + return ( + <> + + {isNotionIntegrationEnabled && ( + <> +
+ + To set up an integration between a Notion database and this space, + please follow the following steps: + +
    +
  1. + Go to {linkToNotionIntegrationsEl}, choose{" "} + New integration, fill it up and submit. You will need to + log-in to your Notion account if you're not already logged-in. + You'll need to choose the Notion workspace that this DB is + placed in if you have several workspaces. You'll need to name it + (e.g. Common), and you can optionally add a logo for the + integration. +
  2. +
  3. + In the next page, copy the Internal Integration Secret{" "} + (click Show first) and paste it here in the first field below. +
  4. +
  5. + In the same page under Capabilities, change User + Capabilities permission to include the read of user information + including email addresses, and save changes. +
  6. +
  7. + Make sure to include in the Notion database you would like to + bridge with this space the following properties: Title, + Description (Text type), Common (URL type). Optionally you can + include also the built-in Notion properties: Created time, + Created by, and Notion ID (those properties exist and will be + used anyhow). +
  8. +
  9. + Within the database page, in the 3-dots menu (top-right corner + of the screen) choose + Add connection, followed by the name of your newly + created integration (e.g. Common) and confirm. +
  10. +
  11. + Click Share in the Notion database and copy the link to + this database. From the link you should be able to extract the + database ID and paste it here in the second field below. The + database ID is the 32-characters string between the prefix + https://www.notion.so/_workspaceName/ and the question mark. +
  12. +
+
+ + + + )} + + ); +}; + +export default NotionIntegration; diff --git a/src/shared/components/Form/Formik/NotionIntegration/index.ts b/src/shared/components/Form/Formik/NotionIntegration/index.ts new file mode 100644 index 0000000000..a4c2df547b --- /dev/null +++ b/src/shared/components/Form/Formik/NotionIntegration/index.ts @@ -0,0 +1,2 @@ +export { default as NotionIntegration } from "./NotionIntegration"; +export type { NotionIntegrationProps } from "./NotionIntegration"; diff --git a/src/shared/components/Form/Formik/index.ts b/src/shared/components/Form/Formik/index.ts index b70334a696..7b37048ad7 100644 --- a/src/shared/components/Form/Formik/index.ts +++ b/src/shared/components/Form/Formik/index.ts @@ -12,3 +12,4 @@ export * from "./TextField"; export * from "./UploadFiles"; export * from "./RolesArray"; export * from "./RolesArrayWrapper"; +export * from "./NotionIntegration"; diff --git a/src/shared/components/index.tsx b/src/shared/components/index.tsx index 0e8ec5fdbf..7c6f30b695 100644 --- a/src/shared/components/index.tsx +++ b/src/shared/components/index.tsx @@ -4,6 +4,7 @@ export * from "./ButtonIcon"; export * from "./ButtonLink"; export * from "./Common"; export * from "./CommonShare"; +export * from "./ConfirmationModal"; export { default as Content } from "./Content"; export * from "./ContributionAmountSelection"; export * from "./DatePicker"; diff --git a/src/shared/constants/endpoint.ts b/src/shared/constants/endpoint.ts index adf419e3c3..a5cc923ae7 100644 --- a/src/shared/constants/endpoint.ts +++ b/src/shared/constants/endpoint.ts @@ -31,6 +31,8 @@ export const ApiEndpoint = { MuteCommon: "/commons/mute", LeaveCommon: "/commons/leave", CreateSubscription: "/commons/immediate-contribution", + AddNotionIntegration: "commons/notion/setup", + RemoveNotionIntegration: "commons/notion/remove", UpdateSubscription: "/subscriptions/update", CancelSubscription: "/subscriptions/cancel", CreateUser: "/users/create", diff --git a/src/shared/hooks/useCases/index.ts b/src/shared/hooks/useCases/index.ts index 8be24f937c..51b09e439f 100644 --- a/src/shared/hooks/useCases/index.ts +++ b/src/shared/hooks/useCases/index.ts @@ -15,6 +15,7 @@ export { useImmediateContribution } from "./useImmediateContribution"; export { useInboxItems } from "./useInboxItems"; export { useMarkChatMessageAsSeen } from "./useMarkChatMessageAsSeen"; export { useMarkFeedItemAsSeen } from "./useMarkFeedItemAsSeen"; +export { useNotionIntegration } from "./useNotionIntegration"; export { default as usePaymentMethodChange } from "./usePaymentMethodChange"; export type { ChangePaymentMethodState } from "./usePaymentMethodChange"; export { useProjectCreation } from "./useProjectCreation"; diff --git a/src/shared/hooks/useCases/useNotionIntegration.ts b/src/shared/hooks/useCases/useNotionIntegration.ts new file mode 100644 index 0000000000..871a246c80 --- /dev/null +++ b/src/shared/hooks/useCases/useNotionIntegration.ts @@ -0,0 +1,111 @@ +import { useCallback, useEffect, useState } from "react"; +import { Logger, NotionService } from "@/services"; +import { ConfirmationModalState } from "@/shared/components"; +import { useModal } from "@/shared/hooks"; +import { NotionIntegrationIntermediate } from "@/shared/models"; + +interface Data { + projectId?: string; + isNotionIntegrationEnabled: boolean; +} + +interface Return { + isNotionIntegrationUpdated: boolean; + notionIntegrationErrorModalState: ConfirmationModalState; + disconnectNotionModalState: ConfirmationModalState; + setNotionIntegrationFormData: ( + notion?: NotionIntegrationIntermediate, + ) => void; +} + +export const useNotionIntegration = (data: Data): Return => { + const { projectId, isNotionIntegrationEnabled } = data; + const [notionData, setNotionData] = useState(); + const [isNotionIntegrationUpdated, setIsNotionIntegrationUpdated] = + useState(false); + const notionIntegrationErrorModalState = useModal(false); + const disconnectNotionModalState = useModal(false); + + const setNotionIntegrationFormData = (notion) => { + setNotionData(notion); + }; + + const setupNotionIntegration = async (projectId: string) => { + if (!notionData) { + return; + } + + try { + await NotionService.setupIntegration(projectId, { + databaseId: notionData.databaseId, + token: notionData.token, + }); + setIsNotionIntegrationUpdated(true); + } catch (error) { + Logger.error(error); + notionIntegrationErrorModalState.onOpen(); + } + }; + + const removeNotionIntegration = async (projectId?: string) => { + if (!projectId) { + onCloseDisconnectNotionModal(); + return; + } + + try { + await NotionService.removeIntegration(projectId); + } catch (error) { + Logger.error(error); + } finally { + onCloseDisconnectNotionModal(); + } + }; + + useEffect(() => { + if (!projectId) { + return; + } + + if (!isNotionIntegrationEnabled && notionData?.isEnabled) { + setupNotionIntegration(projectId); + return; + } + + if (isNotionIntegrationEnabled && notionData && !notionData.isEnabled) { + disconnectNotionModalState.onOpen(); + return; + } + + setIsNotionIntegrationUpdated(true); + }, [projectId, isNotionIntegrationEnabled, notionData]); + + const onCloseNotionIntegrationErrorModal = useCallback(() => { + setIsNotionIntegrationUpdated(true); + notionIntegrationErrorModalState.onClose(); + }, []); + + const onCloseDisconnectNotionModal = useCallback(() => { + setIsNotionIntegrationUpdated(true); + disconnectNotionModalState.onClose(); + }, []); + + const onConfirmDisconnectNotionModal = useCallback(() => { + removeNotionIntegration(projectId); + }, [projectId]); + + return { + isNotionIntegrationUpdated, + notionIntegrationErrorModalState: { + isShowing: notionIntegrationErrorModalState.isShowing, + onConfirm: onCloseNotionIntegrationErrorModal, + onClose: onCloseNotionIntegrationErrorModal, + }, + disconnectNotionModalState: { + isShowing: disconnectNotionModalState.isShowing, + onConfirm: onConfirmDisconnectNotionModal, + onClose: onCloseDisconnectNotionModal, + }, + setNotionIntegrationFormData, + }; +}; diff --git a/src/shared/icons/index.ts b/src/shared/icons/index.ts index c10cc93f3e..5fe92d3c25 100644 --- a/src/shared/icons/index.ts +++ b/src/shared/icons/index.ts @@ -34,6 +34,7 @@ export { default as LTRDirectionMarkIcon } from "./ltrDirection.icon"; export { default as MenuIcon } from "./menu.icon"; export { default as Menu2Icon } from "./menu2.icon"; export { default as MoreIcon } from "./more.icon"; +export { default as NotionIcon } from "./notion.icon"; export { OpenIcon } from "./open.icon"; export { default as PeopleGroupIcon } from "./people-group.icon"; export { PinIcon } from "./pin.icon"; diff --git a/src/shared/icons/notion.icon.tsx b/src/shared/icons/notion.icon.tsx new file mode 100644 index 0000000000..696f4d97aa --- /dev/null +++ b/src/shared/icons/notion.icon.tsx @@ -0,0 +1,34 @@ +import React, { FC } from "react"; + +interface NotionIconProps { + className?: string; + fill?: string; + width?: number; + height?: number; +} + +const NotionIcon: FC = (props) => { + const { className, fill = "#B7BCD2", width = 13, height = 13 } = props; + + return ( + + + + + ); +}; + +export default NotionIcon; diff --git a/src/shared/interfaces/CreateProjectPayload.ts b/src/shared/interfaces/CreateProjectPayload.ts index 42645c695a..a75c1d9484 100644 --- a/src/shared/interfaces/CreateProjectPayload.ts +++ b/src/shared/interfaces/CreateProjectPayload.ts @@ -1,4 +1,10 @@ -import { BaseRule, CommonLink, Roles } from "@/shared/models"; +import { + BaseRule, + CommonLink, + NotionIntegration, + NotionIntegrationIntermediate, + Roles, +} from "@/shared/models"; import { TextEditorValue } from "@/shared/ui-kit"; import { UploadFile } from "./UploadFile"; @@ -13,6 +19,7 @@ export interface CreateProjectPayload { video?: CommonLink; tags?: string[]; highestCircleId: string; + notion?: NotionIntegration; } export interface IntermediateCreateProjectPayload { @@ -25,4 +32,5 @@ export interface IntermediateCreateProjectPayload { links?: CommonLink[]; roles?: Roles; highestCircleId: string; + notion?: NotionIntegrationIntermediate; } diff --git a/src/shared/models/Common.tsx b/src/shared/models/Common.tsx index 31989caa75..1ca6ecc736 100644 --- a/src/shared/models/Common.tsx +++ b/src/shared/models/Common.tsx @@ -2,6 +2,7 @@ import firebase from "firebase/app"; import { BaseEntity } from "./BaseEntity"; import { Discussion } from "./Discussion"; import { DiscussionMessage } from "./DiscussionMessage"; +import { NotionIntegration } from "./NotionIntegration"; import { PaymentAmount } from "./Payment"; import { Proposal } from "./Proposals"; import { Timestamp } from "./Timestamp"; @@ -122,6 +123,8 @@ export interface Common extends BaseEntity { rootCommonId?: string; lastActivity?: Timestamp; + + notion?: CommonNotion; } export interface Project extends Common { @@ -197,6 +200,10 @@ export interface CommonPayment { link: string; } +export interface CommonNotion extends Omit { + title: string; +} + interface FeedItem { feedObjectId: string; pinnedAt: Time; diff --git a/src/shared/models/Discussion.tsx b/src/shared/models/Discussion.tsx index e9605fed56..c8e0434ea8 100644 --- a/src/shared/models/Discussion.tsx +++ b/src/shared/models/Discussion.tsx @@ -24,6 +24,7 @@ export interface Discussion extends BaseEntity, SoftDeleteEntity { messageCount: number; discussionMessages: DiscussionMessage[]; predefinedType?: PredefinedTypes; + notion?: DiscussionNotion; /** * A discussion can be linked to a proposal, if it does - proposalId will exist. @@ -45,6 +46,10 @@ export interface DiscussionWithHighlightedMessage extends Discussion { highlightedMessageId: string; } +export interface DiscussionNotion { + pageId: string; +} + export const isDiscussionWithHighlightedMessage = ( discussion: any, ): discussion is DiscussionWithHighlightedMessage => diff --git a/src/shared/models/NotionIntegration.ts b/src/shared/models/NotionIntegration.ts new file mode 100644 index 0000000000..3473ce2a47 --- /dev/null +++ b/src/shared/models/NotionIntegration.ts @@ -0,0 +1,8 @@ +export interface NotionIntegration { + databaseId: string; + token: string; +} + +export interface NotionIntegrationIntermediate extends NotionIntegration { + isEnabled: boolean; +} diff --git a/src/shared/models/index.tsx b/src/shared/models/index.tsx index 60a7bbbe28..d1c61c51e4 100644 --- a/src/shared/models/index.tsx +++ b/src/shared/models/index.tsx @@ -24,3 +24,4 @@ export * from "./BankAccountDetails"; export * from "./Currency"; export * from "./SupportersData"; export * from "./Timestamp"; +export * from "./NotionIntegration"; diff --git a/src/shared/ui-kit/Tooltip/components/TooltipContent/TooltipContent.module.scss b/src/shared/ui-kit/Tooltip/components/TooltipContent/TooltipContent.module.scss index a7b140b025..5c9d4d6a9f 100644 --- a/src/shared/ui-kit/Tooltip/components/TooltipContent/TooltipContent.module.scss +++ b/src/shared/ui-kit/Tooltip/components/TooltipContent/TooltipContent.module.scss @@ -1,16 +1,13 @@ @import "../../../../../constants"; .container { - --tooltip-content-color: #{$c-neutrals-600}; - --tooltip-content-bg-color: #{$c-shades-white}; - width: max-content; padding: 0.25rem 0.625rem; font-size: $xsmall; line-height: 143%; letter-spacing: 0.02em; - color: var(--tooltip-content-color); - background-color: var(--tooltip-content-bg-color); + color: var(--primary-text); + background-color: var(--secondary-background); border-radius: 0.3125rem; box-shadow: 0 0.25rem 0.9375rem rgba(0, 0, 0, 0.15259); box-sizing: border-box; @@ -21,5 +18,5 @@ width: 0.625rem; height: 0.625rem; display: flex; - color: var(--tooltip-content-bg-color); + color: var(--secondary-background); }