From 7b5db9d1141edbe288b9efb4ee3a22ade8943ed2 Mon Sep 17 00:00:00 2001 From: Henrique Date: Thu, 29 Aug 2024 13:39:27 -0300 Subject: [PATCH 1/9] Expected funds --- components/dashboard/DashboardPage.tsx | 291 ++++++++++++++++++ components/dashboard/DashboardSection.tsx | 167 +--------- .../dashboard/sections/AccountSettings.js | 4 +- .../dashboard/sections/accounts/index.tsx | 7 +- .../sections/collectives/AllCollectives.tsx | 13 +- .../collectives/HostedCollectives.tsx | 5 +- .../sections/contributions/Contributions.tsx | 2 +- .../reports/HostExpensesReportView.tsx | 3 +- .../sections/reports/preview/Reports.tsx | 5 + .../dashboard/sections/updates/index.tsx | 6 +- .../dashboard/sections/updates/queries.ts | 13 +- .../[slug]/accounts/[[...subpath]].tsx | 19 ++ pages/dashboard/[slug]/chart-of-accounts.tsx | 18 ++ pages/dashboard/[slug]/contributors.tsx | 11 + pages/dashboard/[slug]/expected-funds.tsx | 18 ++ pages/dashboard/[slug]/expenses.tsx | 11 + pages/dashboard/[slug]/host-agreements.tsx | 18 ++ pages/dashboard/[slug]/host-applications.tsx | 18 ++ pages/dashboard/[slug]/host-expenses.tsx | 13 + pages/dashboard/[slug]/host-tax-forms.tsx | 18 ++ .../host-transactions/[[...subpath]].tsx | 19 ++ .../[slug]/host-virtual-card-requests.tsx | 18 ++ pages/dashboard/[slug]/host-virtual-cards.tsx | 18 ++ .../hosted-collectives/[[...subpath]].tsx | 19 ++ .../[slug]/incoming-contributions.tsx | 18 ++ pages/dashboard/[slug]/invoices-receipts.tsx | 26 ++ .../legacy-settings-sections/[section].tsx | 39 +++ .../[slug]/notifications/[[...subpath]].tsx | 19 ++ pages/dashboard/[slug]/orders.tsx | 18 ++ .../[slug]/outgoing-contributions.tsx | 18 ++ .../[slug]/reports/[[...subpath]].tsx | 37 +++ pages/dashboard/[slug]/submitted-expenses.tsx | 18 ++ pages/dashboard/[slug]/tax-information.tsx | 26 ++ pages/dashboard/[slug]/team.tsx | 11 + pages/dashboard/[slug]/tickets.tsx | 29 ++ pages/dashboard/[slug]/tiers.tsx | 27 ++ pages/dashboard/[slug]/transactions.tsx | 18 ++ .../[slug]/updates/[[...subpath]].tsx | 19 ++ pages/dashboard/[slug]/vendors.tsx | 11 + pages/dashboard/[slug]/virtual-cards.tsx | 13 + pages/{dashboard.tsx => dashboard/index.tsx} | 47 +-- pages/dashboard/root/account-settings.tsx | 27 ++ pages/dashboard/root/account-type.tsx | 20 ++ pages/dashboard/root/activity-log.tsx | 20 ++ pages/dashboard/root/all-collectives.tsx | 31 ++ pages/dashboard/root/ban-account.tsx | 20 ++ pages/dashboard/root/clear-cache.tsx | 25 ++ pages/dashboard/root/connect-accounts.tsx | 27 ++ pages/dashboard/root/host-transactions.tsx | 25 ++ pages/dashboard/root/merge-accounts.tsx | 27 ++ .../root/move-authored-contributions.tsx | 32 ++ pages/dashboard/root/move-expenses.tsx | 20 ++ .../root/move-received-contributions.tsx | 32 ++ .../root/recurring-contributions.tsx | 32 ++ pages/dashboard/root/search-and-ban.tsx | 27 ++ pages/dashboard/root/unhost-accounts.tsx | 27 ++ rewrites.js | 15 + 57 files changed, 1328 insertions(+), 207 deletions(-) create mode 100644 components/dashboard/DashboardPage.tsx create mode 100644 pages/dashboard/[slug]/accounts/[[...subpath]].tsx create mode 100644 pages/dashboard/[slug]/chart-of-accounts.tsx create mode 100644 pages/dashboard/[slug]/contributors.tsx create mode 100644 pages/dashboard/[slug]/expected-funds.tsx create mode 100644 pages/dashboard/[slug]/expenses.tsx create mode 100644 pages/dashboard/[slug]/host-agreements.tsx create mode 100644 pages/dashboard/[slug]/host-applications.tsx create mode 100644 pages/dashboard/[slug]/host-expenses.tsx create mode 100644 pages/dashboard/[slug]/host-tax-forms.tsx create mode 100644 pages/dashboard/[slug]/host-transactions/[[...subpath]].tsx create mode 100644 pages/dashboard/[slug]/host-virtual-card-requests.tsx create mode 100644 pages/dashboard/[slug]/host-virtual-cards.tsx create mode 100644 pages/dashboard/[slug]/hosted-collectives/[[...subpath]].tsx create mode 100644 pages/dashboard/[slug]/incoming-contributions.tsx create mode 100644 pages/dashboard/[slug]/invoices-receipts.tsx create mode 100644 pages/dashboard/[slug]/legacy-settings-sections/[section].tsx create mode 100644 pages/dashboard/[slug]/notifications/[[...subpath]].tsx create mode 100644 pages/dashboard/[slug]/orders.tsx create mode 100644 pages/dashboard/[slug]/outgoing-contributions.tsx create mode 100644 pages/dashboard/[slug]/reports/[[...subpath]].tsx create mode 100644 pages/dashboard/[slug]/submitted-expenses.tsx create mode 100644 pages/dashboard/[slug]/tax-information.tsx create mode 100644 pages/dashboard/[slug]/team.tsx create mode 100644 pages/dashboard/[slug]/tickets.tsx create mode 100644 pages/dashboard/[slug]/tiers.tsx create mode 100644 pages/dashboard/[slug]/transactions.tsx create mode 100644 pages/dashboard/[slug]/updates/[[...subpath]].tsx create mode 100644 pages/dashboard/[slug]/vendors.tsx create mode 100644 pages/dashboard/[slug]/virtual-cards.tsx rename pages/{dashboard.tsx => dashboard/index.tsx} (87%) create mode 100644 pages/dashboard/root/account-settings.tsx create mode 100644 pages/dashboard/root/account-type.tsx create mode 100644 pages/dashboard/root/activity-log.tsx create mode 100644 pages/dashboard/root/all-collectives.tsx create mode 100644 pages/dashboard/root/ban-account.tsx create mode 100644 pages/dashboard/root/clear-cache.tsx create mode 100644 pages/dashboard/root/connect-accounts.tsx create mode 100644 pages/dashboard/root/host-transactions.tsx create mode 100644 pages/dashboard/root/merge-accounts.tsx create mode 100644 pages/dashboard/root/move-authored-contributions.tsx create mode 100644 pages/dashboard/root/move-expenses.tsx create mode 100644 pages/dashboard/root/move-received-contributions.tsx create mode 100644 pages/dashboard/root/recurring-contributions.tsx create mode 100644 pages/dashboard/root/search-and-ban.tsx create mode 100644 pages/dashboard/root/unhost-accounts.tsx diff --git a/components/dashboard/DashboardPage.tsx b/components/dashboard/DashboardPage.tsx new file mode 100644 index 00000000000..6978e46e76b --- /dev/null +++ b/components/dashboard/DashboardPage.tsx @@ -0,0 +1,291 @@ +import React from 'react'; +import { useQuery } from '@apollo/client'; +import { clsx } from 'clsx'; +import { useRouter } from 'next/router'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import { isHostAccount, isIndividualAccount } from '../../lib/collective'; +import roles from '../../lib/constants/roles'; +import { API_V2_CONTEXT } from '../../lib/graphql/helpers'; +import useLocalStorage from '../../lib/hooks/useLocalStorage'; +import useLoggedInUser from '../../lib/hooks/useLoggedInUser'; +import { LOCAL_STORAGE_KEYS } from '../../lib/local-storage'; +import { require2FAForAdmins } from '../../lib/policies'; +import { PREVIEW_FEATURE_KEYS } from '../../lib/preview-features'; + +import { + ALL_SECTIONS, + ROOT_PROFILE_ACCOUNT, + ROOT_PROFILE_KEY, + ROOT_SECTIONS, + SECTIONS_ACCESSIBLE_TO_ACCOUNTANTS, +} from '../../components/dashboard/constants'; +import { DashboardContext } from '../../components/dashboard/DashboardContext'; +import { getMenuItems } from '../../components/dashboard/Menu'; +import DashboardTopBar from '../../components/dashboard/preview/DashboardTopBar'; +import SubMenu from '../../components/dashboard/preview/SubMenu'; +import { adminPanelQuery } from '../../components/dashboard/queries'; +import AdminPanelSideBar from '../../components/dashboard/SideBar'; +import Link from '../../components/Link'; +import MessageBox from '../../components/MessageBox'; +import Footer from '../../components/navigation/Footer'; +import NotificationBar from '../../components/NotificationBar'; +import Page from '../../components/Page'; +import SignInOrJoinFree from '../../components/SignInOrJoinFree'; +import { TwoFactorAuthRequiredMessage } from '../../components/TwoFactorAuthRequiredMessage'; + +import { OCFBannerWithData } from '../OCFBanner'; + +import type { DashboardSectionProps } from './types'; + +const messages = defineMessages({ + collectiveIsArchived: { + id: 'collective.isArchived', + defaultMessage: '{name} has been archived.', + }, + collectiveIsArchivedDescription: { + id: 'collective.isArchived.edit.description', + defaultMessage: 'This {type} has been archived and is no longer active.', + }, + userIsArchived: { + id: 'user.isArchived', + defaultMessage: 'Account has been archived.', + }, + userIsArchivedDescription: { + id: 'user.isArchived.edit.description', + defaultMessage: 'This account has been archived and is no longer active.', + }, +}); + +const getDefaultSectionForAccount = (account, loggedInUser) => { + if (!account) { + return null; + } else if (account.type === 'ROOT') { + return ROOT_SECTIONS.ALL_COLLECTIVES; + } else if ( + isIndividualAccount(account) || + (!isHostAccount(account) && loggedInUser.hasPreviewFeatureEnabled(PREVIEW_FEATURE_KEYS.COLLECTIVE_OVERVIEW)) + ) { + return ALL_SECTIONS.OVERVIEW; + } else if (isHostAccount(account)) { + return ALL_SECTIONS.HOST_EXPENSES; + } else { + const isAdmin = loggedInUser?.isAdminOfCollective(account); + const isAccountant = loggedInUser?.hasRole(roles.ACCOUNTANT, account); + return !isAdmin && isAccountant ? ALL_SECTIONS.PAYMENT_RECEIPTS : ALL_SECTIONS.EXPENSES; + } +}; + +const getNotification = (intl, account) => { + if (account?.isArchived) { + if (account.type === 'USER') { + return { + type: 'warning', + title: intl.formatMessage(messages.userIsArchived), + description: intl.formatMessage(messages.userIsArchivedDescription), + }; + } else { + return { + type: 'warning', + title: intl.formatMessage(messages.collectiveIsArchived, { name: account.name }), + description: intl.formatMessage(messages.collectiveIsArchivedDescription, { + type: account.type.toLowerCase(), + }), + }; + } + } +}; + +function getBlocker(LoggedInUser, account, section) { + if (!LoggedInUser) { + return ; + } else if (!account) { + return ; + } else if (account.isIncognito) { + return ; + } else if (account.type === 'ROOT' && LoggedInUser.isRoot) { + return; + } + + // Check permissions + const isAdmin = LoggedInUser.isAdminOfCollective(account); + if (SECTIONS_ACCESSIBLE_TO_ACCOUNTANTS.includes(section)) { + if (!isAdmin && !LoggedInUser.hasRole(roles.ACCOUNTANT, account)) { + return ( + + ); + } + } else if (!isAdmin) { + return ; + } +} + +export default function DashboardPage(props: { + Component: React.FC; + slug: string; + section: string; + subpath?: string[]; +}) { + const intl = useIntl(); + const router = useRouter(); + const slug = props.slug; + const section = props.section; + const subpath = props.subpath; + const { LoggedInUser, loadingLoggedInUser } = useLoggedInUser(); + const [lastWorkspaceVisit, setLastWorkspaceVisit] = useLocalStorage(LOCAL_STORAGE_KEYS.DASHBOARD_NAVIGATION_STATE, { + slug: LoggedInUser?.collective.slug, + }); + const isRootUser = LoggedInUser?.isRoot; + const defaultSlug = lastWorkspaceVisit.slug || LoggedInUser?.collective.slug; + const activeSlug = slug || defaultSlug; + const isRootProfile = activeSlug === ROOT_PROFILE_KEY; + + const { data, loading } = useQuery(adminPanelQuery, { + context: API_V2_CONTEXT, + variables: { slug: activeSlug }, + skip: !activeSlug || !LoggedInUser || isRootProfile, + }); + const account = isRootProfile && isRootUser ? ROOT_PROFILE_ACCOUNT : data?.account; + const selectedSection = section || getDefaultSectionForAccount(account, LoggedInUser); + + const useDynamicTopBar = LoggedInUser?.hasPreviewFeatureEnabled(PREVIEW_FEATURE_KEYS.DYNAMIC_TOP_BAR); + + // Keep track of last visited workspace account and sections + React.useEffect(() => { + if (activeSlug && activeSlug !== lastWorkspaceVisit.slug) { + if (LoggedInUser && !useDynamicTopBar) { + // this is instead configured as "default" account in NewAccountSwitcher + setLastWorkspaceVisit({ slug: activeSlug }); + } + } + // If there is no slug set (that means /dashboard) + // And if there is an activeSlug (this means lastWorkspaceVisit OR LoggedInUser) + // And a LoggedInUser + // And if activeSlug is different than LoggedInUser slug + if (!slug && activeSlug && LoggedInUser && activeSlug !== LoggedInUser.collective.slug) { + router.replace(`/dashboard/${activeSlug}`); + } + }, [activeSlug, LoggedInUser]); + + // Clear last visited workspace account if not admin + React.useEffect(() => { + if (account && !LoggedInUser.isAdminOfCollective(account)) { + setLastWorkspaceVisit({ slug: null }); + } + }, [account]); + + const notification = getNotification(intl, account); + const [expandedSection, setExpandedSection] = React.useState(null); + const isLoading = loading || loadingLoggedInUser; + const blocker = !isLoading && getBlocker(LoggedInUser, account, selectedSection); + const titleBase = intl.formatMessage({ id: 'Dashboard', defaultMessage: 'Dashboard' }); + const menuItems = account ? getMenuItems({ intl, account, LoggedInUser }) : []; + const accountIdentifier = account && (account.name || `@${account.slug}`); + + let subMenu = null; + const parentMenuItem = menuItems.find( + item => 'subMenu' in item && item.subMenu?.find(item => item.section === selectedSection), + ); + if (parentMenuItem && 'subMenu' in parentMenuItem) { + subMenu = parentMenuItem.subMenu; + } + + return ( + setLastWorkspaceVisit({ slug }), + }} + > +
+ + {Boolean(notification) && } + {blocker ? ( +
+ +

{blocker}

+ {LoggedInUser && ( + + + + )} +
+ {!LoggedInUser && } +
+ ) : !useDynamicTopBar ? ( +
+ + {LoggedInUser && require2FAForAdmins(account) && !LoggedInUser.hasTwoFactorAuth ? ( + + ) : ( +
+ {!isRootProfile && ( + + )} + +
+ )} +
+ ) : ( +
+ + + {LoggedInUser && require2FAForAdmins(account) && !LoggedInUser.hasTwoFactorAuth ? ( + + ) : ( +
+ {subMenu ? ( + + ) : ( +
+ )} + + +
+ )} +
+ )} + +
+
+ + ); +} + +DashboardPage.getInitialProps = () => { + return { + scripts: { googleMaps: true }, // TODO: This should be enabled only for events + }; +}; diff --git a/components/dashboard/DashboardSection.tsx b/components/dashboard/DashboardSection.tsx index 4983a6ee05b..0b41cab8938 100644 --- a/components/dashboard/DashboardSection.tsx +++ b/components/dashboard/DashboardSection.tsx @@ -1,130 +1,19 @@ -import React, { useContext } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import { values } from 'lodash'; -import { useIntl } from 'react-intl'; - -import useLoggedInUser from '../../lib/hooks/useLoggedInUser'; -import { PREVIEW_FEATURE_KEYS } from '../../lib/preview-features'; import Container from '../Container'; import LoadingPlaceholder from '../LoadingPlaceholder'; import NotFound from '../NotFound'; import { OCFBannerWithData } from '../OCFBanner'; -import AccountSettingsForm from '../root-actions/AccountSettings'; -import AccountType from '../root-actions/AccountType'; -import BanAccount from '../root-actions/BanAccounts'; -import BanAccountsWithSearch from '../root-actions/BanAccountsWithSearch'; -import ClearCacheForAccountForm from '../root-actions/ClearCacheForAccountForm'; -import ConnectAccountsForm from '../root-actions/ConnectAccountsForm'; -import MergeAccountsForm from '../root-actions/MergeAccountsForm'; -import MoveAuthoredContributions from '../root-actions/MoveAuthoredContributions'; -import MoveExpenses from '../root-actions/MoveExpenses'; -import MoveReceivedContributions from '../root-actions/MoveReceivedContributions'; -import RecurringContributions from '../root-actions/RecurringContributions'; -import RootActivityLog from '../root-actions/RootActivityLog'; -import UnhostAccountForm from '../root-actions/UnhostAccountForm'; -import { HostAdminAccountingSection } from './sections/accounting'; -import Accounts from './sections/accounts'; -import AccountSettings from './sections/AccountSettings'; -import AllCollectives from './sections/collectives/AllCollectives'; -import HostApplications from './sections/collectives/HostApplications'; -import HostedCollectives from './sections/collectives/HostedCollectives'; -import HostExpectedFunds from './sections/contributions/HostExpectedFunds'; -import HostFinancialContributions from './sections/contributions/HostFinancialContributions'; -import IncomingContributions from './sections/contributions/IncomingContributions'; -import OutgoingContributions from './sections/contributions/OutgoingContributions'; -import Contributors from './sections/Contributors'; -import HostExpenses from './sections/expenses/HostDashboardExpenses'; -import ReceivedExpenses from './sections/expenses/ReceivedExpenses'; -import SubmittedExpenses from './sections/expenses/SubmittedExpenses'; -import HostDashboardAgreements from './sections/HostDashboardAgreements'; -import HostVirtualCardRequests from './sections/HostVirtualCardRequests'; -import HostVirtualCards from './sections/HostVirtualCards'; -import InvoicesReceipts from './sections/invoices-receipts/InvoicesReceipts'; -import HostDashboardTaxForms from './sections/legal-documents/HostDashboardTaxForms'; -import NotificationsSettings from './sections/NotificationsSettings'; import Overview from './sections/overview/Overview'; -import HostDashboardReports from './sections/reports/HostDashboardReports'; -import PreviewReports from './sections/reports/preview/Reports'; -import { TaxInformationSettingsSection } from './sections/tax-information'; -import Team from './sections/Team'; -import AccountTransactions from './sections/transactions/AccountTransactions'; -import AllTransactions from './sections/transactions/AllTransactions'; -import HostTransactions from './sections/transactions/HostTransactions'; -import Updates from './sections/updates'; -import Vendors from './sections/Vendors'; -import VirtualCards from './sections/virtual-cards/VirtualCards'; -import { - ALL_SECTIONS, - LEGACY_SECTIONS, - LEGACY_SETTINGS_SECTIONS, - ROOT_PROFILE_KEY, - ROOT_SECTIONS, - SECTION_LABELS, - SECTIONS, - SETTINGS_SECTIONS, -} from './constants'; -import { DashboardContext } from './DashboardContext'; -import DashboardHeader from './DashboardHeader'; +import { SECTIONS } from './constants'; const DASHBOARD_COMPONENTS = { - [SECTIONS.HOSTED_COLLECTIVES]: HostedCollectives, - [SECTIONS.CHART_OF_ACCOUNTS]: HostAdminAccountingSection, - [SECTIONS.HOST_FINANCIAL_CONTRIBUTIONS]: HostFinancialContributions, - [SECTIONS.HOST_EXPENSES]: HostExpenses, - [SECTIONS.HOST_AGREEMENTS]: HostDashboardAgreements, - [SECTIONS.HOST_TAX_FORMS]: HostDashboardTaxForms, - [SECTIONS.HOST_APPLICATIONS]: HostApplications, - [SECTIONS.REPORTS]: HostDashboardReports, - [SECTIONS.HOST_VIRTUAL_CARDS]: HostVirtualCards, - [SECTIONS.HOST_VIRTUAL_CARD_REQUESTS]: HostVirtualCardRequests, [SECTIONS.OVERVIEW]: Overview, - [SECTIONS.EXPENSES]: ReceivedExpenses, - [SECTIONS.SUBMITTED_EXPENSES]: SubmittedExpenses, - [SECTIONS.CONTRIBUTORS]: Contributors, - [SECTIONS.INCOMING_CONTRIBUTIONS]: IncomingContributions, - [SECTIONS.OUTGOING_CONTRIBUTIONS]: OutgoingContributions, - [SECTIONS.HOST_EXPECTED_FUNDS]: HostExpectedFunds, - [SECTIONS.TRANSACTIONS]: AccountTransactions, - [SECTIONS.HOST_TRANSACTIONS]: HostTransactions, - [SECTIONS.UPDATES]: Updates, - [SECTIONS.VIRTUAL_CARDS]: VirtualCards, - [SECTIONS.TEAM]: Team, - [SECTIONS.VENDORS]: Vendors, - [SECTIONS.ACCOUNTS]: Accounts, -}; - -const SETTINGS_COMPONENTS = { - [SETTINGS_SECTIONS.INVOICES_RECEIPTS]: InvoicesReceipts, - [SETTINGS_SECTIONS.NOTIFICATIONS]: NotificationsSettings, - [SETTINGS_SECTIONS.TAX_INFORMATION]: TaxInformationSettingsSection, -}; - -const ROOT_COMPONENTS = { - [SECTIONS.HOST_TRANSACTIONS]: AllTransactions, - [ALL_SECTIONS.ACTIVITY_LOG]: RootActivityLog, - [ROOT_SECTIONS.ALL_COLLECTIVES]: AllCollectives, - [ROOT_SECTIONS.BAN_ACCOUNTS]: BanAccount, - [ROOT_SECTIONS.SEARCH_AND_BAN]: BanAccountsWithSearch, - [ROOT_SECTIONS.MOVE_AUTHORED_CONTRIBUTIONS]: MoveAuthoredContributions, - [ROOT_SECTIONS.MOVE_RECEIVED_CONTRIBUTIONS]: MoveReceivedContributions, - [ROOT_SECTIONS.MOVE_EXPENSES]: MoveExpenses, - [ROOT_SECTIONS.CLEAR_CACHE]: ClearCacheForAccountForm, - [ROOT_SECTIONS.CONNECT_ACCOUNTS]: ConnectAccountsForm, - [ROOT_SECTIONS.MERGE_ACCOUNTS]: MergeAccountsForm, - [ROOT_SECTIONS.UNHOST_ACCOUNTS]: UnhostAccountForm, - [ROOT_SECTIONS.ACCOUNT_SETTINGS]: AccountSettingsForm, - [ROOT_SECTIONS.ACCOUNT_TYPE]: AccountType, - [ROOT_SECTIONS.RECURRING_CONTRIBUTIONS]: RecurringContributions, }; const DashboardSection = ({ account, isLoading, section, subpath }) => { - const { LoggedInUser } = useLoggedInUser(); - const { activeSlug } = useContext(DashboardContext); - - const { formatMessage } = useIntl(); - if (isLoading) { return (
@@ -135,22 +24,8 @@ const DashboardSection = ({ account, isLoading, section, subpath }) => { ); } - const RootComponent = ROOT_COMPONENTS[section]; - if (RootComponent && LoggedInUser.isRoot && activeSlug === ROOT_PROFILE_KEY) { - return ( -
- -
- ); - } - - let DashboardComponent = DASHBOARD_COMPONENTS[section]; + const DashboardComponent = DASHBOARD_COMPONENTS[section]; if (DashboardComponent) { - if (section === SECTIONS.REPORTS) { - if (LoggedInUser.hasPreviewFeatureEnabled(PREVIEW_FEATURE_KEYS.HOST_REPORTS)) { - DashboardComponent = PreviewReports; - } - } return (
@@ -159,42 +34,6 @@ const DashboardSection = ({ account, isLoading, section, subpath }) => { ); } - if (values(LEGACY_SECTIONS).includes(section)) { - return ( -
- - {SECTION_LABELS[section] && } - - -
- ); - } - - // Settings component - const SettingsComponent = SETTINGS_COMPONENTS[section]; - if (SettingsComponent) { - return ( - //
-
- - -
- ); - } - - if (values(LEGACY_SETTINGS_SECTIONS).includes(section)) { - return ( - //
-
- - {SECTION_LABELS[section] && } - - -
- //
- ); - } - return ( diff --git a/components/dashboard/sections/AccountSettings.js b/components/dashboard/sections/AccountSettings.js index 9f16bd62c3d..c03dd2ddcb2 100644 --- a/components/dashboard/sections/AccountSettings.js +++ b/components/dashboard/sections/AccountSettings.js @@ -26,10 +26,10 @@ const AccountSettings = ({ account, section }) => { const { toast } = useToast(); const { data, loading } = useQuery(editCollectivePageQuery, { - variables: { slug: account.slug }, + variables: { slug: account?.slug }, fetchPolicy: 'network-only', ssr: false, - skip: !LoggedInUser, + skip: !LoggedInUser || !account?.slug, }); const collective = data?.Collective; const [editCollective] = useMutation(editCollectivePageMutation); diff --git a/components/dashboard/sections/accounts/index.tsx b/components/dashboard/sections/accounts/index.tsx index 218697cb915..74a5d2e3fb5 100644 --- a/components/dashboard/sections/accounts/index.tsx +++ b/components/dashboard/sections/accounts/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { useQuery } from '@apollo/client'; -import { compact, isString, omit } from 'lodash'; +import { compact, isString } from 'lodash'; import { ChevronDown } from 'lucide-react'; import { useRouter } from 'next/router'; import { FormattedMessage, useIntl } from 'react-intl'; @@ -63,8 +63,9 @@ const Accounts = ({ accountSlug, subpath }: DashboardSectionProps) => { const pushSubpath = subpath => { router.push( { - pathname: compact([router.pathname, router.query.slug, router.query.section, subpath]).join('/'), - query: omit(router.query, ['slug', 'section', 'subpath']), + pathname: subpath + ? `/dashboard/${router.query.slug}/accounts/${subpath}` + : `/dashboard/${router.query.slug}/accounts`, }, undefined, { diff --git a/components/dashboard/sections/collectives/AllCollectives.tsx b/components/dashboard/sections/collectives/AllCollectives.tsx index a07af575317..6d6f4ce766d 100644 --- a/components/dashboard/sections/collectives/AllCollectives.tsx +++ b/components/dashboard/sections/collectives/AllCollectives.tsx @@ -95,15 +95,16 @@ const AllCollectives = ({ subpath }: Omit) const intl = useIntl(); const router = useRouter(); const [showCollectiveOverview, setShowCollectiveOverview] = React.useState( - subpath[0], + subpath?.length > 0 && subpath[0], ); const query = useMemo(() => omit(router.query, ['slug', 'section', 'subpath']), [router.query]); const pushSubpath = subpath => { router.push( { - pathname: compact([router.pathname, router.query.slug, router.query.section, subpath]).join('/'), - query, + pathname: subpath + ? `/dashboard/root-actions/all-collectives/${subpath}` + : `/dashboard/root-actions/all-collectives`, }, undefined, { @@ -127,10 +128,10 @@ const AllCollectives = ({ subpath }: Omit) }); useEffect(() => { - if (subpath[0] !== ((showCollectiveOverview as Collective)?.id || showCollectiveOverview)) { - handleDrawer(subpath[0]); + if (subpath?.[0] !== ((showCollectiveOverview as Collective)?.id || showCollectiveOverview)) { + handleDrawer(subpath?.[0]); } - }, [subpath[0]]); + }, [subpath?.length > 0 && subpath[0]]); const handleDrawer = (collective: Collective | string | undefined) => { if (collective) { diff --git a/components/dashboard/sections/collectives/HostedCollectives.tsx b/components/dashboard/sections/collectives/HostedCollectives.tsx index 398b2550235..091c2ca9b1e 100644 --- a/components/dashboard/sections/collectives/HostedCollectives.tsx +++ b/components/dashboard/sections/collectives/HostedCollectives.tsx @@ -125,8 +125,9 @@ const HostedCollectives = ({ accountSlug: hostSlug, subpath }: DashboardSectionP const pushSubpath = subpath => { router.push( { - pathname: compact([router.pathname, router.query.slug, router.query.section, subpath]).join('/'), - query: omit(router.query, ['slug', 'section', 'subpath']), + pathname: subpath + ? `/dashboard/${router.query.slug}/hosted-collectives/${subpath}` + : `/dashboard/${router.query.slug}/hosted-collectives`, }, undefined, { diff --git a/components/dashboard/sections/contributions/Contributions.tsx b/components/dashboard/sections/contributions/Contributions.tsx index 44494fcaf25..3a76856e395 100644 --- a/components/dashboard/sections/contributions/Contributions.tsx +++ b/components/dashboard/sections/contributions/Contributions.tsx @@ -943,7 +943,7 @@ const getContributionActions: (opts: GetContributionActionsOptions) => GetAction secondary: [], }; - const isAdminOfOrder = opts.LoggedInUser.isAdminOfCollective(order.fromAccount); + const isAdminOfOrder = opts.LoggedInUser && opts.LoggedInUser.isAdminOfCollective(order.fromAccount); const canUpdateActiveOrder = order.frequency !== ContributionFrequency.ONETIME && ![ diff --git a/components/dashboard/sections/expenses/reports/HostExpensesReportView.tsx b/components/dashboard/sections/expenses/reports/HostExpensesReportView.tsx index b97f3229234..9a5a4eadc8d 100644 --- a/components/dashboard/sections/expenses/reports/HostExpensesReportView.tsx +++ b/components/dashboard/sections/expenses/reports/HostExpensesReportView.tsx @@ -13,6 +13,7 @@ import { type HostExpensesReportQueryVariables, } from '../../../../../lib/graphql/types/v2/graphql'; import useQueryFilter from '../../../../../lib/hooks/useQueryFilter'; +import { getWebsiteUrl } from '../../../../../lib/utils'; import FormattedMoneyAmount from '../../../../FormattedMoneyAmount'; import Link from '../../../../Link'; @@ -142,7 +143,7 @@ export function HostExpensesReportView(props: DashboardSectionProps) { item.isHost ? `/dashboard/${props.accountSlug}/expenses` : `/dashboard/${props.accountSlug}/host-expenses`, - window.location.href, + getWebsiteUrl(), ); url.searchParams.set('accountingCategory', item.accountingCategory?.code || UNCATEGORIZED_VALUE); diff --git a/components/dashboard/sections/reports/preview/Reports.tsx b/components/dashboard/sections/reports/preview/Reports.tsx index 6a9f912d533..715cb9d2a84 100644 --- a/components/dashboard/sections/reports/preview/Reports.tsx +++ b/components/dashboard/sections/reports/preview/Reports.tsx @@ -1,5 +1,6 @@ import React from 'react'; +import Loading from '../../../../Loading'; import { DashboardContext } from '../../../DashboardContext'; import type { DashboardSectionProps } from '../../../types'; import { HostExpensesReport } from '../../expenses/reports/HostExpensesReport'; @@ -10,6 +11,10 @@ import HostReports from './HostReports'; const Reports = ({ accountSlug, subpath }: DashboardSectionProps) => { const { account } = React.useContext(DashboardContext); + if (!account) { + return ; + } + const reportType = subpath[0]; if (reportType === 'transactions') { diff --git a/components/dashboard/sections/updates/index.tsx b/components/dashboard/sections/updates/index.tsx index 8ae1e6efdad..841fdc5b42a 100644 --- a/components/dashboard/sections/updates/index.tsx +++ b/components/dashboard/sections/updates/index.tsx @@ -103,9 +103,10 @@ const UpdatesList = () => { error: metadataError, } = useQuery(updatesDashboardMetadataQuery, { variables: { - slug: account.slug, + slug: account?.slug, }, context: API_V2_CONTEXT, + skip: !account?.slug, }); const views: Views> = [ @@ -140,9 +141,10 @@ const UpdatesList = () => { error: queryError, } = useQuery(updatesDashboardQuery, { variables: { - slug: account.slug, + slug: account?.slug, ...queryFilter.variables, }, + skip: !account?.slug, context: API_V2_CONTEXT, }); diff --git a/components/dashboard/sections/updates/queries.ts b/components/dashboard/sections/updates/queries.ts index 9b303d89bf0..e410e9e617a 100644 --- a/components/dashboard/sections/updates/queries.ts +++ b/components/dashboard/sections/updates/queries.ts @@ -130,24 +130,31 @@ export const getRefetchQueries = account => [ { query: updatesDashboardQuery, variables: { - slug: account.slug, + slug: account?.slug, limit: 10, offset: 0, onlyPublishedUpdates: true, orderBy: 'CREATED_AT,DESC', }, + skip: !account?.slug, context: API_V2_CONTEXT, }, { query: updatesDashboardQuery, variables: { - slug: account.slug, + slug: account?.slug, limit: 10, offset: 0, isDraft: true, orderBy: 'CREATED_AT,DESC', }, + skip: !account?.slug, context: API_V2_CONTEXT, }, - { query: updatesDashboardMetadataQuery, variables: { slug: account.slug }, context: API_V2_CONTEXT }, + { + query: updatesDashboardMetadataQuery, + variables: { slug: account?.slug }, + context: API_V2_CONTEXT, + skip: !account?.slug, + }, ]; diff --git a/pages/dashboard/[slug]/accounts/[[...subpath]].tsx b/pages/dashboard/[slug]/accounts/[[...subpath]].tsx new file mode 100644 index 00000000000..d78c595f18d --- /dev/null +++ b/pages/dashboard/[slug]/accounts/[[...subpath]].tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../../components/dashboard/constants'; +import DashboardPage from '../../../../components/dashboard/DashboardPage'; +import Accounts from '../../../../components/dashboard/sections/accounts'; + +export default function AccountsPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/chart-of-accounts.tsx b/pages/dashboard/[slug]/chart-of-accounts.tsx new file mode 100644 index 00000000000..f789bb436e2 --- /dev/null +++ b/pages/dashboard/[slug]/chart-of-accounts.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import { HostAdminAccountingSection } from '../../../components/dashboard/sections/accounting'; + +export default function ChartOfAccountsPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/contributors.tsx b/pages/dashboard/[slug]/contributors.tsx new file mode 100644 index 00000000000..3855d9a4576 --- /dev/null +++ b/pages/dashboard/[slug]/contributors.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import Contributors from '../../../components/dashboard/sections/Contributors'; + +export default function ContributorsPage(props) { + const router = useRouter(); + return ; +} diff --git a/pages/dashboard/[slug]/expected-funds.tsx b/pages/dashboard/[slug]/expected-funds.tsx new file mode 100644 index 00000000000..d66fcf884b9 --- /dev/null +++ b/pages/dashboard/[slug]/expected-funds.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import HostExpectedFunds from '../../../components/dashboard/sections/contributions/HostExpectedFunds'; + +export default function ExpectedFunds(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/expenses.tsx b/pages/dashboard/[slug]/expenses.tsx new file mode 100644 index 00000000000..16054ee5d29 --- /dev/null +++ b/pages/dashboard/[slug]/expenses.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import ReceivedExpenses from '../../../components/dashboard/sections/expenses/ReceivedExpenses'; + +export default function ExpensesPage(props) { + const router = useRouter(); + return ; +} diff --git a/pages/dashboard/[slug]/host-agreements.tsx b/pages/dashboard/[slug]/host-agreements.tsx new file mode 100644 index 00000000000..06689c8586b --- /dev/null +++ b/pages/dashboard/[slug]/host-agreements.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import HostDashboardAgreements from '../../../components/dashboard/sections/HostDashboardAgreements'; + +export default function HostAgreementsPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/host-applications.tsx b/pages/dashboard/[slug]/host-applications.tsx new file mode 100644 index 00000000000..831793f63f1 --- /dev/null +++ b/pages/dashboard/[slug]/host-applications.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import HostApplications from '../../../components/dashboard/sections/collectives/HostApplications'; + +export default function HostApplicationsPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/host-expenses.tsx b/pages/dashboard/[slug]/host-expenses.tsx new file mode 100644 index 00000000000..7f642a55fd8 --- /dev/null +++ b/pages/dashboard/[slug]/host-expenses.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import HostExpenses from '../../../components/dashboard/sections/expenses/HostDashboardExpenses'; + +export default function HostExpensesPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/host-tax-forms.tsx b/pages/dashboard/[slug]/host-tax-forms.tsx new file mode 100644 index 00000000000..485e1a3b8a9 --- /dev/null +++ b/pages/dashboard/[slug]/host-tax-forms.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import HostDashboardTaxForms from '../../../components/dashboard/sections/legal-documents/HostDashboardTaxForms'; + +export default function HostTaxFormsPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/host-transactions/[[...subpath]].tsx b/pages/dashboard/[slug]/host-transactions/[[...subpath]].tsx new file mode 100644 index 00000000000..299589197fb --- /dev/null +++ b/pages/dashboard/[slug]/host-transactions/[[...subpath]].tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../../components/dashboard/constants'; +import DashboardPage from '../../../../components/dashboard/DashboardPage'; +import HostTransactions from '../../../../components/dashboard/sections/transactions/HostTransactions'; + +export default function HostTrasactionsPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/host-virtual-card-requests.tsx b/pages/dashboard/[slug]/host-virtual-card-requests.tsx new file mode 100644 index 00000000000..ef063e4e8b1 --- /dev/null +++ b/pages/dashboard/[slug]/host-virtual-card-requests.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import HostVirtualCardRequests from '../../../components/dashboard/sections/HostVirtualCardRequests'; + +export default function HostVirtualCardRequestsPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/host-virtual-cards.tsx b/pages/dashboard/[slug]/host-virtual-cards.tsx new file mode 100644 index 00000000000..a59425c8311 --- /dev/null +++ b/pages/dashboard/[slug]/host-virtual-cards.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import HostVirtualCards from '../../../components/dashboard/sections/HostVirtualCards'; + +export default function HostVirtualCardsPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/hosted-collectives/[[...subpath]].tsx b/pages/dashboard/[slug]/hosted-collectives/[[...subpath]].tsx new file mode 100644 index 00000000000..29f2cb95c67 --- /dev/null +++ b/pages/dashboard/[slug]/hosted-collectives/[[...subpath]].tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../../components/dashboard/constants'; +import DashboardPage from '../../../../components/dashboard/DashboardPage'; +import HostedCollectives from '../../../../components/dashboard/sections/collectives/HostedCollectives'; + +export default function HostedCollectivesPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/incoming-contributions.tsx b/pages/dashboard/[slug]/incoming-contributions.tsx new file mode 100644 index 00000000000..626d7245aec --- /dev/null +++ b/pages/dashboard/[slug]/incoming-contributions.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import IncomingContributions from '../../../components/dashboard/sections/contributions/IncomingContributions'; + +export default function IncomingContributionsPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/invoices-receipts.tsx b/pages/dashboard/[slug]/invoices-receipts.tsx new file mode 100644 index 00000000000..a9a773d9e95 --- /dev/null +++ b/pages/dashboard/[slug]/invoices-receipts.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { ALL_SECTIONS } from '../../../components/dashboard/constants'; +import { DashboardContext } from '../../../components/dashboard/DashboardContext'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import InvoicesReceipts from '../../../components/dashboard/sections/invoices-receipts/InvoicesReceipts'; + +export default function InvoicesReceiptsPage(props) { + const router = useRouter(); + return ( + + ); +} + +function DashboardComponent() { + const { account } = React.useContext(DashboardContext); + + return ; +} diff --git a/pages/dashboard/[slug]/legacy-settings-sections/[section].tsx b/pages/dashboard/[slug]/legacy-settings-sections/[section].tsx new file mode 100644 index 00000000000..01676198a9e --- /dev/null +++ b/pages/dashboard/[slug]/legacy-settings-sections/[section].tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { values } from 'lodash'; +import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; + +import { LEGACY_SECTIONS, LEGACY_SETTINGS_SECTIONS, SECTION_LABELS } from '../../../../components/dashboard/constants'; +import { DashboardContext } from '../../../../components/dashboard/DashboardContext'; +import DashboardHeader from '../../../../components/dashboard/DashboardHeader'; +import DashboardPage from '../../../../components/dashboard/DashboardPage'; +import AccountSettings from '../../../../components/dashboard/sections/AccountSettings'; +import NotFound from '../../../../components/NotFound'; + +const SECTIONS = [...values(LEGACY_SECTIONS), ...values(LEGACY_SETTINGS_SECTIONS)]; + +export default function LegacySettingsSectionPage(props) { + const router = useRouter(); + const section = router.query.section as string; + + if (!SECTIONS.includes(section)) { + return ; + } + + return ; +} + +function SectionComponent() { + const router = useRouter(); + const section = router.query.section as string; + + const { account } = React.useContext(DashboardContext); + const intl = useIntl(); + + return ( + + + + + ); +} diff --git a/pages/dashboard/[slug]/notifications/[[...subpath]].tsx b/pages/dashboard/[slug]/notifications/[[...subpath]].tsx new file mode 100644 index 00000000000..22245d67972 --- /dev/null +++ b/pages/dashboard/[slug]/notifications/[[...subpath]].tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { ALL_SECTIONS } from '../../../../components/dashboard/constants'; +import DashboardPage from '../../../../components/dashboard/DashboardPage'; +import NotificationsSettings from '../../../../components/dashboard/sections/NotificationsSettings'; + +export default function NotificationSettingsPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/orders.tsx b/pages/dashboard/[slug]/orders.tsx new file mode 100644 index 00000000000..e84c30a5042 --- /dev/null +++ b/pages/dashboard/[slug]/orders.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import HostFinancialContributions from '../../../components/dashboard/sections/contributions/HostFinancialContributions'; + +export default function HostFinancialContributionsPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/outgoing-contributions.tsx b/pages/dashboard/[slug]/outgoing-contributions.tsx new file mode 100644 index 00000000000..afdb1a8ee05 --- /dev/null +++ b/pages/dashboard/[slug]/outgoing-contributions.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import OutgoingContributions from '../../../components/dashboard/sections/contributions/OutgoingContributions'; + +export default function OutgoingContributionsPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/reports/[[...subpath]].tsx b/pages/dashboard/[slug]/reports/[[...subpath]].tsx new file mode 100644 index 00000000000..f9599dcea18 --- /dev/null +++ b/pages/dashboard/[slug]/reports/[[...subpath]].tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import useLoggedInUser from '../../../../lib/hooks/useLoggedInUser'; +import { PREVIEW_FEATURE_KEYS } from '../../../../lib/preview-features'; + +import { SECTIONS } from '../../../../components/dashboard/constants'; +import DashboardPage from '../../../../components/dashboard/DashboardPage'; +import HostDashboardReports from '../../../../components/dashboard/sections/reports/HostDashboardReports'; +import PreviewReports from '../../../../components/dashboard/sections/reports/preview/Reports'; +import type { DashboardSectionProps } from '../../../../components/dashboard/types'; +import Loading from '../../../../components/Loading'; + +export default function ReportsPage(props) { + const router = useRouter(); + const { LoggedInUser } = useLoggedInUser(); + + let Component: React.FC; + + if (!LoggedInUser) { + Component = Loading; + } else if (LoggedInUser.hasPreviewFeatureEnabled(PREVIEW_FEATURE_KEYS.HOST_REPORTS)) { + Component = PreviewReports; + } else { + Component = HostDashboardReports; + } + + return ( + + ); +} diff --git a/pages/dashboard/[slug]/submitted-expenses.tsx b/pages/dashboard/[slug]/submitted-expenses.tsx new file mode 100644 index 00000000000..bfdc354dfaa --- /dev/null +++ b/pages/dashboard/[slug]/submitted-expenses.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import SubmittedExpenses from '../../../components/dashboard/sections/expenses/SubmittedExpenses'; + +export default function SubmittedExpensesPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/tax-information.tsx b/pages/dashboard/[slug]/tax-information.tsx new file mode 100644 index 00000000000..b3258bc1ba8 --- /dev/null +++ b/pages/dashboard/[slug]/tax-information.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { ALL_SECTIONS } from '../../../components/dashboard/constants'; +import { DashboardContext } from '../../../components/dashboard/DashboardContext'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import { TaxInformationSettingsSection } from '../../../components/dashboard/sections/tax-information'; + +export default function TaxInformationPage(props) { + const router = useRouter(); + return ( + + ); +} + +function DashboardComponent() { + const { account } = React.useContext(DashboardContext); + + return ; +} diff --git a/pages/dashboard/[slug]/team.tsx b/pages/dashboard/[slug]/team.tsx new file mode 100644 index 00000000000..9020e6c17bf --- /dev/null +++ b/pages/dashboard/[slug]/team.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import Team from '../../../components/dashboard/sections/Team'; + +export default function TeamPage(props) { + const router = useRouter(); + return ; +} diff --git a/pages/dashboard/[slug]/tickets.tsx b/pages/dashboard/[slug]/tickets.tsx new file mode 100644 index 00000000000..2af7743365b --- /dev/null +++ b/pages/dashboard/[slug]/tickets.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; + +import { ALL_SECTIONS, SECTION_LABELS } from '../../../components/dashboard/constants'; +import { DashboardContext } from '../../../components/dashboard/DashboardContext'; +import DashboardHeader from '../../../components/dashboard/DashboardHeader'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import AccountSettings from '../../../components/dashboard/sections/AccountSettings'; + +export default function TiersPage(props) { + const router = useRouter(); + + return ( + + ); +} + +function TicketsComponent() { + const { account } = React.useContext(DashboardContext); + const intl = useIntl(); + + return ( + + + + + ); +} diff --git a/pages/dashboard/[slug]/tiers.tsx b/pages/dashboard/[slug]/tiers.tsx new file mode 100644 index 00000000000..c9457d56296 --- /dev/null +++ b/pages/dashboard/[slug]/tiers.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useRouter } from 'next/router'; +import { useIntl } from 'react-intl'; + +import { ALL_SECTIONS, SECTION_LABELS } from '../../../components/dashboard/constants'; +import { DashboardContext } from '../../../components/dashboard/DashboardContext'; +import DashboardHeader from '../../../components/dashboard/DashboardHeader'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import AccountSettings from '../../../components/dashboard/sections/AccountSettings'; + +export default function TiersPage(props) { + const router = useRouter(); + + return ; +} + +function TierComponent() { + const { account } = React.useContext(DashboardContext); + const intl = useIntl(); + + return ( + + + + + ); +} diff --git a/pages/dashboard/[slug]/transactions.tsx b/pages/dashboard/[slug]/transactions.tsx new file mode 100644 index 00000000000..c871aa8c6e0 --- /dev/null +++ b/pages/dashboard/[slug]/transactions.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import AccountTransactions from '../../../components/dashboard/sections/transactions/AccountTransactions'; + +export default function TransactionsPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/updates/[[...subpath]].tsx b/pages/dashboard/[slug]/updates/[[...subpath]].tsx new file mode 100644 index 00000000000..e32f44b7f88 --- /dev/null +++ b/pages/dashboard/[slug]/updates/[[...subpath]].tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../../components/dashboard/constants'; +import DashboardPage from '../../../../components/dashboard/DashboardPage'; +import Updates from '../../../../components/dashboard/sections/updates'; + +export default function UpdatesPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard/[slug]/vendors.tsx b/pages/dashboard/[slug]/vendors.tsx new file mode 100644 index 00000000000..b6eaf4aa449 --- /dev/null +++ b/pages/dashboard/[slug]/vendors.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import Vendors from '../../../components/dashboard/sections/Vendors'; + +export default function AccountsPage(props) { + const router = useRouter(); + return ; +} diff --git a/pages/dashboard/[slug]/virtual-cards.tsx b/pages/dashboard/[slug]/virtual-cards.tsx new file mode 100644 index 00000000000..e2a64b81f1c --- /dev/null +++ b/pages/dashboard/[slug]/virtual-cards.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import VirtualCards from '../../../components/dashboard/sections/virtual-cards/VirtualCards'; + +export default function VirtualCardsPage(props) { + const router = useRouter(); + return ( + + ); +} diff --git a/pages/dashboard.tsx b/pages/dashboard/index.tsx similarity index 87% rename from pages/dashboard.tsx rename to pages/dashboard/index.tsx index 3fa32cd0b67..a513eb1583d 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard/index.tsx @@ -4,14 +4,14 @@ import { clsx } from 'clsx'; import { useRouter } from 'next/router'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { isHostAccount, isIndividualAccount } from '../lib/collective'; -import roles from '../lib/constants/roles'; -import { API_V2_CONTEXT } from '../lib/graphql/helpers'; -import useLocalStorage from '../lib/hooks/useLocalStorage'; -import useLoggedInUser from '../lib/hooks/useLoggedInUser'; -import { LOCAL_STORAGE_KEYS } from '../lib/local-storage'; -import { require2FAForAdmins } from '../lib/policies'; -import { PREVIEW_FEATURE_KEYS } from '../lib/preview-features'; +import { isHostAccount, isIndividualAccount } from '../../lib/collective'; +import roles from '../../lib/constants/roles'; +import { API_V2_CONTEXT } from '../../lib/graphql/helpers'; +import useLocalStorage from '../../lib/hooks/useLocalStorage'; +import useLoggedInUser from '../../lib/hooks/useLoggedInUser'; +import { LOCAL_STORAGE_KEYS } from '../../lib/local-storage'; +import { require2FAForAdmins } from '../../lib/policies'; +import { PREVIEW_FEATURE_KEYS } from '../../lib/preview-features'; import { ALL_SECTIONS, @@ -19,21 +19,21 @@ import { ROOT_PROFILE_KEY, ROOT_SECTIONS, SECTIONS_ACCESSIBLE_TO_ACCOUNTANTS, -} from '../components/dashboard/constants'; -import { DashboardContext } from '../components/dashboard/DashboardContext'; -import DashboardSection from '../components/dashboard/DashboardSection'; -import { getMenuItems } from '../components/dashboard/Menu'; -import DashboardTopBar from '../components/dashboard/preview/DashboardTopBar'; -import SubMenu from '../components/dashboard/preview/SubMenu'; -import { adminPanelQuery } from '../components/dashboard/queries'; -import AdminPanelSideBar from '../components/dashboard/SideBar'; -import Link from '../components/Link'; -import MessageBox from '../components/MessageBox'; -import Footer from '../components/navigation/Footer'; -import NotificationBar from '../components/NotificationBar'; -import Page from '../components/Page'; -import SignInOrJoinFree from '../components/SignInOrJoinFree'; -import { TwoFactorAuthRequiredMessage } from '../components/TwoFactorAuthRequiredMessage'; +} from '../../components/dashboard/constants'; +import { DashboardContext } from '../../components/dashboard/DashboardContext'; +import DashboardSection from '../../components/dashboard/DashboardSection'; +import { getMenuItems } from '../../components/dashboard/Menu'; +import DashboardTopBar from '../../components/dashboard/preview/DashboardTopBar'; +import SubMenu from '../../components/dashboard/preview/SubMenu'; +import { adminPanelQuery } from '../../components/dashboard/queries'; +import AdminPanelSideBar from '../../components/dashboard/SideBar'; +import Link from '../../components/Link'; +import MessageBox from '../../components/MessageBox'; +import Footer from '../../components/navigation/Footer'; +import NotificationBar from '../../components/NotificationBar'; +import Page from '../../components/Page'; +import SignInOrJoinFree from '../../components/SignInOrJoinFree'; +import { TwoFactorAuthRequiredMessage } from '../../components/TwoFactorAuthRequiredMessage'; const messages = defineMessages({ collectiveIsArchived: { @@ -54,6 +54,7 @@ const messages = defineMessages({ }, }); +// TODO[henrique]: make this work with a redirect const getDefaultSectionForAccount = (account, loggedInUser) => { if (!account) { return null; diff --git a/pages/dashboard/root/account-settings.tsx b/pages/dashboard/root/account-settings.tsx new file mode 100644 index 00000000000..19f50f15a86 --- /dev/null +++ b/pages/dashboard/root/account-settings.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import useLoggedInUser from '../../../lib/hooks/useLoggedInUser'; + +import { ROOT_SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import Loading from '../../../components/Loading'; +import AccountSettings from '../../../components/root-actions/AccountSettings'; + +export default function RootAccountSettingsPage(props) { + const { LoggedInUser } = useLoggedInUser(); + + if (LoggedInUser && LoggedInUser.isRoot) { + return ( + + ); + } else { + return ( + + ); + } +} diff --git a/pages/dashboard/root/account-type.tsx b/pages/dashboard/root/account-type.tsx new file mode 100644 index 00000000000..e554d058271 --- /dev/null +++ b/pages/dashboard/root/account-type.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import useLoggedInUser from '../../../lib/hooks/useLoggedInUser'; + +import { ROOT_SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import Loading from '../../../components/Loading'; +import AccountType from '../../../components/root-actions/AccountType'; + +export default function RootAccountTypePage(props) { + const { LoggedInUser } = useLoggedInUser(); + + if (LoggedInUser && LoggedInUser.isRoot) { + return ( + + ); + } else { + return ; + } +} diff --git a/pages/dashboard/root/activity-log.tsx b/pages/dashboard/root/activity-log.tsx new file mode 100644 index 00000000000..711fd9e601e --- /dev/null +++ b/pages/dashboard/root/activity-log.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import useLoggedInUser from '../../../lib/hooks/useLoggedInUser'; + +import { ALL_SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import Loading from '../../../components/Loading'; +import RootActivityLog from '../../../components/root-actions/RootActivityLog'; + +export default function RootHostTransactionsPage(props) { + const { LoggedInUser } = useLoggedInUser(); + + if (LoggedInUser && LoggedInUser.isRoot) { + return ( + + ); + } else { + return ; + } +} diff --git a/pages/dashboard/root/all-collectives.tsx b/pages/dashboard/root/all-collectives.tsx new file mode 100644 index 00000000000..d9f7aa73020 --- /dev/null +++ b/pages/dashboard/root/all-collectives.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import useLoggedInUser from '../../../lib/hooks/useLoggedInUser'; + +import { ROOT_SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import AllCollectives from '../../../components/dashboard/sections/collectives/AllCollectives'; +import Loading from '../../../components/Loading'; + +export default function RootAllCollectivesPage(props) { + const router = useRouter(); + const subpath = router.query.subpath; + const { LoggedInUser } = useLoggedInUser(); + + if (LoggedInUser && LoggedInUser.isRoot) { + return ( + + ); + } else { + return ( + + ); + } +} diff --git a/pages/dashboard/root/ban-account.tsx b/pages/dashboard/root/ban-account.tsx new file mode 100644 index 00000000000..12bdde28526 --- /dev/null +++ b/pages/dashboard/root/ban-account.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import useLoggedInUser from '../../../lib/hooks/useLoggedInUser'; + +import { ROOT_SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import Loading from '../../../components/Loading'; +import BanAccount from '../../../components/root-actions/BanAccounts'; + +export default function RootBanAccountPage(props) { + const { LoggedInUser } = useLoggedInUser(); + + if (LoggedInUser && LoggedInUser.isRoot) { + return ( + + ); + } else { + return ; + } +} diff --git a/pages/dashboard/root/clear-cache.tsx b/pages/dashboard/root/clear-cache.tsx new file mode 100644 index 00000000000..19f20c9dea3 --- /dev/null +++ b/pages/dashboard/root/clear-cache.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import useLoggedInUser from '../../../lib/hooks/useLoggedInUser'; + +import { ROOT_SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import Loading from '../../../components/Loading'; +import ClearCacheForAccountForm from '../../../components/root-actions/ClearCacheForAccountForm'; + +export default function RootClearCachePage(props) { + const { LoggedInUser } = useLoggedInUser(); + + if (LoggedInUser && LoggedInUser.isRoot) { + return ( + + ); + } else { + return ; + } +} diff --git a/pages/dashboard/root/connect-accounts.tsx b/pages/dashboard/root/connect-accounts.tsx new file mode 100644 index 00000000000..9a5dfcf80d8 --- /dev/null +++ b/pages/dashboard/root/connect-accounts.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import useLoggedInUser from '../../../lib/hooks/useLoggedInUser'; + +import { ROOT_SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import Loading from '../../../components/Loading'; +import ConnectAccountsForm from '../../../components/root-actions/ConnectAccountsForm'; + +export default function RootConnectAccountsPage(props) { + const { LoggedInUser } = useLoggedInUser(); + + if (LoggedInUser && LoggedInUser.isRoot) { + return ( + + ); + } else { + return ( + + ); + } +} diff --git a/pages/dashboard/root/host-transactions.tsx b/pages/dashboard/root/host-transactions.tsx new file mode 100644 index 00000000000..b3226d7c0fd --- /dev/null +++ b/pages/dashboard/root/host-transactions.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import useLoggedInUser from '../../../lib/hooks/useLoggedInUser'; + +import { SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import AllTransactions from '../../../components/dashboard/sections/transactions/AllTransactions'; +import Loading from '../../../components/Loading'; + +export default function RootHostTransactionsPage(props) { + const { LoggedInUser } = useLoggedInUser(); + + if (LoggedInUser && LoggedInUser.isRoot) { + return ( + + ); + } else { + return ; + } +} diff --git a/pages/dashboard/root/merge-accounts.tsx b/pages/dashboard/root/merge-accounts.tsx new file mode 100644 index 00000000000..406083c0ddd --- /dev/null +++ b/pages/dashboard/root/merge-accounts.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import useLoggedInUser from '../../../lib/hooks/useLoggedInUser'; + +import { ROOT_SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import Loading from '../../../components/Loading'; +import MergeAccountsForm from '../../../components/root-actions/MergeAccountsForm'; + +export default function RootMergeAccountsPage(props) { + const { LoggedInUser } = useLoggedInUser(); + + if (LoggedInUser && LoggedInUser.isRoot) { + return ( + + ); + } else { + return ( + + ); + } +} diff --git a/pages/dashboard/root/move-authored-contributions.tsx b/pages/dashboard/root/move-authored-contributions.tsx new file mode 100644 index 00000000000..73e56aed512 --- /dev/null +++ b/pages/dashboard/root/move-authored-contributions.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +import useLoggedInUser from '../../../lib/hooks/useLoggedInUser'; + +import { ROOT_SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import Loading from '../../../components/Loading'; +import MoveAuthoredContributions from '../../../components/root-actions/MoveAuthoredContributions'; + +export default function RootMoveAuthoredContributionsPage(props) { + const { LoggedInUser } = useLoggedInUser(); + + if (LoggedInUser && LoggedInUser.isRoot) { + return ( + + ); + } else { + return ( + + ); + } +} diff --git a/pages/dashboard/root/move-expenses.tsx b/pages/dashboard/root/move-expenses.tsx new file mode 100644 index 00000000000..2ed3d2375de --- /dev/null +++ b/pages/dashboard/root/move-expenses.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import useLoggedInUser from '../../../lib/hooks/useLoggedInUser'; + +import { ROOT_SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import Loading from '../../../components/Loading'; +import MoveExpenses from '../../../components/root-actions/MoveExpenses'; + +export default function RootMoveExpensesPage(props) { + const { LoggedInUser } = useLoggedInUser(); + + if (LoggedInUser && LoggedInUser.isRoot) { + return ( + + ); + } else { + return ; + } +} diff --git a/pages/dashboard/root/move-received-contributions.tsx b/pages/dashboard/root/move-received-contributions.tsx new file mode 100644 index 00000000000..17b390a2e63 --- /dev/null +++ b/pages/dashboard/root/move-received-contributions.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +import useLoggedInUser from '../../../lib/hooks/useLoggedInUser'; + +import { ROOT_SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import Loading from '../../../components/Loading'; +import MoveAuthoredContributions from '../../../components/root-actions/MoveAuthoredContributions'; + +export default function RootMoveReceivedContributionsPage(props) { + const { LoggedInUser } = useLoggedInUser(); + + if (LoggedInUser && LoggedInUser.isRoot) { + return ( + + ); + } else { + return ( + + ); + } +} diff --git a/pages/dashboard/root/recurring-contributions.tsx b/pages/dashboard/root/recurring-contributions.tsx new file mode 100644 index 00000000000..ad41edfcfa6 --- /dev/null +++ b/pages/dashboard/root/recurring-contributions.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +import useLoggedInUser from '../../../lib/hooks/useLoggedInUser'; + +import { ROOT_SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import Loading from '../../../components/Loading'; +import RecurringContributions from '../../../components/root-actions/RecurringContributions'; + +export default function RootRecurringContributionsPage(props) { + const { LoggedInUser } = useLoggedInUser(); + + if (LoggedInUser && LoggedInUser.isRoot) { + return ( + + ); + } else { + return ( + + ); + } +} diff --git a/pages/dashboard/root/search-and-ban.tsx b/pages/dashboard/root/search-and-ban.tsx new file mode 100644 index 00000000000..bc2277c1bdf --- /dev/null +++ b/pages/dashboard/root/search-and-ban.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import useLoggedInUser from '../../../lib/hooks/useLoggedInUser'; + +import { ROOT_SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import Loading from '../../../components/Loading'; +import BanAccountsWithSearch from '../../../components/root-actions/BanAccountsWithSearch'; + +export default function RootSearchAndBanPage(props) { + const { LoggedInUser } = useLoggedInUser(); + + if (LoggedInUser && LoggedInUser.isRoot) { + return ( + + ); + } else { + return ( + + ); + } +} diff --git a/pages/dashboard/root/unhost-accounts.tsx b/pages/dashboard/root/unhost-accounts.tsx new file mode 100644 index 00000000000..cf81b86f7ae --- /dev/null +++ b/pages/dashboard/root/unhost-accounts.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import useLoggedInUser from '../../../lib/hooks/useLoggedInUser'; + +import { ROOT_SECTIONS } from '../../../components/dashboard/constants'; +import DashboardPage from '../../../components/dashboard/DashboardPage'; +import Loading from '../../../components/Loading'; +import UnhostAccountForm from '../../../components/root-actions/UnhostAccountForm'; + +export default function RootUnhostAccountsPage(props) { + const { LoggedInUser } = useLoggedInUser(); + + if (LoggedInUser && LoggedInUser.isRoot) { + return ( + + ); + } else { + return ( + + ); + } +} diff --git a/rewrites.js b/rewrites.js index e7cd135692c..8bdb392aabb 100644 --- a/rewrites.js +++ b/rewrites.js @@ -59,6 +59,21 @@ exports.REWRITES = [ destination: '/dashboard', }, { source: '/workspace', destination: '/dashboard' }, + { + source: + '/dashboard/root-actions/:section(recurring-contributions|account-type|account-settings|unhost-accounts|merge-accounts|connect-accounts|clear-cache|move-expenses|move-received-contributions|move-authored-contributions|search-and-ban|ban-account|all-collectives|host-transactions|activity-log)/:subpath*', + destination: '/dashboard/root/:section', + }, + { + source: + '/dashboard/:slug/:section(expected-funds|contributors|vendors|team|host-expenses|host-agreements|host-applications|host-tax-forms|expenses|submitted-expenses|host-virtual-cards|host-virtual-card-requests|incoming-contributions|outgoing-contributions|transactions|virtual-cards|orders|chart-of-accounts|reports|hosted-collectives|updates|host-transactions|accounts|invoices-receipts|tax-information|notifications)/:subpath*', + destination: '/dashboard/:slug/:section/:subpath*', + }, + { + source: + '/dashboard/:slug/:section(goals|connected-accounts|export|for-developers|user-security|custom-email|activity-log|collective-page|authorized-apps|advanced|webhooks|sending-money|policies|receiving-money|tiers|tickets|host|info|gift-cards|gift-cards-create|payment-methods|payment-receipts|fiscal-hosting|security|host-virtual-cards-settings)/:subpath*', + destination: '/dashboard/:slug/legacy-settings-sections/:section', + }, { source: '/dashboard/:slug/:section?/:subpath*', destination: '/dashboard', From eecc8a2efff961895d921302d18c1347cff6f07e Mon Sep 17 00:00:00 2001 From: Henrique Date: Tue, 3 Sep 2024 09:06:36 -0300 Subject: [PATCH 2/9] wip --- components/dashboard/AccountSwitcher.tsx | 12 +- components/dashboard/DashboardPage.tsx | 66 ++-- components/dashboard/DashboardSection.tsx | 56 --- components/dashboard/Menu.tsx | 4 +- .../dashboard/sections/overview/Overview.tsx | 7 +- .../[slug]/overview/[[...subpath]].tsx | 11 + pages/dashboard/index.tsx | 336 ++++-------------- rewrites.js | 10 +- 8 files changed, 127 insertions(+), 375 deletions(-) delete mode 100644 components/dashboard/DashboardSection.tsx create mode 100644 pages/dashboard/[slug]/overview/[[...subpath]].tsx diff --git a/components/dashboard/AccountSwitcher.tsx b/components/dashboard/AccountSwitcher.tsx index 82d0e43a36b..84d842cf329 100644 --- a/components/dashboard/AccountSwitcher.tsx +++ b/components/dashboard/AccountSwitcher.tsx @@ -15,6 +15,7 @@ import Avatar from '../Avatar'; import Container from '../Container'; import { Flex } from '../Grid'; import Link from '../Link'; +import LoadingPlaceholder from '../LoadingPlaceholder'; import { Dropdown, DropdownContent } from '../StyledDropdown'; import StyledHr from '../StyledHr'; import StyledRoundButton from '../StyledRoundButton'; @@ -255,7 +256,16 @@ const AccountSwitcher = ({ activeSlug }: { activeSlug: string }) => { - {activeSlug === ROOT_PROFILE_KEY ? : diff --git a/components/dashboard/DashboardPage.tsx b/components/dashboard/DashboardPage.tsx index 6978e46e76b..499ac153788 100644 --- a/components/dashboard/DashboardPage.tsx +++ b/components/dashboard/DashboardPage.tsx @@ -1,23 +1,17 @@ import React from 'react'; import { useQuery } from '@apollo/client'; import { clsx } from 'clsx'; -import { useRouter } from 'next/router'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { isHostAccount, isIndividualAccount } from '../../lib/collective'; import roles from '../../lib/constants/roles'; import { API_V2_CONTEXT } from '../../lib/graphql/helpers'; -import useLocalStorage from '../../lib/hooks/useLocalStorage'; import useLoggedInUser from '../../lib/hooks/useLoggedInUser'; -import { LOCAL_STORAGE_KEYS } from '../../lib/local-storage'; import { require2FAForAdmins } from '../../lib/policies'; import { PREVIEW_FEATURE_KEYS } from '../../lib/preview-features'; import { - ALL_SECTIONS, ROOT_PROFILE_ACCOUNT, ROOT_PROFILE_KEY, - ROOT_SECTIONS, SECTIONS_ACCESSIBLE_TO_ACCOUNTANTS, } from '../../components/dashboard/constants'; import { DashboardContext } from '../../components/dashboard/DashboardContext'; @@ -57,26 +51,11 @@ const messages = defineMessages({ }, }); -const getDefaultSectionForAccount = (account, loggedInUser) => { +const getNotification = (intl, account) => { if (!account) { - return null; - } else if (account.type === 'ROOT') { - return ROOT_SECTIONS.ALL_COLLECTIVES; - } else if ( - isIndividualAccount(account) || - (!isHostAccount(account) && loggedInUser.hasPreviewFeatureEnabled(PREVIEW_FEATURE_KEYS.COLLECTIVE_OVERVIEW)) - ) { - return ALL_SECTIONS.OVERVIEW; - } else if (isHostAccount(account)) { - return ALL_SECTIONS.HOST_EXPENSES; - } else { - const isAdmin = loggedInUser?.isAdminOfCollective(account); - const isAccountant = loggedInUser?.hasRole(roles.ACCOUNTANT, account); - return !isAdmin && isAccountant ? ALL_SECTIONS.PAYMENT_RECEIPTS : ALL_SECTIONS.EXPENSES; + return; } -}; -const getNotification = (intl, account) => { if (account?.isArchived) { if (account.type === 'USER') { return { @@ -96,8 +75,10 @@ const getNotification = (intl, account) => { } }; -function getBlocker(LoggedInUser, account, section) { - if (!LoggedInUser) { +function getBlocker(LoggedInUser, activeSlug, account, section) { + if (!activeSlug) { + return; + } else if (!LoggedInUser) { return ; } else if (!account) { return ; @@ -123,24 +104,28 @@ function getBlocker(LoggedInUser, account, section) { } } +function setLastWorkspaceVisit(value: { slug: string }) { + if (typeof document !== 'undefined') { + const val = JSON.stringify(value); + document.cookie = `lastWorkspaceVisit=${val};Max-Age=9999999;secure;path=/`; + } +} + export default function DashboardPage(props: { Component: React.FC; slug: string; section: string; subpath?: string[]; + isIndex?: boolean; }) { const intl = useIntl(); - const router = useRouter(); const slug = props.slug; + const section = props.section; const subpath = props.subpath; const { LoggedInUser, loadingLoggedInUser } = useLoggedInUser(); - const [lastWorkspaceVisit, setLastWorkspaceVisit] = useLocalStorage(LOCAL_STORAGE_KEYS.DASHBOARD_NAVIGATION_STATE, { - slug: LoggedInUser?.collective.slug, - }); const isRootUser = LoggedInUser?.isRoot; - const defaultSlug = lastWorkspaceVisit.slug || LoggedInUser?.collective.slug; - const activeSlug = slug || defaultSlug; + const activeSlug = slug; const isRootProfile = activeSlug === ROOT_PROFILE_KEY; const { data, loading } = useQuery(adminPanelQuery, { @@ -149,38 +134,31 @@ export default function DashboardPage(props: { skip: !activeSlug || !LoggedInUser || isRootProfile, }); const account = isRootProfile && isRootUser ? ROOT_PROFILE_ACCOUNT : data?.account; - const selectedSection = section || getDefaultSectionForAccount(account, LoggedInUser); + const selectedSection = section; const useDynamicTopBar = LoggedInUser?.hasPreviewFeatureEnabled(PREVIEW_FEATURE_KEYS.DYNAMIC_TOP_BAR); // Keep track of last visited workspace account and sections React.useEffect(() => { - if (activeSlug && activeSlug !== lastWorkspaceVisit.slug) { + if (activeSlug) { if (LoggedInUser && !useDynamicTopBar) { // this is instead configured as "default" account in NewAccountSwitcher setLastWorkspaceVisit({ slug: activeSlug }); } } - // If there is no slug set (that means /dashboard) - // And if there is an activeSlug (this means lastWorkspaceVisit OR LoggedInUser) - // And a LoggedInUser - // And if activeSlug is different than LoggedInUser slug - if (!slug && activeSlug && LoggedInUser && activeSlug !== LoggedInUser.collective.slug) { - router.replace(`/dashboard/${activeSlug}`); - } - }, [activeSlug, LoggedInUser]); + }, [activeSlug, LoggedInUser, useDynamicTopBar]); // Clear last visited workspace account if not admin React.useEffect(() => { if (account && !LoggedInUser.isAdminOfCollective(account)) { setLastWorkspaceVisit({ slug: null }); } - }, [account]); + }, [account, LoggedInUser]); const notification = getNotification(intl, account); const [expandedSection, setExpandedSection] = React.useState(null); const isLoading = loading || loadingLoggedInUser; - const blocker = !isLoading && getBlocker(LoggedInUser, account, selectedSection); + const blocker = !isLoading && getBlocker(LoggedInUser, activeSlug, account, selectedSection); const titleBase = intl.formatMessage({ id: 'Dashboard', defaultMessage: 'Dashboard' }); const menuItems = account ? getMenuItems({ intl, account, LoggedInUser }) : []; const accountIdentifier = account && (account.name || `@${account.slug}`); @@ -202,7 +180,7 @@ export default function DashboardPage(props: { setExpandedSection, account, activeSlug, - defaultSlug, + defaultSlug: slug, setDefaultSlug: slug => setLastWorkspaceVisit({ slug }), }} > diff --git a/components/dashboard/DashboardSection.tsx b/components/dashboard/DashboardSection.tsx deleted file mode 100644 index 0b41cab8938..00000000000 --- a/components/dashboard/DashboardSection.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import Container from '../Container'; -import LoadingPlaceholder from '../LoadingPlaceholder'; -import NotFound from '../NotFound'; -import { OCFBannerWithData } from '../OCFBanner'; - -import Overview from './sections/overview/Overview'; -import { SECTIONS } from './constants'; - -const DASHBOARD_COMPONENTS = { - [SECTIONS.OVERVIEW]: Overview, -}; - -const DashboardSection = ({ account, isLoading, section, subpath }) => { - if (isLoading) { - return ( -
- - - -
- ); - } - - const DashboardComponent = DASHBOARD_COMPONENTS[section]; - if (DashboardComponent) { - return ( -
- - -
- ); - } - - return ( - - - - ); -}; - -DashboardSection.propTypes = { - isLoading: PropTypes.bool, - section: PropTypes.string, - subpath: PropTypes.arrayOf(PropTypes.string), - /** The account. Can be null if isLoading is true */ - account: PropTypes.shape({ - slug: PropTypes.string.isRequired, - name: PropTypes.string, - isHost: PropTypes.bool, - }), -}; - -export default DashboardSection; diff --git a/components/dashboard/Menu.tsx b/components/dashboard/Menu.tsx index 1b363896932..50e3b6f09cc 100644 --- a/components/dashboard/Menu.tsx +++ b/components/dashboard/Menu.tsx @@ -496,12 +496,12 @@ const Menu = ({ onRoute, menuItems }) => { }, [router, onRoute]); const showLinkToProfilePrototype = - !['ROOT', 'ORGANIZATION', 'FUND', 'INDIVIDUAL'].includes(account.type) && + !['ROOT', 'ORGANIZATION', 'FUND', 'INDIVIDUAL'].includes(account?.type) && LoggedInUser?.hasPreviewFeatureEnabled(PREVIEW_FEATURE_KEYS.COLLECTIVE_OVERVIEW); return (
- {account.type !== 'ROOT' && ( + {account && account.type !== 'ROOT' && (
; + } + if (isIndividualAccount(account)) { - return ; + return ; } return ; diff --git a/pages/dashboard/[slug]/overview/[[...subpath]].tsx b/pages/dashboard/[slug]/overview/[[...subpath]].tsx new file mode 100644 index 00000000000..4a30651801c --- /dev/null +++ b/pages/dashboard/[slug]/overview/[[...subpath]].tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SECTIONS } from '../../../../components/dashboard/constants'; +import DashboardPage from '../../../../components/dashboard/DashboardPage'; +import Overview from '../../../../components/dashboard/sections/overview/Overview'; + +export default function DashboardOverviewPage(props) { + const router = useRouter(); + return ; +} diff --git a/pages/dashboard/index.tsx b/pages/dashboard/index.tsx index a513eb1583d..301f056f4da 100644 --- a/pages/dashboard/index.tsx +++ b/pages/dashboard/index.tsx @@ -1,61 +1,60 @@ import React from 'react'; import { useQuery } from '@apollo/client'; -import { clsx } from 'clsx'; +import type { NextPageContext } from 'next'; import { useRouter } from 'next/router'; -import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { isHostAccount, isIndividualAccount } from '../../lib/collective'; import roles from '../../lib/constants/roles'; import { API_V2_CONTEXT } from '../../lib/graphql/helpers'; -import useLocalStorage from '../../lib/hooks/useLocalStorage'; import useLoggedInUser from '../../lib/hooks/useLoggedInUser'; -import { LOCAL_STORAGE_KEYS } from '../../lib/local-storage'; -import { require2FAForAdmins } from '../../lib/policies'; import { PREVIEW_FEATURE_KEYS } from '../../lib/preview-features'; -import { - ALL_SECTIONS, - ROOT_PROFILE_ACCOUNT, - ROOT_PROFILE_KEY, - ROOT_SECTIONS, - SECTIONS_ACCESSIBLE_TO_ACCOUNTANTS, -} from '../../components/dashboard/constants'; -import { DashboardContext } from '../../components/dashboard/DashboardContext'; -import DashboardSection from '../../components/dashboard/DashboardSection'; -import { getMenuItems } from '../../components/dashboard/Menu'; -import DashboardTopBar from '../../components/dashboard/preview/DashboardTopBar'; -import SubMenu from '../../components/dashboard/preview/SubMenu'; +import { ALL_SECTIONS, ROOT_SECTIONS } from '../../components/dashboard/constants'; +import DashboardPage from '../../components/dashboard/DashboardPage'; import { adminPanelQuery } from '../../components/dashboard/queries'; -import AdminPanelSideBar from '../../components/dashboard/SideBar'; -import Link from '../../components/Link'; -import MessageBox from '../../components/MessageBox'; -import Footer from '../../components/navigation/Footer'; -import NotificationBar from '../../components/NotificationBar'; -import Page from '../../components/Page'; -import SignInOrJoinFree from '../../components/SignInOrJoinFree'; -import { TwoFactorAuthRequiredMessage } from '../../components/TwoFactorAuthRequiredMessage'; +import type { DashboardSectionProps } from '../../components/dashboard/types'; +import MessageBoxGraphqlError from '../../components/MessageBoxGraphqlError'; + +// TODO: what we ideally want from the dashboard index page is a HTTP redirect +// based on the authentication state and user preference +export default function DashboardIndexPage({ lastWorkspaceVisit }) { + const Component: React.FC = () => ( + + ); + + return ; +} + +function DashboardComponent({ lastWorkspaceVisit }) { + const router = useRouter(); + + const { LoggedInUser, loadingLoggedInUser } = useLoggedInUser(); + + const defaultSlug = lastWorkspaceVisit?.slug || LoggedInUser?.collective.slug; + const activeSlug = router.query.slug || defaultSlug; + + const { data, loading, error } = useQuery(adminPanelQuery, { + context: API_V2_CONTEXT, + variables: { slug: activeSlug }, + skip: !activeSlug || !LoggedInUser, + }); + + const account = data?.account; + + const defaultSection = getDefaultSectionForAccount(account, LoggedInUser); + + React.useEffect(() => { + if (!activeSlug || !LoggedInUser) { + router.replace('/'); + } else if (account) { + router.replace(`/dashboard/${activeSlug}/${defaultSection}`); + } + }, [loading, loadingLoggedInUser, LoggedInUser, activeSlug, defaultSection, account, router]); -const messages = defineMessages({ - collectiveIsArchived: { - id: 'collective.isArchived', - defaultMessage: '{name} has been archived.', - }, - collectiveIsArchivedDescription: { - id: 'collective.isArchived.edit.description', - defaultMessage: 'This {type} has been archived and is no longer active.', - }, - userIsArchived: { - id: 'user.isArchived', - defaultMessage: 'Account has been archived.', - }, - userIsArchivedDescription: { - id: 'user.isArchived.edit.description', - defaultMessage: 'This account has been archived and is no longer active.', - }, -}); + return error ? :
; +} -// TODO[henrique]: make this work with a redirect -const getDefaultSectionForAccount = (account, loggedInUser) => { +function getDefaultSectionForAccount(account, loggedInUser) { if (!account) { return null; } else if (account.type === 'ROOT') { @@ -72,238 +71,39 @@ const getDefaultSectionForAccount = (account, loggedInUser) => { const isAccountant = loggedInUser?.hasRole(roles.ACCOUNTANT, account); return !isAdmin && isAccountant ? ALL_SECTIONS.PAYMENT_RECEIPTS : ALL_SECTIONS.EXPENSES; } -}; - -const getNotification = (intl, account) => { - if (account?.isArchived) { - if (account.type === 'USER') { - return { - type: 'warning', - title: intl.formatMessage(messages.userIsArchived), - description: intl.formatMessage(messages.userIsArchivedDescription), - }; - } else { - return { - type: 'warning', - title: intl.formatMessage(messages.collectiveIsArchived, { name: account.name }), - description: intl.formatMessage(messages.collectiveIsArchivedDescription, { - type: account.type.toLowerCase(), - }), - }; - } - } -}; - -function getBlocker(LoggedInUser, account, section) { - if (!LoggedInUser) { - return ; - } else if (!account) { - return ; - } else if (account.isIncognito) { - return ; - } else if (account.type === 'ROOT' && LoggedInUser.isRoot) { - return; - } - - // Check permissions - const isAdmin = LoggedInUser.isAdminOfCollective(account); - if (SECTIONS_ACCESSIBLE_TO_ACCOUNTANTS.includes(section)) { - if (!isAdmin && !LoggedInUser.hasRole(roles.ACCOUNTANT, account)) { - return ( - - ); - } - } else if (!isAdmin) { - return ; - } -} - -function getSingleParam(queryParam: string | string[]): string { - return Array.isArray(queryParam) ? queryParam[0] : queryParam; } -function getAsArray(queryParam: string | string[]): string[] { - return Array.isArray(queryParam) ? queryParam : [queryParam]; -} - -const parseQuery = query => { - return { - slug: getSingleParam(query.slug), - section: getSingleParam(query.section), - subpath: getAsArray(query.subpath)?.filter(Boolean), - }; -}; - -const DashboardPage = () => { - const intl = useIntl(); - const router = useRouter(); - const { slug, section, subpath } = parseQuery(router.query); - const { LoggedInUser, loadingLoggedInUser } = useLoggedInUser(); - const [lastWorkspaceVisit, setLastWorkspaceVisit] = useLocalStorage(LOCAL_STORAGE_KEYS.DASHBOARD_NAVIGATION_STATE, { - slug: LoggedInUser?.collective.slug, - }); - const isRootUser = LoggedInUser?.isRoot; - const defaultSlug = lastWorkspaceVisit.slug || LoggedInUser?.collective.slug; - const activeSlug = slug || defaultSlug; - const isRootProfile = activeSlug === ROOT_PROFILE_KEY; - - const { data, loading } = useQuery(adminPanelQuery, { - context: API_V2_CONTEXT, - variables: { slug: activeSlug }, - skip: !activeSlug || !LoggedInUser || isRootProfile, - }); - const account = isRootProfile && isRootUser ? ROOT_PROFILE_ACCOUNT : data?.account; - const selectedSection = section || getDefaultSectionForAccount(account, LoggedInUser); - - const useDynamicTopBar = LoggedInUser?.hasPreviewFeatureEnabled(PREVIEW_FEATURE_KEYS.DYNAMIC_TOP_BAR); - - // Keep track of last visited workspace account and sections - React.useEffect(() => { - if (activeSlug && activeSlug !== lastWorkspaceVisit.slug) { - if (LoggedInUser && !useDynamicTopBar) { - // this is instead configured as "default" account in NewAccountSwitcher - setLastWorkspaceVisit({ slug: activeSlug }); +function getLastWorkspaceVisitClient() { + if (typeof document !== 'undefined') { + const valStr = document.cookie + .split('; ') + .find(row => row.startsWith('lastWorkspaceVisit=')) + ?.split('=')[1]; + + if (valStr) { + try { + return JSON.parse(valStr); + } catch { + return null; } } - // If there is no slug set (that means /dashboard) - // And if there is an activeSlug (this means lastWorkspaceVisit OR LoggedInUser) - // And a LoggedInUser - // And if activeSlug is different than LoggedInUser slug - if (!slug && activeSlug && LoggedInUser && activeSlug !== LoggedInUser.collective.slug) { - router.replace(`/dashboard/${activeSlug}`); - } - }, [activeSlug, LoggedInUser]); + } +} - // Clear last visited workspace account if not admin - React.useEffect(() => { - if (account && !LoggedInUser.isAdminOfCollective(account)) { - setLastWorkspaceVisit({ slug: null }); +function getLastWorkspaceVisitServer(ctx: NextPageContext) { + if ((ctx.req as any).cookies.lastWorkspaceVisit) { + try { + return JSON.parse((ctx.req as any).cookies.lastWorkspaceVisit); + } catch { + return null; } - }, [account]); - - const notification = getNotification(intl, account); - const [expandedSection, setExpandedSection] = React.useState(null); - const isLoading = loading || loadingLoggedInUser; - const blocker = !isLoading && getBlocker(LoggedInUser, account, selectedSection); - const titleBase = intl.formatMessage({ id: 'Dashboard', defaultMessage: 'Dashboard' }); - const menuItems = account ? getMenuItems({ intl, account, LoggedInUser }) : []; - const accountIdentifier = account && (account.name || `@${account.slug}`); - - let subMenu = null; - const parentMenuItem = menuItems.find( - item => 'subMenu' in item && item.subMenu?.find(item => item.section === selectedSection), - ); - if (parentMenuItem && 'subMenu' in parentMenuItem) { - subMenu = parentMenuItem.subMenu; } +} - return ( - setLastWorkspaceVisit({ slug }), - }} - > -
- - {Boolean(notification) && } - {blocker ? ( -
- -

{blocker}

- {LoggedInUser && ( - - - - )} -
- {!LoggedInUser && } -
- ) : !useDynamicTopBar ? ( -
- - {LoggedInUser && require2FAForAdmins(account) && !LoggedInUser.hasTwoFactorAuth ? ( - - ) : ( -
- -
- )} -
- ) : ( -
- - - {LoggedInUser && require2FAForAdmins(account) && !LoggedInUser.hasTwoFactorAuth ? ( - - ) : ( -
- {subMenu ? ( - - ) : ( -
- )} +DashboardIndexPage.getInitialProps = ctx => { + const lastWorkspaceVisit = ctx.req ? getLastWorkspaceVisitServer(ctx) : getLastWorkspaceVisitClient(); - -
- )} -
- )} - -
-
- - ); -}; - -DashboardPage.getInitialProps = () => { return { - scripts: { googleMaps: true }, // TODO: This should be enabled only for events + lastWorkspaceVisit, }; }; - -// next.js export -// ts-unused-exports:disable-next-line -export default DashboardPage; diff --git a/rewrites.js b/rewrites.js index 8bdb392aabb..d83a2165757 100644 --- a/rewrites.js +++ b/rewrites.js @@ -66,7 +66,7 @@ exports.REWRITES = [ }, { source: - '/dashboard/:slug/:section(expected-funds|contributors|vendors|team|host-expenses|host-agreements|host-applications|host-tax-forms|expenses|submitted-expenses|host-virtual-cards|host-virtual-card-requests|incoming-contributions|outgoing-contributions|transactions|virtual-cards|orders|chart-of-accounts|reports|hosted-collectives|updates|host-transactions|accounts|invoices-receipts|tax-information|notifications)/:subpath*', + '/dashboard/:slug/:section(overview|expected-funds|contributors|vendors|team|host-expenses|host-agreements|host-applications|host-tax-forms|expenses|submitted-expenses|host-virtual-cards|host-virtual-card-requests|incoming-contributions|outgoing-contributions|transactions|virtual-cards|orders|chart-of-accounts|reports|hosted-collectives|updates|host-transactions|accounts|invoices-receipts|tax-information|notifications)/:subpath*', destination: '/dashboard/:slug/:section/:subpath*', }, { @@ -75,11 +75,15 @@ exports.REWRITES = [ destination: '/dashboard/:slug/legacy-settings-sections/:section', }, { - source: '/dashboard/:slug/:section?/:subpath*', + source: '/dashboard/root-actions', + destination: '/dashboard/root/all-collectives', + }, + { + source: '/dashboard/:slug', destination: '/dashboard', }, { - source: '/workspace/:slug/:section?/:subpath*', + source: '/workspace/:slug', destination: '/dashboard', }, { From 574aab0a3b2a0a9f4665756226d29f2b42ce5cd9 Mon Sep 17 00:00:00 2001 From: Henrique Date: Tue, 3 Sep 2024 11:57:31 -0300 Subject: [PATCH 3/9] wip --- components/dashboard/constants.ts | 2 +- components/root-actions/MoveReceivedContributions.js | 1 + package.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/components/dashboard/constants.ts b/components/dashboard/constants.ts index f65252b2d6a..ea079c496f7 100644 --- a/components/dashboard/constants.ts +++ b/components/dashboard/constants.ts @@ -48,7 +48,7 @@ export const ROOT_SECTIONS = { RECURRING_CONTRIBUTIONS: 'recurring-contributions', }; -export const SETTINGS_SECTIONS = { +const SETTINGS_SECTIONS = { NOTIFICATIONS: 'notifications', INVOICES_RECEIPTS: 'invoices-receipts', TAX_INFORMATION: 'tax-information', diff --git a/components/root-actions/MoveReceivedContributions.js b/components/root-actions/MoveReceivedContributions.js index f4b14a678ac..629a00b5c84 100644 --- a/components/root-actions/MoveReceivedContributions.js +++ b/components/root-actions/MoveReceivedContributions.js @@ -273,4 +273,5 @@ const MoveReceivedContributions = () => { MoveReceivedContributions.propTypes = {}; +// ts-unused-exports:disable-next-line export default MoveReceivedContributions; diff --git a/package.json b/package.json index 4c43f20be20..e2eb7d190e1 100644 --- a/package.json +++ b/package.json @@ -201,7 +201,7 @@ "test:e2e:3": "cross-env TZ=UTC cypress run --spec \"test/cypress/integration/3*.js\"", "test:jest": "cross-env NODE_ENV=test TZ=UTC jest components lib pages", "test:update": "npm run test:jest -- --updateSnapshot", - "ts-unused-exports": "ts-unused-exports tsconfig.json --ignoreFiles=lib/graphql/types/ --ignoreFiles=stories/", + "ts-unused-exports": "ts-unused-exports tsconfig.json --ignoreFiles=lib/graphql/types/ --ignoreFiles=stories/ --excludePathsFromReport=pages/", "type:check": "tsc", "jest": "cross-env NODE_ENV=test TZ=UTC jest" }, From 6b21307c977ab167744bb78d0cde2f2974b053c1 Mon Sep 17 00:00:00 2001 From: Henrique Date: Tue, 3 Sep 2024 13:44:46 -0300 Subject: [PATCH 4/9] wip --- .../[[...subpath]].tsx} | 20 ++++++++++++------- rewrites.js | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) rename pages/dashboard/[slug]/legacy-settings-sections/{[section].tsx => [section]/[[...subpath]].tsx} (56%) diff --git a/pages/dashboard/[slug]/legacy-settings-sections/[section].tsx b/pages/dashboard/[slug]/legacy-settings-sections/[section]/[[...subpath]].tsx similarity index 56% rename from pages/dashboard/[slug]/legacy-settings-sections/[section].tsx rename to pages/dashboard/[slug]/legacy-settings-sections/[section]/[[...subpath]].tsx index 01676198a9e..b627127fe22 100644 --- a/pages/dashboard/[slug]/legacy-settings-sections/[section].tsx +++ b/pages/dashboard/[slug]/legacy-settings-sections/[section]/[[...subpath]].tsx @@ -3,12 +3,16 @@ import { values } from 'lodash'; import { useRouter } from 'next/router'; import { useIntl } from 'react-intl'; -import { LEGACY_SECTIONS, LEGACY_SETTINGS_SECTIONS, SECTION_LABELS } from '../../../../components/dashboard/constants'; -import { DashboardContext } from '../../../../components/dashboard/DashboardContext'; -import DashboardHeader from '../../../../components/dashboard/DashboardHeader'; -import DashboardPage from '../../../../components/dashboard/DashboardPage'; -import AccountSettings from '../../../../components/dashboard/sections/AccountSettings'; -import NotFound from '../../../../components/NotFound'; +import { + LEGACY_SECTIONS, + LEGACY_SETTINGS_SECTIONS, + SECTION_LABELS, +} from '../../../../../components/dashboard/constants'; +import { DashboardContext } from '../../../../../components/dashboard/DashboardContext'; +import DashboardHeader from '../../../../../components/dashboard/DashboardHeader'; +import DashboardPage from '../../../../../components/dashboard/DashboardPage'; +import AccountSettings from '../../../../../components/dashboard/sections/AccountSettings'; +import NotFound from '../../../../../components/NotFound'; const SECTIONS = [...values(LEGACY_SECTIONS), ...values(LEGACY_SETTINGS_SECTIONS)]; @@ -32,7 +36,9 @@ function SectionComponent() { return ( - + {SECTION_LABELS[section] && ( + + )} ); diff --git a/rewrites.js b/rewrites.js index d83a2165757..7c8361bcddf 100644 --- a/rewrites.js +++ b/rewrites.js @@ -72,7 +72,7 @@ exports.REWRITES = [ { source: '/dashboard/:slug/:section(goals|connected-accounts|export|for-developers|user-security|custom-email|activity-log|collective-page|authorized-apps|advanced|webhooks|sending-money|policies|receiving-money|tiers|tickets|host|info|gift-cards|gift-cards-create|payment-methods|payment-receipts|fiscal-hosting|security|host-virtual-cards-settings)/:subpath*', - destination: '/dashboard/:slug/legacy-settings-sections/:section', + destination: '/dashboard/:slug/legacy-settings-sections/:section/:subpath*', }, { source: '/dashboard/root-actions', From aa9ec691fb2c0d598ddb8dde8b73e235082e45cd Mon Sep 17 00:00:00 2001 From: Henrique Date: Fri, 6 Sep 2024 08:28:07 -0300 Subject: [PATCH 5/9] Add dashboard middleware and fix test cases --- components/dashboard/DashboardPage.tsx | 6 - .../sections/updates/UpdateFormView.tsx | 2 +- middleware.ts | 23 ++-- middlewares/dashboard.ts | 112 ++++++++++++++++++ middlewares/home.ts | 10 ++ middlewares/utils.ts | 57 +++++++++ package.json | 2 +- .../[section]/[[...subpath]].tsx | 6 + pages/dashboard/index.tsx | 2 - tsconfig.json | 1 + 10 files changed, 199 insertions(+), 22 deletions(-) create mode 100644 middlewares/dashboard.ts create mode 100644 middlewares/home.ts create mode 100644 middlewares/utils.ts diff --git a/components/dashboard/DashboardPage.tsx b/components/dashboard/DashboardPage.tsx index 499ac153788..818364147e6 100644 --- a/components/dashboard/DashboardPage.tsx +++ b/components/dashboard/DashboardPage.tsx @@ -261,9 +261,3 @@ export default function DashboardPage(props: { ); } - -DashboardPage.getInitialProps = () => { - return { - scripts: { googleMaps: true }, // TODO: This should be enabled only for events - }; -}; diff --git a/components/dashboard/sections/updates/UpdateFormView.tsx b/components/dashboard/sections/updates/UpdateFormView.tsx index dc2c3748fcf..2e86bcaf7e8 100644 --- a/components/dashboard/sections/updates/UpdateFormView.tsx +++ b/components/dashboard/sections/updates/UpdateFormView.tsx @@ -531,7 +531,7 @@ const UpdateFormView = ({ updateId }) => { )} - {!loading && } + {!loading && account && }
); diff --git a/middleware.ts b/middleware.ts index 465e0fe9419..9be6eb4e0b4 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,20 +1,19 @@ -import type { NextRequest } from 'next/server'; -import { NextResponse } from 'next/server'; +import { type NextRequest, NextResponse } from 'next/server'; -// config file -// ts-unused-exports:disable-next-line -export function middleware(req: NextRequest) { - // Note: only need to check presence rootRedirectDashboard cookie, not its value - const redirectToDashboard = req.cookies.get('rootRedirectDashboard'); +import dashboardMiddleware from './middlewares/dashboard'; +import homeMiddleware from './middlewares/home'; - if (redirectToDashboard) { - return NextResponse.redirect(new URL('/dashboard', req.url)); +export function middleware(req: NextRequest) { + const pathname = req.nextUrl.pathname; + if (pathname === '/') { + return homeMiddleware(req); + } else if (pathname === '/dashboard') { + return dashboardMiddleware(req); } + return NextResponse.next(); } -// config file -// ts-unused-exports:disable-next-line export const config = { - matcher: '/', + matcher: ['/', { source: '/dashboard', missing: [{ type: 'query', key: 'slug' }] }], }; diff --git a/middlewares/dashboard.ts b/middlewares/dashboard.ts new file mode 100644 index 00000000000..e1308d27486 --- /dev/null +++ b/middlewares/dashboard.ts @@ -0,0 +1,112 @@ +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; + +import { isHostAccount, isIndividualAccount } from '../lib/collective'; + +import { fetchGraphQLV1, getTokenFromRequest } from './utils'; + +export default async function dashboardMiddleware(req: NextRequest) { + // if (req.nextUrl.searchParams.has('slug')) { + // return NextResponse.next(); + // } + + const token = getTokenFromRequest(req); + + if (!token) { + return NextResponse.redirect(new URL('/signin', req.url)); + } + + const response = await fetchGraphQLV1<{ + LoggedInUser: { + id: number; + isRoot: boolean; + collective: { + type: string; + slug: string; + }; + memberOf: { + role: string; + collective: { + type: string; + slug: string; + isHost: boolean; + }; + }[]; + }; + }>( + { + query: ` + query DashboardMiddlewareQuery { + LoggedInUser { + id + isRoot + collective { + slug + type + } + memberOf { + role + collective { + slug + type + isHost + } + } + } + } + `, + }, + { accessToken: token }, + ); + + if (!response.data?.LoggedInUser) { + return NextResponse.redirect(new URL('/signin', req.url)); + } + + const LoggedInUser = response.data.LoggedInUser; + + let lastWorkspaceVisit: { slug?: string }; + if (req.cookies.has('lastWorkspaceVisit')) { + try { + lastWorkspaceVisit = JSON.parse(req.cookies.get('lastWorkspaceVisit').value); + } catch { + /* empty */ + } + } + + const { role, collective } = getDashboardRole(LoggedInUser, lastWorkspaceVisit?.slug); + let section; + if (isIndividualAccount(collective)) { + section = 'overview'; + } else if (isHostAccount(collective)) { + section = 'host-expenses'; + } else if (role === 'ACCOUNTANT') { + section = 'payment-receipts'; + } else { + section = 'expenses'; + } + + return NextResponse.redirect(new URL(`/dashboard/${collective.slug}/${section}`, req.url)); +} + +function getDashboardRole( + LoggedInUser, + slug, +): { collective: { slug: string; type: string; isHost?: boolean }; role: string } { + let role = { + collective: { slug: LoggedInUser.collective.slug, type: 'INDIVIDUAL' }, + role: 'ADMIN', + }; + if (!slug || slug === LoggedInUser.collective.slug) { + return role; + } + + const roles = (LoggedInUser.memberOf || []).filter(a => a.collective.slug === slug); + if (!roles || roles.length === 0) { + return role; + } + + role = roles.find(a => a.role === 'ADMIN'); + role = role || roles.find(a => a.role === 'ACCOUNTANT'); + return role; +} diff --git a/middlewares/home.ts b/middlewares/home.ts new file mode 100644 index 00000000000..40835fdb7ea --- /dev/null +++ b/middlewares/home.ts @@ -0,0 +1,10 @@ +import type { NextRequest} from 'next/server'; +import { NextResponse } from 'next/server'; + +export default function homeMiddleware(req: NextRequest) { + if (req.cookies.has('rootRedirectDashboard')) { + return NextResponse.redirect(new URL('/dashboard', req.url)); + } + + return NextResponse.next(); +} diff --git a/middlewares/utils.ts b/middlewares/utils.ts new file mode 100644 index 00000000000..1bae58197e5 --- /dev/null +++ b/middlewares/utils.ts @@ -0,0 +1,57 @@ +import type { NextRequest } from 'next/server'; + +export function getTokenFromRequest(req: NextRequest): string { + return req.cookies.has('accessTokenPayload') && req.cookies.has('accessTokenSignature') + ? [req.cookies.get('accessTokenPayload').value, req.cookies.get('accessTokenSignature').value].join('.') + : null; +} + +type GraphQLRequest = { + query: string; + operationName?: string; + variables?: Record; +}; + +type GraphQLResponse = { + data?: Data; + errors?: Record[]; +}; + +type GraphQLRequestOptions = { + accessToken?: string; +}; + +export async function fetchGraphQLV1( + request: GraphQLRequest, + options?: GraphQLRequestOptions, +): Promise>> { + + const response = await fetch(getGraphQLUrl('v1'), { + method: 'POST', + body: JSON.stringify(request), + headers: { + ...defaultGraphQLRequestHeaders(), + 'Content-Type': 'application/json', + ...(options?.accessToken + ? { + Authorization: `Bearer ${options.accessToken}`, + } + : {}), + }, + }); + + return await response.json(); +} + +function defaultGraphQLRequestHeaders() { + const headers = { 'oc-application': process.env.OC_APPLICATION }; + headers['oc-env'] = process.env.OC_ENV; + headers['oc-secret'] = process.env.OC_SECRET; + headers['oc-application'] = process.env.OC_APPLICATION; + headers['user-agent'] = 'opencollective-frontend/1.0 edge-fetch/1.0'; + return headers; +} + +function getGraphQLUrl(apiVersion: 'v1' | 'v2'): string { + return `${process.env.API_URL}/graphql/${apiVersion}?api_key=${process.env.API_KEY}`; +} diff --git a/package.json b/package.json index e2eb7d190e1..7264ffa3d1f 100644 --- a/package.json +++ b/package.json @@ -201,7 +201,7 @@ "test:e2e:3": "cross-env TZ=UTC cypress run --spec \"test/cypress/integration/3*.js\"", "test:jest": "cross-env NODE_ENV=test TZ=UTC jest components lib pages", "test:update": "npm run test:jest -- --updateSnapshot", - "ts-unused-exports": "ts-unused-exports tsconfig.json --ignoreFiles=lib/graphql/types/ --ignoreFiles=stories/ --excludePathsFromReport=pages/", + "ts-unused-exports": "ts-unused-exports tsconfig.json --ignoreFiles=lib/graphql/types/ --ignoreFiles=stories/ --excludePathsFromReport=pages/ --excludePathsFromReport=middlewares/ --excludePathsFromReport=middleware.ts", "type:check": "tsc", "jest": "cross-env NODE_ENV=test TZ=UTC jest" }, diff --git a/pages/dashboard/[slug]/legacy-settings-sections/[section]/[[...subpath]].tsx b/pages/dashboard/[slug]/legacy-settings-sections/[section]/[[...subpath]].tsx index b627127fe22..ac7a639666a 100644 --- a/pages/dashboard/[slug]/legacy-settings-sections/[section]/[[...subpath]].tsx +++ b/pages/dashboard/[slug]/legacy-settings-sections/[section]/[[...subpath]].tsx @@ -27,6 +27,12 @@ export default function LegacySettingsSectionPage(props) { return ; } +LegacySettingsSectionPage.getInitialProps = () => { + return { + scripts: { googleMaps: true }, // TODO: This should be enabled only for events + }; +}; + function SectionComponent() { const router = useRouter(); const section = router.query.section as string; diff --git a/pages/dashboard/index.tsx b/pages/dashboard/index.tsx index 301f056f4da..2f34f7ddd96 100644 --- a/pages/dashboard/index.tsx +++ b/pages/dashboard/index.tsx @@ -15,8 +15,6 @@ import { adminPanelQuery } from '../../components/dashboard/queries'; import type { DashboardSectionProps } from '../../components/dashboard/types'; import MessageBoxGraphqlError from '../../components/MessageBoxGraphqlError'; -// TODO: what we ideally want from the dashboard index page is a HTTP redirect -// based on the authentication state and user preference export default function DashboardIndexPage({ lastWorkspaceVisit }) { const Component: React.FC = () => ( diff --git a/tsconfig.json b/tsconfig.json index 19fd7959a84..278c6c6462e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,6 +25,7 @@ "*.config.*", "lib", "middleware.ts", + "middlewares", "next-env.d.ts", "pages", "rewrites.js", From be2e4e6152f01d2ba57687ebc433ba8abd53486b Mon Sep 17 00:00:00 2001 From: Henrique Date: Fri, 6 Sep 2024 08:41:40 -0300 Subject: [PATCH 6/9] remove dependency due to edge runtime --- middlewares/dashboard.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/middlewares/dashboard.ts b/middlewares/dashboard.ts index e1308d27486..7c54e0bce46 100644 --- a/middlewares/dashboard.ts +++ b/middlewares/dashboard.ts @@ -1,8 +1,6 @@ import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; -import { isHostAccount, isIndividualAccount } from '../lib/collective'; - import { fetchGraphQLV1, getTokenFromRequest } from './utils'; export default async function dashboardMiddleware(req: NextRequest) { @@ -76,9 +74,9 @@ export default async function dashboardMiddleware(req: NextRequest) { const { role, collective } = getDashboardRole(LoggedInUser, lastWorkspaceVisit?.slug); let section; - if (isIndividualAccount(collective)) { + if (['USER', 'INDIVIDUAL'].includes(collective.type)) { section = 'overview'; - } else if (isHostAccount(collective)) { + } else if (collective.isHost === true && collective.type !== 'COLLECTIVE') { section = 'host-expenses'; } else if (role === 'ACCOUNTANT') { section = 'payment-receipts'; From d05f478ab8897607ec331e4ed284189fd1218518 Mon Sep 17 00:00:00 2001 From: Henrique Date: Fri, 6 Sep 2024 08:54:43 -0300 Subject: [PATCH 7/9] fix codeql warning --- components/dashboard/sections/Contributors.tsx | 7 ++++--- lib/api.js | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/components/dashboard/sections/Contributors.tsx b/components/dashboard/sections/Contributors.tsx index 1dcba196f36..b7232c46d0f 100644 --- a/components/dashboard/sections/Contributors.tsx +++ b/components/dashboard/sections/Contributors.tsx @@ -104,6 +104,7 @@ const dashboardContributorsQuery = gql` ) { account(slug: $slug) { id + slug members(role: $role, offset: $offset, limit: $limit, orderBy: $orderBy, email: $email) { totalCount nodes { @@ -291,12 +292,12 @@ const Contributors = ({ accountSlug }: ContributorsProps) => { size="sm" variant="outline" loading={isDownloadingCsv} + disabled={!data?.account?.slug} onClick={async () => { try { setDownloadingCsv(true); - const filename = `${accountSlug}-contributors.csv`; - const url = `${process.env.REST_URL}/v2/${accountSlug}/contributors.csv?fetchAll=1`; - await fetchCSVFileFromRESTService(url, filename); + const filename = `${data?.account?.slug}-contributors.csv`; + await fetchCSVFileFromRESTService(data?.account?.slug, filename); } finally { setDownloadingCsv(false); } diff --git a/lib/api.js b/lib/api.js index 1503f70ab81..e187590250c 100644 --- a/lib/api.js +++ b/lib/api.js @@ -372,7 +372,8 @@ export async function downloadLegalDocument(legalDocument, account, prompt2fa) { /** * Fetch a CSV file, usually from the REST service */ -export async function fetchCSVFileFromRESTService(url, filename, { isAuthenticated = true } = {}) { +export async function fetchCSVFileFromRESTService(accountSlug, filename, { isAuthenticated = true } = {}) { + const url = `${process.env.REST_URL}/v2/${accountSlug}/contributors.csv?fetchAll=1`; const fetchParams = { method: 'GET' }; if (isAuthenticated) { const accessToken = getFromLocalStorage(LOCAL_STORAGE_KEYS.ACCESS_TOKEN); From 49ce8fd14edade398ed31fcf24c114e56e1b4426 Mon Sep 17 00:00:00 2001 From: Henrique Date: Fri, 6 Sep 2024 09:20:16 -0300 Subject: [PATCH 8/9] fix e2e assertion --- test/cypress/integration/07-signin.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cypress/integration/07-signin.test.js b/test/cypress/integration/07-signin.test.js index 7aabdf161cf..a307fe90cb4 100644 --- a/test/cypress/integration/07-signin.test.js +++ b/test/cypress/integration/07-signin.test.js @@ -96,7 +96,7 @@ describe('signin', () => { cy.visit('/signin?next=/signin'); cy.get('input[name=email]').type('testuser+admin@opencollective.com'); cy.get('button[type=submit]').click(); - cy.url().should('eq', `${Cypress.config().baseUrl}/dashboard`); + cy.url().should('eq', `${Cypress.config().baseUrl}/dashboard/testuseradmin/overview`); }); it('can signup as regular user', () => { From 2213e6c3c092ccc5ea0109f320413630d9746b5a Mon Sep 17 00:00:00 2001 From: Henrique Date: Fri, 6 Sep 2024 12:09:44 -0300 Subject: [PATCH 9/9] On vercel api proxy route propagate the auth cookies --- pages/api/users/exchange-login-token.js | 4 ++++ pages/api/users/refresh-token.js | 4 ++++ pages/api/users/signin.js | 4 ++++ pages/api/users/two-factor-auth.js | 4 ++++ pages/api/users/update-token.js | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/pages/api/users/exchange-login-token.js b/pages/api/users/exchange-login-token.js index 07193c6a309..b5611461eaf 100644 --- a/pages/api/users/exchange-login-token.js +++ b/pages/api/users/exchange-login-token.js @@ -13,6 +13,10 @@ export default async function handle(req, res) { const json = await result.json(); + result.headers + .getSetCookie() + .filter(cookie => cookie.includes('accessTokenPayload') || cookie.includes('accessTokenSignature')) + .forEach(cookie => res.appendHeader('Set-Cookie', cookie)); res.setHeader('Content-Type', 'application/json'); res.status(result.status).json(json); } diff --git a/pages/api/users/refresh-token.js b/pages/api/users/refresh-token.js index 85e1ecd3a63..5881ca515e3 100644 --- a/pages/api/users/refresh-token.js +++ b/pages/api/users/refresh-token.js @@ -13,6 +13,10 @@ export default async function handle(req, res) { const json = await result.json(); + result.headers + .getSetCookie() + .filter(cookie => cookie.includes('accessTokenPayload') || cookie.includes('accessTokenSignature')) + .forEach(cookie => res.appendHeader('Set-Cookie', cookie)); res.setHeader('Content-Type', 'application/json'); res.status(result.status).json(json); } diff --git a/pages/api/users/signin.js b/pages/api/users/signin.js index 55b63d288c5..798ec96c8aa 100644 --- a/pages/api/users/signin.js +++ b/pages/api/users/signin.js @@ -12,6 +12,10 @@ export default async function handle(req, res) { const json = await result.json(); const statusCode = result.status; + result.headers + .getSetCookie() + .filter(cookie => cookie.includes('accessTokenPayload') || cookie.includes('accessTokenSignature')) + .forEach(cookie => res.appendHeader('Set-Cookie', cookie)); res.setHeader('Content-Type', 'application/json'); res.status(statusCode).json(json); } diff --git a/pages/api/users/two-factor-auth.js b/pages/api/users/two-factor-auth.js index 34fc255ad15..3324e04307c 100644 --- a/pages/api/users/two-factor-auth.js +++ b/pages/api/users/two-factor-auth.js @@ -13,6 +13,10 @@ export default async function handle(req, res) { const json = await result.json(); + result.headers + .getSetCookie() + .filter(cookie => cookie.includes('accessTokenPayload') || cookie.includes('accessTokenSignature')) + .forEach(cookie => res.appendHeader('Set-Cookie', cookie)); res.setHeader('Content-Type', 'application/json'); res.status(result.status).json(json); } diff --git a/pages/api/users/update-token.js b/pages/api/users/update-token.js index c4f458422ca..0ba3f1cf52b 100644 --- a/pages/api/users/update-token.js +++ b/pages/api/users/update-token.js @@ -13,6 +13,10 @@ export default async function handle(req, res) { const json = await result.json(); + result.headers + .getSetCookie() + .filter(cookie => cookie.includes('accessTokenPayload') || cookie.includes('accessTokenSignature')) + .forEach(cookie => res.appendHeader('Set-Cookie', cookie)); res.setHeader('Content-Type', 'application/json'); res.status(result.status).json(json); }