diff --git a/.cspell.json b/.cspell.json index 7347918a7..207b86a4c 100644 --- a/.cspell.json +++ b/.cspell.json @@ -53,6 +53,7 @@ "ipsum", "JITSU", "kanban", + "kanbandata", "Lorem", "libappindicator", "lucide", @@ -74,6 +75,7 @@ "testid", "Timesheet", "tanstack", + "taskstatus", "tempor", "vcpu", "Vercel", diff --git a/apps/mobile/app/app.tsx b/apps/mobile/app/app.tsx index 8b706947b..4f79ec1dc 100644 --- a/apps/mobile/app/app.tsx +++ b/apps/mobile/app/app.tsx @@ -9,24 +9,24 @@ * The app navigation resides in ./app/navigators, so head over there * if you're interested in adding screens and navigators. */ -import "./i18n" -import "./utils/ignoreWarnings" -import { useFonts } from "expo-font" -import React, { useEffect } from "react" -import { Provider as PaperProvider } from "react-native-paper" +import './i18n'; +import './utils/ignoreWarnings'; +import { useFonts } from 'expo-font'; +import React, { useEffect } from 'react'; +import { Provider as PaperProvider } from 'react-native-paper'; -import { initialWindowMetrics, SafeAreaProvider } from "react-native-safe-area-context" -import { useInitialRootStore, useStores } from "./models" -import { AppNavigator, useNavigationPersistence } from "./navigators" -import { ErrorBoundary } from "./screens/ErrorScreen/ErrorBoundary" -import * as storage from "./utils/storage" -import { customDarkTheme, customFontsToLoad, customLightTheme } from "./theme" -import { setupReactotron } from "./services/reactotron" -import Config from "./config" -import { observer } from "mobx-react-lite" -import { initCrashReporting } from "./utils/crashReporting" -import FlashMessage from "react-native-flash-message" -import { ClickOutsideProvider } from "react-native-click-outside" +import { initialWindowMetrics, SafeAreaProvider } from 'react-native-safe-area-context'; +import { useInitialRootStore, useStores } from './models'; +import { AppNavigator, useNavigationPersistence } from './navigators'; +import { ErrorBoundary } from './screens/ErrorScreen/ErrorBoundary'; +import * as storage from './utils/storage'; +import { customDarkTheme, customFontsToLoad, customLightTheme } from './theme'; +import { setupReactotron } from './services/reactotron'; +import Config from './config'; +import { observer } from 'mobx-react-lite'; +import { initCrashReporting } from './utils/crashReporting'; +import FlashMessage from 'react-native-flash-message'; +import { ClickOutsideProvider } from 'react-native-click-outside'; // Set up Reactotron, which is a free desktop app for inspecting and debugging // React Native apps. Learn more here: https://github.com/infinitered/reactotron @@ -34,40 +34,40 @@ setupReactotron({ // clear the Reactotron window when the app loads/reloads clearOnLoad: true, // generally going to be localhost - host: "localhost", + host: 'localhost', // Reactotron can monitor AsyncStorage for you useAsyncStorage: true, // log the initial restored state from AsyncStorage logInitialState: true, // log out any snapshots as they happen (this is useful for debugging but slow) - logSnapshots: false, -}) + logSnapshots: false +}); -export const NAVIGATION_PERSISTENCE_KEY = "NAVIGATION_STATE" +export const NAVIGATION_PERSISTENCE_KEY = 'NAVIGATION_STATE'; interface AppProps { - hideSplashScreen: () => Promise + hideSplashScreen: () => Promise; } /** * This is the root component of our app. */ const App = observer((props: AppProps) => { - const { hideSplashScreen } = props + const { hideSplashScreen } = props; const { initialNavigationState, onNavigationStateChange, - isRestored: isNavigationStateRestored, - } = useNavigationPersistence(storage, NAVIGATION_PERSISTENCE_KEY) + isRestored: isNavigationStateRestored + } = useNavigationPersistence(storage, NAVIGATION_PERSISTENCE_KEY); const { - authenticationStore: { isDarkMode }, - } = useStores() + authenticationStore: { isDarkMode } + } = useStores(); useEffect(() => { - initCrashReporting() // To initialize Sentry.io - }, []) + initCrashReporting(); // To initialize Sentry.io + }, []); - const [areFontsLoaded] = useFonts(customFontsToLoad) + const [areFontsLoaded] = useFonts(customFontsToLoad); const { rehydrated } = useInitialRootStore(() => { // This runs after the root store has been initialized and rehydrated. @@ -76,8 +76,8 @@ const App = observer((props: AppProps) => { // Slightly delaying splash screen hiding for better UX; can be customized or removed as needed, // Note: (vanilla Android) The splash-screen will not appear if you launch your app via the terminal or Android Studio. Kill the app and launch it normally by tapping on the launcher icon. https://stackoverflow.com/a/69831106 // Note: (vanilla iOS) You might notice the splash-screen logo change size. This happens in debug/development mode. Try building the app for release. - setTimeout(hideSplashScreen, 500) - }) + setTimeout(hideSplashScreen, 500); + }); // Before we show the app, we have to wait for our state to be ready. // In the meantime, don't render anything. This will be the background @@ -85,11 +85,11 @@ const App = observer((props: AppProps) => { // In iOS: application:didFinishLaunchingWithOptions: // In Android: https://stackoverflow.com/a/45838109/204044 // You can replace with your own loading component if you wish. - if (!rehydrated || !isNavigationStateRestored || !areFontsLoaded) return null + if (!rehydrated || !isNavigationStateRestored || !areFontsLoaded) return null; // otherwise, we're ready to render the app - const theme = isDarkMode ? customDarkTheme : customLightTheme + const theme = isDarkMode ? customDarkTheme : customLightTheme; return ( @@ -105,6 +105,6 @@ const App = observer((props: AppProps) => { - ) -}) -export default App + ); +}); +export default App; diff --git a/apps/mobile/app/navigators/AppNavigator.tsx b/apps/mobile/app/navigators/AppNavigator.tsx index 2979ded99..1ea89942d 100644 --- a/apps/mobile/app/navigators/AppNavigator.tsx +++ b/apps/mobile/app/navigators/AppNavigator.tsx @@ -11,7 +11,7 @@ import { import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { StackScreenProps } from '@react-navigation/stack'; import { observer } from 'mobx-react-lite'; -import React from 'react'; +import React, { useEffect } from 'react'; import Config from '../config'; import { QueryClient, QueryClientProvider } from 'react-query'; import { useStores } from '../models'; // @demo remove-current-line @@ -25,6 +25,8 @@ import { } from './AuthenticatedNavigator'; import { DemoTabParamList } from './DemoNavigator'; // @demo remove-current-line import { navigationRef, useBackButtonHandler } from './navigationUtilities'; +import { Appearance } from 'react-native'; +import AsyncStorage from '@react-native-async-storage/async-storage'; /** * This type allows TypeScript to know what routes are defined in this navigator @@ -94,6 +96,27 @@ const AppStack = observer(function AppStack() { interface NavigationProps extends Partial> {} export const AppNavigator = observer(function AppNavigator(props: NavigationProps) { + const { + authenticationStore: { isDarkMode, toggleTheme } + } = useStores(); + + useEffect(() => { + const checkAppInstallAndSetTheme = async () => { + const firstTimeAppOpen = await AsyncStorage.getItem('initialThemeSetupDone'); + + if (!firstTimeAppOpen) { + const colorsScheme = Appearance.getColorScheme(); + if (colorsScheme === 'dark' && !isDarkMode) { + toggleTheme(); + } else if (colorsScheme === 'light' && isDarkMode) { + toggleTheme(); + } + await AsyncStorage.setItem('initialThemeSetupDone', JSON.stringify(true)); + } + }; + checkAppInstallAndSetTheme(); + }, []); + useBackButtonHandler((routeName) => exitRoutes.includes(routeName)); const queryClient = new QueryClient(); return ( diff --git a/apps/web/app/constants.ts b/apps/web/app/constants.ts index 39ccdb801..55dd56ec2 100644 --- a/apps/web/app/constants.ts +++ b/apps/web/app/constants.ts @@ -1,5 +1,6 @@ import { JitsuOptions } from '@jitsu/jitsu-react/dist/useJitsu'; import { I_SMTPRequest } from './interfaces/ISmtp'; +import { getNextPublicEnv } from './env'; export const API_BASE_URL = '/api'; export const DEFAULT_APP_PATH = '/auth/passcode'; @@ -28,16 +29,34 @@ export const NO_TEAM_POPUP_SHOW_COOKIE_NAME = 'no-team-popup-show'; export const ACTIVE_PROJECT_COOKIE_NAME = 'auth-active-project'; // Recaptcha -export const RECAPTCHA_SITE_KEY = process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY; +export const RECAPTCHA_SITE_KEY = getNextPublicEnv( + 'NEXT_PUBLIC_CAPTCHA_SITE_KEY', + process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY +); export const RECAPTCHA_SECRET_KEY = process.env.CAPTCHA_SECRET_KEY; +// Gauzy Server URL export const GAUZY_API_SERVER_URL = process.env.GAUZY_API_SERVER_URL || 'https://api.gauzy.co/api'; +export const GAUZY_API_BASE_SERVER_URL = getNextPublicEnv( + 'NEXT_PUBLIC_GAUZY_API_SERVER_URL', + process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL +); +// Invite export const INVITE_CALLBACK_URL = process.env.INVITE_CALLBACK_URL || 'https://app.ever.team/auth/passcode'; export const INVITE_CALLBACK_PATH = '/auth/passcode'; export const VERIFY_EMAIL_CALLBACK_URL = process.env.VERIFY_EMAIL_CALLBACK_URL || 'https://app.ever.team/verify-email'; export const VERIFY_EMAIL_CALLBACK_PATH = '/verify-email'; -export const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID; +export const GA_MEASUREMENT_ID = getNextPublicEnv( + 'NEXT_PUBLIC_GA_MEASUREMENT_ID', + process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID +); + +// Chatwoot +export const CHATWOOT_API_KEY = getNextPublicEnv( + 'NEXT_PUBLIC_CHATWOOT_API_KEY', + process.env.NEXT_PUBLIC_CHATWOOT_API_KEY +); export const SMTP_FROM_ADDRESS = process.env.SMTP_FROM_ADDRESS || 'noreply@ever.team'; export const SMTP_HOST = process.env.SMTP_HOST || ''; @@ -45,7 +64,12 @@ export const SMTP_PORT = process.env.SMTP_PORT || ''; export const SMTP_SECURE = process.env.SMTP_SECURE || ''; export const SMTP_USERNAME = process.env.SMTP_USERNAME || ''; export const SMTP_PASSWORD = process.env.SMTP_PASSWORD || ''; -export const DISABLE_AUTO_REFRESH = process.env.NEXT_PUBLIC_DISABLE_AUTO_REFRESH === 'true'; +export const DISABLE_AUTO_REFRESH = getNextPublicEnv('NEXT_PUBLIC_DISABLE_AUTO_REFRESH', { + default: process.env.NEXT_PUBLIC_DISABLE_AUTO_REFRESH, + map(value) { + return value === 'true'; + } +}); export const APP_NAME = process.env.APP_NAME || 'Ever Teams'; export const APP_SIGNATURE = process.env.APP_SIGNATURE || 'Ever Teams'; @@ -64,23 +88,42 @@ export const smtpConfiguration: () => I_SMTPRequest = () => ({ }); // Cookies -export const COOKIE_DOMAINS = (process.env.NEXT_PUBLIC_COOKIE_DOMAINS || 'ever.team').split(',').map((d) => d.trim()); +export const COOKIE_DOMAINS = getNextPublicEnv('NEXT_PUBLIC_COOKIE_DOMAINS', { + default: process.env.NEXT_PUBLIC_COOKIE_DOMAINS || 'ever.team', + map(value) { + return value?.split(',').map((d) => d.trim()) || []; + } +}); // MEET Constants -export const MEET_DOMAIN = process.env.NEXT_PUBLIC_MEET_DOMAIN || 'meet.ever.team'; +export const MEET_DOMAIN = getNextPublicEnv( + 'NEXT_PUBLIC_MEET_DOMAIN', + process.env.NEXT_PUBLIC_MEET_DOMAIN || 'meet.ever.team' +); export const MEET_JWT_APP_ID = process.env.MEET_JWT_APP_ID || 'ever_teams'; export const MEET_JWT_APP_SECRET = process.env.MEET_JWT_APP_SECRET; export const MEET_JWT_TOKEN_COOKIE_NAME = 'meet-jwt-session'; // BOARD board -export const BOARD_APP_DOMAIN = process.env.NEXT_PUBLIC_BOARD_APP_DOMAIN || 'https://board.ever.team'; -export const BOARD_BACKEND_POST_URL = process.env.NEXT_PUBLIC_BOARD_BACKEND_POST_URL || 'https://jsonboard.ever.team/api/v2/post/'; -export const BOARD_FIREBASE_CONFIG = process.env.NEXT_PUBLIC_BOARD_FIREBASE_CONFIG; +export const BOARD_APP_DOMAIN = getNextPublicEnv( + 'NEXT_PUBLIC_BOARD_APP_DOMAIN', + process.env.NEXT_PUBLIC_BOARD_APP_DOMAIN || 'https://board.ever.team' +); + +export const BOARD_BACKEND_POST_URL = getNextPublicEnv( + 'NEXT_PUBLIC_BOARD_BACKEND_POST_URL', + process.env.NEXT_PUBLIC_BOARD_BACKEND_POST_URL || 'https://jsonboard.ever.team/api/v2/post/' +); +export const BOARD_FIREBASE_CONFIG = getNextPublicEnv( + 'NEXT_PUBLIC_BOARD_FIREBASE_CONFIG', + process.env.NEXT_PUBLIC_BOARD_FIREBASE_CONFIG +); // Jitsu export const jitsuConfiguration: () => JitsuOptions = () => ({ - host: process.env.NEXT_PUBLIC_JITSU_BROWSER_URL || '', - writeKey: process.env.NEXT_PUBLIC_JITSU_BROWSER_WRITE_KEY || '', + host: getNextPublicEnv('NEXT_PUBLIC_JITSU_BROWSER_URL', process.env.NEXT_PUBLIC_JITSU_BROWSER_URL).value, + writeKey: getNextPublicEnv('NEXT_PUBLIC_JITSU_BROWSER_WRITE_KEY', process.env.NEXT_PUBLIC_JITSU_BROWSER_WRITE_KEY) + .value, // if enabled - events will be sent to the console but no data sent to Jitsu. // Strange this is not mentioned in the documentation https://github.com/jitsucom/jitsu/blob/35c4ecaff54d61a87853381cb17262b7bfbd4a6e/libs/jitsu-js/src/jitsu.ts#L40 echoEvents: false, @@ -88,7 +131,10 @@ export const jitsuConfiguration: () => JitsuOptions = () => ({ }); // Github Integration -export const GITHUB_APP_NAME = process.env.NEXT_PUBLIC_GITHUB_APP_NAME || 'ever-github'; +export const GITHUB_APP_NAME = getNextPublicEnv( + 'NEXT_PUBLIC_GITHUB_APP_NAME', + process.env.NEXT_PUBLIC_GITHUB_APP_NAME || 'ever-github' +); // Application Languages export const APPLICATION_LANGUAGES = [ @@ -125,3 +171,7 @@ export enum IssuesView { TABLE = 'TABLE', BLOCKS = 'BLOCKS' } + +export const TaskStatus = { + INPROGRESS: 'in-progress' +} diff --git a/apps/web/app/env.ts b/apps/web/app/env.ts new file mode 100644 index 000000000..1da87ec87 --- /dev/null +++ b/apps/web/app/env.ts @@ -0,0 +1,65 @@ +const NEXT_PUBLIC_ENVS: { value: Env } = { value: {} }; + +type Env = Record; + +type OptionObject = { + default?: string; + map?: (value: string | undefined) => T; +}; +type Options = string | OptionObject; + +type InferValue = T extends { map: (value: any) => infer U } ? U : string | undefined; + +type ReturnedType = { + readonly value: T extends string ? string : InferValue; +}; + +/** + * This function only loads environment variables starting with NEXT_PUBLIC_* + * + * Useful for getting the latest value of the variable at runtime rather than at build time + * + * @param name + * @param options + * @returns + */ +export function getNextPublicEnv>(name: string, options?: O): ReturnedType { + return { + get value() { + const defaultValue = typeof options === 'string' ? options : options?.default; + + let value = NEXT_PUBLIC_ENVS.value[name] || defaultValue; + if (typeof options === 'object' && options.map) { + value = options.map(value) as any; + } + + return value as any; + } + }; +} + +export function setNextPublicEnv(envs: Env) { + if (envs) { + NEXT_PUBLIC_ENVS.value = { + ...NEXT_PUBLIC_ENVS.value, + ...envs + }; + } +} + +export function loadNextPublicEnvs() { + return Object.keys(process.env) + .filter((key) => key.startsWith('NEXT_PUBLIC')) + .reduce( + (acc, value) => { + if (process.env[value]) { + acc[value] = process.env[value] as string; + } + return acc; + }, + {} as Record + ); +} + +// Preload Some variables +setNextPublicEnv(loadNextPublicEnvs()); diff --git a/apps/web/app/helpers/cookies/helpers.ts b/apps/web/app/helpers/cookies/helpers.ts index 9c0e0782a..980e1024d 100644 --- a/apps/web/app/helpers/cookies/helpers.ts +++ b/apps/web/app/helpers/cookies/helpers.ts @@ -4,7 +4,7 @@ import { deleteCookie as _deleteCookie, getCookie as _getCookie, setCookie as _s export const deleteCookie: typeof _deleteCookie = (key, options) => { _deleteCookie(key, options); - COOKIE_DOMAINS.forEach((domain) => { + COOKIE_DOMAINS.value.forEach((domain) => { _deleteCookie(key, { domain, ...options @@ -22,7 +22,7 @@ export const setCookie: SetCookie = (key, data, options, crossSite) => { _setCookie(key, data, options); crossSite && - COOKIE_DOMAINS.forEach((domain) => { + COOKIE_DOMAINS.value.forEach((domain) => { _setCookie(key, data, { domain, ...options diff --git a/apps/web/app/hooks/features/useImageAssets.ts b/apps/web/app/hooks/features/useImageAssets.ts index fac4dd61d..7164968e7 100644 --- a/apps/web/app/hooks/features/useImageAssets.ts +++ b/apps/web/app/hooks/features/useImageAssets.ts @@ -1,6 +1,7 @@ import { getAccessTokenCookie } from '@app/helpers'; import { useCallback, useState } from 'react'; import axios, { AxiosResponse } from 'axios'; +import { GAUZY_API_BASE_SERVER_URL } from '@app/constants'; export function useImageAssets() { const [loading, setLoading] = useState(false); @@ -15,7 +16,7 @@ export function useImageAssets() { setLoading(true); return axios - .post(process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL + `/api/image-assets/upload/${folder}`, formData, { + .post(GAUZY_API_BASE_SERVER_URL.value + `/api/image-assets/upload/${folder}`, formData, { headers: { 'tenant-id': tenantId, authorization: `Bearer ${bearer_token}` diff --git a/apps/web/app/hooks/features/useKanban.ts b/apps/web/app/hooks/features/useKanban.ts index dac4f75d8..0c0bc2659 100644 --- a/apps/web/app/hooks/features/useKanban.ts +++ b/apps/web/app/hooks/features/useKanban.ts @@ -12,9 +12,9 @@ export function useKanban() { const [kanbanBoard, setKanbanBoard] = useRecoilState(kanbanBoardState); const taskStatusHook = useTaskStatus(); - const { tasks, tasksFetching } = useTeamTasks(); - + const { tasks, tasksFetching, updateTask } = useTeamTasks(); + /** * format data for kanban board */ @@ -42,6 +42,8 @@ export function useKanban() { return { data: kanbanBoard, isLoading: loading, - columns: taskStatusHook.taskStatus + columns: taskStatusHook.taskStatus, + updateKanbanBoard: setKanbanBoard, + updateTaskStatus: updateTask } } \ No newline at end of file diff --git a/apps/web/app/hooks/useCollaborative.ts b/apps/web/app/hooks/useCollaborative.ts index 24b552486..b285330cf 100644 --- a/apps/web/app/hooks/useCollaborative.ts +++ b/apps/web/app/hooks/useCollaborative.ts @@ -65,8 +65,8 @@ export function useCollaborative(user?: IUser) { const onBoardClick = useCallback(() => { const members = collaborativeMembers.map((m) => m.id).join(','); - if (collaborativeMembers.length > 0 && BOARD_APP_DOMAIN) { - const url = new URL(BOARD_APP_DOMAIN); + if (collaborativeMembers.length > 0 && BOARD_APP_DOMAIN.value) { + const url = new URL(BOARD_APP_DOMAIN.value); url.searchParams.set('live', 'true'); url.searchParams.set('members', btoa(members)); diff --git a/apps/web/app/services/client/api/auth.ts b/apps/web/app/services/client/api/auth.ts index e45048331..5b1ee2b91 100644 --- a/apps/web/app/services/client/api/auth.ts +++ b/apps/web/app/services/client/api/auth.ts @@ -2,6 +2,7 @@ import { getRefreshTokenCookie } from '@app/helpers/cookies'; import { ISuccessResponse } from '@app/interfaces'; import { ILoginResponse, IRegisterDataAPI, ISigninEmailConfirmResponse } from '@app/interfaces/IAuthentication'; import api, { get } from '../axios'; +import { GAUZY_API_BASE_SERVER_URL } from '@app/constants'; export const signInWithEmailAndCodeAPI = (email: string, code: string) => { return api.post(`/auth/login`, { @@ -44,7 +45,7 @@ export const getAuthenticatedUserDataAPI = async () => { const endpoint = `/user/me?${query.toString()}`; const data = await get(endpoint, true); - return process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL ? data.data : data; + return GAUZY_API_BASE_SERVER_URL.value ? data.data : data; }; export const verifyUserEmailByCodeAPI = (code: string) => { diff --git a/apps/web/app/services/client/api/employee.ts b/apps/web/app/services/client/api/employee.ts index 54f942a91..eb2d6559d 100644 --- a/apps/web/app/services/client/api/employee.ts +++ b/apps/web/app/services/client/api/employee.ts @@ -1,3 +1,4 @@ +import { GAUZY_API_BASE_SERVER_URL } from '@app/constants'; import { get } from '../axios'; export async function getWorkingEmployeesAPI(tenantId: string, organizationId: string) { @@ -6,12 +7,10 @@ export async function getWorkingEmployeesAPI(tenantId: string, organizationId: s 'where[organizationId]': organizationId, 'relations[0]': 'user' }; - const query = new URLSearchParams(params); + const query = new URLSearchParams(params); - const endpoint = process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL - ? `/employee/pagination?${query.toString()}` - : '/employee/working'; + const endpoint = GAUZY_API_BASE_SERVER_URL.value ? `/employee/pagination?${query.toString()}` : '/employee/working'; const data = await get(endpoint, true, { tenantId }); - return process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL ? data.data : data; + return GAUZY_API_BASE_SERVER_URL.value ? data.data : data; } diff --git a/apps/web/app/services/client/api/invite.ts b/apps/web/app/services/client/api/invite.ts index 73e46035c..4bac1c949 100644 --- a/apps/web/app/services/client/api/invite.ts +++ b/apps/web/app/services/client/api/invite.ts @@ -1,6 +1,6 @@ import { PaginationResponse } from '@app/interfaces/IDataResponse'; import { IInvitation, MyInvitationActionEnum, CreateResponse, IInviteCreate, IRole } from '@app/interfaces'; -import { INVITE_CALLBACK_URL } from '@app/constants'; +import { GAUZY_API_BASE_SERVER_URL, INVITE_CALLBACK_URL } from '@app/constants'; import api, { get, post } from '../axios'; import { AxiosResponse } from 'axios'; @@ -41,7 +41,7 @@ export async function inviteByEmailsAPI(data: IIInviteRequest, tenantId: string) // 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; + return GAUZY_API_BASE_SERVER_URL.value ? fetchData.data : fetchData; } export async function getTeamInvitationsAPI(tenantId: string, organizationId: string, role: string, teamId: string) { @@ -56,7 +56,7 @@ export async function getTeamInvitationsAPI(tenantId: string, organizationId: st const endpoint = `/invite?${query.toString()}`; const data = await get(endpoint, true, { tenantId }); - return process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL ? data.data : data; + return GAUZY_API_BASE_SERVER_URL.value ? data.data : data; } export function removeTeamInvitationsAPI(invitationId: string) { @@ -73,7 +73,7 @@ export async function getMyInvitationsAPI(tenantId: string) { const endpoint = '/invite/me'; const data = await get(endpoint, true, { tenantId }); - return process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL ? data.data : data; + return GAUZY_API_BASE_SERVER_URL.value ? data.data : data; } export function acceptRejectMyInvitationsAPI(invitationId: string, action: MyInvitationActionEnum) { diff --git a/apps/web/app/services/client/api/languages.ts b/apps/web/app/services/client/api/languages.ts index 48a158c1f..15c7294e7 100644 --- a/apps/web/app/services/client/api/languages.ts +++ b/apps/web/app/services/client/api/languages.ts @@ -1,8 +1,9 @@ +import { GAUZY_API_BASE_SERVER_URL } from '@app/constants'; import { get } from '../axios'; export async function getLanguageListAPI(is_system: boolean) { const endpoint = `/languages?is_system=${is_system}`; const data = await get(endpoint, true); - return process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL ? data.data : data; + return GAUZY_API_BASE_SERVER_URL.value ? data.data : data; } diff --git a/apps/web/app/services/client/api/organization-team.ts b/apps/web/app/services/client/api/organization-team.ts index f9222f3b0..b2ea8e31f 100644 --- a/apps/web/app/services/client/api/organization-team.ts +++ b/apps/web/app/services/client/api/organization-team.ts @@ -9,6 +9,7 @@ import { } from '@app/interfaces'; import moment from 'moment'; import api, { get } from '../axios'; +import { GAUZY_API_BASE_SERVER_URL } from '@app/constants'; export async function getOrganizationTeamsAPI(organizationId: string, tenantId: string) { const relations = [ @@ -36,7 +37,7 @@ export async function getOrganizationTeamsAPI(organizationId: string, tenantId: const endpoint = `/organization-team?${query.toString()}`; const data = await get(endpoint, true, { tenantId }); - return process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL ? data.data : data; + return GAUZY_API_BASE_SERVER_URL.value ? data.data : data; } export function createOrganizationTeamAPI(name: string) { @@ -74,7 +75,7 @@ export async function getOrganizationTeamAPI(teamId: string, organizationId: str const endpoint = `/organization-team/${teamId}?${queries.toString()}`; const data = await get(endpoint, true); - return process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL ? data.data : data; + return GAUZY_API_BASE_SERVER_URL.value ? data.data : data; } export function editOrganizationTeamAPI(data: IOrganizationTeamUpdate) { diff --git a/apps/web/app/services/client/api/tasks.ts b/apps/web/app/services/client/api/tasks.ts index 62c81e640..50fa49ae9 100644 --- a/apps/web/app/services/client/api/tasks.ts +++ b/apps/web/app/services/client/api/tasks.ts @@ -3,6 +3,7 @@ import { CreateResponse, DeleteResponse, PaginationResponse } from '@app/interfa import { ICreateTask, ITeamTask } from '@app/interfaces/ITask'; import { ITasksTimesheet } from '@app/interfaces/ITimer'; import api, { get } from '../axios'; +import { GAUZY_API_BASE_SERVER_URL } from '@app/constants'; export function getTasksByIdAPI(taskId: string) { return api.get>(`/tasks/${taskId}`); @@ -40,7 +41,7 @@ export async function getTeamTasksAPI(organizationId: string, tenantId: string, const endpoint = `/tasks/team?${query.toString()}`; const data = await get(endpoint, true, { tenantId }); - return process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL ? data.data : data; + return GAUZY_API_BASE_SERVER_URL.value ? data.data : data; } export function deleteTaskAPI(taskId: string) { @@ -61,7 +62,7 @@ export async function tasksTimesheetStatisticsAPI( organizationId: string, employeeId?: string ) { - if (process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL) { + if (GAUZY_API_BASE_SERVER_URL.value) { const employeesParams = employeeId ? [employeeId].reduce((acc: any, v, i) => { acc[`employeeIds[${i}]`] = v; @@ -109,7 +110,7 @@ export async function activeTaskTimesheetStatisticsAPI( organizationId: string, employeeId?: string ) { - if (process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL) { + if (GAUZY_API_BASE_SERVER_URL.value) { const employeesParams = employeeId ? [employeeId].reduce((acc: any, v, i) => { acc[`employeeIds[${i}]`] = v; diff --git a/apps/web/app/services/client/api/timer.ts b/apps/web/app/services/client/api/timer.ts index 45928cadb..59af332f2 100644 --- a/apps/web/app/services/client/api/timer.ts +++ b/apps/web/app/services/client/api/timer.ts @@ -1,14 +1,13 @@ import { ITimerStatus, IToggleTimerParams, TimerSource } from '@app/interfaces/ITimer'; import api, { get } from '../axios'; +import { GAUZY_API_BASE_SERVER_URL } from '@app/constants'; export async function getTimerStatusAPI(tenantId: string, organizationId: string) { const params = new URLSearchParams({ tenantId, organizationId }); - const endpoint = process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL - ? `/timesheet/timer/status?${params.toString()}` - : '/timer/status'; + const endpoint = GAUZY_API_BASE_SERVER_URL.value ? `/timesheet/timer/status?${params.toString()}` : '/timer/status'; const data = await get(endpoint, true); - return process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL ? data.data : data; + return GAUZY_API_BASE_SERVER_URL.value ? data.data : data; } export function toggleTimerAPI(body: Pick) { diff --git a/apps/web/app/services/client/axios.ts b/apps/web/app/services/client/axios.ts index bd53becb2..0d210b312 100644 --- a/apps/web/app/services/client/axios.ts +++ b/apps/web/app/services/client/axios.ts @@ -1,5 +1,5 @@ /* eslint-disable no-mixed-spaces-and-tabs */ -import { API_BASE_URL, DEFAULT_APP_PATH } from '@app/constants'; +import { API_BASE_URL, DEFAULT_APP_PATH, GAUZY_API_BASE_SERVER_URL } from '@app/constants'; import { getAccessTokenCookie, getActiveTeamIdCookie } from '@app/helpers/cookies'; import axios, { AxiosResponse } from 'axios'; @@ -36,9 +36,9 @@ 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(); @@ -53,6 +53,7 @@ apiDirect.interceptors.request.use( Promise.reject(error); } ); + apiDirect.interceptors.response.use( (response: AxiosResponse) => { return { @@ -78,8 +79,12 @@ function get( tenantId: string; } ) { - return isDirect && process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL + let baseURL: string | undefined = GAUZY_API_BASE_SERVER_URL.value; + baseURL = baseURL ? `${baseURL}/api` : undefined; + + return isDirect && baseURL ? apiDirect.get(endpoint, { + baseURL, headers: { ...(extras?.tenantId ? { 'tenant-id': extras?.tenantId } : {}) } @@ -95,8 +100,12 @@ function post( tenantId: string; } ) { - return isDirect && process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL + let baseURL: string | undefined = GAUZY_API_BASE_SERVER_URL.value; + baseURL = baseURL ? `${baseURL}/api` : undefined; + + return isDirect && baseURL ? apiDirect.post(endpoint, data, { + baseURL, headers: { ...(extras?.tenantId ? { 'tenant-id': extras?.tenantId } : {}) } @@ -104,6 +113,6 @@ function post( : api.post(endpoint, data); } -export default api; +export { get, post }; -export { apiDirect, get, post }; +export default api; diff --git a/apps/web/components/shared/skeleton/KanbanBoardSkeleton.tsx b/apps/web/components/shared/skeleton/KanbanBoardSkeleton.tsx new file mode 100644 index 000000000..523481c5b --- /dev/null +++ b/apps/web/components/shared/skeleton/KanbanBoardSkeleton.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import Skeleton from "react-loading-skeleton"; + +const KanbanBoardSkeleton = () => { + + const columns = Array.from(Array(3)); + + const tasks = Array.from(Array(2)); + + return ( + <> +
+ {columns.map((_, index: number)=> { + return ( + +
+ + +
+ {tasks.map((_, index: number)=> { + return ( + + + + ) + })} + + +
+
+
+ ) + })} +
+ + ) +} + +export default KanbanBoardSkeleton; \ No newline at end of file diff --git a/apps/web/components/ui/services/recaptcha.tsx b/apps/web/components/ui/services/recaptcha.tsx index f3606d44e..41b930157 100644 --- a/apps/web/components/ui/services/recaptcha.tsx +++ b/apps/web/components/ui/services/recaptcha.tsx @@ -1,18 +1,17 @@ -import { RECAPTCHA_SITE_KEY } from '@app/constants'; import ReCAPTCHA from 'react-google-recaptcha'; const ReactReCAPTCHA = ReCAPTCHA as any; export function SiteReCAPTCHA({ - key = RECAPTCHA_SITE_KEY, + siteKey, onChange, onErrored, onExpired }: { - key?: string; + siteKey: string; onChange: (token: string | null) => void; onExpired?: () => void; onErrored?: () => void; }) { - return ; + return ; } diff --git a/apps/web/lib/app/init-state.tsx b/apps/web/lib/app/init-state.tsx index 2fd2bd7b5..247e1f116 100644 --- a/apps/web/lib/app/init-state.tsx +++ b/apps/web/lib/app/init-state.tsx @@ -121,7 +121,7 @@ function InitState() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - return DISABLE_AUTO_REFRESH !== true ? : <>; + return !DISABLE_AUTO_REFRESH.value ? : <>; } function useOneTimeLoad(func: () => void) { diff --git a/apps/web/lib/components/Kanban.tsx b/apps/web/lib/components/Kanban.tsx index b615790f0..1f6cdb8af 100644 --- a/apps/web/lib/components/Kanban.tsx +++ b/apps/web/lib/components/Kanban.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from 'react'; import { Draggable, DraggableProvided, DraggableStateSnapshot, Droppable, DroppableProvided, DroppableStateSnapshot } from 'react-beautiful-dnd'; import Item from './kanban-card'; import { ITeamTask } from '@app/interfaces'; +import { TaskStatus } from '@app/constants'; const grid = 8; @@ -63,7 +64,7 @@ function InnerItemList({items, title}: { isDragging={dragSnapshot.isDragging} isGroupedOver={Boolean(dragSnapshot.combineTargetFor)} provided={dragProvided} - style={title === 'review' && { + style={title === TaskStatus.INPROGRESS && { borderWidth: '1px', borderColor: '#6FCF97', borderStyle: 'solid' @@ -168,7 +169,7 @@ export const KanbanDroppable = ({ title, droppableId, type, content }: { export const EmptyKanbanDroppable = ({index,title, items}: { index: number; title: string; - items: any; + items: ITeamTask[]; })=> { const [enabled, setEnabled] = useState(false); diff --git a/apps/web/lib/components/kanban-card.tsx b/apps/web/lib/components/kanban-card.tsx index 2a7d5e9be..909a7efd9 100644 --- a/apps/web/lib/components/kanban-card.tsx +++ b/apps/web/lib/components/kanban-card.tsx @@ -4,6 +4,7 @@ import VerticalThreeDot from "@components/ui/svgs/vertical-three-dot"; import { DraggableProvided } from "react-beautiful-dnd"; import CircularProgress from "@components/ui/svgs/circular-progress"; import PriorityIcon from "@components/ui/svgs/priority-icon"; +import { Tag } from "@app/interfaces"; function getStyle(provided: DraggableProvided, style: any) { if (!style) { @@ -37,7 +38,7 @@ function setCommentIconColor(commentType: "tagged" | "untagged") { return style } -function Tag({title, backgroundColor, color}: { +function TagCard({title, backgroundColor, color}: { title: string, backgroundColor: string, color: string @@ -64,17 +65,17 @@ function Tag({title, backgroundColor, color}: { } function TagList({tags}: { - tags: any[] + tags: Tag[] }){ return ( <>
- {tags.map((tag: any, index: number)=> { + {tags.map((tag: Tag, index: number)=> { return ( - ) @@ -194,7 +195,7 @@ export default function Item(props: any) {
- {item.hasComment !== "none" && + {item.hasComment && (
void; onExpired?: () => void; onErrored?: () => void; theme?: string; }) { return ( - + ); } diff --git a/apps/web/lib/features/integrations/boards/export-to-backend.ts b/apps/web/lib/features/integrations/boards/export-to-backend.ts index 870d5ea5a..d708873f1 100644 --- a/apps/web/lib/features/integrations/boards/export-to-backend.ts +++ b/apps/web/lib/features/integrations/boards/export-to-backend.ts @@ -15,7 +15,7 @@ export const exportToBackend = async ( appState: Partial, files: BinaryFiles ): Promise => { - if (!BOARD_BACKEND_POST_URL || !BOARD_APP_DOMAIN) { + if (!BOARD_BACKEND_POST_URL.value || !BOARD_APP_DOMAIN.value) { return { url: null, errorMessage: 'could Not Create Shareable Link' }; } @@ -40,13 +40,13 @@ export const exportToBackend = async ( maxBytes: FILE_UPLOAD_MAX_BYTES }); - const response = await fetch(BOARD_BACKEND_POST_URL, { + const response = await fetch(BOARD_BACKEND_POST_URL.value, { method: 'POST', body: payload.buffer }); const json = await response.json(); if (json.id) { - const url = new URL(BOARD_APP_DOMAIN); + const url = new URL(BOARD_APP_DOMAIN.value); // We need to store the key (and less importantly the id) as hash instead // of queryParam in order to never send it to the server url.hash = `json=${json.id},${encryptionKey}`; diff --git a/apps/web/lib/features/integrations/boards/firebase.ts b/apps/web/lib/features/integrations/boards/firebase.ts index a807b444d..20b4c4b9c 100644 --- a/apps/web/lib/features/integrations/boards/firebase.ts +++ b/apps/web/lib/features/integrations/boards/firebase.ts @@ -14,10 +14,10 @@ const _loadFirebase = async () => { if (!isFirebaseInitialized) { try { - if (!BOARD_FIREBASE_CONFIG) { + if (!BOARD_FIREBASE_CONFIG.value) { throw Error('Invalid Firebase configuration'); } - firebase.initializeApp(JSON.parse(BOARD_FIREBASE_CONFIG)); + firebase.initializeApp(JSON.parse(BOARD_FIREBASE_CONFIG.value)); } catch (error: any) { // trying initialize again throws. Usually this is harmless, and happens // mainly in dev (HMR) diff --git a/apps/web/lib/features/integrations/chatwoot/index.tsx b/apps/web/lib/features/integrations/chatwoot/index.tsx index 9bcce5240..849acf12e 100644 --- a/apps/web/lib/features/integrations/chatwoot/index.tsx +++ b/apps/web/lib/features/integrations/chatwoot/index.tsx @@ -1,3 +1,4 @@ +import { CHATWOOT_API_KEY } from '@app/constants'; import React, { useEffect } from 'react'; declare global { @@ -9,7 +10,7 @@ declare global { export default function ChatwootWidget() { useEffect(() => { - const websiteToken = process.env.NEXT_PUBLIC_CHATWOOT_API_KEY; + const websiteToken = CHATWOOT_API_KEY.value; if (!websiteToken) { return; } diff --git a/apps/web/lib/features/integrations/meet/index.tsx b/apps/web/lib/features/integrations/meet/index.tsx index c909ac17f..6a8c4602b 100644 --- a/apps/web/lib/features/integrations/meet/index.tsx +++ b/apps/web/lib/features/integrations/meet/index.tsx @@ -7,7 +7,7 @@ export default function MeetPage({ jwt, roomName }: { jwt: string; roomName: str return ( { - const result = Array.from(list); - const [removed] = result.splice(startIndex, 1); - result.splice(endIndex, 0, removed); - return result; -}; +export const KanbanView = ({ kanbanBoardTasks }: { kanbanBoardTasks: IKanban}) => { -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, + const { columns:kanbanColumns, updateKanbanBoard, updateTaskStatus } = useKanban(); + + const [items, setItems] = useState(kanbanBoardTasks); + + const [columns, setColumn] = useState(Object.keys(kanbanBoardTasks)); + + const reorderTask = (list: ITeamTask[], startIndex:number , endIndex:number ) => { + const tasks = Array.from(list); + const [removedTask] = tasks.splice(startIndex, 1); + tasks.splice(endIndex, 0, removedTask); + + return tasks; }; - return { - quoteItem: result, + + const reorderKanbanTasks = ({ kanbanTasks, source, destination }: { + kanbanTasks: IKanban, + source: DraggableLocation, + destination: DraggableLocation + }) => { + const sourceDroppableID = source.droppableId; + const destinationDroppableID = destination.droppableId; + const sourceIndex = source.index; + const destinationIndex = destination.index; + const currentTaskStatus = [...kanbanTasks[sourceDroppableID]]; + const nextTaskStatus = [...kanbanTasks[destinationDroppableID]]; + const targetStatus = currentTaskStatus[source.index]; + + // moving to same list + if (sourceDroppableID === destinationDroppableID) { + const reorderedKanbanTasks = reorderTask(currentTaskStatus, sourceIndex, destinationIndex); + const result = { + ...kanbanTasks, + [sourceDroppableID]: reorderedKanbanTasks, + }; + return { + kanbanBoard: result, + }; + } + + // remove from original + currentTaskStatus.splice(sourceIndex, 1); + + const taskstatus = destinationDroppableID as ITaskStatus; + + const updateTaskStatusData = {...targetStatus, status: taskstatus}; + + // update task status on server + updateTaskStatus(updateTaskStatusData); + + // insert into next + nextTaskStatus.splice(destinationIndex, 0, updateTaskStatusData); + + const result = { + ...kanbanTasks, + [sourceDroppableID]: currentTaskStatus, + [destinationDroppableID]: nextTaskStatus, + }; + + return { + kanbanBoard: 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, - }; -}; - -const getHeaderBackground = (columns: any, column: any) => { - - const selectState = columns.filter((item: any)=> { - return item.name === column - }); - - return selectState[0].color -} - -export const KanbanView = ({ itemsArray }: { itemsArray: any}) => { - - const { columns:kanbanColumns } = useKanban(); - - const [items, setItems] = useState(itemsArray); - - const [columns, setColumn] = useState(Object.keys(itemsArray)); + + const getHeaderBackground = (columns: ITaskStatusItemList[], column: string) => { + + const selectState = columns.filter((item: ITaskStatusItemList)=> { + return item.name === column + }); + + return selectState[0].color + } /** * This function handles all drag and drop logic @@ -96,6 +109,7 @@ export const KanbanView = ({ itemsArray }: { itemsArray: any}) => { [result.source.droppableId]: withItemRemoved, }; setItems(orderedItems); + return; } @@ -115,23 +129,24 @@ export const KanbanView = ({ itemsArray }: { itemsArray: any}) => { return; } - // reordering column - if (result.type === 'COLUMN') { - const reorderedItem = reorder(items, source.index, destination.index); - - setItems(reorderedItem); + // TODO: fix issues with reordering column + // if (result.type === 'COLUMN') { + // const reorderedItem = reorder(items, source.index, destination.index); - return; - } + // setItems(reorderedItem); + // // updateKanbanBoard(reorderedItem); + // // console.log('data '+ kanbandata) + // return; + // } - const data = reorderItemMap({ - itemMap: items, + const data = reorderKanbanTasks({ + kanbanTasks: items, source, destination, }); - setItems(data.quoteItem); - + setItems(data.kanbanBoard); + updateKanbanBoard(() => data.kanbanBoard) } const [enabled, setEnabled] = useState(false); diff --git a/apps/web/lib/settings/icon-items.tsx b/apps/web/lib/settings/icon-items.tsx index d624e36e5..5ba87b2db 100644 --- a/apps/web/lib/settings/icon-items.tsx +++ b/apps/web/lib/settings/icon-items.tsx @@ -1,3 +1,4 @@ +import { GAUZY_API_BASE_SERVER_URL } from '@app/constants'; import { IIcon } from '@app/interfaces'; import { clsxm } from '@app/utils'; import { DropdownItem } from 'lib/components'; @@ -93,7 +94,7 @@ export function IconItem({ export function generateIconList(iconFor: string, icons: string[]) { return icons.map((icon) => { return { - fullUrl: `${process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL}/public/ever-icons/${iconFor}/${icon}.svg`, + fullUrl: `${GAUZY_API_BASE_SERVER_URL.value}/public/ever-icons/${iconFor}/${icon}.svg`, path: `ever-icons/${iconFor}/${icon}.svg`, title: icon }; diff --git a/apps/web/lib/settings/integration-setting.tsx b/apps/web/lib/settings/integration-setting.tsx index 03c3e990d..ea72f157b 100644 --- a/apps/web/lib/settings/integration-setting.tsx +++ b/apps/web/lib/settings/integration-setting.tsx @@ -27,7 +27,7 @@ export const IntegrationSetting = () => { }, []); const queries = new URLSearchParams(params || {}); - const url = `https://github.com/apps/${GITHUB_APP_NAME}/installations/new?${queries.toString()}`; + const url = `https://github.com/apps/${GITHUB_APP_NAME.value}/installations/new?${queries.toString()}`; const { activeTeam } = useOrganizationTeams(); diff --git a/apps/web/pages/_app.tsx b/apps/web/pages/_app.tsx index 845ca8655..e2c9b958a 100644 --- a/apps/web/pages/_app.tsx +++ b/apps/web/pages/_app.tsx @@ -1,8 +1,10 @@ /* eslint-disable no-mixed-spaces-and-tabs */ +import { getNextPublicEnv, loadNextPublicEnvs, setNextPublicEnv } from '@app/env'; import { GA_MEASUREMENT_ID, jitsuConfiguration } from '@app/constants'; import { JitsuProvider } from '@jitsu/jitsu-react'; import { Analytics } from '@vercel/analytics/react'; import { AppState } from 'lib/app/init-state'; +import type { JitsuOptions } from '@jitsu/jitsu-react/dist/useJitsu'; import ChatwootWidget from 'lib/features/integrations/chatwoot'; import { NextPage, NextPageContext } from 'next'; import { ThemeProvider } from 'next-themes'; @@ -11,15 +13,23 @@ import Head from 'next/head'; import Script from 'next/script'; import { I18nextProvider } from 'react-i18next'; import { SkeletonTheme } from 'react-loading-skeleton'; -import 'react-loading-skeleton/dist/skeleton.css'; import { RecoilRoot } from 'recoil'; import { JitsuAnalytics } from '../lib/components/services/jitsu-analytics'; import i18n from '../ni18n.config'; +import 'react-loading-skeleton/dist/skeleton.css'; import '../styles/globals.css'; -const MyApp = ({ Component, pageProps }: AppProps) => { +type MyAppProps = { + jitsuConf?: JitsuOptions; + jitsuHost?: string; + envs: Record; + user?: any; +}; + +const MyApp = ({ Component, pageProps }: AppProps) => { + setNextPublicEnv(pageProps.envs); + const jitsuConf = pageProps?.jitsuConf; - console.log('Jitsu Host', pageProps); console.log(`Jitsu Configuration: ${JSON.stringify(jitsuConf)}`); const isJitsuEnvsPresent: boolean = jitsuConf?.host !== '' && jitsuConf?.writeKey !== ''; @@ -27,17 +37,17 @@ const MyApp = ({ Component, pageProps }: AppProps) => { return ( <> - {GA_MEASUREMENT_ID && ( + {GA_MEASUREMENT_ID.value && ( <> )} @@ -51,11 +61,11 @@ const MyApp = ({ Component, pageProps }: AppProps) => { options={ isJitsuEnvsPresent ? { - host: jitsuConf.host ?? '', - writeKey: jitsuConf.writeKey ?? undefined, - debug: jitsuConf.debug, - cookieDomain: jitsuConf.cookieDomain ?? undefined, - echoEvents: jitsuConf.echoEvents + host: jitsuConf?.host ?? '', + writeKey: jitsuConf?.writeKey ?? undefined, + debug: jitsuConf?.debug, + cookieDomain: jitsuConf?.cookieDomain ?? undefined, + echoEvents: jitsuConf?.echoEvents } : { disabled: true @@ -82,8 +92,8 @@ const MyApp = ({ Component, pageProps }: AppProps) => { MyApp.getInitialProps = async ({ Component, ctx }: { Component: NextPage; ctx: NextPageContext }) => { // Recover environment variables - const jitsuHost = process.env.NEXT_PUBLIC_JITSU_BROWSER_URL; - const jitsuWriteKey = process.env.NEXT_PUBLIC_JITSU_BROWSER_WRITE_KEY; + const jitsuHost = getNextPublicEnv('NEXT_PUBLIC_JITSU_BROWSER_URL').value; + const jitsuWriteKey = getNextPublicEnv('NEXT_PUBLIC_JITSU_BROWSER_WRITE_KEY').value; const jitsuConf = jitsuConfiguration(); @@ -105,7 +115,8 @@ MyApp.getInitialProps = async ({ Component, ctx }: { Component: NextPage(''); return ( @@ -153,24 +152,7 @@ function FillUserDataForm({ onChange={handleOnChange} autoComplete="off" /> - { RECAPTCHA_SITE_KEY && -
-
- { - handleOnChange({ target: { name: 'recaptcha', value: res } }); - setFeedback(''); - }} - onErrored={() => setFeedback(t('errors.NETWORK_ISSUE'))} - /> - {(errors['recaptcha'] || feedback) && ( - - {errors['recaptcha'] || feedback} - - )} -
-
- } +
@@ -184,3 +166,28 @@ function FillUserDataForm({ ); } + +function ReCAPTCHA({ handleOnChange, errors }: { handleOnChange: any; errors: any }) { + const { t } = useTranslation(); + const [feedback, setFeedback] = useState(''); + + const content = RECAPTCHA_SITE_KEY.value && ( +
+
+ { + handleOnChange({ target: { name: 'recaptcha', value: res } }); + setFeedback(''); + }} + onErrored={() => setFeedback(t('errors.NETWORK_ISSUE'))} + /> + {(errors['recaptcha'] || feedback) && ( + {errors['recaptcha'] || feedback} + )} +
+
+ ); + + return content || <>; +} diff --git a/apps/web/pages/kanban/index.tsx b/apps/web/pages/kanban/index.tsx index 589389ca3..9887bb646 100644 --- a/apps/web/pages/kanban/index.tsx +++ b/apps/web/pages/kanban/index.tsx @@ -1,4 +1,5 @@ import { useKanban } from "@app/hooks/features/useKanban"; +import KanbanBoardSkeleton from "@components/shared/skeleton/KanbanBoardSkeleton"; import { withAuthentication } from "lib/app/authenticator"; import { KanbanView } from "lib/features/team-members-kanban-view" import { MainLayout } from "lib/layout"; @@ -11,9 +12,9 @@ const Kanban= () => { <> {Object.keys(data).length > 0 ? - + : - null + }