Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/task description block mobile #1770

Merged
merged 10 commits into from
Nov 14, 2023
13 changes: 8 additions & 5 deletions apps/mobile/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,21 @@
import { ErrorBoundary } from "./screens/ErrorScreen/ErrorBoundary"
import * as storage from "./utils/storage"
import { customDarkTheme, customFontsToLoad, customLightTheme } from "./theme"
import { setupReactotron } from "./services/reactotron"

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

View workflow job for this annotation

GitHub Actions / Cspell

Unknown word (Reactotron)

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

View workflow job for this annotation

GitHub Actions / Cspell

Unknown word (reactotron)
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
setupReactotron({

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

View workflow job for this annotation

GitHub Actions / Cspell

Unknown word (Reactotron)
// clear the Reactotron window when the app loads/reloads

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

View workflow job for this annotation

GitHub Actions / Cspell

Unknown word (Reactotron)
clearOnLoad: true,
// generally going to be localhost
host: "localhost",
// Reactotron can monitor AsyncStorage for you

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

View workflow job for this annotation

GitHub Actions / Cspell

Unknown word (Reactotron)
useAsyncStorage: true,
// log the initial restored state from AsyncStorage
logInitialState: true,
Expand Down Expand Up @@ -94,11 +95,13 @@
<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
Loading