diff --git a/.cspell.json b/.cspell.json index 42b1aa603..c574fe37b 100644 --- a/.cspell.json +++ b/.cspell.json @@ -15,6 +15,7 @@ "Chatwoot", "cloc", "cloudinary", + "clsx", "clsxm", "commitlint", "compodoc", @@ -30,21 +31,31 @@ "heroicons", "Huhn", "icnsutils", + "isdragging", + "isdraggingfrom", + "isdropdisabled", "JITSU", "kanban", "libappindicator", + "lucide", "mathieudutour", "ncipollo", + "nextjs", "passcode", "plasmo", "precommit", "RECAPTCHA", "setuptools", + "setwin", "snyk", "stylelint", "svgs", + "Tailess", + "tailess", "tailwindcss", + "testid", "Timesheet", + "tanstack", "vcpu", "Vercel", "HUBSTAFF", diff --git a/apps/mobile/app/components/CodeField.tsx b/apps/mobile/app/components/CodeField.tsx index f6a8bb23c..d67f9b811 100644 --- a/apps/mobile/app/components/CodeField.tsx +++ b/apps/mobile/app/components/CodeField.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-native/no-inline-styles */ /* eslint-disable react-native/no-color-literals */ import React, { FC, useEffect, useState } from 'react'; -import { View, StyleSheet, Text } from 'react-native'; +import { View, StyleSheet, Text, Dimensions, PixelRatio, Platform } from 'react-native'; import { colors, typography, useAppTheme } from '../theme'; import { CodeField, Cursor, useBlurOnFulfill, useClearByFocusCell } from 'react-native-confirmation-code-field'; @@ -12,6 +12,9 @@ interface ICodeField { defaultValue?: string; } +const { height: screeHeight } = Dimensions.get('screen'); +const screenDimension = PixelRatio.get(); + export const CodeInputField: FC = (props) => { const { onChange, editable, length = 6 } = props; const { colors } = useAppTheme(); @@ -54,7 +57,8 @@ export const CodeInputField: FC = (props) => { flexDirection: 'column', justifyContent: 'center', alignItems: 'center', - lineHeight: 52 + lineHeight: + Platform.OS === 'ios' ? screeHeight * 0.055 * (screenDimension / 3) : undefined } ]} onLayout={getCellOnLayoutHandler(index)} diff --git a/apps/mobile/app/components/svgs/icons.tsx b/apps/mobile/app/components/svgs/icons.tsx index 5f6841602..453537650 100644 --- a/apps/mobile/app/components/svgs/icons.tsx +++ b/apps/mobile/app/components/svgs/icons.tsx @@ -650,6 +650,28 @@ export const pauseStatusIconLarge = ` `; +// Profile Screen // + +export const filterLightIcon = ` + + + + + + + +`; + +export const filterDarkIcon = ` + + + + + + + +`; + // Task Screen // // Task Title export const tickIconLight = ` `; + +// Settings Screen Icons // + +export const danGerZoneRemoveUserIcon = ` + + + + + + +`; + +export const galleryLightIcon = ` + + + +`; + +export const galleryDarkIcon = ` + + + +`; diff --git a/apps/mobile/app/screens/Authenticated/ProfileScreen/components/ListCardItem.tsx b/apps/mobile/app/screens/Authenticated/ProfileScreen/components/ListCardItem.tsx index 446b8a2a2..226e6cecd 100644 --- a/apps/mobile/app/screens/Authenticated/ProfileScreen/components/ListCardItem.tsx +++ b/apps/mobile/app/screens/Authenticated/ProfileScreen/components/ListCardItem.tsx @@ -110,8 +110,14 @@ export const ListItemContent: React.FC = observer((props) => { - navigateToTask(props.task?.id)}> - + navigateToTask(props.task?.id)} style={{ width: '80%' }}> + @@ -309,7 +315,7 @@ export default ListCardItem; const styles = StyleSheet.create({ cardContainer: { borderRadius: 14, - elevation: 24, + elevation: 5, shadowColor: '#000', shadowOffset: { width: 0, height: 12 }, shadowOpacity: 0.05, diff --git a/apps/mobile/app/screens/Authenticated/ProfileScreen/components/TaskFilter.tsx b/apps/mobile/app/screens/Authenticated/ProfileScreen/components/TaskFilter.tsx index 6d383f85d..92e10947a 100644 --- a/apps/mobile/app/screens/Authenticated/ProfileScreen/components/TaskFilter.tsx +++ b/apps/mobile/app/screens/Authenticated/ProfileScreen/components/TaskFilter.tsx @@ -1,5 +1,5 @@ /* eslint-disable react-native/no-inline-styles */ -import { Text, TouchableOpacity, Image, View, TextStyle, ViewStyle, Dimensions } from 'react-native'; +import { Text, TouchableOpacity, View, TextStyle, ViewStyle, Dimensions } from 'react-native'; import React, { useState } from 'react'; import { typography, useAppTheme } from '../../../../theme'; import { translate } from '../../../../i18n'; @@ -8,6 +8,8 @@ import ProfileTabs from './ProfileTabs'; import AssignTaskFormModal from './AssignTaskSection'; import FilterPopup from './FilterPopup'; import { ITaskFilter } from '../../../../services/hooks/features/useTaskFilters'; +import { SvgXml } from 'react-native-svg'; +import { filterDarkIcon, filterLightIcon } from '../../../../components/svgs/icons'; const TaskFilter = ({ profile, hook }: { profile: IUserProfile; hook: ITaskFilter }) => { const { colors, dark } = useAppTheme(); @@ -47,11 +49,7 @@ const TaskFilter = ({ profile, hook }: { profile: IUserProfile; hook: ITaskFilte style={{ ...$filterButton, borderColor: colors.border }} onPress={() => setShowFilterPopup(true)} > - {dark ? ( - - ) : ( - - )} + {dark ? : } {translate('tasksScreen.filter')} diff --git a/apps/mobile/app/screens/Authenticated/ProfileScreen/components/TaskLabelFilter.tsx b/apps/mobile/app/screens/Authenticated/ProfileScreen/components/TaskLabelFilter.tsx index eb6ba5c96..21120cda0 100644 --- a/apps/mobile/app/screens/Authenticated/ProfileScreen/components/TaskLabelFilter.tsx +++ b/apps/mobile/app/screens/Authenticated/ProfileScreen/components/TaskLabelFilter.tsx @@ -24,6 +24,7 @@ import { useTaskLabels } from '../../../../services/hooks/features/useTaskLabels import { ITaskLabelItem } from '../../../../services/interfaces/ITaskLabel'; import { StatusType } from './FilterPopup'; import { BlurView } from 'expo-blur'; +import { translate } from '../../../../i18n'; interface TaskLabelFilterProps { showLabelPopup: boolean; @@ -48,7 +49,9 @@ const TaskStatusFilter: FC = observer( setShowLabelPopup(!showLabelPopup)}> - Labels + + {translate('settingScreen.labelScreen.labels')} + {selectedLabels.length === 0 ? null : ( )} @@ -92,7 +95,9 @@ const TaskStatusFilterDropDown: FC = observer( ]} > - Statuses + + {translate('settingScreen.labelScreen.labels')} + = observer( setShowPriorityPopup(!showPriorityPopup)}> - Priorities + + {translate('settingScreen.priorityScreen.priorities')} + {selectedPriorities.length === 0 ? null : ( )} @@ -93,7 +96,9 @@ const TaskStatusFilterDropDown: FC = observer( ]} > - Statuses + + {translate('settingScreen.priorityScreen.priorities')} + = observer( setShowSizePopup(!showSizePopup)}> - Sizes + + {translate('settingScreen.sizeScreen.sizes')} + {selectedSizes.length === 0 ? null : ( )} @@ -92,7 +95,9 @@ const TaskStatusFilterDropDown: FC = observer( ]} > - Statuses + + {translate('settingScreen.sizeScreen.sizes')} + void; } +const { width: screenWidth } = Dimensions.get('screen'); + const TaskTitleDisplay: FC = ({ editMode, setEditMode, task, navigateToTask }) => { const { colors } = useAppTheme(); const { updateTask } = useTeamTasks(); @@ -65,7 +67,11 @@ const TaskTitleDisplay: FC = ({ editMode, setEditMode, task, navigateToTa #{task.number} - + {limitTextCharaters({ text: task.title, numChars: 43 @@ -78,7 +84,7 @@ const TaskTitleDisplay: FC = ({ editMode, setEditMode, task, navigateToTa }; const styles = StyleSheet.create({ - container: {}, + container: { width: '90%' }, titleContainer: { flexDirection: 'row', width: '100%' @@ -93,8 +99,7 @@ const styles = StyleSheet.create({ totalTimeTitle: { color: '#7E7991', fontFamily: typography.secondary.medium, - fontSize: 14, - maxWidth: '78%' + fontSize: screenWidth * 0.0327 }, wrapTitleInput: { alignItems: 'center', diff --git a/apps/mobile/app/screens/Authenticated/SettingScreen/Team/QuitTeam.tsx b/apps/mobile/app/screens/Authenticated/SettingScreen/Team/QuitTeam.tsx index bea60c733..ae48c404a 100644 --- a/apps/mobile/app/screens/Authenticated/SettingScreen/Team/QuitTeam.tsx +++ b/apps/mobile/app/screens/Authenticated/SettingScreen/Team/QuitTeam.tsx @@ -1,12 +1,14 @@ /* eslint-disable react-native/no-inline-styles */ /* eslint-disable react-native/no-color-literals */ import React from 'react'; -import { View, Text, Image, TouchableOpacity, StyleSheet, TouchableWithoutFeedback } from 'react-native'; +import { View, Text, TouchableOpacity, StyleSheet, TouchableWithoutFeedback } from 'react-native'; import { useOrganizationTeam } from '../../../../services/hooks/useOrganization'; import { translate } from '../../../../i18n'; import { typography, useAppTheme } from '../../../../theme'; import { observer } from 'mobx-react-lite'; import useAuthenticateUser from '../../../../services/hooks/features/useAuthentificateUser'; +import { SvgXml } from 'react-native-svg'; +import { danGerZoneRemoveUserIcon } from '../../../../components/svgs/icons'; const QuitTheTeam = observer(({ onDismiss }: { onDismiss: () => unknown }) => { const { colors, dark } = useAppTheme(); @@ -23,8 +25,8 @@ const QuitTheTeam = observer(({ onDismiss }: { onDismiss: () => unknown }) => { onDismiss()}> - - + + diff --git a/apps/mobile/app/screens/Authenticated/SettingScreen/Team/RemoveTeam.tsx b/apps/mobile/app/screens/Authenticated/SettingScreen/Team/RemoveTeam.tsx index 77774601a..551b46e33 100644 --- a/apps/mobile/app/screens/Authenticated/SettingScreen/Team/RemoveTeam.tsx +++ b/apps/mobile/app/screens/Authenticated/SettingScreen/Team/RemoveTeam.tsx @@ -1,12 +1,14 @@ /* eslint-disable react-native/no-color-literals */ /* eslint-disable react-native/no-inline-styles */ import React from 'react'; -import { View, Text, Image, TouchableOpacity, StyleSheet, TouchableWithoutFeedback } from 'react-native'; +import { View, Text, TouchableOpacity, StyleSheet, TouchableWithoutFeedback } from 'react-native'; import { useOrganizationTeam } from '../../../../services/hooks/useOrganization'; import { translate } from '../../../../i18n'; import { typography, useAppTheme } from '../../../../theme'; import { useStores } from '../../../../models'; import { observer } from 'mobx-react-lite'; +import { SvgXml } from 'react-native-svg'; +import { danGerZoneRemoveUserIcon } from '../../../../components/svgs/icons'; const RemoveTeam = observer(({ onDismiss }: { onDismiss: () => unknown }) => { const { colors, dark } = useAppTheme(); @@ -29,8 +31,8 @@ const RemoveTeam = observer(({ onDismiss }: { onDismiss: () => unknown }) => { onDismiss()}> - - + + diff --git a/apps/mobile/app/screens/Authenticated/SettingScreen/components/ChangeTeamLogo.tsx b/apps/mobile/app/screens/Authenticated/SettingScreen/components/ChangeTeamLogo.tsx index 89202877b..2bb5e8be6 100644 --- a/apps/mobile/app/screens/Authenticated/SettingScreen/components/ChangeTeamLogo.tsx +++ b/apps/mobile/app/screens/Authenticated/SettingScreen/components/ChangeTeamLogo.tsx @@ -25,6 +25,8 @@ import { useOrganizationTeam } from '../../../../services/hooks/useOrganization' import { useStores } from '../../../../models'; import { observer } from 'mobx-react-lite'; import LoadingModal from '../../../../components/LoadingModal'; +import { SvgXml } from 'react-native-svg'; +import { galleryDarkIcon, galleryLightIcon } from '../../../../components/svgs/icons'; const ChangeTeamLogo = observer(({ onDismiss, onExtend }: { onDismiss: () => unknown; onExtend: () => unknown }) => { const { colors, dark } = useAppTheme(); @@ -109,9 +111,7 @@ const ChangeTeamLogo = observer(({ onDismiss, onExtend }: { onDismiss: () => unk setSelectedImage(null); }, [onDismiss]); - const image = dark - ? require('../../../../../assets/images/new/image-dark.png') - : require('../../../../../assets/images/new/image-light.png'); + const galleryImage = dark ? : ; return ( <> @@ -132,7 +132,7 @@ const ChangeTeamLogo = observer(({ onDismiss, onExtend }: { onDismiss: () => unk pickImageFromGalery()}> - + {galleryImage} {translate('settingScreen.changeAvatar.selectFromGalery')} diff --git a/apps/mobile/app/screens/Authenticated/SettingScreen/components/ChangeUserAvatar.tsx b/apps/mobile/app/screens/Authenticated/SettingScreen/components/ChangeUserAvatar.tsx index 95ad33707..a16992a10 100644 --- a/apps/mobile/app/screens/Authenticated/SettingScreen/components/ChangeUserAvatar.tsx +++ b/apps/mobile/app/screens/Authenticated/SettingScreen/components/ChangeUserAvatar.tsx @@ -26,6 +26,8 @@ import mime from 'mime'; import LoadingModal from '../../../../components/LoadingModal'; import useAuthenticateUser from '../../../../services/hooks/features/useAuthentificateUser'; import { IUser } from '../../../../services/interfaces/IUserData'; +import { SvgXml } from 'react-native-svg'; +import { galleryDarkIcon, galleryLightIcon } from '../../../../components/svgs/icons'; interface IFileInfo { size: number; @@ -137,9 +139,7 @@ const ChangeUserAvatar = ({ onDismiss, onExtend }: { onDismiss: () => unknown; o setSelectedImage(null); }, [onDismiss]); - const image = dark - ? require('../../../../../assets/images/new/image-dark.png') - : require('../../../../../assets/images/new/image-light.png'); + const galleryImage = dark ? : ; return ( <> @@ -160,7 +160,7 @@ const ChangeUserAvatar = ({ onDismiss, onExtend }: { onDismiss: () => unknown; o pickImageFromGalery()}> - + {galleryImage} {translate('settingScreen.changeAvatar.selectFromGalery')} diff --git a/apps/mobile/app/screens/Authenticated/SettingScreen/components/UserRemoveAccount.tsx b/apps/mobile/app/screens/Authenticated/SettingScreen/components/UserRemoveAccount.tsx index 4b34ed9cd..a2f438888 100644 --- a/apps/mobile/app/screens/Authenticated/SettingScreen/components/UserRemoveAccount.tsx +++ b/apps/mobile/app/screens/Authenticated/SettingScreen/components/UserRemoveAccount.tsx @@ -1,11 +1,13 @@ /* eslint-disable react-native/no-color-literals */ /* eslint-disable react-native/no-inline-styles */ import React, { useCallback } from 'react'; -import { Image, StyleSheet, Text, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native'; +import { StyleSheet, Text, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native'; import { translate } from '../../../../i18n'; import { useUser } from '../../../../services/hooks/features/useUser'; import { useOrganizationTeam } from '../../../../services/hooks/useOrganization'; import { typography, useAppTheme } from '../../../../theme'; +import { SvgXml } from 'react-native-svg'; +import { danGerZoneRemoveUserIcon } from '../../../../components/svgs/icons'; const UserRemoveAccount = ({ onDismiss, @@ -36,8 +38,8 @@ const UserRemoveAccount = ({ onDismiss()}> - - + + diff --git a/apps/mobile/app/screens/Authenticated/TeamScreen/components/ListCardItem.tsx b/apps/mobile/app/screens/Authenticated/TeamScreen/components/ListCardItem.tsx index fd11307d5..55f90ce24 100644 --- a/apps/mobile/app/screens/Authenticated/TeamScreen/components/ListCardItem.tsx +++ b/apps/mobile/app/screens/Authenticated/TeamScreen/components/ListCardItem.tsx @@ -2,7 +2,7 @@ /* eslint-disable react-native/no-color-literals */ /* eslint-disable react-native/no-inline-styles */ import React, { useState } from 'react'; -import { View, ViewStyle, TouchableOpacity, StyleSheet, TouchableWithoutFeedback } from 'react-native'; +import { View, ViewStyle, TouchableOpacity, StyleSheet, TouchableWithoutFeedback, Platform } from 'react-native'; import { Ionicons, Entypo } from '@expo/vector-icons'; // COMPONENTS @@ -32,6 +32,7 @@ import { translate } from '../../../../i18n'; import { useTimer } from '../../../../services/hooks/useTimer'; import { SettingScreenNavigationProp } from '../../../../navigators/AuthenticatedNavigator'; import { getTimerStatusValue } from '../../../../helpers/get-timer-status'; +import { useClickOutside } from 'react-native-click-outside'; export type ListItemProps = { member: OT_Member; @@ -54,6 +55,7 @@ export interface Props extends ListItemProps { export const ListItemContent: React.FC = observer(({ memberInfo, taskEdition, onPressIn }) => { // HOOKS const { colors, dark } = useAppTheme(); + const clickOutsideTaskEstimationInputRef = useClickOutside(() => taskEdition.setEstimateEditMode(false)); return ( = observer(({ memberInfo, - { + if (Platform.OS === 'android' && taskEdition.estimateEditMode) { + event.stopPropagation(); + taskEdition.setEstimateEditMode(false); + } }} > - - - + + + + - - - + + + - {memberInfo.memberTask && taskEdition.estimateEditMode ? ( - - + + + ) : ( + taskEdition.setEstimateEditMode(true)} /> - - ) : ( - taskEdition.setEstimateEditMode(true)} - /> - )} - + )} + + @@ -201,7 +213,7 @@ const ListCardItem: React.FC = observer((props) => { ...(props.index !== props.openMenuIndex ? { display: 'none' } : {}) }} > - + {(memberInfo.isAuthTeamManager || memberInfo.isAuthUser) && taskEdition.task && ( = observer((props) => { > - ) : ( + ) : memberInfo.isAuthTeamManager || memberInfo.isAuthUser ? ( props.setOpenMenuIndex(props.openMenuIndex === props.index ? null : props.index) @@ -310,7 +322,7 @@ const ListCardItem: React.FC = observer((props) => { )} - )} + ) : null} } @@ -348,12 +360,11 @@ const styles = StyleSheet.create({ color: '#282048', fontFamily: typography.primary.semiBold, fontSize: 14, - height: 38, + height: 36, width: '100%' }, estimate: { alignItems: 'center', - backgroundColor: '#E8EBF8', borderRadius: 5, flexDirection: 'row', justifyContent: 'space-between', diff --git a/apps/mobile/app/screens/Authenticated/TeamScreen/components/TaskInfo.tsx b/apps/mobile/app/screens/Authenticated/TeamScreen/components/TaskInfo.tsx index cac6b7229..668cd282c 100644 --- a/apps/mobile/app/screens/Authenticated/TeamScreen/components/TaskInfo.tsx +++ b/apps/mobile/app/screens/Authenticated/TeamScreen/components/TaskInfo.tsx @@ -2,7 +2,7 @@ /* eslint-disable react-native/no-inline-styles */ /* eslint-disable react-native/no-color-literals */ import React, { useCallback, useState } from 'react'; -import { ActivityIndicator, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { ActivityIndicator, StyleSheet, Text, TextInput, TouchableOpacity, View, Dimensions } from 'react-native'; import { limitTextCharaters } from '../../../../helpers/sub-text'; import { typography, useAppTheme } from '../../../../theme'; import { useTeamTasks } from '../../../../services/hooks/features/useTeamTasks'; @@ -10,6 +10,7 @@ import { I_TeamMemberCardHook } from '../../../../services/hooks/features/useTea import { Feather } from '@expo/vector-icons'; import IssuesModal from '../../../../components/IssuesModal'; +const { width: screenWidth } = Dimensions.get('screen'); const TaskInfo = ({ memberInfo, editMode, @@ -65,12 +66,12 @@ const TaskInfo = ({ return ( setEditMode(true)} onPress={onPressIn}> - + {task ? ( - + #{task?.taskNumber}{' '} {limitTextCharaters({ text: task.title, @@ -87,20 +88,21 @@ export default TaskInfo; const styles = StyleSheet.create({ inputTitle: { + fontSize: screenWidth * 0.0327, minHeight: 30, width: '80%' }, otherText: { color: '#282048', fontFamily: typography.fonts.PlusJakartaSans.semiBold, - fontSize: 14, + fontSize: screenWidth * 0.0327, fontStyle: 'normal', width: '95%' }, taskNumberStyle: { color: '#7B8089', fontFamily: typography.primary.semiBold, - fontSize: 14 + fontSize: screenWidth * 0.0327 }, wrapBugIcon: { alignItems: 'center', diff --git a/apps/mobile/app/screens/Authenticated/TimerScreen/components/EstimateTime.tsx b/apps/mobile/app/screens/Authenticated/TimerScreen/components/EstimateTime.tsx index 659d9c4ef..69faf6040 100644 --- a/apps/mobile/app/screens/Authenticated/TimerScreen/components/EstimateTime.tsx +++ b/apps/mobile/app/screens/Authenticated/TimerScreen/components/EstimateTime.tsx @@ -174,15 +174,7 @@ const EstimateTime: FC = ({ setEditEstimate, currentTask, setEstimateTime {' m'} - {showCheckIcon && ( - handleSubmit()} - /> - )} + {showCheckIcon && handleSubmit()} />} {isLoading ? : null} ); @@ -197,8 +189,8 @@ const styles = StyleSheet.create({ borderRadius: 8, flexDirection: 'row', justifyContent: 'space-between', - marginLeft: 'auto', - marginRight: 10 + marginLeft: 'auto' + // marginRight: 10 }, estimateInput: { fontFamily: typography.fonts.PlusJakartaSans.semiBold, @@ -207,16 +199,11 @@ const styles = StyleSheet.create({ textAlign: 'center' }, loading: { - position: 'absolute', - right: -18 + marginLeft: 2 }, suffix: { fontFamily: typography.fonts.PlusJakartaSans.semiBold, fontSize: 14 }, - thickIconStyle: { - position: 'absolute', - right: -26 - }, wrapDash: { flexDirection: 'row', justifyContent: 'space-between', paddingLeft: 2 } }); diff --git a/apps/mobile/app/services/client/fetch.ts b/apps/mobile/app/services/client/fetch.ts index b701439b9..a9e5ca351 100644 --- a/apps/mobile/app/services/client/fetch.ts +++ b/apps/mobile/app/services/client/fetch.ts @@ -1,5 +1,4 @@ import Config from '../../config'; -import { getToken } from '../api/tokenHandler'; export function serverFetch({ path, @@ -22,7 +21,7 @@ export function serverFetch({ }; if (bearer_token) { - headers['authorization'] = `Bearer ${bearer_token}`; + headers.authorization = `Bearer ${bearer_token}`; } if (tenantId) { @@ -31,7 +30,7 @@ export function serverFetch({ const datas: { body?: string } = {}; if (body) { - datas['body'] = JSON.stringify(body); + datas.body = JSON.stringify(body); } return fetch((Config.API_URL || '') + path, { diff --git a/apps/web/.env b/apps/web/.env index b78e951dc..409ebce1b 100644 --- a/apps/web/.env +++ b/apps/web/.env @@ -1,7 +1,7 @@ RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED=false -NEXT_PUBLIC_GAUZY_API_SERVER_URL=https://api.gauzy.co GAUZY_API_SERVER_URL=https://api.gauzy.co/api +NEXT_PUBLIC_GAUZY_API_SERVER_URL=https://api.gauzy.co NEXT_PUBLIC_GA_MEASUREMENT_ID= # CAPTCHA Settings diff --git a/apps/web/.env.sample b/apps/web/.env.sample index 2cb957307..61cbba1e7 100644 --- a/apps/web/.env.sample +++ b/apps/web/.env.sample @@ -1,7 +1,7 @@ RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED=false -NEXT_PUBLIC_GAUZY_API_SERVER_URL=https://api.gauzy.co GAUZY_API_SERVER_URL=https://api.gauzy.co/api +NEXT_PUBLIC_GAUZY_API_SERVER_URL=https://api.gauzy.co NEXT_PUBLIC_GA_MEASUREMENT_ID= # CAPTCHA Settings diff --git a/apps/web/app/hooks/features/useAuthenticateUser.ts b/apps/web/app/hooks/features/useAuthenticateUser.ts index af6e7d305..974b13fca 100644 --- a/apps/web/app/hooks/features/useAuthenticateUser.ts +++ b/apps/web/app/hooks/features/useAuthenticateUser.ts @@ -16,13 +16,20 @@ export const useAuthenticateUser = (defaultUser?: IUser) => { const { isTeamManager } = useIsMemberManager(user); - const { queryCall: refreshUserQueryCall, loading: refreshUserLoading } = useQuery(getAuthenticatedUserDataAPI); + const { + queryCall: refreshUserQueryCall, + loading: refreshUserLoading, + loadingRef: refreshUserLoadingRef + } = useQuery(getAuthenticatedUserDataAPI); const updateUserFromAPI = useCallback(() => { + if (refreshUserLoadingRef.current) { + return; + } refreshUserQueryCall().then((res) => { setUser(res.data.user); }); - }, [refreshUserQueryCall, setUser]); + }, [refreshUserQueryCall, setUser, refreshUserLoadingRef]); $user.current = useMemo(() => { return user || $user.current; diff --git a/apps/web/app/hooks/features/useOrganizationTeams.ts b/apps/web/app/hooks/features/useOrganizationTeams.ts index 74c46fff2..db57f7567 100644 --- a/apps/web/app/hooks/features/useOrganizationTeams.ts +++ b/apps/web/app/hooks/features/useOrganizationTeams.ts @@ -158,8 +158,12 @@ function useUpdateOrganizationTeam() { * It returns an object with all the data and functions needed to manage the teams in the organization */ export function useOrganizationTeams() { - const { loading, queryCall } = useQuery(getOrganizationTeamsAPI); - const { loading: loadingTeam, queryCall: queryCallTeam } = useQuery(getOrganizationTeamAPI); + const { loading, queryCall, loadingRef } = useQuery(getOrganizationTeamsAPI); + const { + loading: loadingTeam, + queryCall: queryCallTeam, + loadingRef: loadingRefTeam + } = useQuery(getOrganizationTeamAPI); const { teams, setTeams, setTeamsUpdate, teamsRef } = useTeamsState(); const activeTeam = useRecoilValue(activeTeamState); @@ -222,6 +226,10 @@ export function useOrganizationTeams() { ); const loadTeamsData = useCallback(() => { + if (loadingRef.current || loadingRefTeam.current) { + return; + } + let teamId = getActiveTeamIdCookie(); setActiveTeamId(teamId); @@ -273,6 +281,7 @@ export function useOrganizationTeams() { } } }); + return res; }); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/apps/web/app/hooks/features/useTaskLabels.ts b/apps/web/app/hooks/features/useTaskLabels.ts index ff35a6318..0e0be0685 100644 --- a/apps/web/app/hooks/features/useTaskLabels.ts +++ b/apps/web/app/hooks/features/useTaskLabels.ts @@ -17,7 +17,7 @@ export function useTaskLabels() { const [user] = useRecoilState(userState); const activeTeamId = useRecoilValue(activeTeamIdState); - const { loading, queryCall } = useQuery(getTaskLabelsList); + const { loading, queryCall, loadingRef } = useQuery(getTaskLabelsList); const { loading: createTaskLabelsLoading, queryCall: createQueryCall } = useQuery(createTaskLabelsAPI); const { loading: deleteTaskLabelsLoading, queryCall: deleteQueryCall } = useQuery(deleteTaskLabelsAPI); const { loading: editTaskLabelsLoading, queryCall: editQueryCall } = useQuery(editTaskLabelsAPI); @@ -34,6 +34,9 @@ export function useTaskLabels() { }, [loading, firstLoad, setTaskLabelsFetching]); const loadTaskLabels = useCallback(() => { + if (loadingRef.current) { + return; + } const teamId = getActiveTeamIdCookie(); queryCall( user?.tenantId as string, diff --git a/apps/web/app/hooks/features/useTaskPriorities.ts b/apps/web/app/hooks/features/useTaskPriorities.ts index cc27f0984..d6bd22e50 100644 --- a/apps/web/app/hooks/features/useTaskPriorities.ts +++ b/apps/web/app/hooks/features/useTaskPriorities.ts @@ -17,7 +17,7 @@ export function useTaskPriorities() { const [user] = useRecoilState(userState); const activeTeamId = useRecoilValue(activeTeamIdState); - const { loading, queryCall } = useQuery(getTaskPrioritiesList); + const { loading, queryCall, loadingRef } = useQuery(getTaskPrioritiesList); const { loading: createTaskPrioritiesLoading, queryCall: createQueryCall } = useQuery(createTaskPrioritiesAPI); const { loading: deleteTaskPrioritiesLoading, queryCall: deleteQueryCall } = useQuery(deleteTaskPrioritiesAPI); const { loading: editTaskPrioritiesLoading, queryCall: editQueryCall } = useQuery(editTaskPrioritiesAPI); @@ -34,6 +34,10 @@ export function useTaskPriorities() { }, [loading, firstLoad, setTaskPrioritiesFetching]); const loadTaskPriorities = useCallback(() => { + if (loadingRef.current) { + return; + } + const teamId = getActiveTeamIdCookie(); queryCall( user?.tenantId as string, @@ -46,7 +50,7 @@ export function useTaskPriorities() { return res; }); - }, [user, activeTeamId, setTaskPriorities, taskPriorities, queryCall]); + }, [user, activeTeamId, setTaskPriorities, taskPriorities, queryCall, loadingRef]); useEffect(() => { if (!firstLoad) return; diff --git a/apps/web/app/hooks/features/useTaskSizes.ts b/apps/web/app/hooks/features/useTaskSizes.ts index 884fd38ed..1d19f6fae 100644 --- a/apps/web/app/hooks/features/useTaskSizes.ts +++ b/apps/web/app/hooks/features/useTaskSizes.ts @@ -13,7 +13,7 @@ export function useTaskSizes() { const [user] = useRecoilState(userState); const activeTeamId = useRecoilValue(activeTeamIdState); - const { loading, queryCall } = useQuery(getTaskSizesList); + const { loading, queryCall, loadingRef } = useQuery(getTaskSizesList); const { loading: createTaskSizesLoading, queryCall: createQueryCall } = useQuery(createTaskSizesAPI); const { loading: deleteTaskSizesLoading, queryCall: deleteQueryCall } = useQuery(deleteTaskSizesAPI); const { loading: editTaskSizesLoading, queryCall: editQueryCall } = useQuery(editTaskSizesAPI); @@ -31,6 +31,10 @@ export function useTaskSizes() { }, [loading, firstLoad, setTaskSizesFetching]); const loadTaskSizes = useCallback(() => { + if (loadingRef.current) { + return; + } + const teamId = getActiveTeamIdCookie(); queryCall( user?.tenantId as string, @@ -43,7 +47,7 @@ export function useTaskSizes() { return res; }); - }, [user, activeTeamId, setTaskSizes, taskSizes, queryCall]); + }, [user, activeTeamId, setTaskSizes, taskSizes, queryCall, loadingRef]); useEffect(() => { if (!firstLoad) return; diff --git a/apps/web/app/hooks/features/useTaskStatus.ts b/apps/web/app/hooks/features/useTaskStatus.ts index a11495330..8757bba49 100644 --- a/apps/web/app/hooks/features/useTaskStatus.ts +++ b/apps/web/app/hooks/features/useTaskStatus.ts @@ -17,7 +17,7 @@ export function useTaskStatus() { const [user] = useRecoilState(userState); const activeTeamId = useRecoilValue(activeTeamIdState); - const { loading, queryCall } = useQuery(getTaskStatusList); + const { loading, queryCall, loadingRef } = useQuery(getTaskStatusList); const { loading: createTaskStatusLoading, queryCall: createQueryCall } = useQuery(createTaskStatusAPI); const { loading: deleteTaskStatusLoading, queryCall: deleteQueryCall } = useQuery(deleteTaskStatusAPI); const { loading: editTaskStatusLoading, queryCall: editQueryCall } = useQuery(editTaskStatusAPI); @@ -33,6 +33,9 @@ export function useTaskStatus() { }, [loading, firstLoad, setTaskStatusFetching]); const loadTaskStatusData = useCallback(() => { + if (loadingRef.current) { + return; + } const teamId = getActiveTeamIdCookie(); queryCall( user?.tenantId as string, @@ -44,7 +47,7 @@ export function useTaskStatus() { } return res; }); - }, [user, activeTeamId, setTaskStatus, taskStatus, queryCall]); + }, [user, activeTeamId, setTaskStatus, taskStatus, queryCall, loadingRef]); useEffect(() => { if (!firstLoad) return; diff --git a/apps/web/app/hooks/features/useTeamTasks.ts b/apps/web/app/hooks/features/useTeamTasks.ts index 111ea3f52..d8755c3f6 100644 --- a/apps/web/app/hooks/features/useTeamTasks.ts +++ b/apps/web/app/hooks/features/useTeamTasks.ts @@ -45,7 +45,7 @@ export function useTeamTasks() { const { firstLoad, firstLoadData: firstLoadTasksData } = useFirstLoad(); // Queries hooks - const { queryCall, loading } = useQuery(getTeamTasksAPI); + const { queryCall, loading, loadingRef } = useQuery(getTeamTasksAPI); const { queryCall: getTasksByIdQueryCall, loading: getTasksByIdLoading } = useQuery(getTasksByIdAPI); const { queryCall: deleteQueryCall, loading: deleteLoading } = useQuery(deleteTaskAPI); @@ -104,12 +104,17 @@ export function useTeamTasks() { const loadTeamTasksData = useCallback( (deepCheck?: boolean) => { + if (loadingRef.current) { + return new Promise((response) => { + response(true); + }); + } return queryCall().then((res) => { deepCheckAndUpdateTasks(res?.data?.items || [], deepCheck); return res; }); }, - [queryCall, deepCheckAndUpdateTasks] + [queryCall, deepCheckAndUpdateTasks, loadingRef] ); // Global loading state diff --git a/apps/web/app/hooks/features/useTimer.ts b/apps/web/app/hooks/features/useTimer.ts index 0a2edc8a2..38eb65a5e 100644 --- a/apps/web/app/hooks/features/useTimer.ts +++ b/apps/web/app/hooks/features/useTimer.ts @@ -161,11 +161,15 @@ export function useTimer() { const { firstLoad, firstLoadData: firstLoadTimerData } = useFirstLoad(); // Queries - const { queryCall, loading } = useQuery(getTimerStatusAPI); + const { queryCall, loading, loadingRef } = useQuery(getTimerStatusAPI); const { queryCall: toggleQueryCall } = useQuery(toggleTimerAPI); const { queryCall: startTimerQueryCall } = useQuery(startTimerAPI); const { queryCall: stopTimerQueryCall, loading: stopTimerLoading } = useQuery(stopTimerAPI); - const { queryCall: syncTimerQueryCall, loading: syncTimerLoading } = useQuery(syncTimerAPI); + const { + queryCall: syncTimerQueryCall, + loading: syncTimerLoading, + loadingRef: syncTimerLoadingRef + } = useQuery(syncTimerAPI); // const wasRunning = timerStatus?.running || false; const timerStatusRef = useSyncRef(timerStatus); @@ -187,6 +191,9 @@ export function useTimer() { const getTimerStatus = useCallback( (deepCheck?: boolean) => { + if (loadingRef.current) { + return; + } return queryCall().then((res) => { if (res.data && !isEqual(timerStatus, res.data)) { setTimerStatus((t) => { @@ -199,7 +206,7 @@ export function useTimer() { return res; }); }, - [timerStatus, setTimerStatus, queryCall] + [timerStatus, setTimerStatus, queryCall, loadingRef] ); const toggleTimer = useCallback( @@ -217,13 +224,13 @@ export function useTimer() { ); const syncTimer = useCallback(() => { - if (syncTimerLoading) { + if (syncTimerLoading || syncTimerLoadingRef.current) { return; } return syncTimerQueryCall(timerStatus?.lastLog?.source || TimerSource.TEAMS).then((res) => { return res; }); - }, [syncTimerQueryCall, timerStatus, syncTimerLoading]); + }, [syncTimerQueryCall, timerStatus, syncTimerLoading, syncTimerLoadingRef]); // Loading states useEffect(() => { diff --git a/apps/web/app/hooks/useQuery.ts b/apps/web/app/hooks/useQuery.ts index 3746dda58..a1517755f 100644 --- a/apps/web/app/hooks/useQuery.ts +++ b/apps/web/app/hooks/useQuery.ts @@ -2,21 +2,28 @@ import { useCallback, useRef, useState } from 'react'; export function useQuery Promise>(queryFunction: T) { const [loading, setLoading] = useState(false); + const loadingRef = useRef(false); const infiniteLoading = useRef(false); const queryCall = useCallback((...params: Parameters) => { setLoading(true); + loadingRef.current = true; const promise = queryFunction(...params); promise.finally(() => { - !infiniteLoading.current && setLoading(false); + if (!infiniteLoading.current) { + setLoading(false); + + loadingRef.current = false; + } }); promise.catch(() => { setLoading(false); + loadingRef.current = false; }); return promise; // eslint-disable-next-line react-hooks/exhaustive-deps }, []) as T; - return { queryCall, loading, infiniteLoading }; + return { queryCall, loading, infiniteLoading, loadingRef }; } diff --git a/apps/web/app/services/client/api/task-sizes.ts b/apps/web/app/services/client/api/task-sizes.ts index 15d4a5425..153210226 100644 --- a/apps/web/app/services/client/api/task-sizes.ts +++ b/apps/web/app/services/client/api/task-sizes.ts @@ -1,5 +1,5 @@ import { CreateResponse, DeleteResponse, ITaskSizesCreate } from '@app/interfaces'; -import api from '../axios'; +import api, { get } from '../axios'; export function createTaskSizesAPI(data: ITaskSizesCreate, tenantId?: string) { return api.post>('/task-sizes', data, { @@ -21,6 +21,7 @@ export function deleteTaskSizesAPI(id: string) { return api.delete(`/task-sizes/${id}`); } -export function getTaskSizesList(tenantId: string, organizationId: string, activeTeamId: string | null) { - return api.get(`/task-sizes?tenantId=${tenantId}&organizationId=${organizationId}&activeTeamId=${activeTeamId}`); +export async function getTaskSizesList(tenantId: string, organizationId: string, activeTeamId: string | null) { + const endpoint = `/task-sizes?tenantId=${tenantId}&organizationId=${organizationId}&organizationTeamId=${activeTeamId}`; + return get(endpoint, true); } diff --git a/apps/web/app/services/client/axios.ts b/apps/web/app/services/client/axios.ts index 019ec76e9..216232ef2 100644 --- a/apps/web/app/services/client/axios.ts +++ b/apps/web/app/services/client/axios.ts @@ -1,5 +1,5 @@ import { API_BASE_URL, DEFAULT_APP_PATH } from '@app/constants'; -import { getActiveTeamIdCookie } from '@app/helpers/cookies'; +import { getAccessTokenCookie, getActiveTeamIdCookie } from '@app/helpers/cookies'; import axios, { AxiosResponse } from 'axios'; const api = axios.create({ @@ -7,7 +7,6 @@ const api = axios.create({ withCredentials: true, timeout: 60 * 1000 }); - api.interceptors.request.use( async (config: any) => { const cookie = getActiveTeamIdCookie(); @@ -22,7 +21,6 @@ api.interceptors.request.use( Promise.reject(error); } ); - api.interceptors.response.use( (response: AxiosResponse) => response, async (error: { response: AxiosResponse }) => { @@ -36,4 +34,46 @@ api.interceptors.response.use( } ); +const apiDirect = axios.create({ + baseURL: `${process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL}/api`, + timeout: 60 * 1000 +}); +apiDirect.interceptors.request.use( + async (config: any) => { + const cookie = getAccessTokenCookie(); + + if (cookie) { + config.headers['Authorization'] = `Bearer ${cookie}`; + } + + return config; + }, + (error: any) => { + Promise.reject(error); + } +); +apiDirect.interceptors.response.use( + (response: AxiosResponse) => { + return { + ...response, + data: response + }; + }, + async (error: { response: AxiosResponse }) => { + const statusCode = error.response?.status; + + if (statusCode === 401) { + window.location.assign(DEFAULT_APP_PATH); + } + + return Promise.reject(error); + } +); + +function get(endpoint: string, isDirect: boolean) { + return isDirect && process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL ? apiDirect.get(endpoint) : api.get(endpoint); +} + export default api; + +export { apiDirect, get }; diff --git a/apps/web/app/services/server/requests/task-sizes.ts b/apps/web/app/services/server/requests/task-sizes.ts index 0de33a623..6e63055e3 100644 --- a/apps/web/app/services/server/requests/task-sizes.ts +++ b/apps/web/app/services/server/requests/task-sizes.ts @@ -52,12 +52,12 @@ export function getTaskSizesListRequest( { organizationId, tenantId, - activeTeamId - }: { tenantId: string; organizationId: string; activeTeamId: string | null }, + organizationTeamId + }: { tenantId: string; organizationId: string; organizationTeamId: string | null }, bearer_token: string ) { return serverFetch({ - path: `/task-sizes?tenantId=${tenantId}&organizationId=${organizationId}&organizationTeamId=${activeTeamId}`, + path: `/task-sizes?tenantId=${tenantId}&organizationId=${organizationId}&organizationTeamId=${organizationTeamId}`, method: 'GET', bearer_token }); diff --git a/apps/web/components/ui/svgs/add.tsx b/apps/web/components/ui/svgs/add.tsx new file mode 100644 index 000000000..f248228b7 --- /dev/null +++ b/apps/web/components/ui/svgs/add.tsx @@ -0,0 +1,7 @@ +export default function AddIcon(){ + return ( + <> + + + ) +} \ No newline at end of file diff --git a/apps/web/components/ui/svgs/bug.tsx b/apps/web/components/ui/svgs/bug.tsx new file mode 100644 index 000000000..0545ecb4f --- /dev/null +++ b/apps/web/components/ui/svgs/bug.tsx @@ -0,0 +1,15 @@ +export default function BugIcon({ + width=12, + height=12, + fill="white" +}:{ + width?: number, + height?: number, + fill?: string +}){ + return ( + + + + ) +} \ No newline at end of file diff --git a/apps/web/components/ui/svgs/left-arrow-tailess.tsx b/apps/web/components/ui/svgs/left-arrow-tailess.tsx new file mode 100644 index 000000000..b33767e40 --- /dev/null +++ b/apps/web/components/ui/svgs/left-arrow-tailess.tsx @@ -0,0 +1,7 @@ +export default function LeftArrowTailessIcon() { + return ( + <> + + + ) +} \ No newline at end of file diff --git a/apps/web/components/ui/svgs/three-dot.tsx b/apps/web/components/ui/svgs/three-dot.tsx new file mode 100644 index 000000000..1b24b4bf7 --- /dev/null +++ b/apps/web/components/ui/svgs/three-dot.tsx @@ -0,0 +1,15 @@ +export default function ThreeDotIcon({ + width=20, + height=20, + color="white" +}: { + width?: number, + height?: number, + color?: string +}) { + return ( + <> + + + ) +} \ No newline at end of file diff --git a/apps/web/components/ui/svgs/vertical-three-dot.tsx b/apps/web/components/ui/svgs/vertical-three-dot.tsx new file mode 100644 index 000000000..96f912ac8 --- /dev/null +++ b/apps/web/components/ui/svgs/vertical-three-dot.tsx @@ -0,0 +1,11 @@ +export default function VerticalThreeDot(){ + return ( + <> + + + + + + + ) +} \ No newline at end of file diff --git a/apps/web/lib/components/Kanban.tsx b/apps/web/lib/components/Kanban.tsx new file mode 100644 index 000000000..2072cfe38 --- /dev/null +++ b/apps/web/lib/components/Kanban.tsx @@ -0,0 +1,363 @@ +import LeftArrowTailessIcon from '@components/ui/svgs/left-arrow-tailess'; +import ThreeDotIcon from '@components/ui/svgs/three-dot'; +import React from 'react'; +import { useEffect, useState } from 'react'; +import { Draggable, DraggableProvided, DraggableStateSnapshot, Droppable, DroppableProvided, DroppableStateSnapshot } from 'react-beautiful-dnd'; +import Item from './kanban-card'; + +const grid = 8; + +const getItemStyle = (isDragging: any, draggableStyle: any) => ({ + userSelect: "none", + margin: `0 0 ${grid}px 0`, + background: isDragging ? "lightgreen" : null, + ...draggableStyle +}); + +const getBackgroundColor = (dropSnapshot: DroppableStateSnapshot) => { + + if (dropSnapshot.isDraggingOver) { + return { + backgroundColor: '#FFEBE6', + } + } + if (dropSnapshot.draggingFromThisWith) { + return { + backgroundColor: '#E6FCFF', + } + } + return { + backgroundColor: '', + + } +}; + +// this function changes column header color when dragged +function headerStyleChanger(snapshot: DraggableStateSnapshot){ + const backgroundColor = snapshot.isDragging ? '#0000ee' : '#fffee'; + + return { + backgroundColor + } +} + +/** + * wrapper to ensure card is draggable + * @param param0 + * @returns + */ +function InnerItemList({items}: { + items: any[] +}) { + return ( + <> +
+ {items.map((item: any, index: number) => ( + + {(dragProvided: DraggableProvided, dragSnapshot: DraggableStateSnapshot) => ( + + )} + + )) + } +
+ +)} + +/** + * inner column within a kanban column, + * it holds all cards underneath the name of the column + * @param props + * @returns + */ +function InnerList(props: { + title: string, + items: any, + dropProvided: DroppableProvided, + dropSnapshot: DroppableStateSnapshot +}) { + const { items, dropProvided, dropSnapshot } = props; + + return ( + +
+ + <> + {dropProvided.placeholder} + +
+ + ); +} + +/** + * wrapper to allow inner column act as + * a droppable area for cards being dragged + * @param param0 + * @returns + */ +export const KanbanDroppable = ({ title, droppableId, type, content }: { + title: string, + droppableId: string, + type: string, + content: any +} ) => { + const [enabled, setEnabled] = useState(false); + + useEffect(() => { + const animation = requestAnimationFrame(() => setEnabled(true)); + + return () => { + cancelAnimationFrame(animation); + setEnabled(false); + }; + }, []); + + if (!enabled) return null; + + return ( + <> + + {(dropProvided: DroppableProvided, dropSnapshot: DroppableStateSnapshot) => ( +
+ + + +
+ )} +
+ + ) +}; + +/** + * wrapper to allow inner column act as + * a droppable area for cards being dragged + * @param param0 + * @returns + */ +export const EmptyKanbanDroppable = ({index,title, items}: { + index: number; + title: string; + items: any; +})=> { + const [enabled, setEnabled] = useState(false); + + useEffect(() => { + const animation = requestAnimationFrame(() => setEnabled(true)); + + return () => { + cancelAnimationFrame(animation); + setEnabled(false); + }; + }, []); + + if (!enabled) return null; + + return ( + <> + { title.length > 0 && + + {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => ( + +
+ { title.length > 0 ? + <> +
+
+ + + + + +
+
+
+ {items.length} +
+
+

+ {title} +

+
+
+ +
+ + + : + null + } +
+ + )} +
+ } + + ) +}; + +const KanbanDraggableHeader = ({title, items, snapshot, provided}: { + title: string, + items: any, + snapshot: DraggableStateSnapshot, + provided: DraggableProvided +}) => { + return ( + <> +
+
+

+ {title} +

+
+ {items.length} +
+
+
+ + +
+
+ + ) +} + +/** + * column within the kanban board + * @param param0 + * @returns + */ +const KanbanDraggable = ({index,title, items}: { + index: number; + title: string; + items: any; +}) => { + + return ( + <> + { items.length > 0 && + + {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => ( + +
+ { items.length > 0 ? + <> + + + + + + : + null + } + +
+ + )} +
+ } + + ) +} + +export default KanbanDraggable; diff --git a/apps/web/lib/components/kanban-card.tsx b/apps/web/lib/components/kanban-card.tsx new file mode 100644 index 000000000..d14f3ff16 --- /dev/null +++ b/apps/web/lib/components/kanban-card.tsx @@ -0,0 +1,175 @@ +import BugIcon from "@components/ui/svgs/bug"; +import Image from 'next/image'; +import VerticalThreeDot from "@components/ui/svgs/vertical-three-dot"; +import { DraggableProvided } from "react-beautiful-dnd"; + +function getStyle(provided: DraggableProvided, style: any) { + if (!style) { + return provided.draggableProps.style; + } + + return { + ...provided.draggableProps.style, + ...style, + }; +} + +function Tag({title, backgroundColor, color}: { + title: string, + backgroundColor: string, + color: string +}) { + + return ( + <> +
+ +

{title}

+
+ + ) +} + +function TagList({tags}: { + tags: any[] +}){ + return ( + <> +
+ {tags.map((tag: any, index: number)=> { + return ( + + ) + })} +
+ + ) +} + + +const stackImages = (index: number, length: number) => { + const imageRadius = 20; + + const total_length = ((length+1) * imageRadius); + + return { + zIndex: (index+1).toString(), + right: `calc(${total_length -(imageRadius * (index + 2))}px)` + } +} + +/** + * card that represent each task + * @param props + * @returns + */ +export default function Item(props: any) { + + + + const { + item, + isDragging, + isGroupedOver, + provided, + style, + isClone, + index, + } = props; + + return ( +
+
+
+ +
+
+ +
+

#213

+

{item.content}

+ +
+
+
+ +
+
+
+
+ Worked: +

0 h 0 m

+
+
+
+ {images.map((image: any, index: number)=> { + return ( + {""} + ) + })} +
+
+
+
+ ); +} + +const images = [ + { + id: 0, + url: '/assets/cover/auth-bg-cover-dark.png' + }, + { + id: 1, + url: '/assets/cover/auth-bg-cover-dark.png' + }, + { + id: 2, + url: '/assets/cover/auth-bg-cover-dark.png' + }, + { + id: 3, + url: '/assets/cover/auth-bg-cover-dark.png' + }, + { + id: 4, + url: '/assets/cover/auth-bg-cover-dark.png' + }, +] \ No newline at end of file diff --git a/apps/web/lib/features/team-members-kanban-view.tsx b/apps/web/lib/features/team-members-kanban-view.tsx new file mode 100644 index 000000000..3f0beea7f --- /dev/null +++ b/apps/web/lib/features/team-members-kanban-view.tsx @@ -0,0 +1,203 @@ +import { clsxm } from "@app/utils"; +import KanbanDraggable, { EmptyKanbanDroppable } from "lib/components/Kanban" +import { AddIcon } from "lib/components/svgs"; +import React from "react"; +import { useEffect, useState } from "react"; +import { DragDropContext, DropResult, Droppable, DroppableProvided, DroppableStateSnapshot } from "react-beautiful-dnd"; + +const reorder = (list: any[], startIndex:number , endIndex:number ) => { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + + return result; +}; + +const reorderItemMap = ({ itemMap, source, destination }: { + itemMap: any, + source: any, + destination: any +}) => { + const current = [...itemMap[source.droppableId]]; + const next = [...itemMap[destination.droppableId]]; + const target = current[source.index]; + + // moving to same list + if (source.droppableId === destination.droppableId) { + const reordered = reorder(current, source.index, destination.index); + const result = { + ...itemMap, + [source.droppableId]: reordered, + }; + return { + quoteItem: result, + }; + } + + // moving to different list + + // remove from original + current.splice(source.index, 1); + // insert into next + next.splice(destination.index, 0, target); + + const result = { + ...itemMap, + [source.droppableId]: current, + [destination.droppableId]: next, + }; + + return { + quoteItem: result, + }; +}; + +export const KanbanView = ({ itemsArray }: { itemsArray: any}) => { + + const [items, setItems] = useState(itemsArray); + + const [column, setColumn] = useState(Object.keys(itemsArray)) + + /** + * This function handles all drag and drop logic + * on the kanban board. + * @param result + * @returns + */ + const onDragEnd = (result: DropResult) => { + + if (result.combine) { + if (result.type === 'COLUMN') { + const shallow = [...column]; + shallow.splice(result.source.index, 1); + setColumn(shallow); + return; + } + + const item = items[result.source.droppableId]; + const withItemRemoved = [...item]; + + withItemRemoved.splice(result.source.index, 1); + + const orderedItems = { + ...items, + [result.source.droppableId]: withItemRemoved, + }; + setItems(orderedItems); + return; + } + + // dropped nowhere + if (!result.destination) { + return; + } + + const source = result.source; + const destination = result.destination; + + // did not move anywhere - can bail early + if ( + source.droppableId === destination.droppableId && + source.index === destination.index + ) { + return; + } + + // reordering column + if (result.type === 'COLUMN') { + const reorderedItem = reorder(items, source.index, destination.index); + + setItems(reorderedItem); + + return; + } + + const data = reorderItemMap({ + itemMap: items, + source, + destination, + }); + + setItems(data.quoteItem); + + } + + const [enabled, setEnabled] = useState(false); + + useEffect(() => { + const animation = requestAnimationFrame(() => setEnabled(true)); + + return () => { + cancelAnimationFrame(animation); + setEnabled(false); + }; + }, []); + + if (!enabled) return null; + + return ( + <> + + { column.length > 0 && + + {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => ( +
+ {column.length > 0 ? + <> + {column.map((column: any, index: number) => { + return ( + + { items[column].length > 0 ? + <> +
+ +
+ +

Create Issues

+
+
+ + : +
+ +
+ } +
+ ) + })} + + : + null + } + <> + {provided.placeholder} + +
+ )} +
+ } +
+ + ) +} + + diff --git a/apps/web/package.json b/apps/web/package.json index d541df50d..19d83883b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -65,6 +65,7 @@ "polished": "^4.2.2", "postcss": "^8.4.19", "react": "18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-colorful": "^5.6.1", "react-country-flag": "^3.1.0", "react-day-picker": "^8.8.0", @@ -96,6 +97,7 @@ "@types/node": "18.8.4", "@types/pako": "^2.0.0", "@types/react": "18.0.21", + "@types/react-beautiful-dnd": "^13.1.6", "@types/react-dom": "18.0.6", "@types/react-google-recaptcha": "^2.1.5", "eslint": "^8.28.0", diff --git a/apps/web/pages/api/task-sizes/index.tsx b/apps/web/pages/api/task-sizes/index.tsx index d015c1c84..393b0c084 100644 --- a/apps/web/pages/api/task-sizes/index.tsx +++ b/apps/web/pages/api/task-sizes/index.tsx @@ -7,12 +7,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (!user) return $res(); - const { activeTeamId } = req.query; + const { organizationTeamId } = req.query; const par = { tenantId, organizationId, - activeTeamId: (activeTeamId as string) || null + organizationTeamId: (organizationTeamId as string) || null }; switch (req.method) { diff --git a/apps/web/pages/kanban/index.tsx b/apps/web/pages/kanban/index.tsx new file mode 100644 index 000000000..b0b6d5c08 --- /dev/null +++ b/apps/web/pages/kanban/index.tsx @@ -0,0 +1,166 @@ +import { withAuthentication } from "lib/app/authenticator"; +import { KanbanView } from "lib/features/team-members-kanban-view" +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; + +const Kanban= () => { + const router = useRouter() + const [winReady, setwinReady] = useState(false); + + const todo = { + id: 'status-1', + name: 'TODO' + } + + const ongoing = { + id: 'status-2', + name: 'ONGOING' + } + + const review = { + id: 'status-3', + name: 'REVIEW' + } + + const demoData = { + todo: [ + { + id: '1', + content: 'demo content', + 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', + 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', + 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', + 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', + 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', + tags: [ + { + id: 'tag-1', + title: 'User Profile', + backgroundColor: '#D7EBDF', + color: '#3D9360' + }, + { + id: 'tag-2', + title: 'BackEnd', + backgroundColor: '#D7EBDF', + color: '#3D9360' + }, + ], + status: review + } + ]} + + useEffect(() => { + + setwinReady(true); + + + }, [router.isReady]); + + return ( + <> + {winReady ? + + : + null + } + + + ) +} + +export default withAuthentication(Kanban, { displayName: 'Kanban'}); diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index b8c544421..9cbcbd3ba 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -33,6 +33,9 @@ module.exports = { transparent: 'transparent', current: 'currentColor', neutral: '#7E7991', + indianRed: '#D95F5F', + grey: '#868296', + transparentWhite: 'rgba(255, 255, 255, 0.30)', default: { DEFAULT: '#282048' }, diff --git a/yarn.lock b/yarn.lock index 92b50ed36..cb5ed753d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1161,7 +1161,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.22.5": +"@babel/runtime@^7.22.5", "@babel/runtime@^7.9.2": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== @@ -5660,6 +5660,14 @@ dependencies: "@types/node" "*" +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.4" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.4.tgz#cc477ce0283bb9d19ea0cbfa2941fe2c8493a1be" + integrity sha512-ZchYkbieA+7tnxwX/SCBySx9WwvWR8TaP5tb2jRAzwvLb/rWchGw3v0w3pqUbUvj0GCwW2Xz/AVPSk6kUGctXQ== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/http-errors@*": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.1.tgz#20172f9578b225f6c7da63446f56d4ce108d5a65" @@ -5813,6 +5821,13 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== +"@types/react-beautiful-dnd@^13.1.6": + version "13.1.6" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.6.tgz#a616443903bfc370fee298b0144dbce7234d5561" + integrity sha512-FXAuaa52ux7HWQDumi3MFSAAsW8OKfDImy1pSZPKe85nV9mZ1f4spVzW0a2boYvkIhphjbWUi5EwUiRG8Rq/Qg== + dependencies: + "@types/react" "*" + "@types/react-dom@18.0.6": version "18.0.6" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.6.tgz#36652900024842b74607a17786b6662dd1e103a1" @@ -5834,6 +5849,16 @@ dependencies: "@types/react" "*" +"@types/react-redux@^7.1.20": + version "7.1.28" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.28.tgz#30a44303c7daceb6ede9cfb4aaf72e64f1dde4de" + integrity sha512-EQr7cChVzVUuqbA+J8ArWK1H0hLAHKOs21SIMrskKZ3nHNeE+LFYA+IsoZGhVOT8Ktjn3M20v4rnZKN3fLbypw== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + "@types/react@*": version "18.2.21" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.21.tgz#774c37fd01b522d0b91aed04811b58e4e0514ed9" @@ -9032,6 +9057,12 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" crypto-random-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-4.0.0.tgz#5a3cc53d7dd86183df5da0312816ceeeb5bb1fc2" @@ -14858,6 +14889,11 @@ memfs@^3.4.1, memfs@^3.4.3: dependencies: fs-monkey "^1.0.4" +memoize-one@^5.1.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== + meow@^8.0.0, meow@^8.1.2: version "8.1.2" resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897" @@ -17552,7 +17588,7 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@^15, prop-types@^15.5.0, prop-types@^15.8.1: +prop-types@^15, prop-types@^15.5.0, 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== @@ -17770,6 +17806,11 @@ quote-stream@^1.0.1: minimist "^1.1.3" through2 "^2.0.0" +raf-schd@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -17818,6 +17859,19 @@ react-async-script@^1.1.1: hoist-non-react-statics "^3.3.0" prop-types "^15.5.0" +react-beautiful-dnd@^13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2" + integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ== + dependencies: + "@babel/runtime" "^7.9.2" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.2.0" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-colorful@^5.6.1: version "5.6.1" resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b" @@ -17879,7 +17933,7 @@ react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.1: +react-is@^17.0.1, react-is@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== @@ -17918,6 +17972,18 @@ react-popper@^2.3.0: react-fast-compare "^3.0.1" warning "^4.0.2" +react-redux@^7.2.0: + version "7.2.9" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" + integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ== + dependencies: + "@babel/runtime" "^7.15.4" + "@types/react-redux" "^7.1.20" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^17.0.2" + react-remove-scroll-bar@^2.3.3: version "2.3.4" resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz#53e272d7a5cb8242990c7f144c44d8bd8ab5afd9" @@ -18169,6 +18235,13 @@ redeyed@~2.1.0: dependencies: esprima "~4.0.0" +redux@^4.0.0, redux@^4.0.4: + version "4.2.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" + integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== + dependencies: + "@babel/runtime" "^7.9.2" + reflect-metadata@0.1.13: version "0.1.13" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" @@ -19978,6 +20051,11 @@ tiny-invariant@1.0.6: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73" integrity sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA== +tiny-invariant@^1.0.6: + version "1.3.1" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" + integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== + tiny-relative-date@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07" @@ -20599,6 +20677,11 @@ use-callback-ref@^1.3.0: dependencies: tslib "^2.0.0" +use-memo-one@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" + integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== + use-sidecar@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"