Skip to content

Commit 699114b

Browse files
feat(organization): add new route for organization projects TASK-975 (#5240)
Make it possible to view your organization projects in a table. Add new route for viewing your organization projects at `#/projects/organization`. Make it possible to switch into the new route through the View Switcher UI element.
1 parent eb92123 commit 699114b

File tree

7 files changed

+111
-7
lines changed

7 files changed

+111
-7
lines changed

jsapp/js/api.endpoints.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export const endpoints = {
22
ASSET_URL: '/api/v2/assets/:uid/',
3+
ORG_ASSETS_URL: '/api/v2/organizations/:organization_id/assets/',
34
ME_URL: '/me/',
45
PRODUCTS_URL: '/api/v2/stripe/products/',
56
SUBSCRIPTION_URL: '/api/v2/stripe/subscriptions/',
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Libraries
2+
import React, {useState, useEffect} from 'react';
3+
4+
// Partial components
5+
import UniversalProjectsRoute from './universalProjectsRoute';
6+
import LoadingSpinner from 'js/components/common/loadingSpinner';
7+
8+
// Stores, hooks and utilities
9+
import {useOrganizationQuery} from 'js/account/stripe.api';
10+
11+
// Constants and types
12+
import {
13+
ORG_VIEW,
14+
HOME_ORDERABLE_FIELDS,
15+
HOME_DEFAULT_VISIBLE_FIELDS,
16+
HOME_EXCLUDED_FIELDS,
17+
} from './projectViews/constants';
18+
import {ROOT_URL} from 'js/constants';
19+
import {endpoints} from 'js/api.endpoints';
20+
21+
/**
22+
* Component responsible for rendering organization projects route
23+
* (`#/organization/projects`).
24+
*/
25+
export default function MyOrgProjectsRoute() {
26+
const orgQuery = useOrganizationQuery();
27+
const [apiUrl, setApiUrl] = useState<string | null>(null);
28+
29+
// We need to load organization data to build the api url.
30+
useEffect(() => {
31+
if (orgQuery.data) {
32+
setApiUrl(endpoints.ORG_ASSETS_URL.replace(':organization_id', orgQuery.data.id));
33+
}
34+
}, [orgQuery.data]);
35+
36+
// Display spinner until everything is ready to go forward.
37+
if (!apiUrl) {
38+
return <LoadingSpinner />;
39+
}
40+
41+
return (
42+
<UniversalProjectsRoute
43+
viewUid={ORG_VIEW.uid}
44+
baseUrl={`${ROOT_URL}${apiUrl}`}
45+
defaultVisibleFields={HOME_DEFAULT_VISIBLE_FIELDS}
46+
includeTypeFilter={false}
47+
defaultOrderableFields={HOME_ORDERABLE_FIELDS}
48+
defaultExcludedFields={HOME_EXCLUDED_FIELDS}
49+
isExportButtonVisible={false}
50+
/>
51+
);
52+
}

jsapp/js/projects/projectViews/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ export const HOME_VIEW = {
55
name: t('My Projects'),
66
};
77

8+
export const ORG_VIEW = {
9+
uid: 'kobo_my_organization_projects',
10+
name: t('##organization name## Projects'),
11+
};
12+
813
export interface ProjectsFilterDefinition {
914
fieldName?: ProjectFieldName;
1015
condition?: FilterConditionName;

jsapp/js/projects/projectViews/viewSwitcher.tsx

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import cx from 'classnames';
88
import Icon from 'js/components/common/icon';
99
import KoboDropdown from 'js/components/common/koboDropdown';
1010

11-
// Stores
11+
// Stores and hooks
1212
import projectViewsStore from './projectViewsStore';
13+
import {useOrganizationQuery} from 'js/account/stripe.api';
1314

1415
// Constants
1516
import {PROJECTS_ROUTES} from 'js/router/routerConstants';
16-
import {HOME_VIEW} from './constants';
17+
import {HOME_VIEW, ORG_VIEW} from './constants';
1718

1819
// Styles
1920
import styles from './viewSwitcher.module.scss';
@@ -32,11 +33,14 @@ function ViewSwitcher(props: ViewSwitcherProps) {
3233
// We track the menu visibility for the trigger icon.
3334
const [isMenuVisible, setIsMenuVisible] = useState(false);
3435
const [projectViews] = useState(() => projectViewsStore);
36+
const orgQuery = useOrganizationQuery();
3537
const navigate = useNavigate();
3638

3739
const onOptionClick = (viewUid: string) => {
3840
if (viewUid === HOME_VIEW.uid || viewUid === null) {
3941
navigate(PROJECTS_ROUTES.MY_PROJECTS);
42+
} else if (viewUid === ORG_VIEW.uid) {
43+
navigate(PROJECTS_ROUTES.MY_ORG_PROJECTS);
4044
} else {
4145
navigate(PROJECTS_ROUTES.CUSTOM_VIEW.replace(':viewUid', viewUid));
4246
// The store keeps a number of assets of each view, and that number
@@ -45,8 +49,16 @@ function ViewSwitcher(props: ViewSwitcherProps) {
4549
}
4650
};
4751

52+
const hasMultipleOptions = (
53+
projectViews.views.length !== 0 ||
54+
orgQuery.data?.is_mmo
55+
);
56+
const organizationName = orgQuery.data?.name || t('Organization');
57+
4858
let triggerLabel = HOME_VIEW.name;
49-
if (props.selectedViewUid !== HOME_VIEW.uid) {
59+
if (props.selectedViewUid === ORG_VIEW.uid) {
60+
triggerLabel = ORG_VIEW.name.replace('##organization name##', organizationName);
61+
} else if (props.selectedViewUid !== HOME_VIEW.uid) {
5062
triggerLabel = projectViews.getView(props.selectedViewUid)?.name || '-';
5163
}
5264

@@ -55,9 +67,9 @@ function ViewSwitcher(props: ViewSwitcherProps) {
5567
return null;
5668
}
5769

58-
// If there are no custom views defined, there's no point in displaying
59-
// the dropdown, we will display a "simple" header.
60-
if (projectViews.views.length === 0) {
70+
// If there is only one option in the switcher, there is no point in making
71+
// this piece of UI interactive. We display a "simple" header instead.
72+
if (!hasMultipleOptions) {
6173
return (
6274
<button
6375
className={cx(styles.trigger, styles.triggerSimple)}
@@ -97,6 +109,18 @@ function ViewSwitcher(props: ViewSwitcherProps) {
97109
{HOME_VIEW.name}
98110
</button>
99111

112+
{/* This is the organization view option - depends if user is in MMO
113+
organization */}
114+
{orgQuery.data?.is_mmo &&
115+
<button
116+
key={ORG_VIEW.uid}
117+
className={styles.menuOption}
118+
onClick={() => onOptionClick(ORG_VIEW.uid)}
119+
>
120+
{ORG_VIEW.name.replace('##organization name##', organizationName)}
121+
</button>
122+
}
123+
100124
{/* This is the list of all options for custom views. These are only
101125
being added if custom views are defined (at least one). */}
102126
{projectViews.views.map((view) => (

jsapp/js/projects/routes.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ import React from 'react';
22
import {Navigate, Route} from 'react-router-dom';
33
import RequireAuth from 'js/router/requireAuth';
44
import {PROJECTS_ROUTES} from 'js/router/routerConstants';
5+
import {ValidateOrgPermissions} from 'js/router/validateOrgPermissions.component';
56

67
const MyProjectsRoute = React.lazy(
78
() => import(/* webpackPrefetch: true */ './myProjectsRoute')
89
);
10+
const MyOrgProjectsRoute = React.lazy(
11+
() => import(/* webpackPrefetch: true */ './myOrgProjectsRoute')
12+
);
913
const CustomViewRoute = React.lazy(
1014
() => import(/* webpackPrefetch: true */ './customViewRoute')
1115
);
@@ -25,6 +29,19 @@ export default function routes() {
2529
</RequireAuth>
2630
}
2731
/>
32+
<Route
33+
path={PROJECTS_ROUTES.MY_ORG_PROJECTS}
34+
element={
35+
<RequireAuth>
36+
<ValidateOrgPermissions
37+
mmoOnly
38+
redirectRoute={PROJECTS_ROUTES.MY_PROJECTS}
39+
>
40+
<MyOrgProjectsRoute />
41+
</ValidateOrgPermissions>
42+
</RequireAuth>
43+
}
44+
/>
2845
<Route
2946
path={PROJECTS_ROUTES.CUSTOM_VIEW}
3047
element={

jsapp/js/router/router.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const router = createHashRouter(
5252
element={<Navigate to={ROUTES.FORMS} replace />}
5353
/>
5454
<Route path={ROUTES.ACCOUNT_ROOT}>{accountRoutes()}</Route>
55-
<Route path={ROUTES.PROJECTS_ROOT}>{projectsRoutes()}</Route>
55+
{projectsRoutes()}
5656
<Route path={ROUTES.LIBRARY}>
5757
<Route path='' element={<Navigate to={ROUTES.MY_LIBRARY} replace />} />
5858
<Route

jsapp/js/router/routerConstants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ export const ROUTES = Object.freeze({
4949

5050
export const PROJECTS_ROUTES: {readonly [key: string]: string} = {
5151
MY_PROJECTS: ROUTES.PROJECTS_ROOT + '/home',
52+
/**
53+
* We break from the default way to set routes here, as we want to be
54+
* consistent with other organization related routes.
55+
*/
56+
MY_ORG_PROJECTS: '/organization/projects',
5257
CUSTOM_VIEW: ROUTES.PROJECTS_ROOT + '/:viewUid',
5358
};
5459

0 commit comments

Comments
 (0)