diff --git a/readme.md b/readme.md index 5b020fc..fb8234b 100644 --- a/readme.md +++ b/readme.md @@ -67,8 +67,6 @@ using the `When` component. ``` -(Note: Using `` without a `` is not implemented yet). - In order to avoid passing user/policy/resource props to every usage of the `When` component you can use the `PunditProvider`. diff --git a/src/index.ts b/src/index.ts index 2246ea6..33f554d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ import Policy from './policy'; +import When from './react/when'; export { Policy }; -export { PunditProvider, When, usePundit } from './pundit-react'; +export { PunditProvider, usePundit } from './react/pundit-provider'; +export { When }; diff --git a/src/policy.ts b/src/policy.ts index 6399266..7e15d59 100644 --- a/src/policy.ts +++ b/src/policy.ts @@ -21,4 +21,12 @@ export default class Policy { add(actionName: string, actionFn: ActionFunction): void { this.actions.set(actionName, actionFn); } + + copy(user: unknown, record: unknown): Policy { + const newPolicy = new Policy(user, record); + this.actions.forEach((actionFunction, actionName) => { + newPolicy.add(actionName, actionFunction); + }); + return newPolicy; + } } diff --git a/src/pundit-react.tsx b/src/react/pundit-provider.tsx similarity index 66% rename from src/pundit-react.tsx rename to src/react/pundit-provider.tsx index 7c48d08..07780d1 100644 --- a/src/pundit-react.tsx +++ b/src/react/pundit-provider.tsx @@ -1,5 +1,5 @@ import React, { JSX, ReactElement, useMemo } from 'react'; -import Policy from './policy'; +import Policy from '../policy'; const PunditContext = React.createContext({ policy: new Policy(null, null) }); @@ -8,11 +8,6 @@ interface PunditContextProps { policy: Policy; } -interface WhenProps { - children: JSX.Element | null; - can: string; -} - export const usePundit = (): { policy: Policy } => { const value = React.useContext(PunditContext); return value; @@ -27,9 +22,3 @@ export function PunditProvider({ {children} ); } - -export function When({ can, children }: WhenProps): JSX.Element | null { - const { policy } = usePundit(); - const canPerformAction = policy?.can(can); - return canPerformAction ? children : null; -} diff --git a/src/react/when.tsx b/src/react/when.tsx new file mode 100644 index 0000000..8481476 --- /dev/null +++ b/src/react/when.tsx @@ -0,0 +1,31 @@ +import Policy from '../policy'; +import { usePundit } from './pundit-provider'; + +interface WhenProps { + children: JSX.Element | null; + can: string; + policy?: Policy; + user?: unknown; + record?: unknown; +} + +export default function When({ + children, + can, + policy, + user, + record, +}: WhenProps): JSX.Element | null { + const { policy: hookPolicy } = usePundit(); + const paramPolicy = policy?.copy(user, record); + const canPerformAction = paramPolicy + ? paramPolicy.can(can) + : hookPolicy.can(can); + return canPerformAction ? children : null; +} + +When.defaultProps = { + policy: undefined, + user: undefined, + record: undefined, +}; diff --git a/test/pundit-react.test.tsx b/test/react/pundit-provider.test.tsx similarity index 93% rename from test/pundit-react.test.tsx rename to test/react/pundit-provider.test.tsx index b62732f..9648eb7 100644 --- a/test/pundit-react.test.tsx +++ b/test/react/pundit-provider.test.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import Policy from '../src/policy'; -import { PunditProvider, When } from '../src/pundit-react'; +import Policy from '../../src/policy'; +import { PunditProvider } from '../../src/react/pundit-provider'; +import When from '../../src/react/when'; describe('', () => { const user = {}; diff --git a/test/react/when.test.tsx b/test/react/when.test.tsx new file mode 100644 index 0000000..fc652bb --- /dev/null +++ b/test/react/when.test.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import Policy from '../../src/policy'; +import When from '../../src/react/when'; + +describe('', () => { + describe('policy parameter', () => { + const user = {}; + const record = {}; + const policy = new Policy(user, record); + policy.add('view', () => true); + policy.add('edit', () => false); + + it('displays child when action is permitted using "policy" param', () => { + render( + + + + ); + expect(screen.queryByText('View')).toBeInTheDocument(); + }); + + it('does not display child when action is forbidden using "policy" param', () => { + render( + + + + ); + expect(screen.queryByText('Edit')).not.toBeInTheDocument(); + }); + + it('does not display child when "policy" param is missing', () => { + render( + + + + ); + expect(screen.queryByText('View')).not.toBeInTheDocument(); + }); + }); + + describe('user parameter', () => { + const user = {}; + const record = {}; + const policy = new Policy(null, record); + policy.add('view', () => true); + policy.add('edit', () => false); + + it('displays child when action is permitted using "user" param', () => { + render( + + + + ); + expect(screen.queryByText('View')).toBeInTheDocument(); + }); + + it('does not display child when action is forbidden using "user" param', () => { + render( + + + + ); + expect(screen.queryByText('Edit')).not.toBeInTheDocument(); + }); + + it('does not display child when the "policy" param has a null user and the "user" param is missing', () => { + render( + + + + ); + expect(screen.queryByText('View')).not.toBeInTheDocument(); + }); + + it('does not display child when "user" param is specified but the "policy" param is missing', () => { + render( + + + + ); + expect(screen.queryByText('View')).not.toBeInTheDocument(); + }); + }); + + describe('record parameter', () => { + const user = {}; + const record = {}; + const policy = new Policy(user, null); + policy.add('view', () => true); + policy.add('edit', () => false); + + it('displays child when action is permitted using "record" param', () => { + render( + + + + ); + expect(screen.queryByText('View')).toBeInTheDocument(); + }); + + it('does not display child when action is forbidden using "record" param', () => { + render( + + + + ); + expect(screen.queryByText('Edit')).not.toBeInTheDocument(); + }); + + it('does not display child when the "policy" param has a null record and the "record" param is missing', () => { + render( + + + + ); + expect(screen.queryByText('View')).not.toBeInTheDocument(); + }); + + it('does not display child when the "record" param is specified but the "policy" param is missing', () => { + render( + + + + ); + expect(screen.queryByText('View')).not.toBeInTheDocument(); + }); + }); +});