diff --git a/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx b/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx
index 91c2cf91..ce165dbf 100644
--- a/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx
+++ b/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx
@@ -50,7 +50,7 @@ type OnFilterChange = ({ value }: { value: string }) => void
interface SearchableSingleSelectPropTypes {
onChange: OnChange
onFilterChange: OnFilterChange
- onEndReached: () => void
+ onEndReached?: () => void
onRetryClick: () => void
dense?: boolean
options: Option[]
@@ -103,7 +103,7 @@ export const SearchableSingleSelect = ({
const [{ isIntersecting }] = entries
if (isIntersecting) {
- onEndReached()
+ onEndReached?.()
}
},
{ threshold: 0.8 }
@@ -141,6 +141,7 @@ export const SearchableSingleSelect = ({
setFilterValue(value ?? '')}
placeholder={i18n.t('Filter options')}
diff --git a/src/components/sectionList/SectionListWrapper.tsx b/src/components/sectionList/SectionListWrapper.tsx
index 45a3a2c1..9fde5bbe 100644
--- a/src/components/sectionList/SectionListWrapper.tsx
+++ b/src/components/sectionList/SectionListWrapper.tsx
@@ -2,6 +2,7 @@ import { FetchError } from '@dhis2/app-runtime'
import React, { useMemo, useState } from 'react'
import { useSchemaFromHandle } from '../../lib'
import { Pager, ModelCollection } from '../../types/models'
+import { SectionListHeaderBulk } from './bulk'
import { DetailsPanel, DefaultDetailsPanelContent } from './detailsPanel'
import { FilterWrapper } from './filters/FilterWrapper'
import {
@@ -85,7 +86,14 @@ export const SectionListWrapper = ({
-
+ {selectedModels.size > 0 ? (
+
setSelectedModels(new Set())}
+ />
+ ) : (
+
+ )}
void
+}
+export const ActionSummary = ({
+ action,
+ dataShareable,
+ onRemove,
+}: ActionSummaryProps) => (
+
+
+
+ {action.sharingEntity.displayName}
+
+
+
+
+
+
+)
+
+type ActionAccessSummaryProps = {
+ action: SharingAction
+ dataShareable: boolean
+}
+const ActionAccessSummary = ({
+ action,
+ dataShareable,
+}: ActionAccessSummaryProps) => {
+ const parsed = parseAccessString(action.access)
+
+ if (parsed === null) {
+ return null
+ }
+
+ return (
+
+
+ {dataShareable && }
+
+ )
+}
+
+const MetadataAccess = ({ access }: { access: ParsedAccessPart }) => {
+ 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: ParsedAccessPart }) => {
+ 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/BulkSharing.tsx b/src/components/sectionList/bulk/BulkSharing.tsx
new file mode 100644
index 00000000..834d85bb
--- /dev/null
+++ b/src/components/sectionList/bulk/BulkSharing.tsx
@@ -0,0 +1,269 @@
+import { useAlert } from '@dhis2/app-runtime'
+import i18n from '@dhis2/d2-i18n'
+import { Button, Divider, Field, NoticeBox } from '@dhis2/ui'
+import React, { FormEvent, useState } from 'react'
+import { useSchemaFromHandle } from '../../../lib'
+import {
+ SharingSearchSelect,
+ MetadataAccessField,
+ SharingSearchResult,
+ DataAccessField,
+} from '../../sharing'
+import css from './Bulk.module.css'
+import { ActionSummary } from './BulkActionSummary'
+import {
+ SharingJsonPatchOperation,
+ useBulkSharingMutation,
+} from './useBulkSharing'
+
+type BulkSharingProps = {
+ selectedModels: Set
+ onSaved: () => void
+ children: ({
+ submitting,
+ disableSave,
+ }: {
+ submitting: boolean
+ disableSave: boolean
+ }) => React.ReactNode
+}
+
+export type SharingAction = {
+ op: 'remove' | 'replace'
+ sharingEntity: SharingSearchResult
+ access: string
+}
+
+const actionToJsonPatchOperation = (
+ action: SharingAction
+): SharingJsonPatchOperation => {
+ const { op, sharingEntity, access } = action
+ const value = {
+ access: access,
+ id: sharingEntity.id,
+ }
+
+ return {
+ op,
+ path: `/sharing/${sharingEntity.entity}/${sharingEntity.id}`,
+ ...(op === 'remove' ? undefined : { value }),
+ }
+}
+
+/*
+ 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 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 = async (e: FormEvent) => {
+ e.preventDefault()
+ const ids = Array.from(selectedModels)
+ const operations = sharingActions.map(actionToJsonPatchOperation)
+ 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 (
+
+ )
+}
+
+type SharingSelectionProps = {
+ dataShareable: boolean
+ onAddSharingAction: (action: SharingAction) => void
+}
+
+const SharingSelection = ({
+ dataShareable,
+ onAddSharingAction,
+}: SharingSelectionProps) => {
+ const [selectedSharingEntity, setSelectedSharingEntity] = useState<
+ SharingSearchResult | undefined
+ >()
+
+ const [accessString, setAccessString] = useState('r-------')
+
+ const handleSelectSharingEntity = (selected: SharingSearchResult) => {
+ setSelectedSharingEntity(selected)
+ }
+
+ const handleAddSharingAction = () => {
+ if (!selectedSharingEntity) {
+ return
+ }
+ const action = {
+ op: 'replace',
+ sharingEntity: selectedSharingEntity,
+ access: accessString,
+ } as const
+
+ onAddSharingAction(action)
+ }
+
+ return (
+
+
+ {i18n.t('Update sharing for users and groups')}
+
+
+
+
+
+
+ setAccessString(selected)}
+ />
+
+ {dataShareable && (
+
+ setAccessString(selected)}
+ />
+
+ )}
+
+
+
+ )
+}
+
+const SharingSummary = ({
+ dataShareable,
+ numberOfSelectedModels,
+ sharingActions,
+ onRemoveSharingAction,
+}: {
+ dataShareable: boolean
+ numberOfSelectedModels: number
+ sharingActions: SharingAction[]
+ onRemoveSharingAction: (action: SharingAction) => void
+}) => {
+ return (
+
+
+ {i18n.t('Access to be updated for {{number}} items', {
+ number: numberOfSelectedModels,
+ })}{' '}
+
+ {sharingActions.map((action) => (
+
onRemoveSharingAction(action)}
+ />
+ ))}
+
+ )
+}
+
+const SharingSubTitle = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+
+)
diff --git a/src/components/sectionList/bulk/BulkSharingDialog.tsx b/src/components/sectionList/bulk/BulkSharingDialog.tsx
new file mode 100644
index 00000000..f6ccb716
--- /dev/null
+++ b/src/components/sectionList/bulk/BulkSharingDialog.tsx
@@ -0,0 +1,52 @@
+import i18n from '@dhis2/d2-i18n'
+import {
+ Button,
+ ButtonStrip,
+ Modal,
+ ModalActions,
+ ModalContent,
+ ModalTitle,
+} from '@dhis2/ui'
+import React from 'react'
+import { BulkSharing } from './BulkSharing'
+
+type BulkSharingDialogProps = {
+ onClose: () => void
+ selectedModels: Set
+}
+
+export const BulkSharingDialog = ({
+ onClose,
+ selectedModels,
+}: BulkSharingDialogProps) => {
+ return (
+
+
+ {i18n.t('Update sharing for {{number}} items', {
+ number: selectedModels.size,
+ })}
+
+
+
+ {({ submitting, disableSave }) => (
+
+
+
+
+
+
+ )}
+
+
+
+ )
+}
diff --git a/src/components/sectionList/bulk/SectionListHeaderBulk.module.css b/src/components/sectionList/bulk/SectionListHeaderBulk.module.css
new file mode 100644
index 00000000..eb35e784
--- /dev/null
+++ b/src/components/sectionList/bulk/SectionListHeaderBulk.module.css
@@ -0,0 +1,11 @@
+.listHeaderBulk {
+ background-color: var(--colors-green050);
+ width: 100%;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ height: 48px;
+ padding: var(--spacers-dp8);
+ gap: var(--spacers-dp8);
+ border: 2px solid var(--colors-green800) !important;
+}
diff --git a/src/components/sectionList/bulk/SectionListHeaderBulk.tsx b/src/components/sectionList/bulk/SectionListHeaderBulk.tsx
new file mode 100644
index 00000000..ddb5337d
--- /dev/null
+++ b/src/components/sectionList/bulk/SectionListHeaderBulk.tsx
@@ -0,0 +1,42 @@
+import i18n from '@dhis2/d2-i18n'
+import { Button, DataTableToolbar } from '@dhis2/ui'
+import React from 'react'
+import { useSchemaFromHandle } from '../../../lib'
+import { BulkSharingDialog } from './BulkSharingDialog'
+import css from './SectionListHeaderBulk.module.css'
+
+type SectionListHeaderBulkProps = {
+ selectedModels: Set
+ onDeselectAll: () => void
+}
+
+export const SectionListHeaderBulk = ({
+ selectedModels,
+ onDeselectAll,
+}: SectionListHeaderBulkProps) => {
+ const [sharingDialogOpen, setSharingDialogOpen] = React.useState(false)
+ const sharable = useSchemaFromHandle().shareable
+
+ const handleClose = () => setSharingDialogOpen(false)
+ return (
+
+
+ {i18n.t('{{number}} selected', { number: selectedModels.size })}
+
+ {sharable && (
+
+ )}
+
+ {sharingDialogOpen && (
+
+ )}
+
+ )
+}
diff --git a/src/components/sectionList/bulk/index.ts b/src/components/sectionList/bulk/index.ts
new file mode 100644
index 00000000..140f4f62
--- /dev/null
+++ b/src/components/sectionList/bulk/index.ts
@@ -0,0 +1 @@
+export * from './SectionListHeaderBulk'
diff --git a/src/components/sectionList/bulk/useBulkSharing.tsx b/src/components/sectionList/bulk/useBulkSharing.tsx
new file mode 100644
index 00000000..360fd77e
--- /dev/null
+++ b/src/components/sectionList/bulk/useBulkSharing.tsx
@@ -0,0 +1,63 @@
+import { useConfig } from '@dhis2/app-runtime'
+import { useCallback } from 'react'
+import { JsonPatchOperation } from '../../../types'
+
+// TODO: use this when mutation support objects as json-patch
+//type Mutation = Parameters[0]
+// const query = {
+// type: 'json-patch',
+// data: ({ data }: Record) => data,
+// } as const
+
+// const createQuery = (modelName: string): Mutation => ({
+// ...query,
+// resource: modelName,
+// id: 'sharing',
+// })
+
+export type SharingJsonPatchOperation = Omit & {
+ value?: {
+ access: string
+ id: string
+ }
+}
+
+export const useBulkSharingMutation = ({
+ modelNamePlural,
+}: {
+ modelNamePlural: string
+}) => {
+ const config = useConfig()
+
+ const updateSharing = useCallback(
+ (modelIds: string[], operations: SharingJsonPatchOperation[]) => {
+ const data = {
+ [modelNamePlural]: modelIds,
+ patch: operations,
+ }
+
+ // engine.mutate enforces body to be an array, which does not match the API
+ // so we cant use engine for this mutation
+ const request = fetch(
+ `${config.baseUrl}/api/${modelNamePlural}/sharing`,
+ {
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json-patch+json',
+ Accept: 'application/json',
+ // credentials: 'include',
+ 'X-Requested-With': 'XMLHttpRequest',
+ },
+ method: 'PATCH',
+ body: JSON.stringify(data),
+ }
+ )
+ return request.then((r) =>
+ r.ok ? r.json() : r.json().then((e) => Promise.reject(e))
+ )
+ },
+ [config, modelNamePlural]
+ )
+
+ return updateSharing
+}
diff --git a/src/components/sectionList/filters/filterSelectors/PublicAccessFilter.tsx b/src/components/sectionList/filters/filterSelectors/PublicAccessFilter.tsx
index f9111c93..4260961f 100644
--- a/src/components/sectionList/filters/filterSelectors/PublicAccessFilter.tsx
+++ b/src/components/sectionList/filters/filterSelectors/PublicAccessFilter.tsx
@@ -1,6 +1,6 @@
import i18n from '@dhis2/d2-i18n'
import React from 'react'
-import { formatPublicAccess, parsePublicAccessString } from '../../../../lib'
+import { formatAccessToString, parseAccessString } from '../../../../lib'
import { ConstantSelectionFilter } from './ConstantSelectionFilter'
// currently we only care about metadata access
@@ -17,11 +17,11 @@ export const PublicAccessFilter = () => {
if (!filter) {
return undefined
}
- const parsedPublicAccessString = parsePublicAccessString(filter)
+ const parsedPublicAccessString = parseAccessString(filter)
if (!parsedPublicAccessString) {
return undefined
}
- const withoutDataAccess = formatPublicAccess({
+ const withoutDataAccess = formatAccessToString({
metadata: parsedPublicAccessString.metadata,
data: { read: false, write: false },
})
diff --git a/src/components/sectionList/index.ts b/src/components/sectionList/index.ts
index f930c6b6..32bab90f 100644
--- a/src/components/sectionList/index.ts
+++ b/src/components/sectionList/index.ts
@@ -4,3 +4,4 @@ export * from './SectionListWrapper'
export type * from './types'
export * from './filters'
export * from './SectionListPagination'
+export * from './bulk'
diff --git a/src/components/sectionList/modelValue/PublicAccess.tsx b/src/components/sectionList/modelValue/PublicAccess.tsx
index 923a52f8..812ebae6 100644
--- a/src/components/sectionList/modelValue/PublicAccess.tsx
+++ b/src/components/sectionList/modelValue/PublicAccess.tsx
@@ -1,6 +1,6 @@
import i18n from '@dhis2/d2-i18n'
import React from 'react'
-import { parsePublicAccessString } from '../../../lib'
+import { parseAccessString } from '../../../lib'
import { Sharing } from '../../../types/generated'
export const isSharing = (value: unknown): value is Sharing => {
@@ -8,7 +8,7 @@ export const isSharing = (value: unknown): value is Sharing => {
}
const getPublicAccessString = (value: string): string => {
- const publicAccess = parsePublicAccessString(value)
+ const publicAccess = parseAccessString(value)
if (!publicAccess) {
throw new Error('Invalid public access string')
diff --git a/src/components/sharing/DataAccessField.tsx b/src/components/sharing/DataAccessField.tsx
new file mode 100644
index 00000000..3af0d978
--- /dev/null
+++ b/src/components/sharing/DataAccessField.tsx
@@ -0,0 +1,69 @@
+import i18n from '@dhis2/d2-i18n'
+import { SingleSelect, SingleSelectOption } from '@dhis2/ui'
+import React from 'react'
+import {
+ ParsedAccess,
+ ParsedAccessPart,
+ formatAccessToString,
+ parseAccessString,
+} from '../../lib'
+
+const defaultParsedAccess: ParsedAccess = {
+ metadata: { read: false, write: false },
+ data: { read: false, write: false },
+}
+
+type DataAccessFieldProps = {
+ // value is a full access string, with data-access if applicable
+ value?: string | undefined
+ // selected is full access string, with updated data access
+ onChange: (selected: string) => void
+}
+
+export const DataAccessField = ({ onChange, value }: DataAccessFieldProps) => {
+ const parsed = value
+ ? parseAccessString(value) || defaultParsedAccess
+ : defaultParsedAccess
+
+ // selected is here is data access string only (metadata will always be --)
+ const handleChange = ({ selected }: { selected: string }) => {
+ const selectedDataAccess = parseAccessString(selected)
+ ?.data as NonNullable
+
+ const accessString = formatAccessToString({
+ metadata: parsed.metadata,
+ data: selectedDataAccess,
+ })
+
+ onChange(accessString)
+ }
+
+ const valueWithOnlyData = formatAccessToString({
+ metadata: defaultParsedAccess.metadata,
+ data: parsed.data,
+ })
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/src/components/sharing/MetadataAccessField.tsx b/src/components/sharing/MetadataAccessField.tsx
new file mode 100644
index 00000000..4e30a1ac
--- /dev/null
+++ b/src/components/sharing/MetadataAccessField.tsx
@@ -0,0 +1,67 @@
+import i18n from '@dhis2/d2-i18n'
+import { SingleSelect, SingleSelectOption } from '@dhis2/ui'
+import React from 'react'
+import {
+ ParsedAccess,
+ ParsedAccessPart,
+ formatAccessToString,
+ parseAccessString,
+} from '../../lib'
+
+const defaultParsedAccess: ParsedAccess = {
+ metadata: { read: true, write: false },
+ data: { read: false, write: false },
+}
+
+type MetadataAccessFieldProps = {
+ // value is a full access string, with data-access if applicable
+ value?: string | undefined
+ // selected is full access string, with updated metadata access
+ onChange: (selected: string) => void
+}
+
+export const MetadataAccessField = ({
+ onChange,
+ value,
+}: MetadataAccessFieldProps) => {
+ const parsed = value
+ ? parseAccessString(value) || defaultParsedAccess
+ : defaultParsedAccess
+
+ // selected is here is metadata access string only (data will always be --)
+ const handleChange = ({ selected }: { selected: string }) => {
+ const selectedMetadataAccess = parseAccessString(selected)
+ ?.metadata as NonNullable
+
+ const accessString = formatAccessToString({
+ metadata: selectedMetadataAccess,
+ data: parsed.data,
+ })
+
+ onChange(accessString)
+ }
+
+ const valueWithOnlyMetadata = formatAccessToString({
+ metadata: parsed.metadata,
+ data: defaultParsedAccess.data,
+ })
+
+ return (
+
+
+
+
+ )
+}
diff --git a/src/components/sharing/SharingSearchSelect.tsx b/src/components/sharing/SharingSearchSelect.tsx
new file mode 100644
index 00000000..77f7ac32
--- /dev/null
+++ b/src/components/sharing/SharingSearchSelect.tsx
@@ -0,0 +1,120 @@
+import { useDataQuery } from '@dhis2/app-runtime'
+import i18n from '@dhis2/d2-i18n'
+import React, { useCallback, useMemo, useState } from 'react'
+import { SearchableSingleSelect } from '../SearchableSingleSelect'
+
+const query = {
+ result: {
+ resource: 'sharing/search',
+ params: ({ searchFilter }: Record) => ({
+ key: searchFilter,
+ }),
+ },
+}
+
+type SharingUserResult = {
+ id: string
+ name: string
+ displayName: string
+ username: string
+}
+
+type SharingUserGroupResult = {
+ id: string
+ name: string
+ displayName: string
+}
+
+export type SharingSearchResult =
+ | (SharingUserResult & { entity: 'users' })
+ | (SharingUserGroupResult & {
+ entity: 'userGroups'
+ })
+
+type SharingSearchResponse = {
+ result: {
+ users: SharingUserResult[]
+ userGroups: SharingUserGroupResult[]
+ }
+}
+
+type SharingSearchSelectProps = {
+ onChange: (value: SharingSearchResult) => void
+}
+
+export const SharingSearchSelect = ({ onChange }: SharingSearchSelectProps) => {
+ const [selected, setSelected] = useState('')
+
+ const { data, refetch, error, loading } =
+ useDataQuery(query, {
+ variables: { searchFilter: '' },
+ })
+
+ const formattedData = useMemo(() => {
+ if (!data?.result) {
+ return []
+ }
+
+ const users = data.result.users.map(
+ (user) =>
+ ({
+ ...user,
+ entity: 'users',
+ label: user.displayName,
+ value: user.id,
+ } as const)
+ )
+ const userGroups = data.result.userGroups.map(
+ (group) =>
+ ({
+ ...group,
+ label: group.displayName,
+ entity: 'userGroups',
+ value: group.id,
+ } as const)
+ )
+
+ return [...users, ...userGroups].sort((a, b) =>
+ a.label.localeCompare(b.label)
+ )
+ }, [data])
+
+ const handleChange = useCallback(
+ ({ selected }: { selected: string }) => {
+ if (formattedData.length < 1) {
+ return
+ }
+ const selectedResult = formattedData.find(
+ (res) => res.id === selected
+ )
+
+ if (selectedResult) {
+ setSelected(selectedResult.id)
+ onChange(selectedResult)
+ }
+ },
+ [formattedData, setSelected, onChange]
+ )
+
+ const handleFetch = useCallback(
+ ({ value }: { value: string }) => {
+ refetch({ searchFilter: value.trim() })
+ },
+ [refetch]
+ )
+
+ return (
+ refetch()}
+ loading={loading}
+ showEndLoader={loading}
+ error={error?.message}
+ />
+ )
+}
diff --git a/src/components/sharing/index.ts b/src/components/sharing/index.ts
new file mode 100644
index 00000000..75bdd969
--- /dev/null
+++ b/src/components/sharing/index.ts
@@ -0,0 +1,3 @@
+export * from './SharingSearchSelect'
+export * from './MetadataAccessField'
+export * from './DataAccessField'
diff --git a/src/lib/models/parsePublicAccess.spec.ts b/src/lib/models/access.spec.ts
similarity index 89%
rename from src/lib/models/parsePublicAccess.spec.ts
rename to src/lib/models/access.spec.ts
index 7ff39e2a..052e3ebc 100644
--- a/src/lib/models/parsePublicAccess.spec.ts
+++ b/src/lib/models/access.spec.ts
@@ -1,6 +1,6 @@
-import { parsePublicAccessString } from './parsePublicAccess'
+import { parseAccessString } from './access'
-describe('parsePublicAccessString', () => {
+describe('parseAccessString', () => {
const validAccessCases = [
{
input: '--------',
@@ -59,7 +59,7 @@ describe('parsePublicAccessString', () => {
it.each(validAccessCases)(
'correctly parses valid publicAccess string: $input',
({ input, expected }) => {
- const result = parsePublicAccessString(input)
+ const result = parseAccessString(input)
expect(result).toEqual(expected)
}
)
@@ -67,7 +67,7 @@ describe('parsePublicAccessString', () => {
it.each(invalidAccessCases)(
'returns null for invalid publicAccess string $input',
(testCase) => {
- const result = parsePublicAccessString(testCase)
+ const result = parseAccessString(testCase)
expect(result).toBeNull()
}
)
diff --git a/src/lib/models/parsePublicAccess.ts b/src/lib/models/access.ts
similarity index 62%
rename from src/lib/models/parsePublicAccess.ts
rename to src/lib/models/access.ts
index 968cae63..c33601b6 100644
--- a/src/lib/models/parsePublicAccess.ts
+++ b/src/lib/models/access.ts
@@ -1,13 +1,13 @@
-export type PublicAccessPart = {
+export type ParsedAccessPart = {
read: boolean
write: boolean
}
-export type PublicAccess = {
- metadata: PublicAccessPart
- data: PublicAccessPart
+export type ParsedAccess = {
+ metadata: ParsedAccessPart
+ data: ParsedAccessPart
}
-const parseAccessPart = (accessPart: string): PublicAccessPart => {
+const parseAccessPart = (accessPart: string): ParsedAccessPart => {
const canRead = accessPart[0] === 'r'
return {
read: canRead,
@@ -19,10 +19,10 @@ const parseAccessPart = (accessPart: string): PublicAccessPart => {
// eg. rw------ = metadata: rw, data: --
const publicAccessRegex = /^(r-|rw|--)(r-|rw|--)(-){4}$/
-export const parsePublicAccessString = (
- publicAccess: string
-): PublicAccess | null => {
- const matches = publicAccess.match(publicAccessRegex)
+export const parseAccessString = (
+ accessString: string
+): ParsedAccess | null => {
+ const matches = accessString.match(publicAccessRegex)
if (!matches) {
return null
}
@@ -34,14 +34,14 @@ export const parsePublicAccessString = (
}
}
-const accessPartToString = (accessPart: PublicAccessPart): string => {
+const accessPartToString = (accessPart: ParsedAccessPart): string => {
if (accessPart.write) {
return 'rw'
}
return accessPart.read ? 'r-' : '--'
}
-export const formatPublicAccess = (publicAccess: PublicAccess): string => {
+export const formatAccessToString = (publicAccess: ParsedAccess): string => {
const metadata = accessPartToString(publicAccess.metadata)
const data = accessPartToString(publicAccess.data)
diff --git a/src/lib/models/index.ts b/src/lib/models/index.ts
index cdba6bb1..c8bc5f7c 100644
--- a/src/lib/models/index.ts
+++ b/src/lib/models/index.ts
@@ -1,8 +1,5 @@
export { isValidUid } from './uid'
-export {
- parsePublicAccessString,
- formatPublicAccess,
-} from './parsePublicAccess'
+export * from './access'
export { getIn, stringToPathArray, getFieldFilterFromPath } from './path'
export { useIsFieldValueUnique } from './useIsFieldValueUnique'
export { useCheckMaxLengthFromSchema } from './useCheckMaxLengthFromSchema'
diff --git a/src/lib/sectionList/filters/filterConfig.tsx b/src/lib/sectionList/filters/filterConfig.tsx
index 54cc65e6..f850ce1a 100644
--- a/src/lib/sectionList/filters/filterConfig.tsx
+++ b/src/lib/sectionList/filters/filterConfig.tsx
@@ -2,7 +2,7 @@ import { StringParam } from 'use-query-params'
import { z } from 'zod'
import { DataElement } from '../../../types/generated'
import { IDENTIFIABLE_FILTER_KEY } from '../../constants'
-import { isValidUid, parsePublicAccessString } from '../../models'
+import { isValidUid, parseAccessString } from '../../models'
import { CustomDelimitedArrayParam } from './customParams'
const zodArrayIds = z.array(z.string().refine((val) => isValidUid(val)))
@@ -16,7 +16,7 @@ export const filterParamsSchema = z
dataSet: zodArrayIds,
domainType: z.array(z.nativeEnum(DataElement.domainType)),
publicAccess: z.array(
- z.string().refine((val) => parsePublicAccessString(val) !== null)
+ z.string().refine((val) => parseAccessString(val) !== null)
),
valueType: z.array(z.nativeEnum(DataElement.valueType)),
})
diff --git a/src/lib/useLoadApp.ts b/src/lib/useLoadApp.ts
index acaa2b95..3f288bac 100644
--- a/src/lib/useLoadApp.ts
+++ b/src/lib/useLoadApp.ts
@@ -16,6 +16,8 @@ const schemaFields = [
'singular',
'translatable',
'properties',
+ 'shareable',
+ 'dataShareable',
] as const
// workaround to widen the type, because useQuery() does not allow for