@startsimpli/hooks 0.4.5 → 0.4.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startsimpli/hooks",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Shared React hooks for StartSimpli apps",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -24,14 +24,19 @@
24
24
  },
25
25
  "peerDependencies": {
26
26
  "react": ">=18.0.0",
27
- "@tanstack/react-query": ">=5.0.0"
27
+ "@tanstack/react-query": ">=5.0.0",
28
+ "@startsimpli/api": "workspace:*"
28
29
  },
29
30
  "peerDependenciesMeta": {
30
31
  "@tanstack/react-query": {
31
32
  "optional": true
33
+ },
34
+ "@startsimpli/api": {
35
+ "optional": true
32
36
  }
33
37
  },
34
38
  "devDependencies": {
39
+ "@startsimpli/api": "workspace:*",
35
40
  "@tanstack/react-query": "^5.0.0",
36
41
  "@testing-library/react": "^14.0.0",
37
42
  "@types/node": "^20.11.0",
package/src/index.ts CHANGED
@@ -22,12 +22,39 @@ export {
22
22
  getFilterDescription,
23
23
  } from './filter-encoding'
24
24
  export { useCRUDMutation } from './useCRUDMutation'
25
+ export {
26
+ useMessages,
27
+ useMessage,
28
+ useCreateMessage,
29
+ useSendMessage,
30
+ useScheduleMessage,
31
+ useSendTestMessage,
32
+ useMessageChannels,
33
+ } from './useMessages'
34
+ export {
35
+ useMessageTemplates,
36
+ useMessageTemplate,
37
+ useCreateMessageTemplate,
38
+ useUpdateMessageTemplate,
39
+ useDeleteMessageTemplate,
40
+ } from './useMessageTemplates'
25
41
  export { useSavedViews } from './useSavedViews'
26
42
  export type { SavedView as SavedViewEntry } from './useSavedViews'
27
43
  export { useRecentlyViewed } from './useRecentlyViewed'
28
44
  export { useWizard } from './useWizard'
29
45
  export type { WizardState } from './useWizard'
30
46
  export { useCSVImport, useCSVExport } from './useCSV'
47
+ export { useTargetListDetail, useTargetListMutations, TARGET_LIST_KEYS } from './useTargetLists'
48
+ export type { TargetListApiFns, UseTargetListMutationsOptions } from './useTargetLists'
49
+ export { useEnrichment, useBatchEnrichment, useQueueStatus } from './useEnrichment'
50
+ export type {
51
+ UseEnrichmentState,
52
+ UseEnrichmentOptions,
53
+ UseBatchEnrichmentState,
54
+ UseBatchEnrichmentOptions,
55
+ UseQueueStatusState,
56
+ UseQueueStatusOptions,
57
+ } from './useEnrichment'
31
58
  export type {
32
59
  CSVColumnMapping,
33
60
  CSVPreviewResult,
package/src/useCSV.ts CHANGED
@@ -1,19 +1,19 @@
1
1
  import { useState, useCallback } from 'react'
2
2
 
3
3
  export interface CSVColumnMapping {
4
- csv_column: string
5
- target_field: string
4
+ csvColumn: string
5
+ targetField: string
6
6
  }
7
7
 
8
8
  export interface CSVPreviewResult<TField extends string = string> {
9
9
  columns: string[]
10
- sample_rows: Record<string, string>[]
11
- suggested_mappings?: CSVColumnMapping[]
12
- total_rows?: number
10
+ sampleRows: Record<string, string>[]
11
+ suggestedMappings?: CSVColumnMapping[]
12
+ totalRows?: number
13
13
  }
14
14
 
15
15
  export interface CSVImportResult {
16
- total_rows: number
16
+ totalRows: number
17
17
  successful: number
18
18
  failed: number
19
19
  errors: Array<{ row: number; message: string }>
@@ -63,7 +63,7 @@ export function useCSVImport<TField extends string = string>({
63
63
  try {
64
64
  const previewData = await previewFn(selectedFile)
65
65
  setPreview(previewData)
66
- setMappings(previewData.suggested_mappings ?? [])
66
+ setMappings(previewData.suggestedMappings ?? [])
67
67
  setStep('mapping')
68
68
  } catch {
69
69
  setError('Failed to preview CSV file')
@@ -76,13 +76,13 @@ export function useCSVImport<TField extends string = string>({
76
76
 
77
77
  const updateMapping = useCallback((csvColumn: string, targetField: string) => {
78
78
  setMappings((prev) => {
79
- const existing = prev.find((m) => m.csv_column === csvColumn)
79
+ const existing = prev.find((m) => m.csvColumn === csvColumn)
80
80
  if (existing) {
81
81
  return prev.map((m) =>
82
- m.csv_column === csvColumn ? { ...m, target_field: targetField } : m
82
+ m.csvColumn === csvColumn ? { ...m, targetField: targetField } : m
83
83
  )
84
84
  }
85
- return [...prev, { csv_column: csvColumn, target_field: targetField }]
85
+ return [...prev, { csvColumn: csvColumn, targetField: targetField }]
86
86
  })
87
87
  }, [])
88
88
 
@@ -0,0 +1,156 @@
1
+ import { useState, useCallback, useRef } from 'react'
2
+ import type { EnrichmentResult, QueueStatus } from '@startsimpli/api'
3
+
4
+ export interface EnrichFn {
5
+ (contactId: string): Promise<EnrichmentResult>
6
+ }
7
+
8
+ export interface BulkEnrichFn {
9
+ (contactIds: string[], onProgress?: (completed: number, total: number) => void): Promise<EnrichmentResult[]>
10
+ }
11
+
12
+ export interface QueueStatusFn {
13
+ (): Promise<QueueStatus>
14
+ }
15
+
16
+ export interface UseEnrichmentOptions {
17
+ enrichFn: EnrichFn
18
+ onSuccess?: (result: EnrichmentResult) => void
19
+ }
20
+
21
+ export interface UseEnrichmentState {
22
+ isEnriching: boolean
23
+ result: EnrichmentResult | null
24
+ error: string | null
25
+ enrich: (contactId: string) => Promise<void>
26
+ reset: () => void
27
+ }
28
+
29
+ export function useEnrichment({ enrichFn, onSuccess }: UseEnrichmentOptions): UseEnrichmentState {
30
+ const [isEnriching, setIsEnriching] = useState(false)
31
+ const [result, setResult] = useState<EnrichmentResult | null>(null)
32
+ const [error, setError] = useState<string | null>(null)
33
+
34
+ const enrich = useCallback(
35
+ async (contactId: string) => {
36
+ setIsEnriching(true)
37
+ setError(null)
38
+ try {
39
+ const enrichResult = await enrichFn(contactId)
40
+ setResult(enrichResult)
41
+ onSuccess?.(enrichResult)
42
+ } catch (err: unknown) {
43
+ setError(err instanceof Error ? err.message : 'Enrichment failed')
44
+ } finally {
45
+ setIsEnriching(false)
46
+ }
47
+ },
48
+ [enrichFn, onSuccess]
49
+ )
50
+
51
+ const reset = useCallback(() => {
52
+ setResult(null)
53
+ setError(null)
54
+ }, [])
55
+
56
+ return { isEnriching, result, error, enrich, reset }
57
+ }
58
+
59
+ export interface UseBatchEnrichmentOptions {
60
+ bulkEnrichFn: BulkEnrichFn
61
+ onComplete?: (results: EnrichmentResult[]) => void
62
+ }
63
+
64
+ export interface UseBatchEnrichmentState {
65
+ isEnriching: boolean
66
+ progress: { completed: number; total: number }
67
+ results: EnrichmentResult[]
68
+ error: string | null
69
+ enrichBatch: (contactIds: string[]) => Promise<void>
70
+ cancel: () => void
71
+ reset: () => void
72
+ }
73
+
74
+ export function useBatchEnrichment({
75
+ bulkEnrichFn,
76
+ onComplete,
77
+ }: UseBatchEnrichmentOptions): UseBatchEnrichmentState {
78
+ const [isEnriching, setIsEnriching] = useState(false)
79
+ const [progress, setProgress] = useState({ completed: 0, total: 0 })
80
+ const [results, setResults] = useState<EnrichmentResult[]>([])
81
+ const [error, setError] = useState<string | null>(null)
82
+ const cancelledRef = useRef(false)
83
+
84
+ const enrichBatch = useCallback(
85
+ async (contactIds: string[]) => {
86
+ cancelledRef.current = false
87
+ setIsEnriching(true)
88
+ setError(null)
89
+ setProgress({ completed: 0, total: contactIds.length })
90
+ setResults([])
91
+
92
+ try {
93
+ const batchResults = await bulkEnrichFn(contactIds, (completed, total) => {
94
+ if (!cancelledRef.current) {
95
+ setProgress({ completed, total })
96
+ }
97
+ })
98
+ if (!cancelledRef.current) {
99
+ setResults(batchResults)
100
+ onComplete?.(batchResults)
101
+ }
102
+ } catch (err: unknown) {
103
+ if (!cancelledRef.current) {
104
+ setError(err instanceof Error ? err.message : 'Batch enrichment failed')
105
+ }
106
+ } finally {
107
+ if (!cancelledRef.current) {
108
+ setIsEnriching(false)
109
+ }
110
+ }
111
+ },
112
+ [bulkEnrichFn, onComplete]
113
+ )
114
+
115
+ const cancel = useCallback(() => {
116
+ cancelledRef.current = true
117
+ setIsEnriching(false)
118
+ }, [])
119
+
120
+ const reset = useCallback(() => {
121
+ setResults([])
122
+ setError(null)
123
+ setProgress({ completed: 0, total: 0 })
124
+ }, [])
125
+
126
+ return { isEnriching, progress, results, error, enrichBatch, cancel, reset }
127
+ }
128
+
129
+ export interface UseQueueStatusOptions {
130
+ fetchQueueStatus: QueueStatusFn
131
+ }
132
+
133
+ export interface UseQueueStatusState {
134
+ queueStatus: QueueStatus | null
135
+ isLoading: boolean
136
+ refresh: () => Promise<void>
137
+ }
138
+
139
+ export function useQueueStatus({ fetchQueueStatus }: UseQueueStatusOptions): UseQueueStatusState {
140
+ const [queueStatus, setQueueStatus] = useState<QueueStatus | null>(null)
141
+ const [isLoading, setIsLoading] = useState(false)
142
+
143
+ const refresh = useCallback(async () => {
144
+ setIsLoading(true)
145
+ try {
146
+ const status = await fetchQueueStatus()
147
+ setQueueStatus(status)
148
+ } catch {
149
+ // Queue status may not be available
150
+ } finally {
151
+ setIsLoading(false)
152
+ }
153
+ }, [fetchQueueStatus])
154
+
155
+ return { queueStatus, isLoading, refresh }
156
+ }
@@ -0,0 +1,67 @@
1
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
2
+ import type { MessageTemplatesApi, MessageTemplateFilters, CreateMessageTemplateInput } from '@startsimpli/api'
3
+
4
+ /**
5
+ * List message templates with optional filters (paginated)
6
+ */
7
+ export function useMessageTemplates(api: MessageTemplatesApi, filters?: MessageTemplateFilters) {
8
+ return useQuery({
9
+ queryKey: ['messageTemplates', filters],
10
+ queryFn: () => api.list(filters),
11
+ })
12
+ }
13
+
14
+ /**
15
+ * Get a single message template by ID
16
+ */
17
+ export function useMessageTemplate(api: MessageTemplatesApi, id: string) {
18
+ return useQuery({
19
+ queryKey: ['messageTemplates', id],
20
+ queryFn: () => api.get(id),
21
+ enabled: !!id,
22
+ })
23
+ }
24
+
25
+ /**
26
+ * Create a new message template
27
+ */
28
+ export function useCreateMessageTemplate(api: MessageTemplatesApi) {
29
+ const queryClient = useQueryClient()
30
+
31
+ return useMutation({
32
+ mutationFn: (data: CreateMessageTemplateInput) => api.create(data),
33
+ onSuccess: () => {
34
+ queryClient.invalidateQueries({ queryKey: ['messageTemplates'] })
35
+ },
36
+ })
37
+ }
38
+
39
+ /**
40
+ * Update an existing message template
41
+ */
42
+ export function useUpdateMessageTemplate(api: MessageTemplatesApi) {
43
+ const queryClient = useQueryClient()
44
+
45
+ return useMutation({
46
+ mutationFn: ({ id, data }: { id: string; data: Partial<CreateMessageTemplateInput> }) =>
47
+ api.update(id, data),
48
+ onSuccess: (_, variables) => {
49
+ queryClient.invalidateQueries({ queryKey: ['messageTemplates'] })
50
+ queryClient.invalidateQueries({ queryKey: ['messageTemplates', variables.id] })
51
+ },
52
+ })
53
+ }
54
+
55
+ /**
56
+ * Delete a message template
57
+ */
58
+ export function useDeleteMessageTemplate(api: MessageTemplatesApi) {
59
+ const queryClient = useQueryClient()
60
+
61
+ return useMutation({
62
+ mutationFn: (id: string) => api.delete(id),
63
+ onSuccess: () => {
64
+ queryClient.invalidateQueries({ queryKey: ['messageTemplates'] })
65
+ },
66
+ })
67
+ }
@@ -0,0 +1,86 @@
1
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
2
+ import type { MessagesApi, MessageFilters, CreateMessageInput, ScheduleMessageInput, SendTestInput } from '@startsimpli/api'
3
+
4
+ /**
5
+ * List messages with optional filters (paginated)
6
+ */
7
+ export function useMessages(api: MessagesApi, filters?: MessageFilters) {
8
+ return useQuery({
9
+ queryKey: ['messages', filters],
10
+ queryFn: () => api.list(filters),
11
+ })
12
+ }
13
+
14
+ /**
15
+ * Get a single message by ID
16
+ */
17
+ export function useMessage(api: MessagesApi, id: string) {
18
+ return useQuery({
19
+ queryKey: ['messages', id],
20
+ queryFn: () => api.get(id),
21
+ enabled: !!id,
22
+ })
23
+ }
24
+
25
+ /**
26
+ * Create a new message (draft)
27
+ */
28
+ export function useCreateMessage(api: MessagesApi) {
29
+ const queryClient = useQueryClient()
30
+
31
+ return useMutation({
32
+ mutationFn: (data: CreateMessageInput) => api.create(data),
33
+ onSuccess: () => {
34
+ queryClient.invalidateQueries({ queryKey: ['messages'] })
35
+ },
36
+ })
37
+ }
38
+
39
+ /**
40
+ * Send a message immediately
41
+ */
42
+ export function useSendMessage(api: MessagesApi, id: string) {
43
+ const queryClient = useQueryClient()
44
+
45
+ return useMutation({
46
+ mutationFn: () => api.sendNow(id),
47
+ onSuccess: () => {
48
+ queryClient.invalidateQueries({ queryKey: ['messages'] })
49
+ queryClient.invalidateQueries({ queryKey: ['messages', id] })
50
+ },
51
+ })
52
+ }
53
+
54
+ /**
55
+ * Schedule a message for future sending
56
+ */
57
+ export function useScheduleMessage(api: MessagesApi, id: string) {
58
+ const queryClient = useQueryClient()
59
+
60
+ return useMutation({
61
+ mutationFn: (input: ScheduleMessageInput) => api.schedule(id, input),
62
+ onSuccess: () => {
63
+ queryClient.invalidateQueries({ queryKey: ['messages'] })
64
+ queryClient.invalidateQueries({ queryKey: ['messages', id] })
65
+ },
66
+ })
67
+ }
68
+
69
+ /**
70
+ * Send a test message
71
+ */
72
+ export function useSendTestMessage(api: MessagesApi, id: string) {
73
+ return useMutation({
74
+ mutationFn: (input?: SendTestInput) => api.sendTest(id, input),
75
+ })
76
+ }
77
+
78
+ /**
79
+ * List available messaging channels
80
+ */
81
+ export function useMessageChannels(api: MessagesApi) {
82
+ return useQuery({
83
+ queryKey: ['messages', 'channels'],
84
+ queryFn: () => api.getChannels(),
85
+ })
86
+ }
@@ -0,0 +1,132 @@
1
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
2
+ import type {
3
+ TargetList,
4
+ TargetListMember,
5
+ CreateTargetListInput,
6
+ UpdateTargetListInput,
7
+ TargetListFilters,
8
+ TargetListMemberFilters,
9
+ AddMembersResult,
10
+ RemoveMembersResult,
11
+ RefreshResult,
12
+ PaginatedResponse,
13
+ } from '@startsimpli/api'
14
+
15
+ export const TARGET_LIST_KEYS = {
16
+ all: ['target-lists'] as const,
17
+ lists: (filters?: TargetListFilters) => [...TARGET_LIST_KEYS.all, filters] as const,
18
+ detail: (id: string) => ['target-list', id] as const,
19
+ members: (listId: string, filters?: TargetListMemberFilters) =>
20
+ ['list-members', listId, filters] as const,
21
+ }
22
+
23
+ export interface TargetListApiFns {
24
+ list: (filters?: TargetListFilters) => Promise<PaginatedResponse<TargetList>>
25
+ get: (id: string) => Promise<TargetList>
26
+ create: (data: CreateTargetListInput) => Promise<TargetList>
27
+ update: (id: string, data: UpdateTargetListInput) => Promise<TargetList>
28
+ delete: (id: string) => Promise<void>
29
+ getMembers: (listId: string, filters?: TargetListMemberFilters) => Promise<PaginatedResponse<TargetListMember>>
30
+ addMembers: (listId: string, contactIds: string[]) => Promise<AddMembersResult>
31
+ removeMembers: (listId: string, contactIds: string[]) => Promise<RemoveMembersResult>
32
+ refresh: (listId: string) => Promise<RefreshResult>
33
+ }
34
+
35
+ export function useTargetListDetail(
36
+ id: string,
37
+ apiFns: Pick<TargetListApiFns, 'get'>
38
+ ) {
39
+ return useQuery({
40
+ queryKey: TARGET_LIST_KEYS.detail(id),
41
+ queryFn: () => apiFns.get(id),
42
+ enabled: !!id,
43
+ })
44
+ }
45
+
46
+ export interface UseTargetListMutationsOptions {
47
+ apiFns: Pick<TargetListApiFns, 'create' | 'update' | 'delete' | 'addMembers' | 'removeMembers' | 'refresh'>
48
+ onSuccess?: (action: string) => void
49
+ onError?: (action: string) => void
50
+ }
51
+
52
+ export function useTargetListMutations({
53
+ apiFns,
54
+ onSuccess,
55
+ onError,
56
+ }: UseTargetListMutationsOptions) {
57
+ const queryClient = useQueryClient()
58
+
59
+ const invalidateList = (listId?: string) => {
60
+ queryClient.invalidateQueries({ queryKey: TARGET_LIST_KEYS.all })
61
+ if (listId) {
62
+ queryClient.invalidateQueries({ queryKey: TARGET_LIST_KEYS.detail(listId) })
63
+ queryClient.invalidateQueries({ queryKey: ['list-members', listId] })
64
+ }
65
+ }
66
+
67
+ const createList = useMutation({
68
+ mutationFn: (data: CreateTargetListInput) => apiFns.create(data),
69
+ onSuccess: () => {
70
+ invalidateList()
71
+ onSuccess?.('List created')
72
+ },
73
+ onError: () => onError?.('Failed to create list'),
74
+ })
75
+
76
+ const updateList = useMutation({
77
+ mutationFn: ({ id, data }: { id: string; data: UpdateTargetListInput }) =>
78
+ apiFns.update(id, data),
79
+ onSuccess: (_, variables) => {
80
+ invalidateList(variables.id)
81
+ onSuccess?.('List updated')
82
+ },
83
+ onError: () => onError?.('Failed to update list'),
84
+ })
85
+
86
+ const deleteList = useMutation({
87
+ mutationFn: (id: string) => apiFns.delete(id),
88
+ onSuccess: () => {
89
+ invalidateList()
90
+ onSuccess?.('List deleted')
91
+ },
92
+ onError: () => onError?.('Failed to delete list'),
93
+ })
94
+
95
+ const addMembersMutation = useMutation({
96
+ mutationFn: ({ listId, contactIds }: { listId: string; contactIds: string[] }) =>
97
+ apiFns.addMembers(listId, contactIds),
98
+ onSuccess: (_, variables) => {
99
+ invalidateList(variables.listId)
100
+ onSuccess?.('Members added')
101
+ },
102
+ onError: () => onError?.('Failed to add members'),
103
+ })
104
+
105
+ const removeMembersMutation = useMutation({
106
+ mutationFn: ({ listId, contactIds }: { listId: string; contactIds: string[] }) =>
107
+ apiFns.removeMembers(listId, contactIds),
108
+ onSuccess: (_, variables) => {
109
+ invalidateList(variables.listId)
110
+ onSuccess?.('Members removed')
111
+ },
112
+ onError: () => onError?.('Failed to remove members'),
113
+ })
114
+
115
+ const refreshList = useMutation({
116
+ mutationFn: (listId: string) => apiFns.refresh(listId),
117
+ onSuccess: (_, listId) => {
118
+ invalidateList(listId)
119
+ onSuccess?.('List refreshed')
120
+ },
121
+ onError: () => onError?.('Failed to refresh list'),
122
+ })
123
+
124
+ return {
125
+ createList,
126
+ updateList,
127
+ deleteList,
128
+ addMembers: addMembersMutation,
129
+ removeMembers: removeMembersMutation,
130
+ refreshList,
131
+ }
132
+ }