Skip to content

Commit

Permalink
Add ResourceListPageContent component
Browse files Browse the repository at this point in the history
  • Loading branch information
vesnushka committed Jan 28, 2025
1 parent 5bd579b commit c889d1c
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { S } from "./styles";

export type PageContainerContentProps = React.HTMLAttributes<HTMLDivElement> & {
/* Page content max width */
maxWidth?: number | string;

/**
* PageContainerContent (level=2) can be nested in other PageContainerContent (level=1)
* e.g. ResourceListPageContent use PageContainerContent (level=2)
*
* Styles of the PageContainerContent will be adjusted for provided level.
*/
level?: 1 | 2;
};

export function PageContainerContent(props: PageContainerContentProps) {
const { maxWidth, ...rest } = props;
const { maxWidth, level = 1, ...rest } = props;

return (
<S.PageContentContainer>
<S.PageContent {...rest} $maxWidth={maxWidth} />
<S.PageContentContainer $level={level}>
<S.PageContent {...rest} $maxWidth={maxWidth} $level={level} />
</S.PageContentContainer>
);
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
import styled from 'styled-components';
import styled, { css } from 'styled-components';

import { maxWidthStyles } from '../PageContainerHeader/styles';

export const S = {
PageContentContainer: styled.div`
PageContentContainer: styled.div<{ $level: 1 | 2 }>`
padding: 0 24px;
display: flex;
flex-direction: column;
align-items: center;
${({ $level }) =>
$level === 2 &&
css`
padding: 0;
`}
`,
PageContent: styled.div<{ $maxWidth?: number | string }>`
PageContent: styled.div<{ $level: 1 | 2, $maxWidth?: number | string }>`
flex: 1;
display: flex;
flex-direction: column;
padding: 32px 0;
gap: 24px 0;
width: 100%;
${({ $level }) =>
$level === 2 &&
css`
padding: 0;
`}
${maxWidthStyles}
`,
};
5 changes: 3 additions & 2 deletions src/components/SearchBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import { useMemo } from 'react';

interface SearchBarProps extends SearchBarData {
showInDrawerOnMobile?: boolean;
level?: 1 | 2;
}

export function SearchBar(props: SearchBarProps) {
const { columnsFilterValues, onChangeColumnFilter, onResetFilters, showInDrawerOnMobile = true } = props;
const { columnsFilterValues, onChangeColumnFilter, onResetFilters, showInDrawerOnMobile = true, level = 1 } = props;
const searchBarFilterValues = useMemo(
() => columnsFilterValues.filter((filter) => isSearchBarFilter(filter)),
[JSON.stringify(columnsFilterValues)],
Expand All @@ -36,7 +37,7 @@ export function SearchBar(props: SearchBarProps) {
<Trans>Clear filters</Trans>
</Button>
</S.SearchBar>
<S.MobileFilters $showInDrawerOnMobile={showInDrawerOnMobile}>
<S.MobileFilters $showInDrawerOnMobile={showInDrawerOnMobile} $level={level}>
<SearchBarMobile {...props} />
</S.MobileFilters>
</>
Expand Down
9 changes: 8 additions & 1 deletion src/components/SearchBar/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,18 @@ export const S = {
}
`}
`,
MobileFilters: styled.div<{ $showInDrawerOnMobile?: boolean }>`
MobileFilters: styled.div<{ $level: 1 | 2; $showInDrawerOnMobile?: boolean }>`
position: absolute;
right: 24px;
top: 24px;
${({ $level }) =>
$level === 2 &&
css`
right: 0;
top: 0;
`}
${({ $showInDrawerOnMobile }) =>
$showInDrawerOnMobile &&
css`
Expand Down
60 changes: 54 additions & 6 deletions src/containers/PatientDetails/new.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import { Encounter, Patient } from 'fhir/r4b';

import { ResourceListPage } from 'src/components';
import { DetailPage, Tab } from 'src/uberComponents/DetailPage';
import { compileAsFirst } from 'src/utils';
import { compileAsFirst, formatPeriodDateTime } from 'src/utils';

import { PatientOverview } from './PatientOverviewDynamic';
import { ResourceListPageContent } from 'src/uberComponents/ResourceListPageContent';
import { navigationAction, questionnaireAction } from 'src/uberComponents';
import { t, Trans } from '@lingui/macro';
import { SearchBarColumnType } from 'src/components/SearchBar/types';
import { PlusOutlined } from '@ant-design/icons';

const getName = compileAsFirst<Patient, string>("Patient.name.given.first() + ' ' + Patient.name.family");

function PatientEncounter({ patient }: { patient: Patient }) {
return (
<ResourceListPage<Encounter>
headerTitle="Encounters"
<ResourceListPageContent<Encounter>
resourceType="Encounter"
searchParams={{ patient: patient.id! }}
getTableColumns={() => [
{
title: 'Practitioner',
dataIndex: 'practitioner',
key: 'practitioner',
render: (_text: any, { resource }) => JSON.stringify(resource.participant),
render: (_text: any, { resource }) => resource.participant?.[0]?.individual?.display,
},
{
title: 'Status',
Expand All @@ -34,7 +37,52 @@ function PatientEncounter({ patient }: { patient: Patient }) {
dataIndex: 'date',
key: 'date',
width: 250,
render: (_text: any, { resource }) => JSON.stringify(resource.period),
render: (_text: any, { resource }) => formatPeriodDateTime(resource.period),
},
]}
getFilters={() => [
{
id: 'name',
searchParam: '_ilike',
type: SearchBarColumnType.STRING,
placeholder: t`Find encounter`,
placement: ['search-bar', 'table'],
},
{
id: 'status',
searchParam: 'status',
type: SearchBarColumnType.CHOICE,
placeholder: t`Choose status`,
options: [
{
value: {
Coding: {
code: 'in-progress',
display: 'In progress',
},
},
},
{
value: {
Coding: {
code: 'finished',
display: 'Finished',
},
},
},
],
placement: ['table', 'search-bar'],
},
]}
getRecordActions={(record) => [navigationAction('Open', `/patients2/${record.resource.id}/encounter`)]}
getHeaderActions={() => [
questionnaireAction(<Trans>Create encounter</Trans>, 'encounter-create', { icon: <PlusOutlined /> }),
]}
getBatchActions={() => [questionnaireAction(<Trans>Finish encounters</Trans>, '')]}
getReportColumns={(bundle) => [
{
title: t`Number of Encounters`,
value: bundle.total,
},
]}
/>
Expand Down
6 changes: 3 additions & 3 deletions src/uberComponents/ResourceListPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ interface ReportColumn {
value: React.ReactNode;
}

interface ResourceListPageProps<R extends Resource> {
export interface ResourceListPageProps<R extends Resource> {
/* Page header title (for example, Organizations) */
headerTitle: string;

Expand Down Expand Up @@ -246,7 +246,7 @@ interface ResourcesListPageReportProps<R> {
getReportColumns: (bundle: Bundle, reportBundle?: Bundle) => Array<ReportColumn>;
}

function ResourcesListPageReport<R>(props: ResourcesListPageReportProps<R>) {
export function ResourcesListPageReport<R>(props: ResourcesListPageReportProps<R>) {
const { recordResponse, getReportColumns } = props;
const emptyBundle: Bundle = { resourceType: 'Bundle', entry: [], type: 'searchset' };
const items =
Expand All @@ -257,7 +257,7 @@ function ResourcesListPageReport<R>(props: ResourcesListPageReportProps<R>) {
return <Report items={items} />;
}

function getRecordActionsColumn<R extends Resource>({
export function getRecordActionsColumn<R extends Resource>({
getRecordActions,
defaultLaunchContext,
reload,
Expand Down
159 changes: 159 additions & 0 deletions src/uberComponents/ResourceListPageContent/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { Trans } from '@lingui/macro';
import { Empty } from 'antd';
import { Bundle, Resource } from 'fhir/r4b';
import React, { useMemo } from 'react';

import { formatError } from '@beda.software/fhir-react';
import { isFailure, isLoading, isSuccess } from '@beda.software/remote-data';

import { SearchBar } from 'src/components/SearchBar';
import { useSearchBar } from 'src/components/SearchBar/hooks';
import { isTableFilter } from 'src/components/SearchBar/utils';
import { SpinIndicator } from 'src/components/Spinner';
import { Table } from 'src/components/Table';
import { populateTableColumnsWithFiltersAndSorts } from 'src/components/Table/utils';

import { HeaderQuestionnaireAction } from '../ResourceListPage/actions';
import { useResourceListPage } from '../ResourceListPage/hooks';
import { BatchActions } from '../ResourceListPage/BatchActions';
import { getRecordActionsColumn, ResourceListPageProps, ResourcesListPageReport } from '../ResourceListPage';
import { PageContainerContent } from 'src/components/BaseLayout/PageContainer/PageContainerContent';
import { S } from './styles';

type RecordType<R extends Resource> = { resource: R; bundle: Bundle };

type ResourceListPageContentProps<R extends Resource> = Omit<ResourceListPageProps<R>, 'headerTitle' | 'maxWidth'> & {};

export function ResourceListPageContent<R extends Resource>({
resourceType,
extractPrimaryResources,
searchParams,
getRecordActions,
getHeaderActions,
getBatchActions,
getFilters,
getTableColumns,
defaultLaunchContext,
getReportColumns,
}: ResourceListPageContentProps<R>) {
const allFilters = getFilters?.() ?? [];

const { columnsFilterValues, onChangeColumnFilter, onResetFilters } = useSearchBar({
columns: allFilters ?? [],
});
const tableFilterValues = useMemo(
() => columnsFilterValues.filter((filter) => isTableFilter(filter)),
[JSON.stringify(columnsFilterValues)],
);

const {
recordResponse,
reload,
pagination,
handleTableChange,
selectedRowKeys,
setSelectedRowKeys,
selectedResourcesBundle,
} = useResourceListPage(resourceType, extractPrimaryResources, columnsFilterValues, searchParams ?? {});

// TODO: move to hooks
const initialTableColumns = getTableColumns({ reload });
const tableColumns = populateTableColumnsWithFiltersAndSorts({
tableColumns: initialTableColumns,
filters: tableFilterValues,
onChange: onChangeColumnFilter,
});
const headerActions = getHeaderActions?.() ?? [];
const batchActions = getBatchActions?.() ?? [];

const renderHeader = () => {
const hasFilters = columnsFilterValues.length > 0;

if (!hasFilters && headerActions.length === 0) {
return null;
}

return (
<S.Header>
<S.HeaderLeftColumn>
{columnsFilterValues.length ? (
<SearchBar
columnsFilterValues={columnsFilterValues}
onChangeColumnFilter={onChangeColumnFilter}
onResetFilters={onResetFilters}
level={2}
/>
) : null}
</S.HeaderLeftColumn>
<S.HeaderRightColumn $hasFilters={hasFilters}>
{headerActions.map((action, index) => (
<React.Fragment key={index}>
<HeaderQuestionnaireAction
action={action}
reload={reload}
defaultLaunchContext={defaultLaunchContext ?? []}
/>
</React.Fragment>
))}
</S.HeaderRightColumn>
</S.Header>
);
};

return (
<PageContainerContent level={2}>
{renderHeader()}

{getReportColumns ? (
<ResourcesListPageReport recordResponse={recordResponse} getReportColumns={getReportColumns} />
) : null}

{batchActions.length ? (
<BatchActions
batchActions={batchActions}
selectedRowKeys={selectedRowKeys}
allKeys={isSuccess(recordResponse) ? recordResponse.data.map((d) => d.resource.id!) : []}
setSelectedRowKeys={setSelectedRowKeys}
reload={reload}
selectedResourcesBundle={selectedResourcesBundle}
defaultLaunchContext={defaultLaunchContext}
/>
) : null}

<Table<RecordType<R>>
pagination={pagination}
onChange={handleTableChange}
rowSelection={batchActions.length ? { selectedRowKeys, onChange: setSelectedRowKeys } : undefined}
locale={{
emptyText: isFailure(recordResponse) ? (
<>
<Empty
description={formatError(recordResponse.error)}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
</>
) : (
<>
<Empty description={<Trans>No data</Trans>} image={Empty.PRESENTED_IMAGE_SIMPLE} />
</>
),
}}
rowKey={(p) => p.resource.id!}
dataSource={isSuccess(recordResponse) ? recordResponse.data : []}
columns={[
...tableColumns,
...(getRecordActions
? [
getRecordActionsColumn({
getRecordActions,
reload,
defaultLaunchContext: defaultLaunchContext ?? [],
}),
]
: []),
]}
loading={isLoading(recordResponse) && { indicator: SpinIndicator }}
/>
</PageContainerContent>
);
}
Loading

0 comments on commit c889d1c

Please sign in to comment.