Skip to content

Commit

Permalink
Implement Date Type Filter Selection (#12)
Browse files Browse the repository at this point in the history
* Add element multiselect, modals, builders, and mandatory elements.

* Prettier issues

* Add fhir-spec-tools

* Fixed redundancy with object.keys

* removed console log

* Implement type filter functionality.

* changed buttons and styles

* Type filter table

* Complete with datetime params not working

* Fixed issues with choiceType datetime parameters

* Fixed overlapping add filter button and submitting modal when some values are not given

* fixed whitespace and unused file

* package lock

* updated searchParameters

* Change to functional components.

* use new fhir-spec-tools version

* Revert changes to export

* Fix for error when manually enter bad type filter.

* Minor name changes

* Finally, type filter dates

* Small changes to date locale and style.
  • Loading branch information
cgolemme authored Aug 16, 2024
1 parent d6ca181 commit 058d86d
Show file tree
Hide file tree
Showing 8 changed files with 951 additions and 4,000 deletions.
4,396 changes: 453 additions & 3,943 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
"dependencies": {
"@mantine/code-highlight": "^7.11.1",
"@mantine/core": "^7.10.1",
"@mantine/dates": "^7.11.2",
"@mantine/form": "^7.11.1",
"@mantine/hooks": "^7.10.1",
"@mantine/modals": "^7.12.1",
"@mantine/notifications": "^7.11.1",
"@tabler/icons-react": "^3.6.0",
"dayjs": "^1.11.12",
"dotenv": "^16.4.5",
"eslint-config-prettier": "^9.1.0",
"fhir-spec-tools": "^0.1.0",
"fhir-spec-tools": "^0.2.0",
"filesize": "^10.1.2",
"ndjson": "^2.0.0",
"next": "14.2.3",
Expand Down
5 changes: 5 additions & 0 deletions src/app/global.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,8 @@
.MultiSelectStyles {
margin-left: 10px;
}

.modalHeader {
font-weight: bold;
font-size: var(--mantine-h2-font-size);
}
26 changes: 23 additions & 3 deletions src/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
'use client';

import { Card, createTheme, MantineProvider, MultiSelect } from '@mantine/core';
import { Card, createTheme, MantineProvider, MultiSelect, Select } from '@mantine/core';
import { DatesProvider } from '@mantine/dates';
import { ModalsProvider } from '@mantine/modals';
import { Notifications } from '@mantine/notifications';
import React from 'react';
import { RecoilRoot } from 'recoil';
import '@mantine/core/styles.css';
import '@mantine/dates/styles.css';

const theme = createTheme({
components: {
Expand All @@ -18,6 +22,16 @@ const theme = createTheme({
comboboxProps: { transitionProps: { transition: 'fade-down', duration: 200 }, offset: 0, shadow: 'lg' }
}
}),
Select: Select.extend({
defaultProps: {
size: 'md',
radius: 'md',
searchable: true,
withScrollArea: false,
styles: { dropdown: { maxHeight: 400, overflowY: 'auto' } },
comboboxProps: { transitionProps: { transition: 'fade-down', duration: 200 }, offset: 0, shadow: 'lg' }
}
}),
Card: Card.extend({
defaultProps: {
radius: 'md',
Expand All @@ -34,8 +48,14 @@ const theme = createTheme({
export default function Providers({ children }: { children: React.ReactNode }) {
return (
<MantineProvider theme={theme}>
<Notifications />
<RecoilRoot>{children}</RecoilRoot>
<RecoilRoot>
<ModalsProvider>
<Notifications />
<DatesProvider settings={{ locale: 'en', firstDayOfWeek: 0, weekendDays: [0], timezone: 'UTC' }}>
{children}
</DatesProvider>
</ModalsProvider>
</RecoilRoot>
</MantineProvider>
);
}
175 changes: 175 additions & 0 deletions src/components/parameter-pages/type-filter-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { Modal, Card, MultiSelect, Stack, Title, Button, ScrollAreaAutosize, Group } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { useState } from 'react';
import React from 'react';
import { useRecoilState } from 'recoil';
import { TypeFilterData, TypeFilterParamInput } from './type-filter-param-input';
import { typeFilterParamsState } from '@/state/type-filter-params-state';
import { searchParameters } from 'fhir-spec-tools/build/data/searchParameters';
import { modals } from '@mantine/modals';

export interface TypeFilterModalProps {
resourceType: string;
editingTypeFilter?: string;
}

/*
* Component for selecting type filters and entering values
*/
export default function TypeFilterModal({ resourceType, editingTypeFilter }: TypeFilterModalProps) {
const prevTypeFilter = editingTypeFilter ? createPreviouslyCreatedFilter(resourceType, editingTypeFilter) : undefined;

const [activeElements, setActiveElements] = useState<string[]>(prevTypeFilter?.elements ?? []);
const [stagedFilters, setStagedFilters] = useState<Record<string, string>>(prevTypeFilter?.prevFilterValues ?? {});
const [createdTypeParams, setCreatedTypeParams] = useState<TypeFilterData[]>(prevTypeFilter?.filterParams ?? []);

const [typeFilters, setTypeFilters] = useRecoilState(typeFilterParamsState);

const addFilter = (element: string, filterVal: string) =>
setStagedFilters(prev => ({ ...prev, [element]: filterVal }));

const handleConfirm = () => {
const filters = Object.entries(stagedFilters).map(elemVal => `${elemVal[0]}=${elemVal[1]}`);
const typeFilter = `${resourceType}?${filters.join('&')}`;

if (filters.length === 0) {
notifications.show({
title: 'Type filter not created',
color: 'red',
message: 'Type filter was not created because there were no values specified.'
});
return;
}

setTypeFilters(prev => [
...prev.filter(tyf => tyf.filter !== editingTypeFilter),
{ filter: typeFilter, active: true }
]);

editingTypeFilter
? notifications.show({
title: 'Updated type filter',
message: typeFilter
})
: notifications.show({
title: 'Created type filter',
message: typeFilter
});
};

const resourceSearchParams = searchParameters[resourceType];
if (!resourceSearchParams) {
notifications.show({
title: 'Cannot edit this type filter',
message: `Type filter: ${editingTypeFilter} is incorrectly formatted`,
color: 'red'
});
modals.closeAll();
return;
}
return (
<Modal.Body bg="gray.0" p="xl">
<Stack>
<Card>
<MultiSelect
label="Select Elements"
description="Select elements to add type filters"
placeholder="Search for elements"
nothingFoundMessage="No elements matching search found."
hidePickedOptions
value={activeElements}
data={Object.keys(resourceSearchParams)}
onChange={setActiveElements}
onOptionSubmit={element =>
setCreatedTypeParams([...createdTypeParams, { type: resourceType, element: element } as TypeFilterData])
}
onClear={() => {
setStagedFilters({});
setCreatedTypeParams([]);
}}
onRemove={element => {
setStagedFilters(prev => {
delete prev[element];
return prev;
});
setCreatedTypeParams(createdTypeParams.filter(filter => filter.element !== element));
}}
/>
</Card>
{createdTypeParams.length !== 0 && (
<Card>
<Stack>
<Title order={3}>Input Filter Values</Title>
<ScrollAreaAutosize mah={400} mx="auto" scrollbars="y" pl="lg" pr="lg" w="100%">
{createdTypeParams.map((filter, key) => (
<TypeFilterParamInput
key={key}
type={filter.type}
element={filter.element}
value={filter.value}
addFilter={addFilter}
/>
))}
</ScrollAreaAutosize>
</Stack>
</Card>
)}
<Group grow>
<Button
size="lg"
radius="md"
onClick={() => {
handleConfirm();
modals.closeAll();
}}
disabled={createdTypeParams.length === 0}
>
Confirm
</Button>
{editingTypeFilter && (
<Button
size="lg"
radius="md"
color="red"
onClick={() => {
setTypeFilters(typeFilters.filter(tyf => tyf.filter !== editingTypeFilter));
notifications.show({
title: 'Deleted type filter',
message: editingTypeFilter
});
modals.closeAll();
}}
>
Delete
</Button>
)}
</Group>
</Stack>
</Modal.Body>
);
}

function createPreviouslyCreatedFilter(type: string, typeFilter: string) {
const pattern = /[?&]([^=]+)=([^&]+)/g;
const queryParams: Record<string, string> = {};
let match;
while ((match = pattern.exec(typeFilter)) !== null) {
queryParams[match[1]] = match[2];
}

const elements = Object.keys(queryParams);

const filterParams = elements.map(
element =>
({
type: type,
element: element,
value: queryParams[element]
}) as TypeFilterData
);

const prevFilterValues: Record<string, string> = {};
elements.forEach(e => (prevFilterValues[e] = queryParams[e]));

return { filterParams, elements, prevFilterValues };
}
Loading

0 comments on commit 058d86d

Please sign in to comment.