Skip to content

Commit

Permalink
Merge pull request #1411 from IFRCGo/project/operational-learning
Browse files Browse the repository at this point in the history
Project: Operational Learning
  • Loading branch information
samshara authored Oct 15, 2024
2 parents 97d1d64 + 375f7b7 commit 03afdcc
Show file tree
Hide file tree
Showing 48 changed files with 2,060 additions and 24 deletions.
5 changes: 5 additions & 0 deletions .changeset/afraid-tools-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ifrc-go/ui": patch
---

Add DismissableListOutput, DismissableMultListOutput and DismissableTextOutput components
5 changes: 5 additions & 0 deletions .changeset/chatty-poets-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"go-web-app": patch
---

Create Operational Learning Page and integrate LLM summary generation
5 changes: 5 additions & 0 deletions .changeset/five-elephants-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"go-web-app": patch
---

Integrate multi-select functionality in operational learning filters to allow selection of multiple filter items.
5 changes: 5 additions & 0 deletions .changeset/rude-shoes-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ifrc-go/ui": minor
---

Add Chip component
14 changes: 14 additions & 0 deletions app/src/App/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,19 @@ const resources = customWrapRoute({
visibility: 'anything',
},
});
const operationalLearning = customWrapRoute({
parent: rootLayout,
path: 'operational-learning',
component: {
render: () => import('#views/OperationalLearning'),
props: {},
},
wrapperComponent: Auth,
context: {
title: 'Operational Learning',
visibility: 'anything',
},
});

const search = customWrapRoute({
parent: rootLayout,
Expand Down Expand Up @@ -1227,6 +1240,7 @@ const wrappedRoutes = {
perPrioritizationForm,
perWorkPlanForm,
threeWProjectDetail,
operationalLearning,
...regionRoutes,
...countryRoutes,
...surgeRoutes,
Expand Down
2 changes: 2 additions & 0 deletions app/src/components/Navbar/i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@
"userMenuOperationalToolbox":"Operational Toolbox",
"userMenuCatalogueSurgeServices":"Catalogue of Surge services",
"userMenuLearnLabel":"Learn",
"userMenuOperationalLearning":"Operational Learning",
"userMenuTools":"Tools",
"userMenuResources":"Resources",
"userMenuGoBlog":"GO Blog",
"userMenuOperationalLearningDescription":"Operational learning in emergencies is the lesson learned from managing and dealing with crises, refining protocols for resource allocation, decision-making, communication strategies, and others.",
"userMenuOperationalToolboxItem":"Operational Toolbox",
"userMenuOperationalToolboxItemDescription":"This operational toolbox is a central repository with key operational document helpful for your mission like templates, checklists, guidance and examples.",
"userMenuCatalogueSurgeServicesItem": "Catalogue of Surge services",
Expand Down
25 changes: 23 additions & 2 deletions app/src/components/Navbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ function Navbar(props: Props) {
type RespondOptionKey = 'emergencies' | 'early-warning' | 'dref-process' | 'surge';
const [activeRespondOption, setActiveRespondOption] = useState<RespondOptionKey>('emergencies');

type LearnOptionKey = 'tools' | 'resources';
const [activeLearnOption, setActiveLearnOption] = useState<LearnOptionKey>('tools');
type LearnOptionKey = 'tools' | 'resources' | 'operational-learning';
const [activeLearnOption, setActiveLearnOption] = useState<LearnOptionKey>('operational-learning');

return (
<nav className={_cs(styles.navbar, className)}>
Expand Down Expand Up @@ -414,6 +414,12 @@ function Navbar(props: Props) {
className={styles.optionList}
contentClassName={styles.optionListContent}
>
<Tab
name="operational-learning"
className={styles.option}
>
{strings.userMenuOperationalLearning}
</Tab>
<Tab
name="tools"
className={styles.option}
Expand All @@ -438,6 +444,21 @@ function Navbar(props: Props) {
</DropdownMenuItem>
</TabList>
<div className={styles.optionBorder} />
<TabPanel
name="operational-learning"
className={styles.optionDetail}
>
<DropdownMenuItem
type="link"
to="operationalLearning"
variant="tertiary"
>
{strings.userMenuOperationalLearning}
</DropdownMenuItem>
<div className={styles.description}>
{strings.userMenuOperationalLearningDescription}
</div>
</TabPanel>
<TabPanel
name="tools"
className={styles.optionDetail}
Expand Down
4 changes: 3 additions & 1 deletion app/src/components/domain/CountryMultiSelectInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Props<NAME> = MultiSelectInputProps<
name: NAME;
onChange: (newValue: number[], name: NAME) => void;
value: number[] | undefined | null;
filterByRegion?: number | undefined;
}

function CountrySelectInput<const NAME>(props: Props<NAME>) {
Expand All @@ -31,10 +32,11 @@ function CountrySelectInput<const NAME>(props: Props<NAME>) {
name,
onChange,
value,
filterByRegion,
...otherProps
} = props;

const countries = useCountry();
const countries = useCountry({ region: filterByRegion });

return (
<MultiSelectInput
Expand Down
6 changes: 4 additions & 2 deletions app/src/components/domain/CountrySelectInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ type Props<NAME> = SelectInputProps<
> & {
className?: string;
name: NAME;
onChange: (newValue: number | undefined, name: NAME) => void;
onChange: (newValue: number | undefined, name: NAME, option: Country | undefined) => void;
value: number | undefined | null;
regionId?: number;
}

function CountrySelectInput<const NAME>(props: Props<NAME>) {
Expand All @@ -31,10 +32,11 @@ function CountrySelectInput<const NAME>(props: Props<NAME>) {
name,
onChange,
value,
regionId,
...otherProps
} = props;

const countries = useCountry();
const countries = useCountry({ region: regionId });

return (
<SelectInput
Expand Down
6 changes: 5 additions & 1 deletion app/src/components/domain/DisasterTypeSelectInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ type Props<NAME> = SelectInputProps<
> & {
className?: string;
name: NAME;
onChange: (newValue: number | undefined, name: NAME) => void;
onChange: (
newValue: number | undefined,
name: NAME,
option: DisasterTypeItem | undefined,
) => void;
value: number | undefined | null;
optionsFilter?: (item: DisasterTypeItem) => boolean;
}
Expand Down
3 changes: 2 additions & 1 deletion app/src/components/domain/ExportButton/i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"namespace": "common",
"strings": {
"exportTableButtonLabel": "Export",
"exportTableDownloadingButtonLabel": "Downloading... ({progress}%)"
"exportTableDownloadingButtonLabel": "Downloading... ({progress}%)",
"pendingExportLabel": "Downloading..."
}
}
28 changes: 16 additions & 12 deletions app/src/components/domain/ExportButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import i18n from './i18n.json';
interface Props {
onClick: () => void;
disabled?: boolean;
progress: number;
progress?: number;
pendingExport: boolean;
totalCount: number | undefined;
}
Expand All @@ -31,20 +31,24 @@ function ExportButton(props: Props) {
if (!pendingExport) {
return strings.exportTableButtonLabel;
}
return resolveToComponent(
strings.exportTableDownloadingButtonLabel,
{
progress: (
<NumberOutput
value={progress * 100}
maximumFractionDigits={0}
/>
),
},
);
if (progress) {
return resolveToComponent(
strings.exportTableDownloadingButtonLabel,
{
progress: (
<NumberOutput
value={progress * 100}
maximumFractionDigits={0}
/>
),
},
);
}
return strings.pendingExportLabel;
}, [
strings.exportTableButtonLabel,
strings.exportTableDownloadingButtonLabel,
strings.pendingExportLabel,
progress,
pendingExport,
]);
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/domain/RegionSelectInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type Props<NAME> = SelectInputProps<
> & {
className?: string;
name: NAME;
onChange: (newValue: RegionOption['key'] | undefined, name: NAME) => void;
onChange: (newValue: RegionOption['key'] | undefined, name: NAME, value: RegionOption | undefined) => void;
value: RegionOption['key'] | undefined | null;
}

Expand Down
10 changes: 9 additions & 1 deletion app/src/contexts/domain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { createContext } from 'react';

import { type GoApiResponse } from '#utils/restRequest';

export type CacheKey = 'country' | 'global-enums' | 'disaster-type' | 'user-me' | 'region';
export type CacheKey = 'country' | 'global-enums' | 'disaster-type' | 'user-me' | 'region' | 'secondary-sector' | 'per-components';

export type GlobalEnums = Partial<GoApiResponse<'/api/v2/global-enums/'>>;
export type Countries = GoApiResponse<'/api/v2/country/'>;
export type DisasterTypes = GoApiResponse<'/api/v2/disaster_type/'>;
export type UserMe = GoApiResponse<'/api/v2/user/me/'>;
export type Regions = GoApiResponse<'/api/v2/region/'>;
export type SecondarySectors = GoApiResponse<'/api/v2/secondarysector'>;
export type PerComponents = GoApiResponse<'/api/v2/per-formcomponent/'>;

export interface Domain {
register: (name: CacheKey) => void;
Expand All @@ -28,6 +30,12 @@ export interface Domain {

globalEnums?: GlobalEnums;
globalEnumsPending?: boolean;

secondarySectors?: SecondarySectors;
secondarySectorsPending?: boolean;

perComponents?: PerComponents;
perComponentsPending?: boolean;
}

const DomainContext = createContext<Domain>({
Expand Down
56 changes: 56 additions & 0 deletions app/src/hooks/domain/usePerComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
useContext,
useEffect,
useMemo,
} from 'react';
import { isDefined } from '@togglecorp/fujs';

import DomainContext, { PerComponents } from '#contexts/domain';

export type PerComponent = NonNullable<PerComponents['results']>[number];

type ListProps = {
id?: never;
}

type PropsForId = {
id: number;
}

function usePerComponent(props?: ListProps): Array<PerComponent> | undefined
function usePerComponent(props: PropsForId): PerComponent | undefined
function usePerComponent(
props?: ListProps | PropsForId,
): PerComponent | undefined | Array<PerComponent> | undefined {
const {
register,
perComponents,
} = useContext(DomainContext);

useEffect(
() => {
register('per-components');
},
[register],
);

const returnValue = useMemo(
() => {
const id = props?.id;
if (isDefined(id)) {
return perComponents
?.results?.find((perComponent) => perComponent.id === id);
}

// NOTE: we need to filter out parent components
return perComponents?.results?.filter(
(perComponent) => !perComponent.is_parent,
);
},
[perComponents, props?.id],
);

return returnValue;
}

export default usePerComponent;
52 changes: 52 additions & 0 deletions app/src/hooks/domain/useSecondarySector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
useContext,
useEffect,
useMemo,
} from 'react';
import { isDefined } from '@togglecorp/fujs';

import DomainContext, { type SecondarySectors } from '#contexts/domain';

export type SecondarySector = NonNullable<SecondarySectors>[number];

type ListProps = {
id?: never;
}

type PropsForId = {
id: number;
}

function useSecondarySector(props?: ListProps): Array<SecondarySector> | undefined
function useSecondarySector(props: PropsForId): SecondarySector | undefined
function useSecondarySector(
props?: ListProps | PropsForId,
): SecondarySector | undefined | Array<SecondarySector> | undefined {
const {
register,
secondarySectors,
} = useContext(DomainContext);

useEffect(
() => {
register('secondary-sector');
},
[register],
);

const returnValue = useMemo(
() => {
const id = props?.id;
if (isDefined(id)) {
return secondarySectors?.find((secondaryTag) => secondaryTag.key === id);
}

return secondarySectors;
},
[secondarySectors, props?.id],
);

return returnValue;
}

export default useSecondarySector;
5 changes: 5 additions & 0 deletions app/src/hooks/useFilterState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,14 @@ function useFilterState<FILTER extends object>(options: {
() => hasSomeDefinedValue(debouncedState.filter),
[debouncedState.filter],
);
const rawFiltered = useMemo(
() => hasSomeDefinedValue(state.filter),
[state.filter],
);

return {
rawFilter: state.filter,
rawFiltered,

filter: debouncedState.filter,
filtered,
Expand Down
Loading

0 comments on commit 03afdcc

Please sign in to comment.