Add extra pagination api's
This commit is contained in:
parent
966954acc7
commit
51675fcf32
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"vsicons.presets.angular": true
|
||||
}
|
53
.vscode/tasks.json
vendored
53
.vscode/tasks.json
vendored
|
@ -1,53 +0,0 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Start full",
|
||||
"dependsOn": [
|
||||
"Start backend",
|
||||
"Start frontend",
|
||||
"Start postgres",
|
||||
"Start shared"
|
||||
],
|
||||
"dependsOrder": "parallel",
|
||||
"isBackground": true,
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "Start shared",
|
||||
"command": "yarn start",
|
||||
"options": {
|
||||
"cwd": "./shared"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "Start backend",
|
||||
"command": "yarn start:dev",
|
||||
"options": {
|
||||
"cwd": "./backend"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "Start frontend",
|
||||
"command": "yarn watch",
|
||||
"options": {
|
||||
"cwd": "./frontend"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "Start postgres",
|
||||
"command": "podman-compose -f ./dev.docker-compose.yml stop; podman-compose -f ./dev.docker-compose.yml up",
|
||||
"options": {
|
||||
"cwd": "./support"
|
||||
},
|
||||
"group": "build"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { AsyncFailable, Fail } from 'picsur-shared/dist/types';
|
||||
import { FindResult } from 'picsur-shared/dist/types/find-result';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { EImageDerivativeBackend } from '../../models/entities/image-derivative.entity';
|
||||
import { EImageFileBackend } from '../../models/entities/image-file.entity';
|
||||
|
@ -53,12 +54,12 @@ export class ImageDBService {
|
|||
count: number,
|
||||
page: number,
|
||||
userid: string | undefined,
|
||||
): AsyncFailable<EImageBackend[]> {
|
||||
): AsyncFailable<FindResult<EImageBackend>> {
|
||||
if (count < 1 || page < 0) return Fail('Invalid page');
|
||||
if (count > 100) return Fail('Too many results');
|
||||
|
||||
try {
|
||||
const found = await this.imageRepo.find({
|
||||
const [found, amount] = await this.imageRepo.findAndCount({
|
||||
skip: count * page,
|
||||
take: count,
|
||||
where: {
|
||||
|
@ -67,7 +68,13 @@ export class ImageDBService {
|
|||
});
|
||||
|
||||
if (found === undefined) return Fail('Images not found');
|
||||
return found;
|
||||
|
||||
return {
|
||||
results: found,
|
||||
totalResults: amount,
|
||||
page,
|
||||
pages: Math.ceil(amount / count),
|
||||
};
|
||||
} catch (e) {
|
||||
return Fail(e);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
HasFailed,
|
||||
HasSuccess
|
||||
} from 'picsur-shared/dist/types';
|
||||
import { FindResult } from 'picsur-shared/dist/types/find-result';
|
||||
import { makeUnique } from 'picsur-shared/dist/util/unique';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Permissions } from '../../models/constants/permissions.const';
|
||||
|
@ -95,7 +96,7 @@ export class UsersService {
|
|||
|
||||
if (ImmutableUsersList.includes(userToModify.username)) {
|
||||
// Just fail silently
|
||||
this.logger.verbose("User tried to modify system user, failed silently");
|
||||
this.logger.verbose('User tried to modify system user, failed silently');
|
||||
return userToModify;
|
||||
}
|
||||
|
||||
|
@ -213,15 +214,24 @@ export class UsersService {
|
|||
public async findMany(
|
||||
count: number,
|
||||
page: number,
|
||||
): AsyncFailable<EUserBackend[]> {
|
||||
): AsyncFailable<FindResult<EUserBackend>> {
|
||||
if (count < 1 || page < 0) return Fail('Invalid page');
|
||||
if (count > 100) return Fail('Too many results');
|
||||
|
||||
try {
|
||||
return await this.usersRepository.find({
|
||||
const [users, amount] = await this.usersRepository.findAndCount({
|
||||
take: count,
|
||||
skip: count * page,
|
||||
});
|
||||
|
||||
if (users === undefined) return Fail('Users not found');
|
||||
|
||||
return {
|
||||
results: users,
|
||||
totalResults: amount,
|
||||
page,
|
||||
pages: Math.ceil(amount / count),
|
||||
};
|
||||
} catch (e) {
|
||||
return Fail(e);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { FullMime } from 'picsur-shared/dist/dto/mimes.dto';
|
|||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.dto';
|
||||
import { UsrPreference } from 'picsur-shared/dist/dto/usr-preferences.dto';
|
||||
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { FindResult } from 'picsur-shared/dist/types/find-result';
|
||||
import { ParseMime } from 'picsur-shared/dist/util/parse-mime';
|
||||
import { IsQOI } from 'qoi-img';
|
||||
import { ImageDBService } from '../../collections/image-db/image-db.service';
|
||||
|
@ -33,9 +34,7 @@ export class ImageManagerService {
|
|||
private readonly sysPref: SysPreferenceService,
|
||||
) {}
|
||||
|
||||
public async findOne(
|
||||
id: string,
|
||||
): AsyncFailable<EImageBackend> {
|
||||
public async findOne(id: string): AsyncFailable<EImageBackend> {
|
||||
return await this.imagesService.findOne(id, undefined);
|
||||
}
|
||||
|
||||
|
@ -43,7 +42,7 @@ export class ImageManagerService {
|
|||
count: number,
|
||||
page: number,
|
||||
userid: string | undefined,
|
||||
): AsyncFailable<EImageBackend[]> {
|
||||
): AsyncFailable<FindResult<EImageBackend>> {
|
||||
return await this.imagesService.findMany(count, page, userid);
|
||||
}
|
||||
|
||||
|
@ -54,7 +53,7 @@ export class ImageManagerService {
|
|||
const images = await this.imagesService.findList(ids, userid);
|
||||
if (HasFailed(images)) return images;
|
||||
|
||||
const availableIds = images.map(image => image.id);
|
||||
const availableIds = images.map((image) => image.id);
|
||||
|
||||
const deleteResult = await this.imagesService.delete(availableIds);
|
||||
if (HasFailed(deleteResult)) return deleteResult;
|
||||
|
|
|
@ -43,16 +43,16 @@ export class UserAdminController {
|
|||
async listUsersPaged(
|
||||
@Body() body: UserListRequest,
|
||||
): Promise<UserListResponse> {
|
||||
const users = await this.usersService.findMany(body.count, body.page);
|
||||
if (HasFailed(users)) {
|
||||
this.logger.warn(users.getReason());
|
||||
const found = await this.usersService.findMany(body.count, body.page);
|
||||
if (HasFailed(found)) {
|
||||
this.logger.warn(found.getReason());
|
||||
throw new InternalServerErrorException('Could not list users');
|
||||
}
|
||||
|
||||
return {
|
||||
users: users.map(EUserBackend2EUser),
|
||||
count: users.length,
|
||||
page: body.page,
|
||||
users: found.results.map(EUserBackend2EUser),
|
||||
page: found.page,
|
||||
pages: found.pages,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -61,20 +61,20 @@ export class ImageManageController {
|
|||
body.user_id = userid;
|
||||
}
|
||||
|
||||
const images = await this.imagesService.findMany(
|
||||
const found = await this.imagesService.findMany(
|
||||
body.count,
|
||||
body.page,
|
||||
body.user_id,
|
||||
);
|
||||
if (HasFailed(images)) {
|
||||
this.logger.warn(images.getReason());
|
||||
if (HasFailed(found)) {
|
||||
this.logger.warn(found.getReason());
|
||||
throw new InternalServerErrorException('Could not list images');
|
||||
}
|
||||
|
||||
return {
|
||||
images,
|
||||
count: images.length,
|
||||
page: body.page,
|
||||
images: found.results,
|
||||
page: found.page,
|
||||
pages: found.pages,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,6 @@ export class ImageManageController {
|
|||
|
||||
return {
|
||||
images: deletedImages,
|
||||
count: deletedImages.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<div class="sidenav-content" [class.container]="wrapContentWithContainer">
|
||||
<div class="header-spacer"></div>
|
||||
<div
|
||||
class="grow-full"
|
||||
class="grow-full relative"
|
||||
[class.container]="wrapContentWithContainer"
|
||||
[@mainAnimation]="getRouteAnimData()"
|
||||
>
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-spacer {
|
||||
height: 64px;
|
||||
margin-bottom: 32px;
|
||||
|
|
|
@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
|
|||
import { RouterModule } from '@angular/router';
|
||||
import { PRoutes } from './models/dto/picsur-routes.dto';
|
||||
import { ErrorsRouteModule } from './routes/errors/errors.module';
|
||||
import { ImagesRouteModule } from './routes/images/images.module';
|
||||
import { ProcessingRouteModule } from './routes/processing/processing.module';
|
||||
import { SettingsRouteModule } from './routes/settings/settings.module';
|
||||
import { UploadRouteModule } from './routes/upload/upload.module';
|
||||
|
@ -30,6 +31,10 @@ const routes: PRoutes = [
|
|||
path: 'user',
|
||||
loadChildren: () => UserRouteModule,
|
||||
},
|
||||
{
|
||||
path: 'images',
|
||||
loadChildren: () => ImagesRouteModule,
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
loadChildren: () => SettingsRouteModule,
|
||||
|
|
|
@ -56,6 +56,10 @@
|
|||
<h2>{{ user?.username }}</h2>
|
||||
</div>
|
||||
</span>
|
||||
<button *ngIf="canUpload" mat-menu-item (click)="doImages()">
|
||||
<mat-icon fontSet="material-icons-outlined">image</mat-icon>
|
||||
<span>My Images</span>
|
||||
</button>
|
||||
<button *ngIf="canAccessSettings" mat-menu-item (click)="doSettings()">
|
||||
<mat-icon fontSet="material-icons-outlined">settings</mat-icon>
|
||||
<span>Settings</span>
|
||||
|
|
|
@ -83,4 +83,8 @@ export class HeaderComponent implements OnInit {
|
|||
doUpload() {
|
||||
this.router.navigate(['/upload']);
|
||||
}
|
||||
|
||||
doImages() {
|
||||
this.router.navigate(['/images']);
|
||||
}
|
||||
}
|
||||
|
|
1
frontend/src/app/routes/images/images.component.html
Normal file
1
frontend/src/app/routes/images/images.component.html
Normal file
|
@ -0,0 +1 @@
|
|||
hello
|
12
frontend/src/app/routes/images/images.component.ts
Normal file
12
frontend/src/app/routes/images/images.component.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
templateUrl: './images.component.html',
|
||||
styleUrls: ['./images.component.scss'],
|
||||
})
|
||||
export class ImagesComponent implements OnInit {
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
10
frontend/src/app/routes/images/images.module.ts
Normal file
10
frontend/src/app/routes/images/images.module.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ImagesComponent } from './images.component';
|
||||
import { ImagesRoutingModule } from './images.routing.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ImagesComponent],
|
||||
imports: [CommonModule, ImagesRoutingModule],
|
||||
})
|
||||
export class ImagesRouteModule {}
|
28
frontend/src/app/routes/images/images.routing.module.ts
Normal file
28
frontend/src/app/routes/images/images.routing.module.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { Permission } from 'picsur-shared/dist/dto/permissions.dto';
|
||||
import { PermissionGuard } from 'src/app/guards/permission.guard';
|
||||
import { PRoutes } from 'src/app/models/dto/picsur-routes.dto';
|
||||
import { ImagesComponent } from './images.component';
|
||||
|
||||
const routes: PRoutes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: '0',
|
||||
},
|
||||
{
|
||||
path: ':page',
|
||||
component: ImagesComponent,
|
||||
canActivate: [PermissionGuard],
|
||||
data: {
|
||||
permissions: [Permission.ImageUpload],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ImagesRoutingModule {}
|
|
@ -1,10 +1,17 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { ImageUploadResponse } from 'picsur-shared/dist/dto/api/image-manage.dto';
|
||||
import {
|
||||
ImageDeleteRequest,
|
||||
ImageDeleteResponse,
|
||||
ImageListRequest,
|
||||
ImageListResponse,
|
||||
ImageUploadResponse
|
||||
} from 'picsur-shared/dist/dto/api/image-manage.dto';
|
||||
import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto';
|
||||
import { ImageLinks } from 'picsur-shared/dist/dto/image-links.dto';
|
||||
import { Mime2Ext } from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { EImage } from 'picsur-shared/dist/entities/image.entity';
|
||||
import { AsyncFailable } from 'picsur-shared/dist/types';
|
||||
import { Open } from 'picsur-shared/dist/types/failable';
|
||||
import { Fail, HasFailed, Open } from 'picsur-shared/dist/types/failable';
|
||||
import { ImageUploadRequest } from '../../models/dto/image-upload-request.dto';
|
||||
import { ApiService } from './api.service';
|
||||
|
||||
|
@ -28,6 +35,53 @@ export class ImageService {
|
|||
return await this.api.get(ImageMetaResponse, `/i/meta/${image}`);
|
||||
}
|
||||
|
||||
public async ListImages(
|
||||
count: number,
|
||||
page: number,
|
||||
userID?: string
|
||||
): AsyncFailable<EImage[]> {
|
||||
const result = await this.api.post(
|
||||
ImageListRequest,
|
||||
ImageListResponse,
|
||||
'/api/image/list',
|
||||
{
|
||||
count,
|
||||
page,
|
||||
user_id: userID,
|
||||
}
|
||||
);
|
||||
|
||||
return Open(result, 'images');
|
||||
}
|
||||
|
||||
public async DeleteImages(
|
||||
images: string[]
|
||||
): AsyncFailable<ImageDeleteResponse> {
|
||||
return await this.api.post(
|
||||
ImageDeleteRequest,
|
||||
ImageDeleteResponse,
|
||||
'/api/image/delete',
|
||||
{
|
||||
ids: images,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public async DeleteImage(image: string): AsyncFailable<EImage> {
|
||||
const result = await this.DeleteImages([image]);
|
||||
if (HasFailed(result)) return result;
|
||||
|
||||
if (result.images.length !== 1) {
|
||||
return Fail(
|
||||
`Image ${image} was not deleted, probably lacking permissions`
|
||||
);
|
||||
}
|
||||
|
||||
return result.images[0];
|
||||
}
|
||||
|
||||
// Non api calls
|
||||
|
||||
public GetImageURL(image: string, mime: string | null): string {
|
||||
const baseURL = window.location.protocol + '//' + window.location.host;
|
||||
const extension = mime !== null ? Mime2Ext(mime) ?? 'error' : null;
|
||||
|
|
|
@ -1,23 +1,77 @@
|
|||
{
|
||||
"folders": [
|
||||
{
|
||||
"name": "shared",
|
||||
"path": "shared"
|
||||
},
|
||||
{
|
||||
"name": "backend",
|
||||
"path": "backend"
|
||||
},
|
||||
{
|
||||
"name": "frontend",
|
||||
"path": "frontend"
|
||||
},
|
||||
{
|
||||
"name": "Picsur-Monorepo",
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"vsicons.presets.angular": true
|
||||
}
|
||||
"folders": [
|
||||
{
|
||||
"name": "shared",
|
||||
"path": "shared"
|
||||
},
|
||||
{
|
||||
"name": "backend",
|
||||
"path": "backend"
|
||||
},
|
||||
{
|
||||
"name": "frontend",
|
||||
"path": "frontend"
|
||||
},
|
||||
{
|
||||
"name": "support",
|
||||
"path": "support"
|
||||
}
|
||||
],
|
||||
"tasks": {
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Start full",
|
||||
"dependsOn": [
|
||||
"Start backend",
|
||||
"Start frontend",
|
||||
"Start postgres",
|
||||
"Start shared"
|
||||
],
|
||||
"dependsOrder": "parallel",
|
||||
"isBackground": true,
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "Start shared",
|
||||
"command": "yarn start",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder:shared}"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "Start backend",
|
||||
"command": "yarn start:dev",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder:backend}"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "Start frontend",
|
||||
"command": "yarn watch",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder:frontend}"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "Start postgres",
|
||||
"command": "podman-compose -f ./dev.docker-compose.yml stop; podman-compose -f ./dev.docker-compose.yml up",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder:support}"
|
||||
},
|
||||
"group": "build"
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"vsicons.presets.angular": true,
|
||||
"skipRefreshExplorerOnWindowFocus": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,8 @@ export class ImageListRequest extends createZodDto(ImageListRequestSchema) {}
|
|||
|
||||
export const ImageListResponseSchema = z.object({
|
||||
images: z.array(EImageSchema),
|
||||
count: IsPosInt(),
|
||||
page: IsPosInt(),
|
||||
pages: IsPosInt(),
|
||||
});
|
||||
export class ImageListResponse extends createZodDto(ImageListResponseSchema) {}
|
||||
|
||||
|
@ -36,7 +36,6 @@ export class ImageDeleteRequest extends createZodDto(
|
|||
|
||||
export const ImageDeleteResponseSchema = z.object({
|
||||
images: z.array(EImageSchema),
|
||||
count: IsPosInt(),
|
||||
});
|
||||
export class ImageDeleteResponse extends createZodDto(
|
||||
ImageDeleteResponseSchema,
|
||||
|
|
|
@ -16,8 +16,8 @@ export class UserListRequest extends createZodDto(UserListRequestSchema) {}
|
|||
|
||||
export const UserListResponseSchema = z.object({
|
||||
users: z.array(EUserSchema),
|
||||
count: IsPosInt(),
|
||||
page: IsPosInt(),
|
||||
pages: IsPosInt(),
|
||||
});
|
||||
export class UserListResponse extends createZodDto(UserListResponseSchema) {}
|
||||
|
||||
|
|
7
shared/src/types/find-result.ts
Normal file
7
shared/src/types/find-result.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export interface FindResult<T> {
|
||||
results: T[];
|
||||
totalResults: number;
|
||||
|
||||
page: number;
|
||||
pages: number;
|
||||
}
|
Loading…
Reference in a new issue