/* * Squidex Headless CMS * * @license * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; import { compareStringsAsc, DialogService, ImmutableArray, shareMapSubscribed, shareSubscribed, State, Types } from '@app/framework'; import { AppsState } from './apps.state'; import { AddFieldDto, CreateSchemaDto, FieldDto, NestedFieldDto, RootFieldDto, SchemaDetailsDto, SchemaDto, SchemasService, UpdateFieldDto, UpdateSchemaDto } from './../services/schemas.service'; type AnyFieldDto = NestedFieldDto | RootFieldDto; interface Snapshot { // The schema categories. categories: { [name: string]: boolean }; // The current schemas. schemas: SchemasList; // Indicates if the schemas are loaded. isLoaded?: boolean; // The selected schema. selectedSchema?: SchemaDetailsDto | null; // Indicates if the user can create a schema. canCreate?: boolean; } export type SchemasList = ImmutableArray; export type Categories = { [name: string]: boolean }; function sameSchema(lhs: SchemaDetailsDto | null, rhs?: SchemaDetailsDto | null): boolean { return lhs === rhs || (!!lhs && !!rhs && lhs.id === rhs.id && lhs.version === rhs.version); } @Injectable() export class SchemasState extends State { public get schemaName() { return this.snapshot.selectedSchema ? this.snapshot.selectedSchema.name : ''; } public categories = this.project2(x => x.categories, x => sortedCategoryNames(x)); public selectedSchema = this.project(x => x.selectedSchema, sameSchema); public schemas = this.project(x => x.schemas); public publishedSchemas = this.project2(x => x.schemas, x => x.filter(s => s.isPublished)); public isLoaded = this.project(x => !!x.isLoaded); public canCreate = this.project(x => !!x.canCreate); constructor( private readonly appsState: AppsState, private readonly dialogs: DialogService, private readonly schemasService: SchemasService ) { super({ schemas: ImmutableArray.empty(), categories: buildCategories({}) }); } public select(idOrName: string | null): Observable { return this.loadSchema(idOrName).pipe( tap(selectedSchema => { this.next(s => { const schemas = selectedSchema ? s.schemas.replaceBy('id', selectedSchema) : s.schemas; return { ...s, selectedSchema, schemas }; }); })); } private loadSchema(idOrName: string | null) { return !idOrName ? of(null) : this.schemasService.getSchema(this.appName, idOrName).pipe( catchError(() => of(null))); } public load(isReload = false): Observable { if (!isReload) { const selectedSchema = this.snapshot.selectedSchema; this.resetState({ selectedSchema }); } return this.schemasService.getSchemas(this.appName).pipe( tap(({ items, canCreate }) => { if (isReload) { this.dialogs.notifyInfo('Schemas reloaded.'); } return this.next(s => { const schemas = ImmutableArray.of(items).sortByStringAsc(x => x.displayName); const categories = buildCategories(s.categories, schemas); return { ...s, schemas, isLoaded: true, categories, canCreate }; }); }), shareSubscribed(this.dialogs)); } public create(request: CreateSchemaDto): Observable { return this.schemasService.postSchema(this.appName, request).pipe( tap(created => { this.next(s => { const schemas = s.schemas.push(created).sortByStringAsc(x => x.displayName); const categories = buildCategories(s.categories, schemas); return { ...s, schemas, categories }; }); }), shareSubscribed(this.dialogs, { silent: true })); } public delete(schema: SchemaDto): Observable { return this.schemasService.deleteSchema(this.appName, schema, schema.version).pipe( tap(() => { this.next(s => { const schemas = s.schemas.filter(x => x.id !== schema.id); const selectedSchema = s.selectedSchema && s.selectedSchema.id === schema.id ? null : s.selectedSchema; return { ...s, schemas, selectedSchema }; }); }), shareSubscribed(this.dialogs)); } public addCategory(name: string) { this.next(s => { const categories = addCategory(s.categories, name); return { ...s, categories: categories }; }); } public removeCategory(name: string) { this.next(s => { const categories = removeCategory(s.categories, name); return { ...s, categories: categories }; }); } public publish(schema: SchemaDto): Observable { return this.schemasService.publishSchema(this.appName, schema, schema.version).pipe( tap(updated => { this.replaceSchema(updated); }), shareSubscribed(this.dialogs)); } public unpublish(schema: SchemaDto): Observable { return this.schemasService.unpublishSchema(this.appName, schema, schema.version).pipe( tap(updated => { this.replaceSchema(updated); }), shareSubscribed(this.dialogs)); } public changeCategory(schema: SchemaDto, name: string): Observable { return this.schemasService.putCategory(this.appName, schema, { name }, schema.version).pipe( tap(updated => { this.replaceSchema(updated); }), shareSubscribed(this.dialogs)); } public configurePreviewUrls(schema: SchemaDto, request: {}): Observable { return this.schemasService.putPreviewUrls(this.appName, schema, request, schema.version).pipe( tap(updated => { this.replaceSchema(updated); }), shareSubscribed(this.dialogs)); } public configureScripts(schema: SchemaDto, request: {}): Observable { return this.schemasService.putScripts(this.appName, schema, request, schema.version).pipe( tap(updated => { this.replaceSchema(updated); }), shareSubscribed(this.dialogs)); } public update(schema: SchemaDto, request: UpdateSchemaDto): Observable { return this.schemasService.putSchema(this.appName, schema, request, schema.version).pipe( tap(updated => { this.replaceSchema(updated); }), shareSubscribed(this.dialogs)); } public addField(schema: SchemaDto, request: AddFieldDto, parent?: RootFieldDto): Observable { return this.schemasService.postField(this.appName, parent || schema, request, schema.version).pipe( tap(updated => { this.replaceSchema(updated); }), shareMapSubscribed(this.dialogs, x => getField(x, request, parent), { silent: true })); } public orderFields(schema: SchemaDto, fields: any[], parent?: RootFieldDto): Observable { return this.schemasService.putFieldOrdering(this.appName, parent || schema, fields.map(t => t.fieldId), schema.version).pipe( tap(updated => { this.replaceSchema(updated); }), shareSubscribed(this.dialogs)); } public lockField(schema: SchemaDto, field: T): Observable { return this.schemasService.lockField(this.appName, field, schema.version).pipe( tap(updated => { this.replaceSchema(updated); }), shareSubscribed(this.dialogs)); } public enableField(schema: SchemaDto, field: T): Observable { return this.schemasService.enableField(this.appName, field, schema.version).pipe( tap(updated => { this.replaceSchema(updated); }), shareSubscribed(this.dialogs)); } public disableField(schema: SchemaDto, field: T): Observable { return this.schemasService.disableField(this.appName, field, schema.version).pipe( tap(updated => { this.replaceSchema(updated); }), shareSubscribed(this.dialogs)); } public showField(schema: SchemaDto, field: T): Observable { return this.schemasService.showField(this.appName, field, schema.version).pipe( tap(updated => { this.replaceSchema(updated); }), shareSubscribed(this.dialogs)); } public hideField(schema: SchemaDto, field: T): Observable { return this.schemasService.hideField(this.appName, field, schema.version).pipe( tap(updated => { this.replaceSchema(updated); }), shareSubscribed(this.dialogs)); } public updateField(schema: SchemaDto, field: T, request: UpdateFieldDto): Observable { return this.schemasService.putField(this.appName, field, request, schema.version).pipe( tap(updated => { this.replaceSchema(updated); }), shareSubscribed(this.dialogs)); } public deleteField(schema: SchemaDto, field: AnyFieldDto): Observable { return this.schemasService.deleteField(this.appName, field, schema.version).pipe( tap(updated => { this.replaceSchema(updated); }), shareSubscribed(this.dialogs)); } private replaceSchema(schema: SchemaDto) { return this.next(s => { const schemas = s.schemas.replaceBy('id', schema).sortByStringAsc(x => x.displayName); const selectedSchema = Types.is(schema, SchemaDetailsDto) && schema && s.selectedSchema && s.selectedSchema.id === schema.id ? schema : s.selectedSchema; const categories = buildCategories(s.categories, schemas); return { ...s, schemas, selectedSchema, categories }; }); } private get appName() { return this.appsState.appName; } } function getField(x: SchemaDetailsDto, request: AddFieldDto, parent?: RootFieldDto): FieldDto { if (parent) { return parent.nested.find(f => f.name === request.name)!; } else { return x.fields.find(f => f.name === request.name)!; } } function buildCategories(categories: { [name: string]: boolean }, schemas?: SchemasList) { categories = { ...categories }; for (let category in categories) { if (categories.hasOwnProperty(category)) { if (!categories[category]) { delete categories[category]; } } } if (schemas) { for (let schema of schemas.values) { categories[schema.category || ''] = false; } } return categories; } function addCategory(categories: Categories, category: string) { categories = { ...categories }; categories[category] = true; return categories; } function removeCategory(categories: Categories, category: string) { categories = { ...categories }; delete categories[category]; return categories; } function sortedCategoryNames(categories: Categories) { const names = Object.keys(categories); names.sort(compareStringsAsc); return names; }