Skip to content

Commit d28d40c

Browse files
authored
fix(ui)!: bulk selection and useSelection hook losing types of the IDs its returning (#8194)
This PR changes the type of `selected` returned from the `useSelection` hook from the `SelectionProvider` from an object to a Map. This fixes a bug where in some situations we lose the type of the ID which can break data entry when using postgres, due to keys being cast to strings inside of objects which doesn't happen when using a Map. This PR also fixes a CSS bug with the checkbox when it should be partially selected. ```ts // before selected: Record<number | string, boolean> // after selected: Map<number | string, boolean> ``` This means you now need to read the data differently than before. ```ts // before Object.entries(selected).forEach(([key, value]) => { // do something }) // after for (const [key, value] of selected) { // do something } ```
1 parent a6f13f7 commit d28d40c

File tree

5 files changed

+72
-37
lines changed

5 files changed

+72
-37
lines changed

packages/ui/src/elements/SelectRow/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const SelectRow: React.FC = () => {
1414

1515
return (
1616
<CheckboxInput
17-
checked={selected?.[rowData?.id]}
17+
checked={Boolean(selected.get(rowData.id))}
1818
className={[baseClass, `${baseClass}__checkbox`].join(' ')}
1919
onToggle={() => setSelection(rowData.id)}
2020
/>

packages/ui/src/fields/Checkbox/Input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = ({
7676
type="checkbox"
7777
/>
7878
<span
79-
className={[`${inputBaseClass}__icon`, !checked && partialChecked ? 'check' : 'partial']
79+
className={[`${inputBaseClass}__icon`, !checked && partialChecked ? 'partial' : 'check']
8080
.filter(Boolean)
8181
.join(' ')}
8282
>

packages/ui/src/fields/Checkbox/index.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,19 @@
102102
}
103103
}
104104

105+
.checkbox-input__icon {
106+
.icon--line {
107+
width: 1.4rem;
108+
height: 1.4rem;
109+
}
110+
111+
&.partial {
112+
svg {
113+
opacity: 1;
114+
}
115+
}
116+
}
117+
105118
&--read-only {
106119
.checkbox-input__input {
107120
@include readOnly;

packages/ui/src/fields/Upload/Input.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -290,12 +290,14 @@ export function UploadInput(props: UploadInputProps) {
290290
// only hasMany can bulk select
291291
const onListBulkSelect = React.useCallback<NonNullable<ListDrawerProps['onBulkSelect']>>(
292292
async (docs) => {
293-
const selectedDocIDs = Object.entries(docs).reduce<string[]>((acc, [docID, isSelected]) => {
293+
const selectedDocIDs = []
294+
295+
for (const [id, isSelected] of docs) {
294296
if (isSelected) {
295-
acc.push(docID)
297+
selectedDocIDs.push(id)
296298
}
297-
return acc
298-
}, [])
299+
}
300+
299301
const loadedDocs = await populateDocs(selectedDocIDs, activeRelationTo)
300302
if (loadedDocs) {
301303
setPopulatedDocs((currentDocs) => [

packages/ui/src/providers/Selection/index.tsx

Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type SelectionContext = {
2020
disableBulkEdit?: boolean
2121
getQueryParams: (additionalParams?: Where) => string
2222
selectAll: SelectAllStatus
23-
selected: Record<number | string, boolean>
23+
selected: Map<number | string, boolean>
2424
setSelection: (id: number | string) => void
2525
toggleAll: (allAvailable?: boolean) => void
2626
totalDocs: number
@@ -30,6 +30,7 @@ const Context = createContext({} as SelectionContext)
3030

3131
type Props = {
3232
readonly children: React.ReactNode
33+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3334
readonly docs: any[]
3435
readonly totalDocs: number
3536
}
@@ -39,35 +40,33 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
3940

4041
const { code: locale } = useLocale()
4142
const [selected, setSelected] = useState<SelectionContext['selected']>(() => {
42-
const rows = {}
43+
const rows = new Map()
4344
docs.forEach(({ id }) => {
44-
rows[id] = false
45+
rows.set(id, false)
4546
})
4647
return rows
4748
})
49+
4850
const [selectAll, setSelectAll] = useState<SelectAllStatus>(SelectAllStatus.None)
4951
const [count, setCount] = useState(0)
5052
const { searchParams } = useSearchParams()
5153

5254
const toggleAll = useCallback(
5355
(allAvailable = false) => {
54-
const rows = {}
56+
const rows = new Map()
5557
if (allAvailable) {
5658
setSelectAll(SelectAllStatus.AllAvailable)
5759
docs.forEach(({ id }) => {
58-
rows[id] = true
60+
rows.set(id, true)
5961
})
6062
} else if (
6163
selectAll === SelectAllStatus.AllAvailable ||
6264
selectAll === SelectAllStatus.AllInPage
6365
) {
6466
setSelectAll(SelectAllStatus.None)
65-
docs.forEach(({ id }) => {
66-
rows[id] = false
67-
})
6867
} else {
6968
docs.forEach(({ id }) => {
70-
rows[id] = selectAll !== SelectAllStatus.Some
69+
rows.set(id, selectAll !== SelectAllStatus.Some)
7170
})
7271
}
7372
setSelected(rows)
@@ -77,15 +76,18 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
7776

7877
const setSelection = useCallback(
7978
(id) => {
80-
const isSelected = !selected[id]
81-
const newSelected = {
82-
...selected,
83-
[id]: isSelected,
84-
}
85-
if (!isSelected) {
86-
setSelectAll(SelectAllStatus.Some)
79+
const existingValue = selected.get(id)
80+
const isSelected = typeof existingValue === 'boolean' ? !existingValue : true
81+
82+
let newMap = new Map()
83+
84+
if (isSelected) {
85+
newMap = new Map(selected.set(id, isSelected))
86+
} else {
87+
newMap = new Map(selected.set(id, false))
8788
}
88-
setSelected(newSelected)
89+
90+
setSelected(newMap)
8991
},
9092
[selected],
9193
)
@@ -99,11 +101,17 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
99101
id: { not_equals: '' },
100102
}
101103
} else {
104+
const ids = []
105+
106+
for (const [key, value] of selected) {
107+
if (value) {
108+
ids.push(key)
109+
}
110+
}
111+
102112
where = {
103113
id: {
104-
in: Object.keys(selected)
105-
.filter((id) => selected[id])
106-
.map((id) => id),
114+
in: ids,
107115
},
108116
}
109117
}
@@ -130,30 +138,42 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
130138
let some = false
131139
let all = true
132140

133-
if (!Object.values(selected).length) {
141+
if (!selected.size) {
134142
all = false
135143
some = false
136144
} else {
137-
Object.values(selected).forEach((val) => {
138-
all = all && val
139-
some = some || val
140-
})
145+
for (const [_, value] of selected) {
146+
all = all && value
147+
some = some || value
148+
}
141149
}
142150

143-
if (all) {
151+
if (all && selected.size === docs.length) {
152+
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
144153
setSelectAll(SelectAllStatus.AllInPage)
145154
} else if (some) {
155+
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
146156
setSelectAll(SelectAllStatus.Some)
147157
} else {
158+
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
148159
setSelectAll(SelectAllStatus.None)
149160
}
150-
}, [selectAll, selected])
161+
}, [selectAll, selected, totalDocs, docs])
151162

152163
useEffect(() => {
153-
const newCount =
154-
selectAll === SelectAllStatus.AllAvailable
155-
? totalDocs
156-
: Object.keys(selected).filter((id) => selected[id]).length
164+
let newCount = 0
165+
166+
if (selectAll === SelectAllStatus.AllAvailable) {
167+
newCount = totalDocs
168+
} else {
169+
for (const [_, value] of selected) {
170+
if (value) {
171+
newCount++
172+
}
173+
}
174+
}
175+
176+
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
157177
setCount(newCount)
158178
}, [selectAll, selected, totalDocs])
159179

0 commit comments

Comments
 (0)