mirror of
https://github.com/usatiuk/photos.git
synced 2025-10-28 15:27:49 +01:00
select and delete multiple photos
This commit is contained in:
@@ -4,7 +4,36 @@
|
|||||||
--photoOverlayDrawerWidth: 5rem;
|
--photoOverlayDrawerWidth: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#photoOverlayContainer {
|
.operationsOverlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: $pt-navbar-height;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&.bp4-ovarlay-open {
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bp4-overlay-enter {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bp4-overlay-enter-active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bp4-overlay-exit {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bp4-overlay-exit-active {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.operationsOverlay #photoOverlayContainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -40,11 +69,9 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#photoOverlayDrawer {
|
#photoOverlayDrawer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -89,7 +116,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.bp4-overlay-exit {
|
.bp4-overlay-exit {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
@@ -149,14 +175,11 @@
|
|||||||
|
|
||||||
.month,
|
.month,
|
||||||
.year {
|
.year {
|
||||||
|
|
||||||
h3,
|
h3,
|
||||||
h2 {
|
h2 {
|
||||||
|
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
@@ -203,9 +226,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bp4-dark {
|
.bp4-dark {
|
||||||
#overview {}
|
#overview {
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,22 +3,29 @@ import * as React from "react";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
import { IAppState } from "../redux/reducers";
|
import { IAppState } from "../redux/reducers";
|
||||||
import { photosLoadStart } from "../redux/photos/actions";
|
import {
|
||||||
|
photosDeleteCancel,
|
||||||
|
photosDeleteStart,
|
||||||
|
photosLoadStart,
|
||||||
|
} from "../redux/photos/actions";
|
||||||
import { IPhotoReqJSON } from "../../../src/entity/Photo";
|
import { IPhotoReqJSON } from "../../../src/entity/Photo";
|
||||||
import { LoadingStub } from "../LoadingStub";
|
import { LoadingStub } from "../LoadingStub";
|
||||||
import { PhotoCard } from "./PhotoCard";
|
import { PhotoCard } from "./PhotoCard";
|
||||||
import {
|
import {
|
||||||
|
Alignment,
|
||||||
Button,
|
Button,
|
||||||
Classes,
|
Classes,
|
||||||
H1,
|
H1,
|
||||||
H2,
|
H2,
|
||||||
H3,
|
H3,
|
||||||
|
Navbar,
|
||||||
Overlay,
|
Overlay,
|
||||||
Spinner,
|
Spinner,
|
||||||
} from "@blueprintjs/core";
|
} from "@blueprintjs/core";
|
||||||
import { UploadButton } from "./UploadButton";
|
import { UploadButton } from "./UploadButton";
|
||||||
import { Photo } from "./Photo";
|
import { Photo } from "./Photo";
|
||||||
import { getPhotoThumbPath } from "../redux/api/photos";
|
import { getPhotoThumbPath } from "../redux/api/photos";
|
||||||
|
import { showDeletionToast } from "../AppToaster";
|
||||||
|
|
||||||
export interface IOverviewComponentProps {
|
export interface IOverviewComponentProps {
|
||||||
photos: IPhotoReqJSON[];
|
photos: IPhotoReqJSON[];
|
||||||
@@ -27,8 +34,11 @@ export interface IOverviewComponentProps {
|
|||||||
overviewFetching: boolean;
|
overviewFetching: boolean;
|
||||||
overviewFetchingError: string | null;
|
overviewFetchingError: string | null;
|
||||||
overviewFetchingSpinner: boolean;
|
overviewFetchingSpinner: boolean;
|
||||||
|
darkMode: boolean;
|
||||||
|
|
||||||
fetchPhotos: () => void;
|
fetchPhotos: () => void;
|
||||||
|
startDeletePhotos: (photos: IPhotoReqJSON[]) => void;
|
||||||
|
cancelDelete: (photos: IPhotoReqJSON[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PhotoCardM = React.memo(PhotoCard);
|
const PhotoCardM = React.memo(PhotoCard);
|
||||||
@@ -183,6 +193,49 @@ export const OverviewComponent: React.FunctionComponent<
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
|
<Overlay
|
||||||
|
lazy
|
||||||
|
usePortal
|
||||||
|
isOpen={selectedPhotos.size > 0}
|
||||||
|
transitionDuration={300}
|
||||||
|
hasBackdrop={false}
|
||||||
|
>
|
||||||
|
<div className={"operationsOverlay"}>
|
||||||
|
<Navbar
|
||||||
|
className={props.darkMode ? Classes.DARK : undefined}
|
||||||
|
>
|
||||||
|
<Navbar.Group align={Alignment.LEFT}>
|
||||||
|
<Button minimal={true} icon="edit">
|
||||||
|
Select
|
||||||
|
</Button>
|
||||||
|
<Navbar.Divider />
|
||||||
|
</Navbar.Group>
|
||||||
|
|
||||||
|
<Navbar.Group align={Alignment.RIGHT}>
|
||||||
|
<Button
|
||||||
|
className="bp4-minimal"
|
||||||
|
icon="trash"
|
||||||
|
text="Delete"
|
||||||
|
onClick={() => {
|
||||||
|
const photosObjectsWithIds =
|
||||||
|
props.photos.filter((p) =>
|
||||||
|
selectedPhotosRef.current.has(p.id),
|
||||||
|
);
|
||||||
|
showDeletionToast(() =>
|
||||||
|
props.cancelDelete(
|
||||||
|
photosObjectsWithIds,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
props.startDeletePhotos(
|
||||||
|
photosObjectsWithIds,
|
||||||
|
);
|
||||||
|
selectedPhotosRef.current.clear();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Navbar.Group>
|
||||||
|
</Navbar>
|
||||||
|
</div>
|
||||||
|
</Overlay>
|
||||||
<div id="overviewContainer" onScroll={onLoaderScroll}>
|
<div id="overviewContainer" onScroll={onLoaderScroll}>
|
||||||
<div id="overview">
|
<div id="overview">
|
||||||
<div id="actionbar">
|
<div id="actionbar">
|
||||||
@@ -206,11 +259,18 @@ function mapStateToProps(state: IAppState) {
|
|||||||
overviewFetchingError: state.photos.overviewFetchingError,
|
overviewFetchingError: state.photos.overviewFetchingError,
|
||||||
overviewFetchingSpinner: state.photos.overviewFetchingSpinner,
|
overviewFetchingSpinner: state.photos.overviewFetchingSpinner,
|
||||||
triedLoading: state.photos.triedLoading,
|
triedLoading: state.photos.triedLoading,
|
||||||
|
darkMode: state.localSettings.darkMode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: Dispatch) {
|
function mapDispatchToProps(dispatch: Dispatch) {
|
||||||
return { fetchPhotos: () => dispatch(photosLoadStart()) };
|
return {
|
||||||
|
fetchPhotos: () => dispatch(photosLoadStart()),
|
||||||
|
startDeletePhotos: (photos: IPhotoReqJSON[]) =>
|
||||||
|
dispatch(photosDeleteStart(photos)),
|
||||||
|
cancelDelete: (photos: IPhotoReqJSON[]) =>
|
||||||
|
dispatch(photosDeleteCancel(photos)),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Overview = connect(
|
export const Overview = connect(
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { IPhotoReqJSON } from "../../../src/entity/Photo";
|
|||||||
import { getPhotoImgPath, getPhotoThumbPath } from "../redux/api/photos";
|
import { getPhotoImgPath, getPhotoThumbPath } from "../redux/api/photos";
|
||||||
import { showDeletionToast } from "../AppToaster";
|
import { showDeletionToast } from "../AppToaster";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
import { photoDeleteCancel, photoDeleteStart } from "../redux/photos/actions";
|
import { photosDeleteCancel, photosDeleteStart } from "../redux/photos/actions";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { LoadingStub } from "../LoadingStub";
|
import { LoadingStub } from "../LoadingStub";
|
||||||
import { RouteComponentProps, withRouter } from "react-router";
|
import { RouteComponentProps, withRouter } from "react-router";
|
||||||
@@ -23,8 +23,8 @@ export interface IPhotoCardComponentProps extends RouteComponentProps {
|
|||||||
selected: boolean;
|
selected: boolean;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
deletePhoto: (photo: IPhotoReqJSON) => void;
|
deletePhoto: (photos: IPhotoReqJSON[]) => void;
|
||||||
cancelDelete: (photo: IPhotoReqJSON) => void;
|
cancelDelete: (photos: IPhotoReqJSON[]) => void;
|
||||||
onClick: (e: React.MouseEvent<HTMLElement>) => void;
|
onClick: (e: React.MouseEvent<HTMLElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +55,8 @@ export class PhotoCardComponent extends React.PureComponent<
|
|||||||
}
|
}
|
||||||
|
|
||||||
public handleDelete(): void {
|
public handleDelete(): void {
|
||||||
showDeletionToast(() => this.props.cancelDelete(this.props.photo));
|
showDeletionToast(() => this.props.cancelDelete([this.props.photo]));
|
||||||
this.props.deletePhoto(this.props.photo);
|
this.props.deletePhoto([this.props.photo]);
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
public handleEdit() {
|
public handleEdit() {
|
||||||
@@ -122,10 +122,10 @@ export class PhotoCardComponent extends React.PureComponent<
|
|||||||
|
|
||||||
function mapDispatchToProps(dispatch: Dispatch) {
|
function mapDispatchToProps(dispatch: Dispatch) {
|
||||||
return {
|
return {
|
||||||
deletePhoto: (photo: IPhotoReqJSON) =>
|
deletePhoto: (photos: IPhotoReqJSON[]) =>
|
||||||
dispatch(photoDeleteStart(photo)),
|
dispatch(photosDeleteStart(photos)),
|
||||||
cancelDelete: (photo: IPhotoReqJSON) =>
|
cancelDelete: (photos: IPhotoReqJSON[]) =>
|
||||||
dispatch(photoDeleteCancel(photo)),
|
dispatch(photosDeleteCancel(photos)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { IPhotoReqJSON } from "../../../../src/entity/Photo";
|
import { IPhotoReqJSON } from "../../../../src/entity/Photo";
|
||||||
import {
|
import {
|
||||||
IPhotosByIDDeleteRespBody,
|
|
||||||
IPhotosByIDGetRespBody,
|
IPhotosByIDGetRespBody,
|
||||||
|
IPhotosDeleteRespBody,
|
||||||
IPhotosListRespBody,
|
IPhotosListRespBody,
|
||||||
IPhotosNewRespBody,
|
IPhotosNewRespBody,
|
||||||
IPhotosUploadRespBody,
|
IPhotosUploadRespBody,
|
||||||
@@ -49,8 +49,8 @@ export async function uploadPhoto(
|
|||||||
return fetchJSONAuth(`/photos/upload/${id}`, "POST", file);
|
return fetchJSONAuth(`/photos/upload/${id}`, "POST", file);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deletePhoto(
|
export async function deletePhotos(
|
||||||
photo: IPhotoReqJSON,
|
photos: IPhotoReqJSON[],
|
||||||
): Promise<IPhotosByIDDeleteRespBody> {
|
): Promise<IPhotosDeleteRespBody> {
|
||||||
return fetchJSONAuth(`/photos/byID/${photo.id}`, "DELETE");
|
return fetchJSONAuth(`/photos/delete`, "POST", { photos });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ export enum PhotoTypes {
|
|||||||
PHOTO_UPLOAD_SUCCESS = "PHOTO_UPLOAD_SUCCESS",
|
PHOTO_UPLOAD_SUCCESS = "PHOTO_UPLOAD_SUCCESS",
|
||||||
PHOTO_UPLOAD_FAIL = "PHOTO_UPLOAD_FAIL",
|
PHOTO_UPLOAD_FAIL = "PHOTO_UPLOAD_FAIL",
|
||||||
PHOTOS_START_FETCHING_SPINNER = "PHOTOS_START_FETCHING_SPINNER",
|
PHOTOS_START_FETCHING_SPINNER = "PHOTOS_START_FETCHING_SPINNER",
|
||||||
PHOTO_DELETE_START = "PHOTO_DELETE_START",
|
PHOTOS_DELETE_START = "PHOTOS_DELETE_START",
|
||||||
PHOTO_DELETE_SUCCESS = "PHOTO_DELETE_SUCCESS",
|
PHOTOS_DELETE_SUCCESS = "PHOTOS_DELETE_SUCCESS",
|
||||||
PHOTO_DELETE_FAIL = "PHOTO_DELETE_FAIL",
|
PHOTOS_DELETE_FAIL = "PHOTOS_DELETE_FAIL",
|
||||||
PHOTO_DELETE_CANCEL = "PHOTO_DELETE_CANCEL",
|
PHOTOS_DELETE_CANCEL = "PHOTOS_DELETE_CANCEL",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPhotosLoadStartAction extends Action {
|
export interface IPhotosLoadStartAction extends Action {
|
||||||
@@ -109,25 +109,25 @@ export interface IPhotoCreateFailAction extends Action {
|
|||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPhotoDeleteStartAction extends Action {
|
export interface IPhotosDeleteStartAction extends Action {
|
||||||
type: PhotoTypes.PHOTO_DELETE_START;
|
type: PhotoTypes.PHOTOS_DELETE_START;
|
||||||
photo: IPhotoReqJSON;
|
photos: IPhotoReqJSON[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPhotoDeleteSuccessAction extends Action {
|
export interface IPhotosDeleteSuccessAction extends Action {
|
||||||
type: PhotoTypes.PHOTO_DELETE_SUCCESS;
|
type: PhotoTypes.PHOTOS_DELETE_SUCCESS;
|
||||||
photo: IPhotoReqJSON;
|
photos: IPhotoReqJSON[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPhotoDeleteFailAction extends Action {
|
export interface IPhotosDeleteFailAction extends Action {
|
||||||
type: PhotoTypes.PHOTO_DELETE_FAIL;
|
type: PhotoTypes.PHOTOS_DELETE_FAIL;
|
||||||
photo: IPhotoReqJSON;
|
photos: IPhotoReqJSON[];
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPhotoDeleteCancelAction extends Action {
|
export interface IPhotosDeleteCancelAction extends Action {
|
||||||
type: PhotoTypes.PHOTO_DELETE_CANCEL;
|
type: PhotoTypes.PHOTOS_DELETE_CANCEL;
|
||||||
photo: IPhotoReqJSON;
|
photos: IPhotoReqJSON[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPhotosStartFetchingSpinner extends Action {
|
export interface IPhotosStartFetchingSpinner extends Action {
|
||||||
@@ -220,27 +220,27 @@ export function photoLoadFail(id: number, error: string): IPhotoLoadFailAction {
|
|||||||
return { type: PhotoTypes.PHOTO_LOAD_FAIL, id, error };
|
return { type: PhotoTypes.PHOTO_LOAD_FAIL, id, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function photoDeleteStart(
|
export function photosDeleteStart(
|
||||||
photo: IPhotoReqJSON,
|
photos: IPhotoReqJSON[],
|
||||||
): IPhotoDeleteStartAction {
|
): IPhotosDeleteStartAction {
|
||||||
return { type: PhotoTypes.PHOTO_DELETE_START, photo };
|
return { type: PhotoTypes.PHOTOS_DELETE_START, photos };
|
||||||
}
|
}
|
||||||
export function photoDeleteSuccess(
|
export function photosDeleteSuccess(
|
||||||
photo: IPhotoReqJSON,
|
photos: IPhotoReqJSON[],
|
||||||
): IPhotoDeleteSuccessAction {
|
): IPhotosDeleteSuccessAction {
|
||||||
return { type: PhotoTypes.PHOTO_DELETE_SUCCESS, photo };
|
return { type: PhotoTypes.PHOTOS_DELETE_SUCCESS, photos };
|
||||||
}
|
}
|
||||||
export function photoDeleteFail(
|
export function photosDeleteFail(
|
||||||
photo: IPhotoReqJSON,
|
photos: IPhotoReqJSON[],
|
||||||
error?: string,
|
error?: string,
|
||||||
): IPhotoDeleteFailAction {
|
): IPhotosDeleteFailAction {
|
||||||
return { type: PhotoTypes.PHOTO_DELETE_FAIL, photo, error };
|
return { type: PhotoTypes.PHOTOS_DELETE_FAIL, photos, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function photoDeleteCancel(
|
export function photosDeleteCancel(
|
||||||
photo: IPhotoReqJSON,
|
photos: IPhotoReqJSON[],
|
||||||
): IPhotoDeleteCancelAction {
|
): IPhotosDeleteCancelAction {
|
||||||
return { type: PhotoTypes.PHOTO_DELETE_CANCEL, photo };
|
return { type: PhotoTypes.PHOTOS_DELETE_CANCEL, photos };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function photosStartFetchingSpinner(): IPhotosStartFetchingSpinner {
|
export function photosStartFetchingSpinner(): IPhotosStartFetchingSpinner {
|
||||||
@@ -257,10 +257,10 @@ export type PhotoAction =
|
|||||||
| IPhotoCreateSuccessAction
|
| IPhotoCreateSuccessAction
|
||||||
| IPhotoUploadFailAction
|
| IPhotoUploadFailAction
|
||||||
| IPhotoUploadSuccessAction
|
| IPhotoUploadSuccessAction
|
||||||
| IPhotoDeleteFailAction
|
| IPhotosDeleteFailAction
|
||||||
| IPhotoDeleteStartAction
|
| IPhotosDeleteStartAction
|
||||||
| IPhotoDeleteSuccessAction
|
| IPhotosDeleteSuccessAction
|
||||||
| IPhotoDeleteCancelAction
|
| IPhotosDeleteCancelAction
|
||||||
| IPhotoLoadFailAction
|
| IPhotoLoadFailAction
|
||||||
| IPhotoLoadStartAction
|
| IPhotoLoadStartAction
|
||||||
| IPhotoLoadSuccessAction
|
| IPhotoLoadSuccessAction
|
||||||
|
|||||||
@@ -196,15 +196,18 @@ export const photosReducer: Reducer<IPhotosState, PhotoAction> = (
|
|||||||
photosUploading: state.photosUploading - 1,
|
photosUploading: state.photosUploading - 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case PhotoTypes.PHOTO_DELETE_START: {
|
case PhotoTypes.PHOTOS_DELETE_START: {
|
||||||
const photos = state.photos;
|
const photos = state.photos;
|
||||||
const delPhoto = photos.find((p) => p.id === action.photo.id);
|
const photoIds = action.photos.map((p) => p.id);
|
||||||
if (delPhoto) {
|
const delPhotos = photos.find((p) => photoIds.includes(p.id));
|
||||||
|
if (delPhotos) {
|
||||||
const photosCleaned = photos.filter(
|
const photosCleaned = photos.filter(
|
||||||
(p) => p.id !== action.photo.id,
|
(p) => !photoIds.includes(p.id),
|
||||||
);
|
);
|
||||||
const delCache = { ...state.deleteCache };
|
const delCache = { ...state.deleteCache };
|
||||||
delCache[delPhoto?.id] = delPhoto;
|
for (const photo of action.photos) {
|
||||||
|
delCache[photo.id] = photo;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
photos: sortPhotos(photosCleaned),
|
photos: sortPhotos(photosCleaned),
|
||||||
@@ -214,21 +217,25 @@ export const photosReducer: Reducer<IPhotosState, PhotoAction> = (
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case PhotoTypes.PHOTO_DELETE_SUCCESS: {
|
case PhotoTypes.PHOTOS_DELETE_SUCCESS: {
|
||||||
const delCache = { ...state.deleteCache };
|
const delCache = { ...state.deleteCache };
|
||||||
if (delCache[action.photo.id]) {
|
for (const photo of action.photos) {
|
||||||
delete delCache[action.photo.id];
|
if (delCache[photo.id]) {
|
||||||
|
delete delCache[photo.id];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { ...state, deleteCache: delCache };
|
return { ...state, deleteCache: delCache };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PhotoTypes.PHOTO_DELETE_FAIL:
|
case PhotoTypes.PHOTOS_DELETE_FAIL:
|
||||||
case PhotoTypes.PHOTO_DELETE_CANCEL: {
|
case PhotoTypes.PHOTOS_DELETE_CANCEL: {
|
||||||
const delCache = { ...state.deleteCache };
|
const delCache = { ...state.deleteCache };
|
||||||
let photos: IPhotoReqJSON[] = [...state.photos];
|
let photos: IPhotoReqJSON[] = [...state.photos];
|
||||||
if (delCache[action.photo.id]) {
|
for (const photo of action.photos) {
|
||||||
photos = sortPhotos([...photos, delCache[action.photo.id]]);
|
if (delCache[photo.id]) {
|
||||||
delete delCache[action.photo.id];
|
photos = sortPhotos([...photos, delCache[photo.id]]);
|
||||||
|
delete delCache[photo.id];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { ...state, deleteCache: delCache, photos };
|
return { ...state, deleteCache: delCache, photos };
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -14,21 +14,21 @@ import {
|
|||||||
import * as SparkMD5 from "spark-md5";
|
import * as SparkMD5 from "spark-md5";
|
||||||
import {
|
import {
|
||||||
createPhoto,
|
createPhoto,
|
||||||
deletePhoto,
|
deletePhotos,
|
||||||
fetchPhoto,
|
fetchPhoto,
|
||||||
fetchPhotosList,
|
fetchPhotosList,
|
||||||
uploadPhoto,
|
uploadPhoto,
|
||||||
} from "../../redux/api/photos";
|
} from "../../redux/api/photos";
|
||||||
import {
|
import {
|
||||||
IPhotoDeleteStartAction,
|
IPhotosDeleteStartAction,
|
||||||
IPhotoLoadStartAction,
|
IPhotoLoadStartAction,
|
||||||
IPhotosUploadStartAction,
|
IPhotosUploadStartAction,
|
||||||
photoCreateFail,
|
photoCreateFail,
|
||||||
photoCreateQueue,
|
photoCreateQueue,
|
||||||
photoCreateStart,
|
photoCreateStart,
|
||||||
photoCreateSuccess,
|
photoCreateSuccess,
|
||||||
photoDeleteFail,
|
photosDeleteFail,
|
||||||
photoDeleteSuccess,
|
photosDeleteSuccess,
|
||||||
photoLoadFail,
|
photoLoadFail,
|
||||||
photoLoadSuccess,
|
photoLoadSuccess,
|
||||||
photosLoadFail,
|
photosLoadFail,
|
||||||
@@ -250,34 +250,35 @@ function* photosUpload(action: IPhotosUploadStartAction) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function* photoDelete(action: IPhotoDeleteStartAction) {
|
function* photosDelete(action: IPhotosDeleteStartAction) {
|
||||||
try {
|
try {
|
||||||
const { cancelled } = yield race({
|
const { cancelled } = yield race({
|
||||||
timeout: delay(3000),
|
timeout: delay(3000),
|
||||||
cancelled: take(PhotoTypes.PHOTO_DELETE_CANCEL),
|
//FIXME: what happens if we delete multiple photos and then cancel some of them?
|
||||||
|
cancelled: take(PhotoTypes.PHOTOS_DELETE_CANCEL),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
const { response, timeout } = yield race({
|
const { response, timeout } = yield race({
|
||||||
response: call(deletePhoto, action.photo),
|
response: call(deletePhotos, action.photos),
|
||||||
timeout: delay(10000),
|
timeout: delay(10000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
yield put(photoDeleteFail(action.photo, "Timeout"));
|
yield put(photosDeleteFail(action.photos, "Timeout"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
if (response.data == null) {
|
if (response.data == null) {
|
||||||
yield put(photoDeleteFail(action.photo, response.error));
|
yield put(photosDeleteFail(action.photos, response.error));
|
||||||
} else {
|
} else {
|
||||||
yield put(photoDeleteSuccess(action.photo));
|
yield put(photosDeleteSuccess(action.photos));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
yield put(photoDeleteFail(action.photo, "Internal error"));
|
yield put(photosDeleteFail(action.photos, "Internal error"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +287,7 @@ export function* photosSaga() {
|
|||||||
takeLatest(PhotoTypes.PHOTOS_LOAD_START, photosLoad),
|
takeLatest(PhotoTypes.PHOTOS_LOAD_START, photosLoad),
|
||||||
takeLatest(PhotoTypes.PHOTOS_UPLOAD_START, photosUpload),
|
takeLatest(PhotoTypes.PHOTOS_UPLOAD_START, photosUpload),
|
||||||
takeLatest(PhotoTypes.PHOTO_LOAD_START, photoLoad),
|
takeLatest(PhotoTypes.PHOTO_LOAD_START, photoLoad),
|
||||||
takeEvery(PhotoTypes.PHOTO_DELETE_START, photoDelete),
|
takeEvery(PhotoTypes.PHOTOS_DELETE_START, photosDelete),
|
||||||
takeEvery(PhotoTypes.PHOTO_CREATE_QUEUE, photoCreate),
|
takeEvery(PhotoTypes.PHOTO_CREATE_QUEUE, photoCreate),
|
||||||
takeEvery(PhotoTypes.PHOTO_CREATE_SUCCESS, photoCreate),
|
takeEvery(PhotoTypes.PHOTO_CREATE_SUCCESS, photoCreate),
|
||||||
takeEvery(PhotoTypes.PHOTO_CREATE_FAIL, photoCreate),
|
takeEvery(PhotoTypes.PHOTO_CREATE_FAIL, photoCreate),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": false,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"strictFunctionTypes": true,
|
"strictFunctionTypes": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { getHash, getSize } from "~util";
|
|||||||
import * as jwt from "jsonwebtoken";
|
import * as jwt from "jsonwebtoken";
|
||||||
import { config } from "~config";
|
import { config } from "~config";
|
||||||
import { ValidationError } from "class-validator";
|
import { ValidationError } from "class-validator";
|
||||||
|
import { In } from "typeorm";
|
||||||
|
|
||||||
export const photosRouter = new Router();
|
export const photosRouter = new Router();
|
||||||
|
|
||||||
@@ -277,7 +278,10 @@ photosRouter.get("/photos/showByID/:id/:token", async (ctx) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.request.query["size"] && typeof ctx.request.query["size"] == "string") {
|
if (
|
||||||
|
ctx.request.query["size"] &&
|
||||||
|
typeof ctx.request.query["size"] == "string"
|
||||||
|
) {
|
||||||
const size = parseInt(ctx.request.query["size"]);
|
const size = parseInt(ctx.request.query["size"]);
|
||||||
await send(ctx, await photo.getReadyThumbnailPath(size));
|
await send(ctx, await photo.getReadyThumbnailPath(size));
|
||||||
return;
|
return;
|
||||||
@@ -309,7 +313,10 @@ photosRouter.get("/photos/showByID/:id", async (ctx) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.request.query["size"] && typeof ctx.request.query["size"] == "string") {
|
if (
|
||||||
|
ctx.request.query["size"] &&
|
||||||
|
typeof ctx.request.query["size"] == "string"
|
||||||
|
) {
|
||||||
const size = parseInt(ctx.request.query["size"]);
|
const size = parseInt(ctx.request.query["size"]);
|
||||||
await send(ctx, await photo.getReadyThumbnailPath(size));
|
await send(ctx, await photo.getReadyThumbnailPath(size));
|
||||||
return;
|
return;
|
||||||
@@ -347,7 +354,7 @@ photosRouter.get("/photos/getShowByIDToken/:id", async (ctx) => {
|
|||||||
ctx.body = { error: false, data: token } as IPhotosGetShowTokenByID;
|
ctx.body = { error: false, data: token } as IPhotosGetShowTokenByID;
|
||||||
});
|
});
|
||||||
|
|
||||||
export type IPhotosByIDDeleteRespBody = IAPIResponse<boolean>;
|
export type IPhotoByIDDeleteRespBody = IAPIResponse<boolean>;
|
||||||
photosRouter.delete("/photos/byID/:id", async (ctx) => {
|
photosRouter.delete("/photos/byID/:id", async (ctx) => {
|
||||||
if (!ctx.state.user) {
|
if (!ctx.state.user) {
|
||||||
ctx.throw(401);
|
ctx.throw(401);
|
||||||
@@ -376,5 +383,42 @@ photosRouter.delete("/photos/byID/:id", async (ctx) => {
|
|||||||
ctx.body = {
|
ctx.body = {
|
||||||
error: false,
|
error: false,
|
||||||
data: true,
|
data: true,
|
||||||
} as IPhotosByIDDeleteRespBody;
|
} as IPhotoByIDDeleteRespBody;
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface IPhotosDeleteBody {
|
||||||
|
photos: IPhotoReqJSON[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IPhotosDeleteRespBody = IAPIResponse<boolean>;
|
||||||
|
photosRouter.post("/photos/delete", async (ctx) => {
|
||||||
|
if (!ctx.state.user) {
|
||||||
|
ctx.throw(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = ctx.request.body as IPhotosDeleteBody;
|
||||||
|
const { photos } = body;
|
||||||
|
|
||||||
|
if (!photos || !Array.isArray(photos) || photos.length == 0) {
|
||||||
|
ctx.throw(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = ctx.state;
|
||||||
|
try {
|
||||||
|
await Photo.delete({
|
||||||
|
id: In(photos.map((photo) => photo.id)),
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
error: false,
|
||||||
|
data: true,
|
||||||
|
} as IPhotosDeleteRespBody;
|
||||||
|
} catch (e) {
|
||||||
|
ctx.body = {
|
||||||
|
data: null,
|
||||||
|
error: "Internal server error",
|
||||||
|
} as IPhotosDeleteRespBody;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import * as request from "supertest";
|
|||||||
import { getConnection } from "typeorm";
|
import { getConnection } from "typeorm";
|
||||||
import { app } from "~app";
|
import { app } from "~app";
|
||||||
import { Photo, IPhotoReqJSON } from "~entity/Photo";
|
import { Photo, IPhotoReqJSON } from "~entity/Photo";
|
||||||
import { IPhotosListRespBody, IPhotosNewPostBody } from "~routes/photos";
|
import {
|
||||||
|
IPhotosDeleteBody,
|
||||||
|
IPhotosListRespBody,
|
||||||
|
IPhotosNewPostBody,
|
||||||
|
} from "~routes/photos";
|
||||||
import * as fs from "fs/promises";
|
import * as fs from "fs/promises";
|
||||||
import { constants as fsConstants } from "fs";
|
import { constants as fsConstants } from "fs";
|
||||||
import * as jwt from "jsonwebtoken";
|
import * as jwt from "jsonwebtoken";
|
||||||
@@ -618,10 +622,14 @@ describe("photos", function () {
|
|||||||
512,
|
512,
|
||||||
);
|
);
|
||||||
const response = await request(callback)
|
const response = await request(callback)
|
||||||
.delete(`/photos/byID/${seed.dogPhoto.id}`)
|
.post(`/photos/delete`)
|
||||||
.set({
|
.set({
|
||||||
Authorization: `Bearer ${seed.user2.toJWT()}`,
|
Authorization: `Bearer ${seed.user2.toJWT()}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
})
|
})
|
||||||
|
.send({
|
||||||
|
photos: [await seed.dogPhoto.toReqJSON()],
|
||||||
|
} as IPhotosDeleteBody)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.error).to.be.false;
|
expect(response.body.error).to.be.false;
|
||||||
@@ -636,4 +644,44 @@ describe("photos", function () {
|
|||||||
assert(true);
|
assert(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should delete two photos", async function () {
|
||||||
|
const photo1Path = seed.dogPhoto.getPath();
|
||||||
|
const photo2Path = seed.catPhoto.getPath();
|
||||||
|
const photo1SmallThumbPath = await seed.dogPhoto.getReadyThumbnailPath(
|
||||||
|
512,
|
||||||
|
);
|
||||||
|
const photo2SmallThumbPath = await seed.catPhoto.getReadyThumbnailPath(
|
||||||
|
512,
|
||||||
|
);
|
||||||
|
const response = await request(callback)
|
||||||
|
.post(`/photos/delete`)
|
||||||
|
.set({
|
||||||
|
Authorization: `Bearer ${seed.user2.toJWT()}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
})
|
||||||
|
.send({
|
||||||
|
photos: [
|
||||||
|
await seed.dogPhoto.toReqJSON(),
|
||||||
|
await seed.catPhoto.toReqJSON(),
|
||||||
|
],
|
||||||
|
} as IPhotosDeleteBody)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.error).to.be.false;
|
||||||
|
const dbPhoto1 = await Photo.findOne(seed.dogPhoto.id);
|
||||||
|
expect(dbPhoto1).to.be.undefined;
|
||||||
|
const dbPhoto2 = await Photo.findOne(seed.catPhoto.id);
|
||||||
|
expect(dbPhoto2).to.be.undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.access(photo1Path, fsConstants.F_OK);
|
||||||
|
await fs.access(photo1SmallThumbPath, fsConstants.F_OK);
|
||||||
|
await fs.access(photo2Path, fsConstants.F_OK);
|
||||||
|
await fs.access(photo2SmallThumbPath, fsConstants.F_OK);
|
||||||
|
assert(false);
|
||||||
|
} catch (e) {
|
||||||
|
assert(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user