From 9e4a14e54726af7d156593128d9a682ad390a627 Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Wed, 5 Feb 2025 16:37:20 +0100 Subject: [PATCH] feat(Mattermost Plugin): TipTap Editor for Task and Reflection (#10796) --- packages/client/shared/tiptap/addTagToTask.ts | 2 +- .../shared/tiptap/convertTipTapTaskContent.ts | 2 +- packages/client/shared/types/index.d.ts | 1 + packages/mattermost-plugin/Atmosphere.ts | 33 ++-- .../CreateTaskModal/CreateTaskModal.tsx | 52 +++--- .../PushReflection/PushReflectionModal.tsx | 102 +++++------ .../components/Sidepanel/TeamRow.tsx | 2 +- .../StartActivityModal/StartActivityModal.tsx | 2 +- .../hooks/useMassInvitationToken.tsx | 2 +- .../hooks/useTipTapTaskEditor.tsx | 36 ++++ packages/mattermost-plugin/index.css | 15 ++ packages/mattermost-plugin/index.tsx | 6 +- packages/mattermost-plugin/package.json | 10 +- packages/mattermost-plugin/postcss.config.js | 5 + .../mattermost-plugin/prod.webpack.config.js | 26 ++- packages/mattermost-plugin/tsconfig.json | 1 - packages/mattermost-plugin/webpack.config.js | 20 +++ packages/server/postgres/types/index.d.ts | 3 +- pm2.dev.config.js | 2 +- yarn.lock | 165 ++++++++++++++++-- 20 files changed, 361 insertions(+), 126 deletions(-) create mode 100644 packages/client/shared/types/index.d.ts create mode 100644 packages/mattermost-plugin/hooks/useTipTapTaskEditor.tsx create mode 100644 packages/mattermost-plugin/index.css create mode 100644 packages/mattermost-plugin/postcss.config.js diff --git a/packages/client/shared/tiptap/addTagToTask.ts b/packages/client/shared/tiptap/addTagToTask.ts index acb1feb3946..d5a09814d34 100644 --- a/packages/client/shared/tiptap/addTagToTask.ts +++ b/packages/client/shared/tiptap/addTagToTask.ts @@ -1,5 +1,5 @@ import {JSONContent} from '@tiptap/core' -import {TaskTag} from 'parabol-server/postgres/types' +import {TaskTag} from '../types' const addTagToTask = (content: JSONContent, tag: TaskTag) => { content.content!.push({ diff --git a/packages/client/shared/tiptap/convertTipTapTaskContent.ts b/packages/client/shared/tiptap/convertTipTapTaskContent.ts index b93c0f01e13..28399ecee42 100644 --- a/packages/client/shared/tiptap/convertTipTapTaskContent.ts +++ b/packages/client/shared/tiptap/convertTipTapTaskContent.ts @@ -1,6 +1,6 @@ import {generateJSON} from '@tiptap/html' -import {TaskTag} from 'parabol-server/postgres/types' import {serverTipTapExtensions} from '~/shared/tiptap/serverTipTapExtensions' +import {TaskTag} from '../types' export const convertTipTapTaskContent = (text: string, tags?: TaskTag[]) => { let body = `

${text}

` diff --git a/packages/client/shared/types/index.d.ts b/packages/client/shared/types/index.d.ts new file mode 100644 index 00000000000..48e7f104fbb --- /dev/null +++ b/packages/client/shared/types/index.d.ts @@ -0,0 +1 @@ +export type TaskTag = 'private' | 'archived' diff --git a/packages/mattermost-plugin/Atmosphere.ts b/packages/mattermost-plugin/Atmosphere.ts index 7b9eba6f68e..088e211c3e6 100644 --- a/packages/mattermost-plugin/Atmosphere.ts +++ b/packages/mattermost-plugin/Atmosphere.ts @@ -53,6 +53,21 @@ const fetchGraphQL = (state: State) => (params: RequestParameters, variables: Va ) } +const login = (state: State) => async () => { + const {serverUrl, store} = state + const response = await fetch( + serverUrl + '/login', + Client4.getOptions({ + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }) + ) + const body = await response.json() + store.dispatch(onLogin(body.authToken)) +} + const relayFieldLogger: RelayFieldLogger = (event) => { if (event.kind === 'relay_resolver.error') { console.warn(`Resolver error encountered in ${event.owner}.${event.fieldPath}`) @@ -67,6 +82,7 @@ export type ResolverContext = { export class Atmosphere extends Environment { state: State + login: () => Promise constructor(serverUrl: string, reduxStore: Store) { const state = { @@ -88,21 +104,8 @@ export class Atmosphere extends Environment { relayFieldLogger }) this.state = state - } - - async login() { - const {serverUrl, store} = this.state - const response = await fetch( - serverUrl + '/login', - Client4.getOptions({ - method: 'POST', - headers: { - 'Content-Type': 'application/json' - } - }) - ) - const body = await response.json() - store.dispatch(onLogin(body.authToken)) + // bind it here to avoid this == undefined errors + this.login = login(state) } } diff --git a/packages/mattermost-plugin/components/CreateTaskModal/CreateTaskModal.tsx b/packages/mattermost-plugin/components/CreateTaskModal/CreateTaskModal.tsx index 82ec13ed8b9..5658c26f149 100644 --- a/packages/mattermost-plugin/components/CreateTaskModal/CreateTaskModal.tsx +++ b/packages/mattermost-plugin/components/CreateTaskModal/CreateTaskModal.tsx @@ -8,9 +8,13 @@ import {closeCreateTaskModal} from '../../reducers' import Select from '../Select' import SimpleSelect from '../SimpleSelect' +import {TipTapEditor} from 'parabol-client/components/promptResponse/TipTapEditor' +import useEventCallback from 'parabol-client/hooks/useEventCallback' +import {convertTipTapTaskContent} from 'parabol-client/shared/tiptap/convertTipTapTaskContent' import type {TaskStatusEnum} from '../../__generated__/CreateTaskModalMutation.graphql' import {CreateTaskModalMutation} from '../../__generated__/CreateTaskModalMutation.graphql' import {CreateTaskModalQuery} from '../../__generated__/CreateTaskModalQuery.graphql' +import {useTipTapTaskEditor} from '../../hooks/useTipTapTaskEditor' import LoadingSpinner from '../LoadingSpinner' import Modal from '../Modal' @@ -21,6 +25,7 @@ const CreateTaskModal = () => { graphql` query CreateTaskModalQuery { viewer { + id teams { id name @@ -37,7 +42,7 @@ const CreateTaskModal = () => { ) const {viewer} = data - const {teams} = viewer + const {id: userId, teams} = viewer const [createTask, createTaskLoading] = useMutation(graphql` mutation CreateTaskModalMutation($newTask: CreateTaskInput!) { @@ -52,7 +57,6 @@ const CreateTaskModal = () => { } `) - const [description, setDescription] = useState('') const [selectedTeam, setSelectedTeam] = useState[number]>() const [selectedStatus, setSelectedStatus] = useState('active') @@ -61,47 +65,40 @@ const CreateTaskModal = () => { setSelectedTeam(teams[0]) } }, [teams, selectedTeam]) + const teamId = selectedTeam?.id const dispatch = useDispatch() const handleClose = () => { dispatch(closeCreateTaskModal()) } - const handleStart = async () => { - if (!selectedTeam || !description || !selectedStatus) { + const handleSubmit = useEventCallback(async () => { + if (!teamId || !selectedStatus || !editor || editor.isEmpty) { return } if (createTaskLoading) { return } - // TODO: let's cheat our way to new task content for now - const content = { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { - text: description, - type: 'text' - } - ] - } - ] - } + const content = editor.getJSON() createTask({ variables: { newTask: { content: JSON.stringify(content), status: selectedStatus, - teamId: selectedTeam.id + userId, + teamId } } }) handleClose() + }) + + const {editor} = useTipTapTaskEditor(convertTipTapTaskContent('')) + if (!editor) { + return null } return ( @@ -109,20 +106,19 @@ const CreateTaskModal = () => { title='Add a Task' commitButtonLabel='Add Task' handleClose={handleClose} - handleCommit={handleStart} + handleCommit={handleSubmit} > +
-