diff --git a/enum/CommandParam.ts b/enum/CommandParam.ts index a3a5282..d303064 100644 --- a/enum/CommandParam.ts +++ b/enum/CommandParam.ts @@ -7,7 +7,8 @@ export enum CommandParam { WORKSPACE = "workspace", WS = "ws", SHARE = "share", - VIEW = "view" + VIEW = "view", + APPEND = "append" } export enum SubCommandParam { diff --git a/enum/messages.ts b/enum/messages.ts index 1ee2afd..d05a339 100644 --- a/enum/messages.ts +++ b/enum/messages.ts @@ -5,7 +5,8 @@ export enum Messages { • use \`/notion create\` to create page or record • use \`/notion create db\` to create database • use \`/notion workspace\` to change workspace - • use \`/notion share\` to share pages + • use \`/notion share\` to share pages + • use \`/notion append\` to add content to your page `, HELPER_TEXT = `Need some help with \`/notion\`?`, diff --git a/enum/modals/NotionAppendContent.ts b/enum/modals/NotionAppendContent.ts new file mode 100644 index 0000000..4474a69 --- /dev/null +++ b/enum/modals/NotionAppendContent.ts @@ -0,0 +1,23 @@ +export enum NotionAppendContent { + VIEW_ID = "notion-append-content-view-id", + TITLE = "Add Content to Page", + HEADING_SELECT_PLACEHOLDER = "Heading Type", + HEADING_SELECT_BLOCK_ID = "select-heading-component-block-id", + HEADING_SELECT_ACTION_ID = "select-heading-component-action-id", + HEADING_SELECT_LABEL = "Select Heading Type", + HEADING_PLACEHOLDER = "Enter Your Heading", + HEADING_LABEL = "Heading", + HEADING_BLOCK = "heading-input-block-id", + HEADING_ACTION = "heading-input-action-id", + CONTENT_PLACEHOLDER = "Enter your content...", + CONTENT_LABEL = "Content *", + CONTENT_BLOCK = "content-input-block-id", + CONTENT_ACTION = "content-input-action-id", + SAVE = "Save", + SAVE_ACTION = "append-content-action-id", + SAVE_BLOCK = "append-content-block-id", + CLOSE = "Close", + CLOSE_ACTION = "close-append-content-action-id", + CLOSE_BLOCK = "close-append-content-block-id", +} + diff --git a/src/commands/CommandUtility.ts b/src/commands/CommandUtility.ts index eb8c1a1..0de0f64 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.APPEND: { + await handler.appendContent(); + break; + } case CommandParam.HELP: default: { await sendHelperNotification( diff --git a/src/handlers/ExecuteBlockActionHandler.ts b/src/handlers/ExecuteBlockActionHandler.ts index b271677..e3e5d5a 100644 --- a/src/handlers/ExecuteBlockActionHandler.ts +++ b/src/handlers/ExecuteBlockActionHandler.ts @@ -51,6 +51,8 @@ import { getPropertiesIdsObject } from "../helper/getPropertiesIdsObject"; import { SharePage } from "../../enum/modals/SharePage"; import { NotionTable } from "../../enum/modals/NotionTable"; import { SendMessagePage } from "../../enum/modals/SendMessagePage"; +import { NotionAppendContent } from "../../enum/modals/NotionAppendContent"; +import { appendContentModal } from "../modals/appendContentModal"; export class ExecuteBlockActionHandler { private context: UIKitBlockInteractionContext; @@ -176,6 +178,14 @@ export class ExecuteBlockActionHandler { return this.handleSelectDatabaseAction(); break; } + + case NotionAppendContent.HEADING_SELECT_ACTION_ID: { + return this.handleHeadingSelectAction( + modalInteraction, + oAuth2Storage, + roomInteractionStorage + ); + } default: { // Property Type Select Action const propertyTypeSelected = @@ -1021,4 +1031,59 @@ export class ExecuteBlockActionHandler { errors: {}, }); } + + private async handleHeadingSelectAction( + 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(); + } + + let isHeadingInput = false; + + if(value !== 'no_heading'){ + isHeadingInput = true; + } + + const modal = await appendContentModal( + this.app, + user, + this.read, + this.persistence, + this.modify, + room, + modalInteraction, + tokenInfo, + isHeadingInput + ); + + 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/ExecuteViewSubmitHandler.ts b/src/handlers/ExecuteViewSubmitHandler.ts index 05a9d5f..3415411 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 { NotionAppendContent } from "../../enum/modals/NotionAppendContent"; export class ExecuteViewSubmitHandler { private context: UIKitViewSubmitInteractionContext; @@ -139,6 +140,14 @@ export class ExecuteViewSubmitHandler { ); break; } + + case NotionAppendContent.VIEW_ID: { + return this.handleAppendContent( + room, + oAuth2Storage, + modalInteraction + ); + } default: { } } @@ -341,7 +350,6 @@ export class ExecuteViewSubmitHandler { const parentType: string = parent.type; if (parentType.includes(NotionObjectTypes.PAGE_ID)) { - return this.handleCreationOfPage( tokenInfo, room, @@ -350,7 +358,7 @@ export class ExecuteViewSubmitHandler { Objects as IPage ); } - + return this.handleCreationOfRecord( tokenInfo, room, @@ -998,4 +1006,98 @@ export class ExecuteViewSubmitHandler { return this.context.getInteractionResponder().successResponse(); } + + private async handleAppendContent( + room: IRoom, + oAuth2Storage: OAuth2Storage, + modalInteraction: ModalInteractionStorage + ): Promise { + const { NotionSdk } = this.app.getUtils(); + const { view, user } = this.context.getInteractionData(); + const { state } = view; + + 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 { access_token, workspace_name } = tokenInfo; + + const pageId: string | undefined = + state?.[SearchPage.BLOCK_ID]?.[SearchPage.ACTION_ID]; + + const headingType: string | undefined = + state?.[NotionAppendContent.HEADING_SELECT_BLOCK_ID]?.[NotionAppendContent.HEADING_SELECT_ACTION_ID]; + + const headingInput: string | undefined = + state?.[NotionAppendContent.HEADING_BLOCK]?.[ + NotionAppendContent.HEADING_ACTION + ]; + + const contentInput: string | undefined = + state?.[NotionAppendContent.CONTENT_BLOCK]?.[ + NotionAppendContent.CONTENT_ACTION + ]; + + if (!pageId) { + return this.context.getInteractionResponder().viewErrorResponse({ + viewId: view.id, + errors: { + [SharePage.ACTION_ID]: "Please Select a Page", + }, + }); + } else { + if (!contentInput) { + return this.context + .getInteractionResponder() + .viewErrorResponse({ + viewId: view.id, + errors: { + [NotionAppendContent.CONTENT_ACTION]: + "Content is required. Please provide some information.", + }, + }); + } + } + + const appendMsgStatus = await NotionSdk.appendContentToPage( + access_token, + pageId, + contentInput, + headingType && headingType !== "no_heading" && headingInput + ? { headingType, headingInput } + : undefined + ); + + let message: string; + + if (appendMsgStatus instanceof Error) { + this.app.getLogger().error(appendMsgStatus.message); + message = `Something went wrong while appending content in **${workspace_name}**.`; + } 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; + + message = `Your content has been sucessfully saved in [**${name}**](${url})`; + } + + await sendNotification(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 ad3b741..037fa15 100644 --- a/src/handlers/Handler.ts +++ b/src/handlers/Handler.ts @@ -33,6 +33,9 @@ import { SendMessagePage } from "../../enum/modals/SendMessagePage"; import { sendMessagePageModal } from "../modals/sendMessagePageModal"; import { NotionTable } from "../../enum/modals/NotionTable"; import { viewNotionTableModal } from "../modals/viewNotionTableModal"; +import { appendContentModal } from "../modals/appendContentModal"; +import { NotionAppendContent } from "../../enum/modals/NotionAppendContent"; +import { SearchPage } from "../../enum/modals/common/SearchPageComponent"; export class Handler implements IHandler { public app: NotionApp; @@ -66,6 +69,69 @@ export class Handler implements IHandler { ); } + public async appendContent( + ): 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, + NotionAppendContent.VIEW_ID + ); + + const { workspace_id, access_token } = tokenInfo; + + await Promise.all([ + this.roomInteractionStorage.storeInteractionRoomId(roomId), + modalInteraction.clearPagesOrDatabase(workspace_id), + modalInteraction.clearInputElementState( + SearchPage.ACTION_ID + ), + modalInteraction.clearAllInteractionActionId(), + ]); + + const modal = await appendContentModal( + this.app, + this.sender, + this.read, + this.persis, + this.modify, + this.room, + modalInteraction, + tokenInfo + ); + + if (modal instanceof Error) { + this.app.getLogger().error(modal.message); + return; + } + + const triggerId = this.triggerId; + + if (triggerId) { + await this.modify + .getUiController() + .openSurfaceView(modal, { triggerId }, this.sender); + } + + return; + } + public async createNotionDatabase(): Promise { const userId = this.sender.id; const roomId = this.room.id; @@ -515,4 +581,4 @@ export class Handler implements IHandler { .openSurfaceView(modal, { triggerId }, this.sender); } } -} \ No newline at end of file +} diff --git a/src/lib/NotionSDK.ts b/src/lib/NotionSDK.ts index ec8ef49..cba761d 100644 --- a/src/lib/NotionSDK.ts +++ b/src/lib/NotionSDK.ts @@ -132,7 +132,10 @@ export class NotionSDK implements INotionSDK { } } - private async getPageObjectFromResults(item, emoji:boolean = false): Promise { + private async getPageObjectFromResults( + item, + emoji: boolean = false + ): Promise { const typesWithTitleProperty = [ NotionObjectTypes.WORKSPACE.toString(), NotionObjectTypes.PAGE_ID.toString(), @@ -178,7 +181,11 @@ export class NotionSDK implements INotionSDK { return null; } - private returnPage(name: string, page_id: string, emoji:boolean = false): IPage { + private returnPage( + name: string, + page_id: string, + emoji: boolean = false + ): IPage { return { name: `${emoji ? "📄" : ""} ${name}`, parent: { @@ -541,7 +548,10 @@ export class NotionSDK implements INotionSDK { } } - private async getDatabaseObjectFromResults(item, emoji:boolean = false): Promise { + private async getDatabaseObjectFromResults( + item, + emoji: boolean = false + ): Promise { const databaseNameTitleObject = item?.[NotionObjectTypes.TITLE]; const name: string = databaseNameTitleObject.length ? databaseNameTitleObject[0]?.plain_text @@ -550,7 +560,7 @@ export class NotionSDK implements INotionSDK { return { info: { - name: `${emoji ? "📚":""} ${name}`, + name: `${emoji ? "📚" : ""} ${name}`, link: item?.url, }, parent: { @@ -1384,4 +1394,74 @@ export class NotionSDK implements INotionSDK { } return tableData; } + + public async appendContentToPage( + token: string, + blockId: string, + contentInput: string, + headingInfo: { headingType: string; headingInput: string } | undefined + ): Promise { + try { + const children = headingInfo + ? [ + { + object: "block", + type: headingInfo.headingType, + [headingInfo.headingType]: { + rich_text: [ + { + type: "text", + text: { + content: headingInfo.headingInput, + }, + }, + ], + }, + }, + ] + : []; + + children.push({ + object: "block", + type: "paragraph", + paragraph: { + rich_text: [ + { + type: "text", + text: { + content: contentInput, + }, + }, + ], + }, + }); + + const response = await this.http.patch( + NotionApi.BLOCKS + `/${blockId}/children`, + { + data: { + children: 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 Appending Message Block: `, + response.content + ); + } + + return true; + } catch (err) { + throw new AppsEngineException(err as string); + } + } } diff --git a/src/modals/appendContentModal.ts b/src/modals/appendContentModal.ts new file mode 100644 index 0000000..f5bc7d6 --- /dev/null +++ b/src/modals/appendContentModal.ts @@ -0,0 +1,154 @@ +import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { NotionApp } from "../../NotionApp"; +import { + IModify, + IPersistence, + IRead, + IUIKitSurfaceViewParam, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; +import { ModalInteractionStorage } from "../storage/ModalInteraction"; +import { ITokenInfo } from "../../definition/authorization/IOAuth2Storage"; +import { Error } from "../../errors/Error"; +import { NotionAppendContent } from "../../enum/modals/NotionAppendContent"; +import { + ButtonStyle, + UIKitSurfaceType, +} from "@rocket.chat/apps-engine/definition/uikit"; +import { TextObjectType, Block } from "@rocket.chat/ui-kit"; +import { inputElementComponent } from "./common/inputElementComponent"; +import { SearchPage } from "../../enum/modals/common/SearchPageComponent"; +import { searchPageComponent } from "./common/searchPageComponent"; +import { StaticSelectOptionsParam } from "../../definition/ui-kit/Element/IStaticSelectElement"; +import { DropDownComponent } from "../modals/common/DropDownComponent"; + +export async function appendContentModal( + app: NotionApp, + user: IUser, + read: IRead, + persistence: IPersistence, + modify: IModify, + room: IRoom, + modalInteraction: ModalInteractionStorage, + tokenInfo: ITokenInfo, + headingSelect?: boolean +): Promise { + const { elementBuilder } = app.getUtils(); + const blocks: Block[] = []; + const searchForPageComponent = await searchPageComponent( + app, + modalInteraction, + tokenInfo, + SearchPage.ACTION_ID + ); + + if (searchForPageComponent instanceof Error) { + return searchForPageComponent; + } + + blocks.push(searchForPageComponent); + + const options: StaticSelectOptionsParam = [ + { + text: "No Heading", + value: "no_heading", + }, + { + text: "Heading 1", + value: "heading_1", + }, + { + text: "Heading 2", + value: "heading_2", + }, + { + text: "Heading 3", + value: "heading_3", + }, + ]; + + const selectHeadingComponent = DropDownComponent( + { + app, + options, + placeholder: NotionAppendContent.HEADING_SELECT_PLACEHOLDER, + text: NotionAppendContent.HEADING_SELECT_LABEL, + initialValue: "no_heading", + dispatchActionConfigOnInput: true, + dispatchActionConfigOnSelect: true, + }, + { + blockId: NotionAppendContent.HEADING_SELECT_BLOCK_ID, + actionId: NotionAppendContent.HEADING_SELECT_ACTION_ID, + } + ); + + blocks.push(selectHeadingComponent); + + let labelofHeading = NotionAppendContent.HEADING_LABEL.toString(); + let placeholderofHeading = + NotionAppendContent.HEADING_PLACEHOLDER.toString(); + + const headingInput = inputElementComponent( + { + app, + placeholder: placeholderofHeading, + label: labelofHeading, + optional: true, + }, + { + blockId: NotionAppendContent.HEADING_BLOCK, + actionId: NotionAppendContent.HEADING_ACTION, + } + ); + + if (headingSelect) blocks.push(headingInput); + + let labelOfAppendContent = NotionAppendContent.CONTENT_LABEL.toString(); + let placeholderOfAppendContent = + NotionAppendContent.CONTENT_PLACEHOLDER.toString(); + + const contentInput = inputElementComponent( + { + app, + placeholder: placeholderOfAppendContent, + label: labelOfAppendContent, + optional: false, + multiline: true, + }, + { + blockId: NotionAppendContent.CONTENT_BLOCK, + actionId: NotionAppendContent.CONTENT_ACTION, + } + ); + + blocks.push(contentInput); + + const submit = elementBuilder.addButton( + { text: NotionAppendContent.SAVE, style: ButtonStyle.PRIMARY }, + { + actionId: NotionAppendContent.SAVE_ACTION, + blockId: NotionAppendContent.SAVE_BLOCK, + } + ); + + const close = elementBuilder.addButton( + { text: NotionAppendContent.CLOSE, style: ButtonStyle.DANGER }, + { + actionId: NotionAppendContent.CLOSE_ACTION, + blockId: NotionAppendContent.CLOSE_BLOCK, + } + ); + + return { + id: NotionAppendContent.VIEW_ID, + type: UIKitSurfaceType.CONTEXTUAL_BAR, + title: { + type: TextObjectType.MRKDWN, + text: NotionAppendContent.TITLE, + }, + blocks, + close, + submit, + }; +}