Progress update cleanup and UI improvements.
This commit is contained in:
@@ -13,6 +13,7 @@ function JobTabLayout(props) {
|
||||
tabBarActiveTintColor: theme.colors.primary,
|
||||
tabBarPosition: "top",
|
||||
headerShown: false,
|
||||
animation: "shift",
|
||||
tabBarStyle: {
|
||||
marginTop: -50,
|
||||
},
|
||||
|
||||
@@ -1,31 +1,5 @@
|
||||
import SignOutButton from "@/components-old/sign-out-button/sign-out-button.component";
|
||||
import { selectBodyshop } from "@/redux/user/user.selectors";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import Settings from "../components/settings/settings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(Tab);
|
||||
|
||||
function Tab({ bodyshop }) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>Tab [Home|Settings]</Text>
|
||||
<Text>
|
||||
Using Local Media Server? {bodyshop?.uselocalmediaserver ? "Yes" : "No"}
|
||||
</Text>
|
||||
<SignOutButton />
|
||||
</View>
|
||||
);
|
||||
export default function Tab() {
|
||||
return <Settings />;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import { Checkbox, Switch } from "react-native-paper";
|
||||
import { Switch } from "react-native-paper";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { toggleDeleteAfterUpload } from "../../redux/app/app.actions";
|
||||
@@ -19,7 +18,7 @@ export function UploadDeleteSwitch({
|
||||
deleteAfterUpload,
|
||||
toggleDeleteAfterUpload,
|
||||
}) {
|
||||
console.log("*** ~ deleteAfterUpload:", deleteAfterUpload);
|
||||
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Text } from "react-native";
|
||||
import { Card } from "react-native-paper";
|
||||
import { Button, Card } from "react-native-paper";
|
||||
|
||||
export default function ErrorDisplay({ errorMessage, error }) {
|
||||
export default function ErrorDisplay({ errorMessage, error, onDismiss }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Card style={{ margin: 8, backgroundColor: "#ffdddd" }}>
|
||||
@@ -14,6 +14,11 @@ export default function ErrorDisplay({ errorMessage, error }) {
|
||||
error ||
|
||||
"An unknown error has occured."}
|
||||
</Text>
|
||||
{onDismiss ? (
|
||||
<Card.Actions>
|
||||
<Button onPress={onDismiss}>{t("general.labels.dismiss")}</Button>
|
||||
</Card.Actions>
|
||||
) : null}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
View,
|
||||
} from "react-native";
|
||||
import ImageView from "react-native-image-viewing";
|
||||
import { ActivityIndicator } from "react-native-paper";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import env from "../../env";
|
||||
@@ -119,6 +120,7 @@ export function JobDocumentsComponent({ bodyshop }) {
|
||||
getPhotos();
|
||||
}, [getPhotos]);
|
||||
|
||||
if (loading) return <ActivityIndicator style={{ flex: 1 }} size="large" />;
|
||||
if (error) {
|
||||
return <ErrorDisplay message={JSON.stringify(error)} />;
|
||||
}
|
||||
|
||||
@@ -17,14 +17,13 @@ export default function JobLines() {
|
||||
skip: !jobId,
|
||||
});
|
||||
|
||||
console.log("*** ~ JobLines ~ error:", error);
|
||||
const { t } = useTranslation();
|
||||
const onRefresh = async () => {
|
||||
return refetch();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <ActivityIndicator />;
|
||||
return <ActivityIndicator size="large" style={{ flex: 1 }} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function JobNotes() {
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <ActivityIndicator />;
|
||||
return <ActivityIndicator size="large" style={{ flex: 1 }} />;
|
||||
}
|
||||
if (error) {
|
||||
return <ErrorDisplay message={JSON.stringify(error?.message)} />;
|
||||
|
||||
@@ -23,14 +23,13 @@ export default function JobTombstone() {
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
console.log("*** ~ JobTombstone ~ theme:", theme.colors);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const onRefresh = async () => {
|
||||
return refetch();
|
||||
};
|
||||
if (loading) {
|
||||
return <ActivityIndicator />;
|
||||
return <ActivityIndicator size="large" style={{ flex: 1 }} />;
|
||||
}
|
||||
if (!data.jobs_by_pk) {
|
||||
return (
|
||||
|
||||
@@ -133,7 +133,12 @@ const styles = StyleSheet.create({
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
backdropFilter: "blur(20px)", // web only
|
||||
},
|
||||
cardContents: { flex: 1, flexDirection: "row", display: "flex" },
|
||||
cardContents: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
|
||||
56
components/settings/settings.jsx
Normal file
56
components/settings/settings.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import SignOutButton from "@/components-old/sign-out-button/sign-out-button.component";
|
||||
import { toggleDeleteAfterUpload } from "@/redux/app/app.actions";
|
||||
import { selectDeleteAfterUpload } from "@/redux/app/app.selectors";
|
||||
import { selectBodyshop } from "@/redux/user/user.selectors";
|
||||
import { formatBytes } from "@/util/uploadUtils";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { Button, Divider, Text } from "react-native-paper";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import UploadDeleteSwitch from "./upload-delete-switch";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
deleteAfterUpload: selectDeleteAfterUpload,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleDeleteAfterUpload: () => dispatch(toggleDeleteAfterUpload()),
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Tab);
|
||||
|
||||
function Tab({ bodyshop, deleteAfterUpload, toggleDeleteAfterUpload }) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text variant="titleLarge">Settings</Text>
|
||||
<Text>
|
||||
Media Storage:{" "}
|
||||
{bodyshop?.uselocalmediaserver
|
||||
? bodyshop.localmediaserverhttp
|
||||
: "Cloud"}
|
||||
</Text>
|
||||
{!bodyshop?.uselocalmediaserver && (
|
||||
<Text>Job Size Limit: {formatBytes(bodyshop?.jobsizelimit)}</Text>
|
||||
)}
|
||||
<UploadDeleteSwitch />
|
||||
<Button
|
||||
onPress={() => {
|
||||
AsyncStorage.removeItem("persist:root");
|
||||
}}
|
||||
>
|
||||
Clear Storage
|
||||
</Button>
|
||||
<Divider />
|
||||
<SignOutButton />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
});
|
||||
53
components/settings/upload-delete-switch.jsx
Normal file
53
components/settings/upload-delete-switch.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import { Switch } from "react-native-paper";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { toggleDeleteAfterUpload } from "../../redux/app/app.actions";
|
||||
import { selectDeleteAfterUpload } from "../../redux/app/app.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
deleteAfterUpload: selectDeleteAfterUpload,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleDeleteAfterUpload: () => dispatch(toggleDeleteAfterUpload()),
|
||||
});
|
||||
|
||||
export function UploadDeleteSwitch({
|
||||
deleteAfterUpload,
|
||||
toggleDeleteAfterUpload,
|
||||
}) {
|
||||
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.text}>
|
||||
{t("mediabrowser.labels.deleteafterupload")}
|
||||
</Text>
|
||||
<Switch
|
||||
// trackColor={{ false: '#767577', true: '#81b0ff' }}
|
||||
// thumbColor={deleteAfterUpload ? 'tomato' : '#f4f3f4'}
|
||||
//ios_backgroundColor="#3e3e3e"
|
||||
onValueChange={() => {
|
||||
toggleDeleteAfterUpload();
|
||||
}}
|
||||
value={deleteAfterUpload}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
margin: 10,
|
||||
},
|
||||
|
||||
text: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UploadDeleteSwitch);
|
||||
@@ -1,28 +1,53 @@
|
||||
import { clearUploadError } from "@/redux/photos/photos.actions";
|
||||
import theme from "@/util/theme";
|
||||
import { formatBytes } from "@/util/uploadUtils";
|
||||
import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
|
||||
import { ProgressBar } from "react-native-paper";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { ProgressBar, Text } from "react-native-paper";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
selectPhotos,
|
||||
selectUploadProgress,
|
||||
selectPhotos,
|
||||
selectUploadError,
|
||||
selectUploadProgress,
|
||||
} from "../../redux/photos/photos.selectors";
|
||||
import ErrorDisplay from "../error/error-display";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
photos: selectPhotos,
|
||||
photoUploadProgress: selectUploadProgress,
|
||||
uploadError: selectUploadError,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
clearError: () => dispatch(clearUploadError()),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(UploadProgress);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UploadProgress);
|
||||
|
||||
export function UploadProgress({ photos, photoUploadProgress }) {
|
||||
export function UploadProgress({
|
||||
photos,
|
||||
photoUploadProgress,
|
||||
uploadError,
|
||||
clearError,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (photos?.length === 0) return null;
|
||||
if (uploadError)
|
||||
return <ErrorDisplay error={uploadError} onDismiss={clearError} />;
|
||||
return (
|
||||
<View style={styles.modalContainer}>
|
||||
<View style={styles.modal}>
|
||||
<Text variant="titleLarge" style={styles.title}>
|
||||
{t("general.labels.uploadprogress")}
|
||||
</Text>
|
||||
|
||||
{Object.keys(photoUploadProgress).map((key) => (
|
||||
<View key={key} style={styles.progressItem}>
|
||||
<Text style={styles.progressText}>
|
||||
<Text
|
||||
style={styles.progressText}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{photoUploadProgress[key].fileName}
|
||||
</Text>
|
||||
<View style={styles.progressBarContainer}>
|
||||
@@ -42,38 +67,14 @@ export function UploadProgress({ photos, photoUploadProgress }) {
|
||||
>
|
||||
<Text>{`${formatBytes(
|
||||
photoUploadProgress[key].loaded /
|
||||
(((photoUploadProgress[key].uploadEnd || new Date()) -
|
||||
photoUploadProgress[key].uploadStart) /
|
||||
(((photoUploadProgress[key].endTime || new Date()) -
|
||||
photoUploadProgress[key].startTime) /
|
||||
1000)
|
||||
)}/sec`}</Text>
|
||||
{photoUploadProgress[key].percent === 1 && (
|
||||
<>
|
||||
<ActivityIndicator style={{ marginLeft: 12 }} />
|
||||
<Text style={{ marginLeft: 4 }}>Processing...</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
<View style={styles.centeredView}>
|
||||
{
|
||||
// progress.statusText ? (
|
||||
// <>
|
||||
// <ActivityIndicator style={{ marginLeft: 12 }} />
|
||||
// <Text style={{ marginLeft: 4 }}>{progress.statusText}</Text>
|
||||
// <Divider />
|
||||
// </>
|
||||
// ) : (
|
||||
// <>
|
||||
// <Text>{`${progress.totalFilesCompleted} of ${progress.totalFiles} uploaded.`}</Text>
|
||||
// <Text>{`${formatBytes(progress.totalUploaded)} of ${formatBytes(
|
||||
// progress.totalToUpload
|
||||
// )} uploaded.`}</Text>
|
||||
// </>
|
||||
// )
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -81,17 +82,19 @@ export function UploadProgress({ photos, photoUploadProgress }) {
|
||||
const styles = StyleSheet.create({
|
||||
modalContainer: {
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
// flex: 1,
|
||||
marginTop: 14,
|
||||
marginBottom: 14,
|
||||
justifyContent: "center",
|
||||
},
|
||||
modal: {
|
||||
//flex: 1,
|
||||
display: "flex",
|
||||
marginLeft: 20,
|
||||
marginRight: 20,
|
||||
backgroundColor: "white",
|
||||
marginLeft: 12,
|
||||
marginRight: 12,
|
||||
backgroundColor: theme.colors.elevation.level3,
|
||||
borderRadius: 20,
|
||||
padding: 18,
|
||||
paddingTop: 12,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
@@ -101,6 +104,13 @@ const styles = StyleSheet.create({
|
||||
shadowRadius: 4,
|
||||
elevation: 5,
|
||||
},
|
||||
title: {
|
||||
alignSelf: "center",
|
||||
alignItems: "center",
|
||||
marginBottom: 12,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
},
|
||||
centeredView: {
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
@@ -116,6 +126,12 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
progressText: {
|
||||
flex: 1,
|
||||
flexShrink: 1, // allow shrinking so ellipsis can appear
|
||||
minWidth: 0, // ensures proper shrinking inside a flex row (especially on web)
|
||||
// (Optional) If you find web still not clipping, you can uncomment the next lines:
|
||||
// overflow: 'hidden',
|
||||
// textOverflow: 'ellipsis',
|
||||
// whiteSpace: 'nowrap',
|
||||
},
|
||||
progressBarContainer: {
|
||||
flex: 3,
|
||||
|
||||
@@ -31,7 +31,25 @@ export const mediaUploadProgressBulk = (info) => ({
|
||||
});
|
||||
|
||||
export const mediaUploadCompleted = (photo) => ({
|
||||
type: PhotosActionTypes.MEDIA_UPLOAD_COMPLETED
|
||||
//payload: photo,
|
||||
type: PhotosActionTypes.MEDIA_UPLOAD_COMPLETED,
|
||||
payload: photo,
|
||||
});
|
||||
|
||||
export const deleteMedia = (photos) => ({
|
||||
type: PhotosActionTypes.DELETE_MEDIA,
|
||||
payload: photos,
|
||||
});
|
||||
|
||||
export const deleteMediaSuccess = (photos) => ({
|
||||
type: PhotosActionTypes.DELETE_MEDIA_SUCCESS,
|
||||
payload: photos,
|
||||
});
|
||||
|
||||
export const deleteMediaFailure = (photos) => ({
|
||||
type: PhotosActionTypes.DELETE_MEDIA_FAILURE,
|
||||
payload: photos,
|
||||
});
|
||||
export const clearUploadError = () => ({
|
||||
type: PhotosActionTypes.CLEAR_UPLOAD_ERROR,
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const photosReducer = (state = INITIAL_STATE, action) => {
|
||||
case PhotosActionTypes.MEDIA_UPLOAD_SUCCESS_ONE:
|
||||
return {
|
||||
...state,
|
||||
progress: { ...state.progress, [action.payload.assetId]: { ...state.progress[action.payload.assetId], progress: 100, status: 'completed' } }
|
||||
progress: { ...state.progress, [action.payload.assetId]: { ...state.progress[action.payload.assetId], progress: 100, status: 'completed', endTime: new Date() } }
|
||||
};
|
||||
case PhotosActionTypes.MEDIA_UPLOAD_PROGRESS_UPDATE_BULK:
|
||||
return {
|
||||
@@ -48,6 +48,12 @@ const photosReducer = (state = INITIAL_STATE, action) => {
|
||||
photos: [],
|
||||
progress: {}
|
||||
};
|
||||
case PhotosActionTypes.CLEAR_UPLOAD_ERROR:
|
||||
return {
|
||||
...state,
|
||||
photos: [], progress: {},
|
||||
uploadError: null,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import axios from "axios";
|
||||
import Constants from "expo-constants";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import * as MediaLibrary from "expo-media-library";
|
||||
import moment from 'moment';
|
||||
import { all, call, delay, fork, put, select, takeEvery, takeLatest } from "redux-saga/effects";
|
||||
import { all, call, delay, put, select, takeEvery, takeLatest } from "redux-saga/effects";
|
||||
import env from "../../env";
|
||||
import { client } from '../../graphql/client';
|
||||
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
||||
import { GET_DOC_SIZE_TOTALS, INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
||||
import { axiosAuthInterceptorId } from "../../util/CleanAxios";
|
||||
import { fetchImageFromUri, replaceAccents } from '../../util/uploadUtils';
|
||||
import { selectBodyshop, selectCurrentUser } from "../user/user.selectors";
|
||||
import {
|
||||
deleteMediaSuccess,
|
||||
mediaUploadCompleted,
|
||||
mediaUploadFailure,
|
||||
mediaUploadProgressBulk,
|
||||
@@ -18,14 +20,40 @@ import {
|
||||
mediaUploadSuccessOne
|
||||
} from "./photos.actions";
|
||||
|
||||
import i18n from "@/translations/i18n";
|
||||
import { Platform } from "react-native";
|
||||
import { selectDeleteAfterUpload } from "../app/app.selectors";
|
||||
import PhotosActionTypes from "./photos.types";
|
||||
|
||||
axios.interceptors.request.use(
|
||||
function (config) {
|
||||
config.metadata = { startTime: new Date() };
|
||||
return config;
|
||||
},
|
||||
function (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
axios.interceptors.response.use(
|
||||
function (response) {
|
||||
response.config.metadata.endTime = new Date();
|
||||
response.duration =
|
||||
response.config.metadata.endTime - response.config.metadata.startTime;
|
||||
return response;
|
||||
},
|
||||
function (error) {
|
||||
error.config.metadata.endTime = new Date();
|
||||
error.duration =
|
||||
error.config.metadata.endTime - error.config.metadata.startTime;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
//Required to prevent headers from getting set and rejected from Cloudinary.
|
||||
let cleanAxios = axios.create();
|
||||
cleanAxios.interceptors.request.eject(axiosAuthInterceptorId);
|
||||
|
||||
|
||||
|
||||
export function* onOpenImagePicker() {
|
||||
yield takeLatest(PhotosActionTypes.OPEN_IMAGE_PICKER, openImagePickerAction);
|
||||
}
|
||||
@@ -73,6 +101,17 @@ export function* mediaUploadStartAction({ payload: { photos, jobid } }) {
|
||||
yield call(uploadToLocalMediaServer, photos, bodyshop, jobid);
|
||||
}
|
||||
else {
|
||||
|
||||
//Check to see if the job has enough space before uploading.
|
||||
|
||||
const hasEnoughSpace = yield call(checkJobSpace, jobid, photos, bodyshop);
|
||||
if (!hasEnoughSpace) {
|
||||
|
||||
alert(i18n.t("mediabrowser.labels.storageexceeded"));
|
||||
yield put(mediaUploadFailure(i18n.t("mediabrowser.labels.storageexceeded")));
|
||||
return;
|
||||
}
|
||||
|
||||
// Process photos in batches to avoid overwhelming the system
|
||||
const batchSize = 3; // Upload 3 photos concurrently
|
||||
const batches = [];
|
||||
@@ -83,7 +122,7 @@ export function* mediaUploadStartAction({ payload: { photos, jobid } }) {
|
||||
// Process each batch sequentially, but photos within batch concurrently
|
||||
for (const batch of batches) {
|
||||
const uploadTasks = batch.map((photo, index) =>
|
||||
fork(uploadSinglePhoto, photo, bodyshop, index, jobid)
|
||||
call(uploadSinglePhoto, photo, bodyshop, index, jobid)
|
||||
);
|
||||
// Wait for current batch to complete before starting next batch
|
||||
yield all(uploadTasks);
|
||||
@@ -91,7 +130,8 @@ export function* mediaUploadStartAction({ payload: { photos, jobid } }) {
|
||||
yield delay(100);
|
||||
}
|
||||
}
|
||||
yield put(mediaUploadCompleted());
|
||||
console.log("All uploads completed. This shouldn't fire before the uploads are done.");
|
||||
yield put(mediaUploadCompleted(photos));
|
||||
|
||||
} catch (error) {
|
||||
console.log("Saga Error: upload start", error, error.stack);
|
||||
@@ -99,6 +139,44 @@ export function* mediaUploadStartAction({ payload: { photos, jobid } }) {
|
||||
}
|
||||
}
|
||||
|
||||
function* checkJobSpace(jobid, photos, bodyshop) {
|
||||
try {
|
||||
const totalOfUploads = photos.reduce((acc, val) => {
|
||||
//Get the size of the file based on URI.
|
||||
if (val.fileSize) {
|
||||
return acc + val.fileSize;
|
||||
} else {
|
||||
alert("Asset is missing filesize. Cannot verify job space.");
|
||||
return acc
|
||||
}
|
||||
}, 0);
|
||||
|
||||
if (jobid !== "temp") {
|
||||
const queryData = yield call(client.query, {
|
||||
query: GET_DOC_SIZE_TOTALS,
|
||||
fetchPolicy: "network-only",
|
||||
variables: {
|
||||
jobId: jobid,
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
bodyshop.jobsizelimit -
|
||||
queryData?.data?.documents_aggregate.aggregate.sum.size <=
|
||||
totalOfUploads
|
||||
) {
|
||||
//No more room... abandon ship.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (error) {
|
||||
console.log("Error checking job space", error, error.stack);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function* uploadSinglePhoto(photo, bodyshop, index, jobid) {
|
||||
try {
|
||||
yield put(mediaUploadProgressOne({ ...photo, status: 'starting', progress: 0 }));
|
||||
@@ -110,9 +188,7 @@ function* uploadSinglePhoto(photo, bodyshop, index, jobid) {
|
||||
photoBlob.data.name
|
||||
).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.${extension}`
|
||||
yield call(uploadToImageProxy, photo, photoBlob, extension, key, bodyshop, jobid);
|
||||
|
||||
yield put(mediaUploadSuccessOne(photo));
|
||||
|
||||
} catch (error) {
|
||||
console.log(`Upload failed for photo ${photo.uri}:`, error);
|
||||
yield put(mediaUploadFailure({ ...photo, status: "error", error: error.message }));
|
||||
@@ -127,7 +203,7 @@ function* uploadToLocalMediaServer(photos, bodyshop, jobid) {
|
||||
ims_token: bodyshop.localmediatoken,
|
||||
},
|
||||
onUploadProgress: (e) => {
|
||||
put(mediaUploadProgressBulk({ progress: e.loaded / e.total, loaded: e.loaded }));
|
||||
put(mediaUploadProgressBulk({ progress: e.loaded / e.total, loaded: e.loaded, total: e.total }));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -150,31 +226,22 @@ function* uploadToLocalMediaServer(photos, bodyshop, jobid) {
|
||||
formData,
|
||||
options
|
||||
);
|
||||
|
||||
if (imexMediaServerResponse.status !== 200) {
|
||||
console.log("Error uploading documents:", JSON.stringify(imexMediaServerResponse, null, 2));
|
||||
|
||||
} else {
|
||||
|
||||
// onSuccess({
|
||||
// duration: imexMediaServerResponse.headers["x-response-time"],
|
||||
// });
|
||||
console.log("Local media server upload complete:", imexMediaServerResponse.data);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
console.log("Error uploading documents:", error.message, JSON.stringify(error, null, 2));
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Uncaught error", error);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid) {
|
||||
try {
|
||||
yield put(mediaUploadProgressOne({ ...photo, status: 'uploading', }));
|
||||
yield put(mediaUploadProgressOne({ ...photo, startTime: new Date(), status: 'uploading', }));
|
||||
//Get the signed url allowing us to PUT to S3.
|
||||
const signedURLResponse = yield call(axios.post,
|
||||
`${env.API_URL}/media/imgproxy/sign`,
|
||||
@@ -184,7 +251,6 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid)
|
||||
jobid,
|
||||
}
|
||||
);
|
||||
|
||||
if (signedURLResponse.status !== 200) {
|
||||
console.log("Error Getting Signed URL", signedURLResponse.statusText);
|
||||
throw new Error(`Error getting signed URL : ${signedURLResponse.statusText}`);
|
||||
@@ -228,10 +294,17 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid)
|
||||
//Create doc record.
|
||||
const uploaded_by = yield select(selectCurrentUser);
|
||||
|
||||
const [date, time] = photo.exif?.DateTime?.split(' ') || [];
|
||||
const [year, month, day] = date ? date.split(':') : [];
|
||||
const [hours, minutes, seconds] = time ? time.split(':') : [];
|
||||
const pictureMoment = moment(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}`);
|
||||
let pictureMoment = null;
|
||||
try {
|
||||
if (photo.exif) {
|
||||
const [date, time] = photo.exif?.DateTime?.split(' ') || [];
|
||||
const [year, month, day] = date ? date.split(':') : [];
|
||||
const [hours, minutes, seconds] = time ? time.split(':') : [];
|
||||
pictureMoment = moment(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}`);
|
||||
}
|
||||
} catch (exifError) {
|
||||
console.log('Error parsing exif date. Unable to set created date.', exifError);
|
||||
}
|
||||
|
||||
yield call(client.mutate, ({
|
||||
mutation: INSERT_NEW_DOCUMENT,
|
||||
@@ -245,13 +318,14 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid)
|
||||
extension: extension,
|
||||
bodyshopid: bodyshop.id,
|
||||
size: photoBlob.size,
|
||||
...(photo.exif?.DateTime //TODO :Need to find how to do this.
|
||||
...(pictureMoment && pictureMoment.isValid()
|
||||
? { takenat: pictureMoment }
|
||||
: {}),
|
||||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
console.log("Upload and record creation successful for", photo.uri);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -261,7 +335,7 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid)
|
||||
}
|
||||
|
||||
// Handle cancellation of uploads
|
||||
export function* onCancelUpload() {
|
||||
function* onCancelUpload() {
|
||||
yield takeEvery(PhotosActionTypes.CANCEL_UPLOAD, cancelUploadAction);
|
||||
}
|
||||
|
||||
@@ -274,10 +348,62 @@ function* cancelUploadAction({ payload: photoId }) {
|
||||
// }
|
||||
}
|
||||
|
||||
function* onMediaUploadCompleted() {
|
||||
yield takeLatest(PhotosActionTypes.MEDIA_UPLOAD_COMPLETED, mediaUploadCompletedAction);
|
||||
}
|
||||
|
||||
function* mediaUploadCompletedAction({ payload: photos }) {
|
||||
//Check if this should be getting deleted
|
||||
const deletedAfterUpload = yield select(selectDeleteAfterUpload);
|
||||
if (
|
||||
!deletedAfterUpload
|
||||
) {
|
||||
//Nothing to do here.
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Handle the completion of media uploads
|
||||
const filesToDelete = [...photos]
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
//Create a new asset with the first file to delete.
|
||||
// console.log('Trying new delete.');
|
||||
yield MediaLibrary.getPermissionsAsync(false);
|
||||
|
||||
const album = yield call(MediaLibrary.createAlbumAsync,
|
||||
"ImEX Mobile Deleted",
|
||||
filesToDelete.pop(),
|
||||
false
|
||||
);
|
||||
//Move the rest.
|
||||
if (filesToDelete.length > 0) {
|
||||
const moveResult = yield call(MediaLibrary.addAssetsToAlbumAsync,
|
||||
filesToDelete,
|
||||
album,
|
||||
false
|
||||
);
|
||||
}
|
||||
yield call(MediaLibrary.deleteAlbumsAsync, album);
|
||||
} else {
|
||||
yield call(MediaLibrary.deleteAssetsAsync, filesToDelete.map(f => f.assetId));
|
||||
}
|
||||
|
||||
yield put(deleteMediaSuccess(photos));
|
||||
|
||||
} catch (error) {
|
||||
console.log("Saga Error: upload start", error, error.stack);
|
||||
yield put(mediaUploadFailure(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function* photosSagas() {
|
||||
yield all([
|
||||
call(onOpenImagePicker),
|
||||
call(onMediaUploadStart),
|
||||
call(onMediaUploadCompleted)
|
||||
//call(onCancelUpload)
|
||||
]);
|
||||
}
|
||||
@@ -6,5 +6,9 @@ const PhotosActionTypes = {
|
||||
MEDIA_UPLOAD_PROGRESS_UPDATE_ONE: "MEDIA_UPLOAD_PROGRESS_UPDATE_ONE",
|
||||
MEDIA_UPLOAD_PROGRESS_UPDATE_BULK: "MEDIA_UPLOAD_PROGRESS_UPDATE_BULK",
|
||||
MEDIA_UPLOAD_COMPLETED: "MEDIA_UPLOAD_COMPLETED",
|
||||
DELETE_MEDIA: "DELETE_MEDIA",
|
||||
DELETE_MEDIA_SUCCESS: "DELETE_MEDIA_SUCCESS",
|
||||
DELETE_MEDIA_FAILURE: "DELETE_MEDIA_FAILURE",
|
||||
CLEAR_UPLOAD_ERROR: "CLEAR_UPLOAD_ERROR",
|
||||
};
|
||||
export default PhotosActionTypes;
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
},
|
||||
"labels": {
|
||||
"na": "N/A",
|
||||
"error": "Error"
|
||||
"error": "Error",
|
||||
"uploadprogress": "Upload Progress"
|
||||
}
|
||||
},
|
||||
"jobdetail": {
|
||||
|
||||
Reference in New Issue
Block a user