Skip to content

Commit

Permalink
feat: Action Menu (#7633)
Browse files Browse the repository at this point in the history
* feat: implement action menu

* chore: export `IProps`

* feat: enable keyboard navigation

* feat: implement `Dropdown` component

* feat: make dropdownMenu keyboard accessible

* feat: make dropdownMenu close on click outside and esc

* fix: make some props optional

* fix: close menu on ouside click

* feat: create actionMenu

* feat: add `Update`, `Print`, `Issue`, `Delete` items

* feat: implement delete declaration

* feat: redirect to home after deleting draft

* feat: add label for assigned to someone else

* refactor: move actionItems

* amend: add missing props

* feat: add scope and other checks for: correct record

* feat: add scope and other checks for: archive declaration

* feat: add scope and other checks for: reinstate declaration

* refactor: restructure types

* feat: add scope and other checks for: review

* chore: add todo

* feat: add scope and other checks for: update declaration

* feat: add scope and other checks for: print declaration

* chore: remove console.log

* feat: add scope and other checks for: issue certificate

* feat: add scope and other checks for: delete declaration

* refactor: change order of items

* wip

* Revert "wip"

This reverts commit b25bfa1.

* feat: implement unassign button

* fix: font and color

* fix: keyDown behaviour

* chore: remove record audit buttons

* refactor: use dropdownMenu to refactor toggleMenu

* chore: remove action from component

* chore: deprecate toggleMenu

* feat: move self unassign to actionMenu

* chore: dont unassign from download button

* chore: remove unused imports

* chore: update changelog

* fix: RA will see "correct record" button

* fix: add id in dropdown menu

* refactor: `isDownloadable` logic

* refactor: remove types from actionMessages

* refactor: use `useIntl` and `useDispatch` hooks instead of props drilling

* refactor: dont pass assignment to unassign comp

* refactor: use `offsetX` and `offsetY` instead of `offset_x` and `offset_y`

* refactor: use `<Button>` instead of `<PrimaryButton>`

* refactor: remove unnecessary `<div>`s

* feat: use `anchor` and `popover` api to toggle the dropdown visivility

* fix: focus

* chore: remove as string

* refactor: early return if condition fails

* refactor: move declaration status logic into declarations/utils

* refactor: change type of status to `SUBMISSION_STATUS`

* fix: handle multiple dropdown

* fix: styles

* fix: position options and story

* fix: close dropdown on action click

* refactor: align EVENT in common with Event in client

* refactor: pass id directly to provider

* test: add unit test for view action

* test: cover all statuses for view action

* test: add tests for review action

* test: add tests for update action

* test: add tests for archive action

* test: add tests for reinstate action

* test: add tests for print action

* test: add tests for issue action

* test: refactor:  centralize scoeps

* test: add test for not having scope

* test: add tests for correct action

* test: add tests for delete action

* test: add tests for unassign action

* test: add tests for assignment text

* fix: unhandled errors

* chore: remove old tests

* fix: use EVENT.Birth instead of "Birth"

* fix: use EVENT.Birth instead of "Birth"

* fix: eventToggle id in test

* fix: eventToggle id in test

* fix: ids in UserList.test

* fix: ids in UserAudit.test

* fix: import Event

* fix: remove unassign test from download button

* fix: ids in ProfileMenu.test

* fix: import Event

* fix: import Event

---------

Co-authored-by: Riku Rouvila <[email protected]>
  • Loading branch information
jamil314 and rikukissa authored Oct 28, 2024
1 parent 32b3e39 commit 2f630b3
Show file tree
Hide file tree
Showing 38 changed files with 3,968 additions and 769 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- Two new statuses of record are added: `Validated` and `Correction Requested` for advanced search parameters [#6365](https://github.com/opencrvs/opencrvs-core/issues/6365)
- A new field: `Time Period` is added to advanced search [#6365](https://github.com/opencrvs/opencrvs-core/issues/6365)
- Deploy UI-Kit Storybook to [opencrvs.pages.dev](https://opencrvs.pages.dev) to allow extending OpenCRVS using the component library
- Record audit action buttons are moved into action menu [#7390](https://github.com/opencrvs/opencrvs-core/issues/7390)
- Reoder the sytem user add/edit field for surname to be first, also change labels from `Last name` to `User's surname` and lastly remove the NID question from the form [#6830](https://github.com/opencrvs/opencrvs-core/issues/6830)
- Auth now allows exchanging user's token for a new record-specific token [#7728](https://github.com/opencrvs/opencrvs-core/issues/7728)

Expand Down
22 changes: 14 additions & 8 deletions packages/client/src/components/ProfileMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ describe('when user opens profile menu without user details', () => {
})

it('open menu', () => {
component.find('#ProfileMenuToggleButton').hostNodes().simulate('click')
component.find('#ProfileMenu-dropdownMenu').hostNodes().simulate('click')

expect(component.find('#ProfileMenuSubMenu').hostNodes()).toHaveLength(1)
expect(
component.find('#ProfileMenu-Dropdown-Content').hostNodes()
).toHaveLength(1)
})
})

Expand All @@ -53,27 +55,31 @@ describe('when user opens profile menu with user details', () => {
})

it('open menu', () => {
component.find('#ProfileMenuToggleButton').hostNodes().simulate('click')
component.find('#ProfileMenu-dropdownMenu').hostNodes().simulate('click')

expect(component.find('#ProfileMenuSubMenu').hostNodes()).toHaveLength(1)
expect(
component.find('#ProfileMenu-Dropdown-Content').hostNodes()
).toHaveLength(1)
})

it('handle clicks', () => {
component.find('#ProfileMenuToggleButton').hostNodes().simulate('click')
component.find('#ProfileMenu-dropdownMenu').hostNodes().simulate('click')

// Settings click
component
.find('#ProfileMenuSubMenu')
.find('#ProfileMenu-Dropdown-Content')
.hostNodes()
.childAt(1)
.simulate('click')

component
.find('#ProfileMenuSubMenu')
.find('#ProfileMenu-Dropdown-Content')
.hostNodes()
.childAt(2)
.simulate('click')

expect(component.find('#ProfileMenuSubMenu').hostNodes()).toHaveLength(1)
expect(
component.find('#ProfileMenu-Dropdown-Content').hostNodes()
).toHaveLength(1)
})
})
37 changes: 0 additions & 37 deletions packages/client/src/components/interface/DownloadButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,15 @@ import * as React from 'react'
import { DownloadAction } from '@client/forms'
import { ReactWrapper } from 'enzyme'
import * as declarationReducer from '@client/declarations'
import { vi, SpyInstance } from 'vitest'
import { ApolloClient } from '@apollo/client'
import { createClient } from '@client/utils/apolloClient'

const { DOWNLOAD_STATUS } = declarationReducer

function getAssignmentModal(
testComponent: ReactWrapper<{}, {}>
): ReactWrapper<{}, {}> {
const button = testComponent.find('button')
button.simulate('click')
testComponent.update()
return testComponent.find('#assignment').hostNodes()
}

function clickOnModalAction(
testComponent: ReactWrapper<{}, {}>,
selector: string
) {
const modal = getAssignmentModal(testComponent)
const action = modal.find(selector).hostNodes()
action.simulate('click')
testComponent.update()
}

describe('download button tests', () => {
let store: AppStore
let history: History<unknown>
let testComponent: ReactWrapper<{}, {}>
let unassignSpy: SpyInstance
let client: ApolloClient<{}>

describe('for download status downloaded', () => {
Expand Down Expand Up @@ -73,15 +52,9 @@ describe('download button tests', () => {
it('download button renders', () => {
expect(testComponent).toBeDefined()
})

it('clicking download button pops up unassign modal', () => {
const modal = getAssignmentModal(testComponent)
expect(modal.text()).toContain('Unassign record?')
})
})
describe('when assignment object is defined in props', () => {
beforeEach(async () => {
unassignSpy = vi.spyOn(declarationReducer, 'unassignDeclaration')
const testStore = await createTestStore()
store = testStore.store
history = testStore.history
Expand Down Expand Up @@ -110,16 +83,6 @@ describe('download button tests', () => {
it('download button renders', () => {
expect(testComponent).toBeDefined()
})

it('clicking download button pops up unassign modal', () => {
const modal = getAssignmentModal(testComponent)
expect(modal.text()).toContain('Unassign record?')
})

it('clicking on unassign button triggers unassignDeclaration action', () => {
clickOnModalAction(testComponent, '#unassign')
expect(unassignSpy).toBeCalled()
})
})
})
})
8 changes: 6 additions & 2 deletions packages/client/src/components/interface/DownloadButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,11 @@ function DownloadButtonComponent(props: DownloadButtonProps & HOCProps) {
// field agents can only retrieve declarations
const isNotFieldAgent = !FIELD_AGENT_ROLES.includes(String(userRole))

const onClickDownload = useCallback(
const isDownloadable =
status !== DOWNLOAD_STATUS.DOWNLOADED &&
(!assignment || assignment.practitionerId === practitionerId)

const onDownloadClick = useCallback(
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
if (
(assignment?.practitionerId !== practitionerId ||
Expand Down Expand Up @@ -337,7 +341,7 @@ function DownloadButtonComponent(props: DownloadButtonProps & HOCProps) {
<DownloadAction
type="icon"
id={`${id}-icon${isFailed ? `-failed` : ``}`}
onClick={onClickDownload}
onClick={isDownloadable ? onDownloadClick : undefined}
className={className}
aria-label={intl.formatMessage(constantsMessages.assignRecord)}
>
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/declarations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1373,7 +1373,7 @@ export const declarationsReducer: LoopReducer<IDeclarationsState, Action> = (
const orignalAppliation: IDeclaration = {
...correction,
data: {
...correction.originalData
...correction?.originalData
}
}

Expand Down
65 changes: 65 additions & 0 deletions packages/client/src/declarations/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 { EVENT_STATUS } from '@client/workqueue'
import { SUBMISSION_STATUS } from '.'

export const isPendingCorrection = (status?: SUBMISSION_STATUS) =>
status === EVENT_STATUS.CORRECTION_REQUESTED

export const isReviewableDeclaration = (status?: SUBMISSION_STATUS) =>
status && [EVENT_STATUS.DECLARED, EVENT_STATUS.VALIDATED].includes(status)

export const isUpdatableDeclaration = (status?: SUBMISSION_STATUS) =>
status &&
[
SUBMISSION_STATUS.DRAFT,
EVENT_STATUS.IN_PROGRESS,
EVENT_STATUS.REJECTED
].includes(status)

export const isPrintable = (status?: SUBMISSION_STATUS) =>
status &&
[SUBMISSION_STATUS.REGISTERED, SUBMISSION_STATUS.ISSUED].includes(status)

export const isCertified = (status?: SUBMISSION_STATUS) =>
status === SUBMISSION_STATUS.CERTIFIED

export const isRecordOrDeclaration = (status?: SUBMISSION_STATUS) =>
status
? [
SUBMISSION_STATUS.REGISTERED,
SUBMISSION_STATUS.CORRECTION_REQUESTED,
SUBMISSION_STATUS.CERTIFIED
].includes(status)
? 'record'
: 'declaration'
: ''

export const canBeCorrected = (status?: SUBMISSION_STATUS) =>
status &&
[
SUBMISSION_STATUS.REGISTERED,
SUBMISSION_STATUS.CERTIFIED,
SUBMISSION_STATUS.ISSUED
].includes(status)

export const isArchivable = (status?: SUBMISSION_STATUS) =>
status &&
[
SUBMISSION_STATUS.IN_PROGRESS,
SUBMISSION_STATUS.DECLARED,
SUBMISSION_STATUS.VALIDATED,
SUBMISSION_STATUS.REJECTED
].includes(status)

export const isArchived = (status?: SUBMISSION_STATUS) =>
status === SUBMISSION_STATUS.ARCHIVED
2 changes: 1 addition & 1 deletion packages/client/src/i18n/messages/buttons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ interface IButtonsMessages
const messagesToDefine: IButtonsMessages = {
archive: {
id: 'buttons.archive',
defaultMessage: 'Archive',
defaultMessage: 'Archive declaration',
description: 'Archive button text'
},
approve: {
Expand Down
71 changes: 71 additions & 0 deletions packages/client/src/i18n/messages/views/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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 { defineMessages } from 'react-intl'

const messagesToDefine = {
action: {
defaultMessage: 'Action',
description: 'Label for action button in dropdown menu',
id: 'action.action'
},
assignedTo: {
defaultMessage: 'Assigned to {name} at {officeName}',
description: 'Label for assignee',
id: 'action.assignee'
},
view: {
defaultMessage: 'View {recordOrDeclaration}',
description: 'Label for view button in dropdown menu',
id: 'action.view'
},
correctRecord: {
defaultMessage: 'Correct Record',
description: 'Label for correct record button in dropdown menu',
id: 'action.correct'
},
archiveRecord: {
defaultMessage: 'Archive Record',
description: 'Label for archive record button in dropdown menu',
id: 'action.archive'
},
reinstateRecord: {
defaultMessage: 'Reinstate Record',
description: 'Label for reinstate record button in dropdown menu',
id: 'action.reinstate'
},
reviewCorrection: {
defaultMessage: 'Review correction request',
description: 'Label for review correction request button in dropdown menu',
id: 'action.review.correction'
},
reviewDeclaration: {
defaultMessage:
'Review {isDuplicate, select, true{potential duplicate} other{declaration}}',
description: 'Label for review record button in dropdown menu',
id: 'action.review.declaration'
},
updateDeclaration: {
defaultMessage: 'Update declaration',
description: 'Label for update record button in dropdown menu',
id: 'action.update'
},
printDeclaration: {
defaultMessage: 'Print certified copy',
description: 'Label for print certified copy in dropdown menu',
id: 'action.print'
},
issueCertificate: {
defaultMessage: 'Issue certificate',
description: 'Label for issue certificate in dropdown menu',
id: 'action.issue'
}
}
export const messages = defineMessages(messagesToDefine)
9 changes: 5 additions & 4 deletions packages/client/src/search/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,28 @@ import { formatLongDate } from '@client/utils/date-formatting'
import {
HumanName,
EventSearchSet,
SearchEventsQuery
SearchEventsQuery,
Event
} from '@client/utils/gateway'
import { EMPTY_STRING, LANG_EN } from '@client/utils/constants'
import { ITaskHistory } from '@client/declarations'

export const isBirthEvent = (
req: EventSearchSet
): req is GQLBirthEventSearchSet => {
return req.type === 'Birth'
return req.type === Event.Birth
}

export const isDeathEvent = (
req: EventSearchSet
): req is GQLDeathEventSearchSet => {
return req.type === 'Death'
return req.type === Event.Death
}

export const isMarriageEvent = (
reg: EventSearchSet
): reg is GQLMarriageEventSearchSet => {
return reg.type === 'Marriage'
return reg.type === Event.Marriage
}

export const transformData = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
REMOVE_ADVANCED_SEARCH_RESULT_BOOKMARK_MUTATION
} from '@client/profile/mutations'
import { getStorageUserDetailsSuccess } from '@client/profile/profileActions'
import { Event } from '@client/utils/gateway'

const graphqlMock = [
{
Expand All @@ -54,7 +55,7 @@ const graphqlMock = [
results: [
{
id: 'bc09200d-0160-43b4-9e2b-5b9e90424e95',
type: 'Death',
type: Event.Death,
__typename: 'X',
registration: {
__typename: 'X',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { waitForElement } from '@client/tests/wait-for-element'
import { Workqueue } from '@opencrvs/components/lib/Workqueue'
import { History } from 'history'
import { vi, Mock } from 'vitest'
import { Event } from '@client/utils/gateway'

const EVENT_CREATION_TIME = 1583322631424 // Wed Mar 04 2020 13:50:31 GMT+0200 (Eastern European Standard Time)
const SEND_FOR_VALIDATION_TIME = 1582912800000 // Fri Feb 28 2020 20:00:00 GMT+0200 (Eastern European Standard Time)
Expand All @@ -42,7 +43,7 @@ const getItem = window.localStorage.getItem as Mock

const birthEventSearchSet: GQLBirthEventSearchSet = {
id: '956281c9-1f47-4c26-948a-970dd23c4094',
type: 'Birth',
type: Event.Birth,
registration: {
status: 'WAITING_VALIDATION',
contactNumber: '01622688231',
Expand Down
Loading

0 comments on commit 2f630b3

Please sign in to comment.