Skip to content

Commit

Permalink
Feat/task description block mobile (#1770)
Browse files Browse the repository at this point in the history
* added basic editor displaying task decription

* added some styling and improvements

* defined the tools in toolbar

* added key so it updates colors when theme changes, updated theme, and added translations

* added function to transform the quill html to slate html we ue for web, added save and cancel buttons

* added copy functionality

* added functionality to save the rich text content

* added outside press functionality to the editor

* updated toolbar size and removed unnecessary component
  • Loading branch information
desperado1802 authored Nov 14, 2023
1 parent 3421ee0 commit 738aee8
Show file tree
Hide file tree
Showing 14 changed files with 316 additions and 12 deletions.
13 changes: 8 additions & 5 deletions apps/mobile/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Config from "./config"
import { observer } from "mobx-react-lite"
import { initCrashReporting } from "./utils/crashReporting"
import FlashMessage from "react-native-flash-message"
import { ClickOutsideProvider } from "react-native-click-outside"

// Set up Reactotron, which is a free desktop app for inspecting and debugging

Check warning on line 31 in apps/mobile/app/app.tsx

View workflow job for this annotation

GitHub Actions / Cspell

Unknown word (Reactotron)
// React Native apps. Learn more here: https://github.com/infinitered/reactotron
Expand Down Expand Up @@ -94,11 +95,13 @@ const App = observer((props: AppProps) => {
<PaperProvider theme={theme}>
<ErrorBoundary catchErrors={Config.catchErrors}>
<FlashMessage position="top" />
<AppNavigator
theme={theme}
initialState={initialNavigationState}
onStateChange={onNavigationStateChange}
/>
<ClickOutsideProvider>
<AppNavigator
theme={theme}
initialState={initialNavigationState}
onStateChange={onNavigationStateChange}
/>
</ClickOutsideProvider>
</ErrorBoundary>
</PaperProvider>
</SafeAreaProvider>
Expand Down
9 changes: 8 additions & 1 deletion apps/mobile/app/components/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable react-native/no-color-literals */
/* eslint-disable react-native/no-inline-styles */
import { View, Text, StyleSheet, TouchableOpacity } from "react-native"
import React, { ReactElement, useState } from "react"
import React, { ReactElement, useState, useEffect } from "react"
import { Feather } from "@expo/vector-icons"
import { useAppTheme } from "../theme"

Expand All @@ -11,6 +11,7 @@ interface IAccordion {
titleFontSize?: number
arrowSize?: number
headerElement?: ReactElement
setAccordionExpanded?: (isExpanded: boolean) => void
}

const Accordion: React.FC<IAccordion> = ({
Expand All @@ -19,14 +20,20 @@ const Accordion: React.FC<IAccordion> = ({
arrowSize,
titleFontSize,
headerElement,
setAccordionExpanded,
}) => {
const [expanded, setExpanded] = useState(true)
const { colors } = useAppTheme()

function toggleItem() {
setExpanded(!expanded)
setAccordionExpanded && setAccordionExpanded(expanded)
}

useEffect(() => {
setAccordionExpanded && setAccordionExpanded(expanded)
}, [expanded])

const body = <View style={{ gap: 12 }}>{children}</View>
return (
<View style={[styles.accordContainer, { backgroundColor: colors.background }]}>
Expand Down
247 changes: 247 additions & 0 deletions apps/mobile/app/components/Task/DescrptionBlock/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/* eslint-disable react-native/no-inline-styles */
/* eslint-disable react-native/no-color-literals */
import { View, StyleSheet, TouchableOpacity, Text, TouchableWithoutFeedback } from "react-native"
import React, { RefObject } from "react"
import Accordion from "../../Accordion"
import QuillEditor, { QuillToolbar } from "react-native-cn-quill"
import { useStores } from "../../../models"
import { useAppTheme } from "../../../theme"
import { translate } from "../../../i18n"
import { SvgXml } from "react-native-svg"
import { copyIcon } from "../../svgs/icons"
import * as Clipboard from "expo-clipboard"
import { showMessage } from "react-native-flash-message"
import { useTeamTasks } from "../../../services/hooks/features/useTeamTasks"
import { useClickOutside } from "react-native-click-outside"

const DescriptionBlock = () => {
const _editor: RefObject<QuillEditor> = React.useRef()

const [editorKey, setEditorKey] = React.useState(1)
const [actionButtonsVisible, setActionButtonsVisible] = React.useState<boolean>(false)
const [accordionExpanded, setAccordionExpanded] = React.useState<boolean>(true)
const [htmlValue, setHtmlValue] = React.useState<string>("")

const { updateDescription } = useTeamTasks()

const {
TaskStore: { detailedTask: task },
} = useStores()

const { colors, dark } = useAppTheme()

React.useEffect(() => {
setEditorKey((prevKey) => prevKey + 1)
}, [colors, task?.description])

const handleHtmlChange = (html: string) => {
setHtmlValue(html)
}

function transformHtmlForSlate(html: string) {
// Replace <pre> with <p> and the content inside <pre> with <code>,
// excluding <a> tags from modification
const modifiedHtml = html
.replace(
/<pre class="ql-syntax" spellcheck="false">([\s\S]*?)<\/pre>/g,
(_, content) => {
const codeContent = content.replace(/(<a .*?<\/a>)/g, "PLACEHOLDER_FOR_A_TAG")
return `<p><pre><code>${codeContent}</code></pre></p>`
},
)
.replace(/PLACEHOLDER_FOR_A_TAG/g, (_, content) => content)
.replace(/class="ql-align-(.*?)"/g, (_, alignmentClass) => {
return `style="text-align:${alignmentClass}"`
})

return modifiedHtml
}

const onPressCancel = async (): Promise<void> => {
await _editor.current
.setContents("")
.then(() => _editor.current.dangerouslyPasteHTML(0, task?.description))
.then(() => _editor.current.blur())
.finally(() => setTimeout(() => setActionButtonsVisible(false), 100))
}

const onPressSave = async (): Promise<void> => {
const formattedValue = transformHtmlForSlate(htmlValue)
await updateDescription(formattedValue, task).finally(() => {
_editor.current.blur()
setActionButtonsVisible(false)
})
}

const copyDescription = async () => {
const descriptionPlainText = await _editor.current.getText()
Clipboard.setStringAsync(descriptionPlainText)
showMessage({
message: translate("taskDetailsScreen.copyDescription"),
type: "info",
backgroundColor: colors.secondary,
})
}

const editorContainerOutsidePressRef = useClickOutside<View>(() => {
_editor.current?.blur()
setActionButtonsVisible(false)
})
return (
<Accordion
setAccordionExpanded={setAccordionExpanded}
title={translate("taskDetailsScreen.description")}
headerElement={
accordionExpanded && (
<TouchableWithoutFeedback>
<TouchableOpacity onPress={copyDescription}>
<SvgXml xml={copyIcon} />
</TouchableOpacity>
</TouchableWithoutFeedback>
)
}
>
<View style={{ paddingBottom: 12 }} ref={editorContainerOutsidePressRef}>
<QuillEditor
key={editorKey}
style={styles.editor}
onHtmlChange={(event) => handleHtmlChange(event.html)}
onTextChange={() => setActionButtonsVisible(true)}
webview={{ allowsLinkPreview: true }}
ref={_editor}
initialHtml={task?.description ? task?.description : ""}
quill={{
placeholder: translate("taskDetailsScreen.descriptionBlockPlaceholder"),
modules: {
toolbar: false,
},
}}
theme={{
background: colors.background,
color: colors.primary,
placeholder: "#e0e0e0",
}}
/>

<View style={{ paddingHorizontal: 12 }}>
<View style={styles.horizontalSeparator} />
<QuillToolbar
editor={_editor}
styles={{
toolbar: {
provider: (provided) => ({
...provided,
borderTopWidth: 0,
borderLeftWidth: 0,
borderRightWidth: 0,
borderBottomWidth: 0,
}),
root: (provided) => ({
...provided,
backgroundColor: colors.background,
width: "100%",
}),
},
separator: (provided) => ({
...provided,
color: colors.secondary,
}),
selection: {
root: (provided) => ({
...provided,
backgroundColor: colors.background,
}),
},
}}
options={[
[
"bold",
"italic",
"underline",
"code",
"blockquote",

{ header: "1" },
{ header: "2" },
{ list: "ordered" },
{ list: "bullet" },
{ align: [] },
],
]}
theme={
dark
? {
background: "#1c1e21",
color: "#ebedf0",
overlay: "rgba(255, 255, 255, .15)",
size: 28,
}
: {
background: "#ebedf0",
color: "#1c1e21",
overlay: "rgba(55,99,115, .1)",
size: 28,
}
}
/>
{actionButtonsVisible && (
<View style={styles.actionButtonsWrapper}>
<TouchableOpacity
style={{
...styles.actionButton,
backgroundColor: "#E7E7EA",
}}
onPress={onPressCancel}
>
<Text style={{ fontSize: 12 }}>{translate("common.cancel")}</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={onPressSave}
style={{
...styles.actionButton,
backgroundColor: colors.secondary,
}}
>
<Text style={{ color: "white", fontSize: 12 }}>
{translate("common.save")}
</Text>
</TouchableOpacity>
</View>
)}
</View>
</View>
</Accordion>
)
}

export default DescriptionBlock

const styles = StyleSheet.create({
actionButton: {
alignItems: "center",
borderRadius: 8,
height: 30,
justifyContent: "center",
width: 80,
},
actionButtonsWrapper: {
flexDirection: "row",
gap: 5,
justifyContent: "flex-end",
marginVertical: 5,
},
editor: {
backgroundColor: "white",
borderWidth: 0,
flex: 1,
marginVertical: 5,
minHeight: 230,
padding: 0,
},
horizontalSeparator: {
borderTopColor: "#F2F2F2",
borderTopWidth: 1,
marginBottom: 10,
width: "100%",
},
})
3 changes: 3 additions & 0 deletions apps/mobile/app/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ const ar: Translations = {
isDuplicatedBy: "مكرر بواسطة",
relatesTo: "يتصل بـ",
linkedIssues: "المسائل المرتبطة",
description: "الوصف",
descriptionBlockPlaceholder: "اكتب وصفاً كاملاً لمشروعك...",
copyDescription: "تم نسخ الوصف.",
},
tasksScreen: {
name: "مهام",
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/app/i18n/bg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ const bg = {
isDuplicatedBy: "Is Duplicated By",
relatesTo: "Relates To",
linkedIssues: "Linked Issues",
description: "Description",
descriptionBlockPlaceholder: "Write a complete description of your project...",
copyDescription: "Description Copied.",
},
tasksScreen: {
name: "Tasks",
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/app/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ const en = {
isDuplicatedBy: "Is Duplicated By",
relatesTo: "Relates To",
linkedIssues: "Linked Issues",
description: "Description",
descriptionBlockPlaceholder: "Write a complete description of your project...",
copyDescription: "Description Copied.",
},
tasksScreen: {
name: "Tasks",
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/app/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ const es = {
isDuplicatedBy: "Is Duplicated By",
relatesTo: "Relates To",
linkedIssues: "Linked Issues",
description: "Description",
descriptionBlockPlaceholder: "Write a complete description of your project...",
copyDescription: "Description Copied.",
},
tasksScreen: {
name: "Tasks",
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/app/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ const fr = {
isDuplicatedBy: "Est dupliqué par",
relatesTo: "Se rapporte à",
linkedIssues: "Problèmes liés",
description: "Description",
descriptionBlockPlaceholder: "Écrivez une description complète de votre projet...",
copyDescription: "Description copiée.",
},
tasksScreen: {
name: "Tâches",
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/app/i18n/he.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ const he = {
isDuplicatedBy: "Is Duplicated By",
relatesTo: "Relates To",
linkedIssues: "Linked Issues",
description: "Description",
descriptionBlockPlaceholder: "Write a complete description of your project...",
copyDescription: "Description Copied.",
},
tasksScreen: {
name: "Tasks",
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/app/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ const ko: Translations = {
isDuplicatedBy: "복제됨",
relatesTo: "관련이 있다",
linkedIssues: "연결된 이슈",
description: "설명",
descriptionBlockPlaceholder: "프로젝트에 대한 완전한 설명을 작성하세요...",
copyDescription: "설명이 복사되었습니다.",
},
tasksScreen: {
name: "작업",
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/app/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ const ru = {
isDuplicatedBy: "Is Duplicated By",
relatesTo: "Relates To",
linkedIssues: "Linked Issues",
description: "Description",
descriptionBlockPlaceholder: "Write a complete description of your project...",
copyDescription: "Description Copied.",
},
tasksScreen: {
name: "Tasks",
Expand Down
Loading

0 comments on commit 738aee8

Please sign in to comment.