/* * Squidex Headless CMS * * @license * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ import { Injectable } from '@angular/core'; import { empty, Observable, of } from 'rxjs'; import { catchError, finalize, tap } from 'rxjs/operators'; import { compareStrings, defined, DialogService, shareMapSubscribed, shareSubscribed, State, Types, Version } from '@app/framework'; import { AppsState } from './apps.state'; import { AddFieldDto, CreateSchemaDto, FieldDto, NestedFieldDto, RootFieldDto, SchemaDetailsDto, SchemaDto, SchemasService, UpdateFieldDto, UpdateSchemaDto, UpdateUIFields } from './../services/schemas.service'; type AnyFieldDto = NestedFieldDto | RootFieldDto; interface Snapshot { // The schema categories. categories: ReadonlyArray; // The current schemas. schemas: SchemasList; // Indicates if the schemas are loaded. isLoaded?: boolean; // Indicates if the schemas are loading. isLoading?: boolean; // The selected schema. selectedSchema?: SchemaDetailsDto | null; // Indicates if the user can create a schema. canCreate?: boolean; } export type SchemasList = ReadonlyArray; export type SchemaCategory = { name: string; schemas: SchemasList; upper: string; }; 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 categoriesPlain = this.project(x => x.categories); public selectedSchemaOrNull = this.project(x => x.selectedSchema, sameSchema); public selectedSchema = this.selectedSchemaOrNull.pipe(defined()); public schemas = this.project(x => x.schemas); public isLoaded = this.project(x => x.isLoaded === true); public isLoading = this.project(x => x.isLoading === true); public canCreate = this.project(x => x.canCreate === true); public publishedSchemas = this.projectFrom(this.schemas, x => x.filter(s => s.isPublished)); public categories = this.projectFrom2(this.schemas, this.categoriesPlain, (s, c) => buildCategories(c, s)); public get schemaId() { return this.snapshot.selectedSchema?.id || ''; } public get schemaName() { return this.snapshot.selectedSchema?.name || ''; } constructor( private readonly appsState: AppsState, private readonly dialogs: DialogService, private readonly schemasService: SchemasService ) { super({ schemas: [], categories: [] }); } public select(idOrName: string | null): Observable { return this.loadSchema(idOrName).pipe( tap(selectedSchema => { this.next(s => { return { ...s, selectedSchema }; }); })); } public loadSchema(idOrName: string | null, cached = false) { if (!idOrName) { return of(null); } if (cached) { const found = this.snapshot.schemas.find(x => x.id === idOrName || x.name === idOrName); if (Types.is(found, SchemaDetailsDto)) { return of(found); } } return this.schemasService.getSchema(this.appName, idOrName).pipe( tap(schema => { this.next(s => { const schemas = s.schemas.replaceBy('id', schema); return { ...s, schemas }; }); }), catchError(() => of(null))); } public load(isReload = false): Observable { return this.loadInternal(isReload); } public loadIfNotLoaded(): Observable { if (this.snapshot.isLoaded) { return empty(); } return this.loadInternal(false); } private loadInternal(isReload: boolean): Observable { this.next({ isLoading: true }); return this.schemasService.getSchemas(this.appName).pipe( tap(({ items, canCreate }) => { if (isReload) { this.dialogs.notifyInfo('Schemas reloaded.'); } const schemas = items.sortedByString(x => x.displayName); return this.next({ canCreate, isLoaded: true, isLoading: true, schemas }); }), finalize(() => { this.next({ isLoading: false }); }), 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, created].sortedByString(x => x.displayName); return { ...s, schemas }; }); }), 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?.id !== schema.id ? s.selectedSchema : null; return { ...s, schemas, selectedSchema }; }); }), shareSubscribed(this.dialogs)); } public addCategory(name: string) { this.next(s => { const categories = [...s.categories, name]; return { ...s, categories }; }); } public removeCategory(name: string) { this.next(s => { const categories = s.categories.removed(name); return { ...s, 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, schema.version, 'Schema saved successfully.'); }), 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, schema.version, 'Schema saved successfully.'); }), shareSubscribed(this.dialogs)); } public synchronize(schema: SchemaDto, request: {}): Observable { return this.schemasService.putSchemaSync(this.appName, schema, request, schema.version).pipe( tap(updated => { this.replaceSchema(updated, schema.version, 'Schema synchronized successfully.'); }), 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, schema.version, 'Schema saved successfully.'); }), 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 configureUIFields(schema: SchemaDto, request: UpdateUIFields): Observable { return this.schemasService.putUIFields(this.appName, schema, request, schema.version).pipe( tap(updated => { this.replaceSchema(updated, schema.version, 'Schema saved successfully.'); }), shareSubscribed(this.dialogs)); } public orderFields(schema: SchemaDto, fields: ReadonlyArray, parent?: RootFieldDto): Observable { return this.schemasService.putFieldOrdering(this.appName, parent || schema, fields.map(t => t.fieldId), schema.version).pipe( tap(updated => { this.replaceSchema(updated, schema.version, 'Schema saved successfully.'); }), 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, schema.version, 'Schema saved successfully.'); }), 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, oldVersion?: Version, updateText?: string) { if (!oldVersion || !oldVersion.eq(schema.version)) { if (updateText) { this.dialogs.notifyInfo(updateText); } this.next(s => { const schemas = s.schemas.replaceBy('id', schema).sortedByString(x => x.displayName); const selectedSchema = Types.is(schema, SchemaDetailsDto) && schema && s.selectedSchema && s.selectedSchema.id === schema.id ? schema : s.selectedSchema; return { ...s, schemas, selectedSchema }; }); } else { if (updateText) { this.dialogs.notifyInfo('Nothing has been changed.'); } } } private get appName() { return this.appsState.appName; } } function getField(x: SchemaDetailsDto, request: AddFieldDto, parent?: RootFieldDto): FieldDto { if (parent) { return x.fields.find(f => f.fieldId === parent.fieldId)!.nested.find(f => f.name === request.name)!; } else { return x.fields.find(f => f.name === request.name)!; } } function buildCategories(categories: ReadonlyArray, schemas: SchemasList): ReadonlyArray { const uniqueCategories: { [name: string]: true } = {}; for (const category of categories) { uniqueCategories[category] = true; } for (const schema of schemas) { uniqueCategories[getCategory(schema)] = true; } const result: SchemaCategory[] = []; for (const name in uniqueCategories) { if (uniqueCategories.hasOwnProperty(name)) { result.push({ name, upper: name.toUpperCase(), schemas: schemas.filter(x => isSameCategory(name, x))}); } } result.sort((a, b) => compareStrings(a.upper, b.upper)); return result; } function getCategory(schema: SchemaDto) { return schema.category || 'Schemas'; } export function isSameCategory(name: string, schema: SchemaDto): boolean { return getCategory(schema) === name; }