diff --git a/src/components/sectionList/bulk/Bulk.module.css b/src/components/sectionList/bulk/Bulk.module.css
index 9b28fb3f..ebadd8ca 100644
--- a/src/components/sectionList/bulk/Bulk.module.css
+++ b/src/components/sectionList/bulk/Bulk.module.css
@@ -1,7 +1,7 @@
.bulkSharingWrapper {
display: flex;
flex-direction: column;
- gap: var(--spacers-dp8);
+ gap: var(--spacers-dp16);
}
.fieldWrapper {
@@ -18,7 +18,46 @@
.selectionWrapper {
display: flex;
gap: var(--spacers-dp4);
- background-color: var(--colors-red050);
+ background-color: var(--colors-grey050);
padding: var(--spacers-dp12);
- align-items: flex-end;
+}
+
+.addActionButton {
+ margin-bottom: -1px;
+ align-self: flex-end;
+}
+
+.actionCancelButton {
+ margin-inline-start: auto;
+}
+
+.actionSummary {
+ display: flex;
+ align-items: center;
+ gap: var(--spacers-dp16);
+ padding: 0 var(--spacers-dp8);
+}
+
+.actionSummaryDisplayName {
+ max-width: 350px;
+}
+
+.accessSummary {
+ display: flex;
+ gap: var(--spacers-dp8);
+ align-items: center;
+}
+
+.accessSummary span {
+ display: flex;
+ gap: var(--spacers-dp4);
+ align-items: center;
+}
+
+.actionNoAccess {
+ color: var(--colors-red600);
+}
+
+.actionAccessAdd {
+ color: var(--colors-green600);
}
diff --git a/src/components/sectionList/bulk/BulkActionSummary.tsx b/src/components/sectionList/bulk/BulkActionSummary.tsx
new file mode 100644
index 00000000..7bd21e51
--- /dev/null
+++ b/src/components/sectionList/bulk/BulkActionSummary.tsx
@@ -0,0 +1,100 @@
+import i18n from '@dhis2/d2-i18n'
+import { Button, Divider, IconAdd16, IconCross16 } from '@dhis2/ui'
+import React from 'react'
+import { PublicAccessPart, parsePublicAccessString } from '../../../lib'
+import css from './Bulk.module.css'
+import type { SharingAction } from './BulkSharing'
+
+type ActionSummaryProps = {
+ action: SharingAction
+ dataShareable: boolean
+ onRemove: () => void
+}
+export const ActionSummary = ({
+ action,
+ dataShareable,
+ onRemove,
+}: ActionSummaryProps) => (
+
+
+
+ {action.sharingEntity.displayName}
+
+
+
+
+
+
+)
+
+type ActionAccessSummaryProps = {
+ action: SharingAction
+ dataShareable: boolean
+}
+const ActionAccessSummary = ({
+ action,
+ dataShareable,
+}: ActionAccessSummaryProps) => {
+ const parsed = parsePublicAccessString(action.access)
+
+ if (parsed === null) {
+ return null
+ }
+
+ return (
+
+
+ {dataShareable && }
+
+ )
+}
+
+const MetadataAccess = ({ access }: { access: PublicAccessPart }) => {
+ const noAccess = access.read === false
+
+ if (noAccess) {
+ return
+ }
+ const label = access.write
+ ? i18n.t('Metadata view and edit')
+ : i18n.t('Metadata view only')
+
+ return
+}
+
+const DataAccess = ({ access }: { access: PublicAccessPart }) => {
+ const noAccess = access.read === false
+
+ if (noAccess) {
+ return
+ }
+ const label = access.write
+ ? i18n.t('Data view and capture')
+ : i18n.t('Data view only')
+
+ return
+}
+
+const AddAccess = ({ label }: { label: string }) => {
+ return (
+
+ {label}
+
+ )
+}
+
+const NoAccess = ({ label }: { label: string }) => {
+ return (
+
+ {label}
+
+ )
+}
diff --git a/src/components/sectionList/bulk/BulkActions.tsx b/src/components/sectionList/bulk/BulkActions.tsx
deleted file mode 100644
index 59bb315d..00000000
--- a/src/components/sectionList/bulk/BulkActions.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import i18n from '@dhis2/d2-i18n'
-import { Button } from '@dhis2/ui'
-import React from 'react'
-
-export const UpdateSharingButton = () => {
- return
-}
diff --git a/src/components/sectionList/bulk/BulkSharing.tsx b/src/components/sectionList/bulk/BulkSharing.tsx
index 193a2f58..eb54a29b 100644
--- a/src/components/sectionList/bulk/BulkSharing.tsx
+++ b/src/components/sectionList/bulk/BulkSharing.tsx
@@ -1,14 +1,16 @@
+import { useAlert } from '@dhis2/app-runtime'
import i18n from '@dhis2/d2-i18n'
-import { Button, Divider, Field, SingleSelect } from '@dhis2/ui'
-import React, { useState } from 'react'
+import { Button, Divider, Field, NoticeBox } from '@dhis2/ui'
+import React, { FormEvent, useState } from 'react'
import { useSchemaFromHandle } from '../../../lib'
-import { JsonPatchOperation } from '../../../types'
import {
SharingSearchSelect,
MetadataAccessField,
SharingSearchResult,
+ DataAccessField,
} from '../../sharing'
import css from './Bulk.module.css'
+import { ActionSummary } from './BulkActionSummary'
import {
SharingJsonPatchOperation,
useBulkSharingMutation,
@@ -16,7 +18,14 @@ import {
type BulkSharingProps = {
selectedModels: Set
- children: ({ handleSave }: { handleSave: () => void }) => React.ReactNode
+ onSaved: () => void
+ children: ({
+ submitting,
+ disableSave,
+ }: {
+ submitting: boolean
+ disableSave: boolean
+ }) => React.ReactNode
}
export type SharingAction = {
@@ -41,37 +50,108 @@ const actionToJsonPatchOperation = (
}
}
-export const BulkSharing = ({ children, selectedModels }: BulkSharingProps) => {
+/*
+ Usage of React-Final-Form does not seem necessary because we're not using
+ validation or initialState. And the result of the form (list of added SharingActions) are not kept in form-state.
+ However, we still need some metastate for the form.
+ */
+type FormMetaState = {
+ submitting: boolean
+ error: undefined | string
+}
+
+export const BulkSharing = ({
+ children,
+ selectedModels,
+ onSaved,
+}: BulkSharingProps) => {
const schema = useSchemaFromHandle()
- const dataShareable = schema?.dataShareable
+ const dataShareable = !schema?.dataShareable
const mutation = useBulkSharingMutation({ modelNamePlural: schema.plural })
+
const [sharingActions, setSharingActions] = useState([])
+ const [metaState, setMetaState] = useState({
+ submitting: false,
+ error: undefined,
+ })
+
+ const { show: showSuccessAlert } = useAlert(
+ i18n.t('Successfully updated sharing for {{number}} items', {
+ number: selectedModels.size,
+ }),
+ { success: true }
+ )
- const handleSave = () => {
+ const handleSave = async (e: FormEvent) => {
+ e.preventDefault()
const ids = Array.from(selectedModels)
const operations = sharingActions.map(actionToJsonPatchOperation)
- mutation(ids, operations)
+ setMetaState({ submitting: true, error: undefined })
+ try {
+ await mutation(ids, operations)
+ setMetaState({ submitting: false, error: undefined })
+ showSuccessAlert()
+ onSaved()
+ } catch (e) {
+ console.error(e)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const error = e as any
+ if ('message' in error) {
+ setMetaState({ submitting: false, error: error.message })
+ } else {
+ setMetaState({
+ submitting: false,
+ error: i18n.t('An unknown error occurred'),
+ })
+ }
+ }
+ }
+
+ const handleAddSharingAction = (action: SharingAction) => {
+ setSharingActions((prev) => {
+ const actionExists = sharingActions.some(
+ (a) => a.sharingEntity.id === action.sharingEntity.id
+ )
+ if (actionExists) {
+ // if the user/group already exists, update with new action
+ return sharingActions.map((a) =>
+ a.sharingEntity.id === action.sharingEntity.id ? action : a
+ )
+ }
+ return [action, ...prev]
+ })
}
return (
-
+
+ {metaState.error && (
+
+ {metaState.error}
+
+ )}
+
+ {children({
+ submitting: metaState.submitting,
+ disableSave: sharingActions.length < 1,
+ })}
+
)
}
@@ -88,8 +168,7 @@ const SharingSelection = ({
SharingSearchResult | undefined
>()
- const [metadataAccess, setMetadataAccess] = useState('r-------')
- const [dataAccess, setDataAccess] = useState('--------')
+ const [accessString, setAccessString] = useState('r-------')
const handleSelectSharingEntity = (selected: SharingSearchResult) => {
setSelectedSharingEntity(selected)
@@ -102,7 +181,7 @@ const SharingSelection = ({
const action = {
op: 'replace',
sharingEntity: selectedSharingEntity,
- access: metadataAccess,
+ access: accessString,
} as const
onAddSharingAction(action)
@@ -122,21 +201,20 @@ const SharingSelection = ({
setMetadataAccess(selected)}
+ value={accessString}
+ onChange={(selected) => setAccessString(selected)}
/>
{dataShareable && (
- {}}
- dataTest="dhis2-uicore-singleselect"
+ setAccessString(selected)}
/>
)}