diff --git a/jsapp/js/api.endpoints.ts b/jsapp/js/api.endpoints.ts index 8dc21499db..a60fcbd328 100644 --- a/jsapp/js/api.endpoints.ts +++ b/jsapp/js/api.endpoints.ts @@ -1,5 +1,6 @@ export const endpoints = { ASSET_URL: '/api/v2/assets/:uid/', + ORG_ASSETS_URL: '/api/v2/organizations/:organization_id/assets/', ME_URL: '/me/', PRODUCTS_URL: '/api/v2/stripe/products/', SUBSCRIPTION_URL: '/api/v2/stripe/subscriptions/', diff --git a/jsapp/js/projects/myOrgProjectsRoute.tsx b/jsapp/js/projects/myOrgProjectsRoute.tsx new file mode 100644 index 0000000000..8b8bc80119 --- /dev/null +++ b/jsapp/js/projects/myOrgProjectsRoute.tsx @@ -0,0 +1,52 @@ +// Libraries +import React, {useState, useEffect} from 'react'; + +// Partial components +import UniversalProjectsRoute from './universalProjectsRoute'; +import LoadingSpinner from 'js/components/common/loadingSpinner'; + +// Stores, hooks and utilities +import {useOrganizationQuery} from 'js/account/stripe.api'; + +// Constants and types +import { + ORG_VIEW, + HOME_ORDERABLE_FIELDS, + HOME_DEFAULT_VISIBLE_FIELDS, + HOME_EXCLUDED_FIELDS, +} from './projectViews/constants'; +import {ROOT_URL} from 'js/constants'; +import {endpoints} from 'js/api.endpoints'; + +/** + * Component responsible for rendering organization projects route + * (`#/organization/projects`). + */ +export default function MyOrgProjectsRoute() { + const orgQuery = useOrganizationQuery(); + const [apiUrl, setApiUrl] = useState(null); + + // We need to load organization data to build the api url. + useEffect(() => { + if (orgQuery.data) { + setApiUrl(endpoints.ORG_ASSETS_URL.replace(':organization_id', orgQuery.data.id)); + } + }, [orgQuery.data]); + + // Display spinner until everything is ready to go forward. + if (!apiUrl) { + return ; + } + + return ( + + ); +} diff --git a/jsapp/js/projects/projectViews/constants.ts b/jsapp/js/projects/projectViews/constants.ts index 58ee83b2c5..9c18ef300c 100644 --- a/jsapp/js/projects/projectViews/constants.ts +++ b/jsapp/js/projects/projectViews/constants.ts @@ -5,6 +5,11 @@ export const HOME_VIEW = { name: t('My Projects'), }; +export const ORG_VIEW = { + uid: 'kobo_my_organization_projects', + name: t('##organization name## Projects'), +}; + export interface ProjectsFilterDefinition { fieldName?: ProjectFieldName; condition?: FilterConditionName; diff --git a/jsapp/js/projects/projectViews/viewSwitcher.tsx b/jsapp/js/projects/projectViews/viewSwitcher.tsx index 6f52727525..786bbfaa73 100644 --- a/jsapp/js/projects/projectViews/viewSwitcher.tsx +++ b/jsapp/js/projects/projectViews/viewSwitcher.tsx @@ -8,12 +8,13 @@ import cx from 'classnames'; import Icon from 'js/components/common/icon'; import KoboDropdown from 'js/components/common/koboDropdown'; -// Stores +// Stores and hooks import projectViewsStore from './projectViewsStore'; +import {useOrganizationQuery} from 'js/account/stripe.api'; // Constants import {PROJECTS_ROUTES} from 'js/router/routerConstants'; -import {HOME_VIEW} from './constants'; +import {HOME_VIEW, ORG_VIEW} from './constants'; // Styles import styles from './viewSwitcher.module.scss'; @@ -32,11 +33,14 @@ function ViewSwitcher(props: ViewSwitcherProps) { // We track the menu visibility for the trigger icon. const [isMenuVisible, setIsMenuVisible] = useState(false); const [projectViews] = useState(() => projectViewsStore); + const orgQuery = useOrganizationQuery(); const navigate = useNavigate(); const onOptionClick = (viewUid: string) => { if (viewUid === HOME_VIEW.uid || viewUid === null) { navigate(PROJECTS_ROUTES.MY_PROJECTS); + } else if (viewUid === ORG_VIEW.uid) { + navigate(PROJECTS_ROUTES.MY_ORG_PROJECTS); } else { navigate(PROJECTS_ROUTES.CUSTOM_VIEW.replace(':viewUid', viewUid)); // The store keeps a number of assets of each view, and that number @@ -45,8 +49,16 @@ function ViewSwitcher(props: ViewSwitcherProps) { } }; + const hasMultipleOptions = ( + projectViews.views.length !== 0 || + orgQuery.data?.is_mmo + ); + const organizationName = orgQuery.data?.name || t('Organization'); + let triggerLabel = HOME_VIEW.name; - if (props.selectedViewUid !== HOME_VIEW.uid) { + if (props.selectedViewUid === ORG_VIEW.uid) { + triggerLabel = ORG_VIEW.name.replace('##organization name##', organizationName); + } else if (props.selectedViewUid !== HOME_VIEW.uid) { triggerLabel = projectViews.getView(props.selectedViewUid)?.name || '-'; } @@ -55,9 +67,9 @@ function ViewSwitcher(props: ViewSwitcherProps) { return null; } - // If there are no custom views defined, there's no point in displaying - // the dropdown, we will display a "simple" header. - if (projectViews.views.length === 0) { + // If there is only one option in the switcher, there is no point in making + // this piece of UI interactive. We display a "simple" header instead. + if (!hasMultipleOptions) { return ( + } + {/* This is the list of all options for custom views. These are only being added if custom views are defined (at least one). */} {projectViews.views.map((view) => ( diff --git a/jsapp/js/projects/routes.tsx b/jsapp/js/projects/routes.tsx index 2aa3222dfb..876dbc3351 100644 --- a/jsapp/js/projects/routes.tsx +++ b/jsapp/js/projects/routes.tsx @@ -2,10 +2,14 @@ import React from 'react'; import {Navigate, Route} from 'react-router-dom'; import RequireAuth from 'js/router/requireAuth'; import {PROJECTS_ROUTES} from 'js/router/routerConstants'; +import {ValidateOrgPermissions} from 'js/router/validateOrgPermissions.component'; const MyProjectsRoute = React.lazy( () => import(/* webpackPrefetch: true */ './myProjectsRoute') ); +const MyOrgProjectsRoute = React.lazy( + () => import(/* webpackPrefetch: true */ './myOrgProjectsRoute') +); const CustomViewRoute = React.lazy( () => import(/* webpackPrefetch: true */ './customViewRoute') ); @@ -25,6 +29,19 @@ export default function routes() { } /> + + + + + + } + /> } /> {accountRoutes()} - {projectsRoutes()} + {projectsRoutes()} } />