select and delete multiple photos

This commit is contained in:
2022-05-02 22:03:06 +02:00
committed by Stepan Usatiuk
parent 3800f19768
commit ff058b7487
10 changed files with 277 additions and 94 deletions

View File

@@ -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 {
} }
}

View File

@@ -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(

View File

@@ -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)),
}; };
} }

View File

@@ -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 });
} }

View File

@@ -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

View File

@@ -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;

View File

@@ -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),

View File

@@ -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,

View File

@@ -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;
}
}); });

View File

@@ -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);
}
});
}); });