This commit is contained in:
2026-03-16 23:31:28 +01:00
parent ed18993aeb
commit 30e69ccbb3
4 changed files with 5 additions and 538 deletions

View File

@@ -1,333 +0,0 @@
import { createSlice } from "@reduxjs/toolkit";
//import { addLoadedFiles } from "./slices/details-slice";
import fetchUserProfile from "./fetch-async/fetchUserProfile";
import {
BASE_URL,
METHOD_POST_QUERY,
PREFIX_DOCUMENT,
PREFIX_UPLOAD_STREAM,
FORM_DATA_FILES,
DEFAULT_ERROR_STATUS,
PROGRESS_STATUS_COMPLETED,
PROGRESS_STATUS_ERROR,
ERROR_NO_RESPONSE,
UNKNOWN_ERROR,
MAX_FILES_TO_UPLOAD,
} from "./constants";
const initialState = {
uploading: false,
error: {
status: null,
message: "",
},
success: false,
areFilesValid: true,
quotaError: null,
uploadedFiles: [],
progress: {
percent: 0,
processedFiles: 0,
totalFiles: 0,
currentFile: "",
status: "",
},
};
// Store AbortController outside Redux state
let currentAbortController = null;
const uploadFilesSlice = createSlice({
name: "uploadFiles",
initialState,
reducers: {
resetUploadState() {
// Abort any ongoing upload when resetting
if (currentAbortController) {
currentAbortController.abort();
currentAbortController = null;
}
return initialState;
},
setAreFilesValid(state, action) {
state.areFilesValid = action.payload;
},
setQuotaError(state, action) {
state.quotaError = action.payload;
},
clearQuotaError(state) {
state.quotaError = null;
},
uploadStarted(state, action) {
state.uploading = true;
state.error = { status: null, message: "" };
state.success = false;
state.quotaError = null;
state.progress = {
percent: 0,
processedFiles: 0,
totalFiles: action.payload.totalFiles,
currentFile: "",
status: "starting",
};
},
progressUpdated(state, action) {
state.progress = action.payload;
},
uploadCompleted(state, action) {
state.uploading = false;
state.success = true;
state.uploadedFiles = action.payload.fileNames;
},
uploadFailed(state, action) {
state.uploading = false;
state.success = false;
state.error = {
status: action.payload.status ?? DEFAULT_ERROR_STATUS,
message: action.payload.message ?? UNKNOWN_ERROR,
};
},
uploadCancelled(state) {
state.uploading = false;
state.success = false;
state.error = { status: null, message: "" };
state.progress = initialState.progress;
},
},
});
export const {
resetUploadState,
setAreFilesValid,
setQuotaError,
clearQuotaError,
uploadStarted,
progressUpdated,
uploadCompleted,
uploadFailed,
uploadCancelled,
} = uploadFilesSlice.actions;
export const cancelUpload = () => (dispatch) => {
if (currentAbortController) {
currentAbortController.abort();
currentAbortController = null;
}
dispatch(uploadCancelled());
};
/**
* Validate files against quota limits
* Returns { valid: boolean, error: string | null }
*/
export const validateFilesQuota = (files) => (dispatch, getState) => {
const state = getState();
const loadedFiles = state.userDetails?.loadedFiles || [];
const maxFilesToLoad = state.userDetails?.maxFilesToLoad || 0;
const loadedCount = loadedFiles.length;
const remainingSlots = Math.max(0, maxFilesToLoad - loadedCount);
const filesCount = files.length;
// Check max files per upload
if (filesCount > MAX_FILES_TO_UPLOAD) {
const errorMsg = `You can upload maximum ${MAX_FILES_TO_UPLOAD} files at once`;
dispatch(setQuotaError(errorMsg));
dispatch(setAreFilesValid(false));
return { valid: false, error: errorMsg };
}
// Check remaining slots
if (filesCount > remainingSlots) {
const errorMsg =
remainingSlots === 0
? `You have reached the maximum number of files (${maxFilesToLoad})`
: `You can only upload ${remainingSlots} more file(s). Already loaded: ${loadedCount}/${maxFilesToLoad}`;
dispatch(setQuotaError(errorMsg));
dispatch(setAreFilesValid(false));
return { valid: false, error: errorMsg };
}
dispatch(clearQuotaError());
dispatch(setAreFilesValid(true));
return { valid: true, error: null };
};
/**
* Check if user can upload files (has remaining slots)
*/
export const canUploadFiles = () => (dispatch, getState) => {
const state = getState();
const loadedFiles = state.userDetails?.loadedFiles || [];
const maxFilesToLoad = state.userDetails?.maxFilesToLoad || 0;
const remainingSlots = Math.max(0, maxFilesToLoad - loadedFiles.length);
return remainingSlots > 0;
};
/**
* Get remaining upload slots
*/
export const getRemainingSlots = () => (dispatch, getState) => {
const state = getState();
const loadedFiles = state.userDetails?.loadedFiles || [];
const maxFilesToLoad = state.userDetails?.maxFilesToLoad || 0;
return Math.max(0, maxFilesToLoad - loadedFiles.length);
};
/**
* Thunk for uploading files with SSE progress streaming.
*/
export const uploadFilesWithProgress =
(files) => async (dispatch, getState) => {
const filesArray = Array.from(files);
const fileNames = filesArray.map((f) => f.name);
// Validate quota before upload
const state = getState();
const loadedFiles = state.userDetails?.loadedFiles || [];
const maxFilesToLoad = state.userDetails?.maxFilesToLoad || 0;
const loadedCount = loadedFiles.length;
const remainingSlots = Math.max(0, maxFilesToLoad - loadedCount);
// Check max files per upload
if (filesArray.length > MAX_FILES_TO_UPLOAD) {
dispatch(
uploadFailed({
status: 400,
message: `You can upload maximum ${MAX_FILES_TO_UPLOAD} files at once`,
})
);
return;
}
// Check remaining slots
if (filesArray.length > remainingSlots) {
dispatch(
uploadFailed({
status: 400,
message:
remainingSlots === 0
? `You have reached the maximum number of files (${maxFilesToLoad})`
: `You can only upload ${remainingSlots} more file(s). Already loaded: ${loadedCount}/${maxFilesToLoad}`,
})
);
return;
}
// Create new AbortController for this upload
currentAbortController = new AbortController();
const signal = currentAbortController.signal;
dispatch(uploadStarted({ totalFiles: filesArray.length }));
const formData = new FormData();
filesArray.forEach((file) => {
formData.append(FORM_DATA_FILES, file);
});
const token = state.userDetails?.token;
const headers = {};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
try {
const response = await fetch(
`${BASE_URL}${PREFIX_DOCUMENT}${PREFIX_UPLOAD_STREAM}`,
{
method: METHOD_POST_QUERY,
headers,
body: formData,
signal,
}
);
if (!response.ok) {
dispatch(
uploadFailed({
status: response.status,
message: `Server error: ${response.statusText}`,
})
);
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
// Check if aborted before reading
if (signal.aborted) {
reader.cancel();
return;
}
const { done, value } = await reader.read();
if (done) {
break;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data:")) {
const jsonStr = line.slice(5).trim();
if (jsonStr && jsonStr.startsWith("{")) {
try {
const progressData = JSON.parse(jsonStr);
dispatch(progressUpdated(progressData));
if (progressData.status === PROGRESS_STATUS_COMPLETED) {
dispatch(uploadCompleted({ fileNames }));
dispatch(fetchUserProfile());
//dispatch(addLoadedFiles(fileNames));
} else if (progressData.status === PROGRESS_STATUS_ERROR) {
dispatch(
uploadFailed({
status: DEFAULT_ERROR_STATUS,
message: `Error processing file: ${progressData.currentFile}`,
})
);
}
} catch (parseError) {
console.error("Failed to parse SSE data:", parseError);
}
}
}
}
}
// Ensure completion if stream ended without explicit completed status
const currentState = getState();
if (currentState.uploadFiles.uploading) {
dispatch(uploadCompleted({ fileNames }));
dispatch(addLoadedFiles(fileNames));
}
} catch (err) {
// Don't dispatch error if it was an intentional abort
if (err.name === "AbortError") {
console.log("Upload cancelled by user");
return;
}
console.error("Upload stream error:", err);
dispatch(
uploadFailed({
status: DEFAULT_ERROR_STATUS,
message: `${ERROR_NO_RESPONSE}: ${err.message}`,
})
);
} finally {
currentAbortController = null;
}
};
//export default uploadFilesSlice.reducer;

View File

@@ -1,199 +0,0 @@
import { createSlice } from "@reduxjs/toolkit";
import fetchLoginUser from "./fetch-async/fetchLoginUser";
import fetchRegisterUser from "./fetch-async/fetchRegisterUser";
import fetchRefreshToken from "./fetch-async/fetchRefreshToken";
import fetchUserProfile from "./fetch-async/fetchUserProfile";
import fetchDeleteUploaded from "./fetch-async/fetchDeleteUploaded";
import {
USER_NAME_UNDEFINED,
USER_EMAIL_UNDEFINED,
JWT_TOKEN,
JWT_REFRESH_TOKEN,
TYPE_WINDOW_UNDEFINED,
TOKEN_UNDEFINED,
UNKNOWN_ERROR,
} from "./constants";
const getInitialToken = () => {
if (typeof window === TYPE_WINDOW_UNDEFINED) return null;
const token = localStorage.getItem(JWT_TOKEN);
if (!token || token === TOKEN_UNDEFINED) return null;
return token;
};
const initialState = {
userId: null,
userName: USER_NAME_UNDEFINED,
userEmail: USER_EMAIL_UNDEFINED,
loadedFiles: [],
maxFilesToLoad: 0,
token: getInitialToken(),
refreshToken: localStorage.getItem(JWT_REFRESH_TOKEN),
loading: false,
deleting: false,
deleteSuccess: false,
deletedCount: 0,
error: null,
};
const userDetailsSlice = createSlice({
name: "userDetails",
initialState,
reducers: {
logoutUser(state) {
state.userId = null;
state.userName = USER_NAME_UNDEFINED;
state.userEmail = USER_EMAIL_UNDEFINED;
state.loadedFiles = [];
state.maxFilesToLoad = 0;
state.token = null;
state.refreshToken = null;
state.loading = false;
state.deleting = false;
state.deleteSuccess = false;
state.deletedCount = 0;
state.error = null;
localStorage.removeItem(JWT_TOKEN);
localStorage.removeItem(JWT_REFRESH_TOKEN);
},
clearError(state) {
state.error = null;
},
clearDeleteStatus(state) {
state.deleteSuccess = false;
state.deletedCount = 0;
},
addLoadedFiles(state, action) {
state.loadedFiles = [...state.loadedFiles, ...action.payload];
},
},
extraReducers: (builder) => {
builder.addCase(fetchRegisterUser.pending, (state) => {
state.loading = true;
state.error = null;
});
builder.addCase(fetchRegisterUser.fulfilled, (state, action) => {
state.loading = false;
state.error = null;
state.userName = action.payload.payload.username;
state.userEmail = action.payload.payload.email;
const token = action.payload.payload.token;
state.token = token || null;
if (token) {
localStorage.setItem(JWT_TOKEN, token);
} else {
localStorage.removeItem(JWT_TOKEN);
}
const refreshToken = action.payload.payload.refreshToken;
state.refreshToken = refreshToken || null;
if (refreshToken) {
localStorage.setItem(JWT_REFRESH_TOKEN, refreshToken);
} else {
localStorage.removeItem(JWT_REFRESH_TOKEN);
}
});
builder.addCase(fetchRegisterUser.rejected, (state, action) => {
state.loading = false;
state.error = action.payload?.status ?? 500;
});
builder.addCase(fetchLoginUser.pending, (state) => {
state.loading = true;
state.error = null;
});
builder.addCase(fetchLoginUser.fulfilled, (state, action) => {
state.loading = false;
state.error = null;
state.userName = action.payload.payload.username;
state.userEmail = action.payload.payload.email;
const token = action.payload.payload.token;
state.token = token || null;
if (token) {
localStorage.setItem(JWT_TOKEN, token);
} else {
localStorage.removeItem(JWT_TOKEN);
}
const refreshToken = action.payload.payload.refreshToken;
state.refreshToken = refreshToken || null;
if (refreshToken) {
localStorage.setItem(JWT_REFRESH_TOKEN, refreshToken);
} else {
localStorage.removeItem(JWT_REFRESH_TOKEN);
}
});
builder.addCase(fetchLoginUser.rejected, (state, action) => {
state.loading = false;
state.error = action.payload ?? { status: 500, message: "Unknown error" };
});
builder.addCase(fetchRefreshToken.fulfilled, (state, action) => {
const token = action.payload?.payload?.token;
const refreshToken = action.payload?.payload?.refreshToken;
state.token = token || null;
state.refreshToken = refreshToken || null;
if (token) localStorage.setItem(JWT_TOKEN, token);
else localStorage.removeItem(JWT_TOKEN);
if (refreshToken) localStorage.setItem(JWT_REFRESH_TOKEN, refreshToken);
else localStorage.removeItem(JWT_REFRESH_TOKEN);
});
builder.addCase(fetchRefreshToken.rejected, (state, action) => {
state.error = action.payload ?? { status: 500, message: UNKNOWN_ERROR };
});
builder.addCase(fetchUserProfile.pending, (state) => {
state.loading = true;
state.error = null;
});
builder.addCase(fetchUserProfile.fulfilled, (state, action) => {
state.loading = false;
state.error = null;
const p = action.payload?.payload;
if (p?.id) state.userId = p.id;
if (p?.username) state.userName = p.username;
if (p?.email) state.userEmail = p.email;
if (p?.loadedFiles) state.loadedFiles = p.loadedFiles;
if (p?.maxLoadedFiles != null) state.maxFilesToLoad = p.maxLoadedFiles;
});
builder.addCase(fetchUserProfile.rejected, (state, action) => {
state.loading = false;
state.error = action.payload ?? { status: 500, message: "Unknown error" };
});
// Delete uploaded files
builder.addCase(fetchDeleteUploaded.pending, (state) => {
state.deleting = true;
state.deleteSuccess = false;
state.deletedCount = 0;
state.error = null;
});
builder.addCase(fetchDeleteUploaded.fulfilled, (state, action) => {
state.deleting = false;
state.deleteSuccess = true;
state.deletedCount = action.payload?.payload ?? 0;
state.loadedFiles = [];
state.error = null;
});
builder.addCase(fetchDeleteUploaded.rejected, (state, action) => {
state.deleting = false;
state.deleteSuccess = false;
state.error = action.payload ?? { status: 500, message: "Unknown error" };
});
},
});
//export const { logoutUser, clearError, clearDeleteStatus, addLoadedFiles } = userDetailsSlice.actions;
//export default userDetailsSlice.reducer;