Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions app/Root/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,40 @@ const login: RouteConfig = {
const teams: RouteConfig = {
index: true,
path: '/teams',
load: () => import('#views/Teams/TeamsList'),
load: () => import('#views/Teams/index'),
visibility: 'is-authenticated',
};

const createTeam: RouteConfig = {
index: true,
path: '/teams/new',
load: () => import('#views/Teams/TeamForm'),
visibility: 'is-authenticated',
};

const editTeam: RouteConfig = {
index: true,
path: '/teams/:id/edit',
load: () => import('#views/Teams/TeamForm'),
visibility: 'is-authenticated',
};

const teamMembers: RouteConfig = {
path: '/teams/:id/team-members/',
load: () => import('#views/Teams/TeamMembers'),
visibility: 'is-authenticated',
};

const createTeamMember: RouteConfig = {
path: '/teams/:id/team-members/new',
load: () => import('#views/Teams/TeamMembers/TeamMemberForm'),
visibility: 'is-authenticated',
};

const editTeamMember: RouteConfig = {
path: '/teams/:id/team-members/:member/edit',
load: () => import('#views/Teams/TeamMembers/TeamMemberForm'),
visibility: 'is-authenticated',
};

const users: RouteConfig = {
index: true,
path: '/users',
Expand Down Expand Up @@ -106,6 +122,8 @@ const routes = {
teams,
createTeam,
editTeam,
teamMembers,
createTeamMember,
users,
createUser,
editUser,
Expand All @@ -115,6 +133,7 @@ const routes = {
documents,
onlineInteractive,
galleries,
editTeamMember,
} satisfies Record<string, RouteConfig>;

export type RouteKeys = keyof typeof routes;
Expand Down
13 changes: 11 additions & 2 deletions app/components/EditDeleteActions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import {
EditTwoLineIcon,
} from '@ifrc-go/icons';
import { TableActions } from '@ifrc-go/ui';
import { isDefined } from '@togglecorp/fujs';

import DropdownMenuItem from '#components/DropdownMenuItem';
import useRouting, { type RoutesMap } from '#hooks/useRouting';

export interface Props {
id: string;
member?: string;
onDelete: (id: string) => void;
itemTitle: string;
to: keyof RoutesMap;
Expand All @@ -21,13 +23,20 @@ function EditDeleteActions(props: Props) {
onDelete,
itemTitle,
to,
member,
} = props;

const navigate = useRouting();

const handleEditClick = useCallback(() => {
navigate(to, { id });
}, [navigate, to, id]);
// NOTE: This navigation is for Team member
// as id and memberId is needed to access
if (isDefined(member)) {
navigate(to, { id, member });
} else {
navigate(to, { id });
}
}, [navigate, to, id, member]);

return (
<TableActions
Expand Down
63 changes: 63 additions & 0 deletions app/components/NonFieldError/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useMemo } from 'react';
import { AlertLineIcon } from '@ifrc-go/icons';
import {
_cs,
isFalsyString,
isNotDefined,
} from '@togglecorp/fujs';
import {
analyzeErrors,
type Error,
getErrorObject,
nonFieldError,
} from '@togglecorp/toggle-form';

import styles from './styles.module.css';

interface Props<T> {
className?: string;
error?: Error<T>;
withFallbackError?: boolean;
}

function NonFieldError<T>(props: Props<T>) {
const {
className,
error,
withFallbackError,
} = props;

const errorObject = useMemo(() => getErrorObject(error), [error]);

if (isNotDefined(errorObject)) {
return null;
}

const hasError = analyzeErrors(errorObject);
if (!hasError) {
return null;
}

const stringError = errorObject?.[nonFieldError] || (
withFallbackError ? 'Please correct all the errors before submission!' : undefined);

if (isFalsyString(stringError)) {
return null;
}

return (
<div
className={_cs(
styles.nonFieldError,
className,
)}
>
<AlertLineIcon className={styles.icon} />
<div>
{stringError}
</div>
</div>
);
}

export default NonFieldError;
19 changes: 19 additions & 0 deletions app/components/NonFieldError/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.non-field-error {
display: inline-flex;
align-items: start;
animation: flash var(--go-ui-duration-animation-fast) ease-in-out;
animation-delay: var(--go-ui-duration-animation-slow);
gap: var(--go-ui-spacing-sm);
color: var(--go-ui-color-red);

.icon {
flex-shrink: 0;
font-size: var(--go-ui-height-icon-multiplier);
}
}

@keyframes flash {
0% { opacity: 1; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
29 changes: 9 additions & 20 deletions app/components/Page/index.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,35 @@
import { ListView } from '@ifrc-go/ui';
import type { RefObject } from 'react';
import { _cs } from '@togglecorp/fujs';

import styles from './styles.module.css';

interface Props {
className?: string;
children?: React.ReactNode;
elementRef?: RefObject<HTMLDivElement>
leftPaneContent?: React.ReactNode;
leftPaneContainerClassName?: string;
}
function Page(props: Props) {
const {
className,
children,
elementRef,
leftPaneContent,
leftPaneContainerClassName,
} = props;

return (
<ListView
layout="grid"
withSidebar
sidebarPosition="start"
className={_cs(className, styles.page)}
spacing="none"
>
<div className={_cs(className, styles.page)} ref={elementRef}>
{leftPaneContent && (
<ListView
layout="block"
withBackground
className={leftPaneContainerClassName}
>
<div className={_cs(leftPaneContainerClassName, styles.leftPane)}>
{leftPaneContent}
</ListView>
</div>
)}
<ListView
withBackground
layout="block"
>
<div className={styles.rightPaneContent}>
{children}
</ListView>
</ListView>
</div>
</div>
);
}

Expand Down
25 changes: 23 additions & 2 deletions app/components/Page/styles.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
.page , .page > div {
height: 100%;
.page {
display: flex;
flex-direction: row;
flex-grow: 1;
background: var(--go-ui-color-gray-10);
height: 100%;
overflow: hidden;

>* {
width: 100%
}

.left-pane {
display: flex;
flex-direction: column;
background-color: var(--go-ui-color-white);
gap: var(--go-ui-spacing-md);
width: 20%
}

.right-pane-content {
overflow: auto;
}
}
107 changes: 107 additions & 0 deletions app/components/RegionSearchMultiSelectInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {
useCallback,
useMemo,
useState,
} from 'react';
import {
SearchMultiSelectInput,
type SearchMultiSelectInputProps,
} from '@ifrc-go/ui';
import { unique } from '@togglecorp/fujs';

import {
AdminAreaLevel,
type AdminAreasQuery,
useAdminAreasQuery,
} from '#generated/types/graphql';
import {
idSelector,
nameSelector,
type REGION_LEVEL,
type WOREDA_LEVEL,
type ZONE_LEVEL,
} from '#utils/common';

export type AdminAreaItem = NonNullable<NonNullable<AdminAreasQuery['adminAreas']>['results']>[number];

type Def = { containerClassName?: string; };
type RegionSearchMultiSelectInputProps<NAME> = SearchMultiSelectInputProps<
string,
NAME,
AdminAreaItem,
Def,
'onSearchValueChange' | 'searchOptions' | 'optionsPending'
| 'keySelector' | 'labelSelector' | 'totalOptionsCount' | 'onShowDropdownChange'
| 'selectedOnTop'
> & {
level: REGION_LEVEL | ZONE_LEVEL | WOREDA_LEVEL;
};

function RegionSearchMultiSelectInput<const NAME>(
props: RegionSearchMultiSelectInputProps<NAME>,
) {
const {
className,
name,
value,
onChange,
onOptionsChange,
level = AdminAreaLevel.Region,
disabled,
readOnly,
...otherProps
} = props;

const [opened, setOpened] = useState(false);

const [{ data: adminAreasData, fetching }] = useAdminAreasQuery({
pause: !opened,
variables: {
filters: {
level,
},
},
});

const regionOptions = useMemo(
() => adminAreasData?.adminAreas?.results ?? [],
[adminAreasData?.adminAreas?.results],
);

const handleSelectAllClick = useCallback(() => {
const allIds = regionOptions.map(idSelector);
if (allIds.length > 0) {
onChange(allIds, name);
if (onOptionsChange) {
onOptionsChange((existingOptions) => {
const safeOptions = existingOptions ?? [];
return unique([...safeOptions, ...regionOptions], idSelector);
});
}
}
}, [regionOptions, onChange, name, onOptionsChange]);

return (
<SearchMultiSelectInput
// eslint-disable-next-line react/jsx-props-no-spreading
{...otherProps}
className={className}
name={name}
value={value}
onChange={onChange}
onOptionsChange={onOptionsChange}
searchOptions={regionOptions}
keySelector={idSelector}
labelSelector={nameSelector}
onShowDropdownChange={setOpened}
optionsPending={fetching}
totalOptionsCount={regionOptions.length}
disabled={disabled}
readOnly={readOnly}
selectedOnTop={false}
onSelectAllButtonClick={handleSelectAllClick}
/>
);
}

export default RegionSearchMultiSelectInput;
Loading
Loading