mirror of
https://github.com/usatiuk/photos.git
synced 2025-10-28 23:37:48 +01:00
select and delete multiple photos
This commit is contained in:
@@ -4,7 +4,36 @@
|
||||
--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%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
@@ -40,11 +69,9 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#photoOverlayDrawer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -89,7 +116,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bp4-overlay-exit {
|
||||
opacity: 1;
|
||||
|
||||
@@ -149,14 +175,11 @@
|
||||
|
||||
.month,
|
||||
.year {
|
||||
|
||||
h3,
|
||||
h2 {
|
||||
|
||||
margin-top: 1rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.list {
|
||||
@@ -203,9 +226,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.bp4-dark {
|
||||
#overview {}
|
||||
}
|
||||
#overview {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,29 @@ import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
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 { LoadingStub } from "../LoadingStub";
|
||||
import { PhotoCard } from "./PhotoCard";
|
||||
import {
|
||||
Alignment,
|
||||
Button,
|
||||
Classes,
|
||||
H1,
|
||||
H2,
|
||||
H3,
|
||||
Navbar,
|
||||
Overlay,
|
||||
Spinner,
|
||||
} from "@blueprintjs/core";
|
||||
import { UploadButton } from "./UploadButton";
|
||||
import { Photo } from "./Photo";
|
||||
import { getPhotoThumbPath } from "../redux/api/photos";
|
||||
import { showDeletionToast } from "../AppToaster";
|
||||
|
||||
export interface IOverviewComponentProps {
|
||||
photos: IPhotoReqJSON[];
|
||||
@@ -27,8 +34,11 @@ export interface IOverviewComponentProps {
|
||||
overviewFetching: boolean;
|
||||
overviewFetchingError: string | null;
|
||||
overviewFetchingSpinner: boolean;
|
||||
darkMode: boolean;
|
||||
|
||||
fetchPhotos: () => void;
|
||||
startDeletePhotos: (photos: IPhotoReqJSON[]) => void;
|
||||
cancelDelete: (photos: IPhotoReqJSON[]) => void;
|
||||
}
|
||||
|
||||
const PhotoCardM = React.memo(PhotoCard);
|
||||
@@ -183,6 +193,49 @@ export const OverviewComponent: React.FunctionComponent<
|
||||
</div>
|
||||
</div>
|
||||
</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="overview">
|
||||
<div id="actionbar">
|
||||
@@ -206,11 +259,18 @@ function mapStateToProps(state: IAppState) {
|
||||
overviewFetchingError: state.photos.overviewFetchingError,
|
||||
overviewFetchingSpinner: state.photos.overviewFetchingSpinner,
|
||||
triedLoading: state.photos.triedLoading,
|
||||
darkMode: state.localSettings.darkMode,
|
||||
};
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -12,7 +12,7 @@ import { IPhotoReqJSON } from "../../../src/entity/Photo";
|
||||
import { getPhotoImgPath, getPhotoThumbPath } from "../redux/api/photos";
|
||||
import { showDeletionToast } from "../AppToaster";
|
||||
import { Dispatch } from "redux";
|
||||
import { photoDeleteCancel, photoDeleteStart } from "../redux/photos/actions";
|
||||
import { photosDeleteCancel, photosDeleteStart } from "../redux/photos/actions";
|
||||
import { connect } from "react-redux";
|
||||
import { LoadingStub } from "../LoadingStub";
|
||||
import { RouteComponentProps, withRouter } from "react-router";
|
||||
@@ -23,8 +23,8 @@ export interface IPhotoCardComponentProps extends RouteComponentProps {
|
||||
selected: boolean;
|
||||
id: string;
|
||||
|
||||
deletePhoto: (photo: IPhotoReqJSON) => void;
|
||||
cancelDelete: (photo: IPhotoReqJSON) => void;
|
||||
deletePhoto: (photos: IPhotoReqJSON[]) => void;
|
||||
cancelDelete: (photos: IPhotoReqJSON[]) => void;
|
||||
onClick: (e: React.MouseEvent<HTMLElement>) => void;
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ export class PhotoCardComponent extends React.PureComponent<
|
||||
}
|
||||
|
||||
public handleDelete(): void {
|
||||
showDeletionToast(() => this.props.cancelDelete(this.props.photo));
|
||||
this.props.deletePhoto(this.props.photo);
|
||||
showDeletionToast(() => this.props.cancelDelete([this.props.photo]));
|
||||
this.props.deletePhoto([this.props.photo]);
|
||||
}
|
||||
/*
|
||||
public handleEdit() {
|
||||
@@ -122,10 +122,10 @@ export class PhotoCardComponent extends React.PureComponent<
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
deletePhoto: (photo: IPhotoReqJSON) =>
|
||||
dispatch(photoDeleteStart(photo)),
|
||||
cancelDelete: (photo: IPhotoReqJSON) =>
|
||||
dispatch(photoDeleteCancel(photo)),
|
||||
deletePhoto: (photos: IPhotoReqJSON[]) =>
|
||||
dispatch(photosDeleteStart(photos)),
|
||||
cancelDelete: (photos: IPhotoReqJSON[]) =>
|
||||
dispatch(photosDeleteCancel(photos)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IPhotoReqJSON } from "../../../../src/entity/Photo";
|
||||
import {
|
||||
IPhotosByIDDeleteRespBody,
|
||||
IPhotosByIDGetRespBody,
|
||||
IPhotosDeleteRespBody,
|
||||
IPhotosListRespBody,
|
||||
IPhotosNewRespBody,
|
||||
IPhotosUploadRespBody,
|
||||
@@ -49,8 +49,8 @@ export async function uploadPhoto(
|
||||
return fetchJSONAuth(`/photos/upload/${id}`, "POST", file);
|
||||
}
|
||||
|
||||
export async function deletePhoto(
|
||||
photo: IPhotoReqJSON,
|
||||
): Promise<IPhotosByIDDeleteRespBody> {
|
||||
return fetchJSONAuth(`/photos/byID/${photo.id}`, "DELETE");
|
||||
export async function deletePhotos(
|
||||
photos: IPhotoReqJSON[],
|
||||
): Promise<IPhotosDeleteRespBody> {
|
||||
return fetchJSONAuth(`/photos/delete`, "POST", { photos });
|
||||
}
|
||||
|
||||
@@ -23,10 +23,10 @@ export enum PhotoTypes {
|
||||
PHOTO_UPLOAD_SUCCESS = "PHOTO_UPLOAD_SUCCESS",
|
||||
PHOTO_UPLOAD_FAIL = "PHOTO_UPLOAD_FAIL",
|
||||
PHOTOS_START_FETCHING_SPINNER = "PHOTOS_START_FETCHING_SPINNER",
|
||||
PHOTO_DELETE_START = "PHOTO_DELETE_START",
|
||||
PHOTO_DELETE_SUCCESS = "PHOTO_DELETE_SUCCESS",
|
||||
PHOTO_DELETE_FAIL = "PHOTO_DELETE_FAIL",
|
||||
PHOTO_DELETE_CANCEL = "PHOTO_DELETE_CANCEL",
|
||||
PHOTOS_DELETE_START = "PHOTOS_DELETE_START",
|
||||
PHOTOS_DELETE_SUCCESS = "PHOTOS_DELETE_SUCCESS",
|
||||
PHOTOS_DELETE_FAIL = "PHOTOS_DELETE_FAIL",
|
||||
PHOTOS_DELETE_CANCEL = "PHOTOS_DELETE_CANCEL",
|
||||
}
|
||||
|
||||
export interface IPhotosLoadStartAction extends Action {
|
||||
@@ -109,25 +109,25 @@ export interface IPhotoCreateFailAction extends Action {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface IPhotoDeleteStartAction extends Action {
|
||||
type: PhotoTypes.PHOTO_DELETE_START;
|
||||
photo: IPhotoReqJSON;
|
||||
export interface IPhotosDeleteStartAction extends Action {
|
||||
type: PhotoTypes.PHOTOS_DELETE_START;
|
||||
photos: IPhotoReqJSON[];
|
||||
}
|
||||
|
||||
export interface IPhotoDeleteSuccessAction extends Action {
|
||||
type: PhotoTypes.PHOTO_DELETE_SUCCESS;
|
||||
photo: IPhotoReqJSON;
|
||||
export interface IPhotosDeleteSuccessAction extends Action {
|
||||
type: PhotoTypes.PHOTOS_DELETE_SUCCESS;
|
||||
photos: IPhotoReqJSON[];
|
||||
}
|
||||
|
||||
export interface IPhotoDeleteFailAction extends Action {
|
||||
type: PhotoTypes.PHOTO_DELETE_FAIL;
|
||||
photo: IPhotoReqJSON;
|
||||
export interface IPhotosDeleteFailAction extends Action {
|
||||
type: PhotoTypes.PHOTOS_DELETE_FAIL;
|
||||
photos: IPhotoReqJSON[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface IPhotoDeleteCancelAction extends Action {
|
||||
type: PhotoTypes.PHOTO_DELETE_CANCEL;
|
||||
photo: IPhotoReqJSON;
|
||||
export interface IPhotosDeleteCancelAction extends Action {
|
||||
type: PhotoTypes.PHOTOS_DELETE_CANCEL;
|
||||
photos: IPhotoReqJSON[];
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
export function photoDeleteStart(
|
||||
photo: IPhotoReqJSON,
|
||||
): IPhotoDeleteStartAction {
|
||||
return { type: PhotoTypes.PHOTO_DELETE_START, photo };
|
||||
export function photosDeleteStart(
|
||||
photos: IPhotoReqJSON[],
|
||||
): IPhotosDeleteStartAction {
|
||||
return { type: PhotoTypes.PHOTOS_DELETE_START, photos };
|
||||
}
|
||||
export function photoDeleteSuccess(
|
||||
photo: IPhotoReqJSON,
|
||||
): IPhotoDeleteSuccessAction {
|
||||
return { type: PhotoTypes.PHOTO_DELETE_SUCCESS, photo };
|
||||
export function photosDeleteSuccess(
|
||||
photos: IPhotoReqJSON[],
|
||||
): IPhotosDeleteSuccessAction {
|
||||
return { type: PhotoTypes.PHOTOS_DELETE_SUCCESS, photos };
|
||||
}
|
||||
export function photoDeleteFail(
|
||||
photo: IPhotoReqJSON,
|
||||
export function photosDeleteFail(
|
||||
photos: IPhotoReqJSON[],
|
||||
error?: string,
|
||||
): IPhotoDeleteFailAction {
|
||||
return { type: PhotoTypes.PHOTO_DELETE_FAIL, photo, error };
|
||||
): IPhotosDeleteFailAction {
|
||||
return { type: PhotoTypes.PHOTOS_DELETE_FAIL, photos, error };
|
||||
}
|
||||
|
||||
export function photoDeleteCancel(
|
||||
photo: IPhotoReqJSON,
|
||||
): IPhotoDeleteCancelAction {
|
||||
return { type: PhotoTypes.PHOTO_DELETE_CANCEL, photo };
|
||||
export function photosDeleteCancel(
|
||||
photos: IPhotoReqJSON[],
|
||||
): IPhotosDeleteCancelAction {
|
||||
return { type: PhotoTypes.PHOTOS_DELETE_CANCEL, photos };
|
||||
}
|
||||
|
||||
export function photosStartFetchingSpinner(): IPhotosStartFetchingSpinner {
|
||||
@@ -257,10 +257,10 @@ export type PhotoAction =
|
||||
| IPhotoCreateSuccessAction
|
||||
| IPhotoUploadFailAction
|
||||
| IPhotoUploadSuccessAction
|
||||
| IPhotoDeleteFailAction
|
||||
| IPhotoDeleteStartAction
|
||||
| IPhotoDeleteSuccessAction
|
||||
| IPhotoDeleteCancelAction
|
||||
| IPhotosDeleteFailAction
|
||||
| IPhotosDeleteStartAction
|
||||
| IPhotosDeleteSuccessAction
|
||||
| IPhotosDeleteCancelAction
|
||||
| IPhotoLoadFailAction
|
||||
| IPhotoLoadStartAction
|
||||
| IPhotoLoadSuccessAction
|
||||
|
||||
@@ -196,15 +196,18 @@ export const photosReducer: Reducer<IPhotosState, PhotoAction> = (
|
||||
photosUploading: state.photosUploading - 1,
|
||||
};
|
||||
}
|
||||
case PhotoTypes.PHOTO_DELETE_START: {
|
||||
case PhotoTypes.PHOTOS_DELETE_START: {
|
||||
const photos = state.photos;
|
||||
const delPhoto = photos.find((p) => p.id === action.photo.id);
|
||||
if (delPhoto) {
|
||||
const photoIds = action.photos.map((p) => p.id);
|
||||
const delPhotos = photos.find((p) => photoIds.includes(p.id));
|
||||
if (delPhotos) {
|
||||
const photosCleaned = photos.filter(
|
||||
(p) => p.id !== action.photo.id,
|
||||
(p) => !photoIds.includes(p.id),
|
||||
);
|
||||
const delCache = { ...state.deleteCache };
|
||||
delCache[delPhoto?.id] = delPhoto;
|
||||
for (const photo of action.photos) {
|
||||
delCache[photo.id] = photo;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
photos: sortPhotos(photosCleaned),
|
||||
@@ -214,21 +217,25 @@ export const photosReducer: Reducer<IPhotosState, PhotoAction> = (
|
||||
return state;
|
||||
}
|
||||
}
|
||||
case PhotoTypes.PHOTO_DELETE_SUCCESS: {
|
||||
case PhotoTypes.PHOTOS_DELETE_SUCCESS: {
|
||||
const delCache = { ...state.deleteCache };
|
||||
if (delCache[action.photo.id]) {
|
||||
delete delCache[action.photo.id];
|
||||
for (const photo of action.photos) {
|
||||
if (delCache[photo.id]) {
|
||||
delete delCache[photo.id];
|
||||
}
|
||||
}
|
||||
return { ...state, deleteCache: delCache };
|
||||
break;
|
||||
}
|
||||
case PhotoTypes.PHOTO_DELETE_FAIL:
|
||||
case PhotoTypes.PHOTO_DELETE_CANCEL: {
|
||||
case PhotoTypes.PHOTOS_DELETE_FAIL:
|
||||
case PhotoTypes.PHOTOS_DELETE_CANCEL: {
|
||||
const delCache = { ...state.deleteCache };
|
||||
let photos: IPhotoReqJSON[] = [...state.photos];
|
||||
if (delCache[action.photo.id]) {
|
||||
photos = sortPhotos([...photos, delCache[action.photo.id]]);
|
||||
delete delCache[action.photo.id];
|
||||
for (const photo of action.photos) {
|
||||
if (delCache[photo.id]) {
|
||||
photos = sortPhotos([...photos, delCache[photo.id]]);
|
||||
delete delCache[photo.id];
|
||||
}
|
||||
}
|
||||
return { ...state, deleteCache: delCache, photos };
|
||||
break;
|
||||
|
||||
@@ -14,21 +14,21 @@ import {
|
||||
import * as SparkMD5 from "spark-md5";
|
||||
import {
|
||||
createPhoto,
|
||||
deletePhoto,
|
||||
deletePhotos,
|
||||
fetchPhoto,
|
||||
fetchPhotosList,
|
||||
uploadPhoto,
|
||||
} from "../../redux/api/photos";
|
||||
import {
|
||||
IPhotoDeleteStartAction,
|
||||
IPhotosDeleteStartAction,
|
||||
IPhotoLoadStartAction,
|
||||
IPhotosUploadStartAction,
|
||||
photoCreateFail,
|
||||
photoCreateQueue,
|
||||
photoCreateStart,
|
||||
photoCreateSuccess,
|
||||
photoDeleteFail,
|
||||
photoDeleteSuccess,
|
||||
photosDeleteFail,
|
||||
photosDeleteSuccess,
|
||||
photoLoadFail,
|
||||
photoLoadSuccess,
|
||||
photosLoadFail,
|
||||
@@ -250,34 +250,35 @@ function* photosUpload(action: IPhotosUploadStartAction) {
|
||||
}
|
||||
}
|
||||
|
||||
function* photoDelete(action: IPhotoDeleteStartAction) {
|
||||
function* photosDelete(action: IPhotosDeleteStartAction) {
|
||||
try {
|
||||
const { cancelled } = yield race({
|
||||
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) {
|
||||
const { response, timeout } = yield race({
|
||||
response: call(deletePhoto, action.photo),
|
||||
response: call(deletePhotos, action.photos),
|
||||
timeout: delay(10000),
|
||||
});
|
||||
|
||||
if (timeout) {
|
||||
yield put(photoDeleteFail(action.photo, "Timeout"));
|
||||
yield put(photosDeleteFail(action.photos, "Timeout"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (response) {
|
||||
if (response.data == null) {
|
||||
yield put(photoDeleteFail(action.photo, response.error));
|
||||
yield put(photosDeleteFail(action.photos, response.error));
|
||||
} else {
|
||||
yield put(photoDeleteSuccess(action.photo));
|
||||
yield put(photosDeleteSuccess(action.photos));
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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_UPLOAD_START, photosUpload),
|
||||
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_SUCCESS, photoCreate),
|
||||
takeEvery(PhotoTypes.PHOTO_CREATE_FAIL, photoCreate),
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitAny": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictNullChecks": true,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getHash, getSize } from "~util";
|
||||
import * as jwt from "jsonwebtoken";
|
||||
import { config } from "~config";
|
||||
import { ValidationError } from "class-validator";
|
||||
import { In } from "typeorm";
|
||||
|
||||
export const photosRouter = new Router();
|
||||
|
||||
@@ -277,7 +278,10 @@ photosRouter.get("/photos/showByID/:id/:token", async (ctx) => {
|
||||
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"]);
|
||||
await send(ctx, await photo.getReadyThumbnailPath(size));
|
||||
return;
|
||||
@@ -309,7 +313,10 @@ photosRouter.get("/photos/showByID/:id", async (ctx) => {
|
||||
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"]);
|
||||
await send(ctx, await photo.getReadyThumbnailPath(size));
|
||||
return;
|
||||
@@ -347,7 +354,7 @@ photosRouter.get("/photos/getShowByIDToken/:id", async (ctx) => {
|
||||
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) => {
|
||||
if (!ctx.state.user) {
|
||||
ctx.throw(401);
|
||||
@@ -376,5 +383,42 @@ photosRouter.delete("/photos/byID/:id", async (ctx) => {
|
||||
ctx.body = {
|
||||
error: false,
|
||||
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 { app } from "~app";
|
||||
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 { constants as fsConstants } from "fs";
|
||||
import * as jwt from "jsonwebtoken";
|
||||
@@ -618,10 +622,14 @@ describe("photos", function () {
|
||||
512,
|
||||
);
|
||||
const response = await request(callback)
|
||||
.delete(`/photos/byID/${seed.dogPhoto.id}`)
|
||||
.post(`/photos/delete`)
|
||||
.set({
|
||||
Authorization: `Bearer ${seed.user2.toJWT()}`,
|
||||
"Content-Type": "application/json",
|
||||
})
|
||||
.send({
|
||||
photos: [await seed.dogPhoto.toReqJSON()],
|
||||
} as IPhotosDeleteBody)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.error).to.be.false;
|
||||
@@ -636,4 +644,44 @@ describe("photos", function () {
|
||||
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