diff --git a/apps/mobile/app/components/Accordion.tsx b/apps/mobile/app/components/Accordion.tsx new file mode 100644 index 000000000..34ca4a382 --- /dev/null +++ b/apps/mobile/app/components/Accordion.tsx @@ -0,0 +1,60 @@ +/* eslint-disable react-native/no-color-literals */ +/* eslint-disable react-native/no-inline-styles */ +import { View, Text, StyleSheet, TouchableOpacity } from "react-native" +import React, { useState } from "react" +import { Feather } from "@expo/vector-icons" +import { useAppTheme } from "../theme" + +const Accordion = ({ children, title }) => { + const [expanded, setExpanded] = useState(true) + const { colors } = useAppTheme() + + function toggleItem() { + setExpanded(!expanded) + } + + const body = {children} + return ( + + + {title} + + + {expanded && ( + + + + )} + {expanded && body} + + ) +} + +export default Accordion + +const styles = StyleSheet.create({ + accordContainer: { + borderRadius: 8, + width: "100%", + }, + accordHeader: { + alignItems: "center", + flexDirection: "row", + justifyContent: "space-between", + padding: 12, + }, + accordTitle: { + fontSize: 20, + fontWeight: "600", + }, +}) diff --git a/apps/mobile/app/components/IssuesModal.tsx b/apps/mobile/app/components/IssuesModal.tsx index b78ff7f42..654a4310c 100644 --- a/apps/mobile/app/components/IssuesModal.tsx +++ b/apps/mobile/app/components/IssuesModal.tsx @@ -18,20 +18,16 @@ import { SvgUri } from "react-native-svg" import { IIssueType } from "../services/interfaces/ITaskIssue" import { useTeamTasks } from "../services/hooks/features/useTeamTasks" import { useAppTheme } from "../theme" +import { BlurView } from "expo-blur" interface IssuesModalProps { task: ITeamTask readonly?: boolean nameIncluded?: boolean - responsiveFontSize?: () => number + smallFont?: boolean } -const IssuesModal: FC = ({ - task, - readonly = false, - nameIncluded, - responsiveFontSize, -}) => { +const IssuesModal: FC = ({ task, readonly = false, nameIncluded, smallFont }) => { const { allTaskIssues } = useTaskIssue() const [isModalOpen, setIsModalOpen] = useState(false) const { updateTask } = useTeamTasks() @@ -56,14 +52,14 @@ const IssuesModal: FC = ({ currentIssue?.name === "Bug" ? 15 : currentIssue?.name === "Story" ? 14 : 13 return ( - <> + = ({ {currentIssue?.name} @@ -110,7 +106,7 @@ const IssuesModal: FC = ({ /> - + ) } @@ -141,6 +137,15 @@ const ModalPopUp = ({ visible, children, onDismiss }) => { } return ( + onDismiss()}> @@ -185,7 +190,6 @@ const Item = ({ issue, onChangeIssue, closeModal, readonly = false }: IItem) => const $modalBackGround: ViewStyle = { flex: 1, - backgroundColor: "#000000AA", justifyContent: "center", } diff --git a/apps/mobile/app/components/StatusType.tsx b/apps/mobile/app/components/StatusType.tsx index 86002049d..f42ce589e 100644 --- a/apps/mobile/app/components/StatusType.tsx +++ b/apps/mobile/app/components/StatusType.tsx @@ -1,65 +1,80 @@ -import React, { useMemo } from 'react'; -import { View } from 'react-native'; -import { SvgUri } from 'react-native-svg'; -import { useTaskLabels } from '../services/hooks/features/useTaskLabels'; -import { useTaskPriority } from '../services/hooks/features/useTaskPriority'; -import { useTaskSizes } from '../services/hooks/features/useTaskSizes'; -import { useTaskStatus } from '../services/hooks/features/useTaskStatus'; -import { ITaskStatusItem } from '../services/interfaces/ITaskStatus'; +import React, { useMemo } from "react" +import { View } from "react-native" +import { SvgUri } from "react-native-svg" +import { useTaskLabels } from "../services/hooks/features/useTaskLabels" +import { useTaskPriority } from "../services/hooks/features/useTaskPriority" +import { useTaskSizes } from "../services/hooks/features/useTaskSizes" +import { useTaskStatus } from "../services/hooks/features/useTaskStatus" +import { ITaskStatusItem } from "../services/interfaces/ITaskStatus" +import { useTaskVersion } from "../services/hooks/features/useTaskVersion" export type TStatusItem = { - bgColor?: string; - icon?: React.ReactNode | undefined; - name?: string; - value?: string; - bordered?: boolean; -}; + bgColor?: string + icon?: React.ReactNode | undefined + name?: string + value?: string + bordered?: boolean +} export type TStatus = { - [k in T]: TStatusItem; -}; + [k in T]: TStatusItem +} -export function useMapToTaskStatusValues(data: T[], bordered = false): TStatus { +export function useMapToTaskStatusValues( + data: T[], + bordered = false, +): TStatus { return useMemo(() => { return data.reduce((acc, item) => { const value: TStatus[string] = { - name: item.name?.split('-').join(' '), + name: item.name?.split("-").join(" "), value: item.value || item.name, bgColor: item.color, bordered, - icon: {item.fullIconUrl && } - }; + icon: ( + + {item.fullIconUrl && ( + + )} + + ), + } if (value.name) { - acc[value.name] = value; + acc[value.name] = value } else if (value.value) { - acc[value.value] = value; + acc[value.value] = value } - return acc; - }, {} as TStatus); - }, [data, bordered]); + return acc + }, {} as TStatus) + }, [data, bordered]) } // ==================== Task Status ======================================== export function useTaskStatusValue() { - const { allStatuses } = useTaskStatus(); - return useMapToTaskStatusValues(allStatuses); + const { allStatuses } = useTaskStatus() + return useMapToTaskStatusValues(allStatuses) } // =================== Task Size ============================================== export function useTaskSizeValue() { - const { allTaskSizes } = useTaskSizes(); - return useMapToTaskStatusValues(allTaskSizes); + const { allTaskSizes } = useTaskSizes() + return useMapToTaskStatusValues(allTaskSizes) } // =================== Task Label ============================================== export function useTaskLabelValue() { - const { allTaskLabels } = useTaskLabels(); - return useMapToTaskStatusValues(allTaskLabels); + const { allTaskLabels } = useTaskLabels() + return useMapToTaskStatusValues(allTaskLabels) } // =================== Task Priority ============================================== export function useTaskPriorityValue() { - const { allTaskPriorities } = useTaskPriority(); - return useMapToTaskStatusValues(allTaskPriorities); + const { allTaskPriorities } = useTaskPriority() + return useMapToTaskStatusValues(allTaskPriorities) +} +// =================== Task Priority ============================================== +export function useTaskVersionValue() { + const { taskVersionList } = useTaskVersion() + return useMapToTaskStatusValues(taskVersionList) } diff --git a/apps/mobile/app/components/Task/DetailsBlock/blocks/TaskMainInfo.tsx b/apps/mobile/app/components/Task/DetailsBlock/blocks/TaskMainInfo.tsx new file mode 100644 index 000000000..2261c9811 --- /dev/null +++ b/apps/mobile/app/components/Task/DetailsBlock/blocks/TaskMainInfo.tsx @@ -0,0 +1,350 @@ +/* eslint-disable react-native/no-inline-styles */ +/* eslint-disable react-native/no-color-literals */ +import { View, Text, StyleSheet } from "react-native" +import React from "react" +import { useStores } from "../../../../models" +import TaskRow from "../components/TaskRow" +import { SvgXml } from "react-native-svg" +import { + calendarIcon, + categoryIcon, + clipboardIcon, + peopleIconSmall, + profileIcon, +} from "../../../svgs/icons" +import ProfileInfo from "../components/ProfileInfo" +import ManageAssignees from "../components/ManageAssignees" +import { useOrganizationTeam } from "../../../../services/hooks/useOrganization" +import CalendarModal from "../components/CalendarModal" +import { useTeamTasks } from "../../../../services/hooks/features/useTeamTasks" +import moment from "moment-timezone" +import TaskStatus from "../../../TaskStatus" +import TaskSize from "../../../TaskSize" +import TaskPriority from "../../../TaskPriority" +import TaskLabels from "../../../TaskLabels" +import TaskVersion from "../../../TaskVersion" +import { useAppTheme } from "../../../../theme" +import { ITeamTask } from "../../../../services/interfaces/ITask" +import { TouchableOpacity } from "react-native-gesture-handler" +import { useNavigation } from "@react-navigation/native" +import { SettingScreenNavigationProp } from "../../../../navigators/AuthenticatedNavigator" +import TaskEpic from "../../../TaskEpic" +import IssuesModal from "../../../IssuesModal" +import { observer } from "mobx-react-lite" +import { translate } from "../../../../i18n" + +const TaskMainInfo = observer(() => { + const { + TaskStore: { detailedTask: task }, + } = useStores() + + const { currentTeam } = useOrganizationTeam() + const { updateTask } = useTeamTasks() + + const { colors } = useAppTheme() + + return ( + + {/* Issue type */} + + + + {translate("taskDetailsScreen.typeIssue")} + + + } + > + + + {/* Creator */} + + + + {translate("taskDetailsScreen.creator")} + + + } + > + + + {/* Assignees */} + + + + {translate("taskDetailsScreen.assignees")} + + + } + > + {task?.members?.map((member, index) => ( + + ))} + + {/* Manage Assignees */} + + + + {/* Manage Start Date */} + + + + {translate("taskDetailsScreen.startDate")} + + + } + > + updateTask({ ...task, startDate: date }, task?.id)} + selectedDate={task?.startDate} + /> + + + {/* Manage Due Date */} + + + {translate("taskDetailsScreen.dueDate")} + + + } + > + updateTask({ ...task, dueDate: date }, task?.id)} + selectedDate={task?.dueDate} + isDueDate={true} + /> + + + {/* Days Remaining */} + {task?.startDate && task?.dueDate && ( + + + {translate("taskDetailsScreen.daysRemaining")} + + + } + > + + {moment(task?.dueDate).diff(moment(), "days") < 0 + ? 0 + : moment(task?.dueDate).diff(moment(), "days")} + + + )} + + {/* horizontal separator */} + + + {/* Version */} + + + {translate("taskDetailsScreen.version")} + + + } + > + + + + {/* Epic */} + {task && task.issueType === "Story" && ( + + + {translate("taskDetailsScreen.epic")} + + + } + > + + + )} + {task && } + + {/* Status */} + + + {translate("taskDetailsScreen.status")} + + + } + > + + + + {/* Labels */} + + + {translate("taskDetailsScreen.labels")} + + + } + > + + + + {/* Size */} + + {translate("taskDetailsScreen.size")} + + } + > + + + + {/* Priority */} + + + {translate("taskDetailsScreen.priority")} + + + } + > + + + + ) +}) + +export default TaskMainInfo + +const EpicParent: React.FC<{ task: ITeamTask }> = ({ task }) => { + const { colors } = useAppTheme() + + const navigation = useNavigation>() + + const navigateToEpic = () => { + navigation.navigate("TaskScreen", { taskId: task?.rootEpic?.id }) + } + + if (task?.issueType === "Story") { + return <> + } + return (!task?.issueType || task?.issueType === "Task" || task?.issueType === "Bug") && + task?.rootEpic ? ( + + {translate("taskDetailsScreen.epic")} + + } + > + + + + + {`#${task?.rootEpic?.number} ${task?.rootEpic?.title}`} + + + ) : ( + <> + ) +} + +const styles = StyleSheet.create({ + epicParentButton: { alignItems: "center", flexDirection: "row", gap: 4, width: "60%" }, + epicParentIconWrapper: { + backgroundColor: "#8154BA", + borderRadius: 4, + marginVertical: 5, + padding: 4, + }, + horizontalSeparator: { + borderBottomColor: "#F2F2F2", + borderBottomWidth: 1, + marginVertical: 10, + width: "100%", + }, + labelComponent: { + alignItems: "center", + flexDirection: "row", + gap: 7, + }, + labelText: { + color: "#A5A2B2", + fontSize: 12, + }, +}) diff --git a/apps/mobile/app/components/Task/DetailsBlock/blocks/TaskPublicity.tsx b/apps/mobile/app/components/Task/DetailsBlock/blocks/TaskPublicity.tsx new file mode 100644 index 000000000..98ee9060d --- /dev/null +++ b/apps/mobile/app/components/Task/DetailsBlock/blocks/TaskPublicity.tsx @@ -0,0 +1,86 @@ +/* eslint-disable react-native/no-inline-styles */ +/* eslint-disable react-native/no-color-literals */ +import { View, Text, StyleSheet } from "react-native" +import React, { useCallback, useEffect, useState } from "react" +import { useStores } from "../../../../models" +import { translate } from "../../../../i18n" +import { SvgXml } from "react-native-svg" +import { globeDarkTheme, globeLightTheme, lockDarkTheme, lockLightTheme } from "../../../svgs/icons" +import { useAppTheme } from "../../../../theme" +import { TouchableOpacity } from "react-native-gesture-handler" +import { debounce } from "lodash" +import { useTeamTasks } from "../../../../services/hooks/features/useTeamTasks" + +const TaskPublicity = () => { + const { + TaskStore: { detailedTask: task }, + } = useStores() + const { updatePublicity } = useTeamTasks() + + const { dark, colors } = useAppTheme() + const [isTaskPublic, setIsTaskPublic] = useState(task?.public) + + const handlePublicity = useCallback( + (value: boolean) => { + setIsTaskPublic(value) + const debounceUpdatePublicity = debounce((value) => { + updatePublicity(value, task, true) + }, 500) + debounceUpdatePublicity(value) + }, + [task, updatePublicity], + ) + + useEffect(() => { + setIsTaskPublic(task?.public) + }, [task?.public]) + + return ( + + {isTaskPublic ? ( + + + + + {translate("taskDetailsScreen.taskPublic")} + + + handlePublicity(false)}> + + {translate("taskDetailsScreen.makePrivate")} + + + + ) : ( + + + + + {translate("taskDetailsScreen.taskPrivate")} + + + handlePublicity(true)}> + + {translate("taskDetailsScreen.makePublic")} + + + + )} + + ) +} + +export default TaskPublicity + +const styles = StyleSheet.create({ + taskPrivacyWrapper: { + alignItems: "center", + flexDirection: "row", + gap: 8, + }, + wrapper: { + alignItems: "center", + flexDirection: "row", + justifyContent: "space-between", + }, +}) diff --git a/apps/mobile/app/components/Task/DetailsBlock/components/CalendarModal.tsx b/apps/mobile/app/components/Task/DetailsBlock/components/CalendarModal.tsx new file mode 100644 index 000000000..974f46be6 --- /dev/null +++ b/apps/mobile/app/components/Task/DetailsBlock/components/CalendarModal.tsx @@ -0,0 +1,155 @@ +/* eslint-disable react-native/no-inline-styles */ +import { + View, + Text, + Animated, + Modal, + TouchableWithoutFeedback, + TouchableOpacity, + ViewStyle, + StyleSheet, +} from "react-native" +import React, { useEffect, useState } from "react" +import { useAppTheme } from "../../../../theme" +import { Calendar } from "react-native-calendars" +import moment from "moment-timezone" +import { SvgXml } from "react-native-svg" +import { trashIconSmall } from "../../../svgs/icons" +import { BlurView } from "expo-blur" +import { translate } from "../../../../i18n" + +interface ICalendarModal { + selectedDate: string + isDueDate?: boolean + updateTask: (date: string) => void +} + +const CalendarModal: React.FC = ({ selectedDate, isDueDate, updateTask }) => { + const { colors } = useAppTheme() + const [modalVisible, setModalVisible] = useState(false) + const [selected, setSelected] = useState("") + + useEffect(() => { + selectedDate && setSelected(moment(selectedDate).format("YYYY-MM-DD")) + }, [selectedDate]) + + const formatted = moment(selected).format("DD MMM YYYY") + + return ( + + + setModalVisible(true)}> + + {selected + ? formatted + : isDueDate + ? translate("taskDetailsScreen.setDueDate") + : translate("taskDetailsScreen.setStartDate")} + + + + {selected && selectedDate && ( + { + setSelected("") + updateTask(null) + }} + > + + + )} + + + setModalVisible(false)}> + + { + setSelected(day.dateString) + updateTask(moment(day.dateString).toISOString()) + }} + markedDates={{ + [selected]: { + selected: true, + disableTouchEvent: true, + selectedColor: colors.secondary, + dotColor: "orange", + }, + }} + theme={{ + todayTextColor: colors.secondary, + arrowColor: colors.secondary, + }} + /> + + + + ) +} + +export default CalendarModal + +const ModalPopUp = ({ visible, children, onDismiss }) => { + const [showModal, setShowModal] = React.useState(visible) + const scaleValue = React.useRef(new Animated.Value(0)).current + + React.useEffect(() => { + toggleModal() + }, [visible]) + const toggleModal = () => { + if (visible) { + setShowModal(true) + Animated.spring(scaleValue, { + toValue: 1, + useNativeDriver: true, + }).start() + } else { + setTimeout(() => setShowModal(false), 200) + Animated.timing(scaleValue, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }).start() + } + } + return ( + + + + + + {children} + + + + + ) +} + +const $modalBackGround: ViewStyle = { + flex: 1, + justifyContent: "center", +} + +const styles = StyleSheet.create({ + buttonWrapper: { + alignItems: "center", + flexDirection: "row", + height: 14, + justifyContent: "space-between", + width: 110, + }, + container: { + alignSelf: "center", + borderRadius: 20, + padding: 20, + width: "90%", + }, +}) diff --git a/apps/mobile/app/components/Task/DetailsBlock/components/ManageAssignees.tsx b/apps/mobile/app/components/Task/DetailsBlock/components/ManageAssignees.tsx new file mode 100644 index 000000000..d93fc4fc2 --- /dev/null +++ b/apps/mobile/app/components/Task/DetailsBlock/components/ManageAssignees.tsx @@ -0,0 +1,233 @@ +/* eslint-disable react-native/no-color-literals */ +/* eslint-disable react-native/no-inline-styles */ +/* eslint-disable camelcase */ +import { + View, + Text, + Modal, + TouchableWithoutFeedback, + Animated, + ViewStyle, + StyleSheet, + TouchableOpacity, + Dimensions, +} from "react-native" +import React, { useEffect, useMemo, useState } from "react" +import { useAppTheme } from "../../../../theme" +import { OT_Member } from "../../../../services/interfaces/IOrganizationTeam" +import ProfileInfo from "./ProfileInfo" +import { SvgXml } from "react-native-svg" +import { trashIconLarge } from "../../../svgs/icons" +import { ITeamTask } from "../../../../services/interfaces/ITask" +import { useTeamMemberCard } from "../../../../services/hooks/features/useTeamMemberCard" +import { ScrollView } from "react-native-gesture-handler" +import { BlurView } from "expo-blur" +import { translate } from "../../../../i18n" + +interface IManageAssignees { + memberList: OT_Member[] + task: ITeamTask +} + +const ManageAssignees: React.FC = ({ memberList, task }) => { + const [modalVisible, setModalVisible] = useState(false) + const [member, setMember] = useState() + const [memberToRemove, setMemberToRemove] = useState(false) + const [memberToAdd, setMemberToAdd] = useState(false) + + const { colors } = useAppTheme() + const memberInfo = useTeamMemberCard(member) + const { height } = Dimensions.get("window") + + const assignedToTaskMembers = useMemo( + () => + memberList?.filter((member) => + member.employee + ? task?.members.map((item) => item.userId).includes(member.employee?.userId) + : false, + ), + [memberList, task?.members], + ) + + const unassignedMembers = useMemo( + () => + memberList?.filter((member) => + member.employee + ? !task?.members.map((item) => item.userId).includes(member.employee.userId) + : false, + ), + [memberList, task?.members], + ) + + useEffect(() => { + if (task && member && memberToRemove) { + memberInfo + .unassignTask(task) + .then(() => { + setMember(undefined) + setMemberToRemove(false) + }) + .catch(() => { + setMember(undefined) + setMemberToRemove(false) + }) + } else if (task && member && memberToAdd) { + memberInfo + .assignTask(task) + .then(() => { + setMember(undefined) + setMemberToAdd(false) + }) + .catch(() => { + setMember(undefined) + setMemberToAdd(false) + }) + } + }, [task, member, memberInfo, memberToAdd, memberToRemove]) + + return ( + + setModalVisible(true)} style={styles.button}> + + {translate("taskDetailsScreen.manageAssignees")} + + + + setModalVisible(false)}> + + + {assignedToTaskMembers?.map((member, index) => ( + { + setMember(member) + setMemberToRemove(true) + setModalVisible(false) + }} + key={index} + style={styles.memberWrapper} + > + + + + + + + + ))} + {unassignedMembers?.map((member, index) => ( + { + setMember(member) + setMemberToAdd(true) + setModalVisible(false) + }} + key={index} + style={styles.memberWrapper} + > + + + + + ))} + + + + + ) +} + +export default ManageAssignees + +const ModalPopUp = ({ visible, children, onDismiss }) => { + const [showModal, setShowModal] = React.useState(visible) + const scaleValue = React.useRef(new Animated.Value(0)).current + + React.useEffect(() => { + toggleModal() + }, [visible]) + const toggleModal = () => { + if (visible) { + setShowModal(true) + Animated.spring(scaleValue, { + toValue: 1, + useNativeDriver: true, + }).start() + } else { + setTimeout(() => setShowModal(false), 200) + Animated.timing(scaleValue, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }).start() + } + } + return ( + + + + + + {children} + + + + + ) +} + +const $modalBackGround: ViewStyle = { + flex: 1, + justifyContent: "center", +} + +const styles = StyleSheet.create({ + button: { + alignItems: "center", + borderColor: "#E5E7EB", + borderRadius: 100, + borderWidth: 1, + height: 24, + justifyContent: "center", + marginVertical: 10, + paddingHorizontal: 8, + paddingVertical: 4, + width: 140, + }, + container: { + alignSelf: "center", + borderRadius: 20, + padding: 20, + width: "70%", + }, + memberWrapper: { + alignItems: "center", + flexDirection: "row", + justifyContent: "space-between", + marginBottom: 10, + }, +}) diff --git a/apps/mobile/app/components/Task/DetailsBlock/components/ProfileInfo.tsx b/apps/mobile/app/components/Task/DetailsBlock/components/ProfileInfo.tsx new file mode 100644 index 000000000..a19c22bf2 --- /dev/null +++ b/apps/mobile/app/components/Task/DetailsBlock/components/ProfileInfo.tsx @@ -0,0 +1,84 @@ +/* eslint-disable react-native/no-inline-styles */ +/* eslint-disable react-native/no-color-literals */ +import { Text, StyleSheet, TouchableOpacity } from "react-native" +import React from "react" +import { Avatar } from "react-native-paper" +import { imgTitleProfileAvatar } from "../../../../helpers/img-title-profile-avatar" +import { typography, useAppTheme } from "../../../../theme" +import { limitTextCharaters } from "../../../../helpers/sub-text" +import { useNavigation } from "@react-navigation/native" +import { + DrawerNavigationProp, + SettingScreenNavigationProp, +} from "../../../../navigators/AuthenticatedNavigator" + +interface IProfileInfo { + names: string + profilePicSrc: string + userId?: string + largerProfileInfo?: boolean +} + +const ProfileInfo: React.FC = ({ + profilePicSrc, + names, + userId, + largerProfileInfo, +}) => { + const { colors } = useAppTheme() + + const alternateNavigation = useNavigation>() + const navigation = useNavigation>() + + const navigateToProfile = () => { + alternateNavigation.navigate("AuthenticatedTab") + setTimeout(() => { + navigation.navigate("Profile", { userId, activeTab: "worked" }) + }, 50) + } + return ( + + {profilePicSrc ? ( + + ) : ( + + )} + + + {limitTextCharaters({ text: names.trim(), numChars: 18 })} + + + ) +} + +export default ProfileInfo + +const styles = StyleSheet.create({ + container: { + alignItems: "center", + flexDirection: "row", + gap: 7, + }, + prefix: { + color: "#FFFFFF", + fontFamily: typography.fonts.PlusJakartaSans.light, + }, + profileImage: { + borderRadius: 100, + }, +}) diff --git a/apps/mobile/app/components/Task/DetailsBlock/components/TaskRow.tsx b/apps/mobile/app/components/Task/DetailsBlock/components/TaskRow.tsx new file mode 100644 index 000000000..f2576fa44 --- /dev/null +++ b/apps/mobile/app/components/Task/DetailsBlock/components/TaskRow.tsx @@ -0,0 +1,34 @@ +/* eslint-disable react-native/no-inline-styles */ +import { View, StyleSheet } from "react-native" +import React from "react" + +interface ITaskRow { + labelComponent: React.ReactNode + children: React.ReactNode + alignItems?: boolean +} + +const TaskRow = ({ labelComponent, children, alignItems }: ITaskRow) => { + return ( + + {labelComponent} + {children} + + ) +} + +export default TaskRow + +const styles = StyleSheet.create({ + childrenContainer: { + gap: 7, + width: "56%", + }, + container: { + flexDirection: "row", + justifyContent: "space-between", + }, + labelContainer: { + width: "40%", + }, +}) diff --git a/apps/mobile/app/components/Task/DetailsBlock/index.tsx b/apps/mobile/app/components/Task/DetailsBlock/index.tsx new file mode 100644 index 000000000..a4b209f0a --- /dev/null +++ b/apps/mobile/app/components/Task/DetailsBlock/index.tsx @@ -0,0 +1,16 @@ +import React from "react" +import Accordion from "../../Accordion" +import TaskPublicity from "./blocks/TaskPublicity" +import TaskMainInfo from "./blocks/TaskMainInfo" +import { translate } from "../../../i18n" + +const DetailsBlock = () => { + return ( + + + + + ) +} + +export default DetailsBlock diff --git a/apps/mobile/app/components/Task/TitleBlock/CreateParentTaskModal.tsx b/apps/mobile/app/components/Task/TitleBlock/CreateParentTaskModal.tsx index 8356d53af..c3ffcc69d 100644 --- a/apps/mobile/app/components/Task/TitleBlock/CreateParentTaskModal.tsx +++ b/apps/mobile/app/components/Task/TitleBlock/CreateParentTaskModal.tsx @@ -21,6 +21,7 @@ import { useTaskInput } from "../../../services/hooks/features/useTaskInput" import { typography, useAppTheme } from "../../../theme" import { Feather } from "@expo/vector-icons" import { ITeamTask } from "../../../services/interfaces/ITask" +import { BlurView } from "expo-blur" interface ICreateParentTaskModal { visible: boolean @@ -181,6 +182,15 @@ const ModalPopUp = ({ visible, children, onDismiss }) => { } return ( + @@ -194,7 +204,6 @@ const ModalPopUp = ({ visible, children, onDismiss }) => { const $modalBackGround: ViewStyle = { flex: 1, - backgroundColor: "#000000AA", justifyContent: "center", } diff --git a/apps/mobile/app/components/Task/TitleBlock/index.tsx b/apps/mobile/app/components/Task/TitleBlock/index.tsx index 2adf6a770..f073a4d3c 100644 --- a/apps/mobile/app/components/Task/TitleBlock/index.tsx +++ b/apps/mobile/app/components/Task/TitleBlock/index.tsx @@ -30,8 +30,6 @@ const TaskTitleBlock = observer(() => { const { updateTitle } = useTeamTasks() - const { width } = Dimensions.get("window") - const saveTitle = useCallback((newTitle: string) => { if (newTitle.length > 255) { showMessage({ @@ -60,22 +58,8 @@ const TaskTitleBlock = observer(() => { }) } - const responsiveFontSize = (): number => { - const baseWidth = 428 - const scale = width / baseWidth - const baseFontSize = 10 - - const fontSize = Math.round(baseFontSize * scale) - - if (fontSize < 10) { - return 10 - } - - return fontSize - } - return ( - + { #{task?.number} - + {task?.issueType !== "Epic" && ( { task?.rootEpic && task?.parentId !== task?.rootEpic.id && ( { /> )} - + - + ) @@ -188,117 +167,111 @@ const TitleIcons: React.FC = ({ dark, edit, setEdit, copyTitle, sav ) } -const ParentTaskBadge: React.FC<{ task: ITeamTask; responsiveFontSize: () => number }> = observer( - ({ task, responsiveFontSize }) => { - const navigation = useNavigation>() +const ParentTaskBadge: React.FC<{ task: ITeamTask }> = observer(({ task }) => { + const navigation = useNavigation>() - const { width } = Dimensions.get("window") + const { width } = Dimensions.get("window") - const navigateToParent = (): void => { - navigation.navigate("TaskScreen", { taskId: task?.parentId || task?.parent.id }) - } - return task?.parentId && task?.parent ? ( - { + navigation.navigate("TaskScreen", { taskId: task?.parentId || task?.parent.id }) + } + return task?.parentId && task?.parent ? ( + + - - #{task?.parent?.taskNumber || task?.parent.number} - - {` - ${limitTextCharaters({ - text: task?.parent?.title, - numChars: width < 391 ? 8 : width <= 410 ? 12 : 18, - })}`} + #{task?.parent?.taskNumber || task?.parent.number} - - ) : ( - <> - ) - }, -) + {` - ${limitTextCharaters({ + text: task?.parent?.title, + numChars: width < 391 ? 8 : width <= 410 ? 12 : 18, + })}`} + + + ) : ( + <> + ) +}) -const ParentTaskInput: React.FC<{ task: ITeamTask; responsiveFontSize: () => number }> = observer( - ({ task, responsiveFontSize }) => { - const [modalVisible, setModalVisible] = useState(false) - return task && task?.issueType !== "Epic" ? ( - setModalVisible(true)} - > - - {task?.parentId - ? translate("taskDetailsScreen.changeParent") - : "+ " + translate("taskDetailsScreen.addParent")} - +const ParentTaskInput: React.FC<{ task: ITeamTask }> = observer(({ task }) => { + const [modalVisible, setModalVisible] = useState(false) + return task && task?.issueType !== "Epic" ? ( + setModalVisible(true)} + > + + {task?.parentId + ? translate("taskDetailsScreen.changeParent") + : "+ " + translate("taskDetailsScreen.addParent")} + - setModalVisible(false)} - task={task} - /> - - ) : ( - <> - ) - }, -) + setModalVisible(false)} + task={task} + /> + + ) : ( + <> + ) +}) const styles = StyleSheet.create({ copyButton: { diff --git a/apps/mobile/app/components/TaskEpic.tsx b/apps/mobile/app/components/TaskEpic.tsx new file mode 100644 index 000000000..5ee6b7df9 --- /dev/null +++ b/apps/mobile/app/components/TaskEpic.tsx @@ -0,0 +1,164 @@ +/* eslint-disable react-native/no-color-literals */ +/* eslint-disable react-native/no-inline-styles */ +import { StyleSheet, Text, View, ViewStyle } from "react-native" +import React, { useCallback, useMemo, useState } from "react" +import { TouchableOpacity } from "react-native-gesture-handler" +import { ITeamTask } from "../services/interfaces/ITask" +import { typography, useAppTheme } from "../theme" +import { useTeamTasks } from "../services/hooks/features/useTeamTasks" +import { cloneDeep } from "lodash" +import { SvgXml } from "react-native-svg" +import { categoryIcon } from "./svgs/icons" +import TaskEpicPopup from "./TaskEpicPopup" +import { Entypo } from "@expo/vector-icons" +import { translate } from "../i18n" + +interface ITaskEpic { + task: ITeamTask + containerStyle?: ViewStyle +} + +export type formattedEpic = { + [key: string]: { + icon: React.ReactNode + id: string + name: string + value: string + } +} + +const TaskEpic: React.FC = ({ containerStyle, task }) => { + const { colors, dark } = useAppTheme() + const { updateTask } = useTeamTasks() + + const [openModal, setOpenModal] = useState(false) + + const { teamTasks } = useTeamTasks() + + const epicsList = useMemo(() => { + const temp: formattedEpic = {} + teamTasks.forEach((task) => { + if (task.issueType === "Epic") { + temp[`#${task.taskNumber} ${task.title}`] = { + id: task.id, + name: `#${task.taskNumber} ${task.title}`, + value: task.id, + icon: ( + + + + ), + } + } + }) + return temp + }, [teamTasks]) + + const onTaskSelect = useCallback( + async (parentTask: ITeamTask | undefined) => { + if (!parentTask) return + const childTask = cloneDeep(task) + + await updateTask( + { + ...childTask, + parentId: parentTask.id ? parentTask.id : null, + parent: parentTask.id ? parentTask : null, + }, + task?.id, + ) + }, + [task, updateTask], + ) + return ( + <> + setOpenModal(false)} + onTaskSelect={onTaskSelect} + epicsList={epicsList} + currentEpic={task?.parent?.id} + teamTasks={teamTasks} + /> + + setOpenModal(true)}> + + {task?.parent ? ( + <> + + + + {`#${task?.parent?.number} ${task?.parent?.title}`} + + ) : ( + <> + + + {translate("taskDetailsScreen.epic")} + + + )} + + + + ) +} + +export default TaskEpic + +const styles = StyleSheet.create({ + container: { + alignItems: "center", + borderColor: "rgba(0,0,0,0.16)", + borderRadius: 10, + flexDirection: "row", + gap: 3, + justifyContent: "space-between", + minHeight: 30, + minWidth: 100, + paddingHorizontal: 8, + }, + epicParentIconWrapper: { + backgroundColor: "#8154BA", + borderRadius: 4, + marginVertical: 5, + padding: 4, + }, + text: { + fontFamily: typography.fonts.PlusJakartaSans.semiBold, + fontSize: 10, + textTransform: "capitalize", + width: "90%", + }, +}) diff --git a/apps/mobile/app/components/TaskEpicPopup.tsx b/apps/mobile/app/components/TaskEpicPopup.tsx new file mode 100644 index 000000000..2b3b1eb36 --- /dev/null +++ b/apps/mobile/app/components/TaskEpicPopup.tsx @@ -0,0 +1,187 @@ +/* eslint-disable react-native/no-inline-styles */ +/* eslint-disable react-native/no-color-literals */ +import React from "react" +import { BlurView } from "expo-blur" +import { + Animated, + Modal, + TouchableWithoutFeedback, + View, + Text, + ViewStyle, + StyleSheet, + FlatList, + TouchableOpacity, +} from "react-native" +import { Feather, AntDesign } from "@expo/vector-icons" +import { useAppTheme } from "../theme" +import { ITeamTask } from "../services/interfaces/ITask" +import { formattedEpic } from "./TaskEpic" + +interface ITaskEpicPopup { + visible: boolean + onDismiss: () => unknown + onTaskSelect: (parentTask: ITeamTask | undefined) => Promise + epicsList: formattedEpic + currentEpic: string + teamTasks: ITeamTask[] +} + +const TaskEpicPopup: React.FC = ({ + visible, + onDismiss, + onTaskSelect, + epicsList, + currentEpic, + teamTasks, +}) => { + const { colors } = useAppTheme() + + const allEpics = Object.values(epicsList) + + return ( + + + ( + + )} + legacyImplementation={true} + showsVerticalScrollIndicator={true} + keyExtractor={(_, index) => index.toString()} + /> + + + ) +} + +export default TaskEpicPopup + +interface ItemProps { + currentEpicId: string + onTaskSelect: (parentTask: ITeamTask | undefined) => Promise + epic: formattedEpic[keyof formattedEpic] + teamTasks: ITeamTask[] + onDismiss() +} +const Item: React.FC = ({ currentEpicId, epic, onTaskSelect, teamTasks, onDismiss }) => { + const { colors } = useAppTheme() + const selected = epic.id === currentEpicId + + const epicTask = teamTasks.find((task) => task.id === epic.id) + + return ( + { + onTaskSelect(epicTask) + onDismiss() + }} + > + + + {epic.icon} + {epic.name} + + + {!selected ? ( + + ) : ( + + )} + + + + ) +} + +const ModalPopUp = ({ visible, children, onDismiss }) => { + const [showModal, setShowModal] = React.useState(visible) + const scaleValue = React.useRef(new Animated.Value(0)).current + + React.useEffect(() => { + toggleModal() + }, [visible]) + const toggleModal = () => { + if (visible) { + setShowModal(true) + Animated.spring(scaleValue, { + toValue: 1, + useNativeDriver: true, + }).start() + } else { + setTimeout(() => setShowModal(false), 200) + Animated.timing(scaleValue, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }).start() + } + } + return ( + + + onDismiss()}> + + + {children} + + + + + ) +} + +const $modalBackGround: ViewStyle = { + flex: 1, + justifyContent: "center", +} + +const styles = StyleSheet.create({ + colorFrame: { + alignItems: "center", + borderRadius: 10, + flexDirection: "row", + gap: 5, + height: 44, + justifyContent: "center", + paddingLeft: 16, + width: 180, + }, + container: { + alignSelf: "center", + backgroundColor: "#fff", + borderRadius: 20, + maxHeight: 396, + paddingHorizontal: 6, + paddingVertical: 16, + width: "80%", + }, + wrapperItem: { + alignItems: "center", + borderColor: "rgba(0,0,0,0.13)", + borderRadius: 10, + borderWidth: 1, + flexDirection: "row", + justifyContent: "space-between", + marginBottom: 10, + padding: 2, + paddingRight: 18, + width: "100%", + }, +}) diff --git a/apps/mobile/app/components/TaskLabels.tsx b/apps/mobile/app/components/TaskLabels.tsx index ac18409ed..b4362482b 100644 --- a/apps/mobile/app/components/TaskLabels.tsx +++ b/apps/mobile/app/components/TaskLabels.tsx @@ -1,274 +1,323 @@ /* eslint-disable react-native/no-color-literals */ /* eslint-disable react-native/no-inline-styles */ -import React, { FC, useEffect, useRef, useState } from 'react'; -import { TouchableOpacity, View, Text, StyleSheet, ViewStyle, FlatList } from 'react-native'; -import { AntDesign, Entypo } from '@expo/vector-icons'; -import { observer } from 'mobx-react-lite'; -import { ITeamTask } from '../services/interfaces/ITask'; -import { useTeamTasks } from '../services/hooks/features/useTeamTasks'; -import { useAppTheme, typography } from '../theme'; -import TaskLabelPopup from './TaskLabelPopup'; -import { ITaskLabelItem } from '../services/interfaces/ITaskLabel'; -import { translate } from '../i18n'; -import { limitTextCharaters } from '../helpers/sub-text'; -import { SvgUri } from 'react-native-svg'; -import { isEqual } from 'lodash'; +import React, { FC, useEffect, useRef, useState } from "react" +import { TouchableOpacity, View, Text, StyleSheet, ViewStyle, FlatList } from "react-native" +import { AntDesign, Entypo } from "@expo/vector-icons" +import { observer } from "mobx-react-lite" +import { ITeamTask } from "../services/interfaces/ITask" +import { useTeamTasks } from "../services/hooks/features/useTeamTasks" +import { useAppTheme, typography } from "../theme" +import TaskLabelPopup from "./TaskLabelPopup" +import { ITaskLabelItem } from "../services/interfaces/ITaskLabel" +import { translate } from "../i18n" +import { limitTextCharaters } from "../helpers/sub-text" +import { SvgUri } from "react-native-svg" +import { isEqual } from "lodash" interface TaskLabelProps { - task?: ITeamTask; - containerStyle?: ViewStyle; - labels?: string; - setLabels?: (label: ITaskLabelItem[]) => unknown; - newTaskLabels?: ITaskLabelItem[] | undefined; + task?: ITeamTask + containerStyle?: ViewStyle + labels?: string + setLabels?: (label: ITaskLabelItem[]) => unknown + newTaskLabels?: ITaskLabelItem[] | undefined + taskScreenButton?: boolean + noBorders?: boolean } interface IndividualTaskLabel { - color: string; - createdAt: string; - description: string | null; - fullIconUrl: string; - icon: string; - id: string; - isSystem: boolean; - name: string; - organizationId: string; - organizationTeamId: string; - tenantId: string; - updatedAt: string; + color: string + createdAt: string + description: string | null + fullIconUrl: string + icon: string + id: string + isSystem: boolean + name: string + organizationId: string + organizationTeamId: string + tenantId: string + updatedAt: string } -const TaskLabels: FC = observer(({ task, setLabels, newTaskLabels }) => { - const { colors, dark } = useAppTheme(); - const { updateTask } = useTeamTasks(); - const [openModal, setOpenModal] = useState(false); - const flatListRef = useRef(null); - const [labelIndex, setLabelIndex] = useState(0); - const [tempLabels, setTempLabels] = useState(task?.tags || newTaskLabels || []); - const [arrayChanged, setArrayChanged] = useState(false); +const TaskLabels: FC = observer( + ({ task, setLabels, newTaskLabels, taskScreenButton, noBorders, containerStyle }) => { + const { colors, dark } = useAppTheme() + const { updateTask } = useTeamTasks() + const [openModal, setOpenModal] = useState(false) + const flatListRef = useRef(null) + const [labelIndex, setLabelIndex] = useState(0) + const [tempLabels, setTempLabels] = useState( + task?.tags || newTaskLabels || [], + ) + const [arrayChanged, setArrayChanged] = useState(false) - const freshOpenModal = () => { - setOpenModal(true); - setTempLabels(task?.tags || newTaskLabels || []); - arraysHaveSameValues(tempLabels, task?.tags || newTaskLabels || []); - }; + const freshOpenModal = () => { + setOpenModal(true) + setTempLabels(task?.tags || newTaskLabels || []) + arraysHaveSameValues(tempLabels, task?.tags || newTaskLabels || []) + } - const saveLabels = async () => { - if (task) { - const taskEdit = { - ...task, - tags: tempLabels - }; - await updateTask(taskEdit, task.id); - } else { - setLabels(tempLabels); + const saveLabels = async () => { + if (task) { + const taskEdit = { + ...task, + tags: tempLabels, + } + await updateTask(taskEdit, task.id) + } else { + setLabels(tempLabels) + } + setOpenModal(false) } - setOpenModal(false); - }; - const addOrRemoveLabelsInTempArray = (tag: ITaskLabelItem): void => { - const exist = tempLabels.find((label) => label.id === tag.id); - if (exist) { - setTempLabels(tempLabels.filter((label) => label.id !== tag.id)); - } else { - setTempLabels([...tempLabels, tag]); + const addOrRemoveLabelsInTempArray = (tag: ITaskLabelItem): void => { + const exist = tempLabels.find((label) => label.id === tag.id) + if (exist) { + setTempLabels(tempLabels.filter((label) => label.id !== tag.id)) + } else { + setTempLabels([...tempLabels, tag]) + } } - }; - const arraysHaveSameValues = (array1: ITaskLabelItem[] | [], array2: ITaskLabelItem[] | []): void => { - const sortedArray1 = array1.slice().sort((a, b) => a.id.localeCompare(b.id)); - const sortedArray2 = array2.slice().sort((a, b) => a.id.localeCompare(b.id)); + const arraysHaveSameValues = ( + array1: ITaskLabelItem[] | [], + array2: ITaskLabelItem[] | [], + ): void => { + const sortedArray1 = array1.slice().sort((a, b) => a.id.localeCompare(b.id)) + const sortedArray2 = array2.slice().sort((a, b) => a.id.localeCompare(b.id)) - const areArraysEqual = isEqual(sortedArray1, sortedArray2); + const areArraysEqual = isEqual(sortedArray1, sortedArray2) - setArrayChanged(!areArraysEqual); - }; + setArrayChanged(!areArraysEqual) + } - useEffect(() => { - arraysHaveSameValues(tempLabels, task?.tags || newTaskLabels || []); - }, [tempLabels]); + useEffect(() => { + arraysHaveSameValues(tempLabels, task?.tags || newTaskLabels || []) + }, [tempLabels]) - const scrollToIndexWithDelay = (index: number) => { - flatListRef.current?.scrollToIndex({ - animated: true, - index: index < 0 ? 0 : index, - viewPosition: 0 - }); - }; + const scrollToIndexWithDelay = (index: number) => { + flatListRef.current?.scrollToIndex({ + animated: true, + index: index < 0 ? 0 : index, + viewPosition: 0, + }) + } - const onNextPressed = () => { - if (labelIndex !== task?.tags?.length - 2) { - scrollToIndexWithDelay(labelIndex + 1); + const onNextPressed = () => { + if (labelIndex !== task?.tags?.length - 2) { + scrollToIndexWithDelay(labelIndex + 1) + } } - }; - const onPrevPressed = () => { - if (labelIndex > 0) { - const newIndex = labelIndex - 2; - scrollToIndexWithDelay(newIndex); + const onPrevPressed = () => { + if (labelIndex > 0) { + const newIndex = labelIndex - 2 + scrollToIndexWithDelay(newIndex) + } } - }; - const handleScrollEnd = (event: any) => { - const offsetX = event.nativeEvent.contentOffset.x; - const currentIndex = Math.round(offsetX / 100); // Assuming 100 is the item width - setLabelIndex(currentIndex); - }; + const handleScrollEnd = (event: any) => { + const offsetX = event.nativeEvent.contentOffset.x + const currentIndex = Math.round(offsetX / 100) // Assuming 100 is the item width + setLabelIndex(currentIndex) + } - return ( - <> - setOpenModal(false)} - canCreateLabel={true} - /> - {(task?.tags !== undefined || newTaskLabels !== undefined) && - (task?.tags?.length > 0 || newTaskLabels?.length > 0) ? ( - - - ) : ( - - - - - - - {translate('settingScreen.labelScreen.labels')} - - + + + + + {translate("settingScreen.labelScreen.labels")} + + - - - - )} - - ); -}); + + + + )} + + ) + }, +) interface ILabel { - item: IndividualTaskLabel | null; - freshOpenModal: () => void; + item: IndividualTaskLabel | null + freshOpenModal: () => void + taskScreenButton?: boolean + noBorders?: boolean } -const Label: FC = ({ item, freshOpenModal }) => { - const { colors } = useAppTheme(); +const Label: FC = ({ item, freshOpenModal, taskScreenButton, noBorders }) => { + const { colors } = useAppTheme() return ( - {limitTextCharaters({ text: item?.name, numChars: 12 })} + {limitTextCharaters({ text: item?.name, numChars: taskScreenButton ? 15 : 12 })} - ); -}; + ) +} const styles = StyleSheet.create({ container: { - alignItems: 'center', + alignItems: "center", borderRadius: 10, borderWidth: 1, - flexDirection: 'row', + flexDirection: "row", height: 32, - justifyContent: 'space-between', + justifyContent: "space-between", marginVertical: 20, paddingHorizontal: 12, paddingVertical: 7, - width: 160 + width: 160, }, scrollButtons: { - alignItems: 'center', - backgroundColor: '#fff', + alignItems: "center", + backgroundColor: "#fff", borderRadius: 20, bottom: 23, elevation: 10, height: 27, - justifyContent: 'center', + justifyContent: "center", padding: 5, - position: 'absolute', - shadowColor: 'rgba(0,0,0,0.16)', + position: "absolute", + shadowColor: "rgba(0,0,0,0.16)", shadowOffset: { width: 0, height: 5 }, shadowOpacity: 1, shadowRadius: 15, - width: 28 + width: 28, }, text: { fontFamily: typography.fonts.PlusJakartaSans.semiBold, - fontSize: 10 + fontSize: 10, }, wrapStatus: { - alignItems: 'center', - flexDirection: 'row', - width: '70%' - } -}); + alignItems: "center", + flexDirection: "row", + width: "70%", + }, +}) -export default TaskLabels; +export default TaskLabels diff --git a/apps/mobile/app/components/TaskStatus.tsx b/apps/mobile/app/components/TaskStatus.tsx index 2f248f960..e28f4f9c9 100644 --- a/apps/mobile/app/components/TaskStatus.tsx +++ b/apps/mobile/app/components/TaskStatus.tsx @@ -1,115 +1,131 @@ /* eslint-disable react-native/no-color-literals */ /* eslint-disable react-native/no-inline-styles */ -import React, { FC, useState } from 'react'; -import { TouchableOpacity, View, Text, StyleSheet, ViewStyle, TextStyle } from 'react-native'; -import { AntDesign, Feather } from '@expo/vector-icons'; -import { ITaskStatus, ITeamTask } from '../services/interfaces/ITask'; -import { observer } from 'mobx-react-lite'; -import { useTeamTasks } from '../services/hooks/features/useTeamTasks'; -import TaskStatusPopup from './TaskStatusPopup'; -import { typography, useAppTheme } from '../theme'; -import { translate } from '../i18n'; -import { useTaskStatusValue } from './StatusType'; -import { limitTextCharaters } from '../helpers/sub-text'; +import React, { FC, useState } from "react" +import { TouchableOpacity, View, Text, StyleSheet, ViewStyle, TextStyle } from "react-native" +import { AntDesign, Feather } from "@expo/vector-icons" +import { ITaskStatus, ITeamTask } from "../services/interfaces/ITask" +import { observer } from "mobx-react-lite" +import { useTeamTasks } from "../services/hooks/features/useTeamTasks" +import TaskStatusPopup from "./TaskStatusPopup" +import { typography, useAppTheme } from "../theme" +import { translate } from "../i18n" +import { useTaskStatusValue } from "./StatusType" +import { limitTextCharaters } from "../helpers/sub-text" interface TaskStatusProps { - task?: ITeamTask; - containerStyle?: ViewStyle; - statusTextSyle?: TextStyle; - iconsOnly?: boolean; - status?: string; - setStatus?: (status: string) => unknown; + task?: ITeamTask + containerStyle?: ViewStyle + statusTextSyle?: TextStyle + iconsOnly?: boolean + status?: string + setStatus?: (status: string) => unknown } -const TaskStatus: FC = observer(({ task, containerStyle, status, setStatus, iconsOnly }) => { - const { colors, dark } = useAppTheme(); - const { updateTask } = useTeamTasks(); - const [openModal, setOpenModal] = useState(false); +const TaskStatus: FC = observer( + ({ task, containerStyle, status, setStatus, iconsOnly }) => { + const { colors, dark } = useAppTheme() + const { updateTask } = useTeamTasks() + const [openModal, setOpenModal] = useState(false) - const allStatuses = useTaskStatusValue(); + const allStatuses = useTaskStatusValue() - const statusValue = (task?.status?.split('-').join(' ') || (status && status.split('-').join(' ')))?.toLowerCase(); - const statusItem = - allStatuses && Object.values(allStatuses).find((item) => item?.name.toLowerCase() === statusValue); + const statusValue = ( + task?.status?.split("-").join(" ") || + (status && status.split("-").join(" ")) + )?.toLowerCase() + const statusItem = + allStatuses && + Object.values(allStatuses).find((item) => item?.name.toLowerCase() === statusValue) - const onChangeStatus = async (text) => { - if (task) { - const value: ITaskStatus = text; - const taskEdit = { - ...task, - status: value - }; + const onChangeStatus = async (text) => { + if (task) { + const value: ITaskStatus = text + const taskEdit = { + ...task, + status: value, + } - await updateTask(taskEdit, task.id); - } else { - setStatus(text); + await updateTask(taskEdit, task.id) + } else { + setStatus(text) + } } - }; - return ( - <> - onChangeStatus(e)} - onDismiss={() => setOpenModal(false)} - /> - setOpenModal(true)}> - - {statusItem ? ( - - {statusItem.icon} - {iconsOnly ? null : ( - - {limitTextCharaters({ text: statusItem?.name, numChars: 11 })} - - )} - - ) : ( - - {iconsOnly ? ( - - ) : ( - translate('settingScreen.statusScreen.statuses') - )} - - )} - - - - - ); -}); + return ( + <> + onChangeStatus(e)} + onDismiss={() => setOpenModal(false)} + /> + setOpenModal(true)}> + + {statusItem ? ( + + {statusItem.icon} + {iconsOnly ? null : ( + + {limitTextCharaters({ + text: statusItem?.name, + numChars: 11, + })} + + )} + + ) : ( + + {iconsOnly ? ( + + ) : ( + translate("settingScreen.statusScreen.statuses") + )} + + )} + + + + + ) + }, +) const styles = StyleSheet.create({ container: { - alignItems: 'center', + alignItems: "center", borderRadius: 10, - flexDirection: 'row', - justifyContent: 'space-between', + flexDirection: "row", + justifyContent: "space-between", minHeight: 30, - paddingHorizontal: 8 + paddingHorizontal: 8, }, text: { fontFamily: typography.fonts.PlusJakartaSans.semiBold, fontSize: 10, - textTransform: 'capitalize' + textTransform: "capitalize", }, wrapStatus: { - alignItems: 'center', - flexDirection: 'row' - } -}); + alignItems: "center", + flexDirection: "row", + }, +}) -export default TaskStatus; +export default TaskStatus diff --git a/apps/mobile/app/components/TaskVersion.tsx b/apps/mobile/app/components/TaskVersion.tsx new file mode 100644 index 000000000..5ffbe7398 --- /dev/null +++ b/apps/mobile/app/components/TaskVersion.tsx @@ -0,0 +1,124 @@ +/* eslint-disable react-native/no-color-literals */ +/* eslint-disable react-native/no-inline-styles */ +import React, { FC, useState } from "react" +import { TouchableOpacity, View, Text, StyleSheet, ViewStyle } from "react-native" +import { AntDesign, Entypo } from "@expo/vector-icons" +import { observer } from "mobx-react-lite" +import { ITeamTask } from "../services/interfaces/ITask" +import { useTeamTasks } from "../services/hooks/features/useTeamTasks" +import { typography, useAppTheme } from "../theme" +import { translate } from "../i18n" +import { useTaskVersionValue } from "./StatusType" +import { limitTextCharaters } from "../helpers/sub-text" +import TaskVersionPopup from "./TaskVersionPopup" + +interface TaskVersionProps { + task?: ITeamTask + containerStyle?: ViewStyle + version?: string + setPriority?: (priority: string) => unknown +} + +const TaskVersion: FC = observer( + ({ task, containerStyle, version, setPriority }) => { + const { colors } = useAppTheme() + const { updateTask } = useTeamTasks() + const [openModal, setOpenModal] = useState(false) + + const allTaskVersions = useTaskVersionValue() + + const versionValue = (task?.version || (version && version))?.toLowerCase() + + const currentVersion = + allTaskVersions && + Object.values(allTaskVersions).find((item) => item.value.toLowerCase() === versionValue) + + const onChangeVersion = async (text) => { + if (task) { + const taskEdit = { + ...task, + version: text, + } + + await updateTask(taskEdit, task.id) + } else { + setPriority(text) + } + } + + return ( + <> + onChangeVersion(e.value)} + onDismiss={() => setOpenModal(false)} + /> + setOpenModal(true)}> + + {(task?.version || version) && currentVersion ? ( + + {currentVersion.icon} + + {limitTextCharaters({ + text: currentVersion.name, + numChars: 15, + })} + + + ) : ( + + + + {translate("taskDetailsScreen.version")} + + + )} + + + + + ) + }, +) + +const styles = StyleSheet.create({ + container: { + alignItems: "center", + borderColor: "rgba(0,0,0,0.16)", + borderRadius: 10, + borderWidth: 1, + flexDirection: "row", + justifyContent: "space-between", + minHeight: 30, + minWidth: 100, + paddingHorizontal: 8, + }, + text: { + fontFamily: typography.fonts.PlusJakartaSans.semiBold, + fontSize: 10, + textTransform: "capitalize", + }, + wrapStatus: { + alignItems: "center", + flexDirection: "row", + width: "70%", + }, +}) + +export default TaskVersion diff --git a/apps/mobile/app/components/TaskVersionPopup.tsx b/apps/mobile/app/components/TaskVersionPopup.tsx new file mode 100644 index 000000000..0c5c1349c --- /dev/null +++ b/apps/mobile/app/components/TaskVersionPopup.tsx @@ -0,0 +1,180 @@ +/* eslint-disable react-native/no-inline-styles */ +/* eslint-disable react-native/no-color-literals */ +import React, { FC } from "react" +import { + View, + ViewStyle, + Modal, + Animated, + StyleSheet, + Text, + FlatList, + TouchableOpacity, + TouchableWithoutFeedback, +} from "react-native" +import { Feather, AntDesign } from "@expo/vector-icons" +import { spacing, useAppTheme } from "../theme" +import { ITaskPriorityItem } from "../services/interfaces/ITaskPriority" +// import { translate } from "../i18n" +import { BlurView } from "expo-blur" +import { useTaskVersion } from "../services/hooks/features/useTaskVersion" +import { BadgedTaskVersion } from "./VersionIcon" +import { ITaskVersionItemList } from "../services/interfaces/ITaskVersion" + +export interface Props { + visible: boolean + onDismiss: () => unknown + versionName: string + setSelectedVersion: (status: ITaskVersionItemList) => unknown +} + +const TaskVersionPopup: FC = function TaskPriorityPopup({ + visible, + onDismiss, + setSelectedVersion, + versionName, +}) { + const { taskVersionList } = useTaskVersion() + const { colors } = useAppTheme() + const onVersionSelected = (size: ITaskPriorityItem) => { + setSelectedVersion(size) + onDismiss() + } + + return ( + + + Versions + ( + + )} + legacyImplementation={true} + showsVerticalScrollIndicator={true} + keyExtractor={(_, index) => index.toString()} + /> + + + ) +} + +export default TaskVersionPopup + +interface ItemProps { + currentVersionName: string + version: ITaskPriorityItem + onVersionSelected: (size: ITaskPriorityItem) => unknown +} +const Item: FC = ({ currentVersionName, version, onVersionSelected }) => { + const { colors } = useAppTheme() + const selected = version.value === currentVersionName + + return ( + onVersionSelected(version)}> + + + + + + {!selected ? ( + + ) : ( + + )} + + + + ) +} + +const ModalPopUp = ({ visible, children, onDismiss }) => { + const [showModal, setShowModal] = React.useState(visible) + const scaleValue = React.useRef(new Animated.Value(0)).current + + React.useEffect(() => { + toggleModal() + }, [visible]) + const toggleModal = () => { + if (visible) { + setShowModal(true) + Animated.spring(scaleValue, { + toValue: 1, + useNativeDriver: true, + }).start() + } else { + setTimeout(() => setShowModal(false), 200) + Animated.timing(scaleValue, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }).start() + } + } + return ( + + + onDismiss()}> + + + {children} + + + + + ) +} + +const $modalBackGround: ViewStyle = { + flex: 1, + justifyContent: "center", +} + +const styles = StyleSheet.create({ + colorFrame: { + borderRadius: 10, + height: 44, + justifyContent: "center", + paddingLeft: 16, + width: 180, + }, + container: { + alignSelf: "center", + backgroundColor: "#fff", + borderRadius: 20, + maxHeight: 396, + paddingHorizontal: 6, + paddingVertical: 16, + width: "90%", + }, + title: { + fontSize: spacing.medium - 2, + marginBottom: 16, + marginHorizontal: 10, + }, + wrapperItem: { + alignItems: "center", + borderColor: "rgba(0,0,0,0.13)", + borderRadius: 10, + borderWidth: 1, + flexDirection: "row", + justifyContent: "space-between", + marginBottom: 10, + padding: 6, + paddingRight: 18, + width: "100%", + }, +}) diff --git a/apps/mobile/app/components/VersionIcon.tsx b/apps/mobile/app/components/VersionIcon.tsx new file mode 100644 index 000000000..61219a604 --- /dev/null +++ b/apps/mobile/app/components/VersionIcon.tsx @@ -0,0 +1,42 @@ +/* eslint-disable react-native/no-color-literals */ +/* eslint-disable react-native/no-inline-styles */ +import React, { useMemo } from "react" +import { View, Text } from "react-native" +import { SvgUri } from "react-native-svg" +import { typography } from "../theme" +import { observer } from "mobx-react-lite" +import { limitTextCharaters } from "../helpers/sub-text" +import { useTaskVersion } from "../services/hooks/features/useTaskVersion" + +export const BadgedTaskVersion = observer( + ({ version, TextSize, iconSize }: { version: string; TextSize: number; iconSize: number }) => { + const { taskVersionList } = useTaskVersion() + + const currentSize = useMemo( + () => taskVersionList.find((s) => s.name === version), + [version, taskVersionList], + ) + + return ( + + + + {limitTextCharaters({ text: currentSize?.name, numChars: 15 })} + + + ) + }, +) diff --git a/apps/mobile/app/components/svgs/icons.tsx b/apps/mobile/app/components/svgs/icons.tsx index 48dcd2d17..b84a03ceb 100644 --- a/apps/mobile/app/components/svgs/icons.tsx +++ b/apps/mobile/app/components/svgs/icons.tsx @@ -591,3 +591,233 @@ export const copyIcon = ` ` +// Task Details + +export const globeLightTheme = ` + + + + + + ` + +export const globeDarkTheme = ` + + + + + + ` + +export const lockLightTheme = ` + + + + ` + +export const lockDarkTheme = ` + + + + ` + +export const clipboardIcon = ` + + + + +` + +export const profileIcon = ` + + + +` +export const peopleIconSmall = ` + + + + + + + +` +export const settingsIconSmall = ` + + + +` + +export const trashIconLarge = ` + + + + + +` + +export const trashIconSmall = ` + + + + + +` + +export const calendarIcon = ` + + + + + + + + +` +export const categoryIcon = ` + + + + + +` diff --git a/apps/mobile/app/i18n/ar.ts b/apps/mobile/app/i18n/ar.ts index 58422e238..6fc65e63b 100644 --- a/apps/mobile/app/i18n/ar.ts +++ b/apps/mobile/app/i18n/ar.ts @@ -97,6 +97,27 @@ const ar: Translations = { copyTitle: "تم نسخ العنوان.", changeParent: "تغيير الوالدين", addParent: "أضف أحد الوالدين", + taskScreen: "شاشة المهام", + details: "تفاصيل", + taskPublic: "هذه المهمة عامة", + makePrivate: "جعل خاص", + taskPrivate: "هذه المهمة خاصة", + makePublic: "جعل العامة", + typeIssue: "نوع المشكلة", + creator: "المُنشئ", + assignees: "المُنفذون", + startDate: "تاريخ البدء", + dueDate: "تاريخ الاستحقاق", + daysRemaining: "الأيام المتبقية", + version: "الإصدار", + epic: "ملحمة", + status: "الحالة", + labels: "التسميات", + size: "الحجم", + priority: "الأولوية", + manageAssignees: "إدارة المكلفين", + setDueDate: "تحديد تاريخ الاستحقاق", + setStartDate: "تحديد تاريخ البدء", }, tasksScreen: { name: "مهام", diff --git a/apps/mobile/app/i18n/bg.ts b/apps/mobile/app/i18n/bg.ts index 1b6fc8259..21de8b139 100644 --- a/apps/mobile/app/i18n/bg.ts +++ b/apps/mobile/app/i18n/bg.ts @@ -93,6 +93,27 @@ const bg = { copyTitle: "Title Copied.", changeParent: "Change Parent", addParent: "Add Parent", + taskScreen: "Task Screen", + details: "Details", + taskPublic: "This task is Public", + makePrivate: "Make a Private", + taskPrivate: "This task is Private", + makePublic: "Make a Public", + typeIssue: "Type of Issue", + creator: "Creator", + assignees: "Assignees", + startDate: "Start Date", + dueDate: "Due Date", + daysRemaining: "Days Remaining", + version: "Version", + epic: "Epic", + status: "Status", + labels: "Labels", + size: "Size", + priority: "Priority", + manageAssignees: "Manage Assignees", + setDueDate: "Set Due Date", + setStartDate: "Set Start Date", }, tasksScreen: { name: "Tasks", diff --git a/apps/mobile/app/i18n/en.ts b/apps/mobile/app/i18n/en.ts index 2ad2082fa..5fd9145c8 100644 --- a/apps/mobile/app/i18n/en.ts +++ b/apps/mobile/app/i18n/en.ts @@ -94,6 +94,27 @@ const en = { copyTitle: "Title Copied.", changeParent: "Change Parent", addParent: "Add Parent", + taskScreen: "Task Screen", + details: "Details", + taskPublic: "This task is Public", + makePrivate: "Make a Private", + taskPrivate: "This task is Private", + makePublic: "Make a Public", + typeIssue: "Type of Issue", + creator: "Creator", + assignees: "Assignees", + startDate: "Start Date", + dueDate: "Due Date", + daysRemaining: "Days Remaining", + version: "Version", + epic: "Epic", + status: "Status", + labels: "Labels", + size: "Size", + priority: "Priority", + manageAssignees: "Manage Assignees", + setDueDate: "Set Due Date", + setStartDate: "Set Start Date", }, tasksScreen: { name: "Tasks", diff --git a/apps/mobile/app/i18n/es.ts b/apps/mobile/app/i18n/es.ts index cfb450cd0..c2f29404b 100644 --- a/apps/mobile/app/i18n/es.ts +++ b/apps/mobile/app/i18n/es.ts @@ -93,6 +93,27 @@ const es = { copyTitle: "Title Copied.", changeParent: "Change Parent", addParent: "Add Parent", + taskScreen: "Task Screen", + details: "Details", + taskPublic: "This task is Public", + makePrivate: "Make a Private", + taskPrivate: "This task is Private", + makePublic: "Make a Public", + typeIssue: "Type of Issue", + creator: "Creator", + assignees: "Assignees", + startDate: "Start Date", + dueDate: "Due Date", + daysRemaining: "Days Remaining", + version: "Version", + epic: "Epic", + status: "Status", + labels: "Labels", + size: "Size", + priority: "Priority", + manageAssignees: "Manage Assignees", + setDueDate: "Set Due Date", + setStartDate: "Set Start Date", }, tasksScreen: { name: "Tasks", diff --git a/apps/mobile/app/i18n/fr.ts b/apps/mobile/app/i18n/fr.ts index 53f5d6c86..6000d24ae 100644 --- a/apps/mobile/app/i18n/fr.ts +++ b/apps/mobile/app/i18n/fr.ts @@ -96,6 +96,27 @@ const fr = { copyTitle: "Titre copié.", changeParent: "Changer de parent", addParent: "Ajouter un parent", + taskScreen: "Écran des tâches", + details: "Détails", + taskPublic: "Cette tâche est publique", + makePrivate: "Créer un privé", + taskPrivate: "Cette tâche est privée", + makePublic: "Faire un public", + typeIssue: "Type d'Issue", + creator: "Créateur", + assignees: "Bénéficiaires", + startDate: "Date de début", + dueDate: "Date d'échéance", + daysRemaining: "Jours restants", + version: "Version", + epic: "Épique", + status: "Statut", + labels: "Étiquettes", + size: "Taille", + priority: "Priorité", + manageAssignees: "Gérer les destinataires", + setDueDate: "Définir la date d'échéance", + setStartDate: "Définir la date de début", }, tasksScreen: { name: "Tâches", diff --git a/apps/mobile/app/i18n/he.ts b/apps/mobile/app/i18n/he.ts index d8a5cd820..ab5b47f40 100644 --- a/apps/mobile/app/i18n/he.ts +++ b/apps/mobile/app/i18n/he.ts @@ -93,6 +93,27 @@ const he = { copyTitle: "Title Copied.", changeParent: "Change Parent", addParent: "Add Parent", + taskScreen: "Task Screen", + details: "Details", + taskPublic: "This task is Public", + makePrivate: "Make a Private", + taskPrivate: "This task is Private", + makePublic: "Make a Public", + typeIssue: "Type of Issue", + creator: "Creator", + assignees: "Assignees", + startDate: "Start Date", + dueDate: "Due Date", + daysRemaining: "Days Remaining", + version: "Version", + epic: "Epic", + status: "Status", + labels: "Labels", + size: "Size", + priority: "Priority", + manageAssignees: "Manage Assignees", + setDueDate: "Set Due Date", + setStartDate: "Set Start Date", }, tasksScreen: { name: "Tasks", diff --git a/apps/mobile/app/i18n/ko.ts b/apps/mobile/app/i18n/ko.ts index 1129759a8..7f34d361d 100644 --- a/apps/mobile/app/i18n/ko.ts +++ b/apps/mobile/app/i18n/ko.ts @@ -96,6 +96,27 @@ const ko: Translations = { copyTitle: "제목이 복사되었습니다.", changeParent: "상위 변경", addParent: "상위 추가", + taskScreen: "작업 화면", + details: "세부", + taskPublic: "이 작업은 공개입니다.", + makePrivate: "비공개로 설정", + taskPrivate: "이 작업은 비공개입니다.", + makePublic: "공개로 설정", + typeIssue: "이슈 유형", + creator: "생성자", + assignees: "담당자", + startDate: "시작일", + dueDate: "마감일", + daysRemaining: "남은 일수", + version: "버전", + epic: "에픽", + status: "상태", + labels: "라벨", + size: "크기", + priority: "우선 순위", + manageAssignees: "담당자 관리", + setDueDate: "마감일 설정", + setStartDate: "시작일 설정", }, tasksScreen: { name: "작업", diff --git a/apps/mobile/app/i18n/ru.ts b/apps/mobile/app/i18n/ru.ts index 1d0117d5d..6833e2e73 100644 --- a/apps/mobile/app/i18n/ru.ts +++ b/apps/mobile/app/i18n/ru.ts @@ -93,6 +93,27 @@ const ru = { copyTitle: "Title Copied.", changeParent: "Change Parent", addParent: "Add Parent", + taskScreen: "Task Screen", + details: "Details", + taskPublic: "This task is Public", + makePrivate: "Make a Private", + taskPrivate: "This task is Private", + makePublic: "Make a Public", + typeIssue: "Type of Issue", + creator: "Creator", + assignees: "Assignees", + startDate: "Start Date", + dueDate: "Due Date", + daysRemaining: "Days Remaining", + version: "Version", + epic: "Epic", + status: "Status", + labels: "Labels", + size: "Size", + priority: "Priority", + manageAssignees: "Manage Assignees", + setDueDate: "Set Due Date", + setStartDate: "Set Start Date", }, tasksScreen: { name: "Tasks", diff --git a/apps/mobile/app/navigators/AuthenticatedNavigator.tsx b/apps/mobile/app/navigators/AuthenticatedNavigator.tsx index 30f420187..6143ced2d 100644 --- a/apps/mobile/app/navigators/AuthenticatedNavigator.tsx +++ b/apps/mobile/app/navigators/AuthenticatedNavigator.tsx @@ -86,6 +86,12 @@ export type SettingScreenNavigationProp > +export type DrawerNavigationProp = + CompositeNavigationProp< + BottomTabNavigationProp, + StackNavigationProp + > + export type SettingScreenRouteProp = RouteProp< AuthenticatedTabParamList, T diff --git a/apps/mobile/app/screens/Authenticated/TaskScreen/index.tsx b/apps/mobile/app/screens/Authenticated/TaskScreen/index.tsx index dfe3deaff..efb7b6331 100644 --- a/apps/mobile/app/screens/Authenticated/TaskScreen/index.tsx +++ b/apps/mobile/app/screens/Authenticated/TaskScreen/index.tsx @@ -1,14 +1,14 @@ /* eslint-disable react-native/no-inline-styles */ -import { View, Text, ViewStyle, TouchableOpacity, StyleSheet } from "react-native" +import { View, Text, ViewStyle, TouchableOpacity, StyleSheet, ScrollView } from "react-native" import React, { FC, useEffect } from "react" import { AuthenticatedDrawerScreenProps } from "../../../navigators/AuthenticatedNavigator" import { Screen } from "../../../components" -import Animated from "react-native-reanimated" import { typography, useAppTheme } from "../../../theme" import { AntDesign } from "@expo/vector-icons" import { useTeamTasks } from "../../../services/hooks/features/useTeamTasks" import TaskTitleBlock from "../../../components/Task/TitleBlock" -// import { translate } from "../../../i18n" +import DetailsBlock from "../../../components/Task/DetailsBlock" +import { translate } from "../../../i18n" export const AuthenticatedTaskScreen: FC> = ( _props, @@ -16,7 +16,6 @@ export const AuthenticatedTaskScreen: FC { @@ -27,24 +26,35 @@ export const AuthenticatedTaskScreen: FC - + navigation.navigate("AuthenticatedTab")}> - Task Screen + + {translate("taskDetailsScreen.taskScreen")} + - - + + + + + - + ) } @@ -73,6 +83,13 @@ const styles = StyleSheet.create({ flexDirection: "row", width: "100%", }, + screenContentWrapper: { + alignItems: "center", + flex: 4, + gap: 12, + paddingBottom: 20, + width: "100%", + }, title: { alignSelf: "center", diff --git a/apps/mobile/app/services/client/queries/task/task-version.ts b/apps/mobile/app/services/client/queries/task/task-version.ts new file mode 100644 index 000000000..0dcc31537 --- /dev/null +++ b/apps/mobile/app/services/client/queries/task/task-version.ts @@ -0,0 +1,27 @@ +import { useQuery } from "react-query" +import { getTaskVersionListRequest } from "../../requests/task-version" + +interface IGetTaskVersionsParams { + authToken: string + tenantId: string + organizationId: string + activeTeamId: string +} +const fetchAllVersions = async (params: IGetTaskVersionsParams) => { + const { organizationId, tenantId, activeTeamId, authToken } = params + const { data } = await getTaskVersionListRequest( + { + tenantId, + organizationId, + activeTeamId, + }, + authToken, + ) + return data +} + +const useFetchAllVersions = (IGetTaskVersionsParams) => + useQuery(["versions", IGetTaskVersionsParams], () => fetchAllVersions(IGetTaskVersionsParams), { + refetchInterval: 62000, + }) +export default useFetchAllVersions diff --git a/apps/mobile/app/services/client/requests/task-version.ts b/apps/mobile/app/services/client/requests/task-version.ts new file mode 100644 index 000000000..4c300c850 --- /dev/null +++ b/apps/mobile/app/services/client/requests/task-version.ts @@ -0,0 +1,73 @@ +/* eslint-disable camelcase */ +import { ITaskVersionCreate, ITaskVersionItemList } from "../../interfaces/ITaskVersion" +import { serverFetch } from "../fetch" + +export function createVersionRequest( + datas: ITaskVersionCreate, + bearer_token: string, + tenantId?: any, +) { + return serverFetch({ + path: "/task-versions", + method: "POST", + body: datas, + bearer_token, + tenantId, + }) +} + +export function editTaskVersionRequest({ + id, + datas, + bearer_token, + tenantId, +}: { + id: string | any + datas: ITaskVersionCreate + bearer_token: string + tenantId?: any +}) { + return serverFetch({ + path: `/task-versions/${id}`, + method: "PUT", + body: datas, + bearer_token, + tenantId, + }) +} + +export function deleteTaskVersionRequest({ + id, + bearer_token, + tenantId, +}: { + id: string | any + bearer_token: string | any + tenantId?: any +}) { + return serverFetch({ + path: `/task-versions/${id}`, + method: "DELETE", + bearer_token, + tenantId, + }) +} + +export function getTaskVersionListRequest( + { + organizationId, + tenantId, + activeTeamId, + }: { + tenantId: string + organizationId: string + activeTeamId: string | null + }, + bearer_token: string, +) { + return serverFetch({ + path: `/task-versions?tenantId=${tenantId}&organizationId=${organizationId}&organizationTeamId=${activeTeamId}`, + method: "GET", + bearer_token, + }) +} diff --git a/apps/mobile/app/services/hooks/features/useTaskVersion.ts b/apps/mobile/app/services/hooks/features/useTaskVersion.ts new file mode 100644 index 000000000..95e82a49d --- /dev/null +++ b/apps/mobile/app/services/hooks/features/useTaskVersion.ts @@ -0,0 +1,70 @@ +import { useQueryClient } from "react-query" +import { + createVersionRequest, + deleteTaskVersionRequest, + editTaskVersionRequest, +} from "../../client/requests/task-version" +import { ITaskVersionCreate, ITaskVersionItemList } from "../../interfaces/ITaskVersion" +import { useStores } from "../../../models" +import { useCallback, useEffect, useState } from "react" +import useFetchAllVersions from "../../client/queries/task/task-version" + +export function useTaskVersion() { + const queryClient = useQueryClient() + const { + authenticationStore: { authToken, tenantId, organizationId }, + teamStore: { activeTeamId }, + } = useStores() + + const [taskVersionList, setTaskVersionList] = useState([]) + + const { + data: versions, + isLoading, + isSuccess, + isRefetching, + } = useFetchAllVersions({ tenantId, organizationId, activeTeamId, authToken }) + + const createTaskVersion = useCallback( + async (data: ITaskVersionCreate) => { + await createVersionRequest(data, authToken, tenantId) + queryClient.invalidateQueries("versions") + }, + [authToken, tenantId, queryClient], + ) + + const deleteTaskVersion = useCallback( + async (id: string) => { + await deleteTaskVersionRequest({ bearer_token: authToken, tenantId, id }) + + queryClient.invalidateQueries("versions") + }, + [authToken, tenantId, queryClient], + ) + + const updateTaskVersion = useCallback( + async ({ id, data }) => { + await editTaskVersionRequest({ id, datas: data, bearer_token: authToken, tenantId }) + queryClient.invalidateQueries("versions") + }, + [authToken, tenantId, queryClient], + ) + + useEffect(() => { + if (isSuccess) { + if (versions) { + // @ts-ignore + setTaskVersionList(versions?.items || []) + } + } + }, [isLoading, isRefetching]) + + return { + createTaskVersion, + deleteTaskVersion, + updateTaskVersion, + taskVersionList, + versions, + isLoading, + } +} diff --git a/apps/mobile/app/services/hooks/features/useTeamTasks.ts b/apps/mobile/app/services/hooks/features/useTeamTasks.ts index 47c90e3b5..75bceca4d 100644 --- a/apps/mobile/app/services/hooks/features/useTeamTasks.ts +++ b/apps/mobile/app/services/hooks/features/useTeamTasks.ts @@ -243,6 +243,26 @@ export function useTeamTasks() { [], ) + const updatePublicity = useCallback( + (publicity: boolean, task?: ITeamTask | null, loader?: boolean) => { + if (task && publicity !== task.public) { + loader && setTasksFetching(true) + return updateTask( + { + ...task, + public: publicity, + }, + task.id, + ).then((res) => { + setTasksFetching(false) + return res + }) + } + return Promise.resolve() + }, + [setTasksFetching], + ) + /** * Change active task */ @@ -308,6 +328,7 @@ export function useTeamTasks() { detailedTask, deleteTask, updateTask, + updatePublicity, setActiveTeamTask, updateDescription, updateTitle, diff --git a/apps/mobile/app/services/interfaces/ITask.ts b/apps/mobile/app/services/interfaces/ITask.ts index 200c4f7fb..b6c13fc88 100644 --- a/apps/mobile/app/services/interfaces/ITask.ts +++ b/apps/mobile/app/services/interfaces/ITask.ts @@ -17,6 +17,7 @@ export type ITeamTask = { estimateDays?: number estimateHours?: number estimateMinutes?: number + startDate?: string dueDate: string projectId: string public: boolean diff --git a/apps/mobile/app/services/interfaces/ITaskVersion.ts b/apps/mobile/app/services/interfaces/ITaskVersion.ts new file mode 100644 index 000000000..62653e8a2 --- /dev/null +++ b/apps/mobile/app/services/interfaces/ITaskVersion.ts @@ -0,0 +1,28 @@ +export interface ITaskVersionItemList { + id: string + createdAt: string + updatedAt: string + tenantId: string + organizationId: string + name?: string + value?: string + description?: string + icon?: string + fullIconUrl?: string + color?: string + is_system?: boolean + isSystem?: boolean + projectId?: string +} + +export interface ITaskVersionCreate { + name: string + description?: string + icon?: string + color?: string + projectId?: string + organizationId?: string + tenantId?: string | undefined | null + organizationTeamId?: string | undefined | null + value?: string +} diff --git a/apps/mobile/package.json b/apps/mobile/package.json index b69178a00..38ff3efcf 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -82,6 +82,7 @@ "react-native": "0.71.8", "react-native-animatable": "^1.3.3", "react-native-bootsplash": "4.5.3", + "react-native-calendars": "^1.1302.0", "react-native-circular-progress": "^1.3.8", "react-native-dotenv": "^3.4.8", "react-native-dropdown-picker": "^5.4.6", diff --git a/apps/mobile/yarn.lock b/apps/mobile/yarn.lock index 7bd0935e4..833bae03c 100644 --- a/apps/mobile/yarn.lock +++ b/apps/mobile/yarn.lock @@ -7761,7 +7761,7 @@ hoek@6.x.x: resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c" integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ== -hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -9554,7 +9554,7 @@ lodash-es@^4.2.1: resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== -lodash.debounce@^4.0.8: +lodash.debounce@4.0.8, lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== @@ -9778,7 +9778,7 @@ memfs@^3.4.3: dependencies: fs-monkey "^1.0.3" -memoize-one@^5.0.0: +memoize-one@^5.0.0, memoize-one@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== @@ -12146,7 +12146,7 @@ prompts@^2.0.1, prompts@^2.2.1, prompts@^2.3.2, prompts@^2.4.0: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@*, prop-types@^15.5.10, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@*, prop-types@15.8.1, prop-types@^15.5.10, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -12459,6 +12459,21 @@ react-native-bootsplash@4.5.3: picocolors "^1.0.0" sharp "^0.31.3" +react-native-calendars@^1.1302.0: + version "1.1302.0" + resolved "https://registry.yarnpkg.com/react-native-calendars/-/react-native-calendars-1.1302.0.tgz#1b81074d08a9aa5aadcd2fb546d08517d4974952" + integrity sha512-QZdkFYVKafxjc/oHmbmzyEhMkF0sWl+1hYd9FbKQFcf/c3D0K+sfG81A40C1YsOR8nxb1nq2OpsNT81CGd1L4Q== + dependencies: + hoist-non-react-statics "^3.3.1" + lodash "^4.17.15" + memoize-one "^5.2.1" + prop-types "^15.5.10" + react-native-swipe-gestures "^1.0.5" + recyclerlistview "^4.0.0" + xdate "^0.8.0" + optionalDependencies: + moment "^2.29.4" + react-native-circular-progress@^1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/react-native-circular-progress/-/react-native-circular-progress-1.3.8.tgz#84713b42286e4778aaaeecd7910953bb9de018ce" @@ -12583,6 +12598,11 @@ react-native-svg@13.4.0: css-select "^5.1.0" css-tree "^1.1.3" +react-native-swipe-gestures@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/react-native-swipe-gestures/-/react-native-swipe-gestures-1.0.5.tgz#a172cb0f3e7478ccd681fd36b8bfbcdd098bde7c" + integrity sha512-Ns7Bn9H/Tyw278+5SQx9oAblDZ7JixyzeOczcBK8dipQk2pD7Djkcfnf1nB/8RErAmMLL9iXgW0QHqiII8AhKw== + react-native-tab-view@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/react-native-tab-view/-/react-native-tab-view-3.5.2.tgz#2789b8af6148b16835869566bf13dc3b0e6c1b46" @@ -12840,6 +12860,15 @@ recast@^0.20.4: source-map "~0.6.1" tslib "^2.0.1" +recyclerlistview@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/recyclerlistview/-/recyclerlistview-4.2.0.tgz#a140149aaa470c9787a1426452651934240d69ef" + integrity sha512-uuBCi0c+ggqHKwrzPX4Z/mJOzsBbjZEAwGGmlwpD/sD7raXixdAbdJ6BTcAmuWG50Cg4ru9p12M94Njwhr/27A== + dependencies: + lodash.debounce "4.0.8" + prop-types "15.8.1" + ts-object-utils "0.0.5" + redux-logger@^2.7.4: version "2.10.2" resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-2.10.2.tgz#3c5a5f0a6f32577c1deadf6655f257f82c6c3937" @@ -14694,6 +14723,11 @@ ts-jest@^29.1.0: semver "^7.5.3" yargs-parser "^21.0.1" +ts-object-utils@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/ts-object-utils/-/ts-object-utils-0.0.5.tgz#95361cdecd7e52167cfc5e634c76345e90a26077" + integrity sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA== + tsconfig-paths@^3.14.1: version "3.14.2" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" @@ -15470,6 +15504,11 @@ xcode@3.0.1, xcode@^3.0.0, xcode@^3.0.1: simple-plist "^1.1.0" uuid "^7.0.3" +xdate@^0.8.0: + version "0.8.2" + resolved "https://registry.yarnpkg.com/xdate/-/xdate-0.8.2.tgz#d7b033c00485d02695baf0044f4eacda3fc961a3" + integrity sha512-sNBlLfOC8S3V0vLDEUianQOXcTsc9j4lfeKU/klHe0RjHAYn0CXsSttumTot8dzalboV8gZbH38B+WcCIBjhFQ== + xdl@^51.5.0: version "51.6.4" resolved "https://registry.yarnpkg.com/xdl/-/xdl-51.6.4.tgz#32abaf71be10426f81a98a43b0def2f39ad4590e"