Skip to content

Commit

Permalink
feat(organization): add new route for organization projects TASK-975 (#…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
magicznyleszek authored Nov 14, 2024
1 parent eb92123 commit 699114b
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 7 deletions.
1 change: 1 addition & 0 deletions jsapp/js/api.endpoints.ts
Original file line number Diff line number Diff line change
@@ -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/',
Expand Down
52 changes: 52 additions & 0 deletions jsapp/js/projects/myOrgProjectsRoute.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(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 <LoadingSpinner />;
}

return (
<UniversalProjectsRoute
viewUid={ORG_VIEW.uid}
baseUrl={`${ROOT_URL}${apiUrl}`}
defaultVisibleFields={HOME_DEFAULT_VISIBLE_FIELDS}
includeTypeFilter={false}
defaultOrderableFields={HOME_ORDERABLE_FIELDS}
defaultExcludedFields={HOME_EXCLUDED_FIELDS}
isExportButtonVisible={false}
/>
);
}
5 changes: 5 additions & 0 deletions jsapp/js/projects/projectViews/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
36 changes: 30 additions & 6 deletions jsapp/js/projects/projectViews/viewSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand All @@ -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 || '-';
}

Expand All @@ -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 (
<button
className={cx(styles.trigger, styles.triggerSimple)}
Expand Down Expand Up @@ -97,6 +109,18 @@ function ViewSwitcher(props: ViewSwitcherProps) {
{HOME_VIEW.name}
</button>

{/* This is the organization view option - depends if user is in MMO
organization */}
{orgQuery.data?.is_mmo &&
<button
key={ORG_VIEW.uid}
className={styles.menuOption}
onClick={() => onOptionClick(ORG_VIEW.uid)}
>
{ORG_VIEW.name.replace('##organization name##', organizationName)}
</button>
}

{/* 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) => (
Expand Down
17 changes: 17 additions & 0 deletions jsapp/js/projects/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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')
);
Expand All @@ -25,6 +29,19 @@ export default function routes() {
</RequireAuth>
}
/>
<Route
path={PROJECTS_ROUTES.MY_ORG_PROJECTS}
element={
<RequireAuth>
<ValidateOrgPermissions
mmoOnly
redirectRoute={PROJECTS_ROUTES.MY_PROJECTS}
>
<MyOrgProjectsRoute />
</ValidateOrgPermissions>
</RequireAuth>
}
/>
<Route
path={PROJECTS_ROUTES.CUSTOM_VIEW}
element={
Expand Down
2 changes: 1 addition & 1 deletion jsapp/js/router/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const router = createHashRouter(
element={<Navigate to={ROUTES.FORMS} replace />}
/>
<Route path={ROUTES.ACCOUNT_ROOT}>{accountRoutes()}</Route>
<Route path={ROUTES.PROJECTS_ROOT}>{projectsRoutes()}</Route>
{projectsRoutes()}
<Route path={ROUTES.LIBRARY}>
<Route path='' element={<Navigate to={ROUTES.MY_LIBRARY} replace />} />
<Route
Expand Down
5 changes: 5 additions & 0 deletions jsapp/js/router/routerConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export const ROUTES = Object.freeze({

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

Expand Down

0 comments on commit 699114b

Please sign in to comment.