Skip to content

Commit d87d9e7

Browse files
committed
Hide/disable UI elements based on ability permissions
1 parent 01012fb commit d87d9e7

File tree

20 files changed

+362
-188
lines changed

20 files changed

+362
-188
lines changed

Diff for: components/chemical/ChemicalEntry.tsx

+61-23
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { useUser } from "@supabase/auth-helpers-react"
1+
import { subject } from "@casl/ability"
2+
import _ from "lodash"
23
import { useEffect, useMemo } from "react"
34
import { Controller, UseFormReturn, useFieldArray } from "react-hook-form"
4-
import { preload } from "swr"
55

66
import { Button } from "components/ui"
77
import {
@@ -12,11 +12,12 @@ import {
1212
RhfRadioGroup,
1313
TextArea,
1414
} from "components/ui/forms"
15+
import { useAbility } from "lib/contexts/AbilityContext"
1516
import { NullableChemicalFields } from "lib/fields"
1617
import { useCurrentUser } from "lib/hooks/useCurrentUser"
1718
import useUpdateFieldConditionally from "lib/hooks/useUpdateFieldConditionally"
19+
import ChemicalMapper from "lib/mappers/ChemicalMapper"
1820
import { isCentralPharmacy } from "lib/utils"
19-
import { multiFetcher } from "pages/_app"
2021
import { DataEntryComponent } from "types/common"
2122

2223
type Props = {
@@ -26,9 +27,9 @@ type Props = {
2627
//preload("/api/users/current", multiFetcher)
2728

2829
const ChemicalEntry: DataEntryComponent<NullableChemicalFields, Props> = (
29-
props: Props,
30+
props,
3031
) => {
31-
const { formMethods } = props
32+
const { formMethods, action = "create" } = props
3233

3334
const { user, error: userError } = useCurrentUser()
3435

@@ -37,12 +38,20 @@ const ChemicalEntry: DataEntryComponent<NullableChemicalFields, Props> = (
3738
console.error(userError)
3839
}
3940

40-
const { register, control, watch, setValue, getFieldState, trigger } =
41-
formMethods
41+
const {
42+
register,
43+
control,
44+
watch,
45+
setValue,
46+
getFieldState,
47+
trigger,
48+
getValues,
49+
} = formMethods
4250

4351
const additionalInfoArrayMethods = useFieldArray({
4452
control, // control props comes from useForm (optional: if you are using FormContext)
4553
name: "additionalInfo", // unique name for your Field Array
54+
keyName: "key",
4655
})
4756

4857
const hasNoCasNumber = watch("hasNoCasNumber") as boolean
@@ -54,10 +63,27 @@ const ChemicalEntry: DataEntryComponent<NullableChemicalFields, Props> = (
5463
[user?.pharmacyId],
5564
)
5665

66+
const ability = useAbility()
67+
5768
const canFullEdit = useMemo(
69+
() =>
70+
ability.can(
71+
action,
72+
subject(
73+
"Chemical",
74+
ChemicalMapper.toModel({
75+
pharmacyId: user?.pharmacyId,
76+
...getValues(),
77+
} as any) as any,
78+
),
79+
),
80+
[ability.rules, getValues, user?.pharmacyId],
81+
)
82+
83+
/* const canFullEdit = useMemo(
5884
() => (user ? pharmacyId === user.pharmacyId : false),
5985
[user, pharmacyId],
60-
)
86+
) */
6187

6288
useUpdateFieldConditionally({
6389
updateCondition: hasNoCasNumber === true,
@@ -83,6 +109,8 @@ const ChemicalEntry: DataEntryComponent<NullableChemicalFields, Props> = (
83109
[additionalInfo, user?.pharmacyId],
84110
)
85111

112+
const chemicalId = watch("id")
113+
86114
register("id")
87115
return (
88116
<div className="chemical-entry">
@@ -156,14 +184,23 @@ const ChemicalEntry: DataEntryComponent<NullableChemicalFields, Props> = (
156184
<Fieldset legend="Additional Info">
157185
{additionalInfoArrayMethods.fields.map((item, index) => (
158186
<Fieldset
159-
key={item.id}
187+
key={item.key}
160188
legend={
161189
// If add. info's pharmacyId is undefined, use user's pharmacyId
162190
isCentralPharmacy(item.pharmacyId ?? user?.pharmacyId ?? NaN)
163191
? "Central"
164192
: "Local"
165193
}
166-
disabled={isCentralPharmacy(item.pharmacyId ?? NaN) && !canFullEdit}
194+
disabled={ability.cannot(
195+
action,
196+
subject(
197+
"AdditionalChemicalInfo",
198+
_.defaults(item, {
199+
chemicalId,
200+
pharmacyId: user?.pharmacyId,
201+
}) as any,
202+
),
203+
)}
167204
>
168205
<TextArea
169206
{...register(`additionalInfo.${index}.value`)}
@@ -172,19 +209,20 @@ const ChemicalEntry: DataEntryComponent<NullableChemicalFields, Props> = (
172209
/>
173210
</Fieldset>
174211
))}
175-
{!hasLocalAdditionalInfo && (
176-
<Button
177-
onClick={() =>
178-
additionalInfoArrayMethods.append({
179-
value: "",
180-
pharmacyId: undefined,
181-
})
182-
}
183-
size="small"
184-
>
185-
Add {isCentralUser ? "central" : "local"} additional info
186-
</Button>
187-
)}
212+
{!hasLocalAdditionalInfo &&
213+
ability.can("create", "AdditionalChemicalInfo") && (
214+
<Button
215+
onClick={() =>
216+
additionalInfoArrayMethods.append({
217+
value: "",
218+
pharmacyId: undefined,
219+
})
220+
}
221+
size="small"
222+
>
223+
Add {isCentralUser ? "central" : "local"} additional info
224+
</Button>
225+
)}
188226
</Fieldset>
189227
<style jsx>{`
190228
.chemical-entry .info {

Diff for: components/chemical/ChemicalTable.tsx

+30-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import { subject } from "@casl/ability"
12
import { Chemical } from "@prisma/client"
23
import { createColumnHelper } from "@tanstack/react-table"
34
import { BsGlobe } from "react-icons/bs"
45

56
import { Table } from "components/ui"
67
import DataRowActions from "components/ui/Table/DataRowActions"
8+
import { useAbility } from "lib/contexts/AbilityContext"
9+
import { useCurrentUser } from "lib/hooks/useCurrentUser"
710
import { isCentralPharmacy, toIsoDateString } from "lib/utils"
811

912
type Props = {
@@ -46,13 +49,33 @@ const columns = [
4649
}),
4750
columnHelper.display({
4851
id: "actions",
49-
cell: (info) => (
50-
<DataRowActions
51-
row={info.row}
52-
viewButton={{ getUrl: (data) => `/chemicals/${data.id}` }}
53-
editButton={{ getUrl: (data) => `/chemicals/${data.id}/edit` }}
54-
/>
55-
),
52+
cell: function ActionsCell(info) {
53+
const { user } = useCurrentUser()
54+
const ability = useAbility()
55+
const data = info.row.original
56+
const canEditChemical = ability.can("update", subject("Chemical", data))
57+
const canManageAdditionalInfo = ability.can(
58+
"manage",
59+
subject("AdditionalChemicalInfo", {
60+
id: 0,
61+
chemical: data,
62+
chemicalId: data.id,
63+
value: "PLACEHOLDER",
64+
pharmacyId: user?.pharmacyId ?? NaN,
65+
}),
66+
)
67+
return (
68+
<DataRowActions
69+
row={info.row}
70+
viewButton={{ getUrl: (data) => `/chemicals/${data.id}` }}
71+
editButton={
72+
canEditChemical || canManageAdditionalInfo
73+
? { getUrl: (data) => `/chemicals/${data.id}/edit` }
74+
: undefined
75+
}
76+
/>
77+
)
78+
},
5679
}),
5780
]
5881

Diff for: components/common/data-pages/CreateForm.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ const CreateForm = <
116116
autoComplete="off"
117117
noValidate
118118
>
119-
<EntryComponent formMethods={formMethods} />
119+
<EntryComponent formMethods={formMethods} action="create" />
120120
<div className="action-row">
121121
<Button theme="primary" type="submit">
122122
Submit

Diff for: components/common/data-pages/EditForm.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ const EditForm = <
108108
<EntryComponent
109109
values={values}
110110
formMethods={formMethods}
111+
action="update"
111112
{...entryComponentProps}
112113
/>
113114
<div>

Diff for: components/compound/CompoundDetails.tsx

+8-11
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
import { subject } from "@casl/ability"
12
import Link from "next/link"
2-
import React, { useMemo } from "react"
3+
import { useMemo } from "react"
34
import useSWR from "swr"
45

56
import { IngredientDetails } from "components/compound/ingredient/IngredientDetails"
67
import { Button, Spinner } from "components/ui"
78
import { Fieldset, FormGroup, TextArea } from "components/ui/forms"
9+
import { useAbility } from "lib/contexts/AbilityContext"
810
import { SettingsFields } from "lib/fields"
9-
import { useCurrentUser } from "lib/hooks/useCurrentUser"
1011
import { CompoundWithIngredients, MfrAll } from "types/models"
1112

1213
import { getHwngShortcutString } from "./helpers"
@@ -150,18 +151,14 @@ const MfrsActions = (props: { data: CompoundWithIngredients }) => {
150151
console.error(mfrsError)
151152
}
152153

153-
const { user, error: userError, isLoading: isUserLoading } = useCurrentUser()
154+
const ability = useAbility()
154155

155-
if (userError) {
156-
console.error(userError)
157-
}
158-
159-
const canCreateNewMfr = useMemo(
160-
() => user?.pharmacyId === data.pharmacyId,
161-
[data.pharmacyId, user?.pharmacyId],
156+
const canCreateNewMfr = ability.can(
157+
"create",
158+
subject("Mfr", { compound: data } as any),
162159
)
163160

164-
const isLoading = (!mfrs && !mfrsError) || isUserLoading
161+
const isLoading = !mfrs && !mfrsError
165162
return (
166163
<FormGroup row>
167164
{!isLoading ? (

Diff for: components/compound/CompoundsTable.tsx

+14-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { subject } from "@casl/ability"
12
import { createColumnHelper } from "@tanstack/react-table"
23
import Link from "next/link"
34
import { useMemo } from "react"
@@ -6,7 +7,7 @@ import { BsGlobe } from "react-icons/bs"
67
import { Button, Table } from "components/ui"
78
import DataRowActions from "components/ui/Table/DataRowActions"
89
import RowActions from "components/ui/Table/RowActions"
9-
import { useCurrentUser } from "lib/hooks/useCurrentUser"
10+
import { useAbility } from "lib/contexts/AbilityContext"
1011
import { isCentralPharmacy } from "lib/utils"
1112
import { CompoundWithMfrCount, IngredientAll } from "types/models"
1213

@@ -167,23 +168,28 @@ const columns = [
167168
columnHelper.display({
168169
id: "mfr-actions",
169170
cell: function MfrActionsCell(info) {
170-
const { user, error: userError } = useCurrentUser()
171-
if (userError) {
172-
console.error(userError)
173-
}
171+
const ability = useAbility()
174172

175173
const data = info.row.original
176-
const canEdit = user?.pharmacyId === data.pharmacyId
174+
175+
const canRead = ability.can(
176+
"read",
177+
subject("Mfr", { compound: data } as any),
178+
)
179+
const canCreate = ability.can(
180+
"create",
181+
subject("Mfr", { compound: data } as any),
182+
)
177183

178184
return (
179185
<RowActions>
180-
{data._count.mfrs > 0 ? (
186+
{data._count.mfrs > 0 && canRead ? (
181187
<Link href={`/compounds/${data.id}/mfrs/latest`}>
182188
<Button size="small" theme="primary">
183189
View MFR
184190
</Button>
185191
</Link>
186-
) : canEdit ? (
192+
) : canCreate ? (
187193
<Link href={`/compounds/${data.id}/mfrs/new`}>
188194
<Button size="small">Create MFR</Button>
189195
</Link>

Diff for: components/links/LinkDirectoryDetails.tsx

+8-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { BiEdit } from "react-icons/bi"
44

55
import { IconButton } from "components/ui"
66
import { Fieldset } from "components/ui/forms"
7+
import { Can } from "lib/contexts/AbilityContext"
78

89
export type LinkDirectory = {
910
centralLinks: LinkModel[]
@@ -38,11 +39,13 @@ export const LinkDirectoryDetails = ({ linkDirectory }: Props) => {
3839
</Fieldset>
3940
)}
4041
<div className="actions">
41-
<Link href="/links/edit">
42-
<IconButton icon={BiEdit} size="small">
43-
Edit
44-
</IconButton>
45-
</Link>
42+
<Can do="update" on="Link">
43+
<Link href="/links/edit">
44+
<IconButton icon={BiEdit} size="small">
45+
Edit
46+
</IconButton>
47+
</Link>
48+
</Can>
4649
</div>
4750
<style jsx global>{`
4851
.link-directory .actions {

0 commit comments

Comments
 (0)