gbs-add-block 1.0.5 → 1.0.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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # GBS Building Blocks 2.0 (v1.0.5)
1
+ # GBS Building Blocks 2.0 (v1.0.7)
2
2
 
3
3
  Latest and upgraded version of GBS building blocks with headless UI and removed dependencies.
4
4
 
@@ -6,13 +6,10 @@ Latest and upgraded version of GBS building blocks with headless UI and removed
6
6
 
7
7
  For detailed documentation on usage and props, Please visit: [Building Block Documentation v2.0](https://blackmax-designs.gitbook.io/building-block-v2.0)
8
8
 
9
- ## What's New 🎉 (Ver 1.0.5)
9
+ ## What's New 🎉 (Ver 1.0.7)
10
10
 
11
- - Major Update with more stable features and Data Grid.
12
- - Deprecated Grid Component, Use New Data Grid instead.
13
- - More UI/UX Optimization.
14
- - Animation improvements.
15
- - Custom Components in SideBar and Mobile Screen Support
11
+ - Uploader Enhancement
12
+ - Select Bug Fix
16
13
 
17
14
  ## Authors
18
15
 
package/index.cjs CHANGED
@@ -31,6 +31,7 @@ const CONFIG = {
31
31
  "BreadCrumb",
32
32
  "Bargraph",
33
33
  "UsePaginatedData",
34
+ "UseUploader"
34
35
  ],
35
36
  // Define component dependencies
36
37
  dependencies: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gbs-add-block",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "React Component Library",
5
5
  "type": "module",
6
6
  "files": [
@@ -13,6 +13,7 @@ import React, {
13
13
  useImperativeHandle,
14
14
  useRef,
15
15
  } from "react";
16
+ import { createPortal } from "react-dom";
16
17
  import Icon from "../icon/Icon";
17
18
  import { check, search, upDown, x } from "../icon/iconPaths";
18
19
  import type { ItemsProps, SelectHandle, SelectProps } from "./types";
@@ -21,9 +22,11 @@ import { iconClass, popUp, primary } from "../globalStyle";
21
22
  import {
22
23
  useSelectState,
23
24
  useSelectData,
24
- useClickOutside,
25
25
  applyScrollbarStyles,
26
26
  useKeyboardNavigation,
27
+ usePortalPopup,
28
+ getPortalStyles,
29
+ getAnimationClasses,
27
30
  } from "@grampro/headless-helpers";
28
31
 
29
32
  const Select = forwardRef<SelectHandle, SelectProps>((props, ref) => {
@@ -68,9 +71,16 @@ const Select = forwardRef<SelectHandle, SelectProps>((props, ref) => {
68
71
  const inputRef = useRef<HTMLInputElement>(null);
69
72
  const selectRef = useRef<HTMLDivElement>(null);
70
73
 
71
- useClickOutside(selectRef as React.RefObject<HTMLElement>, () =>
72
- setShowPopover(false)
73
- );
74
+ // Use the custom portal hooks
75
+ const position = usePortalPopup({
76
+ isOpen: showPopover,
77
+ onClose: () => setShowPopover(false),
78
+ triggerRef: selectRef,
79
+ popupHeight: 250,
80
+ offset: 4,
81
+ minSpaceRequired: 50,
82
+ portalSelector: '[data-select-portal="true"]',
83
+ });
74
84
 
75
85
  // Update the handleSelect to accept a SelectItem
76
86
  const handleSelect = useCallback(
@@ -120,97 +130,113 @@ const Select = forwardRef<SelectHandle, SelectProps>((props, ref) => {
120
130
  selected: selectedItem,
121
131
  }));
122
132
 
123
- return (
133
+ // Portal popup content with smart positioning
134
+ const popupContent = showPopover && !disabled && (
124
135
  <div
125
- className="relative w-full mt-2"
126
- ref={selectRef}
127
- id={id}
128
- onKeyDown={handleKeyDown}
136
+ className={`${popUp["pop-up-style"]} ${getAnimationClasses(
137
+ position.placement
138
+ )}`}
139
+ style={getPortalStyles(position, 250)}
140
+ data-select-portal="true"
129
141
  >
130
- <div className="w-full relative">
131
- <button
132
- className={`${error ? primary["error-border"] : "border"} ${
133
- selectStyle["select-button"]
134
- } ${disabled ? "opacity-50 cursor-not-allowed" : ""}`}
135
- onClick={togglePopover}
136
- type="button"
137
- disabled={disabled}
138
- >
139
- {selectedDisplay || placeholder}
140
- <Icon
141
- elements={upDown}
142
- svgClass={`${iconClass["grey-common"]} ${
143
- disabled ? "opacity-50" : ""
144
- }`}
142
+ {showSearch && (
143
+ <div className={selectStyle["input-parent"]}>
144
+ <Icon elements={search} svgClass={iconClass["grey-common"]} />
145
+ <input
146
+ autoComplete="off"
147
+ type="text"
148
+ name="search"
149
+ id="search"
150
+ placeholder="Search a value"
151
+ className="w-full outline-none dark:bg-transparent"
152
+ ref={inputRef}
153
+ value={searchTerm}
154
+ onChange={(e) => {
155
+ setSearchTerm(e.target.value);
156
+ if (onFiltering) onFiltering(e.target.value);
157
+ setFocusedIndex(-1);
158
+ }}
145
159
  />
146
- </button>
147
- {selectedDisplay && !disabled && (
160
+ </div>
161
+ )}
162
+ {filteredItems.length > 0 ? (
163
+ filteredItems.map(({ value, label }, index: number) => (
148
164
  <button
149
- className={selectStyle["selectedDisplay-Button"]}
150
- onClick={handleClear}
165
+ key={value}
166
+ ref={(el: any) => (itemRefs.current[index] = el)}
167
+ className={`${selectStyle["filter-button"]} ${
168
+ focusedIndex === index ? "bg-gray-100 dark:bg-gray-700" : ""
169
+ }`}
170
+ onClick={() => handleSelect({ value, label })}
171
+ onMouseEnter={() => setFocusedIndex(index)}
151
172
  >
152
- <Icon elements={x} svgClass={iconClass["grey-common"]} />
173
+ <Icon
174
+ elements={check}
175
+ svgClass={`h-4 w-4 fill-none ${
176
+ selectedItem === value ? "stroke-gray-500" : ""
177
+ }`}
178
+ />
179
+ {label}
153
180
  </button>
154
- )}
155
- {error && <p className={primary["error-primary"]}>{error}</p>}
156
- </div>
181
+ ))
182
+ ) : (
183
+ <div className="text-sm text-center p-2">No Data Found</div>
184
+ )}
185
+ </div>
186
+ );
157
187
 
158
- <input
159
- type="hidden"
160
- name={name}
161
- value={selectedItem || ""}
162
- readOnly
163
- disabled={disabled}
164
- />
165
-
166
- {showPopover && !disabled && (
167
- <div className={popUp["pop-up-style"]}>
168
- {showSearch && (
169
- <div className={selectStyle["input-parent"]}>
170
- <Icon elements={search} svgClass={iconClass["grey-common"]} />
171
- <input
172
- autoComplete="off"
173
- type="text"
174
- name="search"
175
- id="search"
176
- placeholder="Search a value"
177
- className="w-full outline-none"
178
- ref={inputRef}
179
- value={searchTerm}
180
- onChange={(e) => {
181
- setSearchTerm(e.target.value);
182
- if (onFiltering) onFiltering(e.target.value);
183
- setFocusedIndex(-1);
184
- }}
185
- />
186
- </div>
187
- )}
188
- {filteredItems.length > 0 ? (
189
- filteredItems.map(({ value, label }, index: number) => (
190
- <button
191
- key={value}
192
- ref={(el: any) => (itemRefs.current[index] = el)}
193
- className={`${selectStyle["filter-button"]} ${
194
- focusedIndex === index ? "bg-gray-100" : ""
195
- }`}
196
- onClick={() => handleSelect({ value, label })}
197
- onMouseEnter={() => setFocusedIndex(index)}
198
- >
199
- <Icon
200
- elements={check}
201
- svgClass={`h-4 w-4 fill-none ${
202
- selectedItem === value ? "stroke-gray-500" : ""
203
- }`}
204
- />
205
- {label}
206
- </button>
207
- ))
208
- ) : (
209
- <div className="text-sm text-center">No Data Found</div>
188
+ return (
189
+ <>
190
+ <div
191
+ className="relative w-full mt-2"
192
+ ref={selectRef}
193
+ id={id}
194
+ onKeyDown={handleKeyDown}
195
+ >
196
+ <div className="w-full relative">
197
+ <button
198
+ className={`${error ? primary["error-border"] : "border"} ${
199
+ selectStyle["select-button"]
200
+ } ${disabled ? "opacity-50 cursor-not-allowed" : ""}`}
201
+ onClick={togglePopover}
202
+ type="button"
203
+ disabled={disabled}
204
+ >
205
+ {selectedDisplay || placeholder}
206
+ <Icon
207
+ elements={upDown}
208
+ svgClass={`${iconClass["grey-common"]} ${
209
+ disabled ? "opacity-50" : ""
210
+ } ${
211
+ showPopover && position.placement === "top" ? "rotate-180" : ""
212
+ }`}
213
+ />
214
+ </button>
215
+ {selectedDisplay && !disabled && (
216
+ <button
217
+ className={selectStyle["selectedDisplay-Button"]}
218
+ onClick={handleClear}
219
+ >
220
+ <Icon elements={x} svgClass={iconClass["grey-common"]} />
221
+ </button>
210
222
  )}
223
+ {error && <p className={primary["error-primary"]}>{error}</p>}
211
224
  </div>
212
- )}
213
- </div>
225
+
226
+ <input
227
+ type="hidden"
228
+ name={name}
229
+ value={selectedItem || ""}
230
+ readOnly
231
+ disabled={disabled}
232
+ />
233
+ </div>
234
+
235
+ {/* Render popup in portal to document.body */}
236
+ {typeof document !== "undefined" &&
237
+ popupContent &&
238
+ createPortal(popupContent, document.body)}
239
+ </>
214
240
  );
215
241
  });
216
242
 
@@ -0,0 +1,303 @@
1
+ import { useState, useCallback } from "react";
2
+
3
+ // Types
4
+ interface ChunkInfo {
5
+ file: Blob;
6
+ originalFile: File;
7
+ chunkIndex: number;
8
+ totalChunks: number;
9
+ }
10
+
11
+ interface UploadProgress {
12
+ [fileName: string]: number;
13
+ }
14
+
15
+ interface UploadResult {
16
+ fileName: string;
17
+ status: "complete" | "chunk_received" | "error";
18
+ message?: string;
19
+ }
20
+
21
+ interface UseChunkedUploadOptions {
22
+ chunkSize?: number;
23
+ maxConcurrentUploads?: number;
24
+ baseUrl?: string;
25
+ onProgress?: (fileName: string, progress: number) => void;
26
+ onComplete?: (fileName: string, result: UploadResult) => void;
27
+ onError?: (fileName: string, error: Error) => void;
28
+ additionalParams?: any;
29
+ }
30
+
31
+ interface UseChunkedUploadReturn {
32
+ uploadFiles: (files: File[]) => Promise<void>;
33
+ isUploading: boolean;
34
+ uploadProgress: UploadProgress;
35
+ uploadStatus: string;
36
+ cancelUpload: () => void;
37
+ reset: () => void;
38
+ }
39
+
40
+ // Helper function to process files into chunks
41
+ function processFiles(files: File[], chunkSize: number): ChunkInfo[] {
42
+ const finalFiles: ChunkInfo[] = [];
43
+
44
+ files.forEach((file) => {
45
+ if (file.size > chunkSize) {
46
+ const totalChunks = Math.ceil(file.size / chunkSize);
47
+ let offset = 0;
48
+ let chunkIndex = 0;
49
+
50
+ while (offset < file.size) {
51
+ const chunk = file.slice(offset, offset + chunkSize);
52
+ finalFiles.push({
53
+ file: chunk,
54
+ originalFile: file,
55
+ chunkIndex,
56
+ totalChunks,
57
+ });
58
+ offset += chunkSize;
59
+ chunkIndex++;
60
+ }
61
+ } else {
62
+ finalFiles.push({
63
+ file,
64
+ originalFile: file,
65
+ chunkIndex: 0,
66
+ totalChunks: 1,
67
+ });
68
+ }
69
+ });
70
+
71
+ return finalFiles;
72
+ }
73
+
74
+ // Helper function to upload a single chunk
75
+ async function uploadChunk(
76
+ chunk: Blob,
77
+ originalFile: File,
78
+ chunkIndex: number,
79
+ totalChunks: number,
80
+ baseUrl: string,
81
+ signal?: AbortSignal,
82
+ additionalParams: any = {}
83
+ ): Promise<UploadResult> {
84
+ const formData = new FormData();
85
+ formData.append("chunk", chunk);
86
+ formData.append("fileName", originalFile.name);
87
+ formData.append("chunkIndex", chunkIndex.toString());
88
+ formData.append("totalChunks", totalChunks.toString());
89
+ formData.append("fileSize", originalFile.size.toString());
90
+
91
+ Object.entries(additionalParams).forEach(([key, value]: any) => {
92
+ formData.append(key, value);
93
+ });
94
+
95
+ const response = await fetch(`${baseUrl}`, {
96
+ method: "POST",
97
+ body: formData,
98
+ signal,
99
+ });
100
+
101
+ if (!response.ok) {
102
+ throw new Error(`Upload failed: ${response.statusText}`);
103
+ }
104
+
105
+ return response.json();
106
+ }
107
+
108
+ // Main custom hook
109
+ export function useChunkedUpload(
110
+ options: UseChunkedUploadOptions = {}
111
+ ): UseChunkedUploadReturn {
112
+ const {
113
+ chunkSize = 1024 * 1024, // 1MB default
114
+ maxConcurrentUploads = 3,
115
+ baseUrl = "",
116
+ onProgress,
117
+ onComplete,
118
+ onError,
119
+ additionalParams = {},
120
+ } = options;
121
+
122
+ const [isUploading, setIsUploading] = useState(false);
123
+ const [uploadProgress, setUploadProgress] = useState<UploadProgress>({});
124
+ const [uploadStatus, setUploadStatus] = useState("");
125
+ const [abortController, setAbortController] =
126
+ useState<AbortController | null>(null);
127
+
128
+ const updateProgress = useCallback(
129
+ (fileName: string, progress: number) => {
130
+ setUploadProgress((prev) => ({
131
+ ...prev,
132
+ [fileName]: progress,
133
+ }));
134
+ onProgress?.(fileName, progress);
135
+ },
136
+ [onProgress]
137
+ );
138
+
139
+ const updateStatus = useCallback((status: string) => {
140
+ setUploadStatus(status);
141
+ }, []);
142
+
143
+ const uploadFiles = useCallback(
144
+ async (files: File[]) => {
145
+ if (files.length === 0) {
146
+ throw new Error("No files selected for upload.");
147
+ }
148
+
149
+ // Create new abort controller
150
+ const controller = new AbortController();
151
+ setAbortController(controller);
152
+
153
+ setIsUploading(true);
154
+ updateStatus("Processing files...");
155
+
156
+ try {
157
+ const processedFiles = processFiles(files, chunkSize);
158
+
159
+ // Group chunks by file
160
+ const fileChunks: { [key: string]: ChunkInfo[] } = {};
161
+ processedFiles.forEach((processedFile) => {
162
+ const fileName = processedFile.originalFile.name;
163
+ if (!fileChunks[fileName]) {
164
+ fileChunks[fileName] = [];
165
+ }
166
+ fileChunks[fileName].push(processedFile);
167
+ });
168
+
169
+ // Upload files with concurrency control
170
+ const fileNames = Object.keys(fileChunks);
171
+ const uploadPromises = fileNames.map(async (fileName) => {
172
+ const chunks = fileChunks[fileName];
173
+ updateStatus(`Uploading ${fileName}...`);
174
+
175
+ try {
176
+ // Create semaphore for concurrent chunk uploads
177
+ const semaphore = new Array(maxConcurrentUploads).fill(null);
178
+ let chunkIndex = 0;
179
+ const results: UploadResult[] = [];
180
+
181
+ const uploadNextChunk = async (): Promise<void> => {
182
+ if (chunkIndex >= chunks.length) return;
183
+
184
+ const currentChunk = chunks[chunkIndex++];
185
+ const result = await uploadChunk(
186
+ currentChunk.file,
187
+ currentChunk.originalFile,
188
+ currentChunk.chunkIndex,
189
+ currentChunk.totalChunks,
190
+ baseUrl,
191
+ controller.signal,
192
+ additionalParams
193
+ );
194
+
195
+ results.push(result);
196
+
197
+ // Update progress
198
+ const progress = (results.length / chunks.length) * 100;
199
+ updateProgress(fileName, progress);
200
+
201
+ // Continue uploading if there are more chunks
202
+ if (chunkIndex < chunks.length) {
203
+ await uploadNextChunk();
204
+ }
205
+ };
206
+
207
+ // Start concurrent uploads
208
+ await Promise.all(semaphore.map(() => uploadNextChunk()));
209
+
210
+ // Check if file is complete
211
+ const completeResult = results.find((r) => r.status === "complete");
212
+ if (completeResult) {
213
+ updateStatus(`${fileName} uploaded successfully!`);
214
+ onComplete?.(fileName, completeResult);
215
+ }
216
+
217
+ return { fileName, success: true };
218
+ } catch (error) {
219
+ const errorObj =
220
+ error instanceof Error ? error : new Error(String(error));
221
+ updateStatus(`Failed to upload ${fileName}: ${errorObj.message}`);
222
+ onError?.(fileName, errorObj);
223
+ return { fileName, success: false, error: errorObj };
224
+ }
225
+ });
226
+
227
+ const results = await Promise.all(uploadPromises);
228
+
229
+ const successCount = results.filter((r) => r.success).length;
230
+ const failCount = results.length - successCount;
231
+
232
+ if (failCount === 0) {
233
+ updateStatus("All files uploaded successfully!");
234
+ } else if (successCount === 0) {
235
+ updateStatus("All uploads failed!");
236
+ } else {
237
+ updateStatus(`${successCount} files uploaded, ${failCount} failed`);
238
+ }
239
+ } catch (error) {
240
+ if (error instanceof Error && error.name === "AbortError") {
241
+ updateStatus("Upload cancelled");
242
+ } else {
243
+ const errorMessage =
244
+ error instanceof Error ? error.message : String(error);
245
+ updateStatus(`Upload failed: ${errorMessage}`);
246
+ }
247
+ } finally {
248
+ setIsUploading(false);
249
+ setAbortController(null);
250
+ }
251
+ },
252
+ [
253
+ chunkSize,
254
+ maxConcurrentUploads,
255
+ baseUrl,
256
+ updateProgress,
257
+ updateStatus,
258
+ onComplete,
259
+ onError,
260
+ ]
261
+ );
262
+
263
+ const cancelUpload = useCallback(() => {
264
+ if (abortController) {
265
+ abortController.abort();
266
+ updateStatus("Cancelling upload...");
267
+ }
268
+ }, [abortController, updateStatus]);
269
+
270
+ const reset = useCallback(() => {
271
+ setUploadProgress({});
272
+ setUploadStatus("");
273
+ setIsUploading(false);
274
+ if (abortController) {
275
+ abortController.abort();
276
+ setAbortController(null);
277
+ }
278
+ }, [abortController]);
279
+
280
+ return {
281
+ uploadFiles,
282
+ isUploading,
283
+ uploadProgress,
284
+ uploadStatus,
285
+ cancelUpload,
286
+ reset,
287
+ };
288
+ }
289
+
290
+ // Alternative hook with simplified API for basic use cases
291
+ export function useSimpleUpload(baseUrl?: string) {
292
+ const { uploadFiles, isUploading, uploadStatus } = useChunkedUpload({
293
+ baseUrl,
294
+ chunkSize: 1024 * 1024, // 1MB
295
+ maxConcurrentUploads: 3,
296
+ });
297
+
298
+ return {
299
+ upload: uploadFiles,
300
+ isUploading,
301
+ status: uploadStatus,
302
+ };
303
+ }
@@ -5,7 +5,7 @@ export const primary = {
5
5
 
6
6
  export const popUp = {
7
7
  "pop-up-style":
8
- "w-full absolute overflow-y-auto border px-2 rounded-lg mt-[1px] scrollbar bg-white z-50 scrollbar h-auto dark:bg-black dark:text-white animate-fade-down animate-once animate-duration-200",
8
+ "overflow-y-auto px-2 border border-slate-200 rounded-lg scrollbar bg-white scrollbar h-auto dark:bg-black dark:text-white animate-fade-down animate-once animate-duration-200",
9
9
  };
10
10
 
11
11
  export const iconClass = {