diff --git a/audiences-react/package.json b/audiences-react/package.json
index ff5eca7d..2a2d7a5a 100644
--- a/audiences-react/package.json
+++ b/audiences-react/package.json
@@ -1,9 +1,11 @@
{
"name": "audiences",
- "version": "1.1.0",
+ "version": "1.2.0",
"description": "Audiences SCIM client",
"files": [
- "dist"
+ "dist/*.*",
+ "docs/README.md",
+ "docs/CHANGELOG.md"
],
"main": "./dist/audiences.umd.js",
"module": "./dist/audiences.es.js",
@@ -35,7 +37,6 @@
"license": "MIT",
"dependencies": {
"lodash": "^4.17.21",
- "react-hook-form": "^7.45.2",
"use-http": "^1.0.28"
},
"prettier": "@powerhome/eslint-config/prettier",
diff --git a/audiences-react/src/AudienceForm/AllToggle.tsx b/audiences-react/src/AudienceForm/AllToggle.tsx
deleted file mode 100644
index 466bf0cb..00000000
--- a/audiences-react/src/AudienceForm/AllToggle.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { useFormContext } from "react-hook-form"
-import { Button, Card, Flex } from "playbook-ui"
-
-import { Header } from "./Header"
-
-type MainProps = {
- total: number
- name: string
- children: React.ReactNode
-}
-
-export function AllToggle({ total, name, children }: MainProps) {
- const { formState, watch } = useFormContext()
- const all = watch(name)
-
- return (
-
-
-
-
-
-
-
- {all || children}
-
-
-
-
- {formState.isDirty && (
-
- )}
-
-
-
-
- )
-}
diff --git a/audiences-react/src/AudienceForm/CriteriaActions.tsx b/audiences-react/src/AudienceForm/CriteriaActions.tsx
index 81c4de0c..745b9e15 100644
--- a/audiences-react/src/AudienceForm/CriteriaActions.tsx
+++ b/audiences-react/src/AudienceForm/CriteriaActions.tsx
@@ -1,18 +1,21 @@
import { Button, Icon, PbReactPopover, List, ListItem } from "playbook-ui"
import { useState } from "react"
-import { MembersModalButton } from "./MembersModal"
+import { MembersModalButton } from "./MembersModalButton"
import { GroupCriterion } from "../types"
import { CriteriaDescription } from "./CriteriaDescription"
+import { useAudiences } from "../audiences"
type CriteriaActionsProps = {
- viewUsers: boolean
criterion: GroupCriterion
- onRequestRemove: () => void
+ fetchUsers: ReturnType["fetchUsers"]
onRequestEdit: () => void
+ onRequestRemove: () => void
+ viewUsers: boolean
}
export function CriteriaActions({
viewUsers,
criterion,
+ fetchUsers,
onRequestRemove,
onRequestEdit,
}: CriteriaActionsProps) {
@@ -59,6 +62,7 @@ export function CriteriaActions({
padding="xs"
text="View Members"
title={}
+ fetchUsers={fetchUsers}
total={criterion.count}
criterion={criterion}
/>
diff --git a/audiences-react/src/AudienceForm/CriteriaCard.tsx b/audiences-react/src/AudienceForm/CriteriaCard.tsx
index 35b50669..5237750b 100644
--- a/audiences-react/src/AudienceForm/CriteriaCard.tsx
+++ b/audiences-react/src/AudienceForm/CriteriaCard.tsx
@@ -1,18 +1,21 @@
import { Card, Flex, FlexItem, Caption } from "playbook-ui"
-import isEmpty from "lodash/isEmpty"
+import { isEmpty } from "lodash"
import type { GroupCriterion } from "../types"
import { CriteriaDescription } from "./CriteriaDescription"
import { CriteriaActions } from "./CriteriaActions"
+import { useAudiences } from "../audiences"
type CriteriaCardProps = {
- viewUsers: boolean
criterion?: GroupCriterion
- onRequestRemove: () => void
+ fetchUsers: ReturnType["fetchUsers"]
onRequestEdit: () => void
+ onRequestRemove: () => void
+ viewUsers: boolean
}
export function CriteriaCard({
criterion,
+ fetchUsers,
viewUsers,
onRequestRemove,
onRequestEdit,
@@ -34,6 +37,7 @@ export function CriteriaCard({
{map(Prepositions, (prep, key) =>
isEmpty(groups[key]) ? null : (
-
+
-
+
),
)}
{"."}
diff --git a/audiences-react/src/AudienceForm/CriteriaForm.tsx b/audiences-react/src/AudienceForm/CriteriaForm.tsx
index c54e3794..e91fb20c 100644
--- a/audiences-react/src/AudienceForm/CriteriaForm.tsx
+++ b/audiences-react/src/AudienceForm/CriteriaForm.tsx
@@ -1,25 +1,39 @@
-import { flatten, isEmpty, keys, values } from "lodash"
import { Button, Card, Flex, IconValue } from "playbook-ui"
-import { useFormContext } from "react-hook-form"
+import { flatten, isEmpty, keyBy, mapValues, values } from "lodash"
import { CriteriaDescription } from "./CriteriaDescription"
import { ScimResourceTypeahead } from "./ScimResourceTypeahead"
-import { useMemo } from "react"
+import { GroupCriterion } from "../types"
+import useFormReducer from "../useFormReducer"
export type CriteriaFormProps = {
- current: string
- onClose: () => void
+ resources: string[]
+ criterion: GroupCriterion | undefined
+ onSave: (criterion: GroupCriterion) => void
+ onCancel: () => void
}
-export function CriteriaForm({ current, onClose }: CriteriaFormProps) {
- const { setValue, watch } = useFormContext()
- const value = watch(`${current}.groups`)
- const initialValue = useMemo(() => ({ ...value }), [current])
+function buildCriterion(resources: string[]) {
+ const emptyGroups = mapValues(keyBy(resources), () => [])
+ return { groups: emptyGroups }
+}
+export function CriteriaForm({
+ resources,
+ criterion,
+ onSave,
+ onCancel,
+}: CriteriaFormProps) {
+ const { value, change } = useFormReducer(
+ criterion || buildCriterion(resources),
+ )
- const emptyCriteria = isEmpty(flatten(values(value)))
+ const emptyCriteria = isEmpty(flatten(values(value.groups)))
+
+ const handleSave = () => {
+ onSave(value)
+ }
const handleCancel = () => {
- setValue(`${current}.groups`, initialValue)
- onClose()
+ onCancel()
}
return (
@@ -37,19 +51,20 @@ export function CriteriaForm({ current, onClose }: CriteriaFormProps) {
- {keys(value).map((resourceId) => (
+ {resources.map((resourceId) => (
change(`groups.${resourceId}`, values)}
/>
))}
- {emptyCriteria || }
+ {emptyCriteria || }
-
+
diff --git a/audiences-react/src/AudienceForm/CriteriaList.tsx b/audiences-react/src/AudienceForm/CriteriaList.tsx
index ff50d9ad..aa75470e 100644
--- a/audiences-react/src/AudienceForm/CriteriaList.tsx
+++ b/audiences-react/src/AudienceForm/CriteriaList.tsx
@@ -1,36 +1,34 @@
import { Button, Flex, FlexItem } from "playbook-ui"
-import { useFormContext } from "react-hook-form"
-
-import { GroupCriterion } from "../types"
-
+import { AudienceContext } from "../types"
import { CriteriaCard } from "./CriteriaCard"
+import { useAudiences } from "../audiences"
-type CriteriaListFieldsProps = {
+type CriteriaListProps = {
addCriteriaLabel: string
+ context: AudienceContext
+ fetchUsers: ReturnType["fetchUsers"]
onAddCriteria: () => void
- onRemoveCriteria: (index: number) => void
onEditCriteria: (index: number) => void
+ onRemoveCriteria: (index: number) => void
}
export function CriteriaList({
+ context,
+ fetchUsers,
addCriteriaLabel,
onAddCriteria,
onRemoveCriteria,
onEditCriteria,
-}: CriteriaListFieldsProps) {
- const { watch, getFieldState } = useFormContext()
- const currentCriteria = (watch("criteria") || []) as GroupCriterion[]
- const isCriterionDirty = (index: number) =>
- getFieldState(`criteria.${index}`).isDirty
-
+}: CriteriaListProps) {
return (
- {currentCriteria.map((criterion, index: number) => (
+ {context.criteria.map((criterion, index: number) => (
onEditCriteria(index)}
onRequestRemove={() => onRemoveCriteria(index)}
+ viewUsers={criterion.count !== undefined}
/>
))}
diff --git a/audiences-react/src/AudienceForm/Header.tsx b/audiences-react/src/AudienceForm/Header.tsx
deleted file mode 100644
index 5743e30e..00000000
--- a/audiences-react/src/AudienceForm/Header.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { Flex, FlexItem, Caption, Toggle } from "playbook-ui"
-import { useFormContext } from "react-hook-form"
-
-import { MembersModalButton } from "./MembersModal"
-
-type HeaderProps = {
- name: string
- total?: number
-}
-export function Header({ name, total }: HeaderProps) {
- const { register, formState } = useFormContext()
-
- return (
-
-
-
-
- {formState.isDirty ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/audiences-react/src/AudienceForm/MatchAllToggleHeader.tsx b/audiences-react/src/AudienceForm/MatchAllToggleHeader.tsx
new file mode 100644
index 00000000..6a569be4
--- /dev/null
+++ b/audiences-react/src/AudienceForm/MatchAllToggleHeader.tsx
@@ -0,0 +1,68 @@
+import { FlexItem, Toggle } from "playbook-ui"
+import { ReactNode } from "react"
+import { Card, Caption, Flex } from "playbook-ui"
+
+import { MembersModalButton } from "./MembersModalButton"
+import { useAudiences } from "../audiences"
+
+type MatchAllToggleCardProps = {
+ children: ReactNode
+ count: number
+ enabled: boolean
+ fetchUsers: ReturnType["fetchUsers"]
+ isDirty: boolean
+ onToggle: (all: boolean) => void
+}
+export function MatchAllToggleCard({
+ children,
+ count,
+ enabled,
+ fetchUsers,
+ isDirty,
+ onToggle,
+}: MatchAllToggleCardProps) {
+ return (
+
+
+
+
+
+ {isDirty ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ onToggle(!enabled)}
+ />
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+ )
+}
diff --git a/audiences-react/src/AudienceForm/MembersModal/index.tsx b/audiences-react/src/AudienceForm/MembersModalButton/index.tsx
similarity index 92%
rename from audiences-react/src/AudienceForm/MembersModal/index.tsx
rename to audiences-react/src/AudienceForm/MembersModalButton/index.tsx
index 4ee410dd..5a0701ec 100644
--- a/audiences-react/src/AudienceForm/MembersModal/index.tsx
+++ b/audiences-react/src/AudienceForm/MembersModalButton/index.tsx
@@ -13,22 +13,22 @@ import {
} from "playbook-ui"
import type { GroupCriterion, ScimObject } from "../../types"
-import { useAudienceContext } from "../../audiences"
import styles from "./style.module.css"
+import { useAudiences } from "../../audiences"
type MembersModalButtonProps = any & {
- title: React.ReactNode
criterion?: GroupCriterion
+ fetchUsers: ReturnType["fetchUsers"]
+ title: React.ReactNode
total: number
}
-
export function MembersModalButton({
title,
total,
criterion,
+ fetchUsers,
...buttonOptions
}: MembersModalButtonProps) {
- const { fetchUsers } = useAudienceContext()
const [loading, setLoading] = useState()
const [current, setUsers] = useState<
Awaited> | undefined
@@ -37,8 +37,10 @@ export function MembersModalButton({
const [showMembers, setShowMembers] = useState(false)
useEffect(() => {
- load(0).then(setUsers)
- }, [search])
+ if (showMembers) {
+ load(0).then(setUsers)
+ }
+ }, [search, showMembers])
async function load(offset: number) {
setLoading(true)
@@ -49,7 +51,7 @@ export function MembersModalButton({
async function handleLoadMore() {
const moreUsers = await load(current!.users.length)
- setUsers((current) => ({
+ setUsers((current: any) => ({
count: moreUsers.count,
users: [...current!.users, ...moreUsers.users],
}))
diff --git a/audiences-react/src/AudienceForm/MembersModal/style.module.css b/audiences-react/src/AudienceForm/MembersModalButton/style.module.css
similarity index 100%
rename from audiences-react/src/AudienceForm/MembersModal/style.module.css
rename to audiences-react/src/AudienceForm/MembersModalButton/style.module.css
diff --git a/audiences-react/src/AudienceForm/ScimResourceTypeahead.tsx b/audiences-react/src/AudienceForm/ScimResourceTypeahead.tsx
index 7b7bf549..dd798568 100644
--- a/audiences-react/src/AudienceForm/ScimResourceTypeahead.tsx
+++ b/audiences-react/src/AudienceForm/ScimResourceTypeahead.tsx
@@ -1,7 +1,6 @@
-import debounce from "lodash/debounce"
-import { useController } from "react-hook-form"
-import get from "lodash/get"
+import { debounce, get } from "lodash"
import { Typeahead } from "playbook-ui"
+
import { useScim } from "../scim"
import { ScimObject } from "../types"
@@ -24,19 +23,20 @@ function playbookOptions(objects: ScimObject[]): PlaybookOption[] {
type ScimResourceTypeaheadProps = {
label: string
- name: string
+ value: ScimObject[]
+ onChange: (values: ScimObject[]) => void
resourceId: string
}
export function ScimResourceTypeahead({
- name,
resourceId,
+ onChange,
+ value,
...typeaheadProps
}: ScimResourceTypeaheadProps) {
- const { field } = useController({ name })
const { filter } = useScim()
- function onChange(value: any, ...event: any[]) {
- field.onChange(value || [], ...event)
+ function handleChange(value: any, ...event: any[]) {
+ onChange(value || [])
}
const searchResourceOptions = async (
@@ -54,10 +54,9 @@ export function ScimResourceTypeahead({
loadOptions={debounce(searchResourceOptions, 600)}
placeholder=""
{...typeaheadProps}
- {...field}
ref={undefined} // Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
- value={playbookOptions(field.value)}
- onChange={onChange}
+ value={playbookOptions(value)}
+ onChange={handleChange}
/>
)
}
diff --git a/audiences-react/src/AudienceForm/index.tsx b/audiences-react/src/AudienceForm/index.tsx
index 5182f701..3663c6b8 100644
--- a/audiences-react/src/AudienceForm/index.tsx
+++ b/audiences-react/src/AudienceForm/index.tsx
@@ -1,75 +1,118 @@
-import { FormProvider, useForm } from "react-hook-form"
-import { Card, Flex, Icon } from "playbook-ui"
+import { useState } from "react"
+import { Button, Flex } from "playbook-ui"
-import { AudienceContext } from "../types"
-
-import { useAudienceContext } from "../audiences"
+import { GroupCriterion, ScimObject } from "../types"
import { toSentence } from "./toSentence"
import { ScimResourceTypeahead } from "./ScimResourceTypeahead"
import { CriteriaList } from "./CriteriaList"
-import { AllToggle } from "./AllToggle"
import { CriteriaForm } from "./CriteriaForm"
-import { useCriteriaEditForm } from "./useCriteriaEditForm"
+import { useAudiences } from "../audiences"
+import { MatchAllToggleCard } from "./MatchAllToggleHeader"
type AudienceFormProps = {
+ uri: string
userResource: string
groupResources: string[]
allowIndividuals: boolean
}
export const AudienceForm = ({
+ uri,
userResource,
groupResources,
allowIndividuals = true,
}: AudienceFormProps) => {
- const { context, update } = useAudienceContext()
- const form = useForm({ values: context, mode: "onChange" })
+ const [editing, setEditing] = useState()
+
const {
- currentEditing,
- addNewCriteria,
- editCriteria,
+ saving,
+ fetchUsers,
+ save,
+ value: context,
+ isDirty,
+ change,
+ reset,
removeCriteria,
- closeCriteria,
- } = useCriteriaEditForm({ form, groupResources })
+ updateCriteria,
+ } = useAudiences(uri)
+
+ const handleRemoveCriteria = (index: number) => {
+ if (confirm("Remove criteria?")) {
+ removeCriteria(index)
+ }
+ }
+
+ const handleSaveCriteria = (criterion: GroupCriterion) => {
+ updateCriteria(editing!, criterion)
+ setEditing(undefined)
+ }
+
+ const handleCreateCriteria = () => {
+ setEditing(context.criteria.length)
+ }
if (!context) {
+ return null
+ }
+
+ if (editing !== undefined) {
return (
-
-
-
-
-
+ setEditing(undefined)}
+ />
)
}
return (
-
-
-
+
+
)
}
diff --git a/audiences-react/src/AudienceForm/useCriteriaEditForm.ts b/audiences-react/src/AudienceForm/useCriteriaEditForm.ts
deleted file mode 100644
index bdcdeb0d..00000000
--- a/audiences-react/src/AudienceForm/useCriteriaEditForm.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { useState } from "react"
-import { useFieldArray, UseFormReturn } from "react-hook-form"
-import { every, isEmpty, keyBy, mapValues, omitBy } from "lodash"
-import { AudienceContext, GroupCriterion } from "../types"
-
-const validateCriteria = (criteria: GroupCriterion[]) =>
- every(criteria, validateCriterion)
-const validateCriterion = ({ groups }: GroupCriterion) =>
- !isEmpty(omitBy(groups, isEmpty))
-type UseCriteriaEditFormType = {
- form: UseFormReturn
- groupResources: string[]
-}
-
-export function useCriteriaEditForm({
- form,
- groupResources,
-}: UseCriteriaEditFormType) {
- const [currentEditing, setCurrentEditing] = useState()
- const {
- remove,
- append: appendCriteria,
- fields: currentCriteria,
- } = useFieldArray({
- name: "criteria",
- control: form.control,
- rules: { validate: validateCriteria },
- })
-
- const addNewCriteria = () => {
- const emptyCriteria = mapValues(keyBy(groupResources), () => [])
- appendCriteria({ groups: emptyCriteria })
- setCurrentEditing(currentCriteria.length)
- }
- const removeCriteria = (index: number) => {
- if (confirm("Remove criteria?")) {
- remove(index)
- }
- }
- const editCriteria = (index: number) => {
- setCurrentEditing(index)
- }
- const closeCriteria = () => {
- if (!form.formState.isValid) {
- remove(currentEditing)
- }
- setCurrentEditing(undefined)
- }
-
- return {
- currentEditing,
- addNewCriteria,
- editCriteria,
- removeCriteria,
- closeCriteria,
- }
-}
diff --git a/audiences-react/src/audiences.ts b/audiences-react/src/audiences.ts
index 686211f4..62b975f2 100644
--- a/audiences-react/src/audiences.ts
+++ b/audiences-react/src/audiences.ts
@@ -1,47 +1,66 @@
-import { useState, useEffect, useContext } from "react"
-import { CachePolicies, UseFetchObjectReturn, useFetch } from "use-http"
+import { useEffect } from "react"
+import useFetch, { CachePolicies } from "use-http"
+import useFormReducer, {
+ RegistryAction,
+ UseFormReducer,
+} from "./useFormReducer"
import { AudienceContext, GroupCriterion, ScimObject } from "./types"
-import { createContext } from "react"
+import { set } from "lodash/fp"
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type ContextProps = UseFetchObjectReturn & {
- context?: AudienceContext
- setContext: (newContext: AudienceContext) => void
+type RemoveCriteriaAction = RegistryAction & {
+ index: number
}
-
-const Context = createContext(undefined)
-
-export function useAudience(uri: string): ContextProps {
- const [context, setContext] = useState()
- const fetch = useFetch(uri, {
- cachePolicy: CachePolicies.NO_CACHE,
- onError({ error }) {
- throw error.message
- },
- })
-
- useEffect(
- function () {
- fetch.get().then(setContext)
- },
- [uri],
- )
-
- return { context, setContext, ...fetch }
+type UpdateCriteriaAction = RegistryAction & {
+ index: number
+ criterion: GroupCriterion
}
-type UseAudienceContext = {
- context?: AudienceContext
- update: (attrs: AudienceContext) => void
+type UseAudienceContext = UseFormReducer & {
+ saving: boolean
+ save: () => void
fetchUsers: (
criterion?: GroupCriterion,
search?: string,
offset?: number,
) => Promise<{ count: number; users: ScimObject[] }>
+ removeCriteria: (index: number) => void
+ updateCriteria: (index: number, criteria: GroupCriterion) => void
}
-export function useAudienceContext(): UseAudienceContext {
- const { context, setContext, get, put } = useContext(Context)!
+
+export function useAudiences(uri: string): UseAudienceContext {
+ const { data } = useFetch(uri, [uri])
+ const {
+ get,
+ put,
+ loading: saving,
+ } = useFetch(uri, { cachePolicy: CachePolicies.NO_CACHE })
+ const criteriaForm = useFormReducer(data, {
+ "remove-criteria": (
+ context,
+ _,
+ action = _ as RemoveCriteriaAction,
+ ): AudienceContext => ({
+ ...context,
+ criteria: [
+ ...context.criteria.slice(0, action.index),
+ ...context.criteria.slice(action.index + 1),
+ ],
+ }),
+ "update-criteria": (
+ context,
+ _,
+ action = _ as UpdateCriteriaAction,
+ ): AudienceContext =>
+ set(
+ `criteria.${action.index}`,
+ action.criterion,
+ context,
+ ) as AudienceContext,
+ })
+ useEffect(() => {
+ criteriaForm.reset(data)
+ }, [data])
async function fetchUsers(
criterion?: GroupCriterion,
@@ -53,16 +72,19 @@ export function useAudienceContext(): UseAudienceContext {
)
}
- async function update(attrs: AudienceContext) {
- try {
- const updatedContext = await put(attrs)
- setContext(updatedContext)
- } catch (e) {
- console.log(attrs, e)
- }
+ async function save() {
+ const updatedContext = await put(criteriaForm.value)
+ criteriaForm.reset(updatedContext)
}
- return { context, update, fetchUsers }
+ return {
+ saving,
+ fetchUsers,
+ save,
+ ...criteriaForm,
+ removeCriteria: (index: number) =>
+ criteriaForm.dispatch({ type: "remove-criteria", index }),
+ updateCriteria: (index: number, criterion: GroupCriterion) =>
+ criteriaForm.dispatch({ type: "update-criteria", index, criterion }),
+ }
}
-
-export default Context
diff --git a/audiences-react/src/example.tsx b/audiences-react/src/example.tsx
index e7ca280e..9a291744 100644
--- a/audiences-react/src/example.tsx
+++ b/audiences-react/src/example.tsx
@@ -7,7 +7,7 @@ import { StrictMode } from "react"
import ReactDOM from "react-dom"
import { Title } from "playbook-ui"
-import { AudienceEditor } from "./AudienceEditor"
+import { AudienceEditor } from "."
const audienceKey =
"BAh7CEkiCGdpZAY6BkVUSSIfZ2lkOi8vZHVtbXkvRXhhbXBsZU93bmVyLzEGOwBUSSIMcHVycG9zZQY7AFRJIg5hdWRpZW5jZXMGOwBUSSIPZXhwaXJlc19hdAY7AFRJIh0yMDIzLTA5LTAyVDE4OjE1OjQxLjQxNloGOwBU--7bcda79e962d221c4820cc4afebd194d288d7dc5"
diff --git a/audiences-react/src/index.ts b/audiences-react/src/index.ts
deleted file mode 100644
index 67a9b81f..00000000
--- a/audiences-react/src/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { AudienceEditor } from "./AudienceEditor"
diff --git a/audiences-react/src/AudienceEditor.tsx b/audiences-react/src/index.tsx
similarity index 57%
rename from audiences-react/src/AudienceEditor.tsx
rename to audiences-react/src/index.tsx
index 9c01113a..03227426 100644
--- a/audiences-react/src/AudienceEditor.tsx
+++ b/audiences-react/src/index.tsx
@@ -1,5 +1,4 @@
import { AudienceForm } from "./AudienceForm"
-import Audiences, { useAudience } from "./audiences"
import Scim from "./scim"
const UserResourceId = "Users"
@@ -15,17 +14,14 @@ export function AudienceEditor({
scimUri,
allowIndividuals = true,
}: AudienceEditorProps) {
- const context = useAudience(uri)
-
return (
-
-
-
+
)
}
diff --git a/audiences-react/src/useFormReducer.ts b/audiences-react/src/useFormReducer.ts
new file mode 100644
index 00000000..9099eb7b
--- /dev/null
+++ b/audiences-react/src/useFormReducer.ts
@@ -0,0 +1,89 @@
+import { get, isEqual } from "lodash"
+import { set } from "lodash/fp"
+import { useState, useReducer, useCallback } from "react"
+
+export interface RegistryAction {
+ type: string
+}
+type ResetAction = RegistryAction & { value: T }
+type ChangeAction = RegistryAction & { name: string; value: any } // eslint-disable-line @typescript-eslint/no-explicit-any
+type ReducerAction = (value: T, action: RegistryAction) => T
+type ReducerRegistry = Record>
+
+const DefaultFormReducers = {
+ change(value: T, action: ChangeAction): T {
+ return set(action.name, action.value, value as object) as T
+ },
+ reset(_: T, action: ResetAction) {
+ return action.value
+ },
+}
+
+const form = {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ change(name: string, value: any): ChangeAction {
+ return { type: "change", name, value }
+ },
+ reset(value: T): ResetAction {
+ return { type: "reset", value }
+ },
+ reducer(nestedReducers?: ReducerRegistry): ReducerAction {
+ return (value: T, action: RegistryAction) => {
+ const reducer =
+ get(DefaultFormReducers, action.type) ||
+ get(nestedReducers, action.type)
+
+ if (reducer) {
+ return reducer(value, action)
+ }
+ }
+ },
+}
+
+export type UseFormReducer = {
+ isDirty: (attribute?: string) => boolean
+ value: T
+ dispatch: ReturnType[1]
+ reset: (newInitial?: T) => void
+ change: (name: string, value: any) => void // eslint-disable-line @typescript-eslint/no-explicit-any
+}
+export default function useFormReducer(
+ initial: T,
+ nestedReducer?: ReducerRegistry,
+): UseFormReducer {
+ const [initialValue, setInitialValue] = useState(initial)
+ const [value, dispatch] = useReducer(
+ form.reducer(nestedReducer),
+ initialValue,
+ )
+
+ const isDirty = useCallback(
+ (attribute?: string) => {
+ if (attribute) {
+ return !isEqual(get(initialValue, attribute), get(value, attribute))
+ } else {
+ return !isEqual(initialValue, value)
+ }
+ },
+ [initialValue, value],
+ )
+ const reset = (newInitial?: T) => {
+ if (newInitial) {
+ setInitialValue(newInitial)
+ dispatch(form.reset(newInitial))
+ } else {
+ dispatch(form.reset(initialValue))
+ }
+ }
+
+ return {
+ isDirty,
+ value,
+ dispatch,
+ reset,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ change(name: string, value: any) {
+ dispatch(form.change(name, value))
+ },
+ }
+}
diff --git a/audiences-react/vite.config.ts b/audiences-react/vite.config.ts
index f6748b3e..89e703e5 100644
--- a/audiences-react/vite.config.ts
+++ b/audiences-react/vite.config.ts
@@ -13,7 +13,7 @@ module.exports = defineConfig({
build: {
target: ["es2018"],
lib: {
- entry: path.resolve(__dirname, "src/index.ts"),
+ entry: path.resolve(__dirname, "src/index.tsx"),
name: "audiences",
fileName: (format) => `audiences.${format}.js`,
},
diff --git a/audiences-react/yarn.lock b/audiences-react/yarn.lock
index f4dc3164..51c3ff33 100644
--- a/audiences-react/yarn.lock
+++ b/audiences-react/yarn.lock
@@ -4264,11 +4264,6 @@ react-highlight-words@^0.20.0:
memoize-one "^4.0.0"
prop-types "^15.5.8"
-react-hook-form@^7.45.2:
- version "7.52.0"
- resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.52.0.tgz#e52b33043e283719586b9dd80f6d51b68dd3999c"
- integrity sha512-mJX506Xc6mirzLsmXUJyqlAI3Kj9Ph2RhplYhUVffeOQSnubK2uVqBFOBJmvKikvbFV91pxVXmDiR+QMF19x6A==
-
react-input-autosize@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.2.tgz#fcaa7020568ec206bc04be36f4eb68e647c4d8c2"
@@ -4774,16 +4769,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
-"string-width-cjs@npm:string-width@^4.2.0":
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-string-width@^4.1.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -4874,14 +4860,7 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1"
es-object-atoms "^1.0.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==