Skip to content

Commit

Permalink
Events V2: Implement Advanced Search (#8436)
Browse files Browse the repository at this point in the history
* feat: add advanced search route in v2

* chore: add type in route objects

* chore: add mock tabs in advance search component

* feat: add location search input type field

* fix: change type assertion for route object

* fix: move advance search messages in component

* chore: refactor location search field type into multiple fields

* chore: refactor location and adminstration area components

* chore: update advance search with fields and add story

* chore: add clickable tabs in advanced search and render fields from country repo

* chore: refactor advance search fields

* feat: add validation for advanced search field ids

* chore: amend active tab id in advanced search

* chore: amend advanced search config with type field

* chore: extract unique fields from form

* chore: remove unused message from advanced search

* fix: update field types

* chore: show advanced tab only when configured

* chore: add submit button in search tab

* chore: handle fieldId and values in state for searching

* chore: amend advanced search view

* chore: add advanced search fields in storybook

* chore: resolve dependency issues

* fix: add inferred pattern to config type exports

* chore: update location components import after with new name

* chore: make descriptive field configuration

* fix: amend review changes

* chore: refactor tab search into different component

* fix: amend event configuration backend call

* fix: amend redundant search router

* chore: update advanced search messages with v2 prefix

* chore: rename location filtering function

* chore: revert interfaces to types

* chore: refactor advanced search field validation

* fix: clean up variable names and refactor advanced search section component

---------

Co-authored-by: Markus <[email protected]>
  • Loading branch information
Nil20 and makelicious authored Feb 14, 2025
1 parent 5e30acb commit 1f8df12
Show file tree
Hide file tree
Showing 29 changed files with 744 additions and 132 deletions.
5 changes: 0 additions & 5 deletions packages/client/src/offline/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,6 @@ export const getAdminStructureLocations = createSelector(
(data) => data.locations
)

export const getFacilityLocations = createSelector(
getOfflineData,
(data) => data.facilities
)

export const selectCountryBackground = (store: IStoreState) => {
const countryBackground = getKey(store, 'offlineData').config
?.LOGIN_BACKGROUND
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,16 @@ import {
FileFieldValue,
getConditionalActionsForField,
isAddressFieldType,
isAdministrativeAreaFieldType,
isFacilityFieldType,
isBulletListFieldType,
isCheckboxFieldType,
isCountryFieldType,
isDateFieldType,
isDividerFieldType,
isFileFieldType,
isLocationFieldType,
isOfficeFieldType,
isPageHeaderFieldType,
isParagraphFieldType,
isRadioGroupFieldType,
Expand Down Expand Up @@ -72,11 +75,11 @@ import {
Checkbox,
Date as DateField,
RadioGroup,
Location,
LocationSearch,
Select,
SelectCountry,
Text
Text,
AdministrativeArea
} from '@client/v2-events/features/events/registered-fields'

import { SubHeader } from '@opencrvs/components'
Expand Down Expand Up @@ -352,35 +355,58 @@ const GeneratedInputField = React.memo(
</InputField>
)
}
if (isLocationFieldType(field)) {
if (field.config.options.type === 'HEALTH_FACILITY')
return (
<InputField {...inputFieldProps}>
<LocationSearch.Input
{...field.config}
value={field.value}
setFieldValue={setFieldValue}
/>
</InputField>
)

if (isAdministrativeAreaFieldType(field)) {
return (
<InputField {...inputFieldProps}>
<Location.Input
<AdministrativeArea.Input
{...field.config}
value={field.value}
setFieldValue={setFieldValue}
partOf={
(field.config.options?.partOf?.$data &&
(field.config.configuration?.partOf?.$data &&
(makeFormikFieldIdsOpenCRVSCompatible(formData)[
field.config.options?.partOf.$data
field.config.configuration?.partOf.$data
] as string | undefined | null)) ??
null
}
setFieldValue={setFieldValue}
/>
</InputField>
)
}

if (isLocationFieldType(field)) {
return (
<LocationSearch.Input
{...field.config}
value={field.value}
searchableResource={['locations']}
setFieldValue={setFieldValue}
/>
)
}

if (isOfficeFieldType(field)) {
return (
<LocationSearch.Input
{...field.config}
value={field.value}
searchableResource={['offices']}
setFieldValue={setFieldValue}
/>
)
}

if (isFacilityFieldType(field)) {
return (
<LocationSearch.Input
{...field.config}
value={field.value}
searchableResource={['facilities']}
setFieldValue={setFieldValue}
/>
)
}
if (isDividerFieldType(field)) {
return <Divider />
}
Expand Down
5 changes: 5 additions & 0 deletions packages/client/src/v2-events/components/forms/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import {
} from '@opencrvs/commons/client'
import { DependencyInfo } from '@client/forms'

/*
* Formik has a feature that automatically nests all form keys that have a dot in them.
* Because our form field ids can have dots in them, we temporarily transform those dots
* to a different character before passing the data to Formik. This function unflattens
*/
export const FIELD_SEPARATOR = '____'

export function handleInitialValue(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* OpenCRVS is also distributed under the terms of the Civil Registration
* & Healthcare Disclaimer located at http://opencrvs.org/license.
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/
import { Meta, StoryObj } from '@storybook/react'
import React from 'react'
import { createTRPCMsw, httpLink } from '@vafanassieff/msw-trpc'
import superjson from 'superjson'
import { TRPCProvider, AppRouter } from '@client/v2-events/trpc'
import { ROUTES } from '@client/v2-events/routes'
import { tennisClubMembershipEvent } from '@client/v2-events/features/events/fixtures'
import AdvancedSearch from './AdvancedSearch'

const meta: Meta<typeof AdvancedSearch> = {
title: 'AdvancedSearch',
component: AdvancedSearch,
decorators: [
(Story) => (
<TRPCProvider>
<Story />
</TRPCProvider>
)
]
}

const tRPCMsw = createTRPCMsw<AppRouter>({
links: [
httpLink({
url: '/api/events'
})
],
transformer: { input: superjson, output: superjson }
})

export default meta
type Story = StoryObj<typeof AdvancedSearch>

export const AdvancedSearchStory: Story = {
parameters: {
reactRouter: {
router: {
path: ROUTES.V2.ADVANCED_SEARCH.buildPath({}),
element: <AdvancedSearch />
},
initialPath: ROUTES.V2.ADVANCED_SEARCH.buildPath({})
},
msw: {
handlers: {
event: [
tRPCMsw.event.config.get.query(() => {
return [tennisClubMembershipEvent]
})
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* OpenCRVS is also distributed under the terms of the Civil Registration
* & Healthcare Disclaimer located at http://opencrvs.org/license.
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/
import React, { useState } from 'react'
import { defineMessages, useIntl } from 'react-intl'
import {
Content,
ContentSize,
FormTabs,
IFormTabProps
} from '@opencrvs/components'
import { useEventConfigurations } from '@client/v2-events/features/events/useEventConfiguration'
import { TabSearch } from './TabSearch'

const messagesToDefine = {
advancedSearch: {
id: 'v2.config.advanced.search',
defaultMessage: 'Advanced Search',
description: 'This is used for the advanced search'
},
advancedSearchInstruction: {
id: 'v2.config.advanced.search.instruction',
defaultMessage:
'Select the options to build an advanced search. A minimum of two search parameters is required.',
description: 'This is used for the advanced search'
}
}

const messages = defineMessages(messagesToDefine)

function AdvancedSearch() {
const intl = useIntl()
const allEvents = useEventConfigurations()

const advancedSearchEvents = allEvents.filter(
(event) => event.advancedSearch.length > 0
)

const formTabSections = advancedSearchEvents.map((a) => ({
id: a.id,
title: intl.formatMessage(a.label)
})) satisfies IFormTabProps['sections']

const [activeTabId, setActiveTabId] = useState<string>(formTabSections[0].id)

const handleTabClick = (tabId: string) => {
setActiveTabId(tabId)
}

const currentEvent = allEvents.find((e) => e.id === activeTabId)
if (!currentEvent) {
return null
}
const currentTabSections = currentEvent.advancedSearch

return (
<>
<Content
size={ContentSize.SMALL}
subtitle={intl.formatMessage(messages.advancedSearchInstruction)}
tabBarContent={
<FormTabs
activeTabId={activeTabId}
sections={formTabSections}
onTabClick={handleTabClick}
/>
}
title={intl.formatMessage(messages.advancedSearch)}
titleColor={'copy'}
>
{currentTabSections.length > 0 && (
<TabSearch currentEvent={currentEvent} />
)}
</Content>
</>
)
}

export default AdvancedSearch
Loading

0 comments on commit 1f8df12

Please sign in to comment.