Progress update cleanup and UI improvements.
This commit is contained in:
@@ -13,6 +13,7 @@ function JobTabLayout(props) {
|
|||||||
tabBarActiveTintColor: theme.colors.primary,
|
tabBarActiveTintColor: theme.colors.primary,
|
||||||
tabBarPosition: "top",
|
tabBarPosition: "top",
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
|
animation: "shift",
|
||||||
tabBarStyle: {
|
tabBarStyle: {
|
||||||
marginTop: -50,
|
marginTop: -50,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,31 +1,5 @@
|
|||||||
import SignOutButton from "@/components-old/sign-out-button/sign-out-button.component";
|
import Settings from "../components/settings/settings";
|
||||||
import { selectBodyshop } from "@/redux/user/user.selectors";
|
|
||||||
import { StyleSheet, Text, View } from "react-native";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
export default function Tab() {
|
||||||
bodyshop: selectBodyshop,
|
return <Settings />;
|
||||||
});
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 { useTranslation } from "react-i18next";
|
||||||
import { StyleSheet, Text, View } from "react-native";
|
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 { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { toggleDeleteAfterUpload } from "../../redux/app/app.actions";
|
import { toggleDeleteAfterUpload } from "../../redux/app/app.actions";
|
||||||
@@ -19,7 +18,7 @@ export function UploadDeleteSwitch({
|
|||||||
deleteAfterUpload,
|
deleteAfterUpload,
|
||||||
toggleDeleteAfterUpload,
|
toggleDeleteAfterUpload,
|
||||||
}) {
|
}) {
|
||||||
console.log("*** ~ deleteAfterUpload:", deleteAfterUpload);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Text } from "react-native";
|
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();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Card style={{ margin: 8, backgroundColor: "#ffdddd" }}>
|
<Card style={{ margin: 8, backgroundColor: "#ffdddd" }}>
|
||||||
@@ -14,6 +14,11 @@ export default function ErrorDisplay({ errorMessage, error }) {
|
|||||||
error ||
|
error ||
|
||||||
"An unknown error has occured."}
|
"An unknown error has occured."}
|
||||||
</Text>
|
</Text>
|
||||||
|
{onDismiss ? (
|
||||||
|
<Card.Actions>
|
||||||
|
<Button onPress={onDismiss}>{t("general.labels.dismiss")}</Button>
|
||||||
|
</Card.Actions>
|
||||||
|
) : null}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import ImageView from "react-native-image-viewing";
|
import ImageView from "react-native-image-viewing";
|
||||||
|
import { ActivityIndicator } from "react-native-paper";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import env from "../../env";
|
import env from "../../env";
|
||||||
@@ -119,6 +120,7 @@ export function JobDocumentsComponent({ bodyshop }) {
|
|||||||
getPhotos();
|
getPhotos();
|
||||||
}, [getPhotos]);
|
}, [getPhotos]);
|
||||||
|
|
||||||
|
if (loading) return <ActivityIndicator style={{ flex: 1 }} size="large" />;
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorDisplay message={JSON.stringify(error)} />;
|
return <ErrorDisplay message={JSON.stringify(error)} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,14 +17,13 @@ export default function JobLines() {
|
|||||||
skip: !jobId,
|
skip: !jobId,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("*** ~ JobLines ~ error:", error);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const onRefresh = async () => {
|
const onRefresh = async () => {
|
||||||
return refetch();
|
return refetch();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator size="large" style={{ flex: 1 }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default function JobNotes() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator size="large" style={{ flex: 1 }} />;
|
||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorDisplay message={JSON.stringify(error?.message)} />;
|
return <ErrorDisplay message={JSON.stringify(error?.message)} />;
|
||||||
|
|||||||
@@ -23,14 +23,13 @@ export default function JobTombstone() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
console.log("*** ~ JobTombstone ~ theme:", theme.colors);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const onRefresh = async () => {
|
const onRefresh = async () => {
|
||||||
return refetch();
|
return refetch();
|
||||||
};
|
};
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator size="large" style={{ flex: 1 }} />;
|
||||||
}
|
}
|
||||||
if (!data.jobs_by_pk) {
|
if (!data.jobs_by_pk) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -133,7 +133,12 @@ const styles = StyleSheet.create({
|
|||||||
borderWidth: StyleSheet.hairlineWidth,
|
borderWidth: StyleSheet.hairlineWidth,
|
||||||
backdropFilter: "blur(20px)", // web only
|
backdropFilter: "blur(20px)", // web only
|
||||||
},
|
},
|
||||||
cardContents: { flex: 1, flexDirection: "row", display: "flex" },
|
cardContents: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: "row",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
headerRow: {
|
headerRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "flex-start",
|
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 { formatBytes } from "@/util/uploadUtils";
|
||||||
import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ProgressBar } from "react-native-paper";
|
import { StyleSheet, View } from "react-native";
|
||||||
|
import { ProgressBar, Text } from "react-native-paper";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {
|
import {
|
||||||
selectPhotos,
|
selectPhotos,
|
||||||
selectUploadProgress,
|
selectUploadError,
|
||||||
|
selectUploadProgress,
|
||||||
} from "../../redux/photos/photos.selectors";
|
} from "../../redux/photos/photos.selectors";
|
||||||
|
import ErrorDisplay from "../error/error-display";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
photos: selectPhotos,
|
photos: selectPhotos,
|
||||||
photoUploadProgress: selectUploadProgress,
|
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 (photos?.length === 0) return null;
|
||||||
|
if (uploadError)
|
||||||
|
return <ErrorDisplay error={uploadError} onDismiss={clearError} />;
|
||||||
return (
|
return (
|
||||||
<View style={styles.modalContainer}>
|
<View style={styles.modalContainer}>
|
||||||
<View style={styles.modal}>
|
<View style={styles.modal}>
|
||||||
|
<Text variant="titleLarge" style={styles.title}>
|
||||||
|
{t("general.labels.uploadprogress")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
{Object.keys(photoUploadProgress).map((key) => (
|
{Object.keys(photoUploadProgress).map((key) => (
|
||||||
<View key={key} style={styles.progressItem}>
|
<View key={key} style={styles.progressItem}>
|
||||||
<Text style={styles.progressText}>
|
<Text
|
||||||
|
style={styles.progressText}
|
||||||
|
numberOfLines={1}
|
||||||
|
ellipsizeMode="tail"
|
||||||
|
>
|
||||||
{photoUploadProgress[key].fileName}
|
{photoUploadProgress[key].fileName}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.progressBarContainer}>
|
<View style={styles.progressBarContainer}>
|
||||||
@@ -42,38 +67,14 @@ export function UploadProgress({ photos, photoUploadProgress }) {
|
|||||||
>
|
>
|
||||||
<Text>{`${formatBytes(
|
<Text>{`${formatBytes(
|
||||||
photoUploadProgress[key].loaded /
|
photoUploadProgress[key].loaded /
|
||||||
(((photoUploadProgress[key].uploadEnd || new Date()) -
|
(((photoUploadProgress[key].endTime || new Date()) -
|
||||||
photoUploadProgress[key].uploadStart) /
|
photoUploadProgress[key].startTime) /
|
||||||
1000)
|
1000)
|
||||||
)}/sec`}</Text>
|
)}/sec`}</Text>
|
||||||
{photoUploadProgress[key].percent === 1 && (
|
|
||||||
<>
|
|
||||||
<ActivityIndicator style={{ marginLeft: 12 }} />
|
|
||||||
<Text style={{ marginLeft: 4 }}>Processing...</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</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>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -81,17 +82,19 @@ export function UploadProgress({ photos, photoUploadProgress }) {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
modalContainer: {
|
modalContainer: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flex: 1,
|
// flex: 1,
|
||||||
|
marginTop: 14,
|
||||||
|
marginBottom: 14,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
//flex: 1,
|
//flex: 1,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
marginLeft: 20,
|
marginLeft: 12,
|
||||||
marginRight: 20,
|
marginRight: 12,
|
||||||
backgroundColor: "white",
|
backgroundColor: theme.colors.elevation.level3,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
padding: 18,
|
paddingTop: 12,
|
||||||
shadowColor: "#000",
|
shadowColor: "#000",
|
||||||
shadowOffset: {
|
shadowOffset: {
|
||||||
width: 0,
|
width: 0,
|
||||||
@@ -101,6 +104,13 @@ const styles = StyleSheet.create({
|
|||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
},
|
},
|
||||||
|
title: {
|
||||||
|
alignSelf: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: 12,
|
||||||
|
paddingLeft: 12,
|
||||||
|
paddingRight: 12,
|
||||||
|
},
|
||||||
centeredView: {
|
centeredView: {
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -116,6 +126,12 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
progressText: {
|
progressText: {
|
||||||
flex: 1,
|
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: {
|
progressBarContainer: {
|
||||||
flex: 3,
|
flex: 3,
|
||||||
|
|||||||
@@ -31,7 +31,25 @@ export const mediaUploadProgressBulk = (info) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const mediaUploadCompleted = (photo) => ({
|
export const mediaUploadCompleted = (photo) => ({
|
||||||
type: PhotosActionTypes.MEDIA_UPLOAD_COMPLETED
|
type: PhotosActionTypes.MEDIA_UPLOAD_COMPLETED,
|
||||||
//payload: photo,
|
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:
|
case PhotosActionTypes.MEDIA_UPLOAD_SUCCESS_ONE:
|
||||||
return {
|
return {
|
||||||
...state,
|
...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:
|
case PhotosActionTypes.MEDIA_UPLOAD_PROGRESS_UPDATE_BULK:
|
||||||
return {
|
return {
|
||||||
@@ -48,6 +48,12 @@ const photosReducer = (state = INITIAL_STATE, action) => {
|
|||||||
photos: [],
|
photos: [],
|
||||||
progress: {}
|
progress: {}
|
||||||
};
|
};
|
||||||
|
case PhotosActionTypes.CLEAR_UPLOAD_ERROR:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
photos: [], progress: {},
|
||||||
|
uploadError: null,
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import Constants from "expo-constants";
|
import Constants from "expo-constants";
|
||||||
import * as ImagePicker from "expo-image-picker";
|
import * as ImagePicker from "expo-image-picker";
|
||||||
|
import * as MediaLibrary from "expo-media-library";
|
||||||
import moment from 'moment';
|
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 env from "../../env";
|
||||||
import { client } from '../../graphql/client';
|
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 { axiosAuthInterceptorId } from "../../util/CleanAxios";
|
||||||
import { fetchImageFromUri, replaceAccents } from '../../util/uploadUtils';
|
import { fetchImageFromUri, replaceAccents } from '../../util/uploadUtils';
|
||||||
import { selectBodyshop, selectCurrentUser } from "../user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../user/user.selectors";
|
||||||
import {
|
import {
|
||||||
|
deleteMediaSuccess,
|
||||||
mediaUploadCompleted,
|
mediaUploadCompleted,
|
||||||
mediaUploadFailure,
|
mediaUploadFailure,
|
||||||
mediaUploadProgressBulk,
|
mediaUploadProgressBulk,
|
||||||
@@ -18,14 +20,40 @@ import {
|
|||||||
mediaUploadSuccessOne
|
mediaUploadSuccessOne
|
||||||
} from "./photos.actions";
|
} from "./photos.actions";
|
||||||
|
|
||||||
|
import i18n from "@/translations/i18n";
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
import { selectDeleteAfterUpload } from "../app/app.selectors";
|
||||||
import PhotosActionTypes from "./photos.types";
|
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.
|
//Required to prevent headers from getting set and rejected from Cloudinary.
|
||||||
let cleanAxios = axios.create();
|
let cleanAxios = axios.create();
|
||||||
cleanAxios.interceptors.request.eject(axiosAuthInterceptorId);
|
cleanAxios.interceptors.request.eject(axiosAuthInterceptorId);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function* onOpenImagePicker() {
|
export function* onOpenImagePicker() {
|
||||||
yield takeLatest(PhotosActionTypes.OPEN_IMAGE_PICKER, openImagePickerAction);
|
yield takeLatest(PhotosActionTypes.OPEN_IMAGE_PICKER, openImagePickerAction);
|
||||||
}
|
}
|
||||||
@@ -73,6 +101,17 @@ export function* mediaUploadStartAction({ payload: { photos, jobid } }) {
|
|||||||
yield call(uploadToLocalMediaServer, photos, bodyshop, jobid);
|
yield call(uploadToLocalMediaServer, photos, bodyshop, jobid);
|
||||||
}
|
}
|
||||||
else {
|
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
|
// Process photos in batches to avoid overwhelming the system
|
||||||
const batchSize = 3; // Upload 3 photos concurrently
|
const batchSize = 3; // Upload 3 photos concurrently
|
||||||
const batches = [];
|
const batches = [];
|
||||||
@@ -83,7 +122,7 @@ export function* mediaUploadStartAction({ payload: { photos, jobid } }) {
|
|||||||
// Process each batch sequentially, but photos within batch concurrently
|
// Process each batch sequentially, but photos within batch concurrently
|
||||||
for (const batch of batches) {
|
for (const batch of batches) {
|
||||||
const uploadTasks = batch.map((photo, index) =>
|
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
|
// Wait for current batch to complete before starting next batch
|
||||||
yield all(uploadTasks);
|
yield all(uploadTasks);
|
||||||
@@ -91,7 +130,8 @@ export function* mediaUploadStartAction({ payload: { photos, jobid } }) {
|
|||||||
yield delay(100);
|
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) {
|
} catch (error) {
|
||||||
console.log("Saga Error: upload start", error, error.stack);
|
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) {
|
function* uploadSinglePhoto(photo, bodyshop, index, jobid) {
|
||||||
try {
|
try {
|
||||||
yield put(mediaUploadProgressOne({ ...photo, status: 'starting', progress: 0 }));
|
yield put(mediaUploadProgressOne({ ...photo, status: 'starting', progress: 0 }));
|
||||||
@@ -110,9 +188,7 @@ function* uploadSinglePhoto(photo, bodyshop, index, jobid) {
|
|||||||
photoBlob.data.name
|
photoBlob.data.name
|
||||||
).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.${extension}`
|
).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.${extension}`
|
||||||
yield call(uploadToImageProxy, photo, photoBlob, extension, key, bodyshop, jobid);
|
yield call(uploadToImageProxy, photo, photoBlob, extension, key, bodyshop, jobid);
|
||||||
|
|
||||||
yield put(mediaUploadSuccessOne(photo));
|
yield put(mediaUploadSuccessOne(photo));
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`Upload failed for photo ${photo.uri}:`, error);
|
console.log(`Upload failed for photo ${photo.uri}:`, error);
|
||||||
yield put(mediaUploadFailure({ ...photo, status: "error", error: error.message }));
|
yield put(mediaUploadFailure({ ...photo, status: "error", error: error.message }));
|
||||||
@@ -127,7 +203,7 @@ function* uploadToLocalMediaServer(photos, bodyshop, jobid) {
|
|||||||
ims_token: bodyshop.localmediatoken,
|
ims_token: bodyshop.localmediatoken,
|
||||||
},
|
},
|
||||||
onUploadProgress: (e) => {
|
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,
|
formData,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
|
|
||||||
if (imexMediaServerResponse.status !== 200) {
|
if (imexMediaServerResponse.status !== 200) {
|
||||||
console.log("Error uploading documents:", JSON.stringify(imexMediaServerResponse, null, 2));
|
console.log("Error uploading documents:", JSON.stringify(imexMediaServerResponse, null, 2));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
console.log("Local media server upload complete:", imexMediaServerResponse.data);
|
||||||
// onSuccess({
|
|
||||||
// duration: imexMediaServerResponse.headers["x-response-time"],
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
console.log("Error uploading documents:", error.message, JSON.stringify(error, null, 2));
|
console.log("Error uploading documents:", error.message, JSON.stringify(error, null, 2));
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Uncaught error", error);
|
console.log("Uncaught error", error);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid) {
|
function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid) {
|
||||||
try {
|
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.
|
//Get the signed url allowing us to PUT to S3.
|
||||||
const signedURLResponse = yield call(axios.post,
|
const signedURLResponse = yield call(axios.post,
|
||||||
`${env.API_URL}/media/imgproxy/sign`,
|
`${env.API_URL}/media/imgproxy/sign`,
|
||||||
@@ -184,7 +251,6 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid)
|
|||||||
jobid,
|
jobid,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (signedURLResponse.status !== 200) {
|
if (signedURLResponse.status !== 200) {
|
||||||
console.log("Error Getting Signed URL", signedURLResponse.statusText);
|
console.log("Error Getting Signed URL", signedURLResponse.statusText);
|
||||||
throw new Error(`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.
|
//Create doc record.
|
||||||
const uploaded_by = yield select(selectCurrentUser);
|
const uploaded_by = yield select(selectCurrentUser);
|
||||||
|
|
||||||
const [date, time] = photo.exif?.DateTime?.split(' ') || [];
|
let pictureMoment = null;
|
||||||
const [year, month, day] = date ? date.split(':') : [];
|
try {
|
||||||
const [hours, minutes, seconds] = time ? time.split(':') : [];
|
if (photo.exif) {
|
||||||
const pictureMoment = moment(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}`);
|
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, ({
|
yield call(client.mutate, ({
|
||||||
mutation: INSERT_NEW_DOCUMENT,
|
mutation: INSERT_NEW_DOCUMENT,
|
||||||
@@ -245,13 +318,14 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid)
|
|||||||
extension: extension,
|
extension: extension,
|
||||||
bodyshopid: bodyshop.id,
|
bodyshopid: bodyshop.id,
|
||||||
size: photoBlob.size,
|
size: photoBlob.size,
|
||||||
...(photo.exif?.DateTime //TODO :Need to find how to do this.
|
...(pictureMoment && pictureMoment.isValid()
|
||||||
? { takenat: pictureMoment }
|
? { takenat: pictureMoment }
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
console.log("Upload and record creation successful for", photo.uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -261,7 +335,7 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle cancellation of uploads
|
// Handle cancellation of uploads
|
||||||
export function* onCancelUpload() {
|
function* onCancelUpload() {
|
||||||
yield takeEvery(PhotosActionTypes.CANCEL_UPLOAD, cancelUploadAction);
|
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() {
|
export function* photosSagas() {
|
||||||
yield all([
|
yield all([
|
||||||
call(onOpenImagePicker),
|
call(onOpenImagePicker),
|
||||||
call(onMediaUploadStart),
|
call(onMediaUploadStart),
|
||||||
|
call(onMediaUploadCompleted)
|
||||||
//call(onCancelUpload)
|
//call(onCancelUpload)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -6,5 +6,9 @@ const PhotosActionTypes = {
|
|||||||
MEDIA_UPLOAD_PROGRESS_UPDATE_ONE: "MEDIA_UPLOAD_PROGRESS_UPDATE_ONE",
|
MEDIA_UPLOAD_PROGRESS_UPDATE_ONE: "MEDIA_UPLOAD_PROGRESS_UPDATE_ONE",
|
||||||
MEDIA_UPLOAD_PROGRESS_UPDATE_BULK: "MEDIA_UPLOAD_PROGRESS_UPDATE_BULK",
|
MEDIA_UPLOAD_PROGRESS_UPDATE_BULK: "MEDIA_UPLOAD_PROGRESS_UPDATE_BULK",
|
||||||
MEDIA_UPLOAD_COMPLETED: "MEDIA_UPLOAD_COMPLETED",
|
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;
|
export default PhotosActionTypes;
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"na": "N/A",
|
"na": "N/A",
|
||||||
"error": "Error"
|
"error": "Error",
|
||||||
|
"uploadprogress": "Upload Progress"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"jobdetail": {
|
"jobdetail": {
|
||||||
|
|||||||
Reference in New Issue
Block a user