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 ( +
+ + + setSharingActions((prev) => + prev.filter((a) => a !== action) + ) + } + /> + {metaState.error && ( + + {metaState.error} + + )} + + {children({ + submitting: metaState.submitting, + disableSave: sharingActions.length < 1, + })} + + ) +} + +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