From 1d2dd41e94d56ee924cdb471bdb349ba8c68786a Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Mon, 24 Jul 2023 12:16:30 +0530 Subject: [PATCH 01/31] feat: create record --- definition/lib/INotion.ts | 49 +++ .../ui-kit/Element/IDatePickerElement.ts | 6 + definition/ui-kit/Element/IElementBuilder.ts | 13 +- .../Element/IMultiStaticSelectElement.ts | 7 + enum/Notion.ts | 2 + enum/modals/NotionPageOrRecord.ts | 8 + enum/modals/common/Modals.ts | 4 + enum/modals/common/NotionProperties.ts | 30 ++ src/handlers/ExecuteBlockActionHandler.ts | 314 ++++++++++++++++- src/handlers/ExecuteViewClosedHandler.ts | 13 + src/handlers/ExecuteViewSubmitHandler.ts | 206 ++++++++++- src/handlers/Handler.ts | 11 + src/helper/IsNonSelectedOptionExist.ts | 9 + src/helper/getNonSelectedOptions.ts | 58 ++++ src/helper/getPropertiesIdsObject.ts | 21 ++ src/helper/getPropertySelectedElement.ts | 191 ++++++++++ src/helper/getTitleProperty.ts | 23 ++ src/helper/message.ts | 35 ++ src/lib/ElementBuilder.ts | 68 ++++ src/lib/NotionSDK.ts | 327 ++++++++++++++++++ src/modals/common/DatePickerComponent.ts | 21 ++ src/modals/common/MultiSelectComponent.ts | 33 ++ src/modals/createPageOrRecordModal.ts | 128 ++++++- 23 files changed, 1568 insertions(+), 9 deletions(-) create mode 100644 definition/ui-kit/Element/IDatePickerElement.ts create mode 100644 definition/ui-kit/Element/IMultiStaticSelectElement.ts create mode 100644 src/helper/IsNonSelectedOptionExist.ts create mode 100644 src/helper/getNonSelectedOptions.ts create mode 100644 src/helper/getPropertiesIdsObject.ts create mode 100644 src/helper/getPropertySelectedElement.ts create mode 100644 src/helper/getTitleProperty.ts create mode 100644 src/modals/common/DatePickerComponent.ts create mode 100644 src/modals/common/MultiSelectComponent.ts diff --git a/definition/lib/INotion.ts b/definition/lib/INotion.ts index 6f76626..869e1c9 100644 --- a/definition/lib/INotion.ts +++ b/definition/lib/INotion.ts @@ -4,6 +4,9 @@ import { INotionUser, ITokenInfo } from "../authorization/IOAuth2Storage"; import { ClientError, Error } from "../../errors/Error"; import { NotionObjectTypes, NotionOwnerType } from "../../enum/Notion"; import { RichText } from "@tryfabric/martian/build/src/notion"; +import { RecordPropertyType } from "../../enum/modals/common/NotionProperties"; +import { Modals } from "../../enum/modals/common/Modals"; +import { IMessageAttachmentField } from "@rocket.chat/apps-engine/definition/messages"; export interface INotion { baseUrl: string; @@ -47,6 +50,15 @@ export interface INotionSDK extends INotion { page: IPage, prop: IPageProperties ): Promise; + retrieveDatabase( + token: string, + database_id: string + ): Promise; + createRecord( + token: string, + database: IDatabase, + properties: object + ): Promise | Error>; } export interface IParentPage { @@ -114,3 +126,40 @@ export interface IPageProperties { export interface INotionPage extends INotionDatabase { title: string; } + +export interface IDatabaseProperties { + title: IDatabaseTitle; + additionalProperties?: IDatabaseAddProperties; +} + +export interface IDatabaseTitle { + id: string; + type: NotionObjectTypes.TITLE; + name: string; +} + +export interface IDatabaseAddProperties { + [key: string]: { + id: string; + name: string; + type: RecordPropertyType; + config?: IDatabasePropertiesConfig; + }; +} + +interface IDatabasePropertiesConfig { + [Modals.OPTIONS]?: Array<{ + id: string; + color: string; + name: string; + }>; + + [Modals.GROUPS]?: Array<{ + id: string; + color: string; + name: string; + options_ids: Array; + }>; + + [NotionObjectTypes.EXPRESSION]?: string; +} diff --git a/definition/ui-kit/Element/IDatePickerElement.ts b/definition/ui-kit/Element/IDatePickerElement.ts new file mode 100644 index 0000000..fe59bb3 --- /dev/null +++ b/definition/ui-kit/Element/IDatePickerElement.ts @@ -0,0 +1,6 @@ +import { DatePickerElement } from "@rocket.chat/ui-kit"; + +export type DatePickerElementParam = Omit< + DatePickerElement, + "type" | "placeholder" | "appId" | "blockId" | "actionId" +> & { text?: string }; diff --git a/definition/ui-kit/Element/IElementBuilder.ts b/definition/ui-kit/Element/IElementBuilder.ts index cc0c00c..22f3d6c 100644 --- a/definition/ui-kit/Element/IElementBuilder.ts +++ b/definition/ui-kit/Element/IElementBuilder.ts @@ -1,4 +1,3 @@ -import { ButtonStyle } from "@rocket.chat/apps-engine/definition/uikit"; import { ButtonElement, ImageElement, @@ -6,6 +5,8 @@ import { PlainTextInputElement, OverflowElement, Option, + DatePickerElement, + MultiStaticSelectElement, } from "@rocket.chat/ui-kit"; import { ButtonParam } from "./IButtonElement"; import { ImageParam } from "./IImageElement"; @@ -15,6 +16,8 @@ import { } from "./IStaticSelectElement"; import { PlainTextInputParam } from "./IPlainTextInputElement"; import { OverflowElementParam } from "./IOverflowElement"; +import { DatePickerElementParam } from "./IDatePickerElement"; +import { MultiStaticSelectElementParam } from "./IMultiStaticSelectElement"; export interface IElementBuilder { addButton( @@ -35,6 +38,14 @@ export interface IElementBuilder { param: OverflowElementParam, interaction: ElementInteractionParam ): OverflowElement; + createDatePicker( + param: DatePickerElementParam, + interaction: ElementInteractionParam + ): DatePickerElement; + createMultiStaticSelect( + param: MultiStaticSelectElementParam, + interaction: ElementInteractionParam + ): MultiStaticSelectElement; } export type ElementInteractionParam = { blockId: string; actionId: string }; diff --git a/definition/ui-kit/Element/IMultiStaticSelectElement.ts b/definition/ui-kit/Element/IMultiStaticSelectElement.ts new file mode 100644 index 0000000..f0d8b7b --- /dev/null +++ b/definition/ui-kit/Element/IMultiStaticSelectElement.ts @@ -0,0 +1,7 @@ +import { MultiStaticSelectElement } from "@rocket.chat/ui-kit"; +export type MultiStaticSelectElementParam = Omit< + MultiStaticSelectElement, + "type" | "blockId" | "actionId" | "placeholder" | "appId" +> & { + text: string; +}; diff --git a/enum/Notion.ts b/enum/Notion.ts index a27b2d2..c01c7d6 100644 --- a/enum/Notion.ts +++ b/enum/Notion.ts @@ -45,4 +45,6 @@ export enum NotionObjectTypes { TITLE = "title", INFO = "info", NAME = "name", + PROPERTIES = "properties", + ID = "id", } diff --git a/enum/modals/NotionPageOrRecord.ts b/enum/modals/NotionPageOrRecord.ts index 2d10323..e685707 100644 --- a/enum/modals/NotionPageOrRecord.ts +++ b/enum/modals/NotionPageOrRecord.ts @@ -16,4 +16,12 @@ export enum NotionPageOrRecord { ADD_PROPERTY_ACTION = "add-property-create-page-or-record-action-id", ADD_PROPERTY_BLOCK = "add-property-create-page-or-record-action-id", ADD_PROPERTY_BUTTON_TEXT = "Add Property", + PROPERTY_PLACEHOLDER = "Select a Property", + PROPERTY_LABEL = "Property", + PROPERTY_BLOCK = "property-create-page-or-record-block-id", + PROPERTY_ACTION = "property-create-page-or-record-action-id", + REMOVE_PROPERTY_BUTTON_TEXT = "Remove Property", + REMOVE_PROPERTY_ACTION = "remove-property-create-page-or-record-action-id", + REMOVE_PROPERTY_BLOCK = "remove-property-create-page-or-record-block-id", + PROPERTY_SELECTED_BLOCK_ELEMENT = "property-selected-element-create-page-or-record-block-id", } diff --git a/enum/modals/common/Modals.ts b/enum/modals/common/Modals.ts index 8c3265b..ec0db2e 100644 --- a/enum/modals/common/Modals.ts +++ b/enum/modals/common/Modals.ts @@ -10,4 +10,8 @@ export enum Modals { MISSING = "missing", VIEWERROR = "viewError", DATA = "data", + ADDITIONAL_PROP = "additionalProperties", + GROUPS = "groups", + PROPERTY = "property", + VALUE = "value", } diff --git a/enum/modals/common/NotionProperties.ts b/enum/modals/common/NotionProperties.ts index a8ec4c0..38b1f7a 100644 --- a/enum/modals/common/NotionProperties.ts +++ b/enum/modals/common/NotionProperties.ts @@ -99,3 +99,33 @@ export enum MissingPropertyMessage { EXPRESSION = "Expression is required", OPTION_NAME = "Option Name is required", } + +export type RecordPropertyType = Exclude< + PropertyTypeValue, + | PropertyTypeValue.CREATED_TIME + | PropertyTypeValue.CREATED_BY + | PropertyTypeValue.LAST_EDITED_TIME + | PropertyTypeValue.LAST_EDITED_BY +>; + +export enum NotSupportedPropertyType { + RELATION = "relation", + ROLLUP = "rollup", +} + +export const NotSupportedPropertyTypes = [ + PropertyTypeValue.CREATED_TIME.toString(), + PropertyTypeValue.CREATED_BY.toString(), + PropertyTypeValue.LAST_EDITED_TIME.toString(), + PropertyTypeValue.LAST_EDITED_BY.toString(), + NotSupportedPropertyType.RELATION.toString(), + NotSupportedPropertyType.ROLLUP.toString(), + PropertyTypeValue.FILES.toString(), +]; + +export enum CheckboxEnum { + TRUE = "true", + FALSE = "false", + YES = "Yes", + NO = "No", +} diff --git a/src/handlers/ExecuteBlockActionHandler.ts b/src/handlers/ExecuteBlockActionHandler.ts index 165b842..4662aab 100644 --- a/src/handlers/ExecuteBlockActionHandler.ts +++ b/src/handlers/ExecuteBlockActionHandler.ts @@ -26,8 +26,11 @@ import { getDuplicatePropertyNameViewErrors } from "../helper/getDuplicatePropNa import { SearchPage } from "../../enum/modals/common/SearchPageComponent"; import { createCommentContextualBar } from "../modals/createCommentContextualBar"; import { CommentPage } from "../../enum/modals/CommentPage"; -import { NotionObjectTypes } from "../../enum/Notion"; -import { ITokenInfo } from "../../definition/authorization/IOAuth2Storage"; +import { NotionObjectTypes, NotionOwnerType } from "../../enum/Notion"; +import { + INotionUser, + ITokenInfo, +} from "../../definition/authorization/IOAuth2Storage"; import { ICommentInfo, IDatabase, @@ -41,6 +44,7 @@ import { createPageOrRecordModal } from "../modals/createPageOrRecordModal"; import { NotionPageOrRecord } from "../../enum/modals/NotionPageOrRecord"; import { NotionWorkspace } from "../../enum/modals/NotionWorkspace"; import { changeWorkspaceModal } from "../modals/changeWorkspaceModal"; +import { getPropertiesIdsObject } from "../helper/getPropertiesIdsObject"; export class ExecuteBlockActionHandler { private context: UIKitBlockInteractionContext; @@ -149,6 +153,23 @@ export class ExecuteBlockActionHandler { break; } + case NotionPageOrRecord.ADD_PROPERTY_ACTION: { + return this.handleRecordAddPropertyAction( + modalInteraction, + oAuth2Storage, + roomInteractionStorage + ); + break; + } + case NotionPageOrRecord.REMOVE_PROPERTY_ACTION: { + return this.handleRecordRemovePropertyAction( + modalInteraction, + oAuth2Storage, + roomInteractionStorage + ); + + break; + } case NotionWorkspace.CHANGE_WORKSPACE_ACTION: { return this.handleChangeWorkspaceAction( modalInteraction, @@ -187,12 +208,20 @@ export class ExecuteBlockActionHandler { SelectPropertyOptionNameAction ); + // Notion Record Property Select Action + const propertySelectAction = + NotionPageOrRecord.PROPERTY_ACTION.toString(); + const isPropertySelectAction = + actionId.startsWith(propertySelectAction); + const typeOfActionOccurred = isPropertyTypeDispatchAction ? propertyTypeSelected : isPropertyNameDispatchAction ? propertyNameEntered : isSelectOptionNameDispatchAction ? SelectPropertyOptionNameEntered + : isPropertySelectAction + ? propertySelectAction : null; switch (typeOfActionOccurred) { @@ -213,6 +242,14 @@ export class ExecuteBlockActionHandler { case DatabaseModal.SELECT_PROPERTY_OPTION_NAME: { break; } + case NotionPageOrRecord.PROPERTY_ACTION: { + return this.handleRecordPropertySelectAction( + modalInteraction, + oAuth2Storage, + roomInteractionStorage + ); + break; + } default: { } } @@ -860,6 +897,33 @@ export class ExecuteBlockActionHandler { } const database = Object as IDatabase; + + const databaseId = database.parent.database_id; + const { NotionSdk } = this.app.getUtils(); + const { access_token } = tokenInfo; + + const properties = await NotionSdk.retrieveDatabase( + access_token, + databaseId + ); + + if (properties instanceof Error) { + this.app.getLogger().error(properties.message); + return this.context.getInteractionResponder().errorResponse(); + } + + await modalInteraction.storeInputElementState( + SearchPageAndDatabase.ACTION_ID, + properties + ); + + const propertiesId = getPropertiesIdsObject(properties); + + await modalInteraction.storeInputElementState( + NotionObjectTypes.PROPERTIES, + propertiesId + ); + const modal = await createPageOrRecordModal( this.app, user, @@ -925,4 +989,250 @@ export class ExecuteBlockActionHandler { .getInteractionResponder() .updateModalViewResponse(modal); } + + private async handleRecordAddPropertyAction( + modalInteraction: ModalInteractionStorage, + oAuth2Storage: OAuth2Storage, + roomInteractionStorage: RoomInteractionStorage + ): Promise { + const { value, user } = this.context.getInteractionData(); + + const tokenInfo = await oAuth2Storage.getCurrentWorkspace(user.id); + const roomId = await roomInteractionStorage.getInteractionRoomId(); + const room = (await this.read.getRoomReader().getById(roomId)) as IRoom; + + if (!tokenInfo) { + await sendNotificationWithConnectBlock( + this.app, + user, + this.read, + this.modify, + room + ); + + return this.context.getInteractionResponder().errorResponse(); + } + + if (!value) { + return this.context.getInteractionResponder().errorResponse(); + } + + await modalInteraction.storeInteractionActionId({ + property: NotionPageOrRecord.PROPERTY_ACTION + uuid(), + }); + + const database: IDatabase = JSON.parse(value); + + const modal = await createPageOrRecordModal( + this.app, + user, + this.read, + this.persistence, + this.modify, + room, + modalInteraction, + tokenInfo, + database, + true + ); + + if (modal instanceof Error) { + this.app.getLogger().error(modal.message); + return this.context.getInteractionResponder().errorResponse(); + } + + return this.context + .getInteractionResponder() + .updateModalViewResponse(modal); + } + + private async handleRecordPropertySelectAction( + modalInteraction: ModalInteractionStorage, + oAuth2Storage: OAuth2Storage, + roomInteractionStorage: RoomInteractionStorage + ): Promise { + const { actionId, user, value } = this.context.getInteractionData(); + const tokenInfo = await oAuth2Storage.getCurrentWorkspace(user.id); + const roomId = await roomInteractionStorage.getInteractionRoomId(); + const room = (await this.read.getRoomReader().getById(roomId)) as IRoom; + + if (!tokenInfo) { + await sendNotificationWithConnectBlock( + this.app, + user, + this.read, + this.modify, + room + ); + + return this.context.getInteractionResponder().errorResponse(); + } + + if (!value) { + return this.context.getInteractionResponder().errorResponse(); + } + + const propertyElements = + await modalInteraction.getAllInteractionActionId(); + + const propertiesId = (await modalInteraction.getInputElementState( + NotionObjectTypes.PROPERTIES + )) as object; + + const actionValue: { + parent: IDatabase; + propertyObject: object; + } = JSON.parse(value); + + const { parent, propertyObject } = actionValue; + const propertyId: string = propertyObject?.[NotionObjectTypes.ID]; + const propertyType: string = propertyObject?.[NotionObjectTypes.TYPE]; + + if (propertyType.includes(PropertyTypeValue.PEOPLE.toString())) { + const { NotionSdk } = this.app.getUtils(); + const { access_token } = tokenInfo; + const users = await NotionSdk.retrieveAllUsers(access_token); + + if (users instanceof Error) { + this.app.getLogger().error(users.message); + return this.context.getInteractionResponder().errorResponse(); + } + + let people: Array = []; + + users.forEach((person) => { + if (person.type.includes(NotionOwnerType.PERSON)) { + people.push(person as INotionUser); + } + }); + + await modalInteraction.storeInputElementState( + PropertyTypeValue.PEOPLE, + { people } + ); + } + + const data = propertyElements.data; + + const index = data.findIndex((item) => { + const propertySelectActionId: string = item?.[Modals.PROPERTY]; + return propertySelectActionId === actionId; + }); + + const previouslySelectedOption: object | undefined = + data[index]?.[NotionObjectTypes.OBJECT]; + + if (previouslySelectedOption) { + const previouslySelectedOptionId: string = + previouslySelectedOption?.[NotionObjectTypes.ID]; + + propertiesId[previouslySelectedOptionId] = + !propertiesId[previouslySelectedOptionId]; + } + + propertiesId[propertyId] = true; + + data[index][NotionObjectTypes.OBJECT] = propertyObject; + + data[index][Modals.VALUE] = uuid(); + + await modalInteraction.updateInteractionActionId(data); + + await modalInteraction.storeInputElementState( + NotionObjectTypes.PROPERTIES, + propertiesId + ); + + const modal = await createPageOrRecordModal( + this.app, + user, + this.read, + this.persistence, + this.modify, + room, + modalInteraction, + tokenInfo, + parent + ); + + if (modal instanceof Error) { + this.app.getLogger().error(modal.message); + return this.context.getInteractionResponder().errorResponse(); + } + + return this.context + .getInteractionResponder() + .updateModalViewResponse(modal); + } + + private async handleRecordRemovePropertyAction( + modalInteraction: ModalInteractionStorage, + oAuth2Storage: OAuth2Storage, + roomInteractionStorage: RoomInteractionStorage + ): Promise { + const { user, value } = this.context.getInteractionData(); + const tokenInfo = await oAuth2Storage.getCurrentWorkspace(user.id); + const roomId = await roomInteractionStorage.getInteractionRoomId(); + const room = (await this.read.getRoomReader().getById(roomId)) as IRoom; + + if (!tokenInfo) { + await sendNotificationWithConnectBlock( + this.app, + user, + this.read, + this.modify, + room + ); + + return this.context.getInteractionResponder().errorResponse(); + } + + if (!value) { + return this.context.getInteractionResponder().errorResponse(); + } + + const { + parent, + propertyObject, + }: { parent: IDatabase; propertyObject: object } = JSON.parse(value); + + await modalInteraction.clearInteractionActionId(propertyObject); + + const propertyId: string | undefined = + propertyObject?.[NotionObjectTypes.OBJECT]?.[NotionObjectTypes.ID]; + + if (propertyId) { + const propertiesId = (await modalInteraction.getInputElementState( + NotionObjectTypes.PROPERTIES + )) as object; + + propertiesId[propertyId] = false; + + await modalInteraction.storeInputElementState( + NotionObjectTypes.PROPERTIES, + propertiesId + ); + } + + const modal = await createPageOrRecordModal( + this.app, + user, + this.read, + this.persistence, + this.modify, + room, + modalInteraction, + tokenInfo, + parent + ); + + if (modal instanceof Error) { + this.app.getLogger().error(modal.message); + return this.context.getInteractionResponder().errorResponse(); + } + + return this.context + .getInteractionResponder() + .updateModalViewResponse(modal); + } } diff --git a/src/handlers/ExecuteViewClosedHandler.ts b/src/handlers/ExecuteViewClosedHandler.ts index bd95e24..2c8f9c1 100644 --- a/src/handlers/ExecuteViewClosedHandler.ts +++ b/src/handlers/ExecuteViewClosedHandler.ts @@ -16,6 +16,9 @@ import { OAuth2Storage } from "../authorization/OAuth2Storage"; import { ITokenInfo } from "../../definition/authorization/IOAuth2Storage"; import { CommentPage } from "../../enum/modals/CommentPage"; import { NotionPageOrRecord } from "../../enum/modals/NotionPageOrRecord"; +import { SearchPageAndDatabase } from "../../enum/modals/common/SearchPageAndDatabaseComponent"; +import { NotionObjectTypes } from "../../enum/Notion"; +import { PropertyTypeValue } from "../../enum/modals/common/NotionProperties"; export class ExecuteViewClosedHandler { private context: UIKitViewCloseInteractionContext; @@ -75,6 +78,16 @@ export class ExecuteViewClosedHandler { await Promise.all([ modalInteraction.clearPagesOrDatabase(workspace_id), + modalInteraction.clearInputElementState( + SearchPageAndDatabase.ACTION_ID + ), + modalInteraction.clearAllInteractionActionId(), + modalInteraction.clearInputElementState( + NotionObjectTypes.PROPERTIES + ), + modalInteraction.clearInputElementState( + PropertyTypeValue.PEOPLE + ) ]); break; diff --git a/src/handlers/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index 92c8d1a..4d53546 100644 --- a/src/handlers/ExecuteViewSubmitHandler.ts +++ b/src/handlers/ExecuteViewSubmitHandler.ts @@ -14,6 +14,7 @@ import { ModalInteractionStorage } from "../storage/ModalInteraction"; import { clearAllInteraction } from "../helper/clearInteractions"; import { OAuth2Storage } from "../authorization/OAuth2Storage"; import { + sendMessageWithAttachments, sendNotification, sendNotificationWithAttachments, sendNotificationWithConnectBlock, @@ -29,10 +30,20 @@ import { IMessageAttachmentField } from "@rocket.chat/apps-engine/definition/mes import { NotionPageOrRecord } from "../../enum/modals/NotionPageOrRecord"; import { NotionObjectTypes } from "../../enum/Notion"; import { ITokenInfo } from "../../definition/authorization/IOAuth2Storage"; -import { IDatabase, IPage } from "../../definition/lib/INotion"; +import { + IDatabase, + IPage, + IParentDatabase, +} from "../../definition/lib/INotion"; import { SearchPageAndDatabase } from "../../enum/modals/common/SearchPageAndDatabaseComponent"; import { NotionWorkspace } from "../../enum/modals/NotionWorkspace"; import { getConnectPreview } from "../helper/getConnectLayout"; +import { getTitleProperty } from "../helper/getTitleProperty"; +import { markdownToRichText } from "@tryfabric/martian"; +import { + CheckboxEnum, + PropertyTypeValue, +} from "../../enum/modals/common/NotionProperties"; export class ExecuteViewSubmitHandler { private context: UIKitViewSubmitInteractionContext; @@ -258,7 +269,13 @@ export class ExecuteViewSubmitHandler { ); } - return this.handleCreationOfRecord(); + return this.handleCreationOfRecord( + tokenInfo, + room, + oAuth2Storage, + modalInteraction, + Object as IDatabase + ); } private async handleCreationOfPage( @@ -299,7 +316,69 @@ export class ExecuteViewSubmitHandler { return this.context.getInteractionResponder().successResponse(); } - private async handleCreationOfRecord(): Promise { + private async handleCreationOfRecord( + tokenInfo: ITokenInfo, + room: IRoom, + oAuth2Storage: OAuth2Storage, + modalInteraction: ModalInteractionStorage, + database: IDatabase + ): Promise { + const { view, user } = this.context.getInteractionData(); + const { state } = view; + const { NotionSdk } = this.app.getUtils(); + const { access_token, workspace_name, owner } = tokenInfo; + const username = owner.user.name; + + const properties = (await modalInteraction.getInputElementState( + SearchPageAndDatabase.ACTION_ID + )) as object; + + const propertyElements = + await modalInteraction.getAllInteractionActionId(); + + const data = await this.getPagePropParamObject( + state, + properties, + propertyElements + ); + + const createdRecord = await NotionSdk.createRecord( + access_token, + database, + data + ); + + let message: string; + + if (createdRecord instanceof Error) { + this.app.getLogger().error(createdRecord.message); + message = `🚫 Something went wrong while creating record in **${workspace_name}**.`; + await sendNotification(this.read, this.modify, user, room, { + message, + }); + } else { + const { info } = database; + const databasename = info.name; + const databaselink = info.link; + const title: string = + state?.[NotionPageOrRecord.TITLE_BLOCK]?.[ + NotionPageOrRecord.TITLE_ACTION + ]; + + message = `✨ Created **${title}** in [**${databasename}**](${databaselink})`; + + await sendMessageWithAttachments( + this.read, + this.modify, + user, + room, + { + message: message, + fields: createdRecord, + } + ); + } + return this.context.getInteractionResponder().successResponse(); } @@ -354,4 +433,125 @@ export class ExecuteViewSubmitHandler { await roomInteractionStorage.clearInteractionRoomId(); return this.context.getInteractionResponder().successResponse(); } + + private async getPagePropParamObject( + state: object | undefined, + properties: object, + propertyElements: { data: object[] } | undefined + ): Promise { + const title: string = + state?.[NotionPageOrRecord.TITLE_BLOCK]?.[ + NotionPageOrRecord.TITLE_ACTION + ]; + + const { label } = await getTitleProperty(properties); + + const data: object = { + [label]: { + [NotionObjectTypes.TITLE]: markdownToRichText(title), + }, + }; + + const propertyValues: object = + state?.[NotionPageOrRecord.PROPERTY_SELECTED_BLOCK_ELEMENT]; + + propertyElements?.data?.forEach((propertyInfo: object) => { + const propertyObject: object = + propertyInfo?.[NotionObjectTypes.OBJECT]; + const propertyName: string = + propertyObject?.[NotionObjectTypes.NAME]; + const propertyType: string = + propertyObject?.[NotionObjectTypes.TYPE]; + const actionId: string = propertyInfo?.[Modals.VALUE]; + const propertyValue: string | Array = + propertyValues?.[actionId]; + switch (propertyType) { + case PropertyTypeValue.CHECKBOX: { + data[propertyName] = { + [PropertyTypeValue.CHECKBOX]: + propertyValue == CheckboxEnum.TRUE, + }; + break; + } + case PropertyTypeValue.TEXT: { + data[propertyName] = { + [PropertyTypeValue.TEXT]: markdownToRichText( + propertyValue as string + ), + }; + break; + } + case PropertyTypeValue.NUMBER: { + data[propertyName] = { + [PropertyTypeValue.NUMBER]: Number(propertyValue), + }; + break; + } + case PropertyTypeValue.URL: { + data[propertyName] = { + [PropertyTypeValue.URL]: propertyValue, + }; + break; + } + case PropertyTypeValue.EMAIL: { + data[propertyName] = { + [PropertyTypeValue.EMAIL]: propertyValue, + }; + break; + } + case PropertyTypeValue.PHONE_NUMBER: { + data[propertyName] = { + [PropertyTypeValue.PHONE_NUMBER]: propertyValue, + }; + break; + } + case PropertyTypeValue.DATE: { + data[propertyName] = { + [PropertyTypeValue.DATE]: { + start: propertyValue, + }, + }; + break; + } + case PropertyTypeValue.SELECT: { + data[propertyName] = { + [PropertyTypeValue.SELECT]: { + name: propertyValue, + }, + }; + break; + } + case PropertyTypeValue.PEOPLE: { + const people: Array = []; + (propertyValue as Array)?.forEach((element) => { + people.push(JSON.parse(element)); + }); + data[propertyName] = { + [PropertyTypeValue.PEOPLE]: people, + }; + break; + } + case PropertyTypeValue.MULTI_SELECT: { + const multiSelect: Array = []; + (propertyValue as Array)?.forEach((element) => { + multiSelect.push({ name: element }); + }); + data[propertyName] = { + [PropertyTypeValue.MULTI_SELECT]: multiSelect, + }; + break; + } + case "status": { + data[propertyName] = { + status: { + name: propertyValue, + }, + }; + break; + } + } + }); + + return data; + } } diff --git a/src/handlers/Handler.ts b/src/handlers/Handler.ts index 0cbe083..76ec9e1 100644 --- a/src/handlers/Handler.ts +++ b/src/handlers/Handler.ts @@ -21,6 +21,9 @@ import { NotionPageOrRecord } from "../../enum/modals/NotionPageOrRecord"; import { createPageOrRecordModal } from "../modals/createPageOrRecordModal"; import { changeWorkspaceModal } from "../modals/changeWorkspaceModal"; import { NotionWorkspace } from "../../enum/modals/NotionWorkspace"; +import { SearchPageAndDatabase } from "../../enum/modals/common/SearchPageAndDatabaseComponent"; +import { NotionObjectTypes } from "../../enum/Notion"; +import { PropertyTypeValue } from "../../enum/modals/common/NotionProperties"; export class Handler implements IHandler { public app: NotionApp; @@ -207,6 +210,14 @@ export class Handler implements IHandler { await Promise.all([ this.roomInteractionStorage.storeInteractionRoomId(roomId), modalInteraction.clearPagesOrDatabase(workspace_id), + modalInteraction.clearInputElementState( + SearchPageAndDatabase.ACTION_ID + ), + modalInteraction.clearAllInteractionActionId(), + modalInteraction.clearInputElementState( + NotionObjectTypes.PROPERTIES + ), + modalInteraction.clearInputElementState(PropertyTypeValue.PEOPLE), ]); const modal = await createPageOrRecordModal( diff --git a/src/helper/IsNonSelectedOptionExist.ts b/src/helper/IsNonSelectedOptionExist.ts new file mode 100644 index 0000000..4b915af --- /dev/null +++ b/src/helper/IsNonSelectedOptionExist.ts @@ -0,0 +1,9 @@ +export function IsNonSelectedOptionExist(propertiesId: object): boolean { + for (const [id, value] of Object.entries(propertiesId)) { + if (!value) { + return true; + } + } + + return false; +} diff --git a/src/helper/getNonSelectedOptions.ts b/src/helper/getNonSelectedOptions.ts new file mode 100644 index 0000000..9056d60 --- /dev/null +++ b/src/helper/getNonSelectedOptions.ts @@ -0,0 +1,58 @@ +import { IDatabase } from "../../definition/lib/INotion"; +import { StaticSelectOptionsParam } from "../../definition/ui-kit/Element/IStaticSelectElement"; +import { NotionObjectTypes } from "../../enum/Notion"; +import { + NotSupportedPropertyTypes, + PropertyType, +} from "../../enum/modals/common/NotionProperties"; + +export function getNonSelectedOptions( + properties: object, + data: object, + parent: IDatabase, + propertiesId: object +): StaticSelectOptionsParam { + const result: StaticSelectOptionsParam = []; + const NonSupportedOptionType = [ + NotionObjectTypes.TITLE.toString(), + ...NotSupportedPropertyTypes, + ]; + + const selectedOption: object | undefined = data?.[NotionObjectTypes.OBJECT]; + + if (selectedOption) { + const selectedOptionName: string = + selectedOption?.[NotionObjectTypes.NAME]; + + result.push({ + text: selectedOptionName, + value: JSON.stringify({ + parent, + propertyObject: selectedOption, + }), + }); + } + + for (const [property] of Object.entries(properties)) { + const propertyObject: object = properties[property]; + const propertyId: string = propertyObject?.[NotionObjectTypes.ID]; + const propertyType: string = propertyObject?.[NotionObjectTypes.TYPE]; + + if ( + !NonSupportedOptionType.includes(propertyType) && + !propertiesId[propertyId] + ) { + const propertyName: string = + propertyObject?.[NotionObjectTypes.NAME]; + result.push({ + text: propertyName, + value: JSON.stringify({ + parent, + propertyObject, + }), + }); + } + } + + return result; +} diff --git a/src/helper/getPropertiesIdsObject.ts b/src/helper/getPropertiesIdsObject.ts new file mode 100644 index 0000000..10254bc --- /dev/null +++ b/src/helper/getPropertiesIdsObject.ts @@ -0,0 +1,21 @@ +import { NotionObjectTypes } from "../../enum/Notion"; +import { NotSupportedPropertyTypes } from "../../enum/modals/common/NotionProperties"; + +export function getPropertiesIdsObject(properties: object): { + [key: string]: boolean; +} { + let result = {}; + + for (const [property] of Object.entries(properties)) { + const propertyObject: object = properties[property]; + const propertyType: string = propertyObject?.[NotionObjectTypes.TYPE]; + const propertyId: string = propertyObject?.[NotionObjectTypes.ID]; + + if (!propertyType.includes(NotionObjectTypes.TITLE.toString())) { + if (!NotSupportedPropertyTypes.includes(propertyType)) { + result[propertyId] = false; + } + } + } + return result; +} diff --git a/src/helper/getPropertySelectedElement.ts b/src/helper/getPropertySelectedElement.ts new file mode 100644 index 0000000..d43f8cd --- /dev/null +++ b/src/helper/getPropertySelectedElement.ts @@ -0,0 +1,191 @@ +import { Block } from "@rocket.chat/ui-kit"; +import { NotionObjectTypes } from "../../enum/Notion"; +import { inputElementComponent } from "../modals/common/inputElementComponent"; +import { NotionApp } from "../../NotionApp"; +import { + CheckboxEnum, + PropertyTypeValue, +} from "../../enum/modals/common/NotionProperties"; +import { DropDownComponent } from "../modals/common/DropDownComponent"; +import { StaticSelectOptionsParam } from "../../definition/ui-kit/Element/IStaticSelectElement"; +import { DatePickerComponent } from "../modals/common/DatePickerComponent"; +import { Modals } from "../../enum/modals/common/Modals"; +import { MultiSelectComponent } from "../modals/common/MultiSelectComponent"; +import { ModalInteractionStorage } from "../storage/ModalInteraction"; +import { INotionUser } from "../../definition/authorization/IOAuth2Storage"; +import { NotionPageOrRecord } from "../../enum/modals/NotionPageOrRecord"; +export function getPropertySelectedElement( + app: NotionApp, + actionId: string, + item: object, + modalInteraction: ModalInteractionStorage, + allUsers?: object +): Block { + const propertyObject: object = item?.[NotionObjectTypes.OBJECT]; + const propertyType: string = propertyObject?.[NotionObjectTypes.TYPE]; + const propertyName: string = propertyObject?.[NotionObjectTypes.NAME]; + let block: Block; + switch (propertyType) { + case PropertyTypeValue.DATE: { + block = DatePickerComponent( + { + app, + label: propertyName, + }, + { + actionId, + blockId: NotionPageOrRecord.PROPERTY_SELECTED_BLOCK_ELEMENT, + } + ); + break; + } + case PropertyTypeValue.CHECKBOX: { + const placeholder: string = `Select ${propertyName}...`; + const options: StaticSelectOptionsParam = [ + { + text: CheckboxEnum.YES, + value: CheckboxEnum.TRUE, + }, + { + text: CheckboxEnum.NO, + value: CheckboxEnum.FALSE, + }, + ]; + + block = DropDownComponent( + { + app, + placeholder, + text: propertyName, + options, + }, + { + blockId: NotionPageOrRecord.PROPERTY_SELECTED_BLOCK_ELEMENT, + actionId, + } + ); + break; + } + case "status": + case PropertyTypeValue.SELECT: { + const optionsObject: object = propertyObject?.[propertyType]; + const selectOptions: Array<{ + id: string; + name: string; + color: string; + }> = optionsObject?.[Modals.OPTIONS]; + + const placeholder: string = `Select ${propertyName}...`; + const options: StaticSelectOptionsParam = selectOptions.map( + (option) => { + const { name } = option; + return { + text: name, + value: name, + }; + } + ); + + block = DropDownComponent( + { app, options, placeholder, text: propertyName }, + { + blockId: NotionPageOrRecord.PROPERTY_SELECTED_BLOCK_ELEMENT, + actionId, + } + ); + + break; + } + case PropertyTypeValue.MULTI_SELECT: { + const optionsObject: object = propertyObject?.[propertyType]; + const selectOptions: Array<{ + id: string; + name: string; + color: string; + }> = optionsObject?.[Modals.OPTIONS]; + + const placeholder: string = `Select ${propertyName}...`; + const options: StaticSelectOptionsParam = selectOptions.map( + (option) => { + const { name } = option; + return { + text: name, + value: name, + }; + } + ); + + block = MultiSelectComponent( + { + app, + placeholder, + label: propertyName, + options, + }, + { + blockId: NotionPageOrRecord.PROPERTY_SELECTED_BLOCK_ELEMENT, + actionId, + } + ); + + break; + } + case PropertyTypeValue.PEOPLE: { + const persons = allUsers as object; + const people: Array = + allUsers?.[PropertyTypeValue.PEOPLE]; + const options: StaticSelectOptionsParam = people.map((user) => { + const { object, id, name } = user; + return { + text: name as string, + value: JSON.stringify({ + object, + id, + }), + }; + }); + const placeholder = `Select ${propertyName}...`; + + block = MultiSelectComponent( + { + app, + options, + placeholder, + label: propertyName, + }, + { + blockId: NotionPageOrRecord.PROPERTY_SELECTED_BLOCK_ELEMENT, + actionId, + } + ); + + break; + } + default: { + const multiline: boolean = propertyType.includes( + PropertyTypeValue.TEXT + ) + ? true + : false; + + const placeholder: string = `Enter ${propertyName}...`; + + block = inputElementComponent( + { + app, + placeholder, + label: propertyName, + multiline, + }, + { + blockId: NotionPageOrRecord.PROPERTY_SELECTED_BLOCK_ELEMENT, + actionId, + } + ); + + break; + } + } + + return block; +} diff --git a/src/helper/getTitleProperty.ts b/src/helper/getTitleProperty.ts new file mode 100644 index 0000000..5251167 --- /dev/null +++ b/src/helper/getTitleProperty.ts @@ -0,0 +1,23 @@ +import { NotionObjectTypes } from "../../enum/Notion"; + +export async function getTitleProperty( + properties: object +): Promise<{ label: string; placeholder: string }> { + const columns = Object.keys(properties); + const firstColumn = columns[0]; + const lastColumn = columns[columns.length - 1]; + + // title at first position + if (properties[firstColumn]?.type == NotionObjectTypes.TITLE) { + return { + label: firstColumn, + placeholder: `Enter ${firstColumn}...`, + }; + } + + //title at last position + return { + label: lastColumn, + placeholder: `Enter ${lastColumn}...`, + }; +} diff --git a/src/helper/message.ts b/src/helper/message.ts index 7c5998a..c26d9d0 100644 --- a/src/helper/message.ts +++ b/src/helper/message.ts @@ -180,3 +180,38 @@ export async function sendHelperMessageOnInstall( await modify.getCreator().finish(previewBuilder); await modify.getCreator().finish(textMessageBuilder); } + +export async function sendMessageWithAttachments( + read: IRead, + modify: IModify, + user: IUser, + room: IRoom, + content: { + message?: string; + blocks?: Array; + fields: Array; + } +) { + const { message, blocks, fields } = content; + const messageBuilder = modify + .getCreator() + .startMessage() + .setSender(user) + .setRoom(room) + .setGroupable(false); + + if (message) { + messageBuilder.setText(message); + } else if (blocks) { + messageBuilder.setBlocks(blocks); + } + + messageBuilder.setAttachments([ + { + color: "#000000", + fields, + }, + ]); + + await modify.getCreator().finish(messageBuilder); +} diff --git a/src/lib/ElementBuilder.ts b/src/lib/ElementBuilder.ts index d7d7cd0..36e2bf6 100644 --- a/src/lib/ElementBuilder.ts +++ b/src/lib/ElementBuilder.ts @@ -11,6 +11,8 @@ import { PlainTextInputElement, OverflowElement, Option, + DatePickerElement, + MultiStaticSelectElement, } from "@rocket.chat/ui-kit"; import { ButtonParam } from "../../definition/ui-kit/Element/IButtonElement"; import { ImageParam } from "../../definition/ui-kit/Element/IImageElement"; @@ -20,6 +22,8 @@ import { } from "../../definition/ui-kit/Element/IStaticSelectElement"; import { PlainTextInputParam } from "../../definition/ui-kit/Element/IPlainTextInputElement"; import { OverflowElementParam } from "../../definition/ui-kit/Element/IOverflowElement"; +import { DatePickerElementParam } from "../../definition/ui-kit/Element/IDatePickerElement"; +import { MultiStaticSelectElementParam } from "../../definition/ui-kit/Element/IMultiStaticSelectElement"; export class ElementBuilder implements IElementBuilder { constructor(private readonly appId: string) {} @@ -159,4 +163,68 @@ export class ElementBuilder implements IElementBuilder { }; return overflow; } + + public createDatePicker( + param: DatePickerElementParam, + interaction: ElementInteractionParam + ): DatePickerElement { + const { initialDate, text, dispatchActionConfig } = param; + const { blockId, actionId } = interaction; + + const datePicker: DatePickerElement = { + type: BlockElementType.DATEPICKER, + ...(text + ? { + placeholder: { + type: TextObjectType.PLAIN_TEXT, + text, + }, + } + : undefined), + initialDate, + appId: this.appId, + blockId, + actionId, + dispatchActionConfig, + }; + + return datePicker; + } + + public createMultiStaticSelect( + param: MultiStaticSelectElementParam, + interaction: ElementInteractionParam + ): MultiStaticSelectElement { + const { + text, + options, + optionGroups, + maxSelectItems, + initialOption, + initialValue, + dispatchActionConfig, + confirm, + } = param; + const { blockId, actionId } = interaction; + + const multiStaticSelect: MultiStaticSelectElement = { + type: BlockElementType.MULTI_STATIC_SELECT, + placeholder: { + type: TextObjectType.PLAIN_TEXT, + text, + }, + options, + optionGroups, + maxSelectItems, + initialOption, + initialValue, + appId: this.appId, + blockId, + actionId, + dispatchActionConfig, + confirm, + }; + + return multiStaticSelect; + } } diff --git a/src/lib/NotionSDK.ts b/src/lib/NotionSDK.ts index 6b42495..1ae4eda 100644 --- a/src/lib/NotionSDK.ts +++ b/src/lib/NotionSDK.ts @@ -2,6 +2,7 @@ import { ICommentInfo, ICommentObject, IDatabase, + IDatabaseProperties, INotionDatabase, INotionPage, INotionSDK, @@ -27,6 +28,7 @@ import { ServerError, } from "../../errors/Error"; import { + Notion, NotionApi, NotionObjectTypes, NotionOwnerType, @@ -39,6 +41,13 @@ import { getMentionObject, getWhiteSpaceTextObject, } from "../helper/getNotionObject"; +import { Modals } from "../../enum/modals/common/Modals"; +import { + CheckboxEnum, + NotSupportedPropertyTypes, + PropertyTypeValue, +} from "../../enum/modals/common/NotionProperties"; +import { IMessageAttachmentField } from "@rocket.chat/apps-engine/definition/messages"; export class NotionSDK implements INotionSDK { baseUrl: string; @@ -599,4 +608,322 @@ export class NotionSDK implements INotionSDK { throw new AppsEngineException(err as string); } } + + public async retrieveDatabase( + token: string, + database_id: string + ): Promise { + try { + const response = await this.http.get( + NotionApi.CREATE_DATABASE + `/${database_id}`, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": NotionApi.CONTENT_TYPE, + "User-Agent": NotionApi.USER_AGENT, + "Notion-Version": this.NotionVersion, + }, + } + ); + + if (!response.statusCode.toString().startsWith("2")) { + return this.handleErrorResponse( + response.statusCode, + `Error While retrieving Database: `, + response.content + ); + } + + const database = response.data; + const properties = response.data?.[NotionObjectTypes.PROPERTIES]; + return properties; + } catch (err) { + throw new AppsEngineException(err as string); + } + } + + private async getRecordPropertyObject(item): Promise { + const properties: object = item?.[NotionObjectTypes.PROPERTIES]; + + let result = {}; + + for (const [property] of Object.entries(properties)) { + const propertyObject: object = properties[property]; + const propertyType: string = + propertyObject?.[NotionObjectTypes.TYPE]; + const propertyName: string = + propertyObject?.[NotionObjectTypes.NAME]; + const propertyId: string = propertyObject?.[NotionObjectTypes.ID]; + + if (propertyType.includes(NotionObjectTypes.TITLE.toString())) { + result[NotionObjectTypes.TITLE] = { + id: propertyId, + name: propertyName, + type: NotionObjectTypes.TITLE, + }; + } else { + const commonProperties = { + id: propertyId, + name: propertyName, + type: propertyType, + }; + + switch (propertyType) { + case PropertyTypeValue.MULTI_SELECT: + case PropertyTypeValue.SELECT: { + const options: Array<{ + id: string; + color: string; + name: string; + }> = propertyObject?.[propertyType]?.[Modals.OPTIONS]; + + result[Modals.ADDITIONAL_PROP][propertyName] = { + ...commonProperties, + config: { + [Modals.OPTIONS]: options, + }, + }; + break; + } + case PropertyTypeValue.FORMULA: { + const expression: string = + propertyObject?.[propertyType]?.[ + NotionObjectTypes.EXPRESSION + ]; + result[Modals.ADDITIONAL_PROP][propertyName] = { + ...commonProperties, + config: { + [NotionObjectTypes.EXPRESSION]: expression, + }, + }; + break; + } + case "status": { + const options: Array<{ + id: string; + color: string; + name: string; + }> = propertyObject?.[propertyType]?.[Modals.OPTIONS]; + + const groups: Array<{ + id: string; + name: string; + color: string; + options_ids: Array; + }> = propertyObject?.[propertyType]?.[Modals.GROUPS]; + + result[Modals.ADDITIONAL_PROP][propertyName] = { + ...commonProperties, + config: { + [Modals.OPTIONS]: options, + [Modals.GROUPS]: groups, + }, + }; + + break; + } + default: { + if (!NotSupportedPropertyTypes.includes(propertyType)) { + result[Modals.ADDITIONAL_PROP][propertyName] = { + ...commonProperties, + }; + } + break; + } + } + } + } + return result; + } + + public async createRecord( + token: string, + database: IDatabase, + properties: object + ): Promise | Error> { + try { + const { parent } = database; + const data = { + parent, + properties, + }; + + const response = await this.http.post(NotionApi.PAGES, { + data, + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": NotionApi.CONTENT_TYPE, + "User-Agent": NotionApi.USER_AGENT, + "Notion-Version": this.NotionVersion, + }, + }); + + if (!response.statusCode.toString().startsWith("2")) { + return this.handleErrorResponse( + response.statusCode, + `Error While creating Record: `, + response.content + ); + } + + const prop = response.data?.[NotionObjectTypes.PROPERTIES]; + const result = await this.getFieldsFromRecord(prop); + return result; + } catch (err) { + throw new AppsEngineException(err as string); + } + } + + private async getFieldsFromRecord(properties: object) { + const fields: Array = []; + console.log(properties); + + const propertyKeys = Object.keys(properties); + + for (let index = 0; index < propertyKeys.length; ++index) { + const propertyObject: object = properties[propertyKeys[index]]; + const propertyType: string = + propertyObject?.[NotionObjectTypes.TYPE]; + + switch (propertyType) { + case PropertyTypeValue.CHECKBOX: { + const value: boolean = propertyObject?.[propertyType]; + fields.push({ + short: true, + title: propertyKeys[index], + value: value ? CheckboxEnum.TRUE : CheckboxEnum.FALSE, + }); + break; + } + case PropertyTypeValue.TEXT: { + const value = propertyObject?.[propertyType]; + if (value?.length) { + const markdown = await this.richTextToMarkdown(value); + fields.push({ + title: propertyKeys[index], + value: markdown, + }); + } + break; + } + case PropertyTypeValue.NUMBER: { + const value: number | null = propertyObject?.[propertyType]; + if (value) { + fields.push({ + short: true, + title: propertyKeys[index], + value: value.toString(), + }); + } + break; + } + case PropertyTypeValue.URL: { + const value: string | null = propertyObject?.[propertyType]; + if (value) { + fields.push({ + short: true, + title: propertyKeys[index], + value, + }); + } + + break; + } + case PropertyTypeValue.EMAIL: { + const value: string | null = propertyObject?.[propertyType]; + if (value) { + fields.push({ + short: true, + title: propertyKeys[index], + value, + }); + } + break; + } + case PropertyTypeValue.PHONE_NUMBER: { + const value: string | null = propertyObject?.[propertyType]; + if (value) { + fields.push({ + short: true, + title: propertyKeys[index], + value, + }); + } + break; + } + case PropertyTypeValue.DATE: { + const value: object | null = propertyObject?.[propertyType]; + if (value) { + const date = value?.["start"]; + fields.push({ + short: true, + title: propertyKeys[index], + value: date, + }); + } + + break; + } + case PropertyTypeValue.SELECT: { + const value: object | null = propertyObject?.[propertyType]; + if (value) { + const selectValue = value?.[NotionObjectTypes.NAME]; + fields.push({ + short: true, + title: propertyKeys[index], + value: selectValue, + }); + } + + break; + } + case PropertyTypeValue.PEOPLE: { + const value: Array | null = + propertyObject?.[propertyType]; + let fieldValue = ""; + if (value) { + const fullLength = value.length; + value.forEach((element, index) => { + const name: string = + element?.[NotionObjectTypes.NAME]; + fieldValue += `${name}`; + + if (index < fullLength - 1) { + fieldValue += ", "; + } + }); + + fields.push({ + short: true, + title: propertyKeys[index], + value: fieldValue, + }); + } + break; + } + case PropertyTypeValue.MULTI_SELECT: { + const value: object | null = propertyObject?.[propertyType]; + if (value) { + console.log(value); + } + break; + } + case "status": { + const value: object | null = propertyObject?.[propertyType]; + if (value) { + const statusValue = value?.[NotionObjectTypes.NAME]; + fields.push({ + short: true, + title: propertyKeys[index], + value: statusValue, + }); + } + + break; + } + } + } + + return fields; + } } diff --git a/src/modals/common/DatePickerComponent.ts b/src/modals/common/DatePickerComponent.ts new file mode 100644 index 0000000..b6b51f9 --- /dev/null +++ b/src/modals/common/DatePickerComponent.ts @@ -0,0 +1,21 @@ +import { NotionApp } from "../../../NotionApp"; +import { ElementInteractionParam } from "../../../definition/ui-kit/Element/IElementBuilder"; + +export function DatePickerComponent( + { app, label }: { app: NotionApp; label: string }, + { blockId, actionId }: ElementInteractionParam +) { + const { elementBuilder, blockBuilder } = app.getUtils(); + const datePickerElement = elementBuilder.createDatePicker( + {}, + { blockId, actionId } + ); + + const inputBlock = blockBuilder.createInputBlock({ + text: label, + element: datePickerElement, + optional: false, + }); + + return inputBlock; +} diff --git a/src/modals/common/MultiSelectComponent.ts b/src/modals/common/MultiSelectComponent.ts new file mode 100644 index 0000000..c408424 --- /dev/null +++ b/src/modals/common/MultiSelectComponent.ts @@ -0,0 +1,33 @@ +import { NotionApp } from "../../../NotionApp"; +import { ElementInteractionParam } from "../../../definition/ui-kit/Element/IElementBuilder"; +import { StaticSelectOptionsParam } from "../../../definition/ui-kit/Element/IStaticSelectElement"; + +export function MultiSelectComponent( + { + app, + placeholder, + options, + label, + }: { + app: NotionApp; + placeholder: string; + options: StaticSelectOptionsParam; + label: string; + }, + { blockId, actionId }: ElementInteractionParam +) { + const { elementBuilder, blockBuilder } = app.getUtils(); + const dropDownOption = elementBuilder.createDropDownOptions(options); + const multiSelect = elementBuilder.createMultiStaticSelect( + { text: placeholder, options: dropDownOption }, + { blockId, actionId } + ); + + const inputBlock = blockBuilder.createInputBlock({ + text: label, + element: multiSelect, + optional: false, + }); + + return inputBlock; +} diff --git a/src/modals/createPageOrRecordModal.ts b/src/modals/createPageOrRecordModal.ts index 85169f8..9d71f6a 100644 --- a/src/modals/createPageOrRecordModal.ts +++ b/src/modals/createPageOrRecordModal.ts @@ -25,6 +25,14 @@ import { OverflowMenuComponent } from "./common/OverflowMenuComponent"; import { Modals } from "../../enum/modals/common/Modals"; import { IDatabase } from "../../definition/lib/INotion"; import { getSelectDatabaseLayout } from "../helper/getSelectDatabaseLayout"; +import { getTitleProperty } from "../helper/getTitleProperty"; +import { ButtonInSectionComponent } from "./common/buttonInSectionComponent"; +import { DropDownComponent } from "./common/DropDownComponent"; +import { NotionObjectTypes } from "../../enum/Notion"; +import { getNonSelectedOptions } from "../helper/getNonSelectedOptions"; +import { IsNonSelectedOptionExist } from "../helper/IsNonSelectedOptionExist"; +import { getPropertySelectedElement } from "../helper/getPropertySelectedElement"; +import { PropertyTypeValue } from "../../enum/modals/common/NotionProperties"; export async function createPageOrRecordModal( app: NotionApp, @@ -35,7 +43,8 @@ export async function createPageOrRecordModal( room: IRoom, modalInteraction: ModalInteractionStorage, tokenInfo: ITokenInfo, - parent?: IDatabase + parent?: IDatabase, + addPropertyAction?: boolean ): Promise { const { elementBuilder, blockBuilder } = app.getUtils(); const divider = blockBuilder.createDividerBlock(); @@ -43,6 +52,26 @@ export async function createPageOrRecordModal( const appId = app.getID(); const overFlowMenuText = [DatabaseModal.OVERFLOW_MENU_TEXT.toString()]; const overFlowMenuValue = [DatabaseModal.OVERFLOW_MENU_ACTION.toString()]; + let properties: object | undefined; + let addedProperty: { data: Array } | undefined; + let propertiesId: object | undefined; + let allUsers: object | undefined; + + if (parent) { + properties = await modalInteraction.getInputElementState( + SearchPageAndDatabase.ACTION_ID + ); + + addedProperty = await modalInteraction.getAllInteractionActionId(); + + propertiesId = await modalInteraction.getInputElementState( + NotionObjectTypes.PROPERTIES + ); + + allUsers = await modalInteraction.getInputElementState( + PropertyTypeValue.PEOPLE + ); + } if (parent) { overFlowMenuText.push( @@ -95,12 +124,21 @@ export async function createPageOrRecordModal( blocks.push(SelectedDatabaseLayout); } + let labelOfPageOrRecord = NotionPageOrRecord.TITLE_LABEL.toString(); + let placeholderOfPageOrRecord = + NotionPageOrRecord.TITLE_PLACEHOLDER.toString(); + + if (parent && properties) { + const { label, placeholder } = await getTitleProperty(properties); + labelOfPageOrRecord = label; + placeholderOfPageOrRecord = placeholder; + } const titleOfPageOrRecordBlock = inputElementComponent( { app, - placeholder: NotionPageOrRecord.TITLE_PLACEHOLDER, - label: NotionPageOrRecord.TITLE_LABEL, + placeholder: placeholderOfPageOrRecord, + label: labelOfPageOrRecord, optional: false, }, { @@ -111,6 +149,90 @@ export async function createPageOrRecordModal( blocks.push(titleOfPageOrRecordBlock); + if (parent && addedProperty) { + const data = addedProperty.data; + data.forEach(async (item, index) => { + if (index === 0) { + blocks.push(divider); + } + const propertySelectActionId: string = item?.[Modals.PROPERTY]; + const options = getNonSelectedOptions( + properties as object, + item, + parent, + propertiesId as object + ); + + const propertySelectBlock = DropDownComponent( + { + app, + options, + placeholder: NotionPageOrRecord.PROPERTY_PLACEHOLDER, + text: NotionPageOrRecord.PROPERTY_LABEL, + dispatchActionConfigOnSelect: true, + }, + { + blockId: NotionPageOrRecord.PROPERTY_BLOCK, + actionId: propertySelectActionId, + } + ); + + blocks.push(propertySelectBlock); + + const selectedPropertyTypeElementActionId = item?.[Modals.VALUE]; + if (selectedPropertyTypeElementActionId) { + const PropertySelectedElement = getPropertySelectedElement( + app, + selectedPropertyTypeElementActionId, + item, + modalInteraction, + allUsers + ); + + blocks.push(PropertySelectedElement); + } + + const removeButton = ButtonInSectionComponent( + { + app, + buttonText: NotionPageOrRecord.REMOVE_PROPERTY_BUTTON_TEXT, + value: JSON.stringify({ + parent, + propertyObject: item, + }), + }, + { + blockId: NotionPageOrRecord.REMOVE_PROPERTY_BLOCK, + actionId: NotionPageOrRecord.REMOVE_PROPERTY_ACTION, + } + ); + + blocks.push(removeButton); + blocks.push(divider); + }); + } + + if (parent && properties && !addPropertyAction) { + const isNonSelectedOptionExist = IsNonSelectedOptionExist( + propertiesId as object + ); + + if (isNonSelectedOptionExist) { + const addProperty = ButtonInSectionComponent( + { + app, + buttonText: NotionPageOrRecord.ADD_PROPERTY_BUTTON_TEXT, + value: JSON.stringify(parent), + }, + { + blockId: NotionPageOrRecord.ADD_PROPERTY_BLOCK, + actionId: NotionPageOrRecord.ADD_PROPERTY_ACTION, + } + ); + + blocks.push(addProperty); + } + } const submit = elementBuilder.addButton( { text: NotionPageOrRecord.CREATE, style: ButtonStyle.PRIMARY }, { From 91c5819b62c7a57ee2d76322fc8c88c1a934c9ac Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Wed, 2 Aug 2023 10:52:43 +0530 Subject: [PATCH 02/31] feat: create record improved Version --- enum/modals/NotionPageOrRecord.ts | 10 - src/handlers/ExecuteBlockActionHandler.ts | 274 +++------------------- src/handlers/ExecuteViewClosedHandler.ts | 3 - src/handlers/ExecuteViewSubmitHandler.ts | 115 +++++---- src/handlers/Handler.ts | 31 ++- src/helper/IsNonSelectedOptionExist.ts | 9 - src/helper/getNonSelectedOptions.ts | 58 ----- src/lib/NotionSDK.ts | 30 ++- src/modals/createPageOrRecordModal.ts | 69 +----- 9 files changed, 152 insertions(+), 447 deletions(-) delete mode 100644 src/helper/IsNonSelectedOptionExist.ts delete mode 100644 src/helper/getNonSelectedOptions.ts diff --git a/enum/modals/NotionPageOrRecord.ts b/enum/modals/NotionPageOrRecord.ts index e685707..0122b49 100644 --- a/enum/modals/NotionPageOrRecord.ts +++ b/enum/modals/NotionPageOrRecord.ts @@ -13,15 +13,5 @@ export enum NotionPageOrRecord { TITLE_ACTION = "title-page-or-record-action-id", CHANGE_DATABASE_TEXT = "Change Database", CHANGE_DATABASE_ACTION = "create-page-or-record-change-database-action-id", - ADD_PROPERTY_ACTION = "add-property-create-page-or-record-action-id", - ADD_PROPERTY_BLOCK = "add-property-create-page-or-record-action-id", - ADD_PROPERTY_BUTTON_TEXT = "Add Property", - PROPERTY_PLACEHOLDER = "Select a Property", - PROPERTY_LABEL = "Property", - PROPERTY_BLOCK = "property-create-page-or-record-block-id", - PROPERTY_ACTION = "property-create-page-or-record-action-id", - REMOVE_PROPERTY_BUTTON_TEXT = "Remove Property", - REMOVE_PROPERTY_ACTION = "remove-property-create-page-or-record-action-id", - REMOVE_PROPERTY_BLOCK = "remove-property-create-page-or-record-block-id", PROPERTY_SELECTED_BLOCK_ELEMENT = "property-selected-element-create-page-or-record-block-id", } diff --git a/src/handlers/ExecuteBlockActionHandler.ts b/src/handlers/ExecuteBlockActionHandler.ts index 4662aab..86bd1fe 100644 --- a/src/handlers/ExecuteBlockActionHandler.ts +++ b/src/handlers/ExecuteBlockActionHandler.ts @@ -20,7 +20,10 @@ import { createDatabaseModal } from "../modals/createDatabaseModal"; import { OAuth2Storage } from "../authorization/OAuth2Storage"; import { Error } from "../../errors/Error"; import { sendNotificationWithConnectBlock } from "../helper/message"; -import { PropertyTypeValue } from "../../enum/modals/common/NotionProperties"; +import { + NotSupportedPropertyTypes, + PropertyTypeValue, +} from "../../enum/modals/common/NotionProperties"; import { Modals } from "../../enum/modals/common/Modals"; import { getDuplicatePropertyNameViewErrors } from "../helper/getDuplicatePropNameViewError"; import { SearchPage } from "../../enum/modals/common/SearchPageComponent"; @@ -153,23 +156,6 @@ export class ExecuteBlockActionHandler { break; } - case NotionPageOrRecord.ADD_PROPERTY_ACTION: { - return this.handleRecordAddPropertyAction( - modalInteraction, - oAuth2Storage, - roomInteractionStorage - ); - break; - } - case NotionPageOrRecord.REMOVE_PROPERTY_ACTION: { - return this.handleRecordRemovePropertyAction( - modalInteraction, - oAuth2Storage, - roomInteractionStorage - ); - - break; - } case NotionWorkspace.CHANGE_WORKSPACE_ACTION: { return this.handleChangeWorkspaceAction( modalInteraction, @@ -208,20 +194,12 @@ export class ExecuteBlockActionHandler { SelectPropertyOptionNameAction ); - // Notion Record Property Select Action - const propertySelectAction = - NotionPageOrRecord.PROPERTY_ACTION.toString(); - const isPropertySelectAction = - actionId.startsWith(propertySelectAction); - const typeOfActionOccurred = isPropertyTypeDispatchAction ? propertyTypeSelected : isPropertyNameDispatchAction ? propertyNameEntered : isSelectOptionNameDispatchAction ? SelectPropertyOptionNameEntered - : isPropertySelectAction - ? propertySelectAction : null; switch (typeOfActionOccurred) { @@ -242,14 +220,6 @@ export class ExecuteBlockActionHandler { case DatabaseModal.SELECT_PROPERTY_OPTION_NAME: { break; } - case NotionPageOrRecord.PROPERTY_ACTION: { - return this.handleRecordPropertySelectAction( - modalInteraction, - oAuth2Storage, - roomInteractionStorage - ); - break; - } default: { } } @@ -917,12 +887,7 @@ export class ExecuteBlockActionHandler { properties ); - const propertiesId = getPropertiesIdsObject(properties); - - await modalInteraction.storeInputElementState( - NotionObjectTypes.PROPERTIES, - propertiesId - ); + await this.storeAllElements(properties, tokenInfo, modalInteraction); const modal = await createPageOrRecordModal( this.app, @@ -952,7 +917,6 @@ export class ExecuteBlockActionHandler { roomInteractionStorage: RoomInteractionStorage ): Promise { const { value, user, triggerId } = this.context.getInteractionData(); - const tokenInfo = await oAuth2Storage.getCurrentWorkspace(user.id); const roomId = await roomInteractionStorage.getInteractionRoomId(); const room = (await this.read.getRoomReader().getById(roomId)) as IRoom; @@ -990,12 +954,12 @@ export class ExecuteBlockActionHandler { .updateModalViewResponse(modal); } - private async handleRecordAddPropertyAction( + private async handleChangeWorkspaceAction( modalInteraction: ModalInteractionStorage, oAuth2Storage: OAuth2Storage, roomInteractionStorage: RoomInteractionStorage ): Promise { - const { value, user } = this.context.getInteractionData(); + const { value, user, triggerId } = this.context.getInteractionData(); const tokenInfo = await oAuth2Storage.getCurrentWorkspace(user.id); const roomId = await roomInteractionStorage.getInteractionRoomId(); @@ -1009,7 +973,6 @@ export class ExecuteBlockActionHandler { this.modify, room ); - return this.context.getInteractionResponder().errorResponse(); } @@ -1017,13 +980,9 @@ export class ExecuteBlockActionHandler { return this.context.getInteractionResponder().errorResponse(); } - await modalInteraction.storeInteractionActionId({ - property: NotionPageOrRecord.PROPERTY_ACTION + uuid(), - }); - - const database: IDatabase = JSON.parse(value); + const changedTokenInfo: ITokenInfo = JSON.parse(value); - const modal = await createPageOrRecordModal( + const modal = await changeWorkspaceModal( this.app, user, this.read, @@ -1031,208 +990,37 @@ export class ExecuteBlockActionHandler { this.modify, room, modalInteraction, - tokenInfo, - database, - true + changedTokenInfo ); - if (modal instanceof Error) { - this.app.getLogger().error(modal.message); - return this.context.getInteractionResponder().errorResponse(); - } - return this.context .getInteractionResponder() .updateModalViewResponse(modal); } - private async handleRecordPropertySelectAction( - modalInteraction: ModalInteractionStorage, - oAuth2Storage: OAuth2Storage, - roomInteractionStorage: RoomInteractionStorage - ): Promise { - const { actionId, user, value } = this.context.getInteractionData(); - const tokenInfo = await oAuth2Storage.getCurrentWorkspace(user.id); - const roomId = await roomInteractionStorage.getInteractionRoomId(); - const room = (await this.read.getRoomReader().getById(roomId)) as IRoom; - - if (!tokenInfo) { - await sendNotificationWithConnectBlock( - this.app, - user, - this.read, - this.modify, - room - ); - - return this.context.getInteractionResponder().errorResponse(); - } - - if (!value) { - return this.context.getInteractionResponder().errorResponse(); - } - - const propertyElements = - await modalInteraction.getAllInteractionActionId(); - - const propertiesId = (await modalInteraction.getInputElementState( - NotionObjectTypes.PROPERTIES - )) as object; - - const actionValue: { - parent: IDatabase; - propertyObject: object; - } = JSON.parse(value); - - const { parent, propertyObject } = actionValue; - const propertyId: string = propertyObject?.[NotionObjectTypes.ID]; - const propertyType: string = propertyObject?.[NotionObjectTypes.TYPE]; - - if (propertyType.includes(PropertyTypeValue.PEOPLE.toString())) { - const { NotionSdk } = this.app.getUtils(); - const { access_token } = tokenInfo; - const users = await NotionSdk.retrieveAllUsers(access_token); - - if (users instanceof Error) { - this.app.getLogger().error(users.message); - return this.context.getInteractionResponder().errorResponse(); - } - - let people: Array = []; - - users.forEach((person) => { - if (person.type.includes(NotionOwnerType.PERSON)) { - people.push(person as INotionUser); + private async storeAllElements( + properties, + tokenInfo: ITokenInfo, + modalInteraction: ModalInteractionStorage + ) { + const result: Array = []; + + for (const [property] of Object.entries(properties)) { + const propertyObject: object = properties[property]; + const propertyType: string = + propertyObject?.[NotionObjectTypes.TYPE]; + const propertyId: string = propertyObject?.[NotionObjectTypes.ID]; + + if (!propertyType.includes(NotionObjectTypes.TITLE.toString())) { + if (!NotSupportedPropertyTypes.includes(propertyType)) { + result.push({ + value: uuid(), + object: propertyObject, + }); } - }); - - await modalInteraction.storeInputElementState( - PropertyTypeValue.PEOPLE, - { people } - ); - } - - const data = propertyElements.data; - - const index = data.findIndex((item) => { - const propertySelectActionId: string = item?.[Modals.PROPERTY]; - return propertySelectActionId === actionId; - }); - - const previouslySelectedOption: object | undefined = - data[index]?.[NotionObjectTypes.OBJECT]; - - if (previouslySelectedOption) { - const previouslySelectedOptionId: string = - previouslySelectedOption?.[NotionObjectTypes.ID]; - - propertiesId[previouslySelectedOptionId] = - !propertiesId[previouslySelectedOptionId]; - } - - propertiesId[propertyId] = true; - - data[index][NotionObjectTypes.OBJECT] = propertyObject; - - data[index][Modals.VALUE] = uuid(); - - await modalInteraction.updateInteractionActionId(data); - - await modalInteraction.storeInputElementState( - NotionObjectTypes.PROPERTIES, - propertiesId - ); - - const modal = await createPageOrRecordModal( - this.app, - user, - this.read, - this.persistence, - this.modify, - room, - modalInteraction, - tokenInfo, - parent - ); - - if (modal instanceof Error) { - this.app.getLogger().error(modal.message); - return this.context.getInteractionResponder().errorResponse(); - } - - return this.context - .getInteractionResponder() - .updateModalViewResponse(modal); - } - - private async handleRecordRemovePropertyAction( - modalInteraction: ModalInteractionStorage, - oAuth2Storage: OAuth2Storage, - roomInteractionStorage: RoomInteractionStorage - ): Promise { - const { user, value } = this.context.getInteractionData(); - const tokenInfo = await oAuth2Storage.getCurrentWorkspace(user.id); - const roomId = await roomInteractionStorage.getInteractionRoomId(); - const room = (await this.read.getRoomReader().getById(roomId)) as IRoom; - - if (!tokenInfo) { - await sendNotificationWithConnectBlock( - this.app, - user, - this.read, - this.modify, - room - ); - - return this.context.getInteractionResponder().errorResponse(); - } - - if (!value) { - return this.context.getInteractionResponder().errorResponse(); - } - - const { - parent, - propertyObject, - }: { parent: IDatabase; propertyObject: object } = JSON.parse(value); - - await modalInteraction.clearInteractionActionId(propertyObject); - - const propertyId: string | undefined = - propertyObject?.[NotionObjectTypes.OBJECT]?.[NotionObjectTypes.ID]; - - if (propertyId) { - const propertiesId = (await modalInteraction.getInputElementState( - NotionObjectTypes.PROPERTIES - )) as object; - - propertiesId[propertyId] = false; - - await modalInteraction.storeInputElementState( - NotionObjectTypes.PROPERTIES, - propertiesId - ); - } - - const modal = await createPageOrRecordModal( - this.app, - user, - this.read, - this.persistence, - this.modify, - room, - modalInteraction, - tokenInfo, - parent - ); - - if (modal instanceof Error) { - this.app.getLogger().error(modal.message); - return this.context.getInteractionResponder().errorResponse(); + } } - return this.context - .getInteractionResponder() - .updateModalViewResponse(modal); + await modalInteraction.updateInteractionActionId(result); } } diff --git a/src/handlers/ExecuteViewClosedHandler.ts b/src/handlers/ExecuteViewClosedHandler.ts index 2c8f9c1..dad7c6c 100644 --- a/src/handlers/ExecuteViewClosedHandler.ts +++ b/src/handlers/ExecuteViewClosedHandler.ts @@ -82,9 +82,6 @@ export class ExecuteViewClosedHandler { SearchPageAndDatabase.ACTION_ID ), modalInteraction.clearAllInteractionActionId(), - modalInteraction.clearInputElementState( - NotionObjectTypes.PROPERTIES - ), modalInteraction.clearInputElementState( PropertyTypeValue.PEOPLE ) diff --git a/src/handlers/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index 4d53546..7e95942 100644 --- a/src/handlers/ExecuteViewSubmitHandler.ts +++ b/src/handlers/ExecuteViewSubmitHandler.ts @@ -463,8 +463,9 @@ export class ExecuteViewSubmitHandler { const propertyType: string = propertyObject?.[NotionObjectTypes.TYPE]; const actionId: string = propertyInfo?.[Modals.VALUE]; - const propertyValue: string | Array = + const propertyValue: string | Array | undefined = propertyValues?.[actionId]; + switch (propertyType) { case PropertyTypeValue.CHECKBOX: { data[propertyName] = { @@ -474,79 +475,105 @@ export class ExecuteViewSubmitHandler { break; } case PropertyTypeValue.TEXT: { - data[propertyName] = { - [PropertyTypeValue.TEXT]: markdownToRichText( - propertyValue as string - ), - }; + if (propertyValue) { + data[propertyName] = { + [PropertyTypeValue.TEXT]: markdownToRichText( + propertyValue as string + ), + }; + } break; } case PropertyTypeValue.NUMBER: { - data[propertyName] = { - [PropertyTypeValue.NUMBER]: Number(propertyValue), - }; + if (propertyValue) { + data[propertyName] = { + [PropertyTypeValue.NUMBER]: Number(propertyValue), + }; + } + break; } case PropertyTypeValue.URL: { data[propertyName] = { - [PropertyTypeValue.URL]: propertyValue, + [PropertyTypeValue.URL]: propertyValue + ? propertyValue + : null, }; break; } case PropertyTypeValue.EMAIL: { - data[propertyName] = { - [PropertyTypeValue.EMAIL]: propertyValue, - }; + if (propertyValue) { + data[propertyName] = { + [PropertyTypeValue.EMAIL]: propertyValue, + }; + } + break; } case PropertyTypeValue.PHONE_NUMBER: { - data[propertyName] = { - [PropertyTypeValue.PHONE_NUMBER]: propertyValue, - }; + if (propertyValue) { + data[propertyName] = { + [PropertyTypeValue.PHONE_NUMBER]: propertyValue, + }; + } + break; } case PropertyTypeValue.DATE: { - data[propertyName] = { - [PropertyTypeValue.DATE]: { - start: propertyValue, - }, - }; + if (propertyValue) { + data[propertyName] = { + [PropertyTypeValue.DATE]: { + start: propertyValue, + }, + }; + } + break; } case PropertyTypeValue.SELECT: { - data[propertyName] = { - [PropertyTypeValue.SELECT]: { - name: propertyValue, - }, - }; + if (propertyValue) { + data[propertyName] = { + [PropertyTypeValue.SELECT]: { + name: propertyValue, + }, + }; + } + break; } case PropertyTypeValue.PEOPLE: { const people: Array = []; - (propertyValue as Array)?.forEach((element) => { - people.push(JSON.parse(element)); - }); - data[propertyName] = { - [PropertyTypeValue.PEOPLE]: people, - }; + if (propertyValue) { + (propertyValue as Array)?.forEach((element) => { + people.push(JSON.parse(element)); + }); + data[propertyName] = { + [PropertyTypeValue.PEOPLE]: people, + }; + } break; } case PropertyTypeValue.MULTI_SELECT: { - const multiSelect: Array = []; - (propertyValue as Array)?.forEach((element) => { - multiSelect.push({ name: element }); - }); - data[propertyName] = { - [PropertyTypeValue.MULTI_SELECT]: multiSelect, - }; + if (propertyValue) { + const multiSelect: Array = []; + (propertyValue as Array)?.forEach((element) => { + multiSelect.push({ name: element }); + }); + data[propertyName] = { + [PropertyTypeValue.MULTI_SELECT]: multiSelect, + }; + } break; } case "status": { - data[propertyName] = { - status: { - name: propertyValue, - }, - }; + if (propertyValue) { + data[propertyName] = { + status: { + name: propertyValue, + }, + }; + } + break; } } diff --git a/src/handlers/Handler.ts b/src/handlers/Handler.ts index 76ec9e1..1ce3d8f 100644 --- a/src/handlers/Handler.ts +++ b/src/handlers/Handler.ts @@ -22,8 +22,9 @@ import { createPageOrRecordModal } from "../modals/createPageOrRecordModal"; import { changeWorkspaceModal } from "../modals/changeWorkspaceModal"; import { NotionWorkspace } from "../../enum/modals/NotionWorkspace"; import { SearchPageAndDatabase } from "../../enum/modals/common/SearchPageAndDatabaseComponent"; -import { NotionObjectTypes } from "../../enum/Notion"; +import { NotionObjectTypes, NotionOwnerType } from "../../enum/Notion"; import { PropertyTypeValue } from "../../enum/modals/common/NotionProperties"; +import { INotionUser } from "../../definition/authorization/IOAuth2Storage"; export class Handler implements IHandler { public app: NotionApp; @@ -205,7 +206,7 @@ export class Handler implements IHandler { NotionPageOrRecord.VIEW_ID ); - const { workspace_id } = tokenInfo; + const { workspace_id, access_token } = tokenInfo; await Promise.all([ this.roomInteractionStorage.storeInteractionRoomId(roomId), @@ -214,10 +215,6 @@ export class Handler implements IHandler { SearchPageAndDatabase.ACTION_ID ), modalInteraction.clearAllInteractionActionId(), - modalInteraction.clearInputElementState( - NotionObjectTypes.PROPERTIES - ), - modalInteraction.clearInputElementState(PropertyTypeValue.PEOPLE), ]); const modal = await createPageOrRecordModal( @@ -257,6 +254,28 @@ export class Handler implements IHandler { .openSurfaceView(modal, { triggerId }, this.sender); } + const { NotionSdk } = this.app.getUtils(); + const users = await NotionSdk.retrieveAllUsers(access_token); + + if (users instanceof Error) { + this.app.getLogger().error(users.message); + } else { + let people: Array = []; + + users.forEach((person) => { + if (person.type.includes(NotionOwnerType.PERSON)) { + people.push(person as INotionUser); + } + }); + + await modalInteraction.storeInputElementState( + PropertyTypeValue.PEOPLE, + { + people, + } + ); + } + return; } diff --git a/src/helper/IsNonSelectedOptionExist.ts b/src/helper/IsNonSelectedOptionExist.ts deleted file mode 100644 index 4b915af..0000000 --- a/src/helper/IsNonSelectedOptionExist.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function IsNonSelectedOptionExist(propertiesId: object): boolean { - for (const [id, value] of Object.entries(propertiesId)) { - if (!value) { - return true; - } - } - - return false; -} diff --git a/src/helper/getNonSelectedOptions.ts b/src/helper/getNonSelectedOptions.ts deleted file mode 100644 index 9056d60..0000000 --- a/src/helper/getNonSelectedOptions.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { IDatabase } from "../../definition/lib/INotion"; -import { StaticSelectOptionsParam } from "../../definition/ui-kit/Element/IStaticSelectElement"; -import { NotionObjectTypes } from "../../enum/Notion"; -import { - NotSupportedPropertyTypes, - PropertyType, -} from "../../enum/modals/common/NotionProperties"; - -export function getNonSelectedOptions( - properties: object, - data: object, - parent: IDatabase, - propertiesId: object -): StaticSelectOptionsParam { - const result: StaticSelectOptionsParam = []; - const NonSupportedOptionType = [ - NotionObjectTypes.TITLE.toString(), - ...NotSupportedPropertyTypes, - ]; - - const selectedOption: object | undefined = data?.[NotionObjectTypes.OBJECT]; - - if (selectedOption) { - const selectedOptionName: string = - selectedOption?.[NotionObjectTypes.NAME]; - - result.push({ - text: selectedOptionName, - value: JSON.stringify({ - parent, - propertyObject: selectedOption, - }), - }); - } - - for (const [property] of Object.entries(properties)) { - const propertyObject: object = properties[property]; - const propertyId: string = propertyObject?.[NotionObjectTypes.ID]; - const propertyType: string = propertyObject?.[NotionObjectTypes.TYPE]; - - if ( - !NonSupportedOptionType.includes(propertyType) && - !propertiesId[propertyId] - ) { - const propertyName: string = - propertyObject?.[NotionObjectTypes.NAME]; - result.push({ - text: propertyName, - value: JSON.stringify({ - parent, - propertyObject, - }), - }); - } - } - - return result; -} diff --git a/src/lib/NotionSDK.ts b/src/lib/NotionSDK.ts index 1ae4eda..7024248 100644 --- a/src/lib/NotionSDK.ts +++ b/src/lib/NotionSDK.ts @@ -776,8 +776,6 @@ export class NotionSDK implements INotionSDK { private async getFieldsFromRecord(properties: object) { const fields: Array = []; - console.log(properties); - const propertyKeys = Object.keys(properties); for (let index = 0; index < propertyKeys.length; ++index) { @@ -881,7 +879,7 @@ export class NotionSDK implements INotionSDK { const value: Array | null = propertyObject?.[propertyType]; let fieldValue = ""; - if (value) { + if (value && value.length) { const fullLength = value.length; value.forEach((element, index) => { const name: string = @@ -902,9 +900,29 @@ export class NotionSDK implements INotionSDK { break; } case PropertyTypeValue.MULTI_SELECT: { - const value: object | null = propertyObject?.[propertyType]; - if (value) { - console.log(value); + const value: Array<{ + id: string; + name: string; + color: string; + }> | null = propertyObject?.[propertyType]; + if (value && value.length) { + const fullLength = value.length; + let MultiSelectValue = ""; + value.forEach((element, index) => { + const name: string = + element?.[NotionObjectTypes.NAME]; + MultiSelectValue += `${name}`; + + if (index < fullLength - 1) { + MultiSelectValue += ", "; + } + }); + + fields.push({ + short: true, + title: propertyKeys[index], + value: MultiSelectValue, + }); } break; } diff --git a/src/modals/createPageOrRecordModal.ts b/src/modals/createPageOrRecordModal.ts index 9d71f6a..d6cd623 100644 --- a/src/modals/createPageOrRecordModal.ts +++ b/src/modals/createPageOrRecordModal.ts @@ -29,8 +29,6 @@ import { getTitleProperty } from "../helper/getTitleProperty"; import { ButtonInSectionComponent } from "./common/buttonInSectionComponent"; import { DropDownComponent } from "./common/DropDownComponent"; import { NotionObjectTypes } from "../../enum/Notion"; -import { getNonSelectedOptions } from "../helper/getNonSelectedOptions"; -import { IsNonSelectedOptionExist } from "../helper/IsNonSelectedOptionExist"; import { getPropertySelectedElement } from "../helper/getPropertySelectedElement"; import { PropertyTypeValue } from "../../enum/modals/common/NotionProperties"; @@ -54,7 +52,6 @@ export async function createPageOrRecordModal( const overFlowMenuValue = [DatabaseModal.OVERFLOW_MENU_ACTION.toString()]; let properties: object | undefined; let addedProperty: { data: Array } | undefined; - let propertiesId: object | undefined; let allUsers: object | undefined; if (parent) { @@ -64,10 +61,6 @@ export async function createPageOrRecordModal( addedProperty = await modalInteraction.getAllInteractionActionId(); - propertiesId = await modalInteraction.getInputElementState( - NotionObjectTypes.PROPERTIES - ); - allUsers = await modalInteraction.getInputElementState( PropertyTypeValue.PEOPLE ); @@ -130,7 +123,7 @@ export async function createPageOrRecordModal( if (parent && properties) { const { label, placeholder } = await getTitleProperty(properties); - labelOfPageOrRecord = label; + labelOfPageOrRecord = `${label} *`; placeholderOfPageOrRecord = placeholder; } @@ -155,29 +148,6 @@ export async function createPageOrRecordModal( if (index === 0) { blocks.push(divider); } - const propertySelectActionId: string = item?.[Modals.PROPERTY]; - const options = getNonSelectedOptions( - properties as object, - item, - parent, - propertiesId as object - ); - - const propertySelectBlock = DropDownComponent( - { - app, - options, - placeholder: NotionPageOrRecord.PROPERTY_PLACEHOLDER, - text: NotionPageOrRecord.PROPERTY_LABEL, - dispatchActionConfigOnSelect: true, - }, - { - blockId: NotionPageOrRecord.PROPERTY_BLOCK, - actionId: propertySelectActionId, - } - ); - - blocks.push(propertySelectBlock); const selectedPropertyTypeElementActionId = item?.[Modals.VALUE]; if (selectedPropertyTypeElementActionId) { @@ -192,47 +162,10 @@ export async function createPageOrRecordModal( blocks.push(PropertySelectedElement); } - const removeButton = ButtonInSectionComponent( - { - app, - buttonText: NotionPageOrRecord.REMOVE_PROPERTY_BUTTON_TEXT, - value: JSON.stringify({ - parent, - propertyObject: item, - }), - }, - { - blockId: NotionPageOrRecord.REMOVE_PROPERTY_BLOCK, - actionId: NotionPageOrRecord.REMOVE_PROPERTY_ACTION, - } - ); - - blocks.push(removeButton); blocks.push(divider); }); } - if (parent && properties && !addPropertyAction) { - const isNonSelectedOptionExist = IsNonSelectedOptionExist( - propertiesId as object - ); - - if (isNonSelectedOptionExist) { - const addProperty = ButtonInSectionComponent( - { - app, - buttonText: NotionPageOrRecord.ADD_PROPERTY_BUTTON_TEXT, - value: JSON.stringify(parent), - }, - { - blockId: NotionPageOrRecord.ADD_PROPERTY_BLOCK, - actionId: NotionPageOrRecord.ADD_PROPERTY_ACTION, - } - ); - - blocks.push(addProperty); - } - } const submit = elementBuilder.addButton( { text: NotionPageOrRecord.CREATE, style: ButtonStyle.PRIMARY }, { From e089eec5482d669312255d6b93d41fee2ddb6046 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Mon, 21 Aug 2023 18:53:30 +0530 Subject: [PATCH 03/31] fix: Required Property Closing Modal onSubmit with ViewErrors --- src/handlers/ExecuteBlockActionHandler.ts | 44 -------------- src/handlers/ExecuteViewSubmitHandler.ts | 70 +++++++++++++++++++---- 2 files changed, 60 insertions(+), 54 deletions(-) diff --git a/src/handlers/ExecuteBlockActionHandler.ts b/src/handlers/ExecuteBlockActionHandler.ts index 86bd1fe..2ca3513 100644 --- a/src/handlers/ExecuteBlockActionHandler.ts +++ b/src/handlers/ExecuteBlockActionHandler.ts @@ -954,50 +954,6 @@ export class ExecuteBlockActionHandler { .updateModalViewResponse(modal); } - private async handleChangeWorkspaceAction( - modalInteraction: ModalInteractionStorage, - oAuth2Storage: OAuth2Storage, - roomInteractionStorage: RoomInteractionStorage - ): Promise { - const { value, user, triggerId } = this.context.getInteractionData(); - - const tokenInfo = await oAuth2Storage.getCurrentWorkspace(user.id); - const roomId = await roomInteractionStorage.getInteractionRoomId(); - const room = (await this.read.getRoomReader().getById(roomId)) as IRoom; - - if (!tokenInfo) { - await sendNotificationWithConnectBlock( - this.app, - user, - this.read, - this.modify, - room - ); - return this.context.getInteractionResponder().errorResponse(); - } - - if (!value) { - return this.context.getInteractionResponder().errorResponse(); - } - - const changedTokenInfo: ITokenInfo = JSON.parse(value); - - const modal = await changeWorkspaceModal( - this.app, - user, - this.read, - this.persistence, - this.modify, - room, - modalInteraction, - changedTokenInfo - ); - - return this.context - .getInteractionResponder() - .updateModalViewResponse(modal); - } - private async storeAllElements( properties, tokenInfo: ITokenInfo, diff --git a/src/handlers/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index 7e95942..ff690c0 100644 --- a/src/handlers/ExecuteViewSubmitHandler.ts +++ b/src/handlers/ExecuteViewSubmitHandler.ts @@ -1,4 +1,5 @@ import { + BlockType, IUIKitResponse, UIKitViewSubmitInteractionContext, } from "@rocket.chat/apps-engine/definition/uikit"; @@ -32,8 +33,7 @@ import { NotionObjectTypes } from "../../enum/Notion"; import { ITokenInfo } from "../../definition/authorization/IOAuth2Storage"; import { IDatabase, - IPage, - IParentDatabase, + IPage } from "../../definition/lib/INotion"; import { SearchPageAndDatabase } from "../../enum/modals/common/SearchPageAndDatabaseComponent"; import { NotionWorkspace } from "../../enum/modals/NotionWorkspace"; @@ -44,6 +44,7 @@ import { CheckboxEnum, PropertyTypeValue, } from "../../enum/modals/common/NotionProperties"; +import { Block } from "@rocket.chat/ui-kit"; export class ExecuteViewSubmitHandler { private context: UIKitViewSubmitInteractionContext; @@ -232,7 +233,7 @@ export class ExecuteViewSubmitHandler { modalInteraction: ModalInteractionStorage ): Promise { const { user, view } = this.context.getInteractionData(); - const { state } = view; + const { state, blocks } = view; const tokenInfo = await oAuth2Storage.getCurrentWorkspace(user.id); @@ -248,15 +249,64 @@ export class ExecuteViewSubmitHandler { } // handle missing properties later - - const Object: IPage | IDatabase = JSON.parse( + const pageSelectState: string | undefined = state?.[SearchPageAndDatabase.BLOCK_ID]?.[ SearchPageAndDatabase.ACTION_ID - ] - ); + ]; + + const missingObject = {}; + + const title: string | undefined = + state?.[NotionPageOrRecord.TITLE_BLOCK]?.[ + NotionPageOrRecord.TITLE_ACTION + ]; - const { parent } = Object; + if (!title) { + if (!pageSelectState) { + missingObject[NotionPageOrRecord.TITLE_ACTION] = + "Please Provide a Title"; + } else { + const titleBlockDatabaseSelected = blocks[2] as Block; + let titleViewError; + + if (titleBlockDatabaseSelected.type == BlockType.INPUT) { + if ( + titleBlockDatabaseSelected.element.type === + "plain_text_input" + ) { + titleViewError = titleBlockDatabaseSelected?.[ + "label" + ]?.[NotionObjectTypes.TEXT] as string; + } else { + const titleBlock = blocks[3] as Block; + titleViewError = titleBlock?.["label"]?.[ + NotionObjectTypes.TEXT + ] as string; + } + } + missingObject[ + NotionPageOrRecord.TITLE_ACTION + ] = `Please Provide ${titleViewError}`; + } + } + + if (!pageSelectState) { + missingObject[SearchPageAndDatabase.ACTION_ID] = + "Please Select a Page or Database"; + } + + if (Object.keys(missingObject).length) { + return this.context.getInteractionResponder().viewErrorResponse({ + viewId: view.id, + errors: missingObject, + }); + } + + const Objects: IPage | IDatabase = JSON.parse( + pageSelectState as string + ); + const { parent } = Objects; const parentType: string = parent.type; if (parentType.includes(NotionObjectTypes.PAGE_ID)) { @@ -265,7 +315,7 @@ export class ExecuteViewSubmitHandler { room, oAuth2Storage, modalInteraction, - Object as IPage + Objects as IPage ); } @@ -274,7 +324,7 @@ export class ExecuteViewSubmitHandler { room, oAuth2Storage, modalInteraction, - Object as IDatabase + Objects as IDatabase ); } From 7fe1082fde1a632d67c41488ce53a0cf02479314 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 22 Aug 2023 11:56:48 +0530 Subject: [PATCH 04/31] fix: variable naming to missingPropObject --- src/handlers/ExecuteViewSubmitHandler.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/handlers/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index ff690c0..2db08b7 100644 --- a/src/handlers/ExecuteViewSubmitHandler.ts +++ b/src/handlers/ExecuteViewSubmitHandler.ts @@ -254,7 +254,7 @@ export class ExecuteViewSubmitHandler { SearchPageAndDatabase.ACTION_ID ]; - const missingObject = {}; + const missingPropObject = {}; const title: string | undefined = state?.[NotionPageOrRecord.TITLE_BLOCK]?.[ @@ -263,7 +263,7 @@ export class ExecuteViewSubmitHandler { if (!title) { if (!pageSelectState) { - missingObject[NotionPageOrRecord.TITLE_ACTION] = + missingPropObject[NotionPageOrRecord.TITLE_ACTION] = "Please Provide a Title"; } else { const titleBlockDatabaseSelected = blocks[2] as Block; @@ -285,21 +285,21 @@ export class ExecuteViewSubmitHandler { } } - missingObject[ + missingPropObject[ NotionPageOrRecord.TITLE_ACTION ] = `Please Provide ${titleViewError}`; } } if (!pageSelectState) { - missingObject[SearchPageAndDatabase.ACTION_ID] = + missingPropObject[SearchPageAndDatabase.ACTION_ID] = "Please Select a Page or Database"; } - if (Object.keys(missingObject).length) { + if (Object.keys(missingPropObject).length) { return this.context.getInteractionResponder().viewErrorResponse({ viewId: view.id, - errors: missingObject, + errors: missingPropObject, }); } From 5a51803790cab566b1c1748ef68045f9d37d553a Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Fri, 28 Jul 2023 10:27:58 +0530 Subject: [PATCH 05/31] feat: share page --- definition/handlers/IHandler.ts | 1 + definition/lib/INotion.ts | 4 ++ enum/CommandParam.ts | 1 + enum/messages.ts | 3 +- enum/modals/SharePage.ts | 11 ++++ src/commands/CommandUtility.ts | 4 ++ src/handlers/ExecuteViewSubmitHandler.ts | 54 +++++++++++++++++ src/handlers/Handler.ts | 54 +++++++++++++++++ src/helper/message.ts | 25 ++++++++ src/lib/NotionSDK.ts | 40 ++++++++++++ src/modals/sharePageModal.ts | 77 ++++++++++++++++++++++++ 11 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 enum/modals/SharePage.ts create mode 100644 src/modals/sharePageModal.ts diff --git a/definition/handlers/IHandler.ts b/definition/handlers/IHandler.ts index 8b51c1b..7dc857b 100644 --- a/definition/handlers/IHandler.ts +++ b/definition/handlers/IHandler.ts @@ -9,6 +9,7 @@ export interface IHandler extends Omit { commentOnPages(): Promise; createNotionPageOrRecord(): Promise; changeNotionWorkspace(): Promise; + shareNotionPage(): Promise; } export type IHanderParams = Omit; diff --git a/definition/lib/INotion.ts b/definition/lib/INotion.ts index 869e1c9..78f203f 100644 --- a/definition/lib/INotion.ts +++ b/definition/lib/INotion.ts @@ -59,6 +59,10 @@ export interface INotionSDK extends INotion { database: IDatabase, properties: object ): Promise | Error>; + retrievePage( + token: string, + pageId: string + ): Promise<(IPage & { url: string }) | Error>; } export interface IParentPage { diff --git a/enum/CommandParam.ts b/enum/CommandParam.ts index 6f219b9..226369a 100644 --- a/enum/CommandParam.ts +++ b/enum/CommandParam.ts @@ -6,6 +6,7 @@ export enum CommandParam { COMMENT = "comment", WORKSPACE = "workspace", WS = "ws", + SHARE = "share", } export enum SubCommandParam { diff --git a/enum/messages.ts b/enum/messages.ts index 0cf8ba6..33a6b86 100644 --- a/enum/messages.ts +++ b/enum/messages.ts @@ -4,7 +4,8 @@ export enum Messages { • use \`/notion comment\` to comment on notion page • use \`/notion create\` to create page or record • use \`/notion create db\` to create database - • use \`/notion workspace\` to change workspace + • use \`/notion workspace\` to change workspace + • use \`/notion share\` to share pages `, HELPER_TEXT = `:wave: Need some help with \`/notion\`?`, diff --git a/enum/modals/SharePage.ts b/enum/modals/SharePage.ts new file mode 100644 index 0000000..48b7edc --- /dev/null +++ b/enum/modals/SharePage.ts @@ -0,0 +1,11 @@ +export enum SharePage { + VIEW_ID = "notion-share-page-view-id", + ACTION_ID = "notion-share-page-action-id", + SHARE_ACTION = "notion-share-page-share-action-id", + SHARE_BLOCK = "notion-share-page-share-block-id", + CLOSE_ACTION = "notion-share-page-close-action-id", + CLOSE_BLOCK = "notion-share-page-close-block-id", + SHARE = "Share", + CLOSE = "Close", + TITLE = "Share Notion Page", +} diff --git a/src/commands/CommandUtility.ts b/src/commands/CommandUtility.ts index eb5b083..175459c 100644 --- a/src/commands/CommandUtility.ts +++ b/src/commands/CommandUtility.ts @@ -119,6 +119,10 @@ export class CommandUtility implements ICommandUtility { await handler.changeNotionWorkspace(); break; } + case CommandParam.SHARE: { + await handler.shareNotionPage(); + break; + } case CommandParam.HELP: default: { await sendHelperNotification( diff --git a/src/handlers/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index 2db08b7..2428c33 100644 --- a/src/handlers/ExecuteViewSubmitHandler.ts +++ b/src/handlers/ExecuteViewSubmitHandler.ts @@ -15,6 +15,7 @@ import { ModalInteractionStorage } from "../storage/ModalInteraction"; import { clearAllInteraction } from "../helper/clearInteractions"; import { OAuth2Storage } from "../authorization/OAuth2Storage"; import { + sendMessage, sendMessageWithAttachments, sendNotification, sendNotificationWithAttachments, @@ -45,6 +46,8 @@ import { PropertyTypeValue, } from "../../enum/modals/common/NotionProperties"; import { Block } from "@rocket.chat/ui-kit"; +import { SharePage } from "../../enum/modals/SharePage"; +import { SearchPage } from "../../enum/modals/common/SearchPageComponent"; export class ExecuteViewSubmitHandler { private context: UIKitViewSubmitInteractionContext; @@ -108,6 +111,14 @@ export class ExecuteViewSubmitHandler { ); break; } + case SharePage.VIEW_ID: { + return this.handleSharePage( + room, + oAuth2Storage, + modalInteraction + ); + break; + } default: { } } @@ -631,4 +642,47 @@ export class ExecuteViewSubmitHandler { return data; } + + public async handleSharePage( + room: IRoom, + oAuth2Storage: OAuth2Storage, + modalInteraction: ModalInteractionStorage + ): Promise { + const { view, user } = this.context.getInteractionData(); + const { state } = view; + + const { NotionSdk } = this.app.getUtils(); + const tokenInfo = await oAuth2Storage.getCurrentWorkspace(user.id); + + if (!tokenInfo) { + await sendNotificationWithConnectBlock( + this.app, + user, + this.read, + this.modify, + room + ); + return this.context.getInteractionResponder().errorResponse(); + } + + const { workspace_name, owner, access_token } = tokenInfo; + const pageId: string = + state?.[SearchPage.BLOCK_ID]?.[SharePage.ACTION_ID]; + + const pageInfo = await NotionSdk.retrievePage(access_token, pageId); + + if (pageInfo instanceof Error) { + return this.context.getInteractionResponder().errorResponse(); + } + + const { name, parent, url } = pageInfo; + + const message = `✨ Sharing [**${name}**](${url}) from **${workspace_name}**`; + + await sendMessage(this.read, this.modify, user, room, { + message, + }); + + return this.context.getInteractionResponder().successResponse(); + } } diff --git a/src/handlers/Handler.ts b/src/handlers/Handler.ts index 1ce3d8f..50f4f76 100644 --- a/src/handlers/Handler.ts +++ b/src/handlers/Handler.ts @@ -25,6 +25,8 @@ import { SearchPageAndDatabase } from "../../enum/modals/common/SearchPageAndDat import { NotionObjectTypes, NotionOwnerType } from "../../enum/Notion"; import { PropertyTypeValue } from "../../enum/modals/common/NotionProperties"; import { INotionUser } from "../../definition/authorization/IOAuth2Storage"; +import { sharePageModal } from "../modals/sharePageModal"; +import { SharePage } from "../../enum/modals/SharePage"; export class Handler implements IHandler { public app: NotionApp; @@ -324,4 +326,56 @@ export class Handler implements IHandler { .openSurfaceView(modal, { triggerId }, this.sender); } } + + public async shareNotionPage(): Promise { + const userId = this.sender.id; + const roomId = this.room.id; + const tokenInfo = await this.oAuth2Storage.getCurrentWorkspace(userId); + + if (!tokenInfo) { + await sendNotificationWithConnectBlock( + this.app, + this.sender, + this.read, + this.modify, + this.room + ); + return; + } + + const persistenceRead = this.read.getPersistenceReader(); + const modalInteraction = new ModalInteractionStorage( + this.persis, + persistenceRead, + userId, + SharePage.VIEW_ID + ); + + await this.roomInteractionStorage.storeInteractionRoomId(roomId); + + const modal = await sharePageModal( + this.app, + this.sender, + this.read, + this.persis, + this.modify, + this.room, + modalInteraction, + tokenInfo + ); + + if (modal instanceof Error) { + // Something went Wrong Probably SearchPageComponent Couldn't Fetch the Pages + this.app.getLogger().error(modal.message); + return; + } + + const triggerId = this.triggerId; + + if (triggerId) { + await this.modify + .getUiController() + .openSurfaceView(modal, { triggerId }, this.sender); + } + } } diff --git a/src/helper/message.ts b/src/helper/message.ts index c26d9d0..586d7da 100644 --- a/src/helper/message.ts +++ b/src/helper/message.ts @@ -215,3 +215,28 @@ export async function sendMessageWithAttachments( await modify.getCreator().finish(messageBuilder); } + +export async function sendMessage( + read: IRead, + modify: IModify, + user: IUser, + room: IRoom, + content: { message?: string; blocks?: Array } +): Promise { + const { message, blocks } = content; + const messageBuilder = modify + .getCreator() + .startMessage() + .setSender(user) + .setRoom(room) + .setGroupable(false) + .setParseUrls(true); + + if (message) { + messageBuilder.setText(message); + } else if (blocks) { + messageBuilder.setBlocks(blocks); + } + await modify.getCreator().finish(messageBuilder); + return; +} diff --git a/src/lib/NotionSDK.ts b/src/lib/NotionSDK.ts index 7024248..1289331 100644 --- a/src/lib/NotionSDK.ts +++ b/src/lib/NotionSDK.ts @@ -944,4 +944,44 @@ export class NotionSDK implements INotionSDK { return fields; } + + public async retrievePage( + token: string, + pageId: string + ): Promise<(IPage & { url: string }) | Error> { + try { + const response = await this.http.get( + NotionApi.PAGES + `/${pageId}`, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": NotionApi.CONTENT_TYPE, + "User-Agent": NotionApi.USER_AGENT, + "Notion-Version": this.NotionVersion, + }, + } + ); + + if (!response.statusCode.toString().startsWith("2")) { + return this.handleErrorResponse( + response.statusCode, + `Error While retrieving Page: `, + response.content + ); + } + + const pageInfo = response.data; + const page = (await this.getPageObjectFromResults( + pageInfo + )) as IPage; + const url: string = pageInfo?.url; + + return { + ...page, + url, + }; + } catch (err) { + throw new AppsEngineException(err as string); + } + } } diff --git a/src/modals/sharePageModal.ts b/src/modals/sharePageModal.ts new file mode 100644 index 0000000..0050e35 --- /dev/null +++ b/src/modals/sharePageModal.ts @@ -0,0 +1,77 @@ +import { + IModify, + IPersistence, + IRead, + IUIKitSurfaceViewParam, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { ITokenInfo } from "../../definition/authorization/IOAuth2Storage"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; +import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { NotionApp } from "../../NotionApp"; +import { Error } from "../../errors/Error"; +import { searchPageComponent } from "./common/searchPageComponent"; +import { ModalInteractionStorage } from "../storage/ModalInteraction"; +import { SharePage } from "../../enum/modals/SharePage"; +import { Block, TextObjectType } from "@rocket.chat/ui-kit"; +import { getConnectPreview } from "../helper/getConnectLayout"; +import { + ButtonStyle, + UIKitSurfaceType, +} from "@rocket.chat/apps-engine/definition/uikit"; + +export async function sharePageModal( + app: NotionApp, + user: IUser, + read: IRead, + persistence: IPersistence, + modify: IModify, + room: IRoom, + modalInteraction: ModalInteractionStorage, + tokenInfo: ITokenInfo +): Promise { + const blocks: Block[] = []; + const { elementBuilder } = app.getUtils(); + const connectBlock = getConnectPreview(app.getID(), tokenInfo); + blocks.push(connectBlock); + + const searchForPageComponent = await searchPageComponent( + app, + modalInteraction, + tokenInfo, + SharePage.ACTION_ID + ); + + if (searchForPageComponent instanceof Error) { + return searchForPageComponent; + } + + blocks.push(searchForPageComponent); + + const submit = elementBuilder.addButton( + { text: SharePage.SHARE, style: ButtonStyle.PRIMARY }, + { + actionId: SharePage.SHARE_ACTION, + blockId: SharePage.SHARE_BLOCK, + } + ); + + const close = elementBuilder.addButton( + { text: SharePage.CLOSE, style: ButtonStyle.DANGER }, + { + actionId: SharePage.CLOSE_ACTION, + blockId: SharePage.CLOSE_BLOCK, + } + ); + + return { + id: SharePage.VIEW_ID, + type: UIKitSurfaceType.MODAL, + title: { + type: TextObjectType.MRKDWN, + text: SharePage.TITLE, + }, + blocks, + close, + submit, + }; +} From cce0216ca8674bf96989a163d1c0632bbb798755 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Wed, 2 Aug 2023 11:42:17 +0530 Subject: [PATCH 06/31] fix: Sharing page without selecting with ViewErrors --- src/handlers/ExecuteBlockActionHandler.ts | 14 ++++++++++++++ src/handlers/ExecuteViewSubmitHandler.ts | 11 ++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/handlers/ExecuteBlockActionHandler.ts b/src/handlers/ExecuteBlockActionHandler.ts index 2ca3513..cc9e7a0 100644 --- a/src/handlers/ExecuteBlockActionHandler.ts +++ b/src/handlers/ExecuteBlockActionHandler.ts @@ -48,6 +48,7 @@ import { NotionPageOrRecord } from "../../enum/modals/NotionPageOrRecord"; import { NotionWorkspace } from "../../enum/modals/NotionWorkspace"; import { changeWorkspaceModal } from "../modals/changeWorkspaceModal"; import { getPropertiesIdsObject } from "../helper/getPropertiesIdsObject"; +import { SharePage } from "../../enum/modals/SharePage"; export class ExecuteBlockActionHandler { private context: UIKitBlockInteractionContext; @@ -164,6 +165,10 @@ export class ExecuteBlockActionHandler { ); break; } + case SharePage.ACTION_ID: { + return this.handleSelectPageAction(); + break; + } default: { // Property Type Select Action const propertyTypeSelected = @@ -979,4 +984,13 @@ export class ExecuteBlockActionHandler { await modalInteraction.updateInteractionActionId(result); } + + private async handleSelectPageAction(): Promise { + const { value, container } = this.context.getInteractionData(); + + return this.context.getInteractionResponder().viewErrorResponse({ + viewId: container.id, + errors: {}, + }); + } } diff --git a/src/handlers/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index 2428c33..e70d434 100644 --- a/src/handlers/ExecuteViewSubmitHandler.ts +++ b/src/handlers/ExecuteViewSubmitHandler.ts @@ -666,9 +666,18 @@ export class ExecuteViewSubmitHandler { } const { workspace_name, owner, access_token } = tokenInfo; - const pageId: string = + const pageId: string | undefined = state?.[SearchPage.BLOCK_ID]?.[SharePage.ACTION_ID]; + if (!pageId) { + return this.context.getInteractionResponder().viewErrorResponse({ + viewId: view.id, + errors: { + [SharePage.ACTION_ID]: "Please Select a Page to Share", + }, + }); + } + const pageInfo = await NotionSdk.retrievePage(access_token, pageId); if (pageInfo instanceof Error) { From 5055660dcb6554469a7a50c610f403c177570bfd Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 8 Aug 2023 14:46:13 +0530 Subject: [PATCH 07/31] feat: register send to page message action --- NotionApp.ts | 9 ++++++++- app.json | 3 +++ enum/modals/common/ActionButtons.ts | 2 ++ i18n/en.json | 3 ++- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/NotionApp.ts b/NotionApp.ts index 0b34803..dc4a9ff 100644 --- a/NotionApp.ts +++ b/NotionApp.ts @@ -7,7 +7,7 @@ import { ILogger, IModify, IPersistence, - IRead + IRead, } from "@rocket.chat/apps-engine/definition/accessors"; import { App } from "@rocket.chat/apps-engine/definition/App"; import { IAppInfo } from "@rocket.chat/apps-engine/definition/metadata"; @@ -80,7 +80,14 @@ export class NotionApp extends App { context: UIActionButtonContext.MESSAGE_BOX_ACTION, }; + const sendToPageButton: IUIActionButtonDescriptor = { + actionId: ActionButton.SEND_TO_PAGE_MESSAGE_ACTION, + labelI18n: ActionButton.SEND_TO_PAGE_MESSAGE_ACTION_LABEL, + context: UIActionButtonContext.MESSAGE_ACTION, + }; + configurationExtend.ui.registerButton(commentOnPagesButton); + configurationExtend.ui.registerButton(sendToPageButton); } public getOAuth2Client(): OAuth2Client { diff --git a/app.json b/app.json index e7c3ca8..2af5242 100644 --- a/app.json +++ b/app.json @@ -46,6 +46,9 @@ }, { "name": "room.write" + }, + { + "name": "message.read" } ] } \ No newline at end of file diff --git a/enum/modals/common/ActionButtons.ts b/enum/modals/common/ActionButtons.ts index e34a5cf..304ac81 100644 --- a/enum/modals/common/ActionButtons.ts +++ b/enum/modals/common/ActionButtons.ts @@ -1,4 +1,6 @@ export enum ActionButton { COMMENT_ON_PAGES_MESSAGE_BOX_ACTION = "comment-on-pages-message-box-action", COMMENT_ON_PAGES_MESSAGE_BOX_ACTION_LABEL = "CommentOnPagesLabel", + SEND_TO_PAGE_MESSAGE_ACTION = "send-to-page-message-action", + SEND_TO_PAGE_MESSAGE_ACTION_LABEL = "SendToPageLabel", } \ No newline at end of file diff --git a/i18n/en.json b/i18n/en.json index 0d11fef..17e5390 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -6,5 +6,6 @@ "CredentialsSettings": "Authorization Settings", "NotionCommandParams": "connect | disconnect | workspace | create | schema | comment", "NotionCommandDescription": "Create Notion pages and database from Rocket.Chat", - "CommentOnPagesLabel": "💬 Comment on Page" + "CommentOnPagesLabel": "💬 Comment on Page", + "SendToPageLabel": "📝 Send to Page" } From 59a596c1c4f362159c6cce910c341739db334499 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 8 Aug 2023 14:50:07 +0530 Subject: [PATCH 08/31] feat: Write Message While Creation of Notion Record --- definition/lib/INotion.ts | 2 +- src/handlers/ExecuteActionButtonHandler.ts | 6 +++- src/handlers/ExecuteViewClosedHandler.ts | 6 +++- src/handlers/ExecuteViewSubmitHandler.ts | 36 +++++++++++++++++----- src/handlers/Handler.ts | 19 ++++++++++-- src/helper/message.ts | 17 ++++++++-- src/lib/NotionSDK.ts | 14 ++++++--- 7 files changed, 81 insertions(+), 19 deletions(-) diff --git a/definition/lib/INotion.ts b/definition/lib/INotion.ts index 78f203f..e5b7f90 100644 --- a/definition/lib/INotion.ts +++ b/definition/lib/INotion.ts @@ -58,7 +58,7 @@ export interface INotionSDK extends INotion { token: string, database: IDatabase, properties: object - ): Promise | Error>; + ): Promise<{ fields: Array; url: string } | Error>; retrievePage( token: string, pageId: string diff --git a/src/handlers/ExecuteActionButtonHandler.ts b/src/handlers/ExecuteActionButtonHandler.ts index ea47998..9d1eb05 100644 --- a/src/handlers/ExecuteActionButtonHandler.ts +++ b/src/handlers/ExecuteActionButtonHandler.ts @@ -26,7 +26,7 @@ export class ExecuteActionButtonHandler { } public async handleActions(): Promise { - const { actionId, user, room, triggerId } = + const { actionId, user, room, triggerId, message } = this.context.getInteractionData(); const handler = new Handler({ @@ -45,6 +45,10 @@ export class ExecuteActionButtonHandler { await handler.commentOnPages(); break; } + case ActionButton.SEND_TO_PAGE_MESSAGE_ACTION: { + await handler.createNotionPageOrRecord(false, message); + break; + } } return this.context.getInteractionResponder().successResponse(); diff --git a/src/handlers/ExecuteViewClosedHandler.ts b/src/handlers/ExecuteViewClosedHandler.ts index dad7c6c..96b3f3c 100644 --- a/src/handlers/ExecuteViewClosedHandler.ts +++ b/src/handlers/ExecuteViewClosedHandler.ts @@ -19,6 +19,7 @@ import { NotionPageOrRecord } from "../../enum/modals/NotionPageOrRecord"; import { SearchPageAndDatabase } from "../../enum/modals/common/SearchPageAndDatabaseComponent"; import { NotionObjectTypes } from "../../enum/Notion"; import { PropertyTypeValue } from "../../enum/modals/common/NotionProperties"; +import { ActionButton } from "../../enum/modals/common/ActionButtons"; export class ExecuteViewClosedHandler { private context: UIKitViewCloseInteractionContext; @@ -84,7 +85,10 @@ export class ExecuteViewClosedHandler { modalInteraction.clearAllInteractionActionId(), modalInteraction.clearInputElementState( PropertyTypeValue.PEOPLE - ) + ), + modalInteraction.clearInputElementState( + ActionButton.SEND_TO_PAGE_MESSAGE_ACTION + ), ]); break; diff --git a/src/handlers/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index e70d434..5598c0f 100644 --- a/src/handlers/ExecuteViewSubmitHandler.ts +++ b/src/handlers/ExecuteViewSubmitHandler.ts @@ -32,10 +32,7 @@ import { IMessageAttachmentField } from "@rocket.chat/apps-engine/definition/mes import { NotionPageOrRecord } from "../../enum/modals/NotionPageOrRecord"; import { NotionObjectTypes } from "../../enum/Notion"; import { ITokenInfo } from "../../definition/authorization/IOAuth2Storage"; -import { - IDatabase, - IPage -} from "../../definition/lib/INotion"; +import { IDatabase, IPage } from "../../definition/lib/INotion"; import { SearchPageAndDatabase } from "../../enum/modals/common/SearchPageAndDatabaseComponent"; import { NotionWorkspace } from "../../enum/modals/NotionWorkspace"; import { getConnectPreview } from "../helper/getConnectLayout"; @@ -48,6 +45,7 @@ import { import { Block } from "@rocket.chat/ui-kit"; import { SharePage } from "../../enum/modals/SharePage"; import { SearchPage } from "../../enum/modals/common/SearchPageComponent"; +import { ActionButton } from "../../enum/modals/common/ActionButtons"; export class ExecuteViewSubmitHandler { private context: UIKitViewSubmitInteractionContext; @@ -425,19 +423,43 @@ export class ExecuteViewSubmitHandler { state?.[NotionPageOrRecord.TITLE_BLOCK]?.[ NotionPageOrRecord.TITLE_ACTION ]; + const { fields, url } = createdRecord; - message = `✨ Created **${title}** in [**${databasename}**](${databaselink})`; + message = `✨ Created [**${title}**](${url}) in [**${databasename}**](${databaselink})`; - await sendMessageWithAttachments( + const messageId = await sendMessageWithAttachments( this.read, this.modify, user, room, { message: message, - fields: createdRecord, + fields, } ); + + const preserveMessage = await modalInteraction.getInputElementState( + ActionButton.SEND_TO_PAGE_MESSAGE_ACTION + ); + + if (preserveMessage) { + const { id, text } = preserveMessage as { + id: string; + text: string; + }; + + const preserveText = `📝 Wrote in [**${title}**](${url})`; + + await sendMessage( + this.read, + this.modify, + user, + room, + { message: preserveText }, + messageId, + { collapsed: false, color: "#000000", text: text } + ); + } } return this.context.getInteractionResponder().successResponse(); diff --git a/src/handlers/Handler.ts b/src/handlers/Handler.ts index 50f4f76..92e781e 100644 --- a/src/handlers/Handler.ts +++ b/src/handlers/Handler.ts @@ -22,11 +22,13 @@ import { createPageOrRecordModal } from "../modals/createPageOrRecordModal"; import { changeWorkspaceModal } from "../modals/changeWorkspaceModal"; import { NotionWorkspace } from "../../enum/modals/NotionWorkspace"; import { SearchPageAndDatabase } from "../../enum/modals/common/SearchPageAndDatabaseComponent"; -import { NotionObjectTypes, NotionOwnerType } from "../../enum/Notion"; +import { NotionOwnerType } from "../../enum/Notion"; import { PropertyTypeValue } from "../../enum/modals/common/NotionProperties"; import { INotionUser } from "../../definition/authorization/IOAuth2Storage"; import { sharePageModal } from "../modals/sharePageModal"; import { SharePage } from "../../enum/modals/SharePage"; +import { IMessage } from "@rocket.chat/apps-engine/definition/messages"; +import { ActionButton } from "../../enum/modals/common/ActionButtons"; export class Handler implements IHandler { public app: NotionApp; @@ -184,7 +186,10 @@ export class Handler implements IHandler { } } - public async createNotionPageOrRecord(update?: boolean): Promise { + public async createNotionPageOrRecord( + update?: boolean, + message?: IMessage + ): Promise { const userId = this.sender.id; const roomId = this.room.id; const tokenInfo = await this.oAuth2Storage.getCurrentWorkspace(userId); @@ -217,6 +222,9 @@ export class Handler implements IHandler { SearchPageAndDatabase.ACTION_ID ), modalInteraction.clearAllInteractionActionId(), + modalInteraction.clearInputElementState( + ActionButton.SEND_TO_PAGE_MESSAGE_ACTION + ), ]); const modal = await createPageOrRecordModal( @@ -251,6 +259,13 @@ export class Handler implements IHandler { return; } + if (message) { + await modalInteraction.storeInputElementState( + ActionButton.SEND_TO_PAGE_MESSAGE_ACTION, + message + ); + } + await this.modify .getUiController() .openSurfaceView(modal, { triggerId }, this.sender); diff --git a/src/helper/message.ts b/src/helper/message.ts index 586d7da..7947f81 100644 --- a/src/helper/message.ts +++ b/src/helper/message.ts @@ -191,7 +191,7 @@ export async function sendMessageWithAttachments( blocks?: Array; fields: Array; } -) { +): Promise { const { message, blocks, fields } = content; const messageBuilder = modify .getCreator() @@ -213,7 +213,7 @@ export async function sendMessageWithAttachments( }, ]); - await modify.getCreator().finish(messageBuilder); + return await modify.getCreator().finish(messageBuilder); } export async function sendMessage( @@ -221,7 +221,9 @@ export async function sendMessage( modify: IModify, user: IUser, room: IRoom, - content: { message?: string; blocks?: Array } + content: { message?: string; blocks?: Array }, + threadId?: string, + attachment?: IMessageAttachment ): Promise { const { message, blocks } = content; const messageBuilder = modify @@ -237,6 +239,15 @@ export async function sendMessage( } else if (blocks) { messageBuilder.setBlocks(blocks); } + + if (threadId) { + messageBuilder.setThreadId(threadId); + } + + if (attachment) { + messageBuilder.addAttachment(attachment); + } + await modify.getCreator().finish(messageBuilder); return; } diff --git a/src/lib/NotionSDK.ts b/src/lib/NotionSDK.ts index 1289331..2fd02d1 100644 --- a/src/lib/NotionSDK.ts +++ b/src/lib/NotionSDK.ts @@ -740,7 +740,9 @@ export class NotionSDK implements INotionSDK { token: string, database: IDatabase, properties: object - ): Promise | Error> { + ): Promise< + { fields: Array; url: string } | Error + > { try { const { parent } = database; const data = { @@ -765,10 +767,14 @@ export class NotionSDK implements INotionSDK { response.content ); } - + const pageInfo = response.data; + const url: string = pageInfo?.url; const prop = response.data?.[NotionObjectTypes.PROPERTIES]; - const result = await this.getFieldsFromRecord(prop); - return result; + const fields = await this.getFieldsFromRecord(prop); + return { + fields, + url, + }; } catch (err) { throw new AppsEngineException(err as string); } From bb031ecead57fbd173f222ff76306ec25fd12317 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 8 Aug 2023 15:43:25 +0530 Subject: [PATCH 09/31] fix: message attachment to have a quote instead of text --- src/handlers/ExecuteViewSubmitHandler.ts | 31 +++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/handlers/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index 5598c0f..fee8e97 100644 --- a/src/handlers/ExecuteViewSubmitHandler.ts +++ b/src/handlers/ExecuteViewSubmitHandler.ts @@ -22,7 +22,7 @@ import { sendNotificationWithConnectBlock, } from "../helper/message"; import { RoomInteractionStorage } from "../storage/RoomInteraction"; -import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; +import { IRoom, RoomType } from "@rocket.chat/apps-engine/definition/rooms"; import { getNotionDatabaseObject } from "../helper/getNotionDatabaseObject"; import { Error } from "../../errors/Error"; import { Modals } from "../../enum/modals/common/Modals"; @@ -46,6 +46,8 @@ import { Block } from "@rocket.chat/ui-kit"; import { SharePage } from "../../enum/modals/SharePage"; import { SearchPage } from "../../enum/modals/common/SearchPageComponent"; import { ActionButton } from "../../enum/modals/common/ActionButtons"; +import { getCredentials } from "../helper/getCredential"; +import { ICredential } from "../../definition/authorization/ICredential"; export class ExecuteViewSubmitHandler { private context: UIKitViewSubmitInteractionContext; @@ -443,12 +445,30 @@ export class ExecuteViewSubmitHandler { ); if (preserveMessage) { - const { id, text } = preserveMessage as { + const preserveMessageContext = preserveMessage as { id: string; - text: string; + room: IRoom; }; - const preserveText = `📝 Wrote in [**${title}**](${url})`; + const { id } = preserveMessageContext; + + const { type, displayName } = preserveMessageContext.room; + const urlPath = + type === RoomType.CHANNEL + ? "channel" + : type === RoomType.PRIVATE_GROUP + ? "group" + : "direct"; + + const { siteUrl } = (await getCredentials( + this.read, + this.modify, + user, + room + )) as ICredential; + + const messageLink = `${siteUrl}/${urlPath}/${displayName}?msg=${id}`; + const preserveText = `📝 Created [**${title}**](${url}) Page and Preserved [Message](${messageLink}) `; await sendMessage( this.read, @@ -456,8 +476,7 @@ export class ExecuteViewSubmitHandler { user, room, { message: preserveText }, - messageId, - { collapsed: false, color: "#000000", text: text } + id ); } } From 98f8747aa17eea7c7aaf71ad5566255f42261a2e Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 8 Aug 2023 16:04:48 +0530 Subject: [PATCH 10/31] feat: Write Message while Creating Page --- src/handlers/ExecuteViewSubmitHandler.ts | 40 +++++++++++++++++++++++- src/handlers/Handler.ts | 7 +++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/handlers/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index fee8e97..c75da3f 100644 --- a/src/handlers/ExecuteViewSubmitHandler.ts +++ b/src/handlers/ExecuteViewSubmitHandler.ts @@ -368,6 +368,44 @@ export class ExecuteViewSubmitHandler { } else { const { name, link, title } = createdPage; message = `✨ Your Page [**${title}**](${link}) is created successfully as a subpage in **${name}**.`; + + const preserveMessage = await modalInteraction.getInputElementState( + ActionButton.SEND_TO_PAGE_MESSAGE_ACTION + ); + + if (preserveMessage) { + const preserveMessageContext = preserveMessage as { + id: string; + room: IRoom; + }; + + const { id } = preserveMessageContext; + const { type, displayName } = preserveMessageContext.room; + const urlPath = + type === RoomType.CHANNEL + ? "channel" + : type === RoomType.PRIVATE_GROUP + ? "group" + : "direct"; + const { siteUrl } = (await getCredentials( + this.read, + this.modify, + user, + room + )) as ICredential; + + const messageLink = `${siteUrl}/${urlPath}/${displayName}?msg=${id}`; + const preserveText = `📝 Created New Page [**${title}**](${link}) and Preserved Following [Message](${messageLink}) `; + + await sendMessage( + this.read, + this.modify, + user, + room, + { message: preserveText }, + id + ); + } } await sendNotification(this.read, this.modify, user, room, { @@ -468,7 +506,7 @@ export class ExecuteViewSubmitHandler { )) as ICredential; const messageLink = `${siteUrl}/${urlPath}/${displayName}?msg=${id}`; - const preserveText = `📝 Created [**${title}**](${url}) Page and Preserved [Message](${messageLink}) `; + const preserveText = `📝 Created [**${title}**](${url}) Page and Preserved Following [Message](${messageLink}) `; await sendMessage( this.read, diff --git a/src/handlers/Handler.ts b/src/handlers/Handler.ts index 92e781e..efb8f25 100644 --- a/src/handlers/Handler.ts +++ b/src/handlers/Handler.ts @@ -222,9 +222,6 @@ export class Handler implements IHandler { SearchPageAndDatabase.ACTION_ID ), modalInteraction.clearAllInteractionActionId(), - modalInteraction.clearInputElementState( - ActionButton.SEND_TO_PAGE_MESSAGE_ACTION - ), ]); const modal = await createPageOrRecordModal( @@ -259,6 +256,10 @@ export class Handler implements IHandler { return; } + await modalInteraction.clearInputElementState( + ActionButton.SEND_TO_PAGE_MESSAGE_ACTION + ); + if (message) { await modalInteraction.storeInputElementState( ActionButton.SEND_TO_PAGE_MESSAGE_ACTION, From 9311a855fea1e17f45ae71af624288a038173bdd Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 8 Aug 2023 16:37:35 +0530 Subject: [PATCH 11/31] feat: appendCodeBlock while Creating Message text in Page --- definition/lib/INotion.ts | 14 ++++- enum/Notion.ts | 1 + src/handlers/ExecuteViewSubmitHandler.ts | 72 ++++++++++++++---------- src/lib/NotionSDK.ts | 63 ++++++++++++++++++++- 4 files changed, 118 insertions(+), 32 deletions(-) diff --git a/definition/lib/INotion.ts b/definition/lib/INotion.ts index e5b7f90..b995f2d 100644 --- a/definition/lib/INotion.ts +++ b/definition/lib/INotion.ts @@ -58,11 +58,23 @@ export interface INotionSDK extends INotion { token: string, database: IDatabase, properties: object - ): Promise<{ fields: Array; url: string } | Error>; + ): Promise< + | { + fields: Array; + url: string; + pageId: string; + } + | Error + >; retrievePage( token: string, pageId: string ): Promise<(IPage & { url: string }) | Error>; + appendMessageBlock( + token: string, + message: string, + blockId: string + ): Promise; } export interface IParentPage { diff --git a/enum/Notion.ts b/enum/Notion.ts index c01c7d6..257b11e 100644 --- a/enum/Notion.ts +++ b/enum/Notion.ts @@ -21,6 +21,7 @@ export enum NotionApi { COMMENTS = `https://api.notion.com/v1/comments`, USERS = `https://api.notion.com/v1/users`, PAGES = `https://api.notion.com/v1/pages`, + BLOCKS = `https://api.notion.com/v1/blocks`, } export enum Notion { diff --git a/src/handlers/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index c75da3f..2332afe 100644 --- a/src/handlers/ExecuteViewSubmitHandler.ts +++ b/src/handlers/ExecuteViewSubmitHandler.ts @@ -463,7 +463,7 @@ export class ExecuteViewSubmitHandler { state?.[NotionPageOrRecord.TITLE_BLOCK]?.[ NotionPageOrRecord.TITLE_ACTION ]; - const { fields, url } = createdRecord; + const { fields, url, pageId } = createdRecord; message = `✨ Created [**${title}**](${url}) in [**${databasename}**](${databaselink})`; @@ -485,37 +485,51 @@ export class ExecuteViewSubmitHandler { if (preserveMessage) { const preserveMessageContext = preserveMessage as { id: string; + text: string; room: IRoom; }; - - const { id } = preserveMessageContext; - - const { type, displayName } = preserveMessageContext.room; - const urlPath = - type === RoomType.CHANNEL - ? "channel" - : type === RoomType.PRIVATE_GROUP - ? "group" - : "direct"; - - const { siteUrl } = (await getCredentials( - this.read, - this.modify, - user, - room - )) as ICredential; - - const messageLink = `${siteUrl}/${urlPath}/${displayName}?msg=${id}`; - const preserveText = `📝 Created [**${title}**](${url}) Page and Preserved Following [Message](${messageLink}) `; - - await sendMessage( - this.read, - this.modify, - user, - room, - { message: preserveText }, - id + const { id, text } = preserveMessageContext; + console.log("text", text); + const appendBlock = await NotionSdk.appendMessageBlock( + access_token, + text, + pageId ); + + if (appendBlock instanceof Error) { + this.app.getLogger().error(appendBlock.message); + message = `🚫 Something went wrong while appending message in **${workspace_name}**.`; + await sendNotification(this.read, this.modify, user, room, { + message, + }); + } else { + const { type, displayName } = preserveMessageContext.room; + const urlPath = + type === RoomType.CHANNEL + ? "channel" + : type === RoomType.PRIVATE_GROUP + ? "group" + : "direct"; + + const { siteUrl } = (await getCredentials( + this.read, + this.modify, + user, + room + )) as ICredential; + + const messageLink = `${siteUrl}/${urlPath}/${displayName}?msg=${id}`; + const preserveText = `📝 Created [**${title}**](${url}) Page and Preserved Following [Message](${messageLink}) `; + + await sendMessage( + this.read, + this.modify, + user, + room, + { message: preserveText }, + id + ); + } } } diff --git a/src/lib/NotionSDK.ts b/src/lib/NotionSDK.ts index 2fd02d1..54e4c5e 100644 --- a/src/lib/NotionSDK.ts +++ b/src/lib/NotionSDK.ts @@ -741,7 +741,12 @@ export class NotionSDK implements INotionSDK { database: IDatabase, properties: object ): Promise< - { fields: Array; url: string } | Error + | { + fields: Array; + url: string; + pageId: string; + } + | Error > { try { const { parent } = database; @@ -769,11 +774,14 @@ export class NotionSDK implements INotionSDK { } const pageInfo = response.data; const url: string = pageInfo?.url; + const pageId: string = pageInfo?.id; + const prop = response.data?.[NotionObjectTypes.PROPERTIES]; const fields = await this.getFieldsFromRecord(prop); return { fields, url, + pageId, }; } catch (err) { throw new AppsEngineException(err as string); @@ -981,7 +989,7 @@ export class NotionSDK implements INotionSDK { pageInfo )) as IPage; const url: string = pageInfo?.url; - + return { ...page, url, @@ -990,4 +998,55 @@ export class NotionSDK implements INotionSDK { throw new AppsEngineException(err as string); } } + + public async appendMessageBlock( + token: string, + message: string, + blockId: string + ): Promise { + try { + const response = await this.http.patch( + NotionApi.BLOCKS + `/${blockId}/children`, + { + data: { + children: [ + { + type: "code", + code: { + caption: [], + rich_text: [ + { + type: "text", + text: { + content: message, + }, + }, + ], + language: "markdown", + }, + }, + ], + }, + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": NotionApi.CONTENT_TYPE, + "User-Agent": NotionApi.USER_AGENT, + "Notion-Version": this.NotionVersion, + }, + } + ); + + if (!response.statusCode.toString().startsWith("2")) { + return this.handleErrorResponse( + response.statusCode, + `Error While Appending Message Block: `, + response.content + ); + } + + return true; + } catch (err) { + throw new AppsEngineException(err as string); + } + } } From 1c94a112850d66920f534e1ee11147398f198079 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 8 Aug 2023 16:52:04 +0530 Subject: [PATCH 12/31] feat: appendCallOutBlock on Preserving Message --- definition/lib/INotion.ts | 2 +- src/handlers/ExecuteViewSubmitHandler.ts | 69 ++++++++++++++---------- src/lib/NotionSDK.ts | 20 ++++--- 3 files changed, 57 insertions(+), 34 deletions(-) diff --git a/definition/lib/INotion.ts b/definition/lib/INotion.ts index b995f2d..5e4c908 100644 --- a/definition/lib/INotion.ts +++ b/definition/lib/INotion.ts @@ -49,7 +49,7 @@ export interface INotionSDK extends INotion { token: string, page: IPage, prop: IPageProperties - ): Promise; + ): Promise<(INotionPage & { pageId: string }) | Error>; retrieveDatabase( token: string, database_id: string diff --git a/src/handlers/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index 2332afe..bbdc8b1 100644 --- a/src/handlers/ExecuteViewSubmitHandler.ts +++ b/src/handlers/ExecuteViewSubmitHandler.ts @@ -366,7 +366,7 @@ export class ExecuteViewSubmitHandler { this.app.getLogger().error(createdPage.message); message = `🚫 Something went wrong while creating page in **${workspace_name}**.`; } else { - const { name, link, title } = createdPage; + const { name, link, title, pageId } = createdPage; message = `✨ Your Page [**${title}**](${link}) is created successfully as a subpage in **${name}**.`; const preserveMessage = await modalInteraction.getInputElementState( @@ -376,35 +376,51 @@ export class ExecuteViewSubmitHandler { if (preserveMessage) { const preserveMessageContext = preserveMessage as { id: string; + text: string; room: IRoom; }; - const { id } = preserveMessageContext; - const { type, displayName } = preserveMessageContext.room; - const urlPath = - type === RoomType.CHANNEL - ? "channel" - : type === RoomType.PRIVATE_GROUP - ? "group" - : "direct"; - const { siteUrl } = (await getCredentials( - this.read, - this.modify, - user, - room - )) as ICredential; - - const messageLink = `${siteUrl}/${urlPath}/${displayName}?msg=${id}`; - const preserveText = `📝 Created New Page [**${title}**](${link}) and Preserved Following [Message](${messageLink}) `; - - await sendMessage( - this.read, - this.modify, - user, - room, - { message: preserveText }, - id + const { id, text } = preserveMessageContext; + + const appendBlock = await NotionSdk.appendMessageBlock( + access_token, + text, + pageId ); + + if (appendBlock instanceof Error) { + this.app.getLogger().error(appendBlock.message); + message = `🚫 Something went wrong while appending message in **${workspace_name}**.`; + await sendNotification(this.read, this.modify, user, room, { + message, + }); + } else { + const { type, displayName } = preserveMessageContext.room; + const urlPath = + type === RoomType.CHANNEL + ? "channel" + : type === RoomType.PRIVATE_GROUP + ? "group" + : "direct"; + const { siteUrl } = (await getCredentials( + this.read, + this.modify, + user, + room + )) as ICredential; + + const messageLink = `${siteUrl}/${urlPath}/${displayName}?msg=${id}`; + const preserveText = `📝 Created New Page [**${title}**](${link}) and Preserved Following [Message](${messageLink}) `; + + await sendMessage( + this.read, + this.modify, + user, + room, + { message: preserveText }, + id + ); + } } } @@ -489,7 +505,6 @@ export class ExecuteViewSubmitHandler { room: IRoom; }; const { id, text } = preserveMessageContext; - console.log("text", text); const appendBlock = await NotionSdk.appendMessageBlock( access_token, text, diff --git a/src/lib/NotionSDK.ts b/src/lib/NotionSDK.ts index 54e4c5e..85ebbd4 100644 --- a/src/lib/NotionSDK.ts +++ b/src/lib/NotionSDK.ts @@ -565,7 +565,7 @@ export class NotionSDK implements INotionSDK { token: string, page: IPage, prop: IPageProperties - ): Promise { + ): Promise<(INotionPage & { pageId: string }) | Error> { try { const { name, parent } = page; const { title } = prop; @@ -603,7 +603,12 @@ export class NotionSDK implements INotionSDK { title, }; - return result; + const pageId: string = response?.data?.id; + + return { + ...result, + pageId, + }; } catch (err) { throw new AppsEngineException(err as string); } @@ -1011,18 +1016,21 @@ export class NotionSDK implements INotionSDK { data: { children: [ { - type: "code", - code: { - caption: [], + type: "callout", + callout: { rich_text: [ { type: "text", text: { content: message, + link: null, }, }, ], - language: "markdown", + icon: { + emoji: "⭐", + }, + color: "default", }, }, ], From 40c0989e3567ca84d79fb3daa86373bfd5a7267f Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Thu, 10 Aug 2023 11:37:52 +0530 Subject: [PATCH 13/31] fix: actionbutton to send-to-new-page instead of send-to-page --- NotionApp.ts | 7 +++++++ enum/modals/common/ActionButtons.ts | 2 ++ i18n/en.json | 3 ++- src/handlers/ExecuteActionButtonHandler.ts | 2 +- src/handlers/ExecuteViewClosedHandler.ts | 2 +- src/handlers/ExecuteViewSubmitHandler.ts | 4 ++-- src/handlers/Handler.ts | 4 ++-- 7 files changed, 17 insertions(+), 7 deletions(-) diff --git a/NotionApp.ts b/NotionApp.ts index dc4a9ff..aa77b54 100644 --- a/NotionApp.ts +++ b/NotionApp.ts @@ -86,8 +86,15 @@ export class NotionApp extends App { context: UIActionButtonContext.MESSAGE_ACTION, }; + const sendToNewPageButton: IUIActionButtonDescriptor = { + actionId: ActionButton.SEND_TO_NEW_PAGE_MESSAGE_ACTION, + labelI18n: ActionButton.SEND_TO_NEW_PAGE_MESSAGE_ACTION_LABEL, + context: UIActionButtonContext.MESSAGE_ACTION, + }; + configurationExtend.ui.registerButton(commentOnPagesButton); configurationExtend.ui.registerButton(sendToPageButton); + configurationExtend.ui.registerButton(sendToNewPageButton); } public getOAuth2Client(): OAuth2Client { diff --git a/enum/modals/common/ActionButtons.ts b/enum/modals/common/ActionButtons.ts index 304ac81..c4c7698 100644 --- a/enum/modals/common/ActionButtons.ts +++ b/enum/modals/common/ActionButtons.ts @@ -3,4 +3,6 @@ export enum ActionButton { COMMENT_ON_PAGES_MESSAGE_BOX_ACTION_LABEL = "CommentOnPagesLabel", SEND_TO_PAGE_MESSAGE_ACTION = "send-to-page-message-action", SEND_TO_PAGE_MESSAGE_ACTION_LABEL = "SendToPageLabel", + SEND_TO_NEW_PAGE_MESSAGE_ACTION = "send-to-new-page-message-action", + SEND_TO_NEW_PAGE_MESSAGE_ACTION_LABEL = "SendToNewPageLabel", } \ No newline at end of file diff --git a/i18n/en.json b/i18n/en.json index 17e5390..df5eb48 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -7,5 +7,6 @@ "NotionCommandParams": "connect | disconnect | workspace | create | schema | comment", "NotionCommandDescription": "Create Notion pages and database from Rocket.Chat", "CommentOnPagesLabel": "💬 Comment on Page", - "SendToPageLabel": "📝 Send to Page" + "SendToPageLabel": "📝 Send to Page", + "SendToNewPageLabel": "📢 Send to New Page" } diff --git a/src/handlers/ExecuteActionButtonHandler.ts b/src/handlers/ExecuteActionButtonHandler.ts index 9d1eb05..55d45bd 100644 --- a/src/handlers/ExecuteActionButtonHandler.ts +++ b/src/handlers/ExecuteActionButtonHandler.ts @@ -45,7 +45,7 @@ export class ExecuteActionButtonHandler { await handler.commentOnPages(); break; } - case ActionButton.SEND_TO_PAGE_MESSAGE_ACTION: { + case ActionButton.SEND_TO_NEW_PAGE_MESSAGE_ACTION: { await handler.createNotionPageOrRecord(false, message); break; } diff --git a/src/handlers/ExecuteViewClosedHandler.ts b/src/handlers/ExecuteViewClosedHandler.ts index 96b3f3c..ffecf04 100644 --- a/src/handlers/ExecuteViewClosedHandler.ts +++ b/src/handlers/ExecuteViewClosedHandler.ts @@ -87,7 +87,7 @@ export class ExecuteViewClosedHandler { PropertyTypeValue.PEOPLE ), modalInteraction.clearInputElementState( - ActionButton.SEND_TO_PAGE_MESSAGE_ACTION + ActionButton.SEND_TO_NEW_PAGE_MESSAGE_ACTION ), ]); diff --git a/src/handlers/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index bbdc8b1..55ae792 100644 --- a/src/handlers/ExecuteViewSubmitHandler.ts +++ b/src/handlers/ExecuteViewSubmitHandler.ts @@ -370,7 +370,7 @@ export class ExecuteViewSubmitHandler { message = `✨ Your Page [**${title}**](${link}) is created successfully as a subpage in **${name}**.`; const preserveMessage = await modalInteraction.getInputElementState( - ActionButton.SEND_TO_PAGE_MESSAGE_ACTION + ActionButton.SEND_TO_NEW_PAGE_MESSAGE_ACTION ); if (preserveMessage) { @@ -495,7 +495,7 @@ export class ExecuteViewSubmitHandler { ); const preserveMessage = await modalInteraction.getInputElementState( - ActionButton.SEND_TO_PAGE_MESSAGE_ACTION + ActionButton.SEND_TO_NEW_PAGE_MESSAGE_ACTION ); if (preserveMessage) { diff --git a/src/handlers/Handler.ts b/src/handlers/Handler.ts index efb8f25..7aea233 100644 --- a/src/handlers/Handler.ts +++ b/src/handlers/Handler.ts @@ -257,12 +257,12 @@ export class Handler implements IHandler { } await modalInteraction.clearInputElementState( - ActionButton.SEND_TO_PAGE_MESSAGE_ACTION + ActionButton.SEND_TO_NEW_PAGE_MESSAGE_ACTION ); if (message) { await modalInteraction.storeInputElementState( - ActionButton.SEND_TO_PAGE_MESSAGE_ACTION, + ActionButton.SEND_TO_NEW_PAGE_MESSAGE_ACTION, message ); } From 4e1088261d9d22923e43d3a24401521f9bf09d7d Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Thu, 10 Aug 2023 12:11:03 +0530 Subject: [PATCH 14/31] fix: share page to get new created pages --- src/handlers/Handler.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/handlers/Handler.ts b/src/handlers/Handler.ts index 7aea233..2a30dc5 100644 --- a/src/handlers/Handler.ts +++ b/src/handlers/Handler.ts @@ -367,6 +367,9 @@ export class Handler implements IHandler { SharePage.VIEW_ID ); + const { workspace_id } = tokenInfo; + + await modalInteraction.clearPagesOrDatabase(workspace_id); await this.roomInteractionStorage.storeInteractionRoomId(roomId); const modal = await sharePageModal( From 09ec9fc238f2b0b4fbbfd5b03ae5188508510fc6 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Thu, 10 Aug 2023 12:11:42 +0530 Subject: [PATCH 15/31] feat: created sendMessagePageModal() --- enum/modals/SendMessagePage.ts | 11 +++++ src/modals/sendMessagePageModal.ts | 77 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 enum/modals/SendMessagePage.ts create mode 100644 src/modals/sendMessagePageModal.ts diff --git a/enum/modals/SendMessagePage.ts b/enum/modals/SendMessagePage.ts new file mode 100644 index 0000000..9f2ee64 --- /dev/null +++ b/enum/modals/SendMessagePage.ts @@ -0,0 +1,11 @@ +export enum SendMessagePage { + VIEW_ID = "notion-send-message-page-view-id", + ACTION_ID = "notion-send-message-page-action-id", + SEND_ACTION = "notion-send-message-page-send-action-id", + SEND_BLOCK = "notion-send-message-page-send-block-id", + CANCEL_ACTION = "notion-send-message-page-cancel-action-id", + CANCEL_BLOCK = "notion-send-message-page-cancel-block-id", + SEND = "Send", + CANCEL = "Cancel", + TITLE = "Send To Notion Page", +} diff --git a/src/modals/sendMessagePageModal.ts b/src/modals/sendMessagePageModal.ts new file mode 100644 index 0000000..045c26d --- /dev/null +++ b/src/modals/sendMessagePageModal.ts @@ -0,0 +1,77 @@ +import { + IModify, + IPersistence, + IRead, + IUIKitSurfaceViewParam, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { ITokenInfo } from "../../definition/authorization/IOAuth2Storage"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; +import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { NotionApp } from "../../NotionApp"; +import { Error } from "../../errors/Error"; +import { searchPageComponent } from "./common/searchPageComponent"; +import { ModalInteractionStorage } from "../storage/ModalInteraction"; +import { Block, TextObjectType } from "@rocket.chat/ui-kit"; +import { getConnectPreview } from "../helper/getConnectLayout"; +import { + ButtonStyle, + UIKitSurfaceType, +} from "@rocket.chat/apps-engine/definition/uikit"; +import { SendMessagePage } from "../../enum/modals/SendMessagePage"; + +export async function sendMessagePageModal( + app: NotionApp, + user: IUser, + read: IRead, + persistence: IPersistence, + modify: IModify, + room: IRoom, + modalInteraction: ModalInteractionStorage, + tokenInfo: ITokenInfo +): Promise { + const blocks: Block[] = []; + const { elementBuilder } = app.getUtils(); + const connectBlock = getConnectPreview(app.getID(), tokenInfo); + blocks.push(connectBlock); + + const searchForPageComponent = await searchPageComponent( + app, + modalInteraction, + tokenInfo, + SendMessagePage.ACTION_ID + ); + + if (searchForPageComponent instanceof Error) { + return searchForPageComponent; + } + + blocks.push(searchForPageComponent); + + const submit = elementBuilder.addButton( + { text: SendMessagePage.SEND, style: ButtonStyle.PRIMARY }, + { + actionId: SendMessagePage.SEND_ACTION, + blockId: SendMessagePage.SEND_BLOCK, + } + ); + + const close = elementBuilder.addButton( + { text: SendMessagePage.CANCEL, style: ButtonStyle.DANGER }, + { + actionId: SendMessagePage.CANCEL_ACTION, + blockId: SendMessagePage.CANCEL_BLOCK, + } + ); + + return { + id: SendMessagePage.VIEW_ID, + type: UIKitSurfaceType.MODAL, + title: { + type: TextObjectType.MRKDWN, + text: SendMessagePage.TITLE, + }, + blocks, + close, + submit, + }; +} From 87d8bfeeedf77470cbbcd4cab256b52cabb7a286 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Thu, 10 Aug 2023 12:12:35 +0530 Subject: [PATCH 16/31] feat: handled sendToNotionPage Action --- src/handlers/ExecuteActionButtonHandler.ts | 5 ++ src/handlers/Handler.ts | 62 ++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/handlers/ExecuteActionButtonHandler.ts b/src/handlers/ExecuteActionButtonHandler.ts index 55d45bd..96d4076 100644 --- a/src/handlers/ExecuteActionButtonHandler.ts +++ b/src/handlers/ExecuteActionButtonHandler.ts @@ -11,6 +11,7 @@ import { import { NotionApp } from "../../NotionApp"; import { ActionButton } from "../../enum/modals/common/ActionButtons"; import { Handler } from "./Handler"; +import { IMessage } from "@rocket.chat/apps-engine/definition/messages"; export class ExecuteActionButtonHandler { private context: UIKitActionButtonInteractionContext; @@ -49,6 +50,10 @@ export class ExecuteActionButtonHandler { await handler.createNotionPageOrRecord(false, message); break; } + case ActionButton.SEND_TO_PAGE_MESSAGE_ACTION: { + await handler.sendToNotionPage(message as IMessage); + break; + } } return this.context.getInteractionResponder().successResponse(); diff --git a/src/handlers/Handler.ts b/src/handlers/Handler.ts index 2a30dc5..8b59847 100644 --- a/src/handlers/Handler.ts +++ b/src/handlers/Handler.ts @@ -29,6 +29,8 @@ import { sharePageModal } from "../modals/sharePageModal"; import { SharePage } from "../../enum/modals/SharePage"; import { IMessage } from "@rocket.chat/apps-engine/definition/messages"; import { ActionButton } from "../../enum/modals/common/ActionButtons"; +import { SendMessagePage } from "../../enum/modals/SendMessagePage"; +import { sendMessagePageModal } from "../modals/sendMessagePageModal"; export class Handler implements IHandler { public app: NotionApp; @@ -397,4 +399,64 @@ export class Handler implements IHandler { .openSurfaceView(modal, { triggerId }, this.sender); } } + + public async sendToNotionPage(message: IMessage): Promise { + const userId = this.sender.id; + const roomId = this.room.id; + const tokenInfo = await this.oAuth2Storage.getCurrentWorkspace(userId); + + if (!tokenInfo) { + await sendNotificationWithConnectBlock( + this.app, + this.sender, + this.read, + this.modify, + this.room + ); + return; + } + + const persistenceRead = this.read.getPersistenceReader(); + const modalInteraction = new ModalInteractionStorage( + this.persis, + persistenceRead, + userId, + SendMessagePage.VIEW_ID + ); + + const { workspace_id } = tokenInfo; + + await modalInteraction.clearPagesOrDatabase(workspace_id); + await this.roomInteractionStorage.storeInteractionRoomId(roomId); + + const modal = await sendMessagePageModal( + this.app, + this.sender, + this.read, + this.persis, + this.modify, + this.room, + modalInteraction, + tokenInfo + ); + + if (modal instanceof Error) { + // Something went Wrong Probably SearchPageComponent Couldn't Fetch the Pages + this.app.getLogger().error(modal.message); + return; + } + + const triggerId = this.triggerId; + + if (triggerId) { + await modalInteraction.storeInputElementState( + ActionButton.SEND_TO_PAGE_MESSAGE_ACTION, + message + ); + + await this.modify + .getUiController() + .openSurfaceView(modal, { triggerId }, this.sender); + } + } } From e75a0eb1ee639e9dc388eeca17caa932a3e11767 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Thu, 10 Aug 2023 12:19:14 +0530 Subject: [PATCH 17/31] feat: handled sendToNotionPage Submit Action --- src/handlers/ExecuteViewSubmitHandler.ts | 115 +++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/src/handlers/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index 55ae792..0b81453 100644 --- a/src/handlers/ExecuteViewSubmitHandler.ts +++ b/src/handlers/ExecuteViewSubmitHandler.ts @@ -48,6 +48,7 @@ import { SearchPage } from "../../enum/modals/common/SearchPageComponent"; import { ActionButton } from "../../enum/modals/common/ActionButtons"; import { getCredentials } from "../helper/getCredential"; import { ICredential } from "../../definition/authorization/ICredential"; +import { SendMessagePage } from "../../enum/modals/SendMessagePage"; export class ExecuteViewSubmitHandler { private context: UIKitViewSubmitInteractionContext; @@ -119,6 +120,14 @@ export class ExecuteViewSubmitHandler { ); break; } + case SendMessagePage.VIEW_ID: { + return this.handleSendMessagePage( + room, + oAuth2Storage, + modalInteraction + ); + break; + } default: { } } @@ -802,4 +811,110 @@ export class ExecuteViewSubmitHandler { return this.context.getInteractionResponder().successResponse(); } + + public async handleSendMessagePage( + room: IRoom, + oAuth2Storage: OAuth2Storage, + modalInteraction: ModalInteractionStorage + ): Promise { + const { view, user } = this.context.getInteractionData(); + const { state } = view; + + const { NotionSdk } = this.app.getUtils(); + const tokenInfo = await oAuth2Storage.getCurrentWorkspace(user.id); + + if (!tokenInfo) { + await sendNotificationWithConnectBlock( + this.app, + user, + this.read, + this.modify, + room + ); + return this.context.getInteractionResponder().errorResponse(); + } + + const { workspace_name, owner, access_token } = tokenInfo; + const pageId: string | undefined = + state?.[SearchPage.BLOCK_ID]?.[SendMessagePage.ACTION_ID]; + + if (!pageId) { + return this.context.getInteractionResponder().viewErrorResponse({ + viewId: view.id, + errors: { + [SendMessagePage.ACTION_ID]: + "Please Select a Page to Share", + }, + }); + } + + const preserveMessage = await modalInteraction.getInputElementState( + ActionButton.SEND_TO_PAGE_MESSAGE_ACTION + ); + + if (preserveMessage) { + const preserveMessageContext = preserveMessage as { + id: string; + text: string; + room: IRoom; + }; + const { id, text } = preserveMessageContext; + const appendBlock = await NotionSdk.appendMessageBlock( + access_token, + text, + pageId + ); + + if (appendBlock instanceof Error) { + this.app.getLogger().error(appendBlock.message); + const message = `🚫 Something went wrong while appending message in **${workspace_name}**.`; + await sendNotification(this.read, this.modify, user, room, { + message, + }); + } else { + const pageInfo = await NotionSdk.retrievePage( + access_token, + pageId + ); + + if (pageInfo instanceof Error) { + this.app.getLogger().error(pageInfo.message); + return this.context + .getInteractionResponder() + .errorResponse(); + } + + const { name, url } = pageInfo; + const { type, displayName } = preserveMessageContext.room; + + const urlPath = + type === RoomType.CHANNEL + ? "channel" + : type === RoomType.PRIVATE_GROUP + ? "group" + : "direct"; + + const { siteUrl } = (await getCredentials( + this.read, + this.modify, + user, + room + )) as ICredential; + + const messageLink = `${siteUrl}/${urlPath}/${displayName}?msg=${id}`; + const preserveText = `📝 Preserved Following [Message](${messageLink}) in [**${name}**](${url}) `; + + await sendMessage( + this.read, + this.modify, + user, + room, + { message: preserveText }, + id + ); + } + } + + return this.context.getInteractionResponder().successResponse(); + } } From fbeb93c4534f9e7e9c4b6b486f0a531122069714 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Thu, 10 Aug 2023 18:29:29 +0530 Subject: [PATCH 18/31] feat: created searchDatabases() method in NotionSDK --- definition/lib/INotion.ts | 1 + enum/CommandParam.ts | 1 + enum/Notion.ts | 1 + enum/modals/NotionTable.ts | 11 ++ enum/modals/common/SearchDatabaseComponent.ts | 6 + package-lock.json | 171 +++++++++++++++++- package.json | 3 +- src/lib/NotionSDK.ts | 44 +++++ 8 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 enum/modals/NotionTable.ts create mode 100644 enum/modals/common/SearchDatabaseComponent.ts diff --git a/definition/lib/INotion.ts b/definition/lib/INotion.ts index 5e4c908..9416562 100644 --- a/definition/lib/INotion.ts +++ b/definition/lib/INotion.ts @@ -75,6 +75,7 @@ export interface INotionSDK extends INotion { message: string, blockId: string ): Promise; + searchDatabases(token: string): Promise | Error>; } export interface IParentPage { diff --git a/enum/CommandParam.ts b/enum/CommandParam.ts index 226369a..33c7f5c 100644 --- a/enum/CommandParam.ts +++ b/enum/CommandParam.ts @@ -7,6 +7,7 @@ export enum CommandParam { WORKSPACE = "workspace", WS = "ws", SHARE = "share", + VIEW = "view", } export enum SubCommandParam { diff --git a/enum/Notion.ts b/enum/Notion.ts index 257b11e..2568002 100644 --- a/enum/Notion.ts +++ b/enum/Notion.ts @@ -48,4 +48,5 @@ export enum NotionObjectTypes { NAME = "name", PROPERTIES = "properties", ID = "id", + DATABASE = "database", } diff --git a/enum/modals/NotionTable.ts b/enum/modals/NotionTable.ts new file mode 100644 index 0000000..69a3402 --- /dev/null +++ b/enum/modals/NotionTable.ts @@ -0,0 +1,11 @@ +export enum NotionTable { + VIEW_ID = "notion-view-table-view-id", + ACTION_ID = "notion-view-table-action-id", + VIEW_ACTION = "notion-view-table-view-action-id", + VIEW_BLOCK = "notion-view-table-view-block-id", + CANCEL_ACTION = "notion-view-table-cancel-action-id", + CANCEL_BLOCK = "notion-view-table-cancel-block-id", + VIEW = "View", + CANCEL = "Cancel", + TITLE = "View Notion Table", +} diff --git a/enum/modals/common/SearchDatabaseComponent.ts b/enum/modals/common/SearchDatabaseComponent.ts new file mode 100644 index 0000000..359fec6 --- /dev/null +++ b/enum/modals/common/SearchDatabaseComponent.ts @@ -0,0 +1,6 @@ +export enum SearchDatabaseComponent { + PLACEHOLDER = "Select Database", + BLOCK_ID = "search-database-component-block-id", + ACTION_ID = "search-database-component-action-id", + LABEL = "Database Name *", +} diff --git a/package-lock.json b/package-lock.json index 66d6f65..ce02d3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,8 @@ "packages": { "": { "dependencies": { - "@tryfabric/martian": "^1.2.4" + "@tryfabric/martian": "^1.2.4", + "table": "^6.8.1" }, "devDependencies": { "@rocket.chat/apps-engine": "^1.39.1", @@ -159,6 +160,29 @@ "node": ">=6.0" } }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -180,6 +204,14 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -359,6 +391,11 @@ "node": ">=0.3.1" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -386,6 +423,11 @@ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "node_modules/form-data": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", @@ -540,6 +582,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-hexadecimal": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", @@ -585,6 +635,11 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "node_modules/katex": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/katex/-/katex-0.12.0.tgz", @@ -602,6 +657,11 @@ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "dev": true }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==" + }, "node_modules/longest-streak": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", @@ -994,6 +1054,14 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "engines": { + "node": ">=6" + } + }, "node_modules/remark-gfm": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-1.0.0.tgz", @@ -1040,6 +1108,14 @@ "node": ">=0.10" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -1066,6 +1142,52 @@ "semver": "bin/semver" } }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -1081,6 +1203,30 @@ "node": "*" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1105,6 +1251,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/table": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", + "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -1231,6 +1392,14 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", diff --git a/package.json b/package.json index 455374f..2f73d46 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "typescript": "^4.0.5" }, "dependencies": { - "@tryfabric/martian": "^1.2.4" + "@tryfabric/martian": "^1.2.4", + "table": "^6.8.1" } } diff --git a/src/lib/NotionSDK.ts b/src/lib/NotionSDK.ts index 85ebbd4..2e3aff8 100644 --- a/src/lib/NotionSDK.ts +++ b/src/lib/NotionSDK.ts @@ -1057,4 +1057,48 @@ export class NotionSDK implements INotionSDK { throw new AppsEngineException(err as string); } } + + public async searchDatabases( + token: string + ): Promise | Error> { + try { + const response = await this.http.post(NotionApi.SEARCH, { + data: { + filter: { + value: NotionObjectTypes.DATABASE, + property: NotionObjectTypes.PROPERTY, + }, + }, + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": NotionApi.CONTENT_TYPE, + "User-Agent": NotionApi.USER_AGENT, + "Notion-Version": this.NotionVersion, + }, + }); + + if (!response.statusCode.toString().startsWith("2")) { + return this.handleErrorResponse( + response.statusCode, + `Error While Searching Databases: `, + response.content + ); + } + + const { results } = response.data; + + const result: Array = []; + results.forEach(async (item) => { + const objectType: string = item?.[NotionObjectTypes.OBJECT]; + const databaseObject = await this.getDatabaseObjectFromResults( + item + ); + + result.push(databaseObject); + }); + return result; + } catch (err) { + throw new AppsEngineException(err as string); + } + } } From 592f38688c71fa58bc74116a5d43733eed4be1f3 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Thu, 10 Aug 2023 18:31:30 +0530 Subject: [PATCH 19/31] feat: created searchDatabaseComponent() --- src/modals/common/searchDatabaseComponent.ts | 60 ++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/modals/common/searchDatabaseComponent.ts diff --git a/src/modals/common/searchDatabaseComponent.ts b/src/modals/common/searchDatabaseComponent.ts new file mode 100644 index 0000000..4b0335e --- /dev/null +++ b/src/modals/common/searchDatabaseComponent.ts @@ -0,0 +1,60 @@ +import { NotionApp } from "../../../NotionApp"; +import { InputBlock } from "@rocket.chat/ui-kit"; +import { Error } from "../../../errors/Error"; +import { StaticSelectOptionsParam } from "../../../definition/ui-kit/Element/IStaticSelectElement"; +import { ModalInteractionStorage } from "../../storage/ModalInteraction"; +import { IDatabase } from "../../../definition/lib/INotion"; +import { ITokenInfo } from "../../../definition/authorization/IOAuth2Storage"; +import { Modals } from "../../../enum/modals/common/Modals"; +import { NotionObjectTypes } from "../../../enum/Notion"; +import { SearchDatabaseComponent } from "../../../enum/modals/common/SearchDatabaseComponent"; +export async function searchDatabaseComponent( + app: NotionApp, + modalInteraction: ModalInteractionStorage, + tokenInfo: ITokenInfo, + actionId: string +): Promise { + const { NotionSdk, elementBuilder, blockBuilder } = app.getUtils(); + const { access_token, workspace_id } = tokenInfo; + let Databases; + Databases = await modalInteraction.getPagesOrDatabase(workspace_id); + if (!Databases) { + Databases = await NotionSdk.searchDatabases(access_token); + if (Databases instanceof Error) { + return Databases; + } + + await modalInteraction.storePagesOrDatabase(Databases, workspace_id); + } + const accesibleDatabase: Array = Databases; + + const options: StaticSelectOptionsParam = accesibleDatabase.map((item) => { + const info = NotionObjectTypes.INFO.toString(); + const name = NotionObjectTypes.NAME.toString(); + + const text: string = item?.[info]?.[name]; + const value = JSON.stringify(item); + + return { + text, + value, + }; + }); + + const dropDownOption = elementBuilder.createDropDownOptions(options); + const dropDown = elementBuilder.addDropDown( + { + placeholder: SearchDatabaseComponent.PLACEHOLDER, + options: dropDownOption, + dispatchActionConfig: [Modals.dispatchActionConfigOnSelect], + }, + { blockId: SearchDatabaseComponent.BLOCK_ID, actionId } + ); + const inputBlock = blockBuilder.createInputBlock({ + text: SearchDatabaseComponent.LABEL, + element: dropDown, + optional: false, + }); + + return inputBlock; +} From fa2b46bc1d928f7aecc0259c49cdca8b98df7d1f Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Thu, 10 Aug 2023 18:31:54 +0530 Subject: [PATCH 20/31] feat: created viewNotionTableModal() --- src/modals/viewNotionTableModal.ts | 80 ++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/modals/viewNotionTableModal.ts diff --git a/src/modals/viewNotionTableModal.ts b/src/modals/viewNotionTableModal.ts new file mode 100644 index 0000000..632794f --- /dev/null +++ b/src/modals/viewNotionTableModal.ts @@ -0,0 +1,80 @@ +import { + IModify, + IPersistence, + IRead, + IUIKitSurfaceViewParam, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { ITokenInfo } from "../../definition/authorization/IOAuth2Storage"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; +import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { NotionApp } from "../../NotionApp"; +import { Error } from "../../errors/Error"; +import { searchPageComponent } from "./common/searchPageComponent"; +import { ModalInteractionStorage } from "../storage/ModalInteraction"; +import { SharePage } from "../../enum/modals/SharePage"; +import { Block, TextObjectType } from "@rocket.chat/ui-kit"; +import { getConnectPreview } from "../helper/getConnectLayout"; +import { + ButtonStyle, + UIKitSurfaceType, +} from "@rocket.chat/apps-engine/definition/uikit"; +import { searchDatabaseComponent } from "./common/searchDatabaseComponent"; +import { SearchDatabaseComponent } from "../../enum/modals/common/SearchDatabaseComponent"; +import { NotionTable } from "../../enum/modals/NotionTable"; + +export async function viewNotionTableModal( + app: NotionApp, + user: IUser, + read: IRead, + persistence: IPersistence, + modify: IModify, + room: IRoom, + modalInteraction: ModalInteractionStorage, + tokenInfo: ITokenInfo +): Promise { + const blocks: Block[] = []; + const { elementBuilder } = app.getUtils(); + const connectBlock = getConnectPreview(app.getID(), tokenInfo); + blocks.push(connectBlock); + + const searchForDatabaseComponent = await searchDatabaseComponent( + app, + modalInteraction, + tokenInfo, + NotionTable.ACTION_ID + ); + + if (searchForDatabaseComponent instanceof Error) { + return searchForDatabaseComponent; + } + + blocks.push(searchForDatabaseComponent); + + const submit = elementBuilder.addButton( + { text: NotionTable.VIEW, style: ButtonStyle.PRIMARY }, + { + actionId: NotionTable.VIEW_ACTION, + blockId: NotionTable.VIEW_BLOCK, + } + ); + + const close = elementBuilder.addButton( + { text: NotionTable.CANCEL, style: ButtonStyle.DANGER }, + { + actionId: NotionTable.CANCEL_ACTION, + blockId: NotionTable.CANCEL_BLOCK, + } + ); + + return { + id: NotionTable.VIEW_ID, + type: UIKitSurfaceType.MODAL, + title: { + type: TextObjectType.MRKDWN, + text: NotionTable.TITLE, + }, + blocks, + close, + submit, + }; +} From 212096adeb6362cd6859b21fdf60b060fb513751 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Thu, 10 Aug 2023 18:33:54 +0530 Subject: [PATCH 21/31] feat: created queryDatabasePages() in NotionSDK --- definition/lib/INotion.ts | 4 + src/lib/NotionSDK.ts | 285 +++++++++++++++++++++++++++++++++++++- 2 files changed, 287 insertions(+), 2 deletions(-) diff --git a/definition/lib/INotion.ts b/definition/lib/INotion.ts index 9416562..01ee7b2 100644 --- a/definition/lib/INotion.ts +++ b/definition/lib/INotion.ts @@ -76,6 +76,10 @@ export interface INotionSDK extends INotion { blockId: string ): Promise; searchDatabases(token: string): Promise | Error>; + queryDatabasePages( + token: string, + databaseId: string + ): Promise> | Error>; } export interface IParentPage { diff --git a/src/lib/NotionSDK.ts b/src/lib/NotionSDK.ts index 2e3aff8..618033a 100644 --- a/src/lib/NotionSDK.ts +++ b/src/lib/NotionSDK.ts @@ -2,7 +2,6 @@ import { ICommentInfo, ICommentObject, IDatabase, - IDatabaseProperties, INotionDatabase, INotionPage, INotionSDK, @@ -28,7 +27,6 @@ import { ServerError, } from "../../errors/Error"; import { - Notion, NotionApi, NotionObjectTypes, NotionOwnerType, @@ -1101,4 +1099,287 @@ export class NotionSDK implements INotionSDK { throw new AppsEngineException(err as string); } } + + public async queryDatabasePages( + token: string, + databaseId: string + ): Promise> | Error> { + try { + const response = await this.http.post( + NotionApi.CREATE_DATABASE + `/${databaseId}/query`, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": NotionApi.CONTENT_TYPE, + "User-Agent": NotionApi.USER_AGENT, + "Notion-Version": this.NotionVersion, + }, + } + ); + + if (!response.statusCode.toString().startsWith("2")) { + return this.handleErrorResponse( + response.statusCode, + `Error While quering Database Pages List: `, + response.content + ); + } + const results = response.data?.results; + return this.getDatabaseTable(results); + } catch (err) { + throw new AppsEngineException(err as string); + } + } + + private async getDatabaseTable( + results: Array + ): Promise>> { + const tableData: Array> = []; + + if (results?.length > 0) { + for (let i = 0; i <= results.length; i++) { + tableData.push([]); + } + + const properties = results[0]?.[NotionObjectTypes.PROPERTIES]; + let propertyKeys = Object.keys(properties); + + const supportedProperties = Object.values( + PropertyTypeValue + ) as string[]; + supportedProperties.push(NotionObjectTypes.TITLE); + supportedProperties.push("status"); + + propertyKeys = propertyKeys.filter((propKey) => { + return supportedProperties.includes( + properties[propKey]?.[NotionObjectTypes.TYPE] + ); + }); + + tableData[0].push(...propertyKeys); + + results?.forEach(async (item, index) => { + const propertyItem: object = + item?.[NotionObjectTypes.PROPERTIES]; + + propertyKeys.forEach(async (key) => { + const propertyValueObject = propertyItem?.[key]; + const type: string = + propertyValueObject?.[NotionObjectTypes.TYPE]; + switch (type) { + case NotionObjectTypes.TITLE: { + const richText = propertyValueObject?.[type]; + const markdown = + richText[0]?.[NotionObjectTypes.TEXT]?.[ + "content" + ] ?? ""; + tableData[index + 1].push(markdown); + break; + } + case PropertyTypeValue.CHECKBOX: { + const checkbox: boolean = + propertyValueObject?.[type]; + if (checkbox) { + tableData[index + 1].push("✅"); + } else { + tableData[index + 1].push("❎"); + } + break; + } + case PropertyTypeValue.TEXT: { + const richText = propertyValueObject?.[type]; + const markdown = + richText[0]?.[NotionObjectTypes.TEXT]?.[ + "content" + ] ?? ""; + tableData[index + 1].push(markdown); + break; + } + case PropertyTypeValue.NUMBER: { + const value: number | undefined = + propertyValueObject?.[type]; + if (value) { + tableData[index + 1].push(value.toString()); + } else { + tableData[index + 1].push(""); + } + + break; + } + case PropertyTypeValue.URL: { + const url: string | null = + propertyValueObject?.[type]; + + if (url) { + tableData[index + 1].push(url); + } else { + tableData[index + 1].push(""); + } + + break; + } + case PropertyTypeValue.EMAIL: { + const email: string | null = + propertyValueObject?.[type]; + + if (email) { + tableData[index + 1].push(email); + } else { + tableData[index + 1].push(""); + } + break; + } + case PropertyTypeValue.PHONE_NUMBER: { + const phoneNumber: string | null = + propertyValueObject?.[type]; + if (phoneNumber) { + tableData[index + 1].push(phoneNumber); + } else { + tableData[index + 1].push(""); + } + break; + } + case PropertyTypeValue.DATE: { + const date: object | null = + propertyValueObject?.[type]; + if (date) { + const value = date?.["start"]; + tableData[index + 1].push(value); + } else { + tableData[index + 1].push(""); + } + break; + } + case PropertyTypeValue.SELECT: { + const select: object | null = + propertyValueObject?.[type]; + + if (select) { + const selectValue = + select?.[NotionObjectTypes.NAME]; + tableData[index + 1].push(selectValue); + } else { + tableData[index + 1].push(""); + } + break; + } + case PropertyTypeValue.PEOPLE: { + const people: Array | null = + propertyValueObject?.[type]; + + let fieldValue = ""; + if (people && people.length) { + const fullLength = people.length; + people.forEach((element, eleindex) => { + const name: string = + element?.[NotionObjectTypes.NAME]; + fieldValue += `${name}`; + + if (eleindex < fullLength - 1) { + fieldValue += ", "; + } + }); + } + + tableData[index + 1].push(fieldValue); + break; + } + case PropertyTypeValue.MULTI_SELECT: { + const multiSelect: Array<{ + id: string; + name: string; + color: string; + }> | null = propertyValueObject?.[type]; + + let MultiSelectValue = ""; + + if (multiSelect && multiSelect.length) { + const fullLength = multiSelect.length; + multiSelect.forEach((element, index) => { + const name: string = + element?.[NotionObjectTypes.NAME]; + MultiSelectValue += `${name}`; + + if (index < fullLength - 1) { + MultiSelectValue += ", "; + } + }); + } + tableData[index + 1].push(MultiSelectValue); + break; + } + case "status": { + const status: object | null = + propertyValueObject?.[type]; + if (status) { + const statusValue = + status?.[NotionObjectTypes.NAME]; + tableData[index + 1].push(statusValue); + } else { + tableData[index + 1].push(""); + } + break; + } + case PropertyTypeValue.CREATED_BY: { + const createdBy = propertyValueObject?.[type]; + const name: string = + createdBy?.[NotionObjectTypes.NAME]; + tableData[index + 1].push(createdBy); + break; + } + case PropertyTypeValue.CREATED_TIME: { + const createdTime = propertyValueObject?.[type]; + tableData[index + 1].push(createdTime); + break; + } + case PropertyTypeValue.LAST_EDITED_TIME: { + const lastEditedTime = propertyValueObject?.[type]; + tableData[index + 1].push(lastEditedTime); + break; + } + case PropertyTypeValue.LAST_EDITED_BY: { + const lastEditedBy = propertyValueObject?.[type]; + const name: string = + lastEditedBy?.[NotionObjectTypes.NAME]; + tableData[index + 1].push(lastEditedBy); + break; + } + case PropertyTypeValue.FORMULA: { + const formula = propertyValueObject?.[type]; + const value: string | null = formula?.["string"]; + if (value) { + tableData[index + 1].push(value); + } else { + tableData[index + 1].push(""); + } + break; + } + case PropertyTypeValue.FILES: { + const files: Array | null = + propertyValueObject?.[type]; + let filesLink = ""; + + if (files && files.length) { + files.forEach((file, index) => { + const name: string = + file?.[NotionObjectTypes.NAME]; + const url: string = + file?.["file"]?.[PropertyTypeValue.URL]; + + filesLink += `${name}`; + + if (index < files.length - 1) { + filesLink += ", "; + } + }); + } + tableData[index + 1].push(filesLink); + break; + } + } + }); + }); + } + return tableData; + } } From 9164165737ea19be863012b1a72bb602d5af889f Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Thu, 10 Aug 2023 18:40:29 +0530 Subject: [PATCH 22/31] feat: handled viewNotionTable with View command Param --- src/commands/CommandUtility.ts | 4 +++ src/handlers/Handler.ts | 58 +++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/commands/CommandUtility.ts b/src/commands/CommandUtility.ts index 175459c..eb8c1a1 100644 --- a/src/commands/CommandUtility.ts +++ b/src/commands/CommandUtility.ts @@ -123,6 +123,10 @@ export class CommandUtility implements ICommandUtility { await handler.shareNotionPage(); break; } + case CommandParam.VIEW : { + await handler.viewNotionTable(); + break; + } case CommandParam.HELP: default: { await sendHelperNotification( diff --git a/src/handlers/Handler.ts b/src/handlers/Handler.ts index 8b59847..ad3b741 100644 --- a/src/handlers/Handler.ts +++ b/src/handlers/Handler.ts @@ -31,6 +31,8 @@ import { IMessage } from "@rocket.chat/apps-engine/definition/messages"; import { ActionButton } from "../../enum/modals/common/ActionButtons"; import { SendMessagePage } from "../../enum/modals/SendMessagePage"; import { sendMessagePageModal } from "../modals/sendMessagePageModal"; +import { NotionTable } from "../../enum/modals/NotionTable"; +import { viewNotionTableModal } from "../modals/viewNotionTableModal"; export class Handler implements IHandler { public app: NotionApp; @@ -459,4 +461,58 @@ export class Handler implements IHandler { .openSurfaceView(modal, { triggerId }, this.sender); } } -} + + public async viewNotionTable(): Promise { + const userId = this.sender.id; + const roomId = this.room.id; + const tokenInfo = await this.oAuth2Storage.getCurrentWorkspace(userId); + + if (!tokenInfo) { + await sendNotificationWithConnectBlock( + this.app, + this.sender, + this.read, + this.modify, + this.room + ); + return; + } + await this.roomInteractionStorage.storeInteractionRoomId(roomId); + + const persistenceRead = this.read.getPersistenceReader(); + const modalInteraction = new ModalInteractionStorage( + this.persis, + persistenceRead, + userId, + NotionTable.VIEW_ID + ); + + const { workspace_id } = tokenInfo; + await modalInteraction.clearPagesOrDatabase(workspace_id); + + const modal = await viewNotionTableModal( + this.app, + this.sender, + this.read, + this.persis, + this.modify, + this.room, + modalInteraction, + tokenInfo + ); + + if (modal instanceof Error) { + // Something went Wrong Probably SearchPageComponent Couldn't Fetch the Pages + this.app.getLogger().error(modal.message); + return; + } + + const triggerId = this.triggerId; + + if (triggerId) { + await this.modify + .getUiController() + .openSurfaceView(modal, { triggerId }, this.sender); + } + } +} \ No newline at end of file From 11d8e3ef201f83181d474a77efbedb82a19d0934 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Thu, 10 Aug 2023 18:41:21 +0530 Subject: [PATCH 23/31] fix: handled required elements with view errors --- src/handlers/ExecuteBlockActionHandler.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/handlers/ExecuteBlockActionHandler.ts b/src/handlers/ExecuteBlockActionHandler.ts index cc9e7a0..d5fdd90 100644 --- a/src/handlers/ExecuteBlockActionHandler.ts +++ b/src/handlers/ExecuteBlockActionHandler.ts @@ -49,6 +49,8 @@ import { NotionWorkspace } from "../../enum/modals/NotionWorkspace"; import { changeWorkspaceModal } from "../modals/changeWorkspaceModal"; import { getPropertiesIdsObject } from "../helper/getPropertiesIdsObject"; import { SharePage } from "../../enum/modals/SharePage"; +import { NotionTable } from "../../enum/modals/NotionTable"; +import { SendMessagePage } from "../../enum/modals/SendMessagePage"; export class ExecuteBlockActionHandler { private context: UIKitBlockInteractionContext; @@ -165,10 +167,15 @@ export class ExecuteBlockActionHandler { ); break; } + case SendMessagePage.ACTION_ID: case SharePage.ACTION_ID: { return this.handleSelectPageAction(); break; } + case NotionTable.ACTION_ID: { + return this.handleSelectDatabaseAction(); + break; + } default: { // Property Type Select Action const propertyTypeSelected = @@ -986,6 +993,14 @@ export class ExecuteBlockActionHandler { } private async handleSelectPageAction(): Promise { + return this.removeViewError(); + } + + private async handleSelectDatabaseAction(): Promise { + return this.removeViewError(); + } + + private async removeViewError(): Promise { const { value, container } = this.context.getInteractionData(); return this.context.getInteractionResponder().viewErrorResponse({ From fd6c6a1be25bb69e3828f6612ad49e80c74deebf Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Thu, 10 Aug 2023 18:42:59 +0530 Subject: [PATCH 24/31] feat: handled submit action of viewTable --- src/handlers/ExecuteViewSubmitHandler.ts | 80 ++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/handlers/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index 0b81453..cb2f2d5 100644 --- a/src/handlers/ExecuteViewSubmitHandler.ts +++ b/src/handlers/ExecuteViewSubmitHandler.ts @@ -49,6 +49,9 @@ import { ActionButton } from "../../enum/modals/common/ActionButtons"; import { getCredentials } from "../helper/getCredential"; import { ICredential } from "../../definition/authorization/ICredential"; import { SendMessagePage } from "../../enum/modals/SendMessagePage"; +import { NotionTable } from "../../enum/modals/NotionTable"; +import { SearchDatabaseComponent } from "../../enum/modals/common/SearchDatabaseComponent"; +import { table } from "table"; export class ExecuteViewSubmitHandler { private context: UIKitViewSubmitInteractionContext; @@ -128,6 +131,14 @@ export class ExecuteViewSubmitHandler { ); break; } + case NotionTable.VIEW_ID: { + return this.handleViewTable( + room, + oAuth2Storage, + modalInteraction + ); + break; + } default: { } } @@ -917,4 +928,73 @@ export class ExecuteViewSubmitHandler { return this.context.getInteractionResponder().successResponse(); } + + public async handleViewTable( + room: IRoom, + oAuth2Storage: OAuth2Storage, + modalInteraction: ModalInteractionStorage + ): Promise { + const { view, user } = this.context.getInteractionData(); + const { state } = view; + + const { NotionSdk } = this.app.getUtils(); + const tokenInfo = await oAuth2Storage.getCurrentWorkspace(user.id); + + if (!tokenInfo) { + await sendNotificationWithConnectBlock( + this.app, + user, + this.read, + this.modify, + room + ); + return this.context.getInteractionResponder().errorResponse(); + } + + const databaseObject: string | undefined = + state?.[SearchDatabaseComponent.BLOCK_ID]?.[NotionTable.ACTION_ID]; + + if (!databaseObject) { + return this.context.getInteractionResponder().viewErrorResponse({ + viewId: view.id, + errors: { + [NotionTable.ACTION_ID]: "Please Select a Database to View", + }, + }); + } + const { workspace_name, owner, access_token } = tokenInfo; + const DatabaseInfo: IDatabase = JSON.parse(databaseObject); + const { parent, info } = DatabaseInfo; + const { database_id } = parent; + const { name } = info; + + const response = await NotionSdk.queryDatabasePages( + access_token, + database_id + ); + + if (response instanceof Error) { + return this.context.getInteractionResponder().errorResponse(); + } + + const tableString = table(response, { + columnDefault: { + verticalAlignment: "middle", + wrapWord: true, + truncate: 100, + }, + header: { + alignment: "center", + content: name, + }, + }); + + const tableText = `\`\`\`\n${tableString}\n\`\`\``; + + await sendNotification(this.read, this.modify, user, room, { + message: tableText, + }); + + return this.context.getInteractionResponder().successResponse(); + } } From 6a8d013cc4a467a8438454b7f931b9a115baef1c Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 15 Aug 2023 15:23:58 +0530 Subject: [PATCH 25/31] feat: register view-page command --- enum/CommandParam.ts | 1 + enum/Notion.ts | 2 ++ enum/modals/NotionPage.ts | 11 ++++++++++ enum/modals/common/PageContent.ts | 35 +++++++++++++++++++++++++++++++ src/commands/CommandUtility.ts | 4 ++++ src/handlers/Handler.ts | 3 +++ 6 files changed, 56 insertions(+) create mode 100644 enum/modals/NotionPage.ts create mode 100644 enum/modals/common/PageContent.ts diff --git a/enum/CommandParam.ts b/enum/CommandParam.ts index 33c7f5c..25bb462 100644 --- a/enum/CommandParam.ts +++ b/enum/CommandParam.ts @@ -8,6 +8,7 @@ export enum CommandParam { WS = "ws", SHARE = "share", VIEW = "view", + VIEW_PAGE = "vp", } export enum SubCommandParam { diff --git a/enum/Notion.ts b/enum/Notion.ts index 2568002..d57c444 100644 --- a/enum/Notion.ts +++ b/enum/Notion.ts @@ -49,4 +49,6 @@ export enum NotionObjectTypes { PROPERTIES = "properties", ID = "id", DATABASE = "database", + ICON = "icon", + EMOJI = "emoji", } diff --git a/enum/modals/NotionPage.ts b/enum/modals/NotionPage.ts new file mode 100644 index 0000000..9befc49 --- /dev/null +++ b/enum/modals/NotionPage.ts @@ -0,0 +1,11 @@ +export enum NotionPage { + VIEW_ID = "notion-view-page-view-id", + ACTION_ID = "notion-view-page-action-id", + VIEW_ACTION = "notion-view-page-view-action-id", + VIEW_BLOCK = "notion-view-page-view-block-id", + CANCEL_ACTION = "notion-view-page-cancel-action-id", + CANCEL_BLOCK = "notion-view-page-cancel-block-id", + VIEW = "View", + CANCEL = "Cancel", + TITLE = "View Notion Page", +} diff --git a/enum/modals/common/PageContent.ts b/enum/modals/common/PageContent.ts new file mode 100644 index 0000000..8d09061 --- /dev/null +++ b/enum/modals/common/PageContent.ts @@ -0,0 +1,35 @@ +export enum PageContent { + BOOKMARK = "bookmark", + BREADCRUMB = "breadcrumb", + BULLET_LIST_ITEM = "bulleted_list_item", + CALLOUT = "callout", + CHILD_PAGE = "child_page", + CHILD_DATABASE = "child_database", + CODE = "code", + COLUMN = "column", + COLUMN_LIST = "column_list", + DIVIDER = "divider", + EMBED = "embed", + EQUATION = "equation", + FILE = "file", + HEADING_1 = "heading_1", + HEADING_2 = "heading_2", + HEADING_3 = "heading_3", + IMAGE = "image", + LINK_PREVIEW = "link_preview", + NUMBER_LIST_ITEM = "numbered_list_item", + PARAGRAPH = "paragraph", + PDF = "pdf", + QUOTE = "quote", + SYNCED_BLOCK = "synced_block", + TABLE = "table", + TABLE_OF_CONTENTS = "table_of_contents", + TODO = "to_do", + TOGGLE_BLOCKS = "toggle", + VIDEO = "video", + // Mention Left +} + +export enum FileType { + EXTERNAL = "external", +} diff --git a/src/commands/CommandUtility.ts b/src/commands/CommandUtility.ts index eb8c1a1..5c74f5c 100644 --- a/src/commands/CommandUtility.ts +++ b/src/commands/CommandUtility.ts @@ -127,6 +127,10 @@ export class CommandUtility implements ICommandUtility { await handler.viewNotionTable(); break; } + case CommandParam.VIEW_PAGE: { + await handler.viewNotionPage(); + break; + } case CommandParam.HELP: default: { await sendHelperNotification( diff --git a/src/handlers/Handler.ts b/src/handlers/Handler.ts index ad3b741..e68d8d7 100644 --- a/src/handlers/Handler.ts +++ b/src/handlers/Handler.ts @@ -515,4 +515,7 @@ export class Handler implements IHandler { .openSurfaceView(modal, { triggerId }, this.sender); } } + + public async viewNotionPage(): Promise { + } } \ No newline at end of file From 6bb172a4c1af89dfc113ece0924c7c852e439324 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 15 Aug 2023 15:24:40 +0530 Subject: [PATCH 26/31] feat: created viewNotionPageModal() --- src/modals/viewNotionPageModal.ts | 77 +++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/modals/viewNotionPageModal.ts diff --git a/src/modals/viewNotionPageModal.ts b/src/modals/viewNotionPageModal.ts new file mode 100644 index 0000000..0d7897d --- /dev/null +++ b/src/modals/viewNotionPageModal.ts @@ -0,0 +1,77 @@ +import { + IModify, + IPersistence, + IRead, + IUIKitSurfaceViewParam, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { ITokenInfo } from "../../definition/authorization/IOAuth2Storage"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; +import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { NotionApp } from "../../NotionApp"; +import { Error } from "../../errors/Error"; +import { searchPageComponent } from "./common/searchPageComponent"; +import { ModalInteractionStorage } from "../storage/ModalInteraction"; +import { Block, TextObjectType } from "@rocket.chat/ui-kit"; +import { getConnectPreview } from "../helper/getConnectLayout"; +import { + ButtonStyle, + UIKitSurfaceType, +} from "@rocket.chat/apps-engine/definition/uikit"; +import { NotionPage } from "../../enum/modals/NotionPage"; + +export async function viewNotionPageModal( + app: NotionApp, + user: IUser, + read: IRead, + persistence: IPersistence, + modify: IModify, + room: IRoom, + modalInteraction: ModalInteractionStorage, + tokenInfo: ITokenInfo +): Promise { + const blocks: Block[] = []; + const { elementBuilder } = app.getUtils(); + const connectBlock = getConnectPreview(app.getID(), tokenInfo); + blocks.push(connectBlock); + + const searchForPageComponent = await searchPageComponent( + app, + modalInteraction, + tokenInfo, + NotionPage.ACTION_ID + ); + + if (searchForPageComponent instanceof Error) { + return searchForPageComponent; + } + + blocks.push(searchForPageComponent); + + const submit = elementBuilder.addButton( + { text: NotionPage.VIEW, style: ButtonStyle.PRIMARY }, + { + actionId: NotionPage.VIEW_ACTION, + blockId: NotionPage.VIEW_BLOCK, + } + ); + + const close = elementBuilder.addButton( + { text: NotionPage.CANCEL, style: ButtonStyle.DANGER }, + { + actionId: NotionPage.CANCEL_ACTION, + blockId: NotionPage.CANCEL_BLOCK, + } + ); + + return { + id: NotionPage.VIEW_ID, + type: UIKitSurfaceType.MODAL, + title: { + type: TextObjectType.MRKDWN, + text: NotionPage.TITLE, + }, + blocks, + close, + submit, + }; +} From bc78433fcd3b5ac383993763ecff3b1d8168c75d Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 15 Aug 2023 15:26:04 +0530 Subject: [PATCH 27/31] feat: handled viewNotionPage command Action --- src/handlers/ExecuteBlockActionHandler.ts | 2 + src/handlers/Handler.ts | 53 +++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/handlers/ExecuteBlockActionHandler.ts b/src/handlers/ExecuteBlockActionHandler.ts index d5fdd90..df4a352 100644 --- a/src/handlers/ExecuteBlockActionHandler.ts +++ b/src/handlers/ExecuteBlockActionHandler.ts @@ -51,6 +51,7 @@ import { getPropertiesIdsObject } from "../helper/getPropertiesIdsObject"; import { SharePage } from "../../enum/modals/SharePage"; import { NotionTable } from "../../enum/modals/NotionTable"; import { SendMessagePage } from "../../enum/modals/SendMessagePage"; +import { NotionPage } from "../../enum/modals/NotionPage"; export class ExecuteBlockActionHandler { private context: UIKitBlockInteractionContext; @@ -167,6 +168,7 @@ export class ExecuteBlockActionHandler { ); break; } + case NotionPage.ACTION_ID: case SendMessagePage.ACTION_ID: case SharePage.ACTION_ID: { return this.handleSelectPageAction(); diff --git a/src/handlers/Handler.ts b/src/handlers/Handler.ts index e68d8d7..4a39b2d 100644 --- a/src/handlers/Handler.ts +++ b/src/handlers/Handler.ts @@ -33,6 +33,8 @@ import { SendMessagePage } from "../../enum/modals/SendMessagePage"; import { sendMessagePageModal } from "../modals/sendMessagePageModal"; import { NotionTable } from "../../enum/modals/NotionTable"; import { viewNotionTableModal } from "../modals/viewNotionTableModal"; +import { NotionPage } from "../../enum/modals/NotionPage"; +import { viewNotionPageModal } from "../modals/viewNotionPageModal"; export class Handler implements IHandler { public app: NotionApp; @@ -517,5 +519,56 @@ export class Handler implements IHandler { } public async viewNotionPage(): Promise { + const userId = this.sender.id; + const roomId = this.room.id; + const tokenInfo = await this.oAuth2Storage.getCurrentWorkspace(userId); + + if (!tokenInfo) { + await sendNotificationWithConnectBlock( + this.app, + this.sender, + this.read, + this.modify, + this.room + ); + return; + } + await this.roomInteractionStorage.storeInteractionRoomId(roomId); + + const persistenceRead = this.read.getPersistenceReader(); + const modalInteraction = new ModalInteractionStorage( + this.persis, + persistenceRead, + userId, + NotionPage.VIEW_ID + ); + + const { workspace_id } = tokenInfo; + await modalInteraction.clearPagesOrDatabase(workspace_id); + + const modal = await viewNotionPageModal( + this.app, + this.sender, + this.read, + this.persis, + this.modify, + this.room, + modalInteraction, + tokenInfo + ); + + if (modal instanceof Error) { + // Something went Wrong Probably SearchPageComponent Couldn't Fetch the Pages + this.app.getLogger().error(modal.message); + return; + } + + const triggerId = this.triggerId; + + if (triggerId) { + await this.modify + .getUiController() + .openSurfaceView(modal, { triggerId }, this.sender); + } } } \ No newline at end of file From acca2a615b0cedc57f9ec4c6ef06c6c7860cbb28 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 15 Aug 2023 15:26:46 +0530 Subject: [PATCH 28/31] feat: created retrievePageContent() in SDK --- definition/lib/INotion.ts | 1 + src/lib/NotionSDK.ts | 259 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 260 insertions(+) diff --git a/definition/lib/INotion.ts b/definition/lib/INotion.ts index 01ee7b2..45df08b 100644 --- a/definition/lib/INotion.ts +++ b/definition/lib/INotion.ts @@ -80,6 +80,7 @@ export interface INotionSDK extends INotion { token: string, databaseId: string ): Promise> | Error>; + retrievePageContent(token: string, pageId: string): Promise; } export interface IParentPage { diff --git a/src/lib/NotionSDK.ts b/src/lib/NotionSDK.ts index 618033a..84ad477 100644 --- a/src/lib/NotionSDK.ts +++ b/src/lib/NotionSDK.ts @@ -27,6 +27,7 @@ import { ServerError, } from "../../errors/Error"; import { + Notion, NotionApi, NotionObjectTypes, NotionOwnerType, @@ -46,6 +47,8 @@ import { PropertyTypeValue, } from "../../enum/modals/common/NotionProperties"; import { IMessageAttachmentField } from "@rocket.chat/apps-engine/definition/messages"; +import { FileType, PageContent } from "../../enum/modals/common/PageContent"; +import { TextObjectType } from "@rocket.chat/ui-kit"; export class NotionSDK implements INotionSDK { baseUrl: string; @@ -1382,4 +1385,260 @@ export class NotionSDK implements INotionSDK { } return tableData; } + + public async retrievePageContent( + token: string, + pageId: string + ): Promise { + try { + const response = await this.http.get( + NotionApi.BLOCKS + `/${pageId}/children`, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": NotionApi.CONTENT_TYPE, + "User-Agent": NotionApi.USER_AGENT, + "Notion-Version": this.NotionVersion, + }, + } + ); + + if (!response.statusCode.toString().startsWith("2")) { + return this.handleErrorResponse( + response.statusCode, + `Error While retrieving Page Content: `, + response.content + ); + } + + const results: Array = response.data?.results; + return this.getPageContentInMarkdwn(results); + } catch (err) { + throw new AppsEngineException(err as string); + } + } + + private async getPageContentInMarkdwn(results): Promise { + let pageContent = ""; + console.log(results); + results?.forEach((result) => { + const type = result?.[NotionObjectTypes.TYPE]; + const propertyObject = result?.[type]; + switch (type) { + case PageContent.BOOKMARK: { + const bookmark: string | undefined = + propertyObject?.[PropertyTypeValue.URL]; + + if (bookmark) { + pageContent += `[bookmark](${bookmark})\n`; + } + + break; + } + case PageContent.BREADCRUMB: { + // not supported yet from notion + break; + } + case PageContent.NUMBER_LIST_ITEM: + case PageContent.BULLET_LIST_ITEM: { + const bulletList = propertyObject?.[PropertyTypeValue.TEXT]; + if (bulletList) { + const markdown = + bulletList[0]?.[TextObjectType.PLAIN_TEXT] ?? ""; + pageContent += `- ${markdown}\n`; + } + break; + } + case PageContent.CALLOUT: { + const callout = propertyObject?.[PropertyTypeValue.TEXT]; + const emoji = + propertyObject?.[NotionObjectTypes.ICON]?.[ + NotionObjectTypes.EMOJI + ]; + if (callout) { + const markdown = + callout[0]?.[TextObjectType.PLAIN_TEXT] ?? ""; + pageContent += + "> " + + `${emoji ? `${emoji} ` : ""}` + + `${markdown}\n`; + } + break; + } + case PageContent.CHILD_PAGE: { + break; + } + case PageContent.CHILD_DATABASE: { + break; + } + case PageContent.CODE: { + const code = propertyObject?.[PropertyTypeValue.TEXT]; + if (code) { + const markdown = + code[0]?.[TextObjectType.PLAIN_TEXT] ?? ""; + pageContent += `\`\`\`\n${markdown}\n\`\`\`\n`; + } + break; + } + case PageContent.COLUMN: { + console.log("column", propertyObject); + break; + } + case PageContent.COLUMN_LIST: { + console.log("column list", propertyObject); + break; + } + case PageContent.DIVIDER: { + pageContent += `\n`; + break; + } + case PageContent.EMBED: { + break; + } + case PageContent.EQUATION: { + const expression = + propertyObject?.[NotionObjectTypes.EXPRESSION]; + if (expression) { + pageContent += `\\[${expression}\\]\n`; + } + break; + } + case PageContent.FILE: { + const file: string | undefined = + propertyObject?.[PropertyTypeValue.URL]; + if (file) { + pageContent += `[file](${file})\n`; + } + break; + } + case PageContent.HEADING_1: { + const heading1 = propertyObject?.[PropertyTypeValue.TEXT]; + + if (heading1) { + const markdown = + heading1[0]?.[NotionObjectTypes.TEXT]?.[ + "content" + ] ?? ""; + + pageContent += `# ${markdown}\n`; + } + break; + } + case PageContent.HEADING_2: { + const heading2 = propertyObject?.[PropertyTypeValue.TEXT]; + + if (heading2) { + const markdown = + heading2[0]?.[NotionObjectTypes.TEXT]?.[ + "content" + ] ?? ""; + + pageContent += `## ${markdown}\n`; + } + break; + } + case PageContent.HEADING_3: { + const heading3 = propertyObject?.[PropertyTypeValue.TEXT]; + + if (heading3) { + const markdown = + heading3[0]?.[NotionObjectTypes.TEXT]?.[ + "content" + ] ?? ""; + + pageContent += `### ${markdown}\n`; + } + break; + } + case PageContent.IMAGE: { + const file: { url: string } | undefined = + propertyObject?.[PageContent.FILE]; + if (file) { + pageContent += `![file](${file.url})\n`; + } + break; + } + case PageContent.LINK_PREVIEW: { + console.log("link preview", propertyObject); + break; + } + case PageContent.PARAGRAPH: { + console.log("paragraph", propertyObject); + const paragraph = propertyObject?.[PropertyTypeValue.TEXT]; + if (paragraph) { + const markdown = + paragraph[0]?.[TextObjectType.PLAIN_TEXT] ?? ""; + pageContent += `${markdown}\n`; + } + + break; + } + case PageContent.PDF: { + const pdf: { url: string } | undefined = + propertyObject?.[PageContent.FILE]; + + if (pdf) { + pageContent += `![pdf](${pdf.url})\n`; + } + + break; + } + case PageContent.QUOTE: { + const quote = propertyObject?.[PropertyTypeValue.TEXT]; + if (quote) { + const markdown = + quote[0]?.[NotionObjectTypes.TEXT]?.["content"] ?? + ""; + + pageContent += `> ${markdown}\n`; + } + break; + } + case PageContent.SYNCED_BLOCK: { + // not handling synced block for now + break; + } + case PageContent.TABLE: { + // not handling table for now + break; + } + case PageContent.TABLE_OF_CONTENTS: { + // not handling tableofcontents for now + break; + } + case PageContent.TODO: { + const todo = propertyObject?.[PropertyTypeValue.TEXT]; + if (todo) { + const markdown = + todo[0]?.[TextObjectType.PLAIN_TEXT] ?? undefined; + const checked: boolean = propertyObject?.["checked"]; + if (markdown) { + if (checked) { + pageContent += `- [x] ${markdown}\n`; + } else { + pageContent += `- [ ] ${markdown}\n`; + } + } + } + break; + } + case PageContent.TOGGLE_BLOCKS: { + break; + } + case PageContent.VIDEO: { + const video: { url: string } | undefined = + propertyObject?.[FileType.EXTERNAL] ?? + propertyObject?.[PageContent.FILE]; + + if (video) { + pageContent += `\n![video](${video.url})\n`; + } + + break; + } + } + }); + + return pageContent; + } } From 478e05e1bdcbf21a24d4d540edb3520fc0fcfef8 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 15 Aug 2023 15:27:10 +0530 Subject: [PATCH 29/31] feat: handled submit action of viewPage with ViewErrors --- src/handlers/ExecuteViewSubmitHandler.ts | 61 ++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/handlers/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index cb2f2d5..70d838d 100644 --- a/src/handlers/ExecuteViewSubmitHandler.ts +++ b/src/handlers/ExecuteViewSubmitHandler.ts @@ -52,6 +52,7 @@ import { SendMessagePage } from "../../enum/modals/SendMessagePage"; import { NotionTable } from "../../enum/modals/NotionTable"; import { SearchDatabaseComponent } from "../../enum/modals/common/SearchDatabaseComponent"; import { table } from "table"; +import { NotionPage } from "../../enum/modals/NotionPage"; export class ExecuteViewSubmitHandler { private context: UIKitViewSubmitInteractionContext; @@ -139,6 +140,14 @@ export class ExecuteViewSubmitHandler { ); break; } + case NotionPage.VIEW_ID: { + return this.handleViewNotionPage( + room, + oAuth2Storage, + modalInteraction + ); + break; + } default: { } } @@ -997,4 +1006,56 @@ export class ExecuteViewSubmitHandler { return this.context.getInteractionResponder().successResponse(); } + + private async handleViewNotionPage( + room: IRoom, + oAuth2Storage: OAuth2Storage, + modalInteraction: ModalInteractionStorage + ): Promise { + const { view, user } = this.context.getInteractionData(); + const { state } = view; + + const { NotionSdk } = this.app.getUtils(); + const tokenInfo = await oAuth2Storage.getCurrentWorkspace(user.id); + + if (!tokenInfo) { + await sendNotificationWithConnectBlock( + this.app, + user, + this.read, + this.modify, + room + ); + return this.context.getInteractionResponder().errorResponse(); + } + + const { workspace_name, owner, access_token } = tokenInfo; + const pageId: string | undefined = + state?.[SearchPage.BLOCK_ID]?.[NotionPage.ACTION_ID]; + + if (!pageId) { + return this.context.getInteractionResponder().viewErrorResponse({ + viewId: view.id, + errors: { + [NotionPage.ACTION_ID]: "Please Select a Page to View", + }, + }); + } + + const pageMrkdwn = await NotionSdk.retrievePageContent( + access_token, + pageId + ); + + if (pageMrkdwn instanceof Error) { + return this.context.getInteractionResponder().errorResponse(); + } + + console.log(pageMrkdwn) + await sendNotification(this.read, this.modify, user, room, { + message: pageMrkdwn, + }); + + return this.context.getInteractionResponder().successResponse(); + } } From 764a36d5075218b969413b1b690774d02ff4dbef Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 15 Aug 2023 19:57:51 +0530 Subject: [PATCH 30/31] fix: page to include heading of page --- src/handlers/ExecuteViewSubmitHandler.ts | 10 ++++++++-- src/lib/NotionSDK.ts | 12 ++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/handlers/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index 70d838d..fc1660a 100644 --- a/src/handlers/ExecuteViewSubmitHandler.ts +++ b/src/handlers/ExecuteViewSubmitHandler.ts @@ -1041,6 +1041,11 @@ export class ExecuteViewSubmitHandler { }, }); } + const pageInfo = await NotionSdk.retrievePage(access_token, pageId); + + if (pageInfo instanceof Error) { + return this.context.getInteractionResponder().errorResponse(); + } const pageMrkdwn = await NotionSdk.retrievePageContent( access_token, @@ -1051,9 +1056,10 @@ export class ExecuteViewSubmitHandler { return this.context.getInteractionResponder().errorResponse(); } - console.log(pageMrkdwn) + const { name, url } = pageInfo; + const message = `# ${name}\n` + pageMrkdwn; await sendNotification(this.read, this.modify, user, room, { - message: pageMrkdwn, + message: message, }); return this.context.getInteractionResponder().successResponse(); diff --git a/src/lib/NotionSDK.ts b/src/lib/NotionSDK.ts index 84ad477..4b55e75 100644 --- a/src/lib/NotionSDK.ts +++ b/src/lib/NotionSDK.ts @@ -1420,7 +1420,7 @@ export class NotionSDK implements INotionSDK { private async getPageContentInMarkdwn(results): Promise { let pageContent = ""; - console.log(results); + results?.forEach((result) => { const type = result?.[NotionObjectTypes.TYPE]; const propertyObject = result?.[type]; @@ -1481,11 +1481,9 @@ export class NotionSDK implements INotionSDK { break; } case PageContent.COLUMN: { - console.log("column", propertyObject); break; } case PageContent.COLUMN_LIST: { - console.log("column list", propertyObject); break; } case PageContent.DIVIDER: { @@ -1520,7 +1518,7 @@ export class NotionSDK implements INotionSDK { "content" ] ?? ""; - pageContent += `# ${markdown}\n`; + pageContent += `## ${markdown}\n`; } break; } @@ -1533,7 +1531,7 @@ export class NotionSDK implements INotionSDK { "content" ] ?? ""; - pageContent += `## ${markdown}\n`; + pageContent += `### ${markdown}\n`; } break; } @@ -1546,7 +1544,7 @@ export class NotionSDK implements INotionSDK { "content" ] ?? ""; - pageContent += `### ${markdown}\n`; + pageContent += `#### ${markdown}\n`; } break; } @@ -1559,11 +1557,9 @@ export class NotionSDK implements INotionSDK { break; } case PageContent.LINK_PREVIEW: { - console.log("link preview", propertyObject); break; } case PageContent.PARAGRAPH: { - console.log("paragraph", propertyObject); const paragraph = propertyObject?.[PropertyTypeValue.TEXT]; if (paragraph) { const markdown = From 366ef9962ef5da91e8eb681ce843d2343c52467e Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Fri, 18 Aug 2023 16:08:59 +0530 Subject: [PATCH 31/31] feat: included button for viewing shared page --- enum/modals/SharePage.ts | 3 ++ src/handlers/ExecuteBlockActionHandler.ts | 55 ++++++++++++++++++++++- src/handlers/ExecuteViewSubmitHandler.ts | 19 +++++++- 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/enum/modals/SharePage.ts b/enum/modals/SharePage.ts index 48b7edc..1767f70 100644 --- a/enum/modals/SharePage.ts +++ b/enum/modals/SharePage.ts @@ -8,4 +8,7 @@ export enum SharePage { SHARE = "Share", CLOSE = "Close", TITLE = "Share Notion Page", + VIEW = "Click to View", + VIEW_PAGE_ACTION = "notion-share-page-view-page-action-id", + VIEW_PAGE_BLOCK = "notion-share-page-view-page-block-id", } diff --git a/src/handlers/ExecuteBlockActionHandler.ts b/src/handlers/ExecuteBlockActionHandler.ts index df4a352..85d37ab 100644 --- a/src/handlers/ExecuteBlockActionHandler.ts +++ b/src/handlers/ExecuteBlockActionHandler.ts @@ -19,7 +19,10 @@ import { ModalInteractionStorage } from "../storage/ModalInteraction"; import { createDatabaseModal } from "../modals/createDatabaseModal"; import { OAuth2Storage } from "../authorization/OAuth2Storage"; import { Error } from "../../errors/Error"; -import { sendNotificationWithConnectBlock } from "../helper/message"; +import { + sendNotification, + sendNotificationWithConnectBlock, +} from "../helper/message"; import { NotSupportedPropertyTypes, PropertyTypeValue, @@ -178,6 +181,10 @@ export class ExecuteBlockActionHandler { return this.handleSelectDatabaseAction(); break; } + case SharePage.VIEW_PAGE_ACTION: { + return this.handleViewPage(oAuth2Storage, room as IRoom); + break; + } default: { // Property Type Select Action const propertyTypeSelected = @@ -1010,4 +1017,50 @@ export class ExecuteBlockActionHandler { errors: {}, }); } + + private async handleViewPage( + oAuth2Storage: OAuth2Storage, + room: IRoom + ): Promise { + const { user, value } = this.context.getInteractionData(); + const { NotionSdk } = this.app.getUtils(); + + const tokenInfo = await oAuth2Storage.getCurrentWorkspace(user.id); + + if (!tokenInfo) { + await sendNotificationWithConnectBlock( + this.app, + user, + this.read, + this.modify, + room + ); + return this.context.getInteractionResponder().errorResponse(); + } + + if (!value) { + return this.context.getInteractionResponder().errorResponse(); + } + + const { access_token } = tokenInfo; + const pageInfo: IPage & { url: string } = JSON.parse(value); + const { parent, name, url } = pageInfo; + const pageId = parent.page_id; + + const pageMrkdwn = await NotionSdk.retrievePageContent( + access_token, + pageId + ); + + if (pageMrkdwn instanceof Error) { + return this.context.getInteractionResponder().errorResponse(); + } + + const message = `# ${name}\n` + pageMrkdwn; + await sendNotification(this.read, this.modify, user, room, { + message: message, + }); + + return this.context.getInteractionResponder().successResponse(); + } } diff --git a/src/handlers/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index fc1660a..0bdc1aa 100644 --- a/src/handlers/ExecuteViewSubmitHandler.ts +++ b/src/handlers/ExecuteViewSubmitHandler.ts @@ -1,5 +1,6 @@ import { BlockType, + ButtonStyle, IUIKitResponse, UIKitViewSubmitInteractionContext, } from "@rocket.chat/apps-engine/definition/uikit"; @@ -53,6 +54,7 @@ import { NotionTable } from "../../enum/modals/NotionTable"; import { SearchDatabaseComponent } from "../../enum/modals/common/SearchDatabaseComponent"; import { table } from "table"; import { NotionPage } from "../../enum/modals/NotionPage"; +import { ButtonInSectionComponent } from "../modals/common/buttonInSectionComponent"; export class ExecuteViewSubmitHandler { private context: UIKitViewSubmitInteractionContext; @@ -823,10 +825,23 @@ export class ExecuteViewSubmitHandler { const { name, parent, url } = pageInfo; - const message = `✨ Sharing [**${name}**](${url}) from **${workspace_name}**`; + const message = `Sharing [**${name}**](${url}) from **${workspace_name}✨ **`; + const viewPage = ButtonInSectionComponent( + { + app: this.app, + buttonText: SharePage.VIEW, + value: JSON.stringify(pageInfo), + style: ButtonStyle.PRIMARY, + text: message, + }, + { + blockId: SharePage.VIEW_PAGE_BLOCK, + actionId: SharePage.VIEW_PAGE_ACTION, + } + ); await sendMessage(this.read, this.modify, user, room, { - message, + blocks: [viewPage], }); return this.context.getInteractionResponder().successResponse();