Browse Source

1) Fix singletons

2) Fix modals.
pull/305/head
Sebastian 8 years ago
parent
commit
8e39ec198d
  1. 3
      src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs
  2. 2
      src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs
  3. 4
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts
  4. 7
      src/Squidex/app/features/apps/pages/apps-page.component.ts
  5. 2
      src/Squidex/app/features/content/pages/content/content-page.component.html
  6. 4
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  7. 4
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  8. 4
      src/Squidex/app/features/content/pages/contents/search-form.component.ts
  9. 6
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  10. 2
      src/Squidex/app/features/content/shared/content-item.component.html
  11. 4
      src/Squidex/app/features/content/shared/content-item.component.ts
  12. 4
      src/Squidex/app/features/content/shared/contents-selector.component.ts
  13. 4
      src/Squidex/app/features/content/shared/due-time-selector.component.ts
  14. 4
      src/Squidex/app/features/rules/pages/rules/rules-page.component.ts
  15. 2
      src/Squidex/app/features/schemas/pages/schema/field.component.html
  16. 7
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  17. 13
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  18. 4
      src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts
  19. 4
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts
  20. 4
      src/Squidex/app/features/settings/pages/clients/client.component.ts
  21. 4
      src/Squidex/app/framework/angular/forms/dropdown.component.ts
  22. 4
      src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts
  23. 59
      src/Squidex/app/framework/angular/modals/modal-view.directive.ts
  24. 4
      src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts
  25. 4
      src/Squidex/app/framework/angular/modals/tooltip.component.ts
  26. 100
      src/Squidex/app/framework/utils/modal-view.spec.ts
  27. 77
      src/Squidex/app/framework/utils/modal-view.ts
  28. 4
      src/Squidex/app/shared/components/asset.component.ts
  29. 2
      src/Squidex/app/shared/components/language-selector.component.html
  30. 4
      src/Squidex/app/shared/components/language-selector.component.ts
  31. 8
      src/Squidex/app/shared/components/markdown-editor.component.ts
  32. 8
      src/Squidex/app/shared/components/rich-editor.component.ts
  33. 8
      src/Squidex/app/shared/guards/schema-must-exist-published.guard.spec.ts
  34. 6
      src/Squidex/app/shared/guards/schema-must-exist-published.guard.ts
  35. 2
      src/Squidex/app/shell/pages/internal/apps-menu.component.html
  36. 7
      src/Squidex/app/shell/pages/internal/apps-menu.component.ts
  37. 2
      src/Squidex/app/shell/pages/internal/profile-menu.component.html
  38. 4
      src/Squidex/app/shell/pages/internal/profile-menu.component.ts
  39. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs

3
src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Commands;
@ -25,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
ValidateData(command, e); ValidateData(command, e);
}); });
if (schema.IsSingleton && command.ContentId != Guid.Empty) if (schema.IsSingleton && command.ContentId != schema.Id)
{ {
throw new DomainException("Singleton content cannot be created."); throw new DomainException("Singleton content cannot be created.");
} }

2
src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs

@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var data = new NamedContentData(); var data = new NamedContentData();
var contentId = Guid.Empty; var contentId = schemaId.Id;
var content = new CreateContent { Data = data, ContentId = contentId, SchemaId = schemaId, Publish = true }; var content = new CreateContent { Data = data, ContentId = contentId, SchemaId = schemaId, Publish = true };
SimpleMapper.Map(context.Command, content); SimpleMapper.Map(context.Command, content);

4
src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts

@ -9,7 +9,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription, timer } from 'rxjs'; import { Subscription, timer } from 'rxjs';
import { onErrorResumeNext, switchMap } from 'rxjs/operators'; import { onErrorResumeNext, switchMap } from 'rxjs/operators';
import { ModalView } from '@app/shared'; import { DialogModel } from '@app/shared';
import { EventConsumerDto } from './../../services/event-consumers.service'; import { EventConsumerDto } from './../../services/event-consumers.service';
import { EventConsumersState } from './../../state/event-consumers.state'; import { EventConsumersState } from './../../state/event-consumers.state';
@ -22,7 +22,7 @@ import { EventConsumersState } from './../../state/event-consumers.state';
export class EventConsumersPageComponent implements OnDestroy, OnInit { export class EventConsumersPageComponent implements OnDestroy, OnInit {
private timerSubscription: Subscription; private timerSubscription: Subscription;
public eventConsumerErrorDialog = new ModalView(); public eventConsumerErrorDialog = new DialogModel();
public eventConsumerError = ''; public eventConsumerError = '';
constructor( constructor(

7
src/Squidex/app/features/apps/pages/apps-page.component.ts

@ -11,7 +11,8 @@ import { take } from 'rxjs/operators';
import { import {
AppsState, AppsState,
AuthService, AuthService,
ModalView, DialogModel,
ModalModel,
OnboardingService OnboardingService
} from '@app/shared'; } from '@app/shared';
@ -21,10 +22,10 @@ import {
templateUrl: './apps-page.component.html' templateUrl: './apps-page.component.html'
}) })
export class AppsPageComponent implements OnInit { export class AppsPageComponent implements OnInit {
public addAppDialog = new ModalView(); public addAppDialog = new DialogModel();
public addAppTemplate = ''; public addAppTemplate = '';
public onboardingModal = new ModalView(); public onboardingModal = new ModalModel();
constructor( constructor(
public readonly appsState: AppsState, public readonly appsState: AppsState,

2
src/Squidex/app/features/content/pages/content/content-page.component.html

@ -44,7 +44,7 @@
</button> </button>
<ng-container *ngIf="content.isPending || !schema.isSingleton"> <ng-container *ngIf="content.isPending || !schema.isSingleton">
<div class="dropdown-menu" *sqxModalView="dropdown" [sqxModalTarget]="optionsButton" @fade> <div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="optionsButton" @fade>
<ng-container *ngIf="content.isPending"> <ng-container *ngIf="content.isPending">
<a class="dropdown-item" (click)="discardChanges()"> <a class="dropdown-item" (click)="discardChanges()">
Discard changes Discard changes

4
src/Squidex/app/features/content/pages/content/content-page.component.ts

@ -24,7 +24,7 @@ import {
ImmutableArray, ImmutableArray,
LanguagesState, LanguagesState,
MessageBus, MessageBus,
ModalView, ModalModel,
SchemaDetailsDto, SchemaDetailsDto,
SchemasState, SchemasState,
Version Version
@ -52,7 +52,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
public contentVersion: Version | null; public contentVersion: Version | null;
public contentForm: EditContentForm; public contentForm: EditContentForm;
public dropdown = new ModalView(false, true); public dropdown = new ModalModel();
public language: AppLanguageDto; public language: AppLanguageDto;
public languages: ImmutableArray<AppLanguageDto>; public languages: ImmutableArray<AppLanguageDto>;

4
src/Squidex/app/features/content/pages/contents/contents-page.component.ts

@ -16,7 +16,7 @@ import {
ContentsState, ContentsState,
ImmutableArray, ImmutableArray,
LanguagesState, LanguagesState,
ModalView, ModalModel,
SchemaDetailsDto, SchemaDetailsDto,
SchemasState SchemasState
} from '@app/shared'; } from '@app/shared';
@ -35,7 +35,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
public searchModal = new ModalView(); public searchModal = new ModalModel();
public selectedItems: { [id: string]: boolean; } = {}; public selectedItems: { [id: string]: boolean; } = {};
public selectionCount = 0; public selectionCount = 0;

4
src/Squidex/app/features/content/pages/contents/search-form.component.ts

@ -8,7 +8,7 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms'; import { FormBuilder, FormControl } from '@angular/forms';
import { ModalView } from '@app/shared'; import { ModalModel } from '@app/shared';
@Component({ @Component({
selector: 'sqx-search-form', selector: 'sqx-search-form',
@ -40,7 +40,7 @@ export class SearchFormComponent implements OnChanges {
public contentsFilter = new FormControl(); public contentsFilter = new FormControl();
public searchModal = new ModalView(); public searchModal = new ModalModel();
public searchForm = public searchForm =
this.formBuilder.group({ this.formBuilder.group({
odataOrderBy: '', odataOrderBy: '',

6
src/Squidex/app/features/content/shared/assets-editor.component.ts

@ -15,7 +15,7 @@ import {
AssetDto, AssetDto,
AssetsService, AssetsService,
ImmutableArray, ImmutableArray,
ModalView, ModalModel,
Types Types
} from '@app/shared'; } from '@app/shared';
@ -35,7 +35,7 @@ export class AssetsEditorComponent implements ControlValueAccessor {
private callChange = (v: any) => { /* NOOP */ }; private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ }; private callTouched = () => { /* NOOP */ };
public selectorModal = new ModalView(); public assetsDialog = new ModalModel();
public newAssets = ImmutableArray.empty<File>(); public newAssets = ImmutableArray.empty<File>();
public oldAssets = ImmutableArray.empty<AssetDto>(); public oldAssets = ImmutableArray.empty<AssetDto>();
@ -96,7 +96,7 @@ export class AssetsEditorComponent implements ControlValueAccessor {
this.updateValue(); this.updateValue();
} }
this.selectorModal.hide(); this.assetsDialog.hide();
} }
public onAssetRemoving(asset: AssetDto) { public onAssetRemoving(asset: AssetDto) {

2
src/Squidex/app/features/content/shared/content-item.component.html

@ -86,7 +86,7 @@
<button type="button" class="btn btn-link btn-secondary" (click)="dropdown.toggle(); $event.stopPropagation()" [class.active]="dropdown.isOpen | async" #optionsButton> <button type="button" class="btn btn-link btn-secondary" (click)="dropdown.toggle(); $event.stopPropagation()" [class.active]="dropdown.isOpen | async" #optionsButton>
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </button>
<div class="dropdown-menu" *sqxModalView="dropdown" [sqxModalTarget]="optionsButton" @fade> <div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="optionsButton" @fade>
<a class="dropdown-item" (click)="publishing.emit(); $event.stopPropagation()" *ngIf="content.status === 'Draft'"> <a class="dropdown-item" (click)="publishing.emit(); $event.stopPropagation()" *ngIf="content.status === 'Draft'">
Publish Publish
</a> </a>

4
src/Squidex/app/features/content/shared/content-item.component.ts

@ -14,7 +14,7 @@ import {
fadeAnimation, fadeAnimation,
FieldFormatter, FieldFormatter,
fieldInvariant, fieldInvariant,
ModalView, ModalModel,
PatchContentForm, PatchContentForm,
RootFieldDto, RootFieldDto,
SchemaDetailsDto, SchemaDetailsDto,
@ -70,7 +70,7 @@ export class ContentItemComponent implements OnChanges {
public patchForm: PatchContentForm; public patchForm: PatchContentForm;
public dropdown = new ModalView(false, true); public dropdown = new ModalModel();
public values: any[] = []; public values: any[] = [];

4
src/Squidex/app/features/content/shared/contents-selector.component.ts

@ -12,7 +12,7 @@ import {
ContentDto, ContentDto,
LanguageDto, LanguageDto,
ManualContentsState, ManualContentsState,
ModalView, ModalModel,
SchemaDetailsDto SchemaDetailsDto
} from '@app/shared'; } from '@app/shared';
@ -37,7 +37,7 @@ export class ContentsSelectorComponent implements OnInit {
@Output() @Output()
public selected = new EventEmitter<ContentDto[]>(); public selected = new EventEmitter<ContentDto[]>();
public searchModal = new ModalView(); public searchModal = new ModalModel();
public selectedItems: { [id: string]: ContentDto; } = {}; public selectedItems: { [id: string]: ContentDto; } = {};
public selectionCount = 0; public selectionCount = 0;

4
src/Squidex/app/features/content/shared/due-time-selector.component.ts

@ -8,7 +8,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { fadeAnimation, ModalView } from '@app/shared'; import { DialogModel, fadeAnimation } from '@app/shared';
@Component({ @Component({
selector: 'sqx-due-time-selector', selector: 'sqx-due-time-selector',
@ -19,7 +19,7 @@ import { fadeAnimation, ModalView } from '@app/shared';
] ]
}) })
export class DueTimeSelectorComponent { export class DueTimeSelectorComponent {
public dueTimeDialog = new ModalView(); public dueTimeDialog = new DialogModel();
public dueTime: string | null = ''; public dueTime: string | null = '';
public dueTimeFunction: Subject<string | null>; public dueTimeFunction: Subject<string | null>;
public dueTimeAction: string | null = ''; public dueTimeAction: string | null = '';

4
src/Squidex/app/features/rules/pages/rules/rules-page.component.ts

@ -10,7 +10,7 @@ import { onErrorResumeNext } from 'rxjs/operators';
import { import {
AppsState, AppsState,
ModalView, DialogModel,
ruleActions, ruleActions,
RuleDto, RuleDto,
RulesState, RulesState,
@ -27,7 +27,7 @@ export class RulesPageComponent implements OnInit {
public ruleActions = ruleActions; public ruleActions = ruleActions;
public ruleTriggers = ruleTriggers; public ruleTriggers = ruleTriggers;
public addRuleDialog = new ModalView(); public addRuleDialog = new DialogModel();
public wizardMode = 'Wizard'; public wizardMode = 'Wizard';
public wizardRule: RuleDto | null; public wizardRule: RuleDto | null;

2
src/Squidex/app/features/schemas/pages/schema/field.component.html

@ -121,7 +121,7 @@
</button> </button>
</div> </div>
<ng-container *sqxModalView="addFieldDialog;onRoot:true;closeAuto:false"> <ng-container *sqxModalView="addFieldDialog;onRoot:true;closeAuto:false;closeAlways:true">
<sqx-field-wizard [schema]="schema" [parent]="field" <sqx-field-wizard [schema]="schema" [parent]="field"
(completed)="addFieldDialog.hide()"> (completed)="addFieldDialog.hide()">
</sqx-field-wizard> </sqx-field-wizard>

7
src/Squidex/app/features/schemas/pages/schema/field.component.ts

@ -12,10 +12,11 @@ import { onErrorResumeNext } from 'rxjs/operators';
import { import {
AppPatternDto, AppPatternDto,
createProperties, createProperties,
DialogModel,
EditFieldForm, EditFieldForm,
fadeAnimation, fadeAnimation,
ImmutableArray, ImmutableArray,
ModalView, ModalModel,
NestedFieldDto, NestedFieldDto,
RootFieldDto, RootFieldDto,
SchemaDetailsDto, SchemaDetailsDto,
@ -44,14 +45,14 @@ export class FieldComponent implements OnInit {
@Input() @Input()
public patterns: ImmutableArray<AppPatternDto>; public patterns: ImmutableArray<AppPatternDto>;
public dropdown = new ModalView(false, true); public dropdown = new ModalModel();
public isEditing = false; public isEditing = false;
public selectedTab = 0; public selectedTab = 0;
public editForm: EditFieldForm; public editForm: EditFieldForm;
public addFieldDialog = new ModalView(); public addFieldDialog = new DialogModel();
constructor( constructor(
private readonly formBuilder: FormBuilder, private readonly formBuilder: FormBuilder,

13
src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts

@ -14,11 +14,12 @@ import { filter, map, onErrorResumeNext } from 'rxjs/operators';
import { import {
AppsState, AppsState,
DialogModel,
fadeAnimation, fadeAnimation,
FieldDto, FieldDto,
fieldTypes, fieldTypes,
MessageBus, MessageBus,
ModalView, ModalModel,
PatternsState, PatternsState,
SchemaDetailsDto, SchemaDetailsDto,
SchemasState, SchemasState,
@ -45,14 +46,14 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
public schemaExport: any; public schemaExport: any;
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
public exportSchemaDialog = new ModalView(); public exportSchemaDialog = new DialogModel();
public configureScriptsDialog = new ModalView(); public configureScriptsDialog = new DialogModel();
public editOptionsDropdown = new ModalView(); public editOptionsDropdown = new ModalModel();
public editSchemaDialog = new ModalView(); public editSchemaDialog = new DialogModel();
public addFieldDialog = new ModalView(); public addFieldDialog = new DialogModel();
constructor( constructor(
public readonly appsState: AppsState, public readonly appsState: AppsState,

4
src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts

@ -14,7 +14,7 @@ import {
AppPatternDto, AppPatternDto,
FieldDto, FieldDto,
ImmutableArray, ImmutableArray,
ModalView, ModalModel,
StringFieldPropertiesDto StringFieldPropertiesDto
} from '@app/shared'; } from '@app/shared';
@ -43,7 +43,7 @@ export class StringValidationComponent implements OnDestroy, OnInit {
public showPatternSuggestions: Observable<boolean>; public showPatternSuggestions: Observable<boolean>;
public patternName: string; public patternName: string;
public patternsModal = new ModalView(false, false); public patternsModal = new ModalModel();
public ngOnDestroy() { public ngOnDestroy() {
this.patternSubscription.unsubscribe(); this.patternSubscription.unsubscribe();

4
src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts

@ -14,8 +14,8 @@ import { map, onErrorResumeNext } from 'rxjs/operators';
import { import {
AppsState, AppsState,
CreateCategoryForm, CreateCategoryForm,
DialogModel,
MessageBus, MessageBus,
ModalView,
SchemaDto, SchemaDto,
SchemasState SchemasState
} from '@app/shared'; } from '@app/shared';
@ -30,7 +30,7 @@ import { SchemaCloning } from './../messages';
export class SchemasPageComponent implements OnDestroy, OnInit { export class SchemasPageComponent implements OnDestroy, OnInit {
private schemaCloningSubscription: Subscription; private schemaCloningSubscription: Subscription;
public addSchemaDialog = new ModalView(); public addSchemaDialog = new DialogModel();
public addCategoryForm = new CreateCategoryForm(this.formBuilder); public addCategoryForm = new CreateCategoryForm(this.formBuilder);
public schemasFilter = new FormControl(); public schemasFilter = new FormControl();

4
src/Squidex/app/features/settings/pages/clients/client.component.ts

@ -15,8 +15,8 @@ import {
AppClientsService, AppClientsService,
AppsState, AppsState,
ClientsState, ClientsState,
DialogModel,
DialogService, DialogService,
ModalView,
RenameClientForm, RenameClientForm,
UpdateAppClientDto UpdateAppClientDto
} from '@app/shared'; } from '@app/shared';
@ -37,7 +37,7 @@ export class ClientComponent implements OnChanges {
public isRenaming = false; public isRenaming = false;
public token: AccessTokenDto; public token: AccessTokenDto;
public tokenDialog = new ModalView(); public tokenDialog = new DialogModel();
public renameForm = new RenameClientForm(this.formBuilder); public renameForm = new RenameClientForm(this.formBuilder);

4
src/Squidex/app/framework/angular/forms/dropdown.component.ts

@ -13,7 +13,7 @@ const KEY_ESCAPE = 27;
const KEY_UP = 38; const KEY_UP = 38;
const KEY_DOWN = 40; const KEY_DOWN = 40;
import { ModalView } from '@app/framework/internal'; import { ModalModel } from '@app/framework/internal';
export const SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = { export const SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DropdownComponent), multi: true provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DropdownComponent), multi: true
@ -35,7 +35,7 @@ export class DropdownComponent implements AfterContentInit, ControlValueAccessor
@ContentChildren(TemplateRef) @ContentChildren(TemplateRef)
public templates: QueryList<any>; public templates: QueryList<any>;
public dropdown = new ModalView(); public dropdown = new ModalModel();
public selectedItem: any; public selectedItem: any;
public selectedIndex = -1; public selectedIndex = -1;

4
src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts

@ -9,10 +9,10 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { import {
DialogModel,
DialogRequest, DialogRequest,
DialogService, DialogService,
fadeAnimation, fadeAnimation,
ModalView,
Notification Notification
} from '@app/framework/internal'; } from '@app/framework/internal';
@ -29,7 +29,7 @@ export class DialogRendererComponent implements OnDestroy, OnInit {
private dialogsSubscription: Subscription; private dialogsSubscription: Subscription;
private notificationsSubscription: Subscription; private notificationsSubscription: Subscription;
public dialogView = new ModalView(false, true); public dialogView = new DialogModel();
public dialogRequest: DialogRequest | null = null; public dialogRequest: DialogRequest | null = null;
public notifications: Notification[] = []; public notifications: Notification[] = [];

59
src/Squidex/app/framework/angular/modals/modal-view.directive.ts

@ -8,7 +8,11 @@
import { Directive, EmbeddedViewRef, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges, TemplateRef, ViewContainerRef } from '@angular/core'; import { Directive, EmbeddedViewRef, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges, TemplateRef, ViewContainerRef } from '@angular/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { ModalView, Types } from '@app/framework/internal'; import {
DialogModel,
ModalModel,
Types
} from '@app/framework/internal';
import { RootViewComponent } from './root-view.component'; import { RootViewComponent } from './root-view.component';
@ -21,7 +25,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
private renderedView: EmbeddedViewRef<any> | null = null; private renderedView: EmbeddedViewRef<any> | null = null;
@Input('sqxModalView') @Input('sqxModalView')
public modalView: ModalView | any; public modalView: DialogModel | ModalModel | any;
@Input('sqxModalViewOnRoot') @Input('sqxModalViewOnRoot')
public placeOnRoot = false; public placeOnRoot = false;
@ -29,6 +33,9 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
@Input('sqxModalViewCloseAuto') @Input('sqxModalViewCloseAuto')
public closeAuto = true; public closeAuto = true;
@Input('sqxModalViewCloseAlways')
public closeAlways = false;
constructor( constructor(
private readonly templateRef: TemplateRef<any>, private readonly templateRef: TemplateRef<any>,
private readonly renderer: Renderer2, private readonly renderer: Renderer2,
@ -38,9 +45,10 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
} }
public ngOnDestroy() { public ngOnDestroy() {
this.stopListening(); this.unsubscribeToModal();
this.unsubscribeToClick();
if (Types.is(this.modalView, ModalView)) { if (Types.is(this.modalView, DialogModel) || Types.is(this.modalView, ModalModel)) {
this.modalView.hide(); this.modalView.hide();
} }
} }
@ -50,12 +58,9 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
return; return;
} }
if (this.subscription) { this.unsubscribeToModal();
this.subscription.unsubscribe();
this.subscription = null;
}
if (Types.is(this.modalView, ModalView)) { if (Types.is(this.modalView, DialogModel) || Types.is(this.modalView, ModalModel)) {
this.subscription = this.subscription =
this.modalView.isOpen.subscribe(isOpen => { this.modalView.isOpen.subscribe(isOpen => {
this.update(isOpen); this.update(isOpen);
@ -71,11 +76,9 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
} }
if (isOpen && !this.renderedView) { if (isOpen && !this.renderedView) {
if (this.placeOnRoot) { const container = this.getContainer();
this.renderedView = this.rootView.viewContainer.createEmbeddedView(this.templateRef);
} else { this.renderedView = container.createEmbeddedView(this.templateRef);
this.renderedView = this.viewContainer.createEmbeddedView(this.templateRef);
}
if (this.renderedView.rootNodes[0].style) { if (this.renderedView.rootNodes[0].style) {
this.renderer.setStyle(this.renderedView.rootNodes[0], 'display', 'block'); this.renderer.setStyle(this.renderedView.rootNodes[0], 'display', 'block');
@ -85,18 +88,21 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
this.startListening(); this.startListening();
}); });
} else if (!isOpen && this.renderedView) { } else if (!isOpen && this.renderedView) {
this.renderedView = null; const container = this.getContainer();
const containerIndex = container.indexOf(this.renderedView);
if (this.placeOnRoot) { container.remove(containerIndex);
this.rootView.viewContainer.clear();
} else {
this.viewContainer.clear();
}
this.stopListening(); this.renderedView = null;
this.unsubscribeToClick();
} }
} }
private getContainer() {
return this.placeOnRoot ? this.rootView.viewContainer : this.viewContainer;
}
private startListening() { private startListening() {
if (!this.closeAuto) { if (!this.closeAuto) {
return; return;
@ -112,7 +118,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
return; return;
} }
if (this.modalView.closeAlways) { if (this.closeAlways) {
this.modalView.hide(); this.modalView.hide();
} else { } else {
try { try {
@ -133,7 +139,14 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
}); });
} }
private stopListening() { private unsubscribeToModal() {
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = null;
}
}
private unsubscribeToClick() {
if (this.documentClickListener) { if (this.documentClickListener) {
this.documentClickListener(); this.documentClickListener();
this.documentClickListener = null; this.documentClickListener = null;

4
src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts

@ -9,7 +9,7 @@ import { Component, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import { import {
fadeAnimation, fadeAnimation,
ModalView, ModalModel,
OnboardingService, OnboardingService,
Types Types
} from '@app/framework/internal'; } from '@app/framework/internal';
@ -27,7 +27,7 @@ export class OnboardingTooltipComponent implements OnDestroy, OnInit {
private closeTimer: any; private closeTimer: any;
private forMouseDownListener: Function | null; private forMouseDownListener: Function | null;
public tooltipModal = new ModalView(); public tooltipModal = new ModalModel();
@Input() @Input()
public for: any; public for: any;

4
src/Squidex/app/framework/angular/modals/tooltip.component.ts

@ -7,7 +7,7 @@
import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import { ModalView } from './../../utils/modal-view'; import { ModalModel } from './../../utils/modal-view';
import { fadeAnimation } from './../animations'; import { fadeAnimation } from './../animations';
@ -30,7 +30,7 @@ export class TooltipComponent implements OnDestroy, OnInit {
@Input() @Input()
public position = 'topLeft'; public position = 'topLeft';
public modal = new ModalView(false, false); public modal = new ModalModel();
constructor( constructor(
private readonly renderer: Renderer2 private readonly renderer: Renderer2

100
src/Squidex/app/framework/utils/modal-view.spec.ts

@ -5,75 +5,115 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ModalView } from './modal-view'; import { DialogModel, ModalModel, Openable } from './modal-view';
describe('ModalView', () => { describe('DialogModel', () => {
it('should have default values', () => { it('should have default values', () => {
const dialog = new ModalView(); const dialog = new DialogModel();
checkValue(dialog, false); checkValue(dialog, false);
});
it('should become open after show', () => {
const dialog = new DialogModel();
dialog.show();
expect(dialog.closeAlways).toBeFalsy(); checkValue(dialog, true);
}); });
it('should have initial true value', () => { it('should become open after toggle', () => {
const dialog = new ModalView(true); const dialog = new DialogModel();
dialog.toggle();
checkValue(dialog, true); checkValue(dialog, true);
}); });
it('should have initial false value', () => { it('should become closed after hide', () => {
const dialog = new ModalView(false); const dialog = new DialogModel().show();
dialog.hide();
checkValue(dialog, false); checkValue(dialog, false);
}); });
it('should have close always set by constructor', () => { it('should become closed after toggle', () => {
const dialog = new ModalView(false, true); const dialog = new DialogModel().show();
dialog.toggle();
expect(dialog.closeAlways).toBeTruthy(); checkValue(dialog, false);
});
it('should not hide other dialog', () => {
const dialog1 = new DialogModel().show();
const dialog2 = new DialogModel();
dialog2.toggle();
checkValue(dialog1, true);
checkValue(dialog2, true);
});
});
describe('ModalModel', () => {
it('should have default values', () => {
const modal = new ModalModel();
checkValue(modal, false);
}); });
it('should become open after show', () => { it('should become open after show', () => {
const dialog = new ModalView(false); const modal = new ModalModel();
dialog.show(); modal.show();
checkValue(dialog, true); checkValue(modal, true);
}); });
it('should become open after toggle', () => { it('should become open after toggle', () => {
const dialog = new ModalView(false); const modal = new ModalModel();
dialog.toggle(); modal.toggle();
checkValue(dialog, true); checkValue(modal, true);
}); });
it('should become closed after hide', () => { it('should become closed after hide', () => {
const dialog = new ModalView(true); const modal = new ModalModel().show();
dialog.hide(); modal.hide();
checkValue(dialog, false); checkValue(modal, false);
}); });
it('should become closed after toggle', () => { it('should become closed after toggle', () => {
const dialog = new ModalView(true); const modal = new ModalModel().show();
dialog.toggle(); modal.toggle();
checkValue(dialog, false); checkValue(modal, false);
}); });
function checkValue(dialog: ModalView, expected: boolean) { it('should hide other modal', () => {
let result: boolean; const modal1 = new ModalModel().show();
const modal2 = new ModalModel();
dialog.isOpen.subscribe(value => { modal2.toggle();
result = value;
}).unsubscribe();
expect(result!).toBe(expected); checkValue(modal1, false);
} checkValue(modal2, true);
});
}); });
function checkValue(modal: Openable, expected: boolean) {
let result: boolean;
modal.isOpen.subscribe(value => {
result = value;
}).unsubscribe();
expect(result!).toBe(expected);
}

77
src/Squidex/app/framework/utils/modal-view.ts

@ -7,49 +7,78 @@
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
export class ModalView { export interface Openable {
private readonly isOpen$: BehaviorSubject<boolean>; isOpen: Observable<boolean>;
private static openView: ModalView | null = null; }
export class DialogModel implements Openable {
private readonly isOpen$ = new BehaviorSubject<boolean>(false);
public get isOpen(): Observable<boolean> { public get isOpen(): Observable<boolean> {
return this.isOpen$; return this.isOpen$;
} }
constructor(isOpen = false, public show(): DialogModel {
public readonly closeAlways: boolean = false this.isOpen$.next(true);
) {
this.isOpen$ = new BehaviorSubject(isOpen); return this;
} }
public show() { public hide(): DialogModel {
if (ModalView.openView !== this && ModalView.openView) { this.isOpen$.next(false);
ModalView.openView.hide();
}
ModalView.openView = this; return this;
}
this.isOpen$.next(true); public toggle(): DialogModel {
this.isOpen$.next(!this.isOpen$.value);
return this;
} }
}
export class ModalModel implements Openable {
private readonly isOpen$ = new BehaviorSubject<boolean>(false);
public hide() { public get isOpen(): Observable<boolean> {
if (ModalView.openView === this) { return this.isOpen$;
ModalView.openView = null; }
public show(): ModalModel {
if (!this.isOpen$.value) {
if (openModal && openModal !== this) {
openModal.hide();
}
openModal = this;
this.isOpen$.next(true);
} }
this.isOpen$.next(false); return this;
} }
public toggle() { public hide(): ModalModel {
let isOpenSnapshot = false; if (this.isOpen$.value) {
if (openModal === this) {
openModal = null;
}
this.isOpen.subscribe(v => { this.isOpen$.next(false);
isOpenSnapshot = v; }
}).unsubscribe();
return this;
}
if (isOpenSnapshot) { public toggle(): ModalModel {
if (this.isOpen$.value) {
this.hide(); this.hide();
} else { } else {
this.show(); this.show();
} }
return this;
} }
} }
let openModal: ModalModel | null = null;

4
src/Squidex/app/shared/components/asset.component.ts

@ -14,9 +14,9 @@ import {
AssetsService, AssetsService,
AuthService, AuthService,
DateTime, DateTime,
DialogModel,
DialogService, DialogService,
fadeAnimation, fadeAnimation,
ModalView,
RenameAssetForm, RenameAssetForm,
Types, Types,
UpdateAssetDto, UpdateAssetDto,
@ -68,7 +68,7 @@ export class AssetComponent implements OnInit {
@Output() @Output()
public failed = new EventEmitter(); public failed = new EventEmitter();
public renameDialog = new ModalView(); public renameDialog = new DialogModel();
public renameForm = new RenameAssetForm(this.formBuilder); public renameForm = new RenameAssetForm(this.formBuilder);
public progress = 0; public progress = 0;

2
src/Squidex/app/shared/components/language-selector.component.html

@ -8,7 +8,7 @@
<button type="button" class="btn btn-secondary dropdown-toggle" [attr.title]="selectedLanguage.englishName" (click)="dropdown.toggle(); $event.stopPropagation()" #button tabindex="-1"> <button type="button" class="btn btn-secondary dropdown-toggle" [attr.title]="selectedLanguage.englishName" (click)="dropdown.toggle(); $event.stopPropagation()" #button tabindex="-1">
{{selectedLanguage.iso2Code}} {{selectedLanguage.iso2Code}}
</button> </button>
<div class="dropdown-menu" *sqxModalView="dropdown" [sqxModalTarget]="button" @fade> <div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="button" @fade>
<div class="dropdown-item" *ngFor="let language of languages" [class.active]="language == selectedLanguage" (click)="selectLanguage(language)"> <div class="dropdown-item" *ngFor="let language of languages" [class.active]="language == selectedLanguage" (click)="selectLanguage(language)">
<strong class="iso-code">{{language.iso2Code}}</strong> &nbsp; &nbsp; ({{language.englishName}}) <strong class="iso-code">{{language.iso2Code}}</strong> &nbsp; &nbsp; ({{language.englishName}})
</div> </div>

4
src/Squidex/app/shared/components/language-selector.component.ts

@ -7,7 +7,7 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { fadeAnimation, ModalView } from '@app/shared/internal'; import { fadeAnimation, ModalModel } from '@app/shared/internal';
export interface Language { iso2Code: string; englishName: string; isMasterLanguage: true; } export interface Language { iso2Code: string; englishName: string; isMasterLanguage: true; }
@ -20,7 +20,7 @@ export interface Language { iso2Code: string; englishName: string; isMasterLangu
] ]
}) })
export class LanguageSelectorComponent implements OnChanges, OnInit { export class LanguageSelectorComponent implements OnChanges, OnInit {
public dropdown = new ModalView(false, true); public dropdown = new ModalModel();
@Input() @Input()
public size: string; public size: string;

8
src/Squidex/app/shared/components/markdown-editor.component.ts

@ -10,7 +10,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
AssetDto, AssetDto,
ModalView, DialogModel,
ResourceLoaderService, ResourceLoaderService,
Types Types
} from '@app/shared/internal'; } from '@app/shared/internal';
@ -34,7 +34,7 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
private value: string; private value: string;
private isDisabled = false; private isDisabled = false;
public selectorModal = new ModalView(); public assetsDialog = new DialogModel();
@ViewChild('editor') @ViewChild('editor')
public editor: ElementRef; public editor: ElementRef;
@ -79,7 +79,7 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
} }
private showSelector = () => { private showSelector = () => {
this.selectorModal.show(); this.assetsDialog.show();
} }
public ngAfterViewInit() { public ngAfterViewInit() {
@ -206,6 +206,6 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
this.simplemde.codemirror.replaceSelection(content); this.simplemde.codemirror.replaceSelection(content);
} }
this.selectorModal.hide(); this.assetsDialog.hide();
} }
} }

8
src/Squidex/app/shared/components/rich-editor.component.ts

@ -10,7 +10,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
AssetDto, AssetDto,
ModalView, DialogModel,
ResourceLoaderService, ResourceLoaderService,
Types Types
} from '@app/shared/internal'; } from '@app/shared/internal';
@ -35,7 +35,7 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
private value: string; private value: string;
private isDisabled = false; private isDisabled = false;
public selectorModal = new ModalView(); public assetsDialog = new DialogModel();
@ViewChild('editor') @ViewChild('editor')
public editor: ElementRef; public editor: ElementRef;
@ -63,7 +63,7 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
} }
private showSelector = () => { private showSelector = () => {
this.selectorModal.show(); this.assetsDialog.show();
} }
private getEditorOptions() { private getEditorOptions() {
@ -147,6 +147,6 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
this.tinyEditor.execCommand('mceInsertContent', false, content); this.tinyEditor.execCommand('mceInsertContent', false, content);
} }
this.selectorModal.hide(); this.assetsDialog.hide();
} }
} }

8
src/Squidex/app/shared/guards/schema-must-exist-published.guard.spec.ts

@ -9,8 +9,6 @@ import { Router, RouterStateSnapshot } from '@angular/router';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq'; import { IMock, Mock, Times } from 'typemoq';
import { MathHelper } from '@app/framework';
import { SchemaDetailsDto } from './../services/schemas.service'; import { SchemaDetailsDto } from './../services/schemas.service';
import { SchemasState } from './../state/schemas.state'; import { SchemasState } from './../state/schemas.state';
import { SchemaMustExistPublishedGuard } from './schema-must-exist-published.guard'; import { SchemaMustExistPublishedGuard } from './schema-must-exist-published.guard';
@ -50,7 +48,7 @@ describe('SchemaMustExistPublishedGuard', () => {
it('should load schema and return false when not found', () => { it('should load schema and return false when not found', () => {
schemasState.setup(x => x.select('123')) schemasState.setup(x => x.select('123'))
.returns(() => of(<SchemaDetailsDto>{ isPublished: false })); .returns(() => of(<SchemaDetailsDto>{ isPublished: false }));
let result: boolean; let result: boolean;
@ -80,7 +78,7 @@ describe('SchemaMustExistPublishedGuard', () => {
it('should redirect to content when singleton', () => { it('should redirect to content when singleton', () => {
schemasState.setup(x => x.select('123')) schemasState.setup(x => x.select('123'))
.returns(() => of(<SchemaDetailsDto>{ isSingleton: true })); .returns(() => of(<SchemaDetailsDto>{ isSingleton: true, id: 'schema-id' }));
let result: boolean; let result: boolean;
@ -90,6 +88,6 @@ describe('SchemaMustExistPublishedGuard', () => {
expect(result!).toBeFalsy(); expect(result!).toBeFalsy();
router.verify(x => x.navigate([state.url, MathHelper.EMPTY_GUID]), Times.once()); router.verify(x => x.navigate([state.url, 'schema-id']), Times.once());
}); });
}); });

6
src/Squidex/app/shared/guards/schema-must-exist-published.guard.ts

@ -10,7 +10,7 @@ import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
import { allParams, MathHelper } from '@app/framework'; import { allParams } from '@app/framework';
import { SchemasState } from './../state/schemas.state'; import { SchemasState } from './../state/schemas.state';
@ -32,8 +32,8 @@ export class SchemaMustExistPublishedGuard implements CanActivate {
this.router.navigate(['/404']); this.router.navigate(['/404']);
} }
if (dto && dto.isSingleton && state.url.indexOf(MathHelper.EMPTY_GUID) < 0) { if (dto && dto.isSingleton && state.url.indexOf(dto.id) < 0) {
this.router.navigate([state.url, MathHelper.EMPTY_GUID]); this.router.navigate([state.url, dto.id]);
} }
}), }),
map(s => s !== null && s.isPublished)); map(s => s !== null && s.isPublished));

2
src/Squidex/app/shell/pages/internal/apps-menu.component.html

@ -11,7 +11,7 @@
</span> </span>
<ng-container *ngIf="appsState.apps | async; let apps"> <ng-container *ngIf="appsState.apps | async; let apps">
<div class="dropdown-menu" *sqxModalView="appsMenu" @fade> <div class="dropdown-menu" *sqxModalView="appsMenu;closeAlways:true" @fade>
<a class="dropdown-item all-apps" routerLink="/app"> <a class="dropdown-item all-apps" routerLink="/app">
<span class="all-apps-text">All Apps</span> <span class="all-apps-text">All Apps</span>
<span class="all-apps-pill badge badge-pill badge-primary">{{apps.length}}</span> <span class="all-apps-pill badge badge-pill badge-primary">{{apps.length}}</span>

7
src/Squidex/app/shell/pages/internal/apps-menu.component.ts

@ -9,8 +9,9 @@ import { Component } from '@angular/core';
import { import {
AppsState, AppsState,
DialogModel,
fadeAnimation, fadeAnimation,
ModalView ModalModel
} from '@app/shared'; } from '@app/shared';
@Component({ @Component({
@ -22,9 +23,9 @@ import {
] ]
}) })
export class AppsMenuComponent { export class AppsMenuComponent {
public addAppDialog = new ModalView(); public addAppDialog = new DialogModel();
public appsMenu = new ModalView(false, true); public appsMenu = new ModalModel();
constructor( constructor(
public readonly appsState: AppsState public readonly appsState: AppsState

2
src/Squidex/app/shell/pages/internal/profile-menu.component.html

@ -8,7 +8,7 @@
</span> </span>
</span> </span>
<div class="dropdown-menu" *sqxModalView="modalMenu" @fade> <div class="dropdown-menu" *sqxModalView="modalMenu;closeAlways:true" @fade>
<a class="dropdown-item" routerLink="/app/administration" *ngIf="isAdmin"> <a class="dropdown-item" routerLink="/app/administration" *ngIf="isAdmin">
Administration Administration
</a> </a>

4
src/Squidex/app/shell/pages/internal/profile-menu.component.ts

@ -13,7 +13,7 @@ import {
ApiUrlConfig, ApiUrlConfig,
AuthService, AuthService,
fadeAnimation, fadeAnimation,
ModalView ModalModel
} from '@app/shared'; } from '@app/shared';
@Component({ @Component({
@ -27,7 +27,7 @@ import {
export class ProfileMenuComponent implements OnDestroy, OnInit { export class ProfileMenuComponent implements OnDestroy, OnInit {
private authenticationSubscription: Subscription; private authenticationSubscription: Subscription;
public modalMenu = new ModalView(false, true); public modalMenu = new ModalModel();
public profileDisplayName = ''; public profileDisplayName = '';
public profileId = ''; public profileId = '';

4
tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs

@ -43,11 +43,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
} }
[Fact] [Fact]
public void CanCreate_should_not_throw_exception_if_singleton_and_id_empty() public void CanCreate_should_not_throw_exception_if_singleton_and_id_is_schema_id()
{ {
A.CallTo(() => schema.IsSingleton).Returns(true); A.CallTo(() => schema.IsSingleton).Returns(true);
var command = new CreateContent { Data = new NamedContentData(), ContentId = Guid.Empty }; var command = new CreateContent { Data = new NamedContentData(), ContentId = schema.Id };
GuardContent.CanCreate(schema, command); GuardContent.CanCreate(schema, command);
} }

Loading…
Cancel
Save