@blastlabs/utils 1.11.0 → 1.12.0

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.
Files changed (129) hide show
  1. package/dist/components/dev/ApiLogger.d.ts +1 -1
  2. package/dist/components/dev/ApiLogger.js +2 -2
  3. package/dist/components/dev/DevPanel.d.ts +1 -1
  4. package/dist/components/dev/DevPanel.js +2 -2
  5. package/dist/components/dev/FormDevTools/FormDevTools.d.ts +1 -1
  6. package/dist/components/dev/FormDevTools/FormDevTools.d.ts.map +1 -1
  7. package/dist/components/dev/FormDevTools/FormDevTools.js +11 -11
  8. package/dist/components/dev/FormDevTools/index.d.ts +1 -1
  9. package/dist/components/dev/FormDevTools/index.d.ts.map +1 -1
  10. package/dist/components/dev/{IdSelector.d.ts → IdSelector/IdSelector.d.ts} +3 -4
  11. package/dist/components/dev/IdSelector/IdSelector.d.ts.map +1 -0
  12. package/dist/components/dev/IdSelector/IdSelector.js +60 -0
  13. package/dist/components/dev/IdSelector/IdSelector.test.d.ts +2 -0
  14. package/dist/components/dev/IdSelector/IdSelector.test.d.ts.map +1 -0
  15. package/dist/components/dev/IdSelector/IdSelector.test.js +203 -0
  16. package/dist/components/dev/IdSelector/LoginCard.d.ts +17 -0
  17. package/dist/components/dev/IdSelector/LoginCard.d.ts.map +1 -0
  18. package/dist/components/dev/IdSelector/LoginCard.js +16 -0
  19. package/dist/components/dev/IdSelector/index.d.ts +3 -0
  20. package/dist/components/dev/IdSelector/index.d.ts.map +1 -0
  21. package/dist/components/dev/IdSelector/index.js +1 -0
  22. package/dist/components/dev/IdSelector/styles.d.ts +16 -0
  23. package/dist/components/dev/IdSelector/styles.d.ts.map +1 -0
  24. package/dist/components/dev/IdSelector/styles.js +66 -0
  25. package/dist/components/dev/WindowSizeDisplay.d.ts +1 -1
  26. package/dist/components/dev/WindowSizeDisplay.js +2 -2
  27. package/dist/components/dev/ZIndexDebugger.d.ts +1 -1
  28. package/dist/components/dev/ZIndexDebugger.js +1 -1
  29. package/dist/components/dev/index.d.ts +2 -1
  30. package/dist/components/dev/index.d.ts.map +1 -1
  31. package/dist/hooks/event/index.d.ts +6 -0
  32. package/dist/hooks/event/index.d.ts.map +1 -0
  33. package/dist/hooks/event/index.js +5 -0
  34. package/dist/hooks/event/useClickOutside.d.ts.map +1 -0
  35. package/dist/hooks/event/useEventListener.d.ts.map +1 -0
  36. package/dist/hooks/form/__tests__/useCRUDForm.test.d.ts +2 -0
  37. package/dist/hooks/form/__tests__/useCRUDForm.test.d.ts.map +1 -0
  38. package/dist/hooks/form/__tests__/useCRUDForm.test.js +487 -0
  39. package/dist/hooks/form/index.d.ts +5 -0
  40. package/dist/hooks/form/index.d.ts.map +1 -0
  41. package/dist/hooks/form/index.js +4 -0
  42. package/dist/hooks/form/useCRUDForm.d.ts +232 -0
  43. package/dist/hooks/form/useCRUDForm.d.ts.map +1 -0
  44. package/dist/hooks/form/useCRUDForm.js +287 -0
  45. package/dist/hooks/index.d.ts +9 -14
  46. package/dist/hooks/index.d.ts.map +1 -1
  47. package/dist/hooks/index.js +16 -19
  48. package/dist/hooks/performance/index.d.ts +7 -0
  49. package/dist/hooks/performance/index.d.ts.map +1 -0
  50. package/dist/hooks/performance/index.js +6 -0
  51. package/dist/hooks/performance/useDebounce.d.ts.map +1 -0
  52. package/dist/hooks/performance/useIntersectionObserver.d.ts.map +1 -0
  53. package/dist/hooks/performance/useThrottle.d.ts.map +1 -0
  54. package/dist/hooks/state/index.d.ts +6 -0
  55. package/dist/hooks/state/index.d.ts.map +1 -0
  56. package/dist/hooks/state/index.js +5 -0
  57. package/dist/hooks/state/usePrevious.d.ts.map +1 -0
  58. package/dist/hooks/state/useToggle.d.ts.map +1 -0
  59. package/dist/hooks/storage/index.d.ts +7 -0
  60. package/dist/hooks/storage/index.d.ts.map +1 -0
  61. package/dist/hooks/storage/index.js +6 -0
  62. package/dist/hooks/storage/useCopyToClipboard.d.ts.map +1 -0
  63. package/dist/hooks/storage/useLocalStorage.d.ts.map +1 -0
  64. package/dist/hooks/storage/useSessionStorage.d.ts.map +1 -0
  65. package/dist/hooks/time/__tests__/useCountdown.test.d.ts +2 -0
  66. package/dist/hooks/time/__tests__/useCountdown.test.d.ts.map +1 -0
  67. package/dist/hooks/time/__tests__/useCountdown.test.js +150 -0
  68. package/dist/hooks/time/__tests__/useInterval.test.d.ts +2 -0
  69. package/dist/hooks/time/__tests__/useInterval.test.d.ts.map +1 -0
  70. package/dist/hooks/time/__tests__/useInterval.test.js +39 -0
  71. package/dist/hooks/time/__tests__/useStopwatch.test.d.ts +2 -0
  72. package/dist/hooks/time/__tests__/useStopwatch.test.d.ts.map +1 -0
  73. package/dist/hooks/time/__tests__/useStopwatch.test.js +149 -0
  74. package/dist/hooks/time/index.d.ts +7 -0
  75. package/dist/hooks/time/index.d.ts.map +1 -0
  76. package/dist/hooks/time/index.js +6 -0
  77. package/dist/hooks/time/useCountdown.d.ts +116 -0
  78. package/dist/hooks/time/useCountdown.d.ts.map +1 -0
  79. package/dist/hooks/time/useCountdown.js +152 -0
  80. package/dist/hooks/time/useInterval.d.ts +40 -0
  81. package/dist/hooks/time/useInterval.d.ts.map +1 -0
  82. package/dist/hooks/time/useInterval.js +61 -0
  83. package/dist/hooks/time/useStopwatch.d.ts +142 -0
  84. package/dist/hooks/time/useStopwatch.d.ts.map +1 -0
  85. package/dist/hooks/time/useStopwatch.js +179 -0
  86. package/dist/hooks/ui/index.d.ts +6 -0
  87. package/dist/hooks/ui/index.d.ts.map +1 -0
  88. package/dist/hooks/ui/index.js +5 -0
  89. package/dist/hooks/ui/useMediaQuery.d.ts.map +1 -0
  90. package/dist/hooks/ui/useWindowSize.d.ts.map +1 -0
  91. package/package.json +14 -4
  92. package/dist/components/dev/IdSelector.d.ts.map +0 -1
  93. package/dist/components/dev/IdSelector.js +0 -129
  94. package/dist/hooks/useClickOutside.d.ts.map +0 -1
  95. package/dist/hooks/useCopyToClipboard.d.ts.map +0 -1
  96. package/dist/hooks/useDebounce.d.ts.map +0 -1
  97. package/dist/hooks/useEventListener.d.ts.map +0 -1
  98. package/dist/hooks/useIntersectionObserver.d.ts.map +0 -1
  99. package/dist/hooks/useLocalStorage.d.ts.map +0 -1
  100. package/dist/hooks/useMediaQuery.d.ts.map +0 -1
  101. package/dist/hooks/usePrevious.d.ts.map +0 -1
  102. package/dist/hooks/useSessionStorage.d.ts.map +0 -1
  103. package/dist/hooks/useThrottle.d.ts.map +0 -1
  104. package/dist/hooks/useToggle.d.ts.map +0 -1
  105. package/dist/hooks/useWindowSize.d.ts.map +0 -1
  106. /package/dist/hooks/{useClickOutside.d.ts → event/useClickOutside.d.ts} +0 -0
  107. /package/dist/hooks/{useClickOutside.js → event/useClickOutside.js} +0 -0
  108. /package/dist/hooks/{useEventListener.d.ts → event/useEventListener.d.ts} +0 -0
  109. /package/dist/hooks/{useEventListener.js → event/useEventListener.js} +0 -0
  110. /package/dist/hooks/{useDebounce.d.ts → performance/useDebounce.d.ts} +0 -0
  111. /package/dist/hooks/{useDebounce.js → performance/useDebounce.js} +0 -0
  112. /package/dist/hooks/{useIntersectionObserver.d.ts → performance/useIntersectionObserver.d.ts} +0 -0
  113. /package/dist/hooks/{useIntersectionObserver.js → performance/useIntersectionObserver.js} +0 -0
  114. /package/dist/hooks/{useThrottle.d.ts → performance/useThrottle.d.ts} +0 -0
  115. /package/dist/hooks/{useThrottle.js → performance/useThrottle.js} +0 -0
  116. /package/dist/hooks/{usePrevious.d.ts → state/usePrevious.d.ts} +0 -0
  117. /package/dist/hooks/{usePrevious.js → state/usePrevious.js} +0 -0
  118. /package/dist/hooks/{useToggle.d.ts → state/useToggle.d.ts} +0 -0
  119. /package/dist/hooks/{useToggle.js → state/useToggle.js} +0 -0
  120. /package/dist/hooks/{useCopyToClipboard.d.ts → storage/useCopyToClipboard.d.ts} +0 -0
  121. /package/dist/hooks/{useCopyToClipboard.js → storage/useCopyToClipboard.js} +0 -0
  122. /package/dist/hooks/{useLocalStorage.d.ts → storage/useLocalStorage.d.ts} +0 -0
  123. /package/dist/hooks/{useLocalStorage.js → storage/useLocalStorage.js} +0 -0
  124. /package/dist/hooks/{useSessionStorage.d.ts → storage/useSessionStorage.d.ts} +0 -0
  125. /package/dist/hooks/{useSessionStorage.js → storage/useSessionStorage.js} +0 -0
  126. /package/dist/hooks/{useMediaQuery.d.ts → ui/useMediaQuery.d.ts} +0 -0
  127. /package/dist/hooks/{useMediaQuery.js → ui/useMediaQuery.js} +0 -0
  128. /package/dist/hooks/{useWindowSize.d.ts → ui/useWindowSize.d.ts} +0 -0
  129. /package/dist/hooks/{useWindowSize.js → ui/useWindowSize.js} +0 -0
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { useWindowSize } from '../../hooks/useWindowSize';
2
+ import { useWindowSize } from '../../hooks';
3
3
  const positionStyles = {
4
4
  'top-left': { top: 16, left: 16 },
5
5
  'top-right': { top: 16, right: 16 },
@@ -13,7 +13,7 @@ const positionStyles = {
13
13
  * @example
14
14
  * ```tsx
15
15
  * // Vite 프로젝트
16
- * import { WindowSizeDisplay } from 'goodchuck-utils/components/dev';
16
+ * import { WindowSizeDisplay } from '@blastlabs/utils/components/dev';
17
17
  *
18
18
  * function App() {
19
19
  * return (
@@ -10,7 +10,7 @@ type Props = {
10
10
  * @example
11
11
  * ```tsx
12
12
  * // Vite 프로젝트
13
- * import { ZIndexDebugger } from 'goodchuck-utils/components/dev';
13
+ * import { ZIndexDebugger } from '@blastlabs/utils/components/dev';
14
14
  *
15
15
  * function App() {
16
16
  * return (
@@ -6,7 +6,7 @@ import React, { useState, useEffect } from 'react';
6
6
  * @example
7
7
  * ```tsx
8
8
  * // Vite 프로젝트
9
- * import { ZIndexDebugger } from 'goodchuck-utils/components/dev';
9
+ * import { ZIndexDebugger } from '@blastlabs/utils/components/dev';
10
10
  *
11
11
  * function App() {
12
12
  * return (
@@ -5,11 +5,12 @@
5
5
  * production 환경에서는 제외하는 것을 권장합니다.
6
6
  */
7
7
  export { default as IdSelector } from './IdSelector';
8
+ export type { IdSelectorProps, LoginInfo } from './IdSelector';
8
9
  export { default as WindowSizeDisplay } from './WindowSizeDisplay';
9
10
  export { default as DevPanel } from './DevPanel';
10
11
  export { default as ZIndexDebugger } from './ZIndexDebugger';
11
12
  export { default as ApiLogger, addApiLog, clearApiLogs } from './ApiLogger';
12
13
  export { default as FormDevTools } from './FormDevTools';
13
- export type { FormDevToolsProps, UseFormReturn as FormDevToolsUseFormReturn } from './FormDevTools';
14
+ export type { FormDevToolsProps } from './FormDevTools';
14
15
  export type { ApiLogEntry } from './ApiLogger';
15
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/dev/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,aAAa,IAAI,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AACpG,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/dev/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,cAAc,CAAC;AACrD,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Event Handling Hooks
3
+ */
4
+ export * from './useEventListener';
5
+ export * from './useClickOutside';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/event/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Event Handling Hooks
3
+ */
4
+ export * from './useEventListener';
5
+ export * from './useClickOutside';
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useClickOutside.d.ts","sourceRoot":"","sources":["../../../src/hooks/event/useClickOutside.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EACjE,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,EACjB,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,KAAK,IAAI,EACjD,OAAO,GAAE,OAAc,GACtB,IAAI,CA2BN;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EACzE,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,EACpB,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,KAAK,IAAI,EACjD,OAAO,GAAE,OAAc,GACtB,IAAI,CA6BN"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useEventListener.d.ts","sourceRoot":"","sources":["../../../src/hooks/event/useEventListener.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,SAAS,EAAE,MAAM,OAAO,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,cAAc,EAC7D,SAAS,EAAE,CAAC,EACZ,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,IAAI,EAC3C,OAAO,CAAC,EAAE,SAAS,EACnB,OAAO,CAAC,EAAE,OAAO,GAAG,uBAAuB,GAC1C,IAAI,CAAC;AAER,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,mBAAmB,EAAE,CAAC,SAAS,WAAW,GAAG,cAAc,EAC1G,SAAS,EAAE,CAAC,EACZ,OAAO,EAAE,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC,CAAC,KAAK,IAAI,EAChD,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EACrB,OAAO,CAAC,EAAE,OAAO,GAAG,uBAAuB,GAC1C,IAAI,CAAC;AAER,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAC/D,SAAS,EAAE,CAAC,EACZ,OAAO,EAAE,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,KAAK,IAAI,EAC7C,OAAO,EAAE,QAAQ,EACjB,OAAO,CAAC,EAAE,OAAO,GAAG,uBAAuB,GAC1C,IAAI,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useCRUDForm.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCRUDForm.test.d.ts","sourceRoot":"","sources":["../../../../src/hooks/form/__tests__/useCRUDForm.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,487 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { renderHook, waitFor, act } from '@testing-library/react';
3
+ import { useCRUDForm } from '../useCRUDForm';
4
+ // Mock form object
5
+ const createMockForm = (defaultValues = {}) => ({
6
+ watch: vi.fn((name) => name ? defaultValues[name] : defaultValues),
7
+ getValues: vi.fn((name) => name ? defaultValues[name] : defaultValues),
8
+ setValue: vi.fn(),
9
+ reset: vi.fn(),
10
+ trigger: vi.fn().mockResolvedValue(true),
11
+ formState: {
12
+ errors: {},
13
+ dirtyFields: {},
14
+ touchedFields: {},
15
+ isValid: true,
16
+ isSubmitting: false,
17
+ submitCount: 0,
18
+ defaultValues,
19
+ isDirty: false,
20
+ isValidating: false,
21
+ isLoading: false,
22
+ isSubmitted: false,
23
+ isSubmitSuccessful: false,
24
+ },
25
+ handleSubmit: vi.fn((onValid) => async (e) => {
26
+ e?.preventDefault?.();
27
+ await onValid(defaultValues);
28
+ }),
29
+ getFieldState: vi.fn(),
30
+ setError: vi.fn(),
31
+ clearErrors: vi.fn(),
32
+ resetField: vi.fn(),
33
+ setFocus: vi.fn(),
34
+ unregister: vi.fn(),
35
+ register: vi.fn(),
36
+ control: {},
37
+ });
38
+ describe('useCRUDForm', () => {
39
+ describe('Mode Detection', () => {
40
+ it('should be in create mode when no id is provided', () => {
41
+ const form = createMockForm();
42
+ const { result } = renderHook(() => useCRUDForm({
43
+ form,
44
+ }));
45
+ expect(result.current.mode).toBe('create');
46
+ expect(result.current.isCreateMode).toBe(true);
47
+ expect(result.current.isEditMode).toBe(false);
48
+ });
49
+ it('should be in edit mode when id is provided', () => {
50
+ const form = createMockForm();
51
+ const { result } = renderHook(() => useCRUDForm({
52
+ id: '123',
53
+ form,
54
+ }));
55
+ expect(result.current.mode).toBe('edit');
56
+ expect(result.current.isEditMode).toBe(true);
57
+ expect(result.current.isCreateMode).toBe(false);
58
+ });
59
+ });
60
+ describe('Data Fetching (Edit Mode)', () => {
61
+ it('should fetch data in edit mode', async () => {
62
+ const mockData = { name: 'John', email: 'john@example.com' };
63
+ const fetchFn = vi.fn().mockResolvedValue(mockData);
64
+ const form = createMockForm();
65
+ const { result } = renderHook(() => useCRUDForm({
66
+ id: '123',
67
+ form,
68
+ fetchFn,
69
+ }));
70
+ expect(result.current.isLoading).toBe(true);
71
+ expect(fetchFn).toHaveBeenCalledWith('123');
72
+ await waitFor(() => {
73
+ expect(result.current.isLoading).toBe(false);
74
+ });
75
+ expect(form.reset).toHaveBeenCalledWith(mockData);
76
+ expect(result.current.originalData).toEqual(mockData);
77
+ expect(result.current.error).toBeNull();
78
+ });
79
+ it('should call onFetchSuccess when data is loaded', async () => {
80
+ const mockData = { name: 'John' };
81
+ const fetchFn = vi.fn().mockResolvedValue(mockData);
82
+ const onFetchSuccess = vi.fn();
83
+ const form = createMockForm();
84
+ renderHook(() => useCRUDForm({
85
+ id: '123',
86
+ form,
87
+ fetchFn,
88
+ onFetchSuccess,
89
+ }));
90
+ await waitFor(() => {
91
+ expect(onFetchSuccess).toHaveBeenCalledWith(mockData);
92
+ });
93
+ });
94
+ it('should handle fetch errors', async () => {
95
+ const error = new Error('Failed to fetch');
96
+ const fetchFn = vi.fn().mockRejectedValue(error);
97
+ const onError = vi.fn();
98
+ const form = createMockForm();
99
+ const { result } = renderHook(() => useCRUDForm({
100
+ id: '123',
101
+ form,
102
+ fetchFn,
103
+ onError,
104
+ }));
105
+ await waitFor(() => {
106
+ expect(result.current.isLoading).toBe(false);
107
+ });
108
+ expect(result.current.error).toEqual(error);
109
+ expect(onError).toHaveBeenCalledWith(error, 'edit');
110
+ });
111
+ it('should not fetch data in create mode', () => {
112
+ const fetchFn = vi.fn();
113
+ const form = createMockForm();
114
+ const { result } = renderHook(() => useCRUDForm({
115
+ form,
116
+ fetchFn,
117
+ }));
118
+ expect(fetchFn).not.toHaveBeenCalled();
119
+ expect(result.current.isLoading).toBe(false);
120
+ });
121
+ });
122
+ describe('Create Mode Submit', () => {
123
+ it('should call createFn with form data', async () => {
124
+ const formData = { name: 'John', email: 'john@example.com' };
125
+ const createFn = vi.fn().mockResolvedValue({ id: '123', ...formData });
126
+ const form = createMockForm(formData);
127
+ const { result } = renderHook(() => useCRUDForm({
128
+ form,
129
+ createFn,
130
+ }));
131
+ await act(async () => {
132
+ await result.current.submit(formData);
133
+ });
134
+ expect(createFn).toHaveBeenCalledWith(formData);
135
+ });
136
+ it('should call onSuccess after successful create', async () => {
137
+ const formData = { name: 'John' };
138
+ const createFn = vi.fn().mockResolvedValue({ id: '123' });
139
+ const onSuccess = vi.fn();
140
+ const form = createMockForm(formData);
141
+ const { result } = renderHook(() => useCRUDForm({
142
+ form,
143
+ createFn,
144
+ onSuccess,
145
+ }));
146
+ await act(async () => {
147
+ await result.current.submit(formData);
148
+ });
149
+ expect(onSuccess).toHaveBeenCalledWith(formData, 'create');
150
+ });
151
+ it('should handle create errors', async () => {
152
+ const error = new Error('Failed to create');
153
+ const createFn = vi.fn().mockRejectedValue(error);
154
+ const onError = vi.fn();
155
+ const form = createMockForm();
156
+ const { result } = renderHook(() => useCRUDForm({
157
+ form,
158
+ createFn,
159
+ onError,
160
+ }));
161
+ await act(async () => {
162
+ try {
163
+ await result.current.submit({});
164
+ }
165
+ catch (e) {
166
+ // Expected error
167
+ }
168
+ });
169
+ expect(result.current.error).toEqual(error);
170
+ expect(onError).toHaveBeenCalledWith(error, 'create');
171
+ });
172
+ });
173
+ describe('Edit Mode Submit', () => {
174
+ it('should call updateFn with id and form data', async () => {
175
+ const formData = { name: 'John Updated' };
176
+ const updateFn = vi.fn().mockResolvedValue({ id: '123', ...formData });
177
+ const fetchFn = vi.fn().mockResolvedValue({ name: 'John' });
178
+ const form = createMockForm(formData);
179
+ const { result } = renderHook(() => useCRUDForm({
180
+ id: '123',
181
+ form,
182
+ fetchFn,
183
+ updateFn,
184
+ }));
185
+ await waitFor(() => {
186
+ expect(result.current.isLoading).toBe(false);
187
+ });
188
+ await act(async () => {
189
+ await result.current.submit(formData);
190
+ });
191
+ expect(updateFn).toHaveBeenCalledWith('123', formData);
192
+ });
193
+ it('should call onSuccess after successful update', async () => {
194
+ const formData = { name: 'John Updated' };
195
+ const updateFn = vi.fn().mockResolvedValue({ id: '123' });
196
+ const fetchFn = vi.fn().mockResolvedValue({ name: 'John' });
197
+ const onSuccess = vi.fn();
198
+ const form = createMockForm(formData);
199
+ const { result } = renderHook(() => useCRUDForm({
200
+ id: '123',
201
+ form,
202
+ fetchFn,
203
+ updateFn,
204
+ onSuccess,
205
+ }));
206
+ await waitFor(() => {
207
+ expect(result.current.isLoading).toBe(false);
208
+ });
209
+ await act(async () => {
210
+ await result.current.submit(formData);
211
+ });
212
+ expect(onSuccess).toHaveBeenCalledWith(formData, 'edit');
213
+ });
214
+ it('should handle update errors', async () => {
215
+ const error = new Error('Failed to update');
216
+ const updateFn = vi.fn().mockRejectedValue(error);
217
+ const fetchFn = vi.fn().mockResolvedValue({ name: 'John' });
218
+ const onError = vi.fn();
219
+ const form = createMockForm();
220
+ const { result } = renderHook(() => useCRUDForm({
221
+ id: '123',
222
+ form,
223
+ fetchFn,
224
+ updateFn,
225
+ onError,
226
+ }));
227
+ await waitFor(() => {
228
+ expect(result.current.isLoading).toBe(false);
229
+ });
230
+ await act(async () => {
231
+ try {
232
+ await result.current.submit({});
233
+ }
234
+ catch (e) {
235
+ // Expected error
236
+ }
237
+ });
238
+ expect(result.current.error).toEqual(error);
239
+ expect(onError).toHaveBeenCalledWith(error, 'edit');
240
+ });
241
+ });
242
+ describe('Data Transformation', () => {
243
+ it('should transform data before submit', async () => {
244
+ const formData = { tags: 'react, typescript, hooks' };
245
+ const transformData = vi.fn((data, mode) => ({
246
+ ...data,
247
+ tags: data.tags.split(',').map((t) => t.trim()),
248
+ }));
249
+ const createFn = vi.fn().mockResolvedValue({});
250
+ const form = createMockForm(formData);
251
+ const { result } = renderHook(() => useCRUDForm({
252
+ form,
253
+ createFn,
254
+ transformData,
255
+ }));
256
+ await act(async () => {
257
+ await result.current.submit(formData);
258
+ });
259
+ expect(transformData).toHaveBeenCalledWith(formData, 'create');
260
+ expect(createFn).toHaveBeenCalledWith({
261
+ tags: ['react', 'typescript', 'hooks'],
262
+ });
263
+ });
264
+ });
265
+ describe('Delete Operation', () => {
266
+ it('should delete data in edit mode', async () => {
267
+ const deleteFn = vi.fn().mockResolvedValue({});
268
+ const fetchFn = vi.fn().mockResolvedValue({ name: 'John' });
269
+ const form = createMockForm();
270
+ const { result } = renderHook(() => useCRUDForm({
271
+ id: '123',
272
+ form,
273
+ fetchFn,
274
+ deleteFn,
275
+ }));
276
+ await waitFor(() => {
277
+ expect(result.current.isLoading).toBe(false);
278
+ });
279
+ await act(async () => {
280
+ await result.current.handleDelete();
281
+ });
282
+ expect(deleteFn).toHaveBeenCalledWith('123');
283
+ expect(result.current.isDeleting).toBe(false);
284
+ });
285
+ it('should call onSuccess after successful delete', async () => {
286
+ const deleteFn = vi.fn().mockResolvedValue({});
287
+ const fetchFn = vi.fn().mockResolvedValue({ name: 'John' });
288
+ const onSuccess = vi.fn();
289
+ const form = createMockForm();
290
+ const { result } = renderHook(() => useCRUDForm({
291
+ id: '123',
292
+ form,
293
+ fetchFn,
294
+ deleteFn,
295
+ onSuccess,
296
+ }));
297
+ await waitFor(() => {
298
+ expect(result.current.isLoading).toBe(false);
299
+ });
300
+ await act(async () => {
301
+ await result.current.handleDelete();
302
+ });
303
+ expect(onSuccess).toHaveBeenCalled();
304
+ });
305
+ it('should throw error when delete is called in create mode', async () => {
306
+ const form = createMockForm();
307
+ const { result } = renderHook(() => useCRUDForm({
308
+ form,
309
+ }));
310
+ await expect(async () => {
311
+ await act(async () => {
312
+ await result.current.handleDelete();
313
+ });
314
+ }).rejects.toThrow('Delete is only available in edit mode');
315
+ });
316
+ it('should handle delete errors', async () => {
317
+ const error = new Error('Failed to delete');
318
+ const deleteFn = vi.fn().mockRejectedValue(error);
319
+ const fetchFn = vi.fn().mockResolvedValue({ name: 'John' });
320
+ const onError = vi.fn();
321
+ const form = createMockForm();
322
+ const { result } = renderHook(() => useCRUDForm({
323
+ id: '123',
324
+ form,
325
+ fetchFn,
326
+ deleteFn,
327
+ onError,
328
+ }));
329
+ await waitFor(() => {
330
+ expect(result.current.isLoading).toBe(false);
331
+ });
332
+ await act(async () => {
333
+ try {
334
+ await result.current.handleDelete();
335
+ }
336
+ catch (e) {
337
+ // Expected error
338
+ }
339
+ });
340
+ expect(result.current.error).toEqual(error);
341
+ expect(onError).toHaveBeenCalledWith(error, 'edit');
342
+ });
343
+ });
344
+ describe('Loading States', () => {
345
+ it('should track isSubmitting state', async () => {
346
+ const createFn = vi.fn().mockImplementation(() => new Promise(resolve => setTimeout(() => resolve({}), 100)));
347
+ const form = createMockForm();
348
+ const { result } = renderHook(() => useCRUDForm({
349
+ form,
350
+ createFn,
351
+ }));
352
+ expect(result.current.isSubmitting).toBe(false);
353
+ const submitPromise = act(async () => {
354
+ await result.current.submit({});
355
+ });
356
+ await waitFor(() => {
357
+ expect(result.current.isSubmitting).toBe(false);
358
+ });
359
+ await submitPromise;
360
+ });
361
+ it('should track isDeleting state', async () => {
362
+ const deleteFn = vi.fn().mockImplementation(() => new Promise(resolve => setTimeout(() => resolve({}), 100)));
363
+ const fetchFn = vi.fn().mockResolvedValue({ name: 'John' });
364
+ const form = createMockForm();
365
+ const { result } = renderHook(() => useCRUDForm({
366
+ id: '123',
367
+ form,
368
+ fetchFn,
369
+ deleteFn,
370
+ }));
371
+ await waitFor(() => {
372
+ expect(result.current.isLoading).toBe(false);
373
+ });
374
+ expect(result.current.isDeleting).toBe(false);
375
+ const deletePromise = act(async () => {
376
+ await result.current.handleDelete();
377
+ });
378
+ await waitFor(() => {
379
+ expect(result.current.isDeleting).toBe(false);
380
+ });
381
+ await deletePromise;
382
+ });
383
+ });
384
+ describe('Send Only Dirty Fields', () => {
385
+ it('should send only changed fields when sendOnlyDirtyFields is true', async () => {
386
+ const originalData = { name: 'John', email: 'john@example.com', age: 30 };
387
+ const updatedData = { name: 'John Smith', email: 'john@example.com', age: 30 };
388
+ const fetchFn = vi.fn().mockResolvedValue(originalData);
389
+ const updateFn = vi.fn().mockResolvedValue({});
390
+ const form = createMockForm(updatedData);
391
+ // Mock dirtyFields - only name is dirty
392
+ form.formState.dirtyFields = { name: true };
393
+ const { result } = renderHook(() => useCRUDForm({
394
+ id: '123',
395
+ form,
396
+ fetchFn,
397
+ updateFn,
398
+ sendOnlyDirtyFields: true,
399
+ }));
400
+ await waitFor(() => {
401
+ expect(result.current.isLoading).toBe(false);
402
+ });
403
+ await act(async () => {
404
+ await result.current.submit(updatedData);
405
+ });
406
+ // Only the dirty field should be sent
407
+ expect(updateFn).toHaveBeenCalledWith('123', { name: 'John Smith' });
408
+ });
409
+ it('should send all fields when sendOnlyDirtyFields is false (default)', async () => {
410
+ const originalData = { name: 'John', email: 'john@example.com' };
411
+ const updatedData = { name: 'John Smith', email: 'john@example.com' };
412
+ const fetchFn = vi.fn().mockResolvedValue(originalData);
413
+ const updateFn = vi.fn().mockResolvedValue({});
414
+ const form = createMockForm(updatedData);
415
+ form.formState.dirtyFields = { name: true };
416
+ const { result } = renderHook(() => useCRUDForm({
417
+ id: '123',
418
+ form,
419
+ fetchFn,
420
+ updateFn,
421
+ sendOnlyDirtyFields: false, // explicitly false
422
+ }));
423
+ await waitFor(() => {
424
+ expect(result.current.isLoading).toBe(false);
425
+ });
426
+ await act(async () => {
427
+ await result.current.submit(updatedData);
428
+ });
429
+ // All fields should be sent
430
+ expect(updateFn).toHaveBeenCalledWith('123', updatedData);
431
+ });
432
+ it('should send all fields in create mode even when sendOnlyDirtyFields is true', async () => {
433
+ const formData = { name: 'John', email: 'john@example.com' };
434
+ const createFn = vi.fn().mockResolvedValue({ id: '123' });
435
+ const form = createMockForm(formData);
436
+ form.formState.dirtyFields = { name: true, email: true };
437
+ const { result } = renderHook(() => useCRUDForm({
438
+ form,
439
+ createFn,
440
+ sendOnlyDirtyFields: true, // Should be ignored in create mode
441
+ }));
442
+ await act(async () => {
443
+ await result.current.submit(formData);
444
+ });
445
+ // All fields should be sent in create mode
446
+ expect(createFn).toHaveBeenCalledWith(formData);
447
+ });
448
+ it('should handle nested dirty fields', async () => {
449
+ const originalData = {
450
+ name: 'John',
451
+ address: {
452
+ city: 'Seoul',
453
+ country: 'Korea',
454
+ },
455
+ };
456
+ const updatedData = {
457
+ name: 'John',
458
+ address: {
459
+ city: 'Busan',
460
+ country: 'Korea',
461
+ },
462
+ };
463
+ const fetchFn = vi.fn().mockResolvedValue(originalData);
464
+ const updateFn = vi.fn().mockResolvedValue({});
465
+ const form = createMockForm(updatedData);
466
+ // Only address.city is dirty
467
+ form.formState.dirtyFields = { address: { city: true } };
468
+ const { result } = renderHook(() => useCRUDForm({
469
+ id: '123',
470
+ form,
471
+ fetchFn,
472
+ updateFn,
473
+ sendOnlyDirtyFields: true,
474
+ }));
475
+ await waitFor(() => {
476
+ expect(result.current.isLoading).toBe(false);
477
+ });
478
+ await act(async () => {
479
+ await result.current.submit(updatedData);
480
+ });
481
+ // Only the nested dirty field should be extracted
482
+ expect(updateFn).toHaveBeenCalledWith('123', {
483
+ address: { city: 'Busan' },
484
+ });
485
+ });
486
+ });
487
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Form Management Hooks
3
+ */
4
+ export * from './useCRUDForm';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/form/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,eAAe,CAAC"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Form Management Hooks
3
+ */
4
+ export * from './useCRUDForm';