mirror of https://github.com/Squidex/squidex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
487 lines
14 KiB
487 lines
14 KiB
/*
|
|
* Squidex Headless CMS
|
|
*
|
|
* @license
|
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
|
|
*/
|
|
|
|
import { Injectable } from '@angular/core';
|
|
import { empty, forkJoin, Observable, of, throwError } from 'rxjs';
|
|
import { catchError, finalize, tap } from 'rxjs/operators';
|
|
|
|
import {
|
|
compareStrings,
|
|
DialogService,
|
|
LocalStoreService,
|
|
MathHelper,
|
|
Pager,
|
|
shareSubscribed,
|
|
State
|
|
} from '@app/framework';
|
|
|
|
import {
|
|
AnnotateAssetDto,
|
|
AssetDto,
|
|
AssetFolderDto,
|
|
AssetsService,
|
|
RenameAssetFolderDto
|
|
} from './../services/assets.service';
|
|
|
|
import { AppsState } from './apps.state';
|
|
import { SavedQuery } from './queries';
|
|
import { encodeQuery, Query } from './query';
|
|
|
|
export type AssetPathItem = { id: string, folderName: string };
|
|
|
|
type TagsAvailable = { [name: string]: number };
|
|
type TagsSelected = { [name: string]: boolean };
|
|
|
|
const EMPTY_FOLDERS: { canCreate: boolean, items: ReadonlyArray<AssetFolderDto> } = { canCreate: false, items: [] };
|
|
|
|
const ROOT_PATH: ReadonlyArray<AssetPathItem> = [{ id: MathHelper.EMPTY_GUID, folderName: 'Assets' }];
|
|
|
|
interface Snapshot {
|
|
// All assets tags.
|
|
tagsAvailable: TagsAvailable;
|
|
|
|
// The selected asset tags.
|
|
tagsSelected: TagsSelected;
|
|
|
|
// The current assets.
|
|
assets: ReadonlyArray<AssetDto>;
|
|
|
|
// The current asset folders.
|
|
assetFolders: ReadonlyArray<AssetFolderDto>;
|
|
|
|
// The pagination information.
|
|
assetsPager: Pager;
|
|
|
|
// The query to filter assets.
|
|
assetsQuery?: Query;
|
|
|
|
// The json of the assets query.
|
|
assetsQueryJson: string;
|
|
|
|
// The folder path.
|
|
path: ReadonlyArray<AssetPathItem>;
|
|
|
|
// The parent folder.
|
|
parentFolder?: AssetPathItem;
|
|
|
|
// Indicates if the assets are loaded.
|
|
isLoaded?: boolean;
|
|
|
|
// Indicates if the assets are loading.
|
|
isLoading?: boolean;
|
|
|
|
// Indicates if the user can create assets.
|
|
canCreate?: boolean;
|
|
|
|
// Indicates if the user can create asset folders.
|
|
canCreateFolders?: boolean;
|
|
}
|
|
|
|
@Injectable()
|
|
export class AssetsState extends State<Snapshot> {
|
|
public tagsUnsorted =
|
|
this.project(x => x.tagsAvailable);
|
|
|
|
public tagsSelected =
|
|
this.project(x => x.tagsSelected);
|
|
|
|
public tags =
|
|
this.projectFrom(this.tagsUnsorted, x => sort(x));
|
|
|
|
public tagsNames =
|
|
this.projectFrom(this.tagsUnsorted, x => Object.keys(x));
|
|
|
|
public selectedTagNames =
|
|
this.projectFrom(this.tagsSelected, x => Object.keys(x));
|
|
|
|
public assets =
|
|
this.project(x => x.assets);
|
|
|
|
public assetFolders =
|
|
this.project(x => x.assetFolders);
|
|
|
|
public assetsQuery =
|
|
this.project(x => x.assetsQuery);
|
|
|
|
public assetsPager =
|
|
this.project(x => x.assetsPager);
|
|
|
|
public isLoaded =
|
|
this.project(x => x.isLoaded === true);
|
|
|
|
public isLoading =
|
|
this.project(x => x.isLoading === true);
|
|
|
|
public path =
|
|
this.project(x => x.path);
|
|
|
|
public pathAvailable =
|
|
this.project(x => x.path.length > 0);
|
|
|
|
public parentFolder =
|
|
this.project(x => x.parentFolder);
|
|
|
|
public pathRoot =
|
|
this.project(x => x.path[x.path.length - 1]);
|
|
|
|
public canCreate =
|
|
this.project(x => x.canCreate === true);
|
|
|
|
public canCreateFolders =
|
|
this.project(x => x.canCreateFolders === true);
|
|
|
|
constructor(
|
|
private readonly appsState: AppsState,
|
|
private readonly assetsService: AssetsService,
|
|
private readonly dialogs: DialogService,
|
|
private readonly localStore: LocalStoreService
|
|
) {
|
|
super({
|
|
assetFolders: [],
|
|
assets: [],
|
|
assetsPager: Pager.fromLocalStore('assets', localStore, 30),
|
|
assetsQueryJson: '',
|
|
path: ROOT_PATH,
|
|
tagsAvailable: {},
|
|
tagsSelected: {}
|
|
});
|
|
|
|
this.assetsPager.subscribe(pager => {
|
|
pager.saveTo('assets', this.localStore);
|
|
});
|
|
}
|
|
|
|
public load(isReload = false): Observable<any> {
|
|
if (!isReload) {
|
|
this.resetState();
|
|
}
|
|
|
|
return this.loadInternal(isReload);
|
|
}
|
|
|
|
private loadInternal(isReload: boolean): Observable<any> {
|
|
this.next({ isLoading: true });
|
|
|
|
const searchTags = Object.keys(this.snapshot.tagsSelected);
|
|
|
|
const assets$ =
|
|
this.assetsService.getAssets(this.appName,
|
|
this.snapshot.assetsPager.pageSize,
|
|
this.snapshot.assetsPager.skip,
|
|
this.snapshot.assetsQuery,
|
|
searchTags, undefined, this.parentId);
|
|
|
|
const assetFolders$ =
|
|
this.snapshot.path.length === 0 ?
|
|
of(EMPTY_FOLDERS) :
|
|
this.assetsService.getAssetFolders(this.appName, this.parentId);
|
|
|
|
const tags$ =
|
|
this.snapshot.path.length === 1 ?
|
|
this.assetsService.getTags(this.appName) :
|
|
of(this.snapshot.tagsAvailable);
|
|
|
|
return forkJoin(([assets$, assetFolders$, tags$])).pipe(
|
|
tap(([assets, assetFolders, tagsAvailable]) => {
|
|
if (isReload) {
|
|
this.dialogs.notifyInfo('Assets reloaded.');
|
|
}
|
|
|
|
this.next(s => ({
|
|
...s,
|
|
assetFolders: assetFolders.items,
|
|
assets: assets.items,
|
|
assetsPager: s.assetsPager.setCount(assets.total),
|
|
canCreate: assets.canCreate,
|
|
canCreateFolders: assetFolders.canCreate,
|
|
isLoaded: true,
|
|
isLoading: false,
|
|
parentFolder: getParent(s.path),
|
|
tagsAvailable
|
|
}));
|
|
}),
|
|
finalize(() => {
|
|
this.next({ isLoading: false });
|
|
}),
|
|
shareSubscribed(this.dialogs));
|
|
}
|
|
|
|
public addAsset(asset: AssetDto) {
|
|
if (asset.parentId !== this.parentId || this.snapshot.assets.find(x => x.id === asset.id)) {
|
|
return;
|
|
}
|
|
|
|
this.next(s => {
|
|
const assets = [asset, ...s.assets];
|
|
const assetsPager = s.assetsPager.incrementCount();
|
|
|
|
const tags = updateTags(s, asset);
|
|
|
|
return { ...s, assets, assetsPager, ...tags };
|
|
});
|
|
}
|
|
|
|
public createAssetFolder(folderName: string) {
|
|
return this.assetsService.postAssetFolder(this.appName, { folderName, parentId: this.parentId }).pipe(
|
|
tap(assetFolder => {
|
|
if (assetFolder.parentId !== this.parentId) {
|
|
return;
|
|
}
|
|
|
|
this.next(s => {
|
|
const assetFolders = [...s.assetFolders, assetFolder].sortedByString(x => x.folderName);
|
|
|
|
return { ...s, assetFolders };
|
|
});
|
|
}),
|
|
shareSubscribed(this.dialogs));
|
|
}
|
|
|
|
public updateAsset(asset: AssetDto, request: AnnotateAssetDto) {
|
|
return this.assetsService.putAsset(this.appName, asset, request, asset.version).pipe(
|
|
tap(updated => {
|
|
this.next(s => {
|
|
const tags = updateTags(s, updated);
|
|
|
|
const assets = s.assets.replaceBy('id', updated);
|
|
|
|
return { ...s, assets, ...tags };
|
|
});
|
|
}),
|
|
shareSubscribed(this.dialogs, { silent: true }));
|
|
}
|
|
|
|
public updateAssetFolder(assetFolder: AssetFolderDto, request: RenameAssetFolderDto) {
|
|
return this.assetsService.putAssetFolder(this.appName, assetFolder, request, assetFolder.version).pipe(
|
|
tap(updated => {
|
|
this.next(s => {
|
|
const assetFolders = s.assetFolders.replaceBy('id', updated);
|
|
|
|
return { ...s, assetFolders };
|
|
});
|
|
}),
|
|
shareSubscribed(this.dialogs, { silent: true }));
|
|
}
|
|
|
|
public moveAsset(asset: AssetDto, parentId?: string) {
|
|
if (asset.parentId === parentId) {
|
|
return empty();
|
|
}
|
|
|
|
this.next(s => {
|
|
const assets = s.assets.filter(x => x.id !== asset.id);
|
|
|
|
return { ...s, assets };
|
|
});
|
|
|
|
return this.assetsService.putAssetItemParent(this.appName, asset, { parentId }, asset.version).pipe(
|
|
catchError(error => {
|
|
this.next(s => {
|
|
const assets = [asset, ...s.assets];
|
|
|
|
return { ...s, assets };
|
|
});
|
|
|
|
return throwError(error);
|
|
}),
|
|
shareSubscribed(this.dialogs));
|
|
}
|
|
|
|
public moveAssetFolder(assetFolder: AssetFolderDto, parentId?: string) {
|
|
if (assetFolder.id === parentId || assetFolder.parentId === parentId) {
|
|
return empty();
|
|
}
|
|
|
|
this.next(s => {
|
|
const assetFolders = s.assetFolders.filter(x => x.id !== assetFolder.id);
|
|
|
|
return { ...s, assetFolders };
|
|
});
|
|
|
|
return this.assetsService.putAssetItemParent(this.appName, assetFolder, { parentId }, assetFolder.version).pipe(
|
|
catchError(error => {
|
|
this.next(s => {
|
|
const assetFolders = [...s.assetFolders, assetFolder].sortedByString(x => x.folderName);
|
|
|
|
return { ...s, assetFolders };
|
|
});
|
|
|
|
return throwError(error);
|
|
}),
|
|
shareSubscribed(this.dialogs));
|
|
}
|
|
|
|
public deleteAsset(asset: AssetDto): Observable<any> {
|
|
return this.assetsService.deleteAssetItem(this.appName, asset, asset.version).pipe(
|
|
tap(() => {
|
|
this.next(s => {
|
|
const assets = s.assets.filter(x => x.id !== asset.id);
|
|
const assetsPager = s.assetsPager.decrementCount();
|
|
|
|
const tags = updateTags(s, undefined, asset);
|
|
|
|
return { ...s, assets, assetsPager, ...tags };
|
|
});
|
|
}),
|
|
shareSubscribed(this.dialogs));
|
|
}
|
|
|
|
public deleteAssetFolder(assetFolder: AssetFolderDto): Observable<any> {
|
|
return this.assetsService.deleteAssetItem(this.appName, assetFolder, assetFolder.version).pipe(
|
|
tap(() => {
|
|
this.next(s => {
|
|
const assetFolders = s.assetFolders.filter(x => x.id !== assetFolder.id);
|
|
|
|
return { ...s, assetFolders };
|
|
});
|
|
}),
|
|
shareSubscribed(this.dialogs));
|
|
}
|
|
|
|
public setPager(assetsPager: Pager) {
|
|
this.next({ assetsPager });
|
|
|
|
return this.loadInternal(false);
|
|
}
|
|
|
|
public searchInternal(query?: Query | null, tags?: TagsSelected) {
|
|
this.next(s => {
|
|
const newState = { ...s, assetsPager: s.assetsPager.reset() };
|
|
|
|
if (query !== null) {
|
|
newState.assetsQuery = query;
|
|
newState.assetsQueryJson = encodeQuery(query);
|
|
}
|
|
|
|
if (tags) {
|
|
newState.tagsSelected = tags;
|
|
}
|
|
|
|
if (Object.keys(newState.tagsSelected).length > 0 || (newState.assetsQuery && newState.assetsQuery.fullText)) {
|
|
newState.path = [];
|
|
newState.assetFolders = [];
|
|
} else if (newState.path.length === 0) {
|
|
newState.path = ROOT_PATH;
|
|
}
|
|
|
|
return newState;
|
|
});
|
|
|
|
return this.loadInternal(false);
|
|
}
|
|
|
|
public toggleTag(tag: string): Observable<any> {
|
|
const tagsSelected = { ...this.snapshot.tagsSelected };
|
|
|
|
if (tagsSelected[tag]) {
|
|
delete tagsSelected[tag];
|
|
} else {
|
|
tagsSelected[tag] = true;
|
|
}
|
|
|
|
return this.searchInternal(null, tagsSelected);
|
|
}
|
|
|
|
public selectTags(tags: ReadonlyArray<string>): Observable<any> {
|
|
const tagsSelected = {};
|
|
|
|
for (const tag of tags) {
|
|
tagsSelected[tag] = true;
|
|
}
|
|
|
|
return this.searchInternal(null, tagsSelected);
|
|
}
|
|
|
|
public navigate(folder: AssetPathItem) {
|
|
this.next(s => {
|
|
let path = s.path;
|
|
|
|
const index = path.findIndex(x => x.id === folder.id);
|
|
|
|
if (index >= 0) {
|
|
path = path.slice(0, index + 1);
|
|
} else {
|
|
path = [...path, folder];
|
|
}
|
|
|
|
return { ...s, path };
|
|
});
|
|
|
|
return this.loadInternal(false);
|
|
}
|
|
|
|
public resetTags(): Observable<any> {
|
|
return this.searchInternal(null, {});
|
|
}
|
|
|
|
public search(query?: Query): Observable<any> {
|
|
return this.searchInternal(query);
|
|
}
|
|
|
|
public isQueryUsed(saved: SavedQuery) {
|
|
return this.snapshot.assetsQueryJson === saved.queryJson;
|
|
}
|
|
|
|
public isTagSelected(tag: string) {
|
|
return this.snapshot.tagsSelected[tag];
|
|
}
|
|
|
|
public isTagSelectionEmpty() {
|
|
return Object.keys(this.snapshot.tagsSelected).length === 0;
|
|
}
|
|
|
|
public get parentId() {
|
|
return this.snapshot.path.length > 0 ? this.snapshot.path[this.snapshot.path.length - 1].id : undefined;
|
|
}
|
|
|
|
private get appName() {
|
|
return this.appsState.appName;
|
|
}
|
|
}
|
|
|
|
function updateTags(snapshot: Snapshot, newAsset?: AssetDto, oldAsset?: AssetDto) {
|
|
if (!oldAsset && newAsset) {
|
|
oldAsset = snapshot.assets.find(x => x.id === newAsset.id);
|
|
}
|
|
|
|
const tagsAvailable = { ...snapshot.tagsAvailable };
|
|
const tagsSelected = { ...snapshot.tagsSelected };
|
|
|
|
if (oldAsset) {
|
|
for (const tag of oldAsset.tags) {
|
|
if (tagsAvailable[tag] === 1) {
|
|
delete tagsAvailable[tag];
|
|
delete tagsSelected[tag];
|
|
} else {
|
|
tagsAvailable[tag]--;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (newAsset) {
|
|
for (const tag of newAsset.tags) {
|
|
if (tagsAvailable[tag]) {
|
|
tagsAvailable[tag]++;
|
|
} else {
|
|
tagsAvailable[tag] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return { tagsAvailable, tagsSelected };
|
|
}
|
|
|
|
function sort(tags: { [name: string]: number }) {
|
|
return Object.keys(tags).sort(compareStrings).map(name => ({ name, count: tags[name] }));
|
|
}
|
|
|
|
function getParent(path: ReadonlyArray<AssetPathItem>) {
|
|
return path.length > 1 ? { folderName: '<Parent>', id: path[path.length - 2].id } : undefined;
|
|
}
|
|
|
|
@Injectable()
|
|
export class AssetsDialogState extends AssetsState { }
|