Skip to content

Commit

Permalink
Handle error from audiences form (#372)
Browse files Browse the repository at this point in the history
- **Allow handling errors in useFormReducer**
- **Handle non-ok responses from Audiences context save**
- **Handle errors from AudiencesForm**
- **Fix URL Helper**
- **Allow handling errors in useFormReducer**
  • Loading branch information
xjunior authored Aug 6, 2024
1 parent c9f3dfa commit 1bee173
Show file tree
Hide file tree
Showing 12 changed files with 87 additions and 40 deletions.
14 changes: 14 additions & 0 deletions audiences-react/docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Unreleased

# Version 1.2.1 (2024-08-06)

- Add error handling to audiences form [#372](https://github.com/powerhome/audiences/pull/372)

# Version 1.2.0 (2024-07-18)

- Fix involuntary form reset by [#355](https://github.com/powerhome/audiences/pull/355)
- Update dependency @types/node to v20.14.5 [#343](https://github.com/powerhome/audiences/pull/343)
- Update dependency vite to v5.3.1 by [#340](https://github.com/powerhome/audiences/pull/340)
- Update typescript-eslint monorepo to v7.13.1 [#339](https://github.com/powerhome/audiences/pull/339)
- Update dependency prettier to v3.3.2 by [#334](https://github.com/powerhome/audiences/pull/334)
- Update dependency @types/lodash to v4.17.5 [#335](https://github.com/powerhome/audiences/pull/335)
- Update dependency @vitejs/plugin-react to v4.3 [#337](https://github.com/powerhome/audiences/pull/337)

# Version 1.1.0 (2024-06-10)

- Update UX [#329](https://github.com/powerhome/audiences/pull/329)
Expand Down
2 changes: 1 addition & 1 deletion audiences-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "audiences",
"version": "1.2.0",
"version": "1.2.1",
"description": "Audiences SCIM client",
"files": [
"dist/*.*",
Expand Down
6 changes: 5 additions & 1 deletion audiences-react/src/AudienceForm/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from "react"
import { Button, Flex } from "playbook-ui"
import { FixedConfirmationToast, Button, Flex } from "playbook-ui"

import { GroupCriterion, ScimObject } from "../types"
import { toSentence } from "./toSentence"
Expand Down Expand Up @@ -29,6 +29,7 @@ export const AudienceForm = ({
saving,
fetchUsers,
save,
error,
value: context,
isDirty,
change,
Expand Down Expand Up @@ -75,6 +76,9 @@ export const AudienceForm = ({
isDirty={isDirty()}
onToggle={(all: boolean) => change("match_all", all)}
>
{error && (
<FixedConfirmationToast status="error" text={error} margin="sm" />
)}
{allowIndividuals && !context.match_all && (
<ScimResourceTypeahead
label="Add Individuals"
Expand Down
7 changes: 6 additions & 1 deletion audiences-react/src/audiences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function useAudiences(uri: string): UseAudienceContext {
const {
get,
put,
response,
loading: saving,
} = useFetch(uri, { cachePolicy: CachePolicies.NO_CACHE })
const criteriaForm = useFormReducer<AudienceContext>(data, {
Expand Down Expand Up @@ -74,7 +75,11 @@ export function useAudiences(uri: string): UseAudienceContext {

async function save() {
const updatedContext = await put(criteriaForm.value)
criteriaForm.reset(updatedContext)
if (response.ok) {
criteriaForm.reset(updatedContext)
} else {
criteriaForm.setError("Unhandled server error")
}
}

return {
Expand Down
80 changes: 50 additions & 30 deletions audiences-react/src/useFormReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,30 @@ import { useState, useReducer, useCallback } from "react"
export interface RegistryAction {
type: string
}
type ResetAction<T> = RegistryAction & { value: T }
type ErrorAction = RegistryAction & { message: string }
type ResetAction<T> = RegistryAction & { state: FormState<T> }
type ChangeAction = RegistryAction & { name: string; value: any } // eslint-disable-line @typescript-eslint/no-explicit-any
type ReducerAction<T> = (value: T, action: RegistryAction) => T
type ReducerRegistry<T> = Record<string, ReducerAction<T>>
type ReducerAction<T> = (
value: FormState<T>,
action: RegistryAction,
) => FormState<T>
type NestedReducerAction<T> = (value: T, action: RegistryAction) => T
type NestedReducerRegistry<T> = Record<string, NestedReducerAction<T>>
export type FormState<T> = {
error?: string | undefined
value: T
}

const DefaultFormReducers = {
change<T>(value: T, action: ChangeAction): T {
return set(action.name, action.value, value as object) as T
change<T>(state: FormState<T>, action: ChangeAction): FormState<T> {
const value = set(action.name, action.value, state.value as object) as T
return { ...state, value }
},
reset<T>(_: T, action: ResetAction<T>) {
return action.value
reset<T>(_: FormState<T>, action: ResetAction<T>): FormState<T> {
return action.state
},
error<T>(state: FormState<T>, action: ErrorAction): FormState<T> {
return { ...state, error: action.message }
},
}

Expand All @@ -24,63 +37,70 @@ const form = {
change(name: string, value: any): ChangeAction {
return { type: "change", name, value }
},
reset<T>(value: T): ResetAction<T> {
return { type: "reset", value }
reset<T>(state: FormState<T>): ResetAction<T> {
return { type: "reset", state }
},
reducer<T>(nestedReducers?: ReducerRegistry<T>): ReducerAction<T> {
return (value: T, action: RegistryAction) => {
const reducer =
get(DefaultFormReducers, action.type) ||
get(nestedReducers, action.type)

if (reducer) {
return reducer(value, action)
error<T>(message: string): ErrorAction {

Check warning on line 43 in audiences-react/src/useFormReducer.ts

View workflow job for this annotation

GitHub Actions / node / Node 21

'T' is defined but never used

Check warning on line 43 in audiences-react/src/useFormReducer.ts

View workflow job for this annotation

GitHub Actions / node / Node 21

'T' is defined but never used

Check warning on line 43 in audiences-react/src/useFormReducer.ts

View workflow job for this annotation

GitHub Actions / node / Node 21

'T' is defined but never used
return { type: "error", message }
},
reducer<T>(nestedReducers?: NestedReducerRegistry<T>): ReducerAction<T> {
return (state: FormState<T>, action: RegistryAction) => {
if (action.type in DefaultFormReducers) {
return get(DefaultFormReducers, action.type)(state, action)
} else if (nestedReducers && action.type in nestedReducers) {
const value = get(nestedReducers, action.type)(state.value, action)
return { ...state, value }
}
}
},
}

export type UseFormReducer<T> = {
export type UseFormReducer<T> = FormState<T> & {
isDirty: (attribute?: string) => boolean
value: T
dispatch: ReturnType<typeof useReducer>[1]
reset: (newInitial?: T) => void
setError: (message: string) => void
change: (name: string, value: any) => void // eslint-disable-line @typescript-eslint/no-explicit-any
}
export default function useFormReducer<T>(
initial: T,
nestedReducer?: ReducerRegistry<T>,
nestedReducer?: NestedReducerRegistry<T>,
): UseFormReducer<T> {
const [initialValue, setInitialValue] = useState<T>(initial)
const [value, dispatch] = useReducer(
form.reducer(nestedReducer),
initialValue,
)
const [state, dispatch] = useReducer(form.reducer(nestedReducer), {
value: initialValue,
})

const isDirty = useCallback(
(attribute?: string) => {
if (attribute) {
return !isEqual(get(initialValue, attribute), get(value, attribute))
return !isEqual(
get(initialValue, attribute),
get(state.value, attribute),
)
} else {
return !isEqual(initialValue, value)
return !isEqual(initialValue, state.value)
}
},
[initialValue, value],
[initialValue, state.value],
)
const reset = (newInitial?: T) => {
if (newInitial) {
setInitialValue(newInitial)
dispatch(form.reset(newInitial))
dispatch(form.reset({ value: newInitial }))
} else {
dispatch(form.reset(initialValue))
dispatch(form.reset({ value: initialValue }))
}
}

return {
...state,
isDirty,
value,
dispatch,
reset,
setError(message: string) {
dispatch(form.error(message))
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
change(name: string, value: any) {
dispatch(form.change(name, value))
Expand Down
2 changes: 1 addition & 1 deletion audiences/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
audiences (1.2.0)
audiences (1.2.1)
rails (>= 6.0)

GEM
Expand Down
4 changes: 2 additions & 2 deletions audiences/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
Rails.application.routes.draw do
direct :audience_context do |owner, relation = nil|
context = Audiences::Context.for(owner, relation: relation)
Audiences::Engine.routes.url_helpers.route_for(:signed_context, key: context.signed_key, **url_options)
audiences.route_for(:signed_context, key: context.signed_key, **url_options)
end

direct :audience_scim_proxy do |options|
Audiences::Engine.routes.url_helpers.route_for(:scim_proxy, **url_options, **options)
audiences.route_for(:scim_proxy, **url_options, **options)
end
end
4 changes: 4 additions & 0 deletions audiences/docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Unreleased

# Version 1.2.1 (2024-08-06)

- Fix audiences URL helpers [#372](https://github.com/powerhome/audiences/pull/372)

# Version 1.2.0 (2024-07-24)

- Add `has_audience` and the ability to attach multiple audiences to the same owner [#363](https://github.com/powerhome/audiences/pull/363)
Expand Down
2 changes: 1 addition & 1 deletion audiences/gemfiles/rails_6_1.gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: ..
specs:
audiences (1.2.0)
audiences (1.2.1)
rails (>= 6.0)

GEM
Expand Down
2 changes: 1 addition & 1 deletion audiences/gemfiles/rails_7_0.gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: ..
specs:
audiences (1.2.0)
audiences (1.2.1)
rails (>= 6.0)

GEM
Expand Down
2 changes: 1 addition & 1 deletion audiences/gemfiles/rails_7_1.gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: ..
specs:
audiences (1.2.0)
audiences (1.2.1)
rails (>= 6.0)

GEM
Expand Down
2 changes: 1 addition & 1 deletion audiences/lib/audiences/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Audiences
VERSION = "1.2.0"
VERSION = "1.2.1"
end

0 comments on commit 1bee173

Please sign in to comment.