You've already forked pigallery2
mirror of
https://github.com/bpatrik/pigallery2.git
synced 2025-11-29 22:48:28 +02:00
Merge pull request #316 from bpatrik/feature/saved-search
Creating saved search as Albums #45
This commit is contained in:
63
src/backend/middlewares/AlbumMWs.ts
Normal file
63
src/backend/middlewares/AlbumMWs.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {NextFunction, Request, Response} from 'express';
|
||||
import {ErrorCodes, ErrorDTO} from '../../common/entities/Error';
|
||||
import {ObjectManagers} from '../model/ObjectManagers';
|
||||
import {Utils} from '../../common/Utils';
|
||||
import {Config} from '../../common/config/private/Config';
|
||||
|
||||
|
||||
export class AlbumMWs {
|
||||
|
||||
|
||||
public static async listAlbums(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
if (Config.Client.Album.enabled === false) {
|
||||
return next();
|
||||
}
|
||||
try {
|
||||
req.resultPipe = await ObjectManagers.getInstance()
|
||||
.AlbumManager.getAlbums();
|
||||
return next();
|
||||
|
||||
} catch (err) {
|
||||
return next(new ErrorDTO(ErrorCodes.ALBUM_ERROR, 'Error during listing albums', err));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static async deleteAlbum(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
if (Config.Client.Album.enabled === false) {
|
||||
return next();
|
||||
}
|
||||
if (!req.params.id || !Utils.isUInt32(parseInt(req.params.id, 10))) {
|
||||
return next();
|
||||
}
|
||||
try {
|
||||
await ObjectManagers.getInstance().AlbumManager.deleteAlbum(parseInt(req.params.id, 10));
|
||||
req.resultPipe = 'ok';
|
||||
return next();
|
||||
|
||||
} catch (err) {
|
||||
return next(new ErrorDTO(ErrorCodes.ALBUM_ERROR, 'Error during deleting albums', err));
|
||||
}
|
||||
}
|
||||
|
||||
public static async createSavedSearch(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
if (Config.Client.Album.enabled === false) {
|
||||
return next();
|
||||
}
|
||||
if ((typeof req.body === 'undefined') || (typeof req.body.name !== 'string') || (typeof req.body.searchQuery !== 'object')) {
|
||||
return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'updateSharing filed is missing'));
|
||||
}
|
||||
try {
|
||||
await ObjectManagers.getInstance().AlbumManager.addSavedSearch(req.body.name, req.body.searchQuery);
|
||||
req.resultPipe = 'ok';
|
||||
return next();
|
||||
|
||||
} catch (err) {
|
||||
return next(new ErrorDTO(ErrorCodes.ALBUM_ERROR, 'Error during creating saved search albums', err));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ export class SharingMWs {
|
||||
|
||||
try {
|
||||
req.resultPipe = await ObjectManagers.getInstance().SharingManager.deleteSharing(sharingKey);
|
||||
req.resultPipe = 'ok';
|
||||
return next();
|
||||
} catch (err) {
|
||||
return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'Error during deleting sharing', err));
|
||||
|
||||
@@ -9,6 +9,8 @@ import {IPersonManager} from './database/interfaces/IPersonManager';
|
||||
import {IVersionManager} from './database/interfaces/IVersionManager';
|
||||
import {IJobManager} from './database/interfaces/IJobManager';
|
||||
import {LocationManager} from './database/LocationManager';
|
||||
import {IAlbumManager} from './database/interfaces/IAlbumManager';
|
||||
import {JobManager} from './jobs/JobManager';
|
||||
|
||||
const LOG_TAG = '[ObjectManagers]';
|
||||
|
||||
@@ -25,6 +27,7 @@ export class ObjectManagers {
|
||||
private versionManager: IVersionManager;
|
||||
private jobManager: IJobManager;
|
||||
private locationManager: LocationManager;
|
||||
private albumManager: IAlbumManager;
|
||||
|
||||
|
||||
get VersionManager(): IVersionManager {
|
||||
@@ -43,6 +46,14 @@ export class ObjectManagers {
|
||||
this.locationManager = value;
|
||||
}
|
||||
|
||||
get AlbumManager(): IAlbumManager {
|
||||
return this.albumManager;
|
||||
}
|
||||
|
||||
set AlbumManager(value: IAlbumManager) {
|
||||
this.albumManager = value;
|
||||
}
|
||||
|
||||
get PersonManager(): IPersonManager {
|
||||
return this.personManager;
|
||||
}
|
||||
@@ -121,51 +132,30 @@ export class ObjectManagers {
|
||||
}
|
||||
|
||||
|
||||
public static async InitCommonManagers(): Promise<void> {
|
||||
const JobManager = require('./jobs/JobManager').JobManager;
|
||||
ObjectManagers.getInstance().JobManager = new JobManager();
|
||||
}
|
||||
|
||||
public static async InitMemoryManagers(): Promise<void> {
|
||||
await ObjectManagers.reset();
|
||||
const GalleryManager = require('./database/memory/GalleryManager').GalleryManager;
|
||||
const UserManager = require('./database/memory/UserManager').UserManager;
|
||||
const SearchManager = require('./database/memory/SearchManager').SearchManager;
|
||||
const SharingManager = require('./database/memory/SharingManager').SharingManager;
|
||||
const IndexingManager = require('./database/memory/IndexingManager').IndexingManager;
|
||||
const PersonManager = require('./database/memory/PersonManager').PersonManager;
|
||||
const VersionManager = require('./database/memory/VersionManager').VersionManager;
|
||||
ObjectManagers.getInstance().GalleryManager = new GalleryManager();
|
||||
ObjectManagers.getInstance().UserManager = new UserManager();
|
||||
ObjectManagers.getInstance().SearchManager = new SearchManager();
|
||||
ObjectManagers.getInstance().SharingManager = new SharingManager();
|
||||
ObjectManagers.getInstance().IndexingManager = new IndexingManager();
|
||||
ObjectManagers.getInstance().PersonManager = new PersonManager();
|
||||
ObjectManagers.getInstance().VersionManager = new VersionManager();
|
||||
ObjectManagers.getInstance().LocationManager = new LocationManager();
|
||||
this.InitCommonManagers();
|
||||
this.initManagers('memory');
|
||||
Logger.debug(LOG_TAG, 'Memory DB inited');
|
||||
}
|
||||
|
||||
public static async InitSQLManagers(): Promise<void> {
|
||||
await ObjectManagers.reset();
|
||||
await SQLConnection.init();
|
||||
const GalleryManager = require('./database/sql/GalleryManager').GalleryManager;
|
||||
const UserManager = require('./database/sql/UserManager').UserManager;
|
||||
const SearchManager = require('./database/sql/SearchManager').SearchManager;
|
||||
const SharingManager = require('./database/sql/SharingManager').SharingManager;
|
||||
const IndexingManager = require('./database/sql/IndexingManager').IndexingManager;
|
||||
const PersonManager = require('./database/sql/PersonManager').PersonManager;
|
||||
const VersionManager = require('./database/sql/VersionManager').VersionManager;
|
||||
ObjectManagers.getInstance().GalleryManager = new GalleryManager();
|
||||
ObjectManagers.getInstance().UserManager = new UserManager();
|
||||
ObjectManagers.getInstance().SearchManager = new SearchManager();
|
||||
ObjectManagers.getInstance().SharingManager = new SharingManager();
|
||||
ObjectManagers.getInstance().IndexingManager = new IndexingManager();
|
||||
ObjectManagers.getInstance().PersonManager = new PersonManager();
|
||||
ObjectManagers.getInstance().VersionManager = new VersionManager();
|
||||
ObjectManagers.getInstance().LocationManager = new LocationManager();
|
||||
this.InitCommonManagers();
|
||||
this.initManagers('sql');
|
||||
Logger.debug(LOG_TAG, 'SQL DB inited');
|
||||
}
|
||||
|
||||
private static initManagers(type: 'memory' | 'sql'): void {
|
||||
ObjectManagers.getInstance().AlbumManager = new (require(`./database/${type}/AlbumManager`).AlbumManager)();
|
||||
ObjectManagers.getInstance().GalleryManager = new (require(`./database/${type}/GalleryManager`).GalleryManager)();
|
||||
ObjectManagers.getInstance().IndexingManager = new (require(`./database/${type}/IndexingManager`).IndexingManager)();
|
||||
ObjectManagers.getInstance().PersonManager = new (require(`./database/${type}/PersonManager`).PersonManager)();
|
||||
ObjectManagers.getInstance().SearchManager = new (require(`./database/${type}/SearchManager`).SearchManager)();
|
||||
ObjectManagers.getInstance().SharingManager = new (require(`./database/${type}/SharingManager`).SharingManager)();
|
||||
ObjectManagers.getInstance().UserManager = new (require(`./database/${type}/UserManager`).UserManager)();
|
||||
ObjectManagers.getInstance().VersionManager = new (require(`./database/${type}/VersionManager`).VersionManager)();
|
||||
ObjectManagers.getInstance().JobManager = new JobManager();
|
||||
ObjectManagers.getInstance().LocationManager = new LocationManager();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
19
src/backend/model/database/interfaces/IAlbumManager.ts
Normal file
19
src/backend/model/database/interfaces/IAlbumManager.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
|
||||
import {AlbumBaseDTO} from '../../../../common/entities/album/AlbumBaseDTO';
|
||||
|
||||
export interface IAlbumManager {
|
||||
/**
|
||||
* Creates a saved search type of album
|
||||
*/
|
||||
addSavedSearch(name: string, searchQuery: SearchQueryDTO): Promise<void>;
|
||||
|
||||
/**
|
||||
* Deletes an album
|
||||
*/
|
||||
deleteAlbum(id: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* Returns with all albums
|
||||
*/
|
||||
getAlbums(): Promise<AlbumBaseDTO[]>;
|
||||
}
|
||||
19
src/backend/model/database/memory/AlbumManager.ts
Normal file
19
src/backend/model/database/memory/AlbumManager.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {AlbumBaseDTO} from '../../../../common/entities/album/AlbumBaseDTO';
|
||||
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
|
||||
import {IAlbumManager} from '../interfaces/IAlbumManager';
|
||||
|
||||
export class AlbumManager implements IAlbumManager {
|
||||
|
||||
public async addSavedSearch(name: string, searchQuery: SearchQueryDTO): Promise<void> {
|
||||
throw new Error('not supported by memory DB');
|
||||
|
||||
}
|
||||
|
||||
public async deleteAlbum(id: number): Promise<void> {
|
||||
throw new Error('not supported by memory DB');
|
||||
}
|
||||
|
||||
public async getAlbums(): Promise<AlbumBaseDTO[]> {
|
||||
throw new Error('not supported by memory DB');
|
||||
}
|
||||
}
|
||||
40
src/backend/model/database/sql/AlbumManager.ts
Normal file
40
src/backend/model/database/sql/AlbumManager.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import {SQLConnection} from './SQLConnection';
|
||||
import {AlbumBaseEntity} from './enitites/album/AlbumBaseEntity';
|
||||
import {AlbumBaseDTO} from '../../../../common/entities/album/AlbumBaseDTO';
|
||||
import {SavedSearchDTO} from '../../../../common/entities/album/SavedSearchDTO';
|
||||
import {ObjectManagers} from '../../ObjectManagers';
|
||||
import {ISQLSearchManager} from './ISearchManager';
|
||||
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
|
||||
import {SavedSearchEntity} from './enitites/album/SavedSearchEntity';
|
||||
import { IAlbumManager } from '../interfaces/IAlbumManager';
|
||||
|
||||
export class AlbumManager implements IAlbumManager{
|
||||
private static async fillPreviewToAlbum(album: AlbumBaseDTO): Promise<void> {
|
||||
if (!(album as SavedSearchDTO).searchQuery) {
|
||||
throw new Error('no search query present');
|
||||
}
|
||||
album.preview = await (ObjectManagers.getInstance().SearchManager as ISQLSearchManager)
|
||||
.getPreview((album as SavedSearchDTO).searchQuery);
|
||||
}
|
||||
|
||||
public async addSavedSearch(name: string, searchQuery: SearchQueryDTO): Promise<void> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
await connection.getRepository(SavedSearchEntity).insert({name, searchQuery});
|
||||
|
||||
}
|
||||
|
||||
public async deleteAlbum(id: number): Promise<void> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
await connection.getRepository(AlbumBaseEntity).delete({id});
|
||||
}
|
||||
|
||||
public async getAlbums(): Promise<AlbumBaseDTO[]> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
const albums = await connection.getRepository(AlbumBaseEntity).find();
|
||||
for (const a of albums) {
|
||||
await AlbumManager.fillPreviewToAlbum(a);
|
||||
}
|
||||
|
||||
return albums;
|
||||
}
|
||||
}
|
||||
17
src/backend/model/database/sql/ISearchManager.ts
Normal file
17
src/backend/model/database/sql/ISearchManager.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {SearchQueryDTO, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO';
|
||||
import {MediaDTO} from '../../../../common/entities/MediaDTO';
|
||||
import {ISearchManager} from '../interfaces/ISearchManager';
|
||||
import {AutoCompleteItem} from '../../../../common/entities/AutoCompleteItem';
|
||||
import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO';
|
||||
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
|
||||
|
||||
export interface ISQLSearchManager extends ISearchManager {
|
||||
autocomplete(text: string, type: SearchQueryTypes): Promise<AutoCompleteItem[]>;
|
||||
|
||||
search(query: SearchQueryDTO): Promise<SearchResultDTO>;
|
||||
|
||||
getRandomPhoto(queryFilter: SearchQueryDTO): Promise<PhotoDTO>;
|
||||
|
||||
// "Protected" functions. only called from other Managers, not from middlewares
|
||||
getPreview(query: SearchQueryDTO): Promise<MediaDTO>;
|
||||
}
|
||||
@@ -19,6 +19,8 @@ import {PersonEntry} from './enitites/PersonEntry';
|
||||
import {Utils} from '../../../../common/Utils';
|
||||
import * as path from 'path';
|
||||
import {DatabaseType, ServerDataBaseConfig, SQLLogLevel} from '../../../../common/config/private/PrivateConfig';
|
||||
import {AlbumBaseEntity} from './enitites/album/AlbumBaseEntity';
|
||||
import {SavedSearchEntity} from './enitites/album/SavedSearchEntity';
|
||||
|
||||
|
||||
export class SQLConnection {
|
||||
@@ -43,6 +45,8 @@ export class SQLConnection {
|
||||
VideoEntity,
|
||||
DirectoryEntity,
|
||||
SharingEntity,
|
||||
AlbumBaseEntity,
|
||||
SavedSearchEntity,
|
||||
VersionEntity
|
||||
];
|
||||
options.synchronize = false;
|
||||
@@ -73,6 +77,8 @@ export class SQLConnection {
|
||||
VideoEntity,
|
||||
DirectoryEntity,
|
||||
SharingEntity,
|
||||
AlbumBaseEntity,
|
||||
SavedSearchEntity,
|
||||
VersionEntity
|
||||
];
|
||||
options.synchronize = false;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {AutoCompleteItem} from '../../../../common/entities/AutoCompleteItem';
|
||||
import {ISearchManager} from '../interfaces/ISearchManager';
|
||||
import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO';
|
||||
import {SQLConnection} from './SQLConnection';
|
||||
import {PhotoEntity} from './enitites/PhotoEntity';
|
||||
@@ -32,8 +31,10 @@ import {Utils} from '../../../../common/Utils';
|
||||
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
|
||||
import {DatabaseType} from '../../../../common/config/private/PrivateConfig';
|
||||
import {ISQLGalleryManager} from './IGalleryManager';
|
||||
import {ISQLSearchManager} from './ISearchManager';
|
||||
import {MediaDTO} from '../../../../common/entities/MediaDTO';
|
||||
|
||||
export class SearchManager implements ISearchManager {
|
||||
export class SearchManager implements ISQLSearchManager {
|
||||
|
||||
private static autoCompleteItemsUnique(array: Array<AutoCompleteItem>): Array<AutoCompleteItem> {
|
||||
const a = array.concat();
|
||||
@@ -223,6 +224,21 @@ export class SearchManager implements ISearchManager {
|
||||
|
||||
}
|
||||
|
||||
public async getPreview(queryIN: SearchQueryDTO): Promise<MediaDTO> {
|
||||
let query = this.flattenSameOfQueries(queryIN);
|
||||
query = await this.getGPSData(query);
|
||||
const connection = await SQLConnection.getConnection();
|
||||
|
||||
return await connection
|
||||
.getRepository(MediaEntity)
|
||||
.createQueryBuilder('media')
|
||||
.innerJoinAndSelect('media.directory', 'directory')
|
||||
.where(this.buildWhereQuery(query))
|
||||
.orderBy('media.metadata.creationDate', 'DESC')
|
||||
.limit(1)
|
||||
.getOne();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only those part of a query tree that only contains directory related search queries
|
||||
*/
|
||||
@@ -632,4 +648,5 @@ export class SearchManager implements ISearchManager {
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ export class MediaMetadataEntity implements MediaMetadata {
|
||||
// TODO: fix inheritance once its working in typeorm
|
||||
@Entity()
|
||||
@Unique(['name', 'directory'])
|
||||
@TableInheritance({column: {type: 'varchar', name: 'type', length: 32}})
|
||||
@TableInheritance({column: {type: 'varchar', name: 'type', length: 16}})
|
||||
export abstract class MediaEntity implements MediaDTO {
|
||||
|
||||
@Index()
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import {Column, Entity, Index, PrimaryGeneratedColumn, TableInheritance} from 'typeorm';
|
||||
import {MediaEntity} from '../MediaEntity';
|
||||
import {columnCharsetCS} from '../EntityUtils';
|
||||
import {AlbumBaseDTO} from '../../../../../../common/entities/album/AlbumBaseDTO';
|
||||
|
||||
@Entity()
|
||||
@TableInheritance({column: {type: 'varchar', name: 'type', length: 24}})
|
||||
export class AlbumBaseEntity implements AlbumBaseDTO {
|
||||
|
||||
@Index()
|
||||
@PrimaryGeneratedColumn({unsigned: true})
|
||||
id: number;
|
||||
|
||||
@Index()
|
||||
@Column(columnCharsetCS)
|
||||
name: string;
|
||||
|
||||
// not saving to database, it is only assigned when querying the DB
|
||||
public preview: MediaEntity;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import {ChildEntity, Column} from 'typeorm';
|
||||
import {AlbumBaseEntity} from './AlbumBaseEntity';
|
||||
import {SavedSearchDTO} from '../../../../../../common/entities/album/SavedSearchDTO';
|
||||
import {SearchQueryDTO} from '../../../../../../common/entities/SearchQueryDTO';
|
||||
|
||||
@ChildEntity()
|
||||
export class SavedSearchEntity extends AlbumBaseEntity implements SavedSearchDTO {
|
||||
@Column({
|
||||
type: 'text',
|
||||
nullable: false,
|
||||
transformer: {
|
||||
// used to deserialize your data from db field value
|
||||
from: (val: string) => {
|
||||
return JSON.parse(val);
|
||||
},
|
||||
// used to serialize your data to db field
|
||||
to: (val: object) => {
|
||||
return JSON.stringify(val);
|
||||
}
|
||||
}
|
||||
})
|
||||
searchQuery: SearchQueryDTO;
|
||||
}
|
||||
56
src/backend/routes/AlbumRouter.ts
Normal file
56
src/backend/routes/AlbumRouter.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import {AuthenticationMWs} from '../middlewares/user/AuthenticationMWs';
|
||||
import {Express} from 'express';
|
||||
import {RenderingMWs} from '../middlewares/RenderingMWs';
|
||||
import {UserRoles} from '../../common/entities/UserDTO';
|
||||
import {VersionMWs} from '../middlewares/VersionMWs';
|
||||
import {AlbumMWs} from '../middlewares/AlbumMWs';
|
||||
|
||||
export class AlbumRouter {
|
||||
public static route(app: Express): void {
|
||||
|
||||
this.addListAlbums(app);
|
||||
this.addAddSavedSearch(app);
|
||||
this.addDeleteAlbum(app);
|
||||
}
|
||||
|
||||
|
||||
private static addListAlbums(app: Express): void {
|
||||
app.get(['/api/albums'],
|
||||
// common part
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.authorise(UserRoles.User),
|
||||
VersionMWs.injectGalleryVersion,
|
||||
|
||||
// specific part
|
||||
AlbumMWs.listAlbums,
|
||||
RenderingMWs.renderResult
|
||||
);
|
||||
}
|
||||
|
||||
private static addDeleteAlbum(app: Express): void {
|
||||
app.delete(['/api/albums/:id'],
|
||||
// common part
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.authorise(UserRoles.Admin),
|
||||
VersionMWs.injectGalleryVersion,
|
||||
|
||||
// specific part
|
||||
AlbumMWs.deleteAlbum,
|
||||
RenderingMWs.renderResult
|
||||
);
|
||||
}
|
||||
|
||||
private static addAddSavedSearch(app: Express): void {
|
||||
app.put(['/api/albums/saved-searches'],
|
||||
// common part
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.authorise(UserRoles.Admin),
|
||||
VersionMWs.injectGalleryVersion,
|
||||
|
||||
// specific part
|
||||
AlbumMWs.createSavedSearch,
|
||||
RenderingMWs.renderResult
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -108,7 +108,7 @@ export class PublicRouter {
|
||||
}
|
||||
);
|
||||
|
||||
app.get(['/', '/login', '/gallery*', '/share*', '/admin', '/duplicates', '/faces', '/search*'],
|
||||
app.get(['/', '/login', '/gallery*', '/share*', '/admin', '/duplicates', '/faces', '/albums', '/search*'],
|
||||
AuthenticationMWs.tryAuthenticate,
|
||||
setLocale,
|
||||
renderIndex
|
||||
|
||||
@@ -8,6 +8,7 @@ import {AdminRouter} from './admin/AdminRouter';
|
||||
import {SettingsRouter} from './admin/SettingsRouter';
|
||||
import {NotificationRouter} from './NotificationRouter';
|
||||
import {ErrorRouter} from './ErrorRouter';
|
||||
import {AlbumRouter} from './AlbumRouter';
|
||||
|
||||
export class Router {
|
||||
|
||||
@@ -15,13 +16,14 @@ export class Router {
|
||||
|
||||
PublicRouter.route(app);
|
||||
|
||||
UserRouter.route(app);
|
||||
GalleryRouter.route(app);
|
||||
PersonRouter.route(app);
|
||||
SharingRouter.route(app);
|
||||
AdminRouter.route(app);
|
||||
SettingsRouter.route(app);
|
||||
AlbumRouter.route(app);
|
||||
GalleryRouter.route(app);
|
||||
NotificationRouter.route(app);
|
||||
PersonRouter.route(app);
|
||||
SettingsRouter.route(app);
|
||||
SharingRouter.route(app);
|
||||
UserRouter.route(app);
|
||||
|
||||
ErrorRouter.route(app);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export class SharingRouter {
|
||||
AuthenticationMWs.authenticate,
|
||||
AuthenticationMWs.authorise(UserRoles.Admin),
|
||||
SharingMWs.deleteSharing,
|
||||
RenderingMWs.renderOK
|
||||
RenderingMWs.renderResult
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const DataStructureVersion = 21;
|
||||
export const DataStructureVersion = 22;
|
||||
|
||||
@@ -35,6 +35,13 @@ export class ClientSearchConfig {
|
||||
maxDirectoryResult: number = 200;
|
||||
}
|
||||
|
||||
|
||||
@SubConfigClass()
|
||||
export class ClientAlbumConfig {
|
||||
@ConfigProperty()
|
||||
enabled: boolean = false;
|
||||
}
|
||||
|
||||
@SubConfigClass()
|
||||
export class ClientSharingConfig {
|
||||
@ConfigProperty()
|
||||
@@ -172,6 +179,8 @@ export class ClientConfig {
|
||||
@ConfigProperty()
|
||||
Sharing: ClientSharingConfig = new ClientSharingConfig();
|
||||
@ConfigProperty()
|
||||
Album: ClientAlbumConfig = new ClientAlbumConfig();
|
||||
@ConfigProperty()
|
||||
Map: ClientMapConfig = new ClientMapConfig();
|
||||
@ConfigProperty()
|
||||
RandomPhoto: ClientRandomPhotoConfig = new ClientRandomPhotoConfig();
|
||||
|
||||
@@ -24,6 +24,8 @@ export enum ErrorCodes {
|
||||
TASK_ERROR = 14,
|
||||
JOB_ERROR = 15,
|
||||
LocationLookUp_ERROR = 16,
|
||||
|
||||
ALBUM_ERROR = 17,
|
||||
}
|
||||
|
||||
export class ErrorDTO {
|
||||
|
||||
@@ -158,7 +158,7 @@ export interface TextSearch extends NegatableSearchQuery {
|
||||
SearchQueryTypes.caption |
|
||||
SearchQueryTypes.file_name |
|
||||
SearchQueryTypes.directory;
|
||||
matchType: TextSearchQueryMatchTypes;
|
||||
matchType?: TextSearchQueryMatchTypes;
|
||||
text: string;
|
||||
}
|
||||
|
||||
|
||||
7
src/common/entities/album/AlbumBaseDTO.ts
Normal file
7
src/common/entities/album/AlbumBaseDTO.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {PreviewPhotoDTO} from '../PhotoDTO';
|
||||
|
||||
export interface AlbumBaseDTO {
|
||||
id: number;
|
||||
name: string;
|
||||
preview: PreviewPhotoDTO;
|
||||
}
|
||||
11
src/common/entities/album/SavedSearchDTO.ts
Normal file
11
src/common/entities/album/SavedSearchDTO.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import {AlbumBaseDTO} from './AlbumBaseDTO';
|
||||
import {PreviewPhotoDTO} from '../PhotoDTO';
|
||||
import {SearchQueryDTO} from '../SearchQueryDTO';
|
||||
|
||||
export interface SavedSearchDTO extends AlbumBaseDTO {
|
||||
id: number;
|
||||
name: string;
|
||||
preview: PreviewPhotoDTO;
|
||||
|
||||
searchQuery: SearchQueryDTO;
|
||||
}
|
||||
@@ -100,6 +100,10 @@ import {AppRoutingModule} from './app.routing';
|
||||
import {CookieService} from 'ngx-cookie-service';
|
||||
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
|
||||
import {icon, Marker} from 'leaflet';
|
||||
import {AlbumsComponent} from './ui/albums/albums.component';
|
||||
import {AlbumComponent} from './ui/albums/album/album.component';
|
||||
import {AlbumsService} from './ui/albums/albums.service';
|
||||
import {GallerySearchQueryBuilderComponent} from './ui/gallery/search/query-builder/query-bulder.gallery.component';
|
||||
|
||||
|
||||
@Injectable()
|
||||
@@ -178,6 +182,9 @@ Marker.prototype.options.icon = iconDefault;
|
||||
LanguageComponent,
|
||||
TimeStampDatePickerComponent,
|
||||
TimeStampTimePickerComponent,
|
||||
// Albums
|
||||
AlbumsComponent,
|
||||
AlbumComponent,
|
||||
// Gallery
|
||||
GalleryLightboxMediaComponent,
|
||||
GalleryPhotoLoadingComponent,
|
||||
@@ -190,6 +197,7 @@ Marker.prototype.options.icon = iconDefault;
|
||||
GallerySearchComponent,
|
||||
GallerySearchQueryEntryComponent,
|
||||
GallerySearchFieldComponent,
|
||||
GallerySearchQueryBuilderComponent,
|
||||
GalleryShareComponent,
|
||||
GalleryNavigatorComponent,
|
||||
GalleryPhotoComponent,
|
||||
@@ -241,6 +249,7 @@ Marker.prototype.options.icon = iconDefault;
|
||||
NetworkService,
|
||||
ShareService,
|
||||
UserService,
|
||||
AlbumsService,
|
||||
GalleryCacheService,
|
||||
GalleryService,
|
||||
MapService,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {QueryParams} from '../../common/QueryParams';
|
||||
import {DuplicateComponent} from './ui/duplicates/duplicates.component';
|
||||
import {FacesComponent} from './ui/faces/faces.component';
|
||||
import {AuthGuard} from './model/network/helper/auth.guard';
|
||||
import {AlbumsComponent} from './ui/albums/albums.component';
|
||||
|
||||
export function galleryMatcherFunction(
|
||||
segments: UrlSegment[]): UrlMatchResult | null {
|
||||
@@ -59,6 +60,11 @@ const routes: Routes = [
|
||||
component: DuplicateComponent,
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'albums',
|
||||
component: AlbumsComponent,
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'faces',
|
||||
component: FacesComponent,
|
||||
|
||||
60
src/frontend/app/ui/albums/album/album.component.css
Normal file
60
src/frontend/app/ui/albums/album/album.component.css
Normal file
@@ -0,0 +1,60 @@
|
||||
.delete {
|
||||
margin: 2px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.delete {
|
||||
cursor: pointer;
|
||||
transition: all .05s ease-in-out;
|
||||
transform: scale(1.0, 1.0);
|
||||
}
|
||||
|
||||
.delete:hover {
|
||||
transform: scale(1.4, 1.4);
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.photo-container {
|
||||
border: 2px solid #333;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background-color: #bbbbbb;
|
||||
}
|
||||
|
||||
.no-image {
|
||||
position: absolute;
|
||||
color: #7f7f7f;
|
||||
font-size: 80px;
|
||||
top: calc(50% - 40px);
|
||||
left: calc(50% - 40px);
|
||||
}
|
||||
|
||||
.photo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
font-size: medium;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a:hover .info {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
a:hover .photo-container {
|
||||
border-color: #000;
|
||||
}
|
||||
28
src/frontend/app/ui/albums/album/album.component.html
Normal file
28
src/frontend/app/ui/albums/album/album.component.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<a [routerLink]="RouterLink"
|
||||
style="display: inline-block;">
|
||||
|
||||
|
||||
<div class="photo-container"
|
||||
[style.width.px]="size"
|
||||
[style.height.px]="size">
|
||||
|
||||
<div class="photo"
|
||||
*ngIf="thumbnail && thumbnail.Available"
|
||||
[style.background-image]="getSanitizedThUrl()"></div>
|
||||
|
||||
<span *ngIf="!thumbnail || !thumbnail.Available" class="oi oi-folder no-image"
|
||||
aria-hidden="true">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!--Info box -->
|
||||
<div class="info">
|
||||
{{album.name}}
|
||||
<span *ngIf="CanUpdate"
|
||||
(click)="deleteAlbum($event)"
|
||||
class="delete oi oi-trash float-right"></span>
|
||||
|
||||
</div>
|
||||
</a>
|
||||
|
||||
77
src/frontend/app/ui/albums/album/album.component.ts
Normal file
77
src/frontend/app/ui/albums/album/album.component.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {RouterLink} from '@angular/router';
|
||||
import {DomSanitizer, SafeStyle} from '@angular/platform-browser';
|
||||
import {Thumbnail, ThumbnailManagerService} from '../../gallery/thumbnailManager.service';
|
||||
import {AuthenticationService} from '../../../model/network/authentication.service';
|
||||
import {AlbumsService} from '../albums.service';
|
||||
import {AlbumBaseDTO} from '../../../../../common/entities/album/AlbumBaseDTO';
|
||||
import {Media} from '../../gallery/Media';
|
||||
import {SavedSearchDTO} from '../../../../../common/entities/album/SavedSearchDTO';
|
||||
import {UserRoles} from '../../../../../common/entities/UserDTO';
|
||||
|
||||
@Component({
|
||||
selector: 'app-album',
|
||||
templateUrl: './album.component.html',
|
||||
styleUrls: ['./album.component.css'],
|
||||
providers: [RouterLink],
|
||||
})
|
||||
export class AlbumComponent implements OnInit, OnDestroy {
|
||||
@Input() album: AlbumBaseDTO;
|
||||
@Input() size: number;
|
||||
|
||||
public thumbnail: Thumbnail = null;
|
||||
|
||||
constructor(private thumbnailService: ThumbnailManagerService,
|
||||
private sanitizer: DomSanitizer,
|
||||
private albumService: AlbumsService,
|
||||
public authenticationService: AuthenticationService) {
|
||||
|
||||
}
|
||||
|
||||
get IsSavedSearch(): boolean {
|
||||
return this.album && !!this.AsSavedSearch.searchQuery;
|
||||
}
|
||||
|
||||
get AsSavedSearch(): SavedSearchDTO {
|
||||
return this.album as SavedSearchDTO;
|
||||
}
|
||||
|
||||
get CanUpdate(): boolean {
|
||||
return this.authenticationService.user.getValue().role >= UserRoles.Admin;
|
||||
}
|
||||
|
||||
get RouterLink(): any[] {
|
||||
if (this.IsSavedSearch) {
|
||||
return ['/search', JSON.stringify(this.AsSavedSearch.searchQuery)];
|
||||
}
|
||||
// TODO: add "normal" albums here once they are ready, see: https://github.com/bpatrik/pigallery2/issues/301
|
||||
return null;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.album.preview) {
|
||||
this.thumbnail = this.thumbnailService.getThumbnail(new Media(this.album.preview, this.size, this.size));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getSanitizedThUrl(): SafeStyle {
|
||||
return this.sanitizer.bypassSecurityTrustStyle('url(' + this.thumbnail.Src
|
||||
.replace(/\(/g, '%28')
|
||||
.replace(/'/g, '%27')
|
||||
.replace(/\)/g, '%29') + ')');
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.thumbnail != null) {
|
||||
this.thumbnail.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAlbum($event: MouseEvent): Promise<void> {
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
await this.albumService.deleteAlbum(this.album).catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
29
src/frontend/app/ui/albums/albums.component.css
Normal file
29
src/frontend/app/ui/albums/albums.component.css
Normal file
@@ -0,0 +1,29 @@
|
||||
app-album {
|
||||
margin: 2px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.no-item-msg {
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-face-msg h2 {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.add-saved-search {
|
||||
vertical-align: baseline;
|
||||
position: absolute;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.add-saved-search .text {
|
||||
position: relative;
|
||||
top: calc(50% - 40px);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.add-saved-search .text .oi {
|
||||
font-size: 80px;
|
||||
}
|
||||
74
src/frontend/app/ui/albums/albums.component.html
Normal file
74
src/frontend/app/ui/albums/albums.component.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<app-frame>
|
||||
|
||||
<div body #container class="container-fluid">
|
||||
<app-album *ngFor="let album of albumsService.albums | async"
|
||||
[album]="album"
|
||||
[size]="size"></app-album>
|
||||
|
||||
<div class="add-saved-search btn btn-secondary"
|
||||
[style.width.px]="size"
|
||||
[style.height.px]="size"
|
||||
(click)="openModal(modal)">
|
||||
<div class="text">
|
||||
<span class="oi oi-plus" aria-hidden="true"> </span><br/>
|
||||
<span i18n>Add saved search</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex no-item-msg"
|
||||
*ngIf="(albumsService.albums | async) && (albumsService.albums | async).length == 0">
|
||||
<div class="flex-fill">
|
||||
<h2>:(
|
||||
<ng-container i18n>No albums to show.</ng-container>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-frame>
|
||||
|
||||
|
||||
<ng-template #modal>
|
||||
<!-- sharing Modal-->
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" i18n>Add Saved Search</h5>
|
||||
<button type="button" class="close" (click)="hideModal()" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form #savedSearchPanelForm="ngForm" class="form-horizontal">
|
||||
<div class="row">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="saveSearchName">Album name</label>
|
||||
<input
|
||||
id="saveSearchName"
|
||||
name="saveSearchName"
|
||||
placeholder="Search text"
|
||||
class="form-control input-md"
|
||||
[(ngModel)]="savedSearch.name"
|
||||
type="text"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="album-search-query-builder">Search query</label>
|
||||
<app-gallery-search-query-builder
|
||||
id="album-search-query-builder"
|
||||
name="album-search-query-builder"
|
||||
[(ngModel)]="savedSearch.searchQuery">
|
||||
</app-gallery-search-query-builder>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="input-group-btn float-right row" style="display: block">
|
||||
|
||||
<button class="btn btn-primary" type="button"
|
||||
[disabled]="savedSearch.searchQuery.text == ''"
|
||||
(click)="saveSearch()">
|
||||
<span class="oi oi-folder"></span> Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ng-template>
|
||||
56
src/frontend/app/ui/albums/albums.component.ts
Normal file
56
src/frontend/app/ui/albums/albums.component.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import {Component, ElementRef, OnInit, TemplateRef, ViewChild} from '@angular/core';
|
||||
import {AlbumsService} from './albums.service';
|
||||
import {BsModalService} from 'ngx-bootstrap/modal';
|
||||
import {BsModalRef} from 'ngx-bootstrap/modal/bs-modal-ref.service';
|
||||
import {SearchQueryTypes, TextSearch} from '../../../../common/entities/SearchQueryDTO';
|
||||
|
||||
@Component({
|
||||
selector: 'app-albums',
|
||||
templateUrl: './albums.component.html',
|
||||
styleUrls: ['./albums.component.css']
|
||||
})
|
||||
export class AlbumsComponent implements OnInit {
|
||||
@ViewChild('container', {static: true}) container: ElementRef;
|
||||
public size: number;
|
||||
public savedSearch = {
|
||||
name: '',
|
||||
searchQuery: {type: SearchQueryTypes.any_text, text: ''} as TextSearch
|
||||
};
|
||||
private modalRef: BsModalRef;
|
||||
|
||||
constructor(public albumsService: AlbumsService,
|
||||
private modalService: BsModalService) {
|
||||
this.albumsService.getAlbums().catch(console.error);
|
||||
}
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
this.updateSize();
|
||||
}
|
||||
|
||||
|
||||
public async openModal(template: TemplateRef<any>): Promise<void> {
|
||||
this.modalRef = this.modalService.show(template, {class: 'modal-lg'});
|
||||
document.body.style.paddingRight = '0px';
|
||||
}
|
||||
|
||||
public hideModal(): void {
|
||||
this.modalRef.hide();
|
||||
this.modalRef = null;
|
||||
}
|
||||
|
||||
async saveSearch(): Promise<void> {
|
||||
await this.albumsService.addSavedSearch(this.savedSearch.name, this.savedSearch.searchQuery);
|
||||
this.hideModal();
|
||||
}
|
||||
|
||||
private updateSize(): void {
|
||||
const size = 220 + 5;
|
||||
// body - container margin
|
||||
const containerWidth = this.container.nativeElement.clientWidth - 30;
|
||||
this.size = (containerWidth / Math.round((containerWidth / size))) - 5;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
31
src/frontend/app/ui/albums/albums.service.ts
Normal file
31
src/frontend/app/ui/albums/albums.service.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {NetworkService} from '../../model/network/network.service';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
import {AlbumBaseDTO} from '../../../../common/entities/album/AlbumBaseDTO';
|
||||
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class AlbumsService {
|
||||
public albums: BehaviorSubject<AlbumBaseDTO[]>;
|
||||
|
||||
constructor(private networkService: NetworkService) {
|
||||
this.albums = new BehaviorSubject<AlbumBaseDTO[]>(null);
|
||||
}
|
||||
|
||||
|
||||
public async getAlbums(): Promise<void> {
|
||||
this.albums.next((await this.networkService.getJson<AlbumBaseDTO[]>('/albums'))
|
||||
.sort((a, b): number => a.name.localeCompare(b.name)));
|
||||
}
|
||||
|
||||
async deleteAlbum(album: AlbumBaseDTO): Promise<void> {
|
||||
await this.networkService.deleteJson('/albums/' + album.id);
|
||||
await this.getAlbums();
|
||||
}
|
||||
|
||||
async addSavedSearch(name: string, searchQuery: SearchQueryDTO): Promise<void> {
|
||||
await this.networkService.putJson('/albums/saved-searches', {name, searchQuery});
|
||||
await this.getAlbums();
|
||||
}
|
||||
}
|
||||
@@ -72,8 +72,3 @@ a:hover .photo-container {
|
||||
border-color: #000;
|
||||
}
|
||||
|
||||
.person-name {
|
||||
display: inline-block;
|
||||
width: 180px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
[routerLink]="['/gallery']"
|
||||
[queryParams]="queryService.getParams()" [class.active]="isLinkActive('/gallery')" i18n>Gallery</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="isAlbumsAvailable()">
|
||||
<a class="nav-link" [routerLink]="['/albums']" [class.active]="isLinkActive('/albums')" i18n>Albums</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="isFacesAvailable()">
|
||||
<a class="nav-link" [routerLink]="['/faces']" [class.active]="isLinkActive('/faces')" i18n>Faces</a>
|
||||
</li>
|
||||
|
||||
@@ -45,5 +45,8 @@ export class FrameComponent {
|
||||
this.authService.logout();
|
||||
}
|
||||
|
||||
isAlbumsAvailable(): boolean {
|
||||
return Config.Client.Album.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,15 +59,5 @@ export class GalleryDirectoryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
calcSize() {
|
||||
if (this.size == null || PageHelper.isScrollYVisible()) {
|
||||
const size = 220 + 5;
|
||||
const containerWidth = this.container.nativeElement.parentElement.parentElement.clientWidth;
|
||||
this.size = containerWidth / Math.round((containerWidth / size));
|
||||
}
|
||||
return Math.floor(this.size - 5);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
@@ -32,19 +32,12 @@
|
||||
</div>
|
||||
<hr/>
|
||||
<form #searchPanelForm="ngForm" class="form-horizontal">
|
||||
<app-gallery-search-field [(ngModel)]="rawSearchText"
|
||||
(ngModelChange)="validateRawSearchText()"
|
||||
name="form-search-field">
|
||||
|
||||
</app-gallery-search-field>
|
||||
|
||||
<app-gallery-search-query-entry
|
||||
<app-gallery-search-query-builder
|
||||
id="album-search-query-builder"
|
||||
name="album-search-query-builder"
|
||||
[(ngModel)]="searchQueryDTO"
|
||||
(change)="onQueryChange()"
|
||||
name="search-root"
|
||||
(delete)="resetQuery()">
|
||||
|
||||
</app-gallery-search-query-entry>
|
||||
(change)="onQueryChange()" >
|
||||
</app-gallery-search-query-builder>
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
@@ -20,8 +20,7 @@ import {SearchQueryParserService} from '../search/search-query-parser.service';
|
||||
})
|
||||
export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy {
|
||||
|
||||
public searchQueryDTO: SearchQueryDTO;
|
||||
public rawSearchText: string;
|
||||
public searchQueryDTO: SearchQueryDTO = {type: SearchQueryTypes.any_text, text: ''} as TextSearch;
|
||||
enabled = true;
|
||||
url = '';
|
||||
|
||||
@@ -37,7 +36,6 @@ export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy {
|
||||
private searchQueryParserService: SearchQueryParserService,
|
||||
private route: ActivatedRoute,
|
||||
private modalService: BsModalService) {
|
||||
this.resetQuery();
|
||||
|
||||
this.subscription = this.route.params.subscribe((params: Params) => {
|
||||
if (!params[QueryParams.gallery.search.query]) {
|
||||
@@ -55,17 +53,8 @@ export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy {
|
||||
return JSON.stringify(this.searchQueryDTO);
|
||||
}
|
||||
|
||||
validateRawSearchText(): void {
|
||||
try {
|
||||
this.searchQueryDTO = this.searchQueryParserService.parse(this.rawSearchText);
|
||||
this.url = NetworkService.buildUrl(Config.Client.publicUrl + '/api/gallery/random/' + this.HTMLSearchQuery);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
onQueryChange(): void {
|
||||
this.rawSearchText = this.searchQueryParserService.stringify(this.searchQueryDTO);
|
||||
this.url = NetworkService.buildUrl(Config.Client.publicUrl + '/api/gallery/random/' + this.HTMLSearchQuery);
|
||||
}
|
||||
|
||||
@@ -114,9 +103,4 @@ export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
|
||||
resetQuery(): void {
|
||||
this.searchQueryDTO = {text: '', type: SearchQueryTypes.any_text} as TextSearch;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
.autocomplete-list {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 34px;
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
border: 1px solid #ccc;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
padding: 5px 0;
|
||||
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.insert-button {
|
||||
margin-right: -15px;
|
||||
display: none;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.autocomplete-item-selected .insert-button {
|
||||
display: block;
|
||||
}
|
||||
@media (hover: none) {
|
||||
.insert-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-item-selected .insert-button:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.autocomplete-item {
|
||||
cursor: pointer;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.autocomplete-item {
|
||||
color: #333;
|
||||
padding: 0 20px;
|
||||
line-height: 1.42857143;
|
||||
font-weight: 400;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.autocomplete-item-selected {
|
||||
background-color: #007bff;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
|
||||
.search-text {
|
||||
z-index: 6;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.search-hint {
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<app-gallery-search-field [(ngModel)]="rawSearchText"
|
||||
(ngModelChange)="validateRawSearchText()"
|
||||
(search)="search.emit()"
|
||||
name="form-search-field">
|
||||
|
||||
</app-gallery-search-field>
|
||||
<hr>
|
||||
<app-gallery-search-query-entry
|
||||
[(ngModel)]="searchQueryDTO"
|
||||
(change)="onQueryChange()"
|
||||
(ngModelChange)="onChange()"
|
||||
name="search-root"
|
||||
(delete)="resetQuery()">
|
||||
|
||||
</app-gallery-search-query-entry>
|
||||
@@ -0,0 +1,82 @@
|
||||
import {Component, EventEmitter, forwardRef, Output} from '@angular/core';
|
||||
import {SearchQueryDTO, SearchQueryTypes, TextSearch} from '../../../../../../common/entities/SearchQueryDTO';
|
||||
import {ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator} from '@angular/forms';
|
||||
import {SearchQueryParserService} from '../search-query-parser.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-gallery-search-query-builder',
|
||||
templateUrl: './query-builder.gallery.component.html',
|
||||
styleUrls: ['./query-builder.gallery.component.css'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => GallerySearchQueryBuilderComponent),
|
||||
multi: true
|
||||
},
|
||||
{
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => GallerySearchQueryBuilderComponent),
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class GallerySearchQueryBuilderComponent implements ControlValueAccessor, Validator {
|
||||
public searchQueryDTO: SearchQueryDTO = {type: SearchQueryTypes.any_text, text: ''} as TextSearch;
|
||||
@Output() search = new EventEmitter<void>();
|
||||
public rawSearchText = '';
|
||||
|
||||
|
||||
constructor(
|
||||
private searchQueryParserService: SearchQueryParserService) {
|
||||
}
|
||||
|
||||
|
||||
validateRawSearchText(): void {
|
||||
try {
|
||||
this.searchQueryDTO = this.searchQueryParserService.parse(this.rawSearchText);
|
||||
this.onChange();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
resetQuery(): void {
|
||||
this.searchQueryDTO = ({text: '', type: SearchQueryTypes.any_text} as TextSearch);
|
||||
}
|
||||
|
||||
onQueryChange(): void {
|
||||
this.rawSearchText = this.searchQueryParserService.stringify(this.searchQueryDTO);
|
||||
this.onChange();
|
||||
}
|
||||
|
||||
validate(control: FormControl): ValidationErrors {
|
||||
return {required: true};
|
||||
}
|
||||
|
||||
public onTouched(): void {
|
||||
}
|
||||
|
||||
public writeValue(obj: any): void {
|
||||
this.searchQueryDTO = obj;
|
||||
}
|
||||
|
||||
registerOnChange(fn: (_: any) => void): void {
|
||||
this.propagateChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: () => void): void {
|
||||
this.propagateTouch = fn;
|
||||
}
|
||||
|
||||
public onChange(): void {
|
||||
this.propagateChange(this.searchQueryDTO);
|
||||
}
|
||||
|
||||
|
||||
private propagateChange = (_: any): void => {
|
||||
};
|
||||
|
||||
private propagateTouch = (_: any): void => {
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
<form class="navbar-form" role="search" #SearchForm="ngForm">
|
||||
<div class="input-group">
|
||||
|
||||
<app-gallery-search-field [(ngModel)]="rawSearchText"
|
||||
<app-gallery-search-field [(ngModel)]="rawSearchText"
|
||||
(ngModelChange)="validateRawSearchText()"
|
||||
class="search-field"
|
||||
(search)="Search()"
|
||||
name="search-field">
|
||||
class="search-field"
|
||||
(search)="Search()"
|
||||
name="search-field">
|
||||
|
||||
</app-gallery-search-field>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div class="input-group-btn" style="display: block">
|
||||
<button class="btn btn-light" type="button" (click)="openModal(searchModal)">
|
||||
<button class="btn btn-light" type="button" (click)="openSearchModal(searchModal)">
|
||||
<span class="oi oi-chevron-bottom"></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -29,34 +29,65 @@
|
||||
<!-- sharing Modal-->
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" i18n>Search</h5>
|
||||
<button type="button" class="close" (click)="hideModal()" data-dismiss="modal" aria-label="Close">
|
||||
<button type="button" class="close" (click)="hideSearchModal()" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form #searchPanelForm="ngForm" class="form-horizontal">
|
||||
<app-gallery-search-field [(ngModel)]="rawSearchText"
|
||||
(ngModelChange)="validateRawSearchText()"
|
||||
(search)="Search()"
|
||||
name="form-search-field">
|
||||
|
||||
</app-gallery-search-field>
|
||||
|
||||
<app-gallery-search-query-entry
|
||||
<app-gallery-search-query-builder
|
||||
name="search-query-builder"
|
||||
[(ngModel)]="searchQueryDTO"
|
||||
(change)="onQueryChange()"
|
||||
name="search-root"
|
||||
(delete)="resetQuery()">
|
||||
(search)="Search()">
|
||||
</app-gallery-search-query-builder>
|
||||
|
||||
</app-gallery-search-query-entry>
|
||||
|
||||
<div class="input-group-btn float-right" style="display: block">
|
||||
<div class="input-group-btn float-right row" style="display: block">
|
||||
|
||||
<button *ngIf="AlbumsEnabled"
|
||||
class="btn btn-secondary mr-2" type="button"
|
||||
[disabled]="rawSearchText == ''"
|
||||
(click)="openSaveSearchModal(saveSearchModal)">
|
||||
<span class="oi oi-folder"></span> Save
|
||||
</button>
|
||||
<button class="btn btn-primary" type="button"
|
||||
[routerLink]="['/search', HTMLSearchQuery]"
|
||||
(click)="hideModal()">
|
||||
(click)="hideSearchModal()">
|
||||
<span class="oi oi-magnifying-glass"></span> Search
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #saveSearchModal>
|
||||
<!-- sharing Modal-->
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" i18n>Save search to album</h5>
|
||||
<button type="button" class="close" (click)="hideSaveSearchModal()" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<form #saveSearchPanelForm="ngForm" class="form-horizontal">
|
||||
<div class="row">
|
||||
<input
|
||||
id="saveSearchName"
|
||||
name="saveSearchName"
|
||||
placeholder="Search text"
|
||||
class="form-control input-md"
|
||||
[(ngModel)]="saveSearchName"
|
||||
type="text"/>
|
||||
</div>
|
||||
<div class="input-group-btn float-right row mt-2" style="display: block">
|
||||
<button class="btn btn-primary" type="button"
|
||||
(click)="saveSearch()">
|
||||
<span class="oi oi-folder"></span> Save as album
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -9,6 +9,8 @@ import {MetadataSearchQueryTypes, SearchQueryDTO, SearchQueryTypes, TextSearch}
|
||||
import {BsModalService} from 'ngx-bootstrap/modal';
|
||||
import {BsModalRef} from 'ngx-bootstrap/modal/bs-modal-ref.service';
|
||||
import {SearchQueryParserService} from './search-query-parser.service';
|
||||
import {AlbumsService} from '../../albums/albums.service';
|
||||
import {Config} from '../../../../../common/config/public/Config';
|
||||
|
||||
@Component({
|
||||
selector: 'app-gallery-search',
|
||||
@@ -22,13 +24,17 @@ export class GallerySearchComponent implements OnDestroy {
|
||||
public rawSearchText = '';
|
||||
mouseOverAutoComplete = false;
|
||||
readonly SearchQueryTypes: typeof SearchQueryTypes;
|
||||
modalRef: BsModalRef;
|
||||
public readonly MetadataSearchQueryTypes: { value: string; key: SearchQueryTypes }[];
|
||||
public saveSearchName: string;
|
||||
AlbumsEnabled = Config.Client.Album.enabled;
|
||||
private searchModalRef: BsModalRef;
|
||||
private readonly subscription: Subscription = null;
|
||||
private saveSearchModalRef: BsModalRef;
|
||||
|
||||
constructor(private autoCompleteService: AutoCompleteService,
|
||||
private searchQueryParserService: SearchQueryParserService,
|
||||
private galleryService: GalleryService,
|
||||
private albumService: AlbumsService,
|
||||
private navigationService: NavigationService,
|
||||
private route: ActivatedRoute,
|
||||
public router: Router,
|
||||
@@ -60,25 +66,34 @@ export class GallerySearchComponent implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
public async openModal(template: TemplateRef<any>): Promise<void> {
|
||||
this.modalRef = this.modalService.show(template, {class: 'modal-lg'});
|
||||
public async openSearchModal(template: TemplateRef<any>): Promise<void> {
|
||||
this.searchModalRef = this.modalService.show(template, {class: 'modal-lg'});
|
||||
document.body.style.paddingRight = '0px';
|
||||
}
|
||||
|
||||
public hideModal(): void {
|
||||
this.modalRef.hide();
|
||||
this.modalRef = null;
|
||||
public hideSearchModal(): void {
|
||||
this.searchModalRef.hide();
|
||||
this.searchModalRef = null;
|
||||
}
|
||||
|
||||
resetQuery(): void {
|
||||
this.searchQueryDTO = ({text: '', type: SearchQueryTypes.any_text} as TextSearch);
|
||||
public async openSaveSearchModal(template: TemplateRef<any>): Promise<void> {
|
||||
this.saveSearchModalRef = this.modalService.show(template, {class: 'modal-lg'});
|
||||
document.body.style.paddingRight = '0px';
|
||||
}
|
||||
|
||||
public hideSaveSearchModal(): void {
|
||||
this.saveSearchModalRef.hide();
|
||||
this.saveSearchModalRef = null;
|
||||
}
|
||||
|
||||
|
||||
onQueryChange(): void {
|
||||
console.log('cahnge', this.searchQueryDTO);
|
||||
this.rawSearchText = this.searchQueryParserService.stringify(this.searchQueryDTO);
|
||||
// this.validateRawSearchText();
|
||||
}
|
||||
|
||||
|
||||
validateRawSearchText(): void {
|
||||
try {
|
||||
this.searchQueryDTO = this.searchQueryParserService.parse(this.rawSearchText);
|
||||
@@ -92,6 +107,10 @@ export class GallerySearchComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
|
||||
async saveSearch(): Promise<void> {
|
||||
await this.albumService.addSavedSearch(this.saveSearchName, this.searchQueryDTO);
|
||||
this.hideSaveSearchModal();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export class DBTestHelper {
|
||||
|
||||
static enable = {
|
||||
memory: false,
|
||||
sqlite: true,
|
||||
sqlite: process.env.TEST_SQLITE !== 'false',
|
||||
mysql: process.env.TEST_MYSQL !== 'false'
|
||||
};
|
||||
public static readonly savedDescribe = savedDescribe;
|
||||
@@ -144,6 +144,7 @@ export class DBTestHelper {
|
||||
}
|
||||
|
||||
private async resetMySQL(): Promise<void> {
|
||||
await ObjectManagers.reset();
|
||||
Config.Server.Database.type = DatabaseType.mysql;
|
||||
Config.Server.Database.mysql.database = 'pigallery2_test';
|
||||
const conn = await SQLConnection.getConnection();
|
||||
|
||||
176
test/backend/unit/model/sql/AlbumManager.ts
Normal file
176
test/backend/unit/model/sql/AlbumManager.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import {DBTestHelper} from '../../../DBTestHelper';
|
||||
import {DirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO';
|
||||
import {TestHelper} from './TestHelper';
|
||||
import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers';
|
||||
import {PhotoDTO, PhotoMetadata} from '../../../../../src/common/entities/PhotoDTO';
|
||||
import {VideoDTO} from '../../../../../src/common/entities/VideoDTO';
|
||||
import {AlbumManager} from '../../../../../src/backend/model/database/sql/AlbumManager';
|
||||
import {SearchQueryTypes, TextSearch} from '../../../../../src/common/entities/SearchQueryDTO';
|
||||
import {SQLConnection} from '../../../../../src/backend/model/database/sql/SQLConnection';
|
||||
import {AlbumBaseEntity} from '../../../../../src/backend/model/database/sql/enitites/album/AlbumBaseEntity';
|
||||
import {Utils} from '../../../../../src/common/Utils';
|
||||
import {MediaDTO} from '../../../../../src/common/entities/MediaDTO';
|
||||
|
||||
|
||||
const deepEqualInAnyOrder = require('deep-equal-in-any-order');
|
||||
const chai = require('chai');
|
||||
|
||||
chai.use(deepEqualInAnyOrder);
|
||||
const {expect} = chai;
|
||||
|
||||
// to help WebStorm to handle the test cases
|
||||
declare let describe: any;
|
||||
declare const after: any;
|
||||
declare const before: any;
|
||||
const tmpDescribe = describe;
|
||||
describe = DBTestHelper.describe(); // fake it os IDE plays nicely (recognize the test)
|
||||
|
||||
|
||||
describe('AlbumManager', (sqlHelper: DBTestHelper) => {
|
||||
describe = tmpDescribe;
|
||||
/**
|
||||
* dir
|
||||
* |- v
|
||||
* |- p
|
||||
* |- p2
|
||||
* |-> subDir
|
||||
* |- p3
|
||||
* |-> subDir2
|
||||
* |- p4
|
||||
*/
|
||||
|
||||
let dir: DirectoryDTO;
|
||||
let subDir: DirectoryDTO;
|
||||
let subDir2: DirectoryDTO;
|
||||
let v: VideoDTO;
|
||||
let p: PhotoDTO;
|
||||
let p2: PhotoDTO;
|
||||
let p3: PhotoDTO;
|
||||
let p4: PhotoDTO;
|
||||
|
||||
|
||||
const setUpTestGallery = async (): Promise<void> => {
|
||||
const directory: DirectoryDTO = TestHelper.getDirectoryEntry();
|
||||
subDir = TestHelper.getDirectoryEntry(directory, 'The Phantom Menace');
|
||||
subDir2 = TestHelper.getDirectoryEntry(directory, 'Return of the Jedi');
|
||||
p = TestHelper.getRandomizedPhotoEntry(directory, 'Photo1');
|
||||
p2 = TestHelper.getRandomizedPhotoEntry(directory, 'Photo2');
|
||||
p3 = TestHelper.getRandomizedPhotoEntry(subDir, 'Photo3');
|
||||
p4 = TestHelper.getRandomizedPhotoEntry(subDir2, 'Photo4');
|
||||
v = TestHelper.getVideoEntry1(directory);
|
||||
|
||||
dir = await DBTestHelper.persistTestDir(directory);
|
||||
subDir = dir.directories[0];
|
||||
subDir2 = dir.directories[1];
|
||||
p = (dir.media.filter(m => m.name === p.name)[0] as any);
|
||||
p2 = (dir.media.filter(m => m.name === p2.name)[0] as any);
|
||||
v = (dir.media.filter(m => m.name === v.name)[0] as any);
|
||||
p3 = (dir.directories[0].media[0] as any);
|
||||
p4 = (dir.directories[1].media[0] as any);
|
||||
};
|
||||
|
||||
const setUpSqlDB = async () => {
|
||||
await sqlHelper.initDB();
|
||||
await setUpTestGallery();
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
};
|
||||
|
||||
|
||||
const toAlbumPreview = (m: MediaDTO): MediaDTO => {
|
||||
const tmpM = m.directory.media;
|
||||
const tmpD = m.directory.directories;
|
||||
const tmpP = m.directory.preview;
|
||||
const tmpMT = m.directory.metaFile;
|
||||
delete m.directory.directories;
|
||||
delete m.directory.media;
|
||||
delete m.directory.preview;
|
||||
delete m.directory.metaFile;
|
||||
const ret = Utils.clone(m);
|
||||
delete (ret.metadata as PhotoMetadata).faces;
|
||||
m.directory.directories = tmpD;
|
||||
m.directory.media = tmpM;
|
||||
m.directory.preview = tmpP;
|
||||
m.directory.metaFile = tmpMT;
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
||||
before(setUpSqlDB);
|
||||
after(sqlHelper.clearDB);
|
||||
|
||||
describe('Saved search', () => {
|
||||
|
||||
|
||||
beforeEach(setUpSqlDB);
|
||||
afterEach(sqlHelper.clearDB);
|
||||
|
||||
it('should add album', async () => {
|
||||
const am = new AlbumManager();
|
||||
const connection = await SQLConnection.getConnection();
|
||||
|
||||
const query: TextSearch = {text: 'test', type: SearchQueryTypes.any_text};
|
||||
|
||||
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([]);
|
||||
|
||||
await am.addSavedSearch('Test Album', Utils.clone(query));
|
||||
|
||||
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([{
|
||||
id: 1,
|
||||
name: 'Test Album',
|
||||
searchQuery: query
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should delete album', async () => {
|
||||
const am = new AlbumManager();
|
||||
const connection = await SQLConnection.getConnection();
|
||||
|
||||
const query: TextSearch = {text: 'test', type: SearchQueryTypes.any_text};
|
||||
|
||||
|
||||
await am.addSavedSearch('Test Album', Utils.clone(query));
|
||||
await am.addSavedSearch('Test Album2', Utils.clone(query));
|
||||
|
||||
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Test Album',
|
||||
searchQuery: query
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Test Album2',
|
||||
searchQuery: query
|
||||
}]);
|
||||
|
||||
await am.deleteAlbum(1);
|
||||
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([{
|
||||
id: 2,
|
||||
name: 'Test Album2',
|
||||
searchQuery: query
|
||||
}]);
|
||||
|
||||
await am.deleteAlbum(2);
|
||||
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should list album', async () => {
|
||||
const am = new AlbumManager();
|
||||
|
||||
const query: TextSearch = {text: 'photo1', type: SearchQueryTypes.any_text};
|
||||
|
||||
await am.addSavedSearch('Test Album', Utils.clone(query));
|
||||
|
||||
expect(await am.getAlbums()).to.deep.equalInAnyOrder(([{
|
||||
id: 1,
|
||||
name: 'Test Album',
|
||||
searchQuery: query,
|
||||
preview: toAlbumPreview(p)
|
||||
}]));
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user