@startsimpli/hooks 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +223 -0
- package/dist/index.d.ts +223 -0
- package/dist/index.js +398 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +384 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +51 -0
- package/src/__tests__/useCRUDMutation.test.ts +116 -0
- package/src/__tests__/useRecentlyViewed.test.ts +142 -0
- package/src/__tests__/useSavedViews.test.ts +169 -0
- package/src/__tests__/useTableFilters.test.ts +110 -0
- package/src/__tests__/useWizard.test.ts +76 -0
- package/src/filter-encoding.ts +115 -0
- package/src/filter-types.ts +82 -0
- package/src/index.ts +37 -0
- package/src/useCRUDMutation.ts +26 -0
- package/src/useCSV.ts +158 -0
- package/src/useRecentlyViewed.ts +67 -0
- package/src/useSavedViews.ts +117 -0
- package/src/useTableFilters.ts +67 -0
- package/src/useWizard.ts +60 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export { useTableFilters } from './useTableFilters'
|
|
2
|
+
|
|
3
|
+
// Filter types (generic, no domain-specific fields)
|
|
4
|
+
export type {
|
|
5
|
+
FilterOperator,
|
|
6
|
+
FilterValue,
|
|
7
|
+
FilterCondition,
|
|
8
|
+
FilterGroup,
|
|
9
|
+
FilterConfig,
|
|
10
|
+
EncodedFilterState,
|
|
11
|
+
FilterValidationError,
|
|
12
|
+
FilterValidationResult,
|
|
13
|
+
} from './filter-types'
|
|
14
|
+
|
|
15
|
+
// URL filter encoding utilities (pure functions, no React/Next.js dependencies)
|
|
16
|
+
export {
|
|
17
|
+
encodeFilterConfig,
|
|
18
|
+
decodeFilterConfig,
|
|
19
|
+
parseUrlFilters,
|
|
20
|
+
createSimpleFilter,
|
|
21
|
+
mergeFilters,
|
|
22
|
+
getFilterDescription,
|
|
23
|
+
} from './filter-encoding'
|
|
24
|
+
export { useCRUDMutation } from './useCRUDMutation'
|
|
25
|
+
export { useSavedViews } from './useSavedViews'
|
|
26
|
+
export type { SavedView as SavedViewEntry } from './useSavedViews'
|
|
27
|
+
export { useRecentlyViewed } from './useRecentlyViewed'
|
|
28
|
+
export { useWizard } from './useWizard'
|
|
29
|
+
export type { WizardState } from './useWizard'
|
|
30
|
+
export { useCSVImport, useCSVExport } from './useCSV'
|
|
31
|
+
export type {
|
|
32
|
+
CSVColumnMapping,
|
|
33
|
+
CSVPreviewResult,
|
|
34
|
+
CSVImportResult,
|
|
35
|
+
CSVImportStep,
|
|
36
|
+
UseCSVImportState,
|
|
37
|
+
} from './useCSV'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
2
|
+
import type { QueryKey, UseMutationOptions, UseMutationResult } from '@tanstack/react-query'
|
|
3
|
+
|
|
4
|
+
interface UseCRUDMutationOptions<TInput, TOutput> {
|
|
5
|
+
invalidateKeys: QueryKey[]
|
|
6
|
+
onSuccess?: (data: TOutput, variables: TInput) => void
|
|
7
|
+
onError?: (error: Error, variables: TInput) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useCRUDMutation<TInput, TOutput>(
|
|
11
|
+
mutationFn: (input: TInput) => Promise<TOutput>,
|
|
12
|
+
opts: UseCRUDMutationOptions<TInput, TOutput>
|
|
13
|
+
): UseMutationResult<TOutput, Error, TInput> {
|
|
14
|
+
const queryClient = useQueryClient()
|
|
15
|
+
|
|
16
|
+
return useMutation({
|
|
17
|
+
mutationFn,
|
|
18
|
+
onSuccess: (data, variables) => {
|
|
19
|
+
for (const key of opts.invalidateKeys) {
|
|
20
|
+
queryClient.invalidateQueries({ queryKey: key })
|
|
21
|
+
}
|
|
22
|
+
opts.onSuccess?.(data, variables)
|
|
23
|
+
},
|
|
24
|
+
onError: opts.onError,
|
|
25
|
+
})
|
|
26
|
+
}
|
package/src/useCSV.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface CSVColumnMapping {
|
|
4
|
+
csv_column: string
|
|
5
|
+
target_field: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CSVPreviewResult<TField extends string = string> {
|
|
9
|
+
columns: string[]
|
|
10
|
+
sample_rows: Record<string, string>[]
|
|
11
|
+
suggested_mappings?: CSVColumnMapping[]
|
|
12
|
+
total_rows?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CSVImportResult {
|
|
16
|
+
total_rows: number
|
|
17
|
+
successful: number
|
|
18
|
+
failed: number
|
|
19
|
+
errors: Array<{ row: number; message: string }>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type CSVImportStep = 'upload' | 'mapping' | 'importing' | 'complete'
|
|
23
|
+
|
|
24
|
+
export interface UseCSVImportOptions<TField extends string = string> {
|
|
25
|
+
previewFn: (file: File) => Promise<CSVPreviewResult<TField>>
|
|
26
|
+
importFn: (file: File, mappings: CSVColumnMapping[]) => Promise<CSVImportResult>
|
|
27
|
+
onSuccess?: (result: CSVImportResult) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UseCSVImportState {
|
|
31
|
+
step: CSVImportStep
|
|
32
|
+
file: File | null
|
|
33
|
+
preview: CSVPreviewResult | null
|
|
34
|
+
mappings: CSVColumnMapping[]
|
|
35
|
+
result: CSVImportResult | null
|
|
36
|
+
isLoading: boolean
|
|
37
|
+
error: string | null
|
|
38
|
+
handleFileSelect: (file: File) => Promise<void>
|
|
39
|
+
updateMapping: (csvColumn: string, targetField: string) => void
|
|
40
|
+
startImport: () => Promise<void>
|
|
41
|
+
reset: () => void
|
|
42
|
+
goBack: () => void
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function useCSVImport<TField extends string = string>({
|
|
46
|
+
previewFn,
|
|
47
|
+
importFn,
|
|
48
|
+
onSuccess,
|
|
49
|
+
}: UseCSVImportOptions<TField>): UseCSVImportState {
|
|
50
|
+
const [step, setStep] = useState<CSVImportStep>('upload')
|
|
51
|
+
const [file, setFile] = useState<File | null>(null)
|
|
52
|
+
const [preview, setPreview] = useState<CSVPreviewResult | null>(null)
|
|
53
|
+
const [mappings, setMappings] = useState<CSVColumnMapping[]>([])
|
|
54
|
+
const [result, setResult] = useState<CSVImportResult | null>(null)
|
|
55
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
56
|
+
const [error, setError] = useState<string | null>(null)
|
|
57
|
+
|
|
58
|
+
const handleFileSelect = useCallback(
|
|
59
|
+
async (selectedFile: File) => {
|
|
60
|
+
setFile(selectedFile)
|
|
61
|
+
setError(null)
|
|
62
|
+
setIsLoading(true)
|
|
63
|
+
try {
|
|
64
|
+
const previewData = await previewFn(selectedFile)
|
|
65
|
+
setPreview(previewData)
|
|
66
|
+
setMappings(previewData.suggested_mappings ?? [])
|
|
67
|
+
setStep('mapping')
|
|
68
|
+
} catch {
|
|
69
|
+
setError('Failed to preview CSV file')
|
|
70
|
+
} finally {
|
|
71
|
+
setIsLoading(false)
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
[previewFn]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const updateMapping = useCallback((csvColumn: string, targetField: string) => {
|
|
78
|
+
setMappings((prev) => {
|
|
79
|
+
const existing = prev.find((m) => m.csv_column === csvColumn)
|
|
80
|
+
if (existing) {
|
|
81
|
+
return prev.map((m) =>
|
|
82
|
+
m.csv_column === csvColumn ? { ...m, target_field: targetField } : m
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
return [...prev, { csv_column: csvColumn, target_field: targetField }]
|
|
86
|
+
})
|
|
87
|
+
}, [])
|
|
88
|
+
|
|
89
|
+
const startImport = useCallback(async () => {
|
|
90
|
+
if (!file) return
|
|
91
|
+
setStep('importing')
|
|
92
|
+
setError(null)
|
|
93
|
+
try {
|
|
94
|
+
const importResult = await importFn(file, mappings)
|
|
95
|
+
setResult(importResult)
|
|
96
|
+
setStep('complete')
|
|
97
|
+
onSuccess?.(importResult)
|
|
98
|
+
} catch {
|
|
99
|
+
setError('Failed to import CSV')
|
|
100
|
+
setStep('mapping')
|
|
101
|
+
}
|
|
102
|
+
}, [file, mappings, importFn, onSuccess])
|
|
103
|
+
|
|
104
|
+
const reset = useCallback(() => {
|
|
105
|
+
setStep('upload')
|
|
106
|
+
setFile(null)
|
|
107
|
+
setPreview(null)
|
|
108
|
+
setMappings([])
|
|
109
|
+
setResult(null)
|
|
110
|
+
setError(null)
|
|
111
|
+
}, [])
|
|
112
|
+
|
|
113
|
+
const goBack = useCallback(() => {
|
|
114
|
+
if (step === 'mapping') setStep('upload')
|
|
115
|
+
}, [step])
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
step,
|
|
119
|
+
file,
|
|
120
|
+
preview,
|
|
121
|
+
mappings,
|
|
122
|
+
result,
|
|
123
|
+
isLoading,
|
|
124
|
+
error,
|
|
125
|
+
handleFileSelect,
|
|
126
|
+
updateMapping,
|
|
127
|
+
startImport,
|
|
128
|
+
reset,
|
|
129
|
+
goBack,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface UseCSVExportOptions {
|
|
134
|
+
exportFn: () => Promise<Blob | string>
|
|
135
|
+
filename?: string
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function useCSVExport({ exportFn, filename = 'export.csv' }: UseCSVExportOptions) {
|
|
139
|
+
const [isExporting, setIsExporting] = useState(false)
|
|
140
|
+
|
|
141
|
+
const exportCSV = useCallback(async () => {
|
|
142
|
+
setIsExporting(true)
|
|
143
|
+
try {
|
|
144
|
+
const data = await exportFn()
|
|
145
|
+
const blob = typeof data === 'string' ? new Blob([data], { type: 'text/csv' }) : data
|
|
146
|
+
const url = URL.createObjectURL(blob)
|
|
147
|
+
const a = document.createElement('a')
|
|
148
|
+
a.href = url
|
|
149
|
+
a.download = filename
|
|
150
|
+
a.click()
|
|
151
|
+
URL.revokeObjectURL(url)
|
|
152
|
+
} finally {
|
|
153
|
+
setIsExporting(false)
|
|
154
|
+
}
|
|
155
|
+
}, [exportFn, filename])
|
|
156
|
+
|
|
157
|
+
return { exportCSV, isExporting }
|
|
158
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
interface StoredItem<T> {
|
|
4
|
+
item: T
|
|
5
|
+
viewedAt: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function useRecentlyViewed<T extends { id: string }>(
|
|
9
|
+
storageKey: string,
|
|
10
|
+
maxItems: number = 5
|
|
11
|
+
) {
|
|
12
|
+
const [items, setItems] = useState<StoredItem<T>[]>([])
|
|
13
|
+
|
|
14
|
+
// Load from localStorage on mount
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (typeof window === 'undefined') return
|
|
17
|
+
try {
|
|
18
|
+
const raw = localStorage.getItem(storageKey)
|
|
19
|
+
if (raw) {
|
|
20
|
+
setItems(JSON.parse(raw) as StoredItem<T>[])
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
// Corrupted storage, start fresh
|
|
24
|
+
}
|
|
25
|
+
}, [storageKey])
|
|
26
|
+
|
|
27
|
+
const persist = useCallback(
|
|
28
|
+
(updated: StoredItem<T>[]) => {
|
|
29
|
+
if (typeof window === 'undefined') return
|
|
30
|
+
try {
|
|
31
|
+
localStorage.setItem(storageKey, JSON.stringify(updated))
|
|
32
|
+
} catch {
|
|
33
|
+
// Storage quota exceeded
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
[storageKey]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const trackView = useCallback(
|
|
40
|
+
(item: T) => {
|
|
41
|
+
setItems(prev => {
|
|
42
|
+
const deduped = prev.filter(entry => entry.item.id !== item.id)
|
|
43
|
+
const updated = [{ item, viewedAt: Date.now() }, ...deduped].slice(0, maxItems)
|
|
44
|
+
persist(updated)
|
|
45
|
+
return updated
|
|
46
|
+
})
|
|
47
|
+
},
|
|
48
|
+
[maxItems, persist]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const clear = useCallback(() => {
|
|
52
|
+
setItems([])
|
|
53
|
+
if (typeof window === 'undefined') return
|
|
54
|
+
try {
|
|
55
|
+
localStorage.removeItem(storageKey)
|
|
56
|
+
} catch {
|
|
57
|
+
// ignore
|
|
58
|
+
}
|
|
59
|
+
}, [storageKey])
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
items: items.map(entry => entry.item),
|
|
63
|
+
timestamps: items.map(entry => ({ id: entry.item.id, viewedAt: entry.viewedAt })),
|
|
64
|
+
trackView,
|
|
65
|
+
clear,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface SavedView {
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
isDefault?: boolean
|
|
7
|
+
createdAt?: string
|
|
8
|
+
[key: string]: unknown
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SavedViewsState<T extends SavedView> {
|
|
12
|
+
views: T[]
|
|
13
|
+
currentViewId: string | null
|
|
14
|
+
loading: boolean
|
|
15
|
+
error: string | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface UseSavedViewsOptions<T extends SavedView> {
|
|
19
|
+
resource: string
|
|
20
|
+
loadFn: (resource: string) => Promise<T[]>
|
|
21
|
+
saveFn: (resource: string, view: Omit<T, 'id' | 'createdAt'>) => Promise<T>
|
|
22
|
+
updateFn: (resource: string, viewId: string, updates: Partial<T>) => Promise<T>
|
|
23
|
+
deleteFn: (resource: string, viewId: string) => Promise<void>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useSavedViews<T extends SavedView>({
|
|
27
|
+
resource,
|
|
28
|
+
loadFn,
|
|
29
|
+
saveFn,
|
|
30
|
+
updateFn,
|
|
31
|
+
deleteFn,
|
|
32
|
+
}: UseSavedViewsOptions<T>) {
|
|
33
|
+
const [state, setState] = useState<SavedViewsState<T>>({
|
|
34
|
+
views: [],
|
|
35
|
+
currentViewId: null,
|
|
36
|
+
loading: true,
|
|
37
|
+
error: null,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const fetchViews = useCallback(async () => {
|
|
41
|
+
setState(prev => ({ ...prev, loading: true, error: null }))
|
|
42
|
+
try {
|
|
43
|
+
const views = await loadFn(resource)
|
|
44
|
+
const defaultView = views.find(v => v.isDefault)
|
|
45
|
+
setState(prev => ({
|
|
46
|
+
...prev,
|
|
47
|
+
views,
|
|
48
|
+
currentViewId: defaultView?.id || null,
|
|
49
|
+
loading: false,
|
|
50
|
+
}))
|
|
51
|
+
} catch (error) {
|
|
52
|
+
setState(prev => ({
|
|
53
|
+
...prev,
|
|
54
|
+
loading: false,
|
|
55
|
+
error: error instanceof Error ? error.message : 'Failed to load views',
|
|
56
|
+
}))
|
|
57
|
+
}
|
|
58
|
+
}, [resource, loadFn])
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
fetchViews()
|
|
62
|
+
}, [fetchViews])
|
|
63
|
+
|
|
64
|
+
const saveView = useCallback(
|
|
65
|
+
async (viewData: Omit<T, 'id' | 'createdAt'>): Promise<T> => {
|
|
66
|
+
const newView = await saveFn(resource, viewData)
|
|
67
|
+
setState(prev => ({
|
|
68
|
+
...prev,
|
|
69
|
+
views: [...prev.views, newView],
|
|
70
|
+
currentViewId: newView.id,
|
|
71
|
+
}))
|
|
72
|
+
return newView
|
|
73
|
+
},
|
|
74
|
+
[resource, saveFn]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const updateView = useCallback(
|
|
78
|
+
async (viewId: string, updates: Partial<T>): Promise<void> => {
|
|
79
|
+
await updateFn(resource, viewId, updates)
|
|
80
|
+
await fetchViews()
|
|
81
|
+
},
|
|
82
|
+
[resource, updateFn, fetchViews]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
const deleteView = useCallback(
|
|
86
|
+
async (viewId: string): Promise<void> => {
|
|
87
|
+
await deleteFn(resource, viewId)
|
|
88
|
+
setState(prev => ({
|
|
89
|
+
...prev,
|
|
90
|
+
views: prev.views.filter(v => v.id !== viewId),
|
|
91
|
+
currentViewId: prev.currentViewId === viewId ? null : prev.currentViewId,
|
|
92
|
+
}))
|
|
93
|
+
},
|
|
94
|
+
[resource, deleteFn]
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const loadView = useCallback((viewId: string) => {
|
|
98
|
+
setState(prev => ({ ...prev, currentViewId: viewId }))
|
|
99
|
+
}, [])
|
|
100
|
+
|
|
101
|
+
const getCurrentView = useCallback((): T | null => {
|
|
102
|
+
return state.views.find(v => v.id === state.currentViewId) || null
|
|
103
|
+
}, [state.views, state.currentViewId])
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
views: state.views,
|
|
107
|
+
currentViewId: state.currentViewId,
|
|
108
|
+
loading: state.loading,
|
|
109
|
+
error: state.error,
|
|
110
|
+
saveView,
|
|
111
|
+
updateView,
|
|
112
|
+
deleteView,
|
|
113
|
+
loadView,
|
|
114
|
+
getCurrentView,
|
|
115
|
+
refreshViews: fetchViews,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
interface TableFilterBase {
|
|
4
|
+
page: number
|
|
5
|
+
pageSize: number
|
|
6
|
+
search?: string
|
|
7
|
+
sortField?: string
|
|
8
|
+
sortDirection?: 'asc' | 'desc'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type TableFilters<TFilters extends Record<string, unknown>> = TFilters & TableFilterBase
|
|
12
|
+
|
|
13
|
+
interface UseTableFiltersReturn<TFilters extends Record<string, unknown>> {
|
|
14
|
+
filters: TableFilters<TFilters>
|
|
15
|
+
setFilter: <K extends keyof TFilters>(key: K, value: TFilters[K]) => void
|
|
16
|
+
setPage: (page: number) => void
|
|
17
|
+
setPageSize: (pageSize: number) => void
|
|
18
|
+
setSearch: (search: string) => void
|
|
19
|
+
setSort: (field: string, direction: 'asc' | 'desc') => void
|
|
20
|
+
resetFilters: () => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useTableFilters<TFilters extends Record<string, unknown>>(
|
|
24
|
+
initial: TFilters & { page?: number; pageSize?: number; sortField?: string; sortDirection?: 'asc' | 'desc' }
|
|
25
|
+
): UseTableFiltersReturn<TFilters> {
|
|
26
|
+
const defaultFilters: TableFilters<TFilters> = {
|
|
27
|
+
page: 1,
|
|
28
|
+
pageSize: 20,
|
|
29
|
+
...initial,
|
|
30
|
+
} as TableFilters<TFilters>
|
|
31
|
+
|
|
32
|
+
const [filters, setFilters] = useState<TableFilters<TFilters>>(defaultFilters)
|
|
33
|
+
|
|
34
|
+
const setFilter = useCallback(<K extends keyof TFilters>(key: K, value: TFilters[K]) => {
|
|
35
|
+
setFilters(prev => ({ ...prev, [key]: value, page: 1 }))
|
|
36
|
+
}, [])
|
|
37
|
+
|
|
38
|
+
const setPage = useCallback((page: number) => {
|
|
39
|
+
setFilters(prev => ({ ...prev, page }))
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
42
|
+
const setPageSize = useCallback((pageSize: number) => {
|
|
43
|
+
setFilters(prev => ({ ...prev, pageSize, page: 1 }))
|
|
44
|
+
}, [])
|
|
45
|
+
|
|
46
|
+
const setSearch = useCallback((search: string) => {
|
|
47
|
+
setFilters(prev => ({ ...prev, search, page: 1 }))
|
|
48
|
+
}, [])
|
|
49
|
+
|
|
50
|
+
const setSort = useCallback((field: string, direction: 'asc' | 'desc') => {
|
|
51
|
+
setFilters(prev => ({ ...prev, sortField: field, sortDirection: direction }))
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
const resetFilters = useCallback(() => {
|
|
55
|
+
setFilters(defaultFilters)
|
|
56
|
+
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
filters,
|
|
60
|
+
setFilter,
|
|
61
|
+
setPage,
|
|
62
|
+
setPageSize,
|
|
63
|
+
setSearch,
|
|
64
|
+
setSort,
|
|
65
|
+
resetFilters,
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/useWizard.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface WizardState<TStep extends string> {
|
|
4
|
+
currentStep: TStep
|
|
5
|
+
stepIndex: number
|
|
6
|
+
totalSteps: number
|
|
7
|
+
isFirstStep: boolean
|
|
8
|
+
isLastStep: boolean
|
|
9
|
+
canGoBack: boolean
|
|
10
|
+
canGoNext: boolean
|
|
11
|
+
goTo: (step: TStep) => void
|
|
12
|
+
next: () => void
|
|
13
|
+
prev: () => void
|
|
14
|
+
reset: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useWizard<TStep extends string>(
|
|
18
|
+
steps: readonly TStep[],
|
|
19
|
+
initialStep?: TStep
|
|
20
|
+
): WizardState<TStep> {
|
|
21
|
+
const [currentStep, setCurrentStep] = useState<TStep>(initialStep ?? steps[0])
|
|
22
|
+
|
|
23
|
+
const stepIndex = steps.indexOf(currentStep)
|
|
24
|
+
const totalSteps = steps.length
|
|
25
|
+
const isFirstStep = stepIndex === 0
|
|
26
|
+
const isLastStep = stepIndex === totalSteps - 1
|
|
27
|
+
|
|
28
|
+
const goTo = useCallback(
|
|
29
|
+
(step: TStep) => {
|
|
30
|
+
if (steps.includes(step)) setCurrentStep(step)
|
|
31
|
+
},
|
|
32
|
+
[steps]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const next = useCallback(() => {
|
|
36
|
+
if (!isLastStep) setCurrentStep(steps[stepIndex + 1])
|
|
37
|
+
}, [isLastStep, stepIndex, steps])
|
|
38
|
+
|
|
39
|
+
const prev = useCallback(() => {
|
|
40
|
+
if (!isFirstStep) setCurrentStep(steps[stepIndex - 1])
|
|
41
|
+
}, [isFirstStep, stepIndex, steps])
|
|
42
|
+
|
|
43
|
+
const reset = useCallback(() => {
|
|
44
|
+
setCurrentStep(initialStep ?? steps[0])
|
|
45
|
+
}, [initialStep, steps])
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
currentStep,
|
|
49
|
+
stepIndex,
|
|
50
|
+
totalSteps,
|
|
51
|
+
isFirstStep,
|
|
52
|
+
isLastStep,
|
|
53
|
+
canGoBack: !isFirstStep,
|
|
54
|
+
canGoNext: !isLastStep,
|
|
55
|
+
goTo,
|
|
56
|
+
next,
|
|
57
|
+
prev,
|
|
58
|
+
reset,
|
|
59
|
+
}
|
|
60
|
+
}
|