diff --git a/Layerfile b/Layerfile index 45b09dbfe..5df270bf5 100644 --- a/Layerfile +++ b/Layerfile @@ -13,6 +13,7 @@ RUN apt-get update && \ RUN REPEATABLE docker pull everco/ever-teams-webapp -RUN REPEATABLE docker run -d -p 3000:3000 everco/ever-teams-webapp && sleep 5 +RUN REPEATABLE (docker rm ever-teams --force || true) &&\ + docker run --name ever-teams -d -p 3000:3000 everco/ever-teams-webapp && sleep 5 EXPOSE WEBSITE localhost:3000 diff --git a/Layerfile.build b/Layerfile.build index bacc929c2..13f4cd554 100644 --- a/Layerfile.build +++ b/Layerfile.build @@ -17,6 +17,7 @@ COPY . . RUN REPEATABLE docker build -t ever-teams . -RUN REPEATABLE docker run -d -p 3000:3000 ever-teams && sleep 5 +RUN REPEATABLE (docker rm ever-teams --force || true) &&\ + docker run --name ever-teams -d -p 3000:3000 ever-teams && sleep 5 EXPOSE WEBSITE localhost:3000 diff --git a/README.md b/README.md index d00d7dd6f..6ab23a1fd 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,10 @@ WIP [![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](https://app.koyeb.com/deploy?name=ever-teams&type=docker&builder=dockerfile&image=ghcr.io/ever-co/ever-teams-webapp:latest&env[PORT]=3000&ports=3000;http;/) +### Northflank + +[Deploy to Northflank](https://app.northflank.com/s/account/templates/new?data=656ed069216b5d387f5379c6) + ## 📄 Content - `/web` - NextJs-based (React) Web App at (deployed from `main` branch) diff --git a/apps/mobile/app/components/Task/DetailsBlock/blocks/TaskMainInfo.tsx b/apps/mobile/app/components/Task/DetailsBlock/blocks/TaskMainInfo.tsx index 107dafe59..849dfd31d 100644 --- a/apps/mobile/app/components/Task/DetailsBlock/blocks/TaskMainInfo.tsx +++ b/apps/mobile/app/components/Task/DetailsBlock/blocks/TaskMainInfo.tsx @@ -148,6 +148,7 @@ const TaskMainInfo = observer(() => { > { > { > { > = ({ memberList, task }) => { const assignedToTaskMembers = useMemo( () => memberList?.filter((member) => - member.employee ? task?.members.map((item) => item.userId).includes(member.employee?.userId) : false + member.employee + ? task?.members.map((item) => item.userId).includes(member.employee?.userId) && + member.employee?.isActive + : false ), [memberList, task?.members] ); @@ -50,7 +53,10 @@ const ManageAssignees: React.FC = ({ memberList, task }) => { const unassignedMembers = useMemo( () => memberList?.filter((member) => - member.employee ? !task?.members.map((item) => item.userId).includes(member.employee.userId) : false + member.employee + ? !task?.members.map((item) => item.userId).includes(member.employee.userId) && + member.employee?.isActive + : false ), [memberList, task?.members] ); diff --git a/apps/mobile/app/components/TaskPriority.tsx b/apps/mobile/app/components/TaskPriority.tsx index 8a53788e1..e404f983f 100644 --- a/apps/mobile/app/components/TaskPriority.tsx +++ b/apps/mobile/app/components/TaskPriority.tsx @@ -1,53 +1,53 @@ /* 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 TaskPriorityPopup from "./TaskPriorityPopup" -import { translate } from "../i18n" -import { useTaskPriorityValue } from "./StatusType" -import { limitTextCharaters } from "../helpers/sub-text" +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 TaskPriorityPopup from './TaskPriorityPopup'; +import { translate } from '../i18n'; +import { useTaskPriorityValue } from './StatusType'; +import { limitTextCharaters } from '../helpers/sub-text'; interface TaskPriorityProps { - task?: ITeamTask - containerStyle?: ViewStyle - priority?: string - setPriority?: (priority: string) => unknown + task?: ITeamTask; + containerStyle?: ViewStyle; + priority?: string; + setPriority?: (priority: string) => unknown; + canCreatePriority?: boolean; } const TaskPriority: FC = observer( - ({ task, containerStyle, priority, setPriority }) => { - const { colors } = useAppTheme() - const { updateTask } = useTeamTasks() - const [openModal, setOpenModal] = useState(false) + ({ task, containerStyle, priority, setPriority, canCreatePriority }) => { + const { colors } = useAppTheme(); + const { updateTask } = useTeamTasks(); + const [openModal, setOpenModal] = useState(false); - const allTaskPriorities = useTaskPriorityValue() + const allTaskPriorities = useTaskPriorityValue(); const sizeValue = ( - task?.priority?.split("-").join(" ") || - (priority && priority.split("-").join(" ")) - )?.toLowerCase() + task?.priority?.split('-').join(' ') || + (priority && priority.split('-').join(' ')) + )?.toLowerCase(); const currentPriority = - allTaskPriorities && - Object.values(allTaskPriorities).find((item) => item.name.toLowerCase() === sizeValue) + allTaskPriorities && Object.values(allTaskPriorities).find((item) => item.name.toLowerCase() === sizeValue); const onChangePriority = async (text: string) => { if (task) { const taskEdit = { ...task, - priority: task?.priority === text ? null : text, - } + priority: task?.priority === text ? null : text + }; - await updateTask(taskEdit, task.id) + await updateTask(taskEdit, task.id); } else { - setPriority(text) + setPriority(text); } - } + }; return ( <> @@ -56,6 +56,7 @@ const TaskPriority: FC = observer( visible={openModal} setSelectedPriority={(e) => onChangePriority(e.value)} onDismiss={() => setOpenModal(false)} + canCreatePriority={canCreatePriority} /> setOpenModal(true)}> = observer( ...styles.container, ...containerStyle, borderColor: colors.border, - backgroundColor: currentPriority?.bgColor, + backgroundColor: currentPriority?.bgColor }} > {(task?.priority || priority) && currentPriority ? ( @@ -72,53 +73,51 @@ const TaskPriority: FC = observer( {limitTextCharaters({ text: currentPriority.name, - numChars: 15, + numChars: 15 })} ) : ( - - {translate("settingScreen.priorityScreen.priorities")} + + {translate('settingScreen.priorityScreen.priorities')} )} - ) - }, -) + ); + } +); const styles = StyleSheet.create({ container: { - alignItems: "center", - borderColor: "rgba(0,0,0,0.16)", + alignItems: 'center', + borderColor: 'rgba(0,0,0,0.16)', borderRadius: 10, borderWidth: 1, - flexDirection: "row", - justifyContent: "space-between", + flexDirection: 'row', + justifyContent: 'space-between', minHeight: 30, minWidth: 100, - paddingHorizontal: 8, + paddingHorizontal: 8 }, 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 TaskPriority +export default TaskPriority; diff --git a/apps/mobile/app/components/TaskPriorityPopup.tsx b/apps/mobile/app/components/TaskPriorityPopup.tsx index 900fe1dc8..d492db6fd 100644 --- a/apps/mobile/app/components/TaskPriorityPopup.tsx +++ b/apps/mobile/app/components/TaskPriorityPopup.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-native/no-inline-styles */ /* eslint-disable react-native/no-color-literals */ -import React, { FC } from 'react'; +import React, { FC, useState } from 'react'; import { View, ViewStyle, @@ -12,19 +12,21 @@ import { TouchableOpacity, TouchableWithoutFeedback } from 'react-native'; -import { Feather, AntDesign } from '@expo/vector-icons'; -import { spacing, useAppTheme } from '../theme'; +import { Feather, AntDesign, Ionicons } from '@expo/vector-icons'; +import { spacing, useAppTheme, typography } from '../theme'; import { BadgedTaskPriority } from './PriorityIcon'; import { useTaskPriority } from '../services/hooks/features/useTaskPriority'; import { ITaskPriorityItem } from '../services/interfaces/ITaskPriority'; import { translate } from '../i18n'; import { BlurView } from 'expo-blur'; +import TaskPriorityForm from '../screens/Authenticated/TaskPrioritiesScreen/components/TaskPriorityForm'; export interface Props { visible: boolean; onDismiss: () => unknown; priorityName: string; setSelectedPriority: (status: ITaskPriorityItem) => unknown; + canCreatePriority?: boolean; } const ModalPopUp = ({ visible, children, onDismiss }) => { @@ -74,10 +76,15 @@ const TaskPriorityPopup: FC = function TaskPriorityPopup({ visible, onDismiss, setSelectedPriority, - priorityName + priorityName, + canCreatePriority }) { const { allTaskPriorities } = useTaskPriority(); - const { colors } = useAppTheme(); + const { colors, dark } = useAppTheme(); + const { createPriority, updatePriority } = useTaskPriority(); + + const [createPriorityMode, setCreatePriorityMode] = useState(false); + const onPrioritySelected = (size: ITaskPriorityItem) => { setSelectedPriority(size); onDismiss(); @@ -85,20 +92,61 @@ const TaskPriorityPopup: FC = function TaskPriorityPopup({ return ( - - - {translate('settingScreen.priorityScreen.priorities')} - - ( - - )} - legacyImplementation={true} - showsVerticalScrollIndicator={true} - keyExtractor={(_, index) => index.toString()} - /> + + {!createPriorityMode ? ( + <> + + {translate('settingScreen.priorityScreen.priorities')} + + ( + + )} + legacyImplementation={true} + showsVerticalScrollIndicator={true} + keyExtractor={(_, index) => index.toString()} + /> + {canCreatePriority && ( + setCreatePriorityMode(true)} + > + + + {translate('settingScreen.priorityScreen.createNewPriorityText')} + + + )} + + ) : ( + setCreatePriorityMode(false)} + onCreatePriority={createPriority} + onUpdatePriority={updatePriority} + isEdit={false} + /> + )} ); @@ -138,6 +186,12 @@ const $modalBackGround: ViewStyle = { }; const styles = StyleSheet.create({ + btnText: { + color: '#3826A6', + fontFamily: typography.primary.semiBold, + fontSize: 16, + fontStyle: 'normal' + }, colorFrame: { borderRadius: 10, height: 44, @@ -154,6 +208,18 @@ const styles = StyleSheet.create({ paddingVertical: 16, width: '90%' }, + createButton: { + alignItems: 'center', + alignSelf: 'center', + borderColor: '#3826A6', + borderRadius: 12, + borderWidth: 2, + flexDirection: 'row', + justifyContent: 'center', + marginTop: 10, + padding: 12, + width: '80%' + }, title: { fontSize: spacing.medium - 2, marginBottom: 16, diff --git a/apps/mobile/app/components/TaskSize.tsx b/apps/mobile/app/components/TaskSize.tsx index 4d5587902..0aad6c20e 100644 --- a/apps/mobile/app/components/TaskSize.tsx +++ b/apps/mobile/app/components/TaskSize.tsx @@ -1,60 +1,57 @@ /* 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, 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 { showMessage } from "react-native-flash-message" -import { typography, useAppTheme } from "../theme" -import TaskSizePopup from "./TaskSizePopup" -import { translate } from "../i18n" -import { limitTextCharaters } from "../helpers/sub-text" -import { useTaskSizeValue } from "./StatusType" +import React, { FC, useState } from 'react'; +import { TouchableOpacity, View, Text, StyleSheet, ViewStyle, TextStyle } 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 { showMessage } from 'react-native-flash-message'; +import { typography, useAppTheme } from '../theme'; +import TaskSizePopup from './TaskSizePopup'; +import { translate } from '../i18n'; +import { limitTextCharaters } from '../helpers/sub-text'; +import { useTaskSizeValue } from './StatusType'; interface TaskSizeProps { - task?: ITeamTask - containerStyle?: ViewStyle - statusTextSyle?: TextStyle - size?: string - setSize?: (size: string) => unknown + task?: ITeamTask; + containerStyle?: ViewStyle; + statusTextSyle?: TextStyle; + size?: string; + setSize?: (size: string) => unknown; + canCreateSize?: boolean; } -const TaskSize: FC = observer(({ task, containerStyle, setSize, size }) => { - const { colors } = useAppTheme() - const { updateTask } = useTeamTasks() - const [openModal, setOpenModal] = useState(false) +const TaskSize: FC = observer(({ task, containerStyle, setSize, size, canCreateSize }) => { + const { colors } = useAppTheme(); + const { updateTask } = useTeamTasks(); + const [openModal, setOpenModal] = useState(false); - const allTaskSizes = useTaskSizeValue() + const allTaskSizes = useTaskSizeValue(); - const sizeValue = ( - task?.size?.split("-").join(" ") || - (size && size.split("-").join(" ")) - )?.toLowerCase() + const sizeValue = (task?.size?.split('-').join(' ') || (size && size.split('-').join(' ')))?.toLowerCase(); const currentSize = - allTaskSizes && - Object.values(allTaskSizes).find((item) => item.name.toLowerCase() === sizeValue) + allTaskSizes && Object.values(allTaskSizes).find((item) => item.name.toLowerCase() === sizeValue); const onChangeSize = async (text: string) => { if (task) { const taskEdit = { ...task, - size: task?.size === text ? null : text, - } + size: task?.size === text ? null : text + }; - const { response } = await updateTask(taskEdit, task.id) + const { response } = await updateTask(taskEdit, task.id); if (response.status !== 202) { showMessage({ - message: "Something went wrong", - type: "danger", - }) + message: 'Something went wrong', + type: 'danger' + }); } } else { - setSize(text) + setSize(text); } - } + }; return ( <> @@ -63,6 +60,7 @@ const TaskSize: FC = observer(({ task, containerStyle, setSize, s visible={openModal} setSelectedSize={(e) => onChangeSize(e.value)} onDismiss={() => setOpenModal(false)} + canCreateSize={canCreateSize} /> setOpenModal(true)}> = observer(({ task, containerStyle, setSize, s ...styles.container, ...containerStyle, borderColor: colors.border, - backgroundColor: currentSize?.bgColor, + backgroundColor: currentSize?.bgColor }} > {(task?.size || size) && currentSize ? ( @@ -84,42 +82,38 @@ const TaskSize: FC = observer(({ task, containerStyle, setSize, s - {translate("settingScreen.sizeScreen.sizes")} + {translate('settingScreen.sizeScreen.sizes')} )} - + - ) -}) + ); +}); const styles = StyleSheet.create({ container: { - alignItems: "center", - borderColor: "rgba(0,0,0,0.16)", + alignItems: 'center', + borderColor: 'rgba(0,0,0,0.16)', borderRadius: 10, borderWidth: 1, - flexDirection: "row", - justifyContent: "space-between", + flexDirection: 'row', + justifyContent: 'space-between', minHeight: 30, minWidth: 100, - paddingHorizontal: 8, + paddingHorizontal: 8 }, 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 TaskSize +export default TaskSize; diff --git a/apps/mobile/app/components/TaskSizePopup.tsx b/apps/mobile/app/components/TaskSizePopup.tsx index 9e5c83896..557256607 100644 --- a/apps/mobile/app/components/TaskSizePopup.tsx +++ b/apps/mobile/app/components/TaskSizePopup.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-native/no-inline-styles */ /* eslint-disable react-native/no-color-literals */ -import React, { FC } from 'react'; +import React, { FC, useState } from 'react'; import { View, ViewStyle, @@ -12,19 +12,21 @@ import { TouchableOpacity, TouchableWithoutFeedback } from 'react-native'; -import { Feather, AntDesign } from '@expo/vector-icons'; -import { spacing, useAppTheme } from '../theme'; +import { Feather, AntDesign, Ionicons } from '@expo/vector-icons'; +import { spacing, useAppTheme, typography } from '../theme'; import { useTaskSizes } from '../services/hooks/features/useTaskSizes'; import { ITaskSizeItem } from '../services/interfaces/ITaskSize'; import { BadgedTaskSize } from './SizeIcon'; import { translate } from '../i18n'; import { BlurView } from 'expo-blur'; +import TaskSizeForm from '../screens/Authenticated/TaskSizeScreen/components/TaskSizeForm'; export interface Props { visible: boolean; onDismiss: () => unknown; sizeName: string; setSelectedSize: (status: ITaskSizeItem) => unknown; + canCreateSize?: boolean; } const ModalPopUp = ({ visible, children, onDismiss }) => { @@ -70,9 +72,19 @@ const ModalPopUp = ({ visible, children, onDismiss }) => { ); }; -const TaskStatusPopup: FC = function FilterPopup({ visible, onDismiss, setSelectedSize, sizeName }) { +const TaskStatusPopup: FC = function FilterPopup({ + visible, + onDismiss, + setSelectedSize, + sizeName, + canCreateSize +}) { const { allTaskSizes } = useTaskSizes(); - const { colors } = useAppTheme(); + const { colors, dark } = useAppTheme(); + const { createSize, updateSize } = useTaskSizes(); + + const [createSizeMode, setCreateSizeMode] = useState(false); + const onStatusSelected = (size: ITaskSizeItem) => { setSelectedSize(size); onDismiss(); @@ -80,20 +92,57 @@ const TaskStatusPopup: FC = function FilterPopup({ visible, onDismiss, se return ( - - - {translate('settingScreen.sizeScreen.sizes')} - - ( - - )} - legacyImplementation={true} - showsVerticalScrollIndicator={true} - keyExtractor={(_, index) => index.toString()} - /> + + {!createSizeMode ? ( + <> + + {translate('settingScreen.sizeScreen.sizes')} + + ( + + )} + legacyImplementation={true} + showsVerticalScrollIndicator={true} + keyExtractor={(_, index) => index.toString()} + /> + {canCreateSize && ( + setCreateSizeMode(true)} + > + + + {translate('settingScreen.sizeScreen.createNewSizeText')} + + + )} + + ) : ( + setCreateSizeMode(false)} + onCreateSize={createSize} + onUpdateSize={updateSize} + isEdit={false} + /> + )} ); @@ -133,6 +182,12 @@ const $modalBackGround: ViewStyle = { }; const styles = StyleSheet.create({ + btnText: { + color: '#3826A6', + fontFamily: typography.primary.semiBold, + fontSize: 16, + fontStyle: 'normal' + }, colorFrame: { borderRadius: 10, height: 44, @@ -149,6 +204,18 @@ const styles = StyleSheet.create({ paddingVertical: 16, width: '90%' }, + createButton: { + alignItems: 'center', + alignSelf: 'center', + borderColor: '#3826A6', + borderRadius: 12, + borderWidth: 2, + flexDirection: 'row', + justifyContent: 'center', + marginTop: 10, + padding: 12, + width: '80%' + }, title: { fontSize: spacing.medium - 2, marginBottom: 16, diff --git a/apps/mobile/app/components/TaskStatus.tsx b/apps/mobile/app/components/TaskStatus.tsx index e2d54a7aa..e42821c89 100644 --- a/apps/mobile/app/components/TaskStatus.tsx +++ b/apps/mobile/app/components/TaskStatus.tsx @@ -1,55 +1,55 @@ /* 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 { 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 { 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 - labelOnly?: boolean - status?: string - setStatus?: (status: string) => unknown + task?: ITeamTask; + containerStyle?: ViewStyle; + statusTextSyle?: TextStyle; + iconsOnly?: boolean; + labelOnly?: boolean; + status?: string; + setStatus?: (status: string) => unknown; + canCreateStatus?: boolean; } const TaskStatus: FC = observer( - ({ task, containerStyle, status, setStatus, iconsOnly, labelOnly }) => { - const { colors, dark } = useAppTheme() - const { updateTask } = useTeamTasks() - const [openModal, setOpenModal] = useState(false) + ({ task, containerStyle, status, setStatus, iconsOnly, labelOnly, canCreateStatus }) => { + 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() + task?.status?.split('-').join(' ') || + (status && status.split('-').join(' ')) + )?.toLowerCase(); const statusItem = - allStatuses && - Object.values(allStatuses).find((item) => item?.name.toLowerCase() === statusValue) + allStatuses && Object.values(allStatuses).find((item) => item?.name.toLowerCase() === statusValue); const onChangeStatus = async (text: string) => { if (task) { const taskEdit = { ...task, - status: task?.status === text ? null : text, - } + status: task?.status === text ? null : text + }; - await updateTask(taskEdit, task.id) + await updateTask(taskEdit, task.id); } else { - setStatus(text) + setStatus(text); } - } + }; return ( <> @@ -58,6 +58,7 @@ const TaskStatus: FC = observer( visible={openModal} setSelectedStatus={(e) => onChangeStatus(e)} onDismiss={() => setOpenModal(false)} + canCreateStatus={canCreateStatus} /> setOpenModal(true)}> = observer( ...styles.container, borderWidth: iconsOnly ? 0 : 1, ...containerStyle, - backgroundColor: !dark ? "#F2F2F2" : colors.background, - borderColor: colors.border, + backgroundColor: !dark ? '#F2F2F2' : colors.background, + borderColor: colors.border }, - statusItem ? { backgroundColor: statusItem?.bgColor } : null, + statusItem ? { backgroundColor: statusItem?.bgColor } : null ]} > {statusItem ? ( - + {!labelOnly && statusItem.icon} {iconsOnly ? null : ( = observer( style={{ ...styles.text, marginLeft: labelOnly ? 0 : 11, - fontSize: labelOnly ? 8 : 10, + fontSize: labelOnly ? 8 : 10 }} > {limitTextCharaters({ text: statusItem?.name, - numChars: 11, + numChars: 11 })} )} @@ -96,40 +97,40 @@ const TaskStatus: FC = observer( {iconsOnly ? ( ) : ( - translate("settingScreen.statusScreen.statuses") + 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/TaskStatusPopup.tsx b/apps/mobile/app/components/TaskStatusPopup.tsx index 236277269..2caa183b9 100644 --- a/apps/mobile/app/components/TaskStatusPopup.tsx +++ b/apps/mobile/app/components/TaskStatusPopup.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-native/no-inline-styles */ /* eslint-disable react-native/no-color-literals */ -import React, { FC } from 'react'; +import React, { FC, useState } from 'react'; import { View, ViewStyle, @@ -12,19 +12,21 @@ import { TouchableOpacity, TouchableWithoutFeedback } from 'react-native'; -import { Feather, AntDesign } from '@expo/vector-icons'; +import { Feather, AntDesign, Ionicons } from '@expo/vector-icons'; import { useTaskStatus } from '../services/hooks/features/useTaskStatus'; import { ITaskStatusItem } from '../services/interfaces/ITaskStatus'; import { spacing, typography, useAppTheme } from '../theme'; import { translate } from '../i18n'; import { useTaskStatusValue } from './StatusType'; import { BlurView } from 'expo-blur'; +import TaskStatusForm from '../screens/Authenticated/TaskStatusScreen/components/TaskStatusForm'; export interface Props { visible: boolean; onDismiss: () => unknown; statusName: string; setSelectedStatus?: (status: string) => unknown; + canCreateStatus?: boolean; } const ModalPopUp = ({ visible, children, onDismiss }) => { @@ -70,9 +72,20 @@ const ModalPopUp = ({ visible, children, onDismiss }) => { ); }; -const TaskStatusPopup: FC = function FilterPopup({ visible, onDismiss, setSelectedStatus, statusName }) { +const TaskStatusPopup: FC = function FilterPopup({ + visible, + onDismiss, + setSelectedStatus, + statusName, + canCreateStatus +}) { const { allStatuses } = useTaskStatus(); - const { colors } = useAppTheme(); + const { colors, dark } = useAppTheme(); + + const [createStatusMode, setCreateStatusMode] = useState(false); + + const { createStatus, updateStatus } = useTaskStatus(); + const onStatusSelected = (status: string) => { setSelectedStatus(status); onDismiss(); @@ -80,20 +93,61 @@ const TaskStatusPopup: FC = function FilterPopup({ visible, onDismiss, se return ( - - - {translate('settingScreen.statusScreen.statuses')} - - ( - - )} - legacyImplementation={true} - showsVerticalScrollIndicator={true} - keyExtractor={(_, index) => index.toString()} - /> + + {!createStatusMode ? ( + <> + + {translate('settingScreen.statusScreen.statuses')} + + ( + + )} + legacyImplementation={true} + showsVerticalScrollIndicator={true} + keyExtractor={(_, index) => index.toString()} + /> + {canCreateStatus && ( + setCreateStatusMode(true)} + > + + + {translate('settingScreen.statusScreen.createNewStatusText')} + + + )} + + ) : ( + setCreateStatusMode(false)} + onCreateStatus={createStatus} + onUpdateStatus={updateStatus} + isEdit={false} + /> + )} ); @@ -139,6 +193,12 @@ const $modalBackGround: ViewStyle = { }; const styles = StyleSheet.create({ + btnText: { + color: '#3826A6', + fontFamily: typography.primary.semiBold, + fontSize: 16, + fontStyle: 'normal' + }, colorFrame: { alignItems: 'center', backgroundColor: '#D4EFDF', @@ -158,6 +218,18 @@ const styles = StyleSheet.create({ paddingVertical: 16, width: '90%' }, + createButton: { + alignItems: 'center', + alignSelf: 'center', + borderColor: '#3826A6', + borderRadius: 12, + borderWidth: 2, + flexDirection: 'row', + justifyContent: 'center', + marginTop: 10, + padding: 12, + width: '80%' + }, text: { fontFamily: typography.primary.medium, fontSize: 14, diff --git a/apps/mobile/app/components/TaskVersion.tsx b/apps/mobile/app/components/TaskVersion.tsx index 1499727a0..dc1425534 100644 --- a/apps/mobile/app/components/TaskVersion.tsx +++ b/apps/mobile/app/components/TaskVersion.tsx @@ -1,50 +1,50 @@ /* 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" +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 - setVersion?: (priority: string) => unknown + task?: ITeamTask; + containerStyle?: ViewStyle; + version?: string; + setVersion?: (priority: string) => unknown; + canCreateVersion?: boolean; } const TaskVersion: FC = observer( - ({ task, containerStyle, version, setVersion }) => { - const { colors } = useAppTheme() - const { updateTask } = useTeamTasks() - const [openModal, setOpenModal] = useState(false) + ({ task, containerStyle, version, setVersion, canCreateVersion }) => { + const { colors } = useAppTheme(); + const { updateTask } = useTeamTasks(); + const [openModal, setOpenModal] = useState(false); - const allTaskVersions = useTaskVersionValue() + const allTaskVersions = useTaskVersionValue(); - const versionValue = (task?.version || (version && version))?.toLowerCase() + const versionValue = (task?.version || (version && version))?.toLowerCase(); const currentVersion = - allTaskVersions && - Object.values(allTaskVersions).find((item) => item.value.toLowerCase() === versionValue) + allTaskVersions && Object.values(allTaskVersions).find((item) => item.value.toLowerCase() === versionValue); const onChangeVersion = async (text: string) => { if (task) { const taskEdit = { ...task, - version: task?.version === text ? null : text, - } + version: task?.version === text ? null : text + }; - await updateTask(taskEdit, task.id) + await updateTask(taskEdit, task.id); } else { - setVersion(text) + setVersion(text); } - } + }; return ( <> @@ -53,6 +53,7 @@ const TaskVersion: FC = observer( visible={openModal} setSelectedVersion={(e) => onChangeVersion(e.value)} onDismiss={() => setOpenModal(false)} + canCreateVersion={canCreateVersion} /> setOpenModal(true)}> = observer( ...styles.container, ...containerStyle, borderColor: colors.border, - backgroundColor: colors.background, + backgroundColor: colors.background }} > {(task?.version || version) && currentVersion ? ( @@ -70,22 +71,20 @@ const TaskVersion: FC = observer( style={{ ...styles.text, marginLeft: 10, - color: colors.primary, + color: colors.primary }} > {limitTextCharaters({ text: currentVersion.name, - numChars: 15, + numChars: 15 })} ) : ( - - {translate("taskDetailsScreen.version")} + + {translate('taskDetailsScreen.version')} )} @@ -93,32 +92,32 @@ const TaskVersion: FC = observer( - ) - }, -) + ); + } +); const styles = StyleSheet.create({ container: { - alignItems: "center", - borderColor: "rgba(0,0,0,0.16)", + alignItems: 'center', + borderColor: 'rgba(0,0,0,0.16)', borderRadius: 10, borderWidth: 1, - flexDirection: "row", - justifyContent: "space-between", + flexDirection: 'row', + justifyContent: 'space-between', minHeight: 30, minWidth: 100, - paddingHorizontal: 8, + paddingHorizontal: 8 }, text: { fontFamily: typography.fonts.PlusJakartaSans.semiBold, fontSize: 10, - textTransform: "capitalize", + textTransform: 'capitalize' }, wrapStatus: { - alignItems: "center", - flexDirection: "row", - width: "70%", - }, -}) + alignItems: 'center', + flexDirection: 'row', + width: '70%' + } +}); -export default TaskVersion +export default TaskVersion; diff --git a/apps/mobile/app/components/TaskVersionPopup.tsx b/apps/mobile/app/components/TaskVersionPopup.tsx index 3de99d19d..3c9798e26 100644 --- a/apps/mobile/app/components/TaskVersionPopup.tsx +++ b/apps/mobile/app/components/TaskVersionPopup.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-native/no-inline-styles */ /* eslint-disable react-native/no-color-literals */ -import React, { FC } from "react" +import React, { FC, useState } from 'react'; import { View, ViewStyle, @@ -10,22 +10,25 @@ import { 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" + TouchableWithoutFeedback +} from 'react-native'; +import { Feather, AntDesign, Ionicons } from '@expo/vector-icons'; +import { spacing, useAppTheme, typography } 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" +import { BlurView } from 'expo-blur'; +import { useTaskVersion } from '../services/hooks/features/useTaskVersion'; +import { BadgedTaskVersion } from './VersionIcon'; +import { ITaskVersionItemList } from '../services/interfaces/ITaskVersion'; +import { translate } from '../i18n'; +import TaskVersionForm from '../screens/Authenticated/TaskVersionScreen/components/TaskVersionForm'; export interface Props { - visible: boolean - onDismiss: () => unknown - versionName: string - setSelectedVersion: (status: ITaskVersionItemList) => unknown + visible: boolean; + onDismiss: () => unknown; + versionName: string; + setSelectedVersion: (status: ITaskVersionItemList) => unknown; + canCreateVersion?: boolean; } const TaskVersionPopup: FC = function TaskPriorityPopup({ @@ -33,52 +36,96 @@ const TaskVersionPopup: FC = function TaskPriorityPopup({ onDismiss, setSelectedVersion, versionName, + canCreateVersion }) { - const { taskVersionList } = useTaskVersion() - const { colors } = useAppTheme() + const { taskVersionList } = useTaskVersion(); + const { colors, dark } = useAppTheme(); + const { createTaskVersion, updateTaskVersion } = useTaskVersion(); + + const [createVersionMode, setCreateVersionMode] = useState(false); + const onVersionSelected = (size: ITaskPriorityItem) => { - setSelectedVersion(size) - onDismiss() - } + setSelectedVersion(size); + onDismiss(); + setCreateVersionMode(false); + }; return ( - - Versions - ( - + {!createVersionMode ? ( + <> + Versions + ( + + )} + legacyImplementation={true} + showsVerticalScrollIndicator={true} + keyExtractor={(_, index) => index.toString()} /> - )} - legacyImplementation={true} - showsVerticalScrollIndicator={true} - keyExtractor={(_, index) => index.toString()} - /> + {canCreateVersion && ( + setCreateVersionMode(true)} + > + + + {translate('settingScreen.versionScreen.createNewVersionText')} + + + )} + + ) : ( + setCreateVersionMode(false)} + onCreateVersion={createTaskVersion} + onUpdateVersion={updateTaskVersion} + isEdit={false} + createVersionModal={true} + /> + )} - ) -} + ); +}; -export default TaskVersionPopup +export default TaskVersionPopup; interface ItemProps { - currentVersionName: string - version: ITaskPriorityItem - onVersionSelected: (size: ITaskPriorityItem) => unknown + currentVersionName: string; + version: ITaskPriorityItem; + onVersionSelected: (size: ITaskPriorityItem) => unknown; } const Item: FC = ({ currentVersionName, version, onVersionSelected }) => { - const { colors } = useAppTheme() - const selected = version.value === currentVersionName + const { colors } = useAppTheme(); + const selected = version.value === currentVersionName; return ( onVersionSelected(version)}> - + @@ -90,91 +137,107 @@ const Item: FC = ({ currentVersionName, version, onVersionSelected }) - ) -} + ); +}; const ModalPopUp = ({ visible, children, onDismiss }) => { - const [showModal, setShowModal] = React.useState(visible) - const scaleValue = React.useRef(new Animated.Value(0)).current + const [showModal, setShowModal] = React.useState(visible); + const scaleValue = React.useRef(new Animated.Value(0)).current; React.useEffect(() => { - toggleModal() - }, [visible]) + toggleModal(); + }, [visible]); const toggleModal = () => { if (visible) { - setShowModal(true) + setShowModal(true); Animated.spring(scaleValue, { toValue: 1, - useNativeDriver: true, - }).start() + useNativeDriver: true + }).start(); } else { - setTimeout(() => setShowModal(false), 200) + setTimeout(() => setShowModal(false), 200); Animated.timing(scaleValue, { toValue: 0, duration: 300, - useNativeDriver: true, - }).start() + useNativeDriver: true + }).start(); } - } + }; return ( onDismiss()}> - - {children} - + {children} - ) -} + ); +}; const $modalBackGround: ViewStyle = { flex: 1, - justifyContent: "center", -} + justifyContent: 'center' +}; const styles = StyleSheet.create({ + btnText: { + color: '#3826A6', + fontFamily: typography.primary.semiBold, + fontSize: 16, + fontStyle: 'normal' + }, colorFrame: { borderRadius: 10, height: 44, - justifyContent: "center", + justifyContent: 'center', paddingLeft: 16, - width: 180, + width: 180 }, container: { - alignSelf: "center", - backgroundColor: "#fff", + alignSelf: 'center', + backgroundColor: '#fff', borderRadius: 20, maxHeight: 396, paddingHorizontal: 6, paddingVertical: 16, - width: "90%", + width: '90%' + }, + createButton: { + alignItems: 'center', + alignSelf: 'center', + borderColor: '#3826A6', + borderRadius: 12, + borderWidth: 2, + flexDirection: 'row', + justifyContent: 'center', + marginTop: 10, + padding: 12, + width: '80%' }, title: { fontSize: spacing.medium - 2, marginBottom: 16, - marginHorizontal: 10, + marginHorizontal: 10 }, wrapperItem: { - alignItems: "center", - borderColor: "rgba(0,0,0,0.13)", + alignItems: 'center', + borderColor: 'rgba(0,0,0,0.13)', borderRadius: 10, borderWidth: 1, - flexDirection: "row", - justifyContent: "space-between", + flexDirection: 'row', + justifyContent: 'space-between', marginBottom: 10, padding: 6, paddingRight: 18, - width: "100%", - }, -}) + width: '100%' + } +}); diff --git a/apps/mobile/app/screens/Authenticated/TaskVersionScreen/components/TaskVersionForm.tsx b/apps/mobile/app/screens/Authenticated/TaskVersionScreen/components/TaskVersionForm.tsx index b3fa3cf6c..591b4080f 100644 --- a/apps/mobile/app/screens/Authenticated/TaskVersionScreen/components/TaskVersionForm.tsx +++ b/apps/mobile/app/screens/Authenticated/TaskVersionScreen/components/TaskVersionForm.tsx @@ -1,14 +1,11 @@ /* eslint-disable react-native/no-color-literals */ /* eslint-disable react-native/no-inline-styles */ -import React, { useEffect, useState } from "react" -import { View, Text, TouchableOpacity, TextInput, StyleSheet, Keyboard } from "react-native" -import { translate } from "../../../../i18n" -import { typography, useAppTheme } from "../../../../theme" +import React, { useEffect, useState } from 'react'; +import { View, Text, TouchableOpacity, TextInput, StyleSheet, Keyboard } from 'react-native'; +import { translate } from '../../../../i18n'; +import { typography, useAppTheme } from '../../../../theme'; -import { - ITaskVersionCreate, - ITaskVersionItemList, -} from "../../../../services/interfaces/ITaskVersion" +import { ITaskVersionCreate, ITaskVersionItemList } from '../../../../services/interfaces/ITaskVersion'; const TaskVersionForm = ({ isEdit, @@ -16,43 +13,45 @@ const TaskVersionForm = ({ item, onCreateVersion, onUpdateVersion, + createVersionModal }: { - isEdit: boolean - onDismiss: () => unknown - item?: ITaskVersionItemList - onUpdateVersion: (id: string, data: ITaskVersionCreate) => unknown - onCreateVersion: (data: ITaskVersionCreate) => unknown + isEdit: boolean; + onDismiss: () => unknown; + item?: ITaskVersionItemList; + onUpdateVersion: (id: string, data: ITaskVersionCreate) => unknown; + onCreateVersion: (data: ITaskVersionCreate) => unknown; + createVersionModal?: boolean; }) => { - const { colors, dark } = useAppTheme() - const [versionName, setVersionName] = useState(null) + const { colors, dark } = useAppTheme(); + const [versionName, setVersionName] = useState(null); useEffect(() => { if (isEdit) { - setVersionName(item.name) + setVersionName(item.name); } else { - setVersionName(null) + setVersionName(null); } - }, [item, isEdit]) + }, [item, isEdit]); const handleSubmit = async () => { if (versionName.trim().length <= 0) { - return + return; } if (isEdit) { await onUpdateVersion(item?.id, { - name: versionName, - }) + name: versionName + }); } else { await onCreateVersion({ name: versionName, - color: "#FFFFFF", - }) + color: '#FFFFFF' + }); } - setVersionName(null) - onDismiss() - } + setVersionName(null); + onDismiss(); + }; return ( - {translate("settingScreen.versionScreen.createNewVersionText")} + {translate('settingScreen.versionScreen.createNewVersionText')} setVersionName(text)} /> - + { - onDismiss() - Keyboard.dismiss() + onDismiss(); + Keyboard.dismiss(); }} > - - {translate("settingScreen.versionScreen.cancelButtonText")} - + {translate('settingScreen.versionScreen.cancelButtonText')} { if (versionName) { - handleSubmit().finally(() => Keyboard.dismiss()) + handleSubmit().finally(() => Keyboard.dismiss()); } }} > {isEdit - ? translate("settingScreen.versionScreen.updateButtonText") - : translate("settingScreen.versionScreen.createButtonText")} + ? translate('settingScreen.versionScreen.updateButtonText') + : translate('settingScreen.versionScreen.createButtonText')} - ) -} + ); +}; -export default TaskVersionForm +export default TaskVersionForm; const styles = StyleSheet.create({ cancelBtn: { - alignItems: "center", - backgroundColor: "#E6E6E9", + alignItems: 'center', + backgroundColor: '#E6E6E9', borderRadius: 12, height: 57, - justifyContent: "center", - width: "48%", + justifyContent: 'center', + width: '48%' }, cancelTxt: { - color: "#1A1C1E", + color: '#1A1C1E', fontFamily: typography.primary.semiBold, - fontSize: 18, + fontSize: 18 }, createBtn: { - alignItems: "center", - backgroundColor: "#3826A6", + alignItems: 'center', + backgroundColor: '#3826A6', borderRadius: 12, height: 57, - justifyContent: "center", - width: "48%", + justifyContent: 'center', + width: '48%' }, createTxt: { - color: "#FFF", + color: '#FFF', fontFamily: typography.primary.semiBold, - fontSize: 18, + fontSize: 18 }, formTitle: { - color: "#1A1C1E", + color: '#1A1C1E', fontFamily: typography.primary.semiBold, - fontSize: 24, + fontSize: 24 }, versionNameInput: { - alignItems: "center", - borderColor: "#DCE4E8", + alignItems: 'center', + borderColor: '#DCE4E8', borderRadius: 12, borderWidth: 1, height: 57, marginTop: 16, paddingHorizontal: 18, - width: "100%", + width: '100%' }, wrapButtons: { - flexDirection: "row", - justifyContent: "space-between", - marginTop: 40, - width: "100%", - }, -}) + flexDirection: 'row', + justifyContent: 'space-between', + width: '100%' + } +}); diff --git a/apps/web/app/hooks/features/useKanban.ts b/apps/web/app/hooks/features/useKanban.ts new file mode 100644 index 000000000..94b0c653c --- /dev/null +++ b/apps/web/app/hooks/features/useKanban.ts @@ -0,0 +1,37 @@ +import { kanbanBoardState } from "@app/stores/kanban"; +import { useTaskStatus } from "./useTaskStatus"; +import { useRecoilState } from "recoil"; +import { useEffect, useState } from "react"; +import { ITaskStatusItemList } from "@app/interfaces"; + +export function useKanban() { + + const [loading, setLoading] = useState(true); + + const [kanbanBoard, setKanbanBoard] = useRecoilState(kanbanBoardState); + + const taskStatusHook = useTaskStatus(); + + /** + * format data for kanban board + */ + useEffect(()=> { + if(taskStatusHook.loading) { + let kanban = {}; + taskStatusHook.taskStatus.map((taskStatus: ITaskStatusItemList,)=> { + kanban = { + ...kanban, + [taskStatus.name ? taskStatus.name : ''] : [] + } + }); + setKanbanBoard(kanban) + setLoading(false) + } + },[taskStatusHook.loading]) + + return { + data: kanbanBoard, + isLoading: loading, + columns: taskStatusHook.taskStatus + } +} \ No newline at end of file diff --git a/apps/web/app/hooks/features/useTeamInvitations.ts b/apps/web/app/hooks/features/useTeamInvitations.ts index 5e1118437..6559b6159 100644 --- a/apps/web/app/hooks/features/useTeamInvitations.ts +++ b/apps/web/app/hooks/features/useTeamInvitations.ts @@ -21,8 +21,6 @@ import { useQuery } from '../useQuery'; import { useAuthenticateUser } from './useAuthenticateUser'; export function useTeamInvitations() { - const { user } = useAuthenticateUser(); - const setTeamInvitations = useSetRecoilState(teamInvitationsState); const [myInvitationsList, setMyInvitationsList] = useRecoilState(myInvitationsState); @@ -48,14 +46,24 @@ export function useTeamInvitations() { const { queryCall: acceptRejectMyInvitationsQueryCall, loading: acceptRejectMyInvitationsLoading } = useQuery(acceptRejectMyInvitationsAPI); + const { user } = useAuthenticateUser(); + const inviteUser = useCallback( (email: string, name: string) => { - return inviteQueryCall({ email, name }).then((res) => { - setTeamInvitations(res.data?.items || []); + return inviteQueryCall( + { + email, + name, + organizationId: user?.employee.organizationId as string, + teamId: activeTeamId as string + }, + user?.tenantId as string + ).then((res) => { + setTeamInvitations((prev) => [...prev, ...(res.data?.items || [])]); return res; }); }, - [inviteQueryCall, setTeamInvitations] + [inviteQueryCall, setTeamInvitations, user?.tenantId, activeTeamId, user?.employee.organizationId] ); useEffect(() => { diff --git a/apps/web/app/interfaces/IKanban.ts b/apps/web/app/interfaces/IKanban.ts new file mode 100644 index 000000000..2d17b3da5 --- /dev/null +++ b/apps/web/app/interfaces/IKanban.ts @@ -0,0 +1,5 @@ +import { ITeamTask } from "./ITask"; + +export interface IKanban { + [key: string]: ITeamTask[] +} \ No newline at end of file diff --git a/apps/web/app/services/client/api/invite.ts b/apps/web/app/services/client/api/invite.ts index 63e9ceb7c..73e46035c 100644 --- a/apps/web/app/services/client/api/invite.ts +++ b/apps/web/app/services/client/api/invite.ts @@ -1,9 +1,47 @@ import { PaginationResponse } from '@app/interfaces/IDataResponse'; -import { IInvitation, IInviteRequest, MyInvitationActionEnum, CreateResponse } from '@app/interfaces'; -import api, { get } from '../axios'; +import { IInvitation, MyInvitationActionEnum, CreateResponse, IInviteCreate, IRole } from '@app/interfaces'; +import { INVITE_CALLBACK_URL } from '@app/constants'; +import api, { get, post } from '../axios'; +import { AxiosResponse } from 'axios'; -export function inviteByEmailsAPI(data: IInviteRequest) { - return api.post>('/invite/emails', data); +interface IIInviteRequest { + email: string; + name: string; + teamId: string; + organizationId: string; +} + +export async function inviteByEmailsAPI(data: IIInviteRequest, tenantId: string) { + const endpoint = '/invite/emails'; + + const date = new Date(); + date.setDate(date.getDate() - 1); + + const getRoleEndpoint = '/roles/options?name=EMPLOYEE'; + + const employeeRole: AxiosResponse = (await get(getRoleEndpoint, true, { tenantId })).data; + + const dataToInviteUser: IInviteCreate & { tenantId: string } = { + emailIds: [data.email], + projectIds: [], + departmentIds: [], + organizationContactIds: [], + teamIds: [data.teamId], + roleId: employeeRole.data.id || '', + invitationExpirationPeriod: 'Never', + inviteType: 'TEAM', + appliedDate: null, + fullName: data.name, + callbackUrl: INVITE_CALLBACK_URL, + organizationId: data.organizationId, + tenantId, + startedWorkOn: date.toISOString() + }; + + // for not direct call we need to adjust data to include name and email only + const fetchData = await post(endpoint, dataToInviteUser, true, { tenantId }); + + return process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL ? fetchData.data : fetchData; } export async function getTeamInvitationsAPI(tenantId: string, organizationId: string, role: string, teamId: string) { diff --git a/apps/web/app/services/client/axios.ts b/apps/web/app/services/client/axios.ts index 92f3452d2..bd53becb2 100644 --- a/apps/web/app/services/client/axios.ts +++ b/apps/web/app/services/client/axios.ts @@ -87,6 +87,23 @@ function get( : api.get(endpoint); } +function post( + endpoint: string, + data: any, + isDirect: boolean, + extras?: { + tenantId: string; + } +) { + return isDirect && process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL + ? apiDirect.post(endpoint, data, { + headers: { + ...(extras?.tenantId ? { 'tenant-id': extras?.tenantId } : {}) + } + }) + : api.post(endpoint, data); +} + export default api; -export { apiDirect, get }; +export { apiDirect, get, post }; diff --git a/apps/web/app/stores/kanban.ts b/apps/web/app/stores/kanban.ts new file mode 100644 index 000000000..0badd4c91 --- /dev/null +++ b/apps/web/app/stores/kanban.ts @@ -0,0 +1,7 @@ +import { IKanban } from "@app/interfaces/IKanban"; +import { atom } from "recoil"; + +export const kanbanBoardState = atom({ + key: 'kanbanBoardState', + default: {} +}) \ No newline at end of file diff --git a/apps/web/components/pages/task/details-section/blocks/task-main-info.tsx b/apps/web/components/pages/task/details-section/blocks/task-main-info.tsx index cc08ecc02..bc86add07 100644 --- a/apps/web/components/pages/task/details-section/blocks/task-main-info.tsx +++ b/apps/web/components/pages/task/details-section/blocks/task-main-info.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-mixed-spaces-and-tabs */ import { calculateRemainingDays, formatDateString } from '@app/helpers'; import { useOrganizationTeams, useSyncRef, useTeamMemberCard, useTeamTasks } from '@app/hooks'; import { ITeamTask, OT_Member } from '@app/interfaces'; @@ -217,7 +218,10 @@ const ManageMembersPopover = (memberList: OT_Member[], task: ITeamTask | null) = const unassignedMembers = useMemo( () => memberList.filter((member) => - member.employee ? !task?.members.map((item) => item.userId).includes(member.employee.userId) : false + member.employee + ? !task?.members.map((item) => item.userId).includes(member.employee.userId) && + member.employee?.isActive + : false ), [memberList, task?.members] ); @@ -225,7 +229,10 @@ const ManageMembersPopover = (memberList: OT_Member[], task: ITeamTask | null) = const assignedTaskMembers = useMemo( () => memberList.filter((member) => - member.employee ? task?.members.map((item) => item.userId).includes(member.employee?.userId) : false + member.employee + ? task?.members.map((item) => item.userId).includes(member.employee?.userId) && + member.employee?.isActive + : false ), [memberList, task?.members] ); diff --git a/apps/web/lib/components/Kanban.tsx b/apps/web/lib/components/Kanban.tsx index b6b0a42d9..37d11a5d7 100644 --- a/apps/web/lib/components/Kanban.tsx +++ b/apps/web/lib/components/Kanban.tsx @@ -206,7 +206,7 @@ export const EmptyKanbanDroppable = ({index,title, items}: { { title.length > 0 ? <>
@@ -220,24 +220,28 @@ export const EmptyKanbanDroppable = ({index,title, items}: {
-
- {items.length} -
-
-

- {title} -

+
+
+ {items.length} +
+
+

+ + {title} + +

+
diff --git a/apps/web/lib/features/team-members-kanban-view.tsx b/apps/web/lib/features/team-members-kanban-view.tsx index cad9ba0b9..9b116c7c3 100644 --- a/apps/web/lib/features/team-members-kanban-view.tsx +++ b/apps/web/lib/features/team-members-kanban-view.tsx @@ -1,7 +1,7 @@ +import { useKanban } from "@app/hooks/features/useKanban"; import { clsxm } from "@app/utils"; import KanbanDraggable, { EmptyKanbanDroppable } from "lib/components/Kanban" import { AddIcon } from "lib/components/svgs"; -import { state } from "pages/kanban"; import React from "react"; import { useEffect, useState } from "react"; import { DragDropContext, DropResult, Droppable, DroppableProvided, DroppableStateSnapshot } from "react-beautiful-dnd"; @@ -53,19 +53,22 @@ const reorderItemMap = ({ itemMap, source, destination }: { }; }; -const getHeaderBackground = (column: any) => { - const selectState = state.filter((item: any)=> { +const getHeaderBackground = (columns: any, column: any) => { + const selectState = columns.filter((item: any)=> { return item.name === column.toUpperCase() }); - return selectState[0].backgroundColor + return selectState[0].color } export const KanbanView = ({ itemsArray }: { itemsArray: any}) => { - const [items, setItems] = useState(itemsArray); + const { columns:kanbanColumns } = useKanban(); + const [items, setItems] = useState(itemsArray); + const [columns, setColumn] = useState(Object.keys(itemsArray)); + /** * This function handles all drag and drop logic * on the kanban board. @@ -156,7 +159,7 @@ export const KanbanView = ({ itemsArray }: { itemsArray: any}) => { > {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => (
@@ -172,7 +175,7 @@ export const KanbanView = ({ itemsArray }: { itemsArray: any}) => { index={index} title={column} items={items[column]} - backgroundColor={getHeaderBackground(column)} + backgroundColor={getHeaderBackground(kanbanColumns, column)} />
diff --git a/apps/web/lib/features/team-members.tsx b/apps/web/lib/features/team-members.tsx index 5647308ff..eda098ff0 100644 --- a/apps/web/lib/features/team-members.tsx +++ b/apps/web/lib/features/team-members.tsx @@ -18,8 +18,8 @@ type TeamMembersProps = { export function TeamMembers({ publicTeam = false, kanbanView: view = IssuesView.CARDS }: TeamMembersProps) { const { user } = useAuthenticateUser(); const activeFilter = useRecoilValue(taskBlockFilterState); - const { activeTeam } = useOrganizationTeams(); - const { teamsFetching } = useOrganizationTeams(); + const { activeTeam, teamsFetching } = useOrganizationTeams(); + const members = activeFilter == 'all' ? activeTeam?.members || [] diff --git a/apps/web/pages/kanban/index.tsx b/apps/web/pages/kanban/index.tsx index 128eb1f55..589389ca3 100644 --- a/apps/web/pages/kanban/index.tsx +++ b/apps/web/pages/kanban/index.tsx @@ -1,173 +1,17 @@ +import { useKanban } from "@app/hooks/features/useKanban"; import { withAuthentication } from "lib/app/authenticator"; import { KanbanView } from "lib/features/team-members-kanban-view" import { MainLayout } from "lib/layout"; -import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; - -const todo = { - id: 'status-1', - name: 'TODO', - backgroundColor: '#8154BA', -} - -const ongoing = { - id: 'status-2', - name: 'ONGOING', - backgroundColor: '#D7EBDF', -} - -const review = { - id: 'status-3', - name: 'REVIEW', - backgroundColor: '#EAD2D5', -} - -export const state = [ todo, ongoing, review] - -const demoData = { - todo: [ - { - id: '1', - content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', - hasComment: "tagged", - tags: [ - { - id: 'tag-1', - title: 'User Profile', - backgroundColor: '#8154BA', - color: '#fff' - }, - { - id: 'tag-2', - title: 'BackEnd', - backgroundColor: '#EAD2D5', - color: '#DD2F44' - }, - ], - status: todo - }, - { - id: '4', - content: 'demo content2', - hasComment: "none", - tags: [ - { - id: 'tag-1', - title: 'User Profile', - backgroundColor: '#D7EBDF', - color: '#3D9360' - }, - { - id: 'tag-2', - title: 'BackEnd', - backgroundColor: '#EAD9EE', - color: '#9641AB' - }, - ], - status: todo - } - ], - ongoing: [ - { - id: '2', - content: 'another content', - hasComment: "untagged", - tags: [ - { - id: 'tag-1', - title: 'User Profile', - backgroundColor: '#EAD9EE', - color: '#9641AB' - }, - { - id: 'tag-2', - title: 'BackEnd', - backgroundColor: '#EAD2D5', - color: '#DD2F44' - }, - ], - status: ongoing - }, - { - id: '5', - content: 'another content2', - hasComment: "none", - tags: [ - { - id: 'tag-1', - title: 'User Profile', - backgroundColor: '#8154BA', - color: '#fff' - }, - { - id: 'tag-2', - title: 'BackEnd', - backgroundColor: '#D7EBDF', - color: '#3D9360' - }, - ], - status: ongoing - } - ], - review: [ - { - id: '3', - content: 'a simple tes', - hasComment: "none", - tags: [ - { - id: 'tag-1', - title: 'User Profile', - backgroundColor: '#D7EBDF', - color: '#3D9360' - }, - { - id: 'tag-2', - title: 'BackEnd', - backgroundColor: '#D7EBDF', - color: '#3D9360' - }, - ], - status: review - }, - { - id: '6', - content: 'a simple tes', - hasComment: "none", - tags: [ - { - id: 'tag-1', - title: 'User Profile', - backgroundColor: '#D7EBDF', - color: '#3D9360' - }, - { - id: 'tag-2', - title: 'BackEnd', - backgroundColor: '#D7EBDF', - color: '#3D9360' - }, - ], - status: review - } - ]} const Kanban= () => { - const router = useRouter() - const [winReady, setwinReady] = useState(false); - - useEffect(() => { - - setwinReady(true); - - - }, [router.isReady]); - + + const { data } = useKanban(); + return ( <> - {winReady ? - + {Object.keys(data).length > 0 ? + : null } diff --git a/northflank-template.json b/northflank-template.json new file mode 100644 index 000000000..64de68c2d --- /dev/null +++ b/northflank-template.json @@ -0,0 +1,59 @@ +{ + "apiVersion": "v1", + "spec": { + "kind": "Workflow", + "spec": { + "type": "sequential", + "steps": [ + { + "kind": "DeploymentService", + "spec": { + "deployment": { + "instances": 1, + "docker": { + "configType": "default" + }, + "external": { + "imagePath": "ghcr.io/ever-co/ever-teams-webapp:latest" + } + }, + "runtimeEnvironment": {}, + "runtimeFiles": {}, + "billing": { + "deploymentPlan": "nf-compute-10" + }, + "name": "ever-teams-web", + "ports": [ + { + "internalPort": 3000, + "protocol": "HTTP", + "public": true, + "name": "p01", + "domains": [], + "security": { + "policies": [], + "credentials": [] + }, + "disableNfDomain": false + } + ] + } + } + ] + } + }, + "name": "ever-teams-template", + "description": "Open Work and Project Management Platform.", + "project": { + "spec": { + "name": "Ever Teams Template", + "region": "europe-west", + "description": "Open Work and Project Management Platform.", + "color": "#3826A6", + "networking": { + "allowedIngressProjects": [] + } + } + }, + "$schema": "https://api.northflank.com/v1/schemas/template" +} \ No newline at end of file