@shipstatic/drop 0.1.5 → 0.1.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/dist/index.cjs CHANGED
@@ -11834,6 +11834,38 @@ function stripCommonPrefix(files) {
11834
11834
  path: f.path.startsWith(prefix) ? f.path.slice(prefix.length) : f.path
11835
11835
  }));
11836
11836
  }
11837
+ async function traverseFileTree(entry, files, currentPath = "") {
11838
+ if (entry.isFile) {
11839
+ const file = await new Promise((resolve, reject) => {
11840
+ entry.file(resolve, reject);
11841
+ });
11842
+ const relativePath = currentPath ? `${currentPath}/${file.name}` : file.name;
11843
+ Object.defineProperty(file, "webkitRelativePath", {
11844
+ value: relativePath,
11845
+ writable: false
11846
+ });
11847
+ files.push(file);
11848
+ } else if (entry.isDirectory) {
11849
+ const dirReader = entry.createReader();
11850
+ let allEntries = [];
11851
+ const readEntriesBatch = async () => {
11852
+ const batch = await new Promise(
11853
+ (resolve, reject) => {
11854
+ dirReader.readEntries(resolve, reject);
11855
+ }
11856
+ );
11857
+ if (batch.length > 0) {
11858
+ allEntries = allEntries.concat(batch);
11859
+ await readEntriesBatch();
11860
+ }
11861
+ };
11862
+ await readEntriesBatch();
11863
+ for (const childEntry of allEntries) {
11864
+ const entryPath = childEntry.isDirectory ? currentPath ? `${currentPath}/${childEntry.name}` : childEntry.name : currentPath;
11865
+ await traverseFileTree(childEntry, files, entryPath);
11866
+ }
11867
+ }
11868
+ }
11837
11869
  function useDrop(options) {
11838
11870
  const {
11839
11871
  ship: ship$1,
@@ -11841,26 +11873,32 @@ function useDrop(options) {
11841
11873
  onFilesReady,
11842
11874
  stripPrefix = true
11843
11875
  } = options;
11844
- const [files, setFiles] = react.useState([]);
11845
- const [sourceName, setSourceName] = react.useState("");
11846
- const [statusText, setStatusText] = react.useState("");
11847
- const [isProcessing, setIsProcessing] = react.useState(false);
11848
- const [validationError, setValidationError] = react.useState(null);
11876
+ const initialState = {
11877
+ value: "idle",
11878
+ files: [],
11879
+ sourceName: "",
11880
+ status: null
11881
+ };
11882
+ const [state, setState] = react.useState(initialState);
11849
11883
  const isProcessingRef = react.useRef(false);
11884
+ const inputRef = react.useRef(null);
11885
+ const isProcessing = react.useMemo(() => state.value === "processing", [state.value]);
11886
+ const isDragging = react.useMemo(() => state.value === "dragging", [state.value]);
11850
11887
  const processFiles = react.useCallback(async (newFiles) => {
11851
11888
  if (isProcessingRef.current) {
11852
11889
  console.warn("File processing already in progress. Ignoring duplicate call.");
11853
11890
  return;
11854
11891
  }
11855
11892
  if (!newFiles || newFiles.length === 0) {
11856
- setStatusText("No files selected.");
11857
11893
  return;
11858
11894
  }
11859
11895
  isProcessingRef.current = true;
11860
- setIsProcessing(true);
11861
- setFiles([]);
11862
- setValidationError(null);
11863
- setStatusText("Processing files...");
11896
+ setState({
11897
+ value: "processing",
11898
+ files: [],
11899
+ sourceName: "",
11900
+ status: { title: "Processing...", details: "Validating and preparing files." }
11901
+ });
11864
11902
  try {
11865
11903
  let detectedSourceName = "";
11866
11904
  if (newFiles.length === 1 && isZipFile(newFiles[0])) {
@@ -11873,12 +11911,14 @@ function useDrop(options) {
11873
11911
  detectedSourceName = newFiles[0].name;
11874
11912
  }
11875
11913
  }
11876
- setSourceName(detectedSourceName);
11877
11914
  const allFiles = [];
11878
11915
  const shouldExtractZip = newFiles.length === 1 && isZipFile(newFiles[0]);
11879
11916
  if (shouldExtractZip) {
11880
11917
  const zipFile = newFiles[0];
11881
- setStatusText(`Extracting ${zipFile.name}...`);
11918
+ setState((prev) => ({
11919
+ ...prev,
11920
+ status: { title: "Extracting...", details: `Extracting ${zipFile.name}...` }
11921
+ }));
11882
11922
  const { files: extractedFiles, errors } = await extractZipToFiles(zipFile);
11883
11923
  if (errors.length > 0) {
11884
11924
  console.warn("ZIP extraction errors:", errors);
@@ -11894,29 +11934,44 @@ function useDrop(options) {
11894
11934
  const filePaths = allFiles.map(getFilePath);
11895
11935
  const validPaths = new Set(ship.filterJunk(filePaths));
11896
11936
  const cleanFiles = allFiles.filter((f) => validPaths.has(getFilePath(f)));
11897
- setStatusText("Processing files...");
11937
+ setState((prev) => ({
11938
+ ...prev,
11939
+ status: { title: "Processing...", details: "Processing files..." }
11940
+ }));
11898
11941
  const processedFiles = await Promise.all(
11899
11942
  cleanFiles.map((file) => createProcessedFile(file))
11900
11943
  );
11901
11944
  const finalFiles = stripPrefix ? stripCommonPrefix(processedFiles) : processedFiles;
11902
11945
  const config = await ship$1.getConfig();
11903
11946
  const validation = ship.validateFiles(finalFiles, config);
11904
- setFiles(validation.files);
11905
- setValidationError(validation.error);
11906
11947
  if (validation.error) {
11907
- setStatusText(validation.error.details);
11948
+ setState({
11949
+ value: "error",
11950
+ files: validation.files,
11951
+ sourceName: detectedSourceName,
11952
+ status: { title: validation.error.error, details: validation.error.details }
11953
+ });
11908
11954
  onValidationError?.(validation.error);
11909
11955
  } else if (validation.validFiles.length > 0) {
11910
- setStatusText(`${validation.validFiles.length} file(s) ready.`);
11956
+ setState({
11957
+ value: "ready",
11958
+ files: validation.files,
11959
+ sourceName: detectedSourceName,
11960
+ status: { title: "Ready", details: `${validation.validFiles.length} file(s) are ready.` }
11961
+ });
11911
11962
  onFilesReady?.(validation.validFiles);
11912
11963
  } else {
11913
11964
  const noValidError = {
11914
11965
  error: "No Valid Files",
11915
- details: "No files are valid for upload after processing.",
11966
+ details: "None of the provided files could be processed.",
11916
11967
  isClientError: true
11917
11968
  };
11918
- setStatusText(noValidError.details);
11919
- setValidationError(noValidError);
11969
+ setState({
11970
+ value: "error",
11971
+ files: validation.files,
11972
+ sourceName: detectedSourceName,
11973
+ status: { title: noValidError.error, details: noValidError.details }
11974
+ });
11920
11975
  onValidationError?.(noValidError);
11921
11976
  }
11922
11977
  } catch (error) {
@@ -11925,38 +11980,112 @@ function useDrop(options) {
11925
11980
  details: `Failed to process files: ${error instanceof Error ? error.message : String(error)}`,
11926
11981
  isClientError: true
11927
11982
  };
11928
- setStatusText(processingError.details);
11929
- setValidationError(processingError);
11983
+ setState((prev) => ({
11984
+ ...prev,
11985
+ value: "error",
11986
+ status: { title: processingError.error, details: processingError.details }
11987
+ }));
11930
11988
  onValidationError?.(processingError);
11931
11989
  } finally {
11932
11990
  isProcessingRef.current = false;
11933
- setIsProcessing(false);
11934
11991
  }
11935
11992
  }, [ship$1, onValidationError, onFilesReady, stripPrefix]);
11936
11993
  const clearAll = react.useCallback(() => {
11937
- setFiles([]);
11938
- setSourceName("");
11939
- setStatusText("");
11940
- setValidationError(null);
11994
+ setState(initialState);
11941
11995
  isProcessingRef.current = false;
11942
- setIsProcessing(false);
11943
11996
  }, []);
11944
11997
  const getValidFilesCallback = react.useCallback(() => {
11945
- return getValidFiles(files);
11946
- }, [files]);
11947
- const updateFileStatus = react.useCallback((fileId, state) => {
11948
- setFiles((prev) => prev.map(
11949
- (file) => file.id === fileId ? { ...file, ...state } : file
11950
- ));
11998
+ return getValidFiles(state.files);
11999
+ }, [state.files]);
12000
+ const updateFileStatus = react.useCallback((fileId, fileState) => {
12001
+ setState((prev) => ({
12002
+ ...prev,
12003
+ files: prev.files.map(
12004
+ (file) => file.id === fileId ? { ...file, ...fileState } : file
12005
+ )
12006
+ }));
12007
+ }, []);
12008
+ const handleDragOver = react.useCallback((e) => {
12009
+ e.preventDefault();
12010
+ setState((prev) => {
12011
+ if (prev.value === "idle" || prev.value === "ready" || prev.value === "error") {
12012
+ return { ...prev, value: "dragging" };
12013
+ }
12014
+ return prev;
12015
+ });
12016
+ }, []);
12017
+ const handleDragLeave = react.useCallback((e) => {
12018
+ e.preventDefault();
12019
+ setState((prev) => {
12020
+ if (prev.value !== "dragging") return prev;
12021
+ const nextValue = prev.files.length > 0 ? prev.status?.title === "Ready" ? "ready" : "error" : "idle";
12022
+ return { ...prev, value: nextValue };
12023
+ });
12024
+ }, []);
12025
+ const handleDrop = react.useCallback(async (e) => {
12026
+ e.preventDefault();
12027
+ const items = Array.from(e.dataTransfer.items);
12028
+ const files = [];
12029
+ let hasEntries = false;
12030
+ for (const item of items) {
12031
+ if (item.kind === "file") {
12032
+ const entry = item.webkitGetAsEntry?.();
12033
+ if (entry) {
12034
+ hasEntries = true;
12035
+ await traverseFileTree(
12036
+ entry,
12037
+ files,
12038
+ entry.isDirectory ? entry.name : ""
12039
+ );
12040
+ }
12041
+ }
12042
+ }
12043
+ if (!hasEntries && e.dataTransfer.files.length > 0) {
12044
+ files.push(...Array.from(e.dataTransfer.files));
12045
+ }
12046
+ if (files.length > 0) {
12047
+ await processFiles(files);
12048
+ } else if (state.value === "dragging") {
12049
+ setState((prev) => ({ ...prev, value: "idle" }));
12050
+ }
12051
+ }, [processFiles, state.value]);
12052
+ const handleInputChange = react.useCallback((e) => {
12053
+ const files = Array.from(e.target.files || []);
12054
+ if (files.length > 0) {
12055
+ processFiles(files);
12056
+ }
12057
+ }, [processFiles]);
12058
+ const open = react.useCallback(() => {
12059
+ inputRef.current?.click();
11951
12060
  }, []);
12061
+ const getDropzoneProps = react.useCallback(() => ({
12062
+ onDragOver: handleDragOver,
12063
+ onDragLeave: handleDragLeave,
12064
+ onDrop: handleDrop,
12065
+ onClick: open
12066
+ }), [handleDragOver, handleDragLeave, handleDrop, open]);
12067
+ const getInputProps = react.useCallback(() => ({
12068
+ ref: inputRef,
12069
+ type: "file",
12070
+ style: { display: "none" },
12071
+ multiple: true,
12072
+ webkitdirectory: "",
12073
+ onChange: handleInputChange
12074
+ }), [handleInputChange]);
11952
12075
  return {
11953
- files,
11954
- sourceName,
11955
- statusText,
12076
+ // State machine
12077
+ state,
12078
+ // Convenience getters (computed from state)
11956
12079
  isProcessing,
11957
- validationError,
12080
+ isDragging,
12081
+ // Primary API: Prop getters
12082
+ getDropzoneProps,
12083
+ getInputProps,
12084
+ // Actions
12085
+ open,
11958
12086
  processFiles,
11959
12087
  clearAll,
12088
+ // Helpers
11960
12089
  getValidFiles: getValidFilesCallback,
11961
12090
  updateFileStatus
11962
12091
  };