Browse Source

Merge pull request #271 from Squidex/ui-logic-rework

Ui logic rework
pull/272/head
Sebastian Stehle 8 years ago
committed by GitHub
parent
commit
90017f2a2e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      Dockerfile
  2. 27
      Dockerfile.build
  3. 28
      libs/Dockerfile
  4. 1
      src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs
  5. 10
      src/Squidex/app-config/karma.coverage.conf.js
  6. 5
      src/Squidex/app-config/webpack.config.js
  7. 16
      src/Squidex/app/app.component.html
  8. 9
      src/Squidex/app/app.module.ts
  9. 3
      src/Squidex/app/app.routes.ts
  10. 10
      src/Squidex/app/features/administration/declarations.ts
  11. 37
      src/Squidex/app/features/administration/guards/unset-user.guard.spec.ts
  12. 24
      src/Squidex/app/features/administration/guards/unset-user.guard.ts
  13. 62
      src/Squidex/app/features/administration/guards/user-must-exist.guard.spec.ts
  14. 38
      src/Squidex/app/features/administration/guards/user-must-exist.guard.ts
  15. 30
      src/Squidex/app/features/administration/module.ts
  16. 139
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html
  17. 49
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts
  18. 22
      src/Squidex/app/features/administration/pages/messages.ts
  19. 95
      src/Squidex/app/features/administration/pages/users/user-page.component.html
  20. 154
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  21. 167
      src/Squidex/app/features/administration/pages/users/users-page.component.html
  22. 96
      src/Squidex/app/features/administration/pages/users/users-page.component.ts
  23. 29
      src/Squidex/app/features/administration/services/event-consumers.service.spec.ts
  24. 16
      src/Squidex/app/features/administration/services/event-consumers.service.ts
  25. 202
      src/Squidex/app/features/administration/services/users.service.spec.ts
  26. 134
      src/Squidex/app/features/administration/services/users.service.ts
  27. 211
      src/Squidex/app/features/administration/state/users.state.spec.ts
  28. 229
      src/Squidex/app/features/administration/state/users.state.ts
  29. 46
      src/Squidex/app/features/api/api-area.component.html
  30. 9
      src/Squidex/app/features/api/api-area.component.ts
  31. 2
      src/Squidex/app/features/api/module.ts
  32. 6
      src/Squidex/app/features/api/pages/graphql/graphql-page.component.html
  33. 10
      src/Squidex/app/features/api/pages/graphql/graphql-page.component.scss
  34. 12
      src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts
  35. 2
      src/Squidex/app/features/apps/module.ts
  36. 38
      src/Squidex/app/features/apps/pages/apps-page.component.html
  37. 28
      src/Squidex/app/features/apps/pages/apps-page.component.ts
  38. 13
      src/Squidex/app/features/apps/pages/onboarding-dialog.component.html
  39. 2
      src/Squidex/app/features/apps/pages/onboarding-dialog.component.ts
  40. 4
      src/Squidex/app/features/assets/module.ts
  41. 92
      src/Squidex/app/features/assets/pages/assets-page.component.html
  42. 50
      src/Squidex/app/features/assets/pages/assets-page.component.scss
  43. 102
      src/Squidex/app/features/assets/pages/assets-page.component.ts
  44. 10
      src/Squidex/app/features/content/module.ts
  45. 8
      src/Squidex/app/features/content/pages/content/content-field.component.html
  46. 13
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  47. 40
      src/Squidex/app/features/content/pages/content/content-history.component.html
  48. 2
      src/Squidex/app/features/content/pages/content/content-history.component.ts
  49. 102
      src/Squidex/app/features/content/pages/content/content-page.component.html
  50. 2
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  51. 354
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  52. 4
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  53. 5
      src/Squidex/app/features/content/pages/contents/search-form.component.ts
  54. 2
      src/Squidex/app/features/content/pages/messages.ts
  55. 42
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.html
  56. 2
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts
  57. 15
      src/Squidex/app/features/content/shared/assets-editor.component.html
  58. 2
      src/Squidex/app/features/content/shared/assets-editor.component.scss
  59. 56
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  60. 8
      src/Squidex/app/features/content/shared/content-item.component.html
  61. 2
      src/Squidex/app/features/content/shared/content-item.component.ts
  62. 2
      src/Squidex/app/features/content/shared/references-editor.component.ts
  63. 2
      src/Squidex/app/features/dashboard/module.ts
  64. 2
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts
  65. 2
      src/Squidex/app/features/rules/module.ts
  66. 185
      src/Squidex/app/features/rules/pages/events/rule-events-page.component.html
  67. 2
      src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts
  68. 2
      src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.html
  69. 9
      src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.ts
  70. 2
      src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.html
  71. 8
      src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.ts
  72. 2
      src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.html
  73. 9
      src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.ts
  74. 2
      src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.html
  75. 6
      src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.ts
  76. 2
      src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.html
  77. 5
      src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.ts
  78. 2
      src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html
  79. 5
      src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts
  80. 237
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html
  81. 6
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts
  82. 166
      src/Squidex/app/features/rules/pages/rules/rules-page.component.html
  83. 6
      src/Squidex/app/features/rules/pages/rules/rules-page.component.ts
  84. 2
      src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html
  85. 2
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html
  86. 2
      src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts
  87. 7
      src/Squidex/app/features/schemas/declarations.ts
  88. 36
      src/Squidex/app/features/schemas/guards/schema-must-exist.guard.ts
  89. 40
      src/Squidex/app/features/schemas/module.ts
  90. 25
      src/Squidex/app/features/schemas/pages/messages.ts
  91. 62
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html
  92. 72
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.scss
  93. 66
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts
  94. 131
      src/Squidex/app/features/schemas/pages/schema/field.component.html
  95. 112
      src/Squidex/app/features/schemas/pages/schema/field.component.ts
  96. 56
      src/Squidex/app/features/schemas/pages/schema/forms/field-form-common.component.html
  97. 2
      src/Squidex/app/features/schemas/pages/schema/forms/field-form-common.component.scss
  98. 31
      src/Squidex/app/features/schemas/pages/schema/forms/field-form-common.component.ts
  99. 29
      src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.html
  100. 2
      src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.scss

2
Dockerfile

@ -1,7 +1,7 @@
#
# Stage 1, Prebuild
#
FROM squidex/aspnetcore-build-phantomjs:2.0.3-jessie as builder
FROM squidex/aspnetcore-build-phantomjs-chromium:2.0.3-jessie-fix1 as builder
COPY src/Squidex/package.json /tmp/package.json

27
Dockerfile.build

@ -1,30 +1,7 @@
FROM microsoft/aspnetcore-build:2.0.0
# Install runtime dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates bzip2 libfontconfig \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install official PhantomJS release
RUN set -x \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
&& mkdir /srv/var \
&& mkdir /tmp/phantomjs \
# Download Phantom JS
&& curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar -xj --strip-components=1 -C /tmp/phantomjs \
# Copy binaries only
&& mv /tmp/phantomjs/bin/phantomjs /usr/local/bin \
# Create symbol link
# Clean up
&& apt-get autoremove -y \
&& apt-get clean all \
&& rm -rf /tmp/* /var/lib/apt/lists/*
RUN phantomjs --version
FROM squidex/aspnetcore-build-phantomjs-chromium:2.0.3-jessie-fix1 as builder
COPY src/Squidex/package.json /tmp/package.json
RUN cd /tmp \
&& npm install \
&& npm rebuild node-sass

28
libs/Dockerfile

@ -6,7 +6,7 @@ RUN apt-get update \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install official PhantomJS release
# Install official PhantomJS release
RUN set -x \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
@ -22,4 +22,28 @@ RUN set -x \
&& apt-get clean all \
&& rm -rf /tmp/* /var/lib/apt/lists/*
RUN phantomjs --version
RUN phantomjs --version
# Install Google Chrome
# See https://crbug.com/795759
RUN apt-get update && apt-get install -yq libgconf-2-4
# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
RUN apt-get update && apt-get install -y wget --no-install-recommends \
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt-get update \
&& apt-get install -y google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get autoremove -y \
&& rm -rf /src/*.deb
# It's a good idea to use dumb-init to help prevent zombie chrome processes.
ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init
RUN chmod +x /usr/local/bin/dumb-init
# Install puppeteer so it's available in the container.
RUN npm i puppeteer

1
src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs

@ -12,7 +12,6 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
{
public sealed class OrleansEventNotifier : IEventNotifier
{
private readonly IGrainFactory factory;
private readonly Lazy<IEventConsumerManagerGrain> eventConsumerManagerGrain;
public OrleansEventNotifier(IGrainFactory factory)

10
src/Squidex/app-config/karma.coverage.conf.js

@ -65,12 +65,20 @@ module.exports = function (config) {
*/
singleRun: true,
customLaunchers: {
ChromeCustom: {
base: 'ChromeHeadless',
// We must disable the Chrome sandbox (Chrome's sandbox needs more permissions than Docker allows by default)
flags: ['--no-sandbox']
}
},
/**
* Run with chrome because phantom js does not provide all types, e.g. DragEvent
*
* available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
*/
browsers: ['PhantomJS']
browsers: ['ChromeCustom']
};
config.set(_config);

5
src/Squidex/app-config/webpack.config.js

@ -2,6 +2,7 @@
path = require('path'),
HtmlWebpackPlugin = require('html-webpack-plugin'),
ExtractTextPlugin = require('extract-text-webpack-plugin'),
TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'),
helpers = require('./helpers');
module.exports = {
@ -23,6 +24,10 @@ module.exports = {
helpers.root('app-libs'),
helpers.root('node_modules')
],
plugins: [
new TsconfigPathsPlugin()
]
},
/*

16
src/Squidex/app/app.component.html

@ -1,9 +1,13 @@
<main>
<router-outlet (activate)="isLoaded = true">
<div class="loading" *ngIf="!isLoaded">
<img src="/images/loader.gif" />
<sqx-root-view>
<sqx-dialog-renderer>
<router-outlet (activate)="isLoaded = true">
<div class="loading" *ngIf="!isLoaded">
<img src="/images/loader.gif" />
<div>Loading Squidex</div>
</div>
</router-outlet>
<div>Loading Squidex</div>
</div>
</router-outlet>
</sqx-dialog-renderer>
</sqx-root-view>
</main>

9
src/Squidex/app/app.module.ts

@ -6,6 +6,10 @@
*/
import { ApplicationRef, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { HttpClientModule } from '@angular/common/http';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { DndModule } from 'ng2-dnd';
@ -56,6 +60,11 @@ export function configUserReport() {
BrowserModule,
BrowserAnimationsModule,
DndModule.forRoot(),
HttpClientModule,
FormsModule,
CommonModule,
RouterModule,
ReactiveFormsModule,
SqxFrameworkModule.forRoot(),
SqxSharedModule.forRoot(),
SqxShellModule,

3
src/Squidex/app/app.routes.ts

@ -19,6 +19,7 @@ import {
import {
AppMustExistGuard,
LoadAppsGuard,
MustBeAuthenticatedGuard,
MustBeNotAuthenticatedGuard,
UnsetAppGuard
@ -33,7 +34,7 @@ export const routes: Routes = [
{
path: 'app',
component: InternalAreaComponent,
canActivate: [MustBeAuthenticatedGuard],
canActivate: [MustBeAuthenticatedGuard, LoadAppsGuard],
children: [
{
path: '',

10
src/Squidex/app/features/administration/declarations.ts

@ -5,8 +5,16 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
export * from './administration-area.component';
export * from './guards/user-must-exist.guard';
export * from './guards/unset-user.guard';
export * from './pages/event-consumers/event-consumers-page.component';
export * from './pages/users/user-page.component';
export * from './pages/users/users-page.component';
export * from './administration-area.component';
export * from './services/event-consumers.service';
export * from './services/users.service';
export * from './state/users.state';

37
src/Squidex/app/features/administration/guards/unset-user.guard.spec.ts

@ -0,0 +1,37 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Observable } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { UsersState } from './../state/users.state';
import { UnsetUserGuard } from './unset-user.guard';
describe('UnsetUserGuard', () => {
let usersState: IMock<UsersState>;
let userGuard: UnsetUserGuard;
beforeEach(() => {
usersState = Mock.ofType<UsersState>();
userGuard = new UnsetUserGuard(usersState.object);
});
it('should unset user', () => {
usersState.setup(x => x.selectUser(null))
.returns(() => Observable.of(null));
let result: boolean;
userGuard.canActivate().subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeTruthy();
usersState.verify(x => x.selectUser(null), Times.once());
});
});

24
src/Squidex/app/features/administration/guards/unset-user.guard.ts

@ -0,0 +1,24 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { Observable } from 'rxjs';
import { UsersState } from './../state/users.state';
@Injectable()
export class UnsetUserGuard implements CanActivate {
constructor(
private readonly usersState: UsersState
) {
}
public canActivate(): Observable<boolean> {
return this.usersState.selectUser(null).map(u => u === null);
}
}

62
src/Squidex/app/features/administration/guards/user-must-exist.guard.spec.ts

@ -0,0 +1,62 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import { UserDto } from './../services/users.service';
import { UsersState } from './../state/users.state';
import { UserMustExistGuard } from './user-must-exist.guard';
describe('UserMustExistGuard', () => {
const route: any = {
params: {
userId: '123'
}
};
let usersState: IMock<UsersState>;
let router: IMock<Router>;
let userGuard: UserMustExistGuard;
beforeEach(() => {
router = Mock.ofType<Router>();
usersState = Mock.ofType<UsersState>();
userGuard = new UserMustExistGuard(usersState.object, router.object);
});
it('should load user and return true when found', () => {
usersState.setup(x => x.selectUser('123'))
.returns(() => Observable.of(<UserDto>{}));
let result: boolean;
userGuard.canActivate(route).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeTruthy();
usersState.verify(x => x.selectUser('123'), Times.once());
});
it('should load user and return false when not found', () => {
usersState.setup(x => x.selectUser('123'))
.returns(() => Observable.of(null));
let result: boolean;
userGuard.canActivate(route).subscribe(x => {
result = x;
}).unsubscribe();
expect(result!).toBeFalsy();
router.verify(x => x.navigate(['/404']), Times.once());
});
});

38
src/Squidex/app/features/administration/guards/user-must-exist.guard.ts

@ -0,0 +1,38 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { allParams } from '@app/framework';
import { UsersState } from './../state/users.state';
@Injectable()
export class UserMustExistGuard implements CanActivate {
constructor(
private readonly usersState: UsersState,
private readonly router: Router
) {
}
public canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
const userId = allParams(route)['userId'];
const result =
this.usersState.selectUser(userId)
.do(dto => {
if (!dto) {
this.router.navigate(['/404']);
}
})
.map(u => u !== null);
return result;
}
}

30
src/Squidex/app/features/administration/module.ts

@ -9,16 +9,20 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {
ResolveUserGuard,
SqxFrameworkModule,
SqxSharedModule
} from 'shared';
SqxSharedModule,
SqxFrameworkModule
} from '@app/shared';
import {
AdministrationAreaComponent,
EventConsumersPageComponent,
EventConsumersService,
UnsetUserGuard,
UserMustExistGuard,
UserPageComponent,
UsersPageComponent
UsersPageComponent,
UsersService,
UsersState
} from './declarations';
const routes: Routes = [
@ -39,14 +43,13 @@ const routes: Routes = [
children: [
{
path: 'new',
component: UserPageComponent
component: UserPageComponent,
canActivate: [UnsetUserGuard]
},
{
path: ':userId',
component: UserPageComponent,
resolve: {
user: ResolveUserGuard
}
canActivate: [UserMustExistGuard]
}
]
}
@ -58,8 +61,8 @@ const routes: Routes = [
@NgModule({
imports: [
SqxFrameworkModule,
SqxSharedModule,
SqxFrameworkModule,
RouterModule.forChild(routes)
],
declarations: [
@ -67,6 +70,13 @@ const routes: Routes = [
EventConsumersPageComponent,
UserPageComponent,
UsersPageComponent
],
providers: [
EventConsumersService,
UnsetUserGuard,
UserMustExistGuard,
UsersService,
UsersState
]
})
export class SqxFeatureAdministrationModule { }

139
src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html

@ -1,89 +1,72 @@
<sqx-title message="Event Consumers"></sqx-title>
<sqx-panel theme="light" desiredWidth="50rem">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Event Consumers (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<ng-container title>
Consumers
</ng-container>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
</div>
<ng-container menu>
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Event Consumers (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<h3 class="panel-title">Event Consumers</h3>
</div>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
</ng-container>
<a class="panel-close" sqxParentLink isLazyLoaded="true">
<i class="icon-close"></i>
</a>
</div>
<ng-container content>
<table class="table table-items table-fixed">
<thead>
<tr>
<th class="cell-auto">
Name
</th>
<th class="cell-auto-right">
Position
</th>
<th class="cell-actions-lg">
Actions
</th>
</tr>
</thead>
<div class="panel-main">
<div class="panel-content panel-content-scroll">
<table class="table table-items table-fixed">
<thead>
<tr>
<th class="cell-auto">
Name
</th>
<th class="cell-auto-right">
Position
</th>
<th class="cell-actions-lg">
Actions
</th>
</tr>
</thead>
<tbody>
<ng-template ngFor let-eventConsumer [ngForOf]="eventConsumers">
<tr [class.faulted]="eventConsumer.error && eventConsumer.error.length > 0">
<td class="auto-auto">
<span class="truncate">
<i class="faulted-icon icon icon-bug" (click)="showError(eventConsumer)" [class.hidden]="!eventConsumer.error || eventConsumer.error.length === 0"></i>
<tbody>
<ng-template ngFor let-eventConsumer [ngForOf]="eventConsumers">
<tr [class.faulted]="eventConsumer.error && eventConsumer.error.length > 0">
<td class="auto-auto">
<span class="truncate">
<i class="faulted-icon icon icon-bug" (click)="showError(eventConsumer)" [class.hidden]="!eventConsumer.error || eventConsumer.error.length === 0"></i>
{{eventConsumer.name}}
</span>
</td>
<td class="cell-auto-right">
<span>{{eventConsumer.position}}</span>
</td>
<td class="cell-actions-lg">
<button class="btn btn-link" (click)="reset(eventConsumer)" *ngIf="!eventConsumer.isResetting" title="Reset Event Consumer">
<i class="icon icon-reset"></i>
</button>
<button class="btn btn-link" (click)="start(eventConsumer)" *ngIf="eventConsumer.isStopped" title="Start Event Consumer">
<i class="icon icon-play"></i>
</button>
<button class="btn btn-link" (click)="stop(eventConsumer)" *ngIf="!eventConsumer.isStopped" title="Stop Event Consumer">
<i class="icon icon-pause"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
</table>
</div>
</div>
{{eventConsumer.name}}
</span>
</td>
<td class="cell-auto-right">
<span>{{eventConsumer.position}}</span>
</td>
<td class="cell-actions-lg">
<button class="btn btn-link" (click)="reset(eventConsumer)" *ngIf="!eventConsumer.isResetting" title="Reset Event Consumer">
<i class="icon icon-reset"></i>
</button>
<button class="btn btn-link" (click)="start(eventConsumer)" *ngIf="eventConsumer.isStopped" title="Start Event Consumer">
<i class="icon icon-play"></i>
</button>
<button class="btn btn-link" (click)="stop(eventConsumer)" *ngIf="!eventConsumer.isStopped" title="Stop Event Consumer">
<i class="icon icon-pause"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
</table>
</ng-container>
</sqx-panel>
<div class="modal" *sqxModalView="eventConsumerErrorDialog;onRoot:true" @fade>
<div class="modal-backdrop"></div>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Error</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="eventConsumerErrorDialog.hide()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<sqx-modal-dialog *sqxModalView="eventConsumerErrorDialog;onRoot:true" (close)="eventConsumerErrorDialog.hide()">
<ng-container #title>
Error
</ng-container>
<div class="modal-body">
<textarea readonly class="form-control error-message">{{eventConsumerError}}</textarea>
</div>
</div>
</div>
</div>
<ng-container #content>
<textarea readonly class="form-control error-message">{{eventConsumerError}}</textarea>
</ng-container>
</sqx-modal-dialog>

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

@ -9,24 +9,17 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import {
AppContext,
EventConsumerDto,
EventConsumersService,
fadeAnimation,
DialogService,
ImmutableArray,
ModalView
} from 'shared';
} from '@app/shared';
import { EventConsumerDto, EventConsumersService } from './../../services/event-consumers.service';
@Component({
selector: 'sqx-event-consumers-page',
styleUrls: ['./event-consumers-page.component.scss'],
templateUrl: './event-consumers-page.component.html',
providers: [
AppContext
],
animations: [
fadeAnimation
]
templateUrl: './event-consumers-page.component.html'
})
export class EventConsumersPageComponent implements OnDestroy, OnInit {
private subscription: Subscription;
@ -35,7 +28,8 @@ export class EventConsumersPageComponent implements OnDestroy, OnInit {
public eventConsumerError = '';
public eventConsumers = ImmutableArray.empty<EventConsumerDto>();
constructor(public readonly ctx: AppContext,
constructor(
private readonly dialogs: DialogService,
private readonly eventConsumersService: EventConsumersService
) {
}
@ -59,11 +53,11 @@ export class EventConsumersPageComponent implements OnDestroy, OnInit {
this.eventConsumers = ImmutableArray.of(dtos);
if (showInfo) {
this.ctx.notifyInfo('Event Consumers reloaded.');
this.dialogs.notifyInfo('Event Consumers reloaded.');
}
}, error => {
if (showError) {
this.ctx.notifyError(error);
this.dialogs.notifyError(error);
}
});
}
@ -71,27 +65,27 @@ export class EventConsumersPageComponent implements OnDestroy, OnInit {
public start(consumer: EventConsumerDto) {
this.eventConsumersService.startEventConsumer(consumer.name)
.subscribe(() => {
this.eventConsumers = this.eventConsumers.replaceBy('name', consumer.start());
this.eventConsumers = this.eventConsumers.replaceBy('name', start(consumer));
}, error => {
this.ctx.notifyError(error);
this.dialogs.notifyError(error);
});
}
public stop(consumer: EventConsumerDto) {
this.eventConsumersService.stopEventConsumer(consumer.name)
.subscribe(() => {
this.eventConsumers = this.eventConsumers.replaceBy('name', consumer.stop());
this.eventConsumers = this.eventConsumers.replaceBy('name', stop(consumer));
}, error => {
this.ctx.notifyError(error);
this.dialogs.notifyError(error);
});
}
public reset(consumer: EventConsumerDto) {
this.eventConsumersService.resetEventConsumer(consumer.name)
.subscribe(() => {
this.eventConsumers = this.eventConsumers.replaceBy('name', consumer.reset());
this.eventConsumers = this.eventConsumers.replaceBy('name', reset(consumer));
}, error => {
this.ctx.notifyError(error);
this.dialogs.notifyError(error);
});
}
@ -101,3 +95,16 @@ export class EventConsumersPageComponent implements OnDestroy, OnInit {
}
}
function start(es: EventConsumerDto) {
return new EventConsumerDto(es.name, false, false, es.error, es.position);
}
function stop(es: EventConsumerDto) {
return new EventConsumerDto(es.name, true, false, es.error, es.position);
}
function reset(es: EventConsumerDto) {
return new EventConsumerDto(es.name, es.isStopped, true, es.error, es.position);
}

22
src/Squidex/app/features/administration/pages/messages.ts

@ -1,22 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { UserDto } from 'shared';
export class UserCreated {
constructor(
public readonly user: UserDto
) {
}
}
export class UserUpdated {
constructor(
public readonly user: UserDto
) {
}
}

95
src/Squidex/app/features/administration/pages/users/user-page.component.html

@ -1,73 +1,62 @@
<sqx-title message="User Management"></sqx-title>
<form [formGroup]="userForm" (ngSubmit)="save()">
<form [formGroup]="userForm.form" (ngSubmit)="save()">
<input style="display:none" type="password" name="foilautofill"/>
<sqx-panel desiredWidth="26rem">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<button type="submit" class="btn btn-primary" title="CTRL + S">
Save
</button>
</div>
<sqx-shortcut keys="ctrl+s" (trigger)="save()"></sqx-shortcut>
<h3 class="panel-title" *ngIf="isNewMode">
New User
</h3>
<h3 class="panel-title" *ngIf="!isNewMode">
Edit User
</h3>
</div>
<sqx-panel desiredWidth="26rem" isBlank="true">
<ng-container title>
<ng-container *ngIf="usersState.selectedUser | async; else noUser">
Edit User
</ng-container>
<ng-template #noUser>
New User
</ng-template>
</ng-container>
<ng-container menu>
<button type="submit" class="btn btn-primary" title="CTRL + S">
Save
</button>
<sqx-shortcut keys="ctrl+s" (trigger)="save()"></sqx-shortcut>
</ng-container>
<a class="panel-close" sqxParentLink>
<i class="icon-close"></i>
</a>
</div>
<ng-container content>
<sqx-form-error [error]="userForm.error | async"></sqx-form-error>
<div class="panel-main">
<div class="panel-content panel-content-blank">
<div *ngIf="userFormError">
<div class="form-alert form-alert-error" [innerHTML]="userFormError"></div>
</div>
<div class="form-group">
<label for="email">Email</label>
<div class="form-group">
<label for="email">Email</label>
<sqx-control-errors for="email" [submitted]="userForm.submitted | async"></sqx-control-errors>
<sqx-control-errors for="email" [submitted]="userFormSubmitted"></sqx-control-errors>
<input type="email" class="form-control" id="email" maxlength="100" formControlName="email" autocomplete="false" />
</div>
<div class="form-group">
<label for="displayName">Display Name</label>
<input type="email" class="form-control" id="email" maxlength="100" formControlName="email" autocomplete="false" />
</div>
<div class="form-group">
<label for="displayName">Display Name</label>
<sqx-control-errors for="displayName" [submitted]="userFormSubmitted"></sqx-control-errors>
<sqx-control-errors for="displayName" [submitted]="userForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control" id="displayName" maxlength="100" formControlName="displayName" autocomplete="false" />
</div>
<input type="text" class="form-control" id="displayName" maxlength="100" formControlName="displayName" autocomplete="false" />
</div>
<div class="form-group form-group-password" [class.hidden]="user.id === ctx.userId">
<div class="form-group">
<label for="password">Password</label>
<div class="form-group form-group-password" [class.hidden]="usersState.isCurrentUser | async">
<div class="form-group">
<label for="password">Password</label>
<sqx-control-errors for="password" [submitted]="userFormSubmitted"></sqx-control-errors>
<sqx-control-errors for="password" [submitted]="userForm.submitted | async"></sqx-control-errors>
<input type="password" class="form-control" id="password" maxlength="100" formControlName="password" autocomplete="false" />
</div>
<input type="password" class="form-control" id="password" maxlength="100" formControlName="password" autocomplete="false" />
</div>
<div class="form-group">
<label for="password">Confirm Password</label>
<div class="form-group">
<label for="password">Confirm Password</label>
<sqx-control-errors for="passwordConfirm" [submitted]="userFormSubmitted"></sqx-control-errors>
<sqx-control-errors for="passwordConfirm" [submitted]="userForm.submitted | async"></sqx-control-errors>
<input type="password" class="form-control" id="passwordConfirm" maxlength="100" formControlName="passwordConfirm" autocomplete="false" />
</div>
<input type="password" class="form-control" id="passwordConfirm" maxlength="100" formControlName="passwordConfirm" autocomplete="false" />
</div>
</div>
</div>
</ng-container>
</sqx-panel>
</form>
</form>

154
src/Squidex/app/features/administration/pages/users/user-page.component.ts

@ -5,147 +5,69 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import {
AppContext,
UserDto,
UserManagementService,
ValidatorsEx
} from 'shared';
import { UserCreated, UserUpdated } from './../messages';
import { UserDto } from './../../services/users.service';
import { UserForm, UsersState } from './../../state/users.state';
@Component({
selector: 'sqx-user-page',
styleUrls: ['./user-page.component.scss'],
templateUrl: './user-page.component.html',
providers: [
AppContext
]
templateUrl: './user-page.component.html'
})
export class UserPageComponent implements OnInit {
public user: UserDto;
public userFormSubmitted = false;
public userForm: FormGroup;
public userFormError = '';
export class UserPageComponent implements OnDestroy, OnInit {
private selectedUserSubscription: Subscription;
private user?: UserDto;
public isCurrentUser = false;
public isNewMode = false;
public userForm: UserForm;
constructor(public readonly ctx: AppContext,
private readonly formBuilder: FormBuilder,
private readonly router: Router,
private readonly userManagementService: UserManagementService
constructor(formBuilder: FormBuilder,
public readonly usersState: UsersState,
private readonly route: ActivatedRoute,
private readonly router: Router
) {
this.userForm = new UserForm(formBuilder);
}
public ngOnInit() {
this.ctx.route.data.map(d => d.user)
.subscribe((user: UserDto) => {
this.user = user;
public ngOnDestroy() {
this.selectedUserSubscription.unsubscribe();
}
this.setupAndPopulateForm();
});
public ngOnInit() {
this.selectedUserSubscription =
this.usersState.selectedUser
.subscribe(user => {
this.user = user;
this.userForm.load(user);
});
}
public save() {
this.userFormSubmitted = true;
if (this.userForm.valid) {
this.userForm.disable();
const requestDto = this.userForm.value;
const value = this.userForm.submit();
if (this.isNewMode) {
this.userManagementService.postUser(requestDto)
.subscribe(created => {
this.user =
new UserDto(
created.id,
requestDto.email,
requestDto.displayName,
false);
this.ctx.notifyInfo('User created successfully.');
this.emitUserCreated(this.user);
this.back();
if (value) {
if (this.user) {
this.usersState.updateUser(this.user, value)
.subscribe(user => {
this.userForm.submitCompleted();
}, error => {
this.resetUserForm(error.displayMessage);
this.userForm.submitFailed(error);
});
} else {
this.userManagementService.putUser(this.user.id, requestDto)
.subscribe(() => {
this.user =
this.user.update(
requestDto.email,
requestDto.displayMessage);
this.ctx.notifyInfo('User saved successfully.');
this.emitUserUpdated(this.user);
this.resetUserForm();
this.usersState.createUser(value)
.subscribe(user => {
this.back();
}, error => {
this.resetUserForm(error.displayMessage);
this.userForm.submitFailed(error);
});
}
}
}
private back() {
this.router.navigate(['../'], { relativeTo: this.ctx.route, replaceUrl: true });
}
private emitUserCreated(user: UserDto) {
this.ctx.bus.emit(new UserCreated(user));
}
private emitUserUpdated(user: UserDto) {
this.ctx.bus.emit(new UserUpdated(user));
}
private setupAndPopulateForm() {
const input = this.user || {};
this.isNewMode = !this.user;
this.userForm =
this.formBuilder.group({
email: [input['email'],
[
Validators.email,
Validators.required,
Validators.maxLength(100)
]],
displayName: [input['displayName'],
[
Validators.required,
Validators.maxLength(100)
]],
password: ['',
[
this.isNewMode ? Validators.required : Validators.nullValidator
]],
passwordConfirm: ['',
[
ValidatorsEx.match('password', 'Passwords must be the same.')
]]
});
this.isCurrentUser = this.user && this.user.id === this.ctx.userId;
this.resetUserForm();
}
private resetUserForm(message: string = '') {
this.userForm.enable();
this.userForm.controls['password'].reset();
this.userForm.controls['passwordConfirm'].reset();
this.userFormSubmitted = false;
this.userFormError = message;
this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true });
}
}

167
src/Squidex/app/features/administration/pages/users/users-page.component.html

@ -1,107 +1,90 @@
<sqx-title message="User Management"></sqx-title>
<sqx-panel desiredWidth="50rem">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Users (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<sqx-panel desiredWidth="50rem" contentClass="grid">
<ng-container title>
Users
</ng-container>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut>
<ng-container menu>
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Users (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<form class="form-inline" (ngSubmit)="search()">
<input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" />
</form>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+n" (trigger)="buttonNew.click()"></sqx-shortcut>
<button class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + M)">
<i class="icon-plus"></i> New
</button>
</div>
<h3 class="panel-title">Users</h3>
</div>
<form class="form-inline" (ngSubmit)="search()">
<input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" />
</form>
<a class="panel-close" sqxParentLink isLazyLoaded="true">
<i class="icon-close"></i>
</a>
</div>
<button class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + N)">
<i class="icon-plus"></i> New
</button>
</ng-container>
<div class="panel-main">
<div class="panel-content grid">
<div class="grid-header">
<ng-container content>
<div class="grid-header">
<table class="table table-items table-fixed">
<thead>
<tr>
<th class="cell-user">
&nbsp;
</th>
<th class="cell-auto">
Name
</th>
<th class="cell-auto">
Email
</th>
<th class="cell-actions">
Actions
</th>
</tr>
</thead>
</table>
</div>
<div class="grid-content">
<div sqxIgnoreScrollbar>
<table class="table table-items table-fixed">
<thead>
<tr>
<th class="cell-user">
&nbsp;
</th>
<th class="cell-auto">
Name
</th>
<th class="cell-auto">
Email
</th>
<th class="cell-actions">
Actions
</th>
</tr>
</thead>
</table>
</div>
<div class="grid-content">
<div sqxIgnoreScrollbar>
<table class="table table-items table-fixed">
<tbody>
<ng-template ngFor let-user [ngForOf]="usersItems">
<tr [routerLink]="user.id" routerLinkActive="active">
<td class="cell-user">
<img class="user-picture" [attr.title]="user.name" [attr.src]="user | sqxUserDtoPicture" />
</td>
<td class="cell-auto">
<span class="user-name table-cell">{{user.displayName}}</span>
</td>
<td class="cell-auto">
<span class="user-email table-cell">{{user.email}}</span>
</td>
<td class="cell-actions">
<span *ngIf="user.id !== ctx.userId">
<button class="btn btn-link" (click)="lock(user); $event.stopPropagation();" *ngIf="!user.isLocked" title="Lock User">
<i class="icon icon-unlocked"></i>
</button>
<button class="btn btn-link" (click)="unlock(user); $event.stopPropagation();" *ngIf="user.isLocked" title="Unlock User">
<i class="icon icon-lock"></i>
</button>
</span>
<button *ngIf="user.id === ctx.userId" class="btn btn-link invisible">
&nbsp;
<tbody>
<ng-template ngFor let-user [ngForOf]="usersState.users | async" [ngForTrackBy]="trackByUser">
<tr [routerLink]="user.id" routerLinkActive="active">
<td class="cell-user">
<img class="user-picture" [attr.title]="user.name" [attr.src]="user | sqxUserDtoPicture" />
</td>
<td class="cell-auto">
<span class="user-name table-cell">{{user.displayName}}</span>
</td>
<td class="cell-auto">
<span class="user-email table-cell">{{user.email}}</span>
</td>
<td class="cell-actions">
<ng-container *ngIf="user.id !== authState.user?.id">
<button class="btn btn-link" (click)="lock(user); $event.stopPropagation();" *ngIf="!user.isLocked" title="Lock User">
<i class="icon icon-unlocked"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
</table>
</div>
<button class="btn btn-link" (click)="unlock(user); $event.stopPropagation();" *ngIf="user.isLocked" title="Unlock User">
<i class="icon icon-lock"></i>
</button>
</ng-container>
<button *ngIf="user.id === authState.user?.id" class="btn btn-link invisible">
&nbsp;
</button>
</td>
</tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
</table>
</div>
</div>
<div class="grid-footer clearfix" *ngIf="usersPager.numberOfItems > 0">
<div class="float-right pagination">
<span class="pagination-text">{{usersPager.itemFirst}}-{{usersPager.itemLast}} of {{usersPager.numberOfItems}}</span>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!usersPager.canGoPrev" (click)="goPrev()">
<i class="icon-angle-left"></i>
</button>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!usersPager.canGoNext" (click)="goNext()">
<i class="icon-angle-right"></i>
</button>
</div>
</div>
<div class="grid-footer">
<sqx-pager [pager]="usersState.usersPager | async" (prev)="goPrev()" (next)="goNext()"></sqx-pager>
</div>
</div>
</ng-container>
</sqx-panel>
<router-outlet></router-outlet>

96
src/Squidex/app/features/administration/pages/users/users-page.component.ts

@ -5,113 +5,59 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Subscription } from 'rxjs';
import {
AppContext,
ImmutableArray,
Pager,
UserDto,
UserManagementService
} from 'shared';
import { AuthService } from '@app/shared';
import { UserCreated, UserUpdated } from './../messages';
import { UserDto } from './../../services/users.service';
import { UsersState } from './../../state/users.state';
@Component({
selector: 'sqx-users-page',
styleUrls: ['./users-page.component.scss'],
templateUrl: './users-page.component.html',
providers: [
AppContext
]
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UsersPageComponent implements OnDestroy, OnInit {
private userCreatedSubscription: Subscription;
private userUpdatedSubscription: Subscription;
public usersItems = ImmutableArray.empty<UserDto>();
public usersPager = new Pager(0);
export class UsersPageComponent implements OnInit {
public usersFilter = new FormControl();
public usersQuery = '';
constructor(public readonly ctx: AppContext,
private readonly userManagementService: UserManagementService
constructor(
public readonly authState: AuthService,
public readonly usersState: UsersState
) {
}
public ngOnDestroy() {
this.userCreatedSubscription.unsubscribe();
this.userUpdatedSubscription.unsubscribe();
}
public ngOnInit() {
this.userCreatedSubscription =
this.ctx.bus.of(UserCreated)
.subscribe(message => {
this.usersItems = this.usersItems.pushFront(message.user);
this.usersPager = this.usersPager.incrementCount();
});
this.userUpdatedSubscription =
this.ctx.bus.of(UserUpdated)
.subscribe(message => {
this.usersItems = this.usersItems.replaceBy('id', message.user);
});
this.load();
}
public search() {
this.usersPager = new Pager(0);
this.usersQuery = this.usersFilter.value;
this.load();
this.usersState.search(this.usersFilter.value).subscribe();
}
public load(showInfo = false) {
this.userManagementService.getUsers(this.usersPager.pageSize, this.usersPager.skip, this.usersQuery)
.subscribe(dtos => {
this.usersItems = ImmutableArray.of(dtos.items);
this.usersPager = this.usersPager.setCount(dtos.total);
if (showInfo) {
this.ctx.notifyInfo('Users reloaded.');
}
}, error => {
this.ctx.notifyError(error);
});
public load(notify = false) {
this.usersState.loadUsers(notify).subscribe();
}
public lock(user: UserDto) {
this.userManagementService.lockUser(user.id)
.subscribe(() => {
this.usersItems = this.usersItems.replaceBy('id', user.lock());
}, error => {
this.ctx.notifyError(error);
});
this.usersState.lockUser(user).subscribe();
}
public unlock(user: UserDto) {
this.userManagementService.unlockUser(user.id)
.subscribe(() => {
this.usersItems = this.usersItems.replaceBy('id', user.unlock());
}, error => {
this.ctx.notifyError(error);
});
this.usersState.unlockUser(user).subscribe();
}
public goNext() {
this.usersPager = this.usersPager.goNext();
this.load();
public goPrev() {
this.usersState.goPrev().subscribe();
}
public goPrev() {
this.usersPager = this.usersPager.goPrev();
public goNext() {
this.usersState.goNext().subscribe();
}
this.load();
public trackByUser(index: number, user: UserDto) {
return user.id;
}
}

29
src/Squidex/app/shared/services/event-consumers.service.spec.ts → src/Squidex/app/features/administration/services/event-consumers.service.spec.ts

@ -8,34 +8,9 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import {
ApiUrlConfig,
EventConsumerDto,
EventConsumersService
} from './../';
describe('EventConsumerDto', () => {
it('should update isStopped property when starting', () => {
const consumer_1 = new EventConsumerDto('consumer', true, false, 'error', 'position');
const consumer_2 = consumer_1.start();
expect(consumer_2.isStopped).toBeFalsy();
});
it('should update isStopped property when starting', () => {
const consumer_1 = new EventConsumerDto('consumer', false, false, 'error', 'position');
const consumer_2 = consumer_1.stop();
expect(consumer_2.isStopped).toBeTruthy();
});
it('should update isResetting property when resetting', () => {
const consumer_1 = new EventConsumerDto('consumer', false, false, 'error', 'position');
const consumer_2 = consumer_1.reset();
import { ApiUrlConfig } from '@app/framework';
expect(consumer_2.isResetting).toBeTruthy();
});
});
import { EventConsumerDto, EventConsumersService } from './event-consumers.service';
describe('EventConsumersService', () => {
beforeEach(() => {

16
src/Squidex/app/shared/services/event-consumers.service.ts → src/Squidex/app/features/administration/services/event-consumers.service.ts

@ -9,9 +9,9 @@ import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import 'framework/angular/http-extensions';
import '@app/framework/angular/http/http-extensions';
import { ApiUrlConfig, HTTP } from 'framework';
import { ApiUrlConfig, HTTP } from '@app/framework';
export class EventConsumerDto {
constructor(
@ -22,18 +22,6 @@ export class EventConsumerDto {
public readonly position: string
) {
}
public start(): EventConsumerDto {
return new EventConsumerDto(this.name, false, false, this.error, this.position);
}
public stop(): EventConsumerDto {
return new EventConsumerDto(this.name, true, false, this.error, this.position);
}
public reset(): EventConsumerDto {
return new EventConsumerDto(this.name, this.isStopped, true, this.error, this.position);
}
}
@Injectable()

202
src/Squidex/app/features/administration/services/users.service.spec.ts

@ -0,0 +1,202 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { ApiUrlConfig } from '@app/framework';
import {
CreateUserDto,
UpdateUserDto,
UserDto,
UsersService,
UsersDto
} from './users.service';
describe('UsersService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [
UsersService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }
]
});
});
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify();
}));
it('should make get request to get many users',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let users: UsersDto | null = null;
userManagementService.getUsers(20, 30).subscribe(result => {
users = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management?take=20&skip=30&query=');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
total: 100,
items: [
{
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
isLocked: true
},
{
id: '456',
email: 'mail2@domain.com',
displayName: 'User2',
isLocked: true
}
]
});
expect(users).toEqual(
new UsersDto(100, [
new UserDto('123', 'mail1@domain.com', 'User1', true),
new UserDto('456', 'mail2@domain.com', 'User2', true)
]));
}));
it('should make get request with query to get many users',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let users: UsersDto | null = null;
userManagementService.getUsers(20, 30, 'my-query').subscribe(result => {
users = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management?take=20&skip=30&query=my-query');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
total: 100,
items: [
{
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
isLocked: true
},
{
id: '456',
email: 'mail2@domain.com',
displayName: 'User2',
isLocked: true
}
]
});
expect(users).toEqual(
new UsersDto(100, [
new UserDto('123', 'mail1@domain.com', 'User1', true),
new UserDto('456', 'mail2@domain.com', 'User2', true)
]));
}));
it('should make get request to get single user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let user: UserDto | null = null;
userManagementService.getUser('123').subscribe(result => {
user = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management/123');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true
});
expect(user).toEqual(new UserDto('123', 'mail1@domain.com', 'User1', true));
}));
it('should make post request to create user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const dto = new CreateUserDto('mail@squidex.io', 'Squidex User', 'password');
let user: UserDto | null = null;
userManagementService.postUser(dto).subscribe(result => {
user = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({ id: '123', pictureUrl: 'path/to/image1' });
expect(user).toEqual(new UserDto('123', dto.email, dto.displayName, false));
}));
it('should make put request to update user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const dto = new UpdateUserDto('mail@squidex.io', 'Squidex User', 'password');
userManagementService.putUser('123', dto).subscribe();
const req = httpMock.expectOne('http://service/p/api/user-management/123');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
it('should make put request to lock user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
userManagementService.lockUser('123').subscribe();
const req = httpMock.expectOne('http://service/p/api/user-management/123/lock');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
it('should make put request to unlock user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
userManagementService.unlockUser('123').subscribe();
const req = httpMock.expectOne('http://service/p/api/user-management/123/unlock');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
});

134
src/Squidex/app/features/administration/services/users.service.ts

@ -0,0 +1,134 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import '@app/framework/angular/http/http-extensions';
import { ApiUrlConfig, HTTP } from '@app/framework';
export class UsersDto {
constructor(
public readonly total: number,
public readonly items: UserDto[]
) {
}
}
export class UserDto {
constructor(
public readonly id: string,
public readonly email: string,
public readonly displayName: string,
public readonly isLocked: boolean
) {
}
}
export class CreateUserDto {
constructor(
public readonly email: string,
public readonly displayName: string,
public readonly password: string
) {
}
}
export class UpdateUserDto {
constructor(
public readonly email: string,
public readonly displayName: string,
public readonly password?: string
) {
}
}
@Injectable()
export class UsersService {
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig
) {
}
public getUsers(take: number, skip: number, query?: string): Observable<UsersDto> {
const url = this.apiUrl.buildUrl(`api/user-management?take=${take}&skip=${skip}&query=${query || ''}`);
return HTTP.getVersioned<any>(this.http, url)
.map(response => {
const body = response.payload.body;
const items: any[] = body.items;
const users = items.map(item => {
return new UserDto(
item.id,
item.email,
item.displayName,
item.isLocked);
});
return new UsersDto(body.total, users);
})
.pretifyError('Failed to load users. Please reload.');
}
public getUser(id: string): Observable<UserDto> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}`);
return HTTP.getVersioned<any>(this.http, url)
.map(response => {
const body = response.payload.body;
return new UserDto(
body.id,
body.email,
body.displayName,
body.isLocked);
})
.pretifyError('Failed to load user. Please reload.');
}
public postUser(dto: CreateUserDto): Observable<UserDto> {
const url = this.apiUrl.buildUrl('api/user-management');
return HTTP.postVersioned<any>(this.http, url, dto)
.map(response => {
const body = response.payload.body;
return new UserDto(
body.id,
dto.email,
dto.displayName,
false);
})
.pretifyError('Failed to create user. Please reload.');
}
public putUser(id: string, dto: UpdateUserDto): Observable<any> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}`);
return HTTP.putVersioned(this.http, url, dto)
.pretifyError('Failed to update user. Please reload.');
}
public lockUser(id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}/lock`);
return HTTP.putVersioned(this.http, url, {})
.pretifyError('Failed to load users. Please retry.');
}
public unlockUser(id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}/unlock`);
return HTTP.putVersioned(this.http, url, {})
.pretifyError('Failed to load users. Please retry.');
}
}

211
src/Squidex/app/features/administration/state/users.state.spec.ts

@ -0,0 +1,211 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Observable } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq';
import { AuthService, DialogService } from '@app/shared';
import { UsersState } from './users.state';
import {
CreateUserDto,
UserDto,
UpdateUserDto,
UsersDto,
UsersService
} from './../services/users.service';
describe('UsersState', () => {
const oldUsers = [
new UserDto('id1', 'mail1@mail.de', 'name1', false),
new UserDto('id2', 'mail2@mail.de', 'name2', true)
];
const newUser = new UserDto('id3', 'mail3@mail.de', 'name3', false);
let authService: IMock<AuthService>;
let dialogs: IMock<DialogService>;
let usersService: IMock<UsersService>;
let usersState: UsersState;
beforeEach(() => {
authService = Mock.ofType<AuthService>();
authService.setup(x => x.user)
.returns(() => <any>{ id: 'id2' });
dialogs = Mock.ofType<DialogService>();
usersService = Mock.ofType<UsersService>();
usersService.setup(x => x.getUsers(10, 0, undefined))
.returns(() => Observable.of(new UsersDto(200, oldUsers)));
usersState = new UsersState(authService.object, dialogs.object, usersService.object);
usersState.loadUsers().subscribe();
});
it('should load users', () => {
expect(usersState.snapshot.users.values).toEqual(oldUsers);
expect(usersState.snapshot.usersPager.numberOfItems).toEqual(200);
usersService.verifyAll();
});
it('should replace selected user when reloading', () => {
usersState.selectUser('id1').subscribe();
const newUsers = [
new UserDto('id1', 'mail1@mail.de_new', 'name1_new', false),
new UserDto('id2', 'mail2@mail.de_new', 'name2_new', true)
];
usersService.setup(x => x.getUsers(10, 0, undefined))
.returns(() => Observable.of(new UsersDto(200, newUsers)));
usersState.loadUsers().subscribe();
expect(usersState.snapshot.selectedUser).toBe(newUsers[0]);
});
it('should raise notification on load when notify is true', () => {
usersState.loadUsers(true).subscribe();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
it('should mark as current user when selected user equals to profile', () => {
usersState.selectUser('id2').subscribe();
expect(usersState.snapshot.isCurrentUser).toBeTruthy();
});
it('should not load user when already loaded', () => {
let selectedUser: UserDto;
usersState.selectUser('id1').subscribe(x => {
selectedUser = x!;
});
expect(selectedUser!).toEqual(oldUsers[0]);
expect(usersState.snapshot.selectedUser).toBe(oldUsers[0]);
usersService.verify(x => x.getUser(It.isAnyString()), Times.never());
});
it('should load user when not loaded', () => {
usersService.setup(x => x.getUser('id3'))
.returns(() => Observable.of(newUser));
let selectedUser: UserDto;
usersState.selectUser('id3').subscribe(x => {
selectedUser = x!;
});
expect(selectedUser!).toEqual(newUser);
expect(usersState.snapshot.selectedUser).toBe(newUser);
});
it('should return null when unselecting user', () => {
let selectedUser: UserDto;
usersState.selectUser(null).subscribe(x => {
selectedUser = x!;
});
expect(selectedUser!).toBeNull();
expect(usersState.snapshot.selectedUser).toBeNull();
usersService.verify(x => x.getUser(It.isAnyString()), Times.never());
});
it('should return null when user to select is not found', () => {
usersService.setup(x => x.getUser('unknown'))
.returns(() => Observable.throw({}));
let selectedUser: UserDto;
usersState.selectUser('unknown').subscribe(x => {
selectedUser = x!;
}).unsubscribe();
expect(selectedUser!).toBeNull();
expect(usersState.snapshot.selectedUser).toBeNull();
});
it('should mark user as locked', () => {
usersService.setup(x => x.lockUser('id1'))
.returns(() => Observable.of({}));
usersState.selectUser('id1').subscribe();
usersState.lockUser(oldUsers[0]).subscribe();
expect(usersState.snapshot.users.at(0).isLocked).toBeTruthy();
expect(usersState.snapshot.selectedUser).toBe(usersState.snapshot.users.at(0));
});
it('should unmark user as locked', () => {
usersService.setup(x => x.unlockUser('id2'))
.returns(() => Observable.of({}));
usersState.selectUser('id2').subscribe();
usersState.unlockUser(oldUsers[1]).subscribe();
expect(usersState.snapshot.users.at(1).isLocked).toBeFalsy();
expect(usersState.snapshot.selectedUser).toBe(usersState.snapshot.users.at(1));
});
it('should update user on update', () => {
const request = new UpdateUserDto('new@mail.com', 'New');
usersService.setup(x => x.putUser('id1', request))
.returns(() => Observable.of({}));
usersState.selectUser('id1').subscribe();
usersState.updateUser(oldUsers[0], request).subscribe();
expect(usersState.snapshot.users.at(0).email).toEqual('new@mail.com');
expect(usersState.snapshot.users.at(0).displayName).toEqual('New');
expect(usersState.snapshot.selectedUser).toBe(usersState.snapshot.users.at(0));
});
it('should add user to state when created', () => {
const request = new CreateUserDto(newUser.email, newUser.displayName, 'password');
usersService.setup(x => x.postUser(request))
.returns(() => Observable.of(newUser));
usersState.createUser(request).subscribe();
expect(usersState.snapshot.users.at(0)).toBe(newUser);
expect(usersState.snapshot.usersPager.numberOfItems).toBe(201);
});
it('should load next page and prev page when paging', () => {
usersService.setup(x => x.getUsers(10, 10, undefined))
.returns(() => Observable.of(new UsersDto(200, [])));
usersState.goNext().subscribe();
usersState.goPrev().subscribe();
usersService.verify(x => x.getUsers(10, 10, undefined), Times.once());
usersService.verify(x => x.getUsers(10, 0, undefined), Times.exactly(2));
});
it('should load with query when searching', () => {
usersService.setup(x => x.getUsers(10, 0, 'my-query'))
.returns(() => Observable.of(new UsersDto(0, [])));
usersState.search('my-query').subscribe();
expect(usersState.snapshot.usersQuery).toEqual('my-query');
usersService.verify(x => x.getUsers(10, 0, 'my-query'), Times.once());
});
});

229
src/Squidex/app/features/administration/state/users.state.ts

@ -0,0 +1,229 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { FormBuilder, Validators, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import '@app/framework/utils/rxjs-extensions';
import {
AuthService,
DialogService,
ImmutableArray,
Pager,
Form,
State,
ValidatorsEx
} from '@app/shared';
import {
CreateUserDto,
UserDto,
UsersService,
UpdateUserDto
} from './../services/users.service';
export class UserForm extends Form<FormGroup> {
constructor(
formBuilder: FormBuilder
) {
super(formBuilder.group({
email: ['',
[
Validators.email,
Validators.required,
Validators.maxLength(100)
]
],
displayName: ['',
[
Validators.required,
Validators.maxLength(100)
]
],
password: ['',
[
Validators.nullValidator
]
],
passwordConfirm: ['',
[
ValidatorsEx.match('password', 'Passwords must be the same.')
]
]
}));
}
public load(user?: UserDto) {
if (user) {
this.form.controls['password'].setValidators(null);
} else {
this.form.controls['password'].setValidators(Validators.required);
}
super.load(user);
}
}
interface Snapshot {
isCurrentUser?: boolean;
users: ImmutableArray<UserDto>;
usersPager: Pager;
usersQuery?: string;
selectedUser?: UserDto;
}
@Injectable()
export class UsersState extends State<Snapshot> {
public users =
this.changes.map(x => x.users)
.distinctUntilChanged();
public usersPager =
this.changes.map(x => x.usersPager)
.distinctUntilChanged();
public selectedUser =
this.changes.map(x => x.selectedUser)
.distinctUntilChanged();
public isCurrentUser =
this.changes.map(x => x.isCurrentUser)
.distinctUntilChanged();
constructor(
private readonly authState: AuthService,
private readonly dialogs: DialogService,
private readonly usersService: UsersService
) {
super({ users: ImmutableArray.empty(), usersPager: new Pager(0) });
}
public selectUser(id: string | null): Observable<UserDto | null> {
return this.loadUser(id)
.do(selectedUser => {
const isCurrentUser = id === this.authState.user!.id;
this.next(s => ({ ...s, selectedUser, isCurrentUser }));
});
}
private loadUser(id: string | null) {
return !id ?
Observable.of(null) :
Observable.of(this.snapshot.users.find(x => x.id === id))
.switchMap(user => {
if (!user) {
return this.usersService.getUser(id).catch(() => Observable.of(null));
} else {
return Observable.of(user);
}
});
}
public loadUsers(notify = false): Observable<any> {
return this.usersService.getUsers(this.snapshot.usersPager.pageSize, this.snapshot.usersPager.skip, this.snapshot.usersQuery)
.do(dtos => {
if (notify) {
this.dialogs.notifyInfo('Users reloaded.');
}
this.next(s => {
const users = ImmutableArray.of(dtos.items);
const usersPager = s.usersPager.setCount(dtos.total);
let selectedUser = s.selectedUser;
if (selectedUser) {
const selectedFromResult = dtos.items.find(x => x.id === selectedUser!.id);
if (selectedFromResult) {
selectedUser = selectedFromResult;
}
}
return { ...s, users, usersPager, selectedUser };
});
})
.notify(this.dialogs);
}
public createUser(request: CreateUserDto): Observable<UserDto> {
return this.usersService.postUser(request)
.do(dto => {
this.next(s => {
const users = s.users.pushFront(dto);
const usersPager = s.usersPager.incrementCount();
return { ...s, users, usersPager };
});
});
}
public updateUser(user: UserDto, request: UpdateUserDto): Observable<any> {
return this.usersService.putUser(user.id, request)
.do(() => {
this.dialogs.notifyInfo('User saved successsfull');
this.replaceUser(update(user, request));
});
}
public lockUser(user: UserDto): Observable<any> {
return this.usersService.lockUser(user.id)
.do(() => {
this.replaceUser(setLocked(user, true));
})
.notify(this.dialogs);
}
public unlockUser(user: UserDto): Observable<any> {
return this.usersService.unlockUser(user.id)
.do(() => {
this.replaceUser(setLocked(user, false));
})
.notify(this.dialogs);
}
public search(query: string): Observable<any> {
this.next(s => ({ ...s, usersPager: new Pager(0), usersQuery: query }));
return this.loadUsers();
}
public goNext(): Observable<any> {
this.next(s => ({ ...s, usersPager: s.usersPager.goNext() }));
return this.loadUsers();
}
public goPrev(): Observable<any> {
this.next(s => ({ ...s, usersPager: s.usersPager.goPrev() }));
return this.loadUsers();
}
private replaceUser(user: UserDto) {
return this.next(s => {
const users = s.users.replaceBy('id', user);
const selectedUser = s.selectedUser && s.selectedUser.id === user.id ? user : s.selectedUser;
return { ...s, users, selectedUser };
});
}
}
const update = (user: UserDto, request: UpdateUserDto) =>
new UserDto(user.id, request.email, request.displayName, user.isLocked);
const setLocked = (user: UserDto, locked: boolean) =>
new UserDto(user.id, user.email, user.displayName, locked);

46
src/Squidex/app/features/api/api-area.component.html

@ -1,33 +1,25 @@
<sqx-title message="{app} | Settings" parameter1="app" [value1]="ctx.appName"></sqx-title>
<sqx-title message="{app} | Settings" parameter1="app" [value1]="appsState.appName"></sqx-title>
<sqx-panel theme="dark" desiredWidth="12rem">
<div class="panel-header">
<div class="panel-title-row">
<h3 class="panel-title">API</h3>
</div>
<ng-container title>
API
</ng-container>
<a class="panel-close" sqxParentLink isLazyLoaded="true">
<i class="icon-close"></i>
</a>
</div>
<div class="panel-main">
<div class="panel-content">
<ul class="nav nav-panel nav-dark flex-column">
<li class="nav-item">
<a class="nav-link" routerLink="graphql" routerLinkActive="active">
GraphQL
<i class="icon-angle-right"></i>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/api/content/{{ctx.appName}}/docs" target="_blank">
Swagger
</a>
</li>
</ul>
</div>
</div>
<ng-container content>
<ul class="nav nav-panel nav-dark flex-column">
<li class="nav-item">
<a class="nav-link" routerLink="graphql" routerLinkActive="active">
GraphQL
<i class="icon-angle-right"></i>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/api/content/{{appsState.appName}}/docs" target="_blank">
Swagger
</a>
</li>
</ul>
</ng-container>
</sqx-panel>
<router-outlet></router-outlet>

9
src/Squidex/app/features/api/api-area.component.ts

@ -7,19 +7,16 @@
import { Component } from '@angular/core';
import { AppContext } from 'shared';
import { AppsState } from '@app/shared';
@Component({
selector: 'sqx-api-area',
styleUrls: ['./api-area.component.scss'],
templateUrl: './api-area.component.html',
providers: [
AppContext
]
templateUrl: './api-area.component.html'
})
export class ApiAreaComponent {
constructor(
public readonly ctx: AppContext
public readonly appsState: AppsState
) {
}
}

2
src/Squidex/app/features/api/module.ts

@ -12,7 +12,7 @@ import { DndModule } from 'ng2-dnd';
import {
SqxFrameworkModule,
SqxSharedModule
} from 'shared';
} from '@app/shared';
import {
ApiAreaComponent,

6
src/Squidex/app/features/api/pages/graphql/graphql-page.component.html

@ -1,5 +1,5 @@
<sqx-title message="{app} | API | GraphQL" parameter1="app" [value1]="ctx.appName"></sqx-title>
<sqx-title message="{app} | API | GraphQL" parameter1="app" [value1]="appsState.appName"></sqx-title>
<sqx-panel desiredWidth="*">
<div #graphiQLContainer></div>
<sqx-panel desiredWidth="*" isFullSize="true">
<div inner #graphiQLContainer></div>
</sqx-panel>

10
src/Squidex/app/features/api/pages/graphql/graphql-page.component.scss

@ -4,11 +4,13 @@
@import '~graphiql/graphiql';
.graphiql-container {
@include absolute(0, 0, 0, 0);
}
& {
@include absolute(0, 0, 0, 0);
}
.graphiql-container > * {
box-sizing: content-box;
& > * {
box-sizing: content-box;
}
// sass-lint:disable class-name-format
& .editorWrap {

12
src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts

@ -16,25 +16,23 @@ const GraphiQL = require('graphiql');
/* tslint:disable:use-view-encapsulation */
import {
AppContext,
AppsState,
GraphQlService,
LocalStoreService
} from 'shared';
} from '@app/shared';
@Component({
selector: 'sqx-graphql-page',
styleUrls: ['./graphql-page.component.scss'],
templateUrl: './graphql-page.component.html',
providers: [
AppContext
],
encapsulation: ViewEncapsulation.None
})
export class GraphQLPageComponent implements OnInit {
@ViewChild('graphiQLContainer')
public graphiQLContainer: ElementRef;
constructor(public readonly ctx: AppContext,
constructor(
public readonly appsState: AppsState,
private readonly graphQlService: GraphQlService,
private readonly localStoreService: LocalStoreService
) {
@ -56,7 +54,7 @@ export class GraphQLPageComponent implements OnInit {
}
private request(params: any) {
return this.graphQlService.query(this.ctx.appName, params).catch(response => Observable.of(response.error)).toPromise();
return this.graphQlService.query(this.appsState.appName, params).catch(response => Observable.of(response.error)).toPromise();
}
}

2
src/Squidex/app/features/apps/module.ts

@ -8,7 +8,7 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SqxFrameworkModule, SqxSharedModule } from 'shared';
import { SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import {
AppsPageComponent,

38
src/Squidex/app/features/apps/pages/apps-page.component.html

@ -1,7 +1,7 @@
<sqx-title message="Apps"></sqx-title>
<div class="apps-section">
<h1 class="apps-title">Hi {{ctx.user.displayName}}</h1>
<h1 class="apps-title">Hi {{authState.user?.displayName}}</h1>
<div class="subtext">
Welcome to Squidex.
@ -49,7 +49,9 @@
<div class="card-text">
<div>Start with our ready to use blog.</div>
<div>Sample Code: <a href="https://github.com/Squidex/squidex-samples/tree/master/csharp/Sample.Blog" (click)="$event.stopPropagation()" target="_blank">ASP.NET Core</a></div>
<div>
Sample Code: <a href="https://github.com/Squidex/squidex-samples/tree/master/csharp/Sample.Blog" (click)="$event.stopPropagation()" target="_blank">C#</a>
</div>
</div>
</div>
</div>
@ -64,34 +66,18 @@
<div class="card-text">
<div>Create your profile page.</div>
<div>Sample Code: <a href="https://github.com/Squidex/squidex-samples/tree/master/csharp/Sample.Profile" (click)="$event.stopPropagation()" target="_blank">ASP.NET Core</a></div>
<div>
Sample Code: <a href="https://github.com/Squidex/squidex-samples/tree/master/csharp/Sample.Profile" (click)="$event.stopPropagation()" target="_blank">C#</a>
</div>
</div>
</div>
</div>
</div>
<div class="modal" *sqxModalView="addAppDialog;onRoot:true" @fade>
<div class="modal-backdrop"></div>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" *ngIf="template">Create {{template}} Sample</h4>
<h4 class="modal-title" *ngIf="!template">Create App</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="addAppDialog.hide()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<sqx-app-form
[template]="template"
(created)="addAppDialog.hide()"
(cancelled)="addAppDialog.hide()">
</sqx-app-form>
</div>
</div>
</div>
</div>
<ng-container *sqxModalView="addAppDialog;onRoot:true">
<sqx-app-form [template]="appTemplate"
(completed)="addAppDialog.hide()">
</sqx-app-form>
</ng-container>
<sqx-onboarding-dialog [modalView]="onboardingModal"></sqx-onboarding-dialog>

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

@ -9,38 +9,32 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import {
AppContext,
AppDto,
AppsStoreService,
fadeAnimation,
AppsState,
AuthService,
ImmutableArray,
ModalView,
OnboardingService
} from 'shared';
} from '@app/shared';
@Component({
selector: 'sqx-apps-page',
styleUrls: ['./apps-page.component.scss'],
templateUrl: './apps-page.component.html',
providers: [
AppContext
],
animations: [
fadeAnimation
]
templateUrl: './apps-page.component.html'
})
export class AppsPageComponent implements OnDestroy, OnInit {
private appsSubscription: Subscription;
public addAppDialog = new ModalView();
public apps: AppDto[];
public template = '';
public apps: ImmutableArray<AppDto>;
public appTemplate = '';
public onboardingModal = new ModalView();
constructor(
public readonly ctx: AppContext,
private readonly appsStore: AppsStoreService,
public readonly appsState: AppsState,
public readonly authState: AuthService,
private readonly onboardingService: OnboardingService
) {
}
@ -51,7 +45,7 @@ export class AppsPageComponent implements OnDestroy, OnInit {
public ngOnInit() {
this.appsSubscription =
this.appsStore.apps
this.appsState.apps
.subscribe(apps => {
if (apps.length === 0 && this.onboardingService.shouldShow('dialog')) {
this.onboardingService.disable('dialog');
@ -63,7 +57,7 @@ export class AppsPageComponent implements OnDestroy, OnInit {
}
public createNewApp(template: string) {
this.template = template;
this.appTemplate = template;
this.addAppDialog.show();
}

13
src/Squidex/app/features/apps/pages/onboarding-dialog.component.html

@ -1,7 +1,6 @@
<div class="modal" *sqxModalView="modalView;onRoot:true;closeAuto:false" @fade>
<div class="modal-backdrop"></div>
<div class="modal-dialog">
<div class="modal-content">
<ng-container *sqxModalView="modalView;onRoot:true;closeAuto:false">
<sqx-modal-dialog showHeader="false">
<ng-container content>
<a class="header-right modal-close" (click)="modalView.hide()">Skip Tour</a>
<div class="onboarding-step" *ngIf="step === 0">
@ -145,6 +144,6 @@
</div>
</div>
</div>
</div>
</div>
</div>
</ng-container>
</sqx-modal-dialog>
</ng-container>

2
src/Squidex/app/features/apps/pages/onboarding-dialog.component.ts

@ -11,7 +11,7 @@ import {
fadeAnimation,
ModalView,
slideAnimation
} from 'framework';
} from '@app/framework';
@Component({
selector: 'sqx-onboarding-dialog',

4
src/Squidex/app/features/assets/module.ts

@ -7,9 +7,8 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DndModule } from 'ng2-dnd';
import { SqxFrameworkModule, SqxSharedModule } from 'shared';
import { SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import {
AssetsPageComponent
@ -26,7 +25,6 @@ const routes: Routes = [
imports: [
SqxFrameworkModule,
SqxSharedModule,
DndModule,
RouterModule.forChild(routes)
],
declarations: [

92
src/Squidex/app/features/assets/pages/assets-page.component.html

@ -1,70 +1,24 @@
<sqx-title message="{app} | Assets" parameter1="app" [value1]="ctx.appName"></sqx-title>
<sqx-panel desiredWidth="60rem">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<form class="form-inline" (ngSubmit)="search()">
<input class="form-control" #inputFind [formControl]="assetsFilter" placeholder="Search for assets" />
</form>
</div>
<h3 class="panel-title">Assets</h3>
</div>
<a class="panel-close" sqxParentLink isLazyLoaded="true">
<i class="icon-close"></i>
</a>
</div>
<div class="panel-main">
<div class="panel-content panel-content-scroll">
<div class="file-drop" (sqxFileDrop)="addFiles($event)">
<h3 class="file-drop-header">Drop files here to upload</h3>
<div class="file-drop-or">or</div>
<div class="file-drop-button">
<span class="btn btn-success" (click)="fileInput.click()">
<span>Select File(s)</span>
<input class="file-drop-button-input" type="file" (change)="addFiles($event.target.files)" #fileInput multiple />
</span>
</div>
<div class="file-drop-info">Drop file on existing item to replace the asset with a newer version.</div>
</div>
<div class="row">
<sqx-asset class="col-3" *ngFor="let file of newFiles" [initFile]="file"
(failed)="onAssetFailed(file)"
(loaded)="onAssetLoaded(file, $event)">
</sqx-asset>
<sqx-asset class="col-3" *ngFor="let asset of assetsItems" [asset]="asset"
(deleting)="onAssetDeleting($event)"
(updated)="onAssetUpdated($event)">
</sqx-asset>
</div>
<div class="clearfix" *ngIf="assetsPager.numberOfItems > 0">
<div class="float-right pagination">
<span class="pagination-text">{{assetsPager.itemFirst}}-{{assetsPager.itemLast}} of {{assetsPager.numberOfItems}}</span>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!assetsPager.canGoPrev" (click)="goPrev()">
<i class="icon-angle-left"></i>
</button>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!assetsPager.canGoNext" (click)="goNext()">
<i class="icon-angle-right"></i>
</button>
</div>
</div>
</div>
</div>
<sqx-title message="{app} | Assets" parameter1="app" [value1]="appsState.appName"></sqx-title>
<sqx-panel desiredWidth="*">
<ng-container title>
Assets
</ng-container>
<ng-container menu>
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<form class="form-inline" (ngSubmit)="search()">
<input class="form-control" #inputFind [formControl]="assetsFilter" placeholder="Search for assets" />
</form>
</ng-container>
<ng-container content>
<sqx-assets-list [state]="assetsState"></sqx-assets-list>
</ng-container>
</sqx-panel>

50
src/Squidex/app/features/assets/pages/assets-page.component.scss

@ -1,50 +1,2 @@
@import '_vars';
@import '_mixins';
.file-drop {
& {
@include transition(border-color .4s ease);
border: 2px dashed $color-border;
background: transparent;
padding: 1rem;
text-align: center;
margin-bottom: 1rem;
margin-right: 0;
}
&.drag {
border-color: darken($color-border, 10%);
border-style: dashed;
cursor: copy;
}
&-button-input {
@include hidden;
}
&-button {
margin: .5rem 0;
}
&-or {
font-size: .8rem;
}
&-info {
color: darken($color-border, 30%);
}
}
.btn {
cursor: default;
}
.row {
margin-left: -8px;
margin-right: -8px;
}
.col-3 {
padding-left: 8px;
padding-right: 8px;
}
@import '_mixins';

102
src/Squidex/app/features/assets/pages/assets-page.component.ts

@ -7,120 +7,44 @@
// tslint:disable:prefer-for-of
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Subscription } from 'rxjs';
import {
AppContext,
AssetDto,
AssetsService,
AssetUpdated,
ImmutableArray,
Pager
} from 'shared';
import { AppsState, AssetsState } from '@app/shared';
@Component({
selector: 'sqx-assets-page',
styleUrls: ['./assets-page.component.scss'],
templateUrl: './assets-page.component.html',
providers: [
AppContext
]
templateUrl: './assets-page.component.html'
})
export class AssetsPageComponent implements OnDestroy, OnInit {
private assetUpdatedSubscription: Subscription;
public newFiles = ImmutableArray.empty<File>();
public assetsItems = ImmutableArray.empty<AssetDto>();
public assetsPager = new Pager(0, 0, 12);
export class AssetsPageComponent implements OnInit {
public assetsFilter = new FormControl();
public assertQuery = '';
constructor(public readonly ctx: AppContext,
private readonly assetsService: AssetsService
constructor(
public readonly appsState: AppsState,
public readonly assetsState: AssetsState
) {
}
public ngOnDestroy() {
this.assetUpdatedSubscription.unsubscribe();
}
public ngOnInit() {
this.assetUpdatedSubscription =
this.ctx.bus.of(AssetUpdated)
.subscribe(event => {
if (event.sender !== this) {
this.assetsItems = this.assetsItems.replaceBy('id', event.assetDto);
}
});
this.load();
}
public search() {
this.assetsPager = new Pager(0, 0, 12);
this.assertQuery = this.assetsFilter.value;
this.load();
}
public load(showInfo = false) {
this.assetsService.getAssets(this.ctx.appName, this.assetsPager.pageSize, this.assetsPager.skip, this.assertQuery)
.subscribe(dtos => {
this.assetsItems = ImmutableArray.of(dtos.items);
this.assetsPager = this.assetsPager.setCount(dtos.total);
if (showInfo) {
this.ctx.notifyInfo('Assets reloaded.');
}
}, error => {
this.ctx.notifyError(error);
});
}
public onAssetDeleting(asset: AssetDto) {
this.assetsService.deleteAsset(this.ctx.appName, asset.id, asset.version)
.subscribe(dto => {
this.assetsItems = this.assetsItems.filter(x => x.id !== asset.id);
this.assetsPager = this.assetsPager.decrementCount();
}, error => {
this.ctx.notifyError(error);
});
}
public onAssetLoaded(file: File, asset: AssetDto) {
this.newFiles = this.newFiles.remove(file);
public load(notify = false) {
this.assetsState.loadAssets(notify).subscribe();
this.assetsItems = this.assetsItems.pushFront(asset);
this.assetsPager = this.assetsPager.incrementCount();
}
public onAssetUpdated(asset: AssetDto) {
this.ctx.bus.emit(new AssetUpdated(asset, this));
}
public onAssetFailed(file: File) {
this.newFiles = this.newFiles.remove(file);
public search() {
this.assetsState.search(this.assetsFilter.value).subscribe();
}
public goNext() {
this.assetsPager = this.assetsPager.goNext();
this.load();
this.assetsState.goNext().subscribe();
}
public goPrev() {
this.assetsPager = this.assetsPager.goPrev();
this.load();
}
public addFiles(files: FileList) {
for (let i = 0; i < files.length; i++) {
this.newFiles = this.newFiles.pushFront(files[i]);
}
this.assetsState.goPrev().subscribe();
}
}

10
src/Squidex/app/features/content/module.ts

@ -16,7 +16,7 @@ import {
ResolvePublishedSchemaGuard,
SqxFrameworkModule,
SqxSharedModule
} from 'shared';
} from '@app/shared';
import {
AssetsEditorComponent,
@ -50,10 +50,6 @@ const routes: Routes = [
component: ContentPageComponent,
canDeactivate: [CanDeactivateGuard],
children: [
{
path: 'assets',
loadChildren: './../assets/module#SqxFeatureAssetsModule'
},
{
path: 'references/:schemaName/:language',
component: ContentsPageComponent,
@ -90,10 +86,6 @@ const routes: Routes = [
resolve: {
schema: ResolvePublishedSchemaGuard
}
},
{
path: 'assets',
loadChildren: './../assets/module#SqxFeatureAssetsModule'
}
]
}

8
src/Squidex/app/features/content/pages/content/content-field.component.html

@ -5,7 +5,7 @@
<span class="field-disabled" *ngIf="field.isDisabled">Disabled</span>
<div *ngIf="field.isLocalizable && languages.length > 1">
<ng-container *ngIf="field.isLocalizable && languages.length > 1">
<div class="languages-buttons" #buttonLanguages>
<sqx-language-selector size="sm" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages"></sqx-language-selector>
</div>
@ -13,7 +13,7 @@
<sqx-onboarding-tooltip id="languages" [for]="buttonLanguages" position="topRight" after="120000">
Please remember to check all languages when you see validation errors.
</sqx-onboarding-tooltip>
</div>
</ng-container>
<sqx-control-errors [for]="selectedFormControl" [fieldName]="field.displayName" [submitted]="contentFormSubmitted"></sqx-control-errors>
@ -54,10 +54,10 @@
<textarea class="form-control" [id]="selectedFormName"[formControl]="selectedFormControl" rows="5" [placeholder]="field.displayPlaceholder"></textarea>
</div>
<div *ngSwitchCase="'RichText'">
<sqx-rich-editor [formControl]="selectedFormControl" (assetPluginClicked)="assetPluginClicked()"></sqx-rich-editor>
<sqx-rich-editor [formControl]="selectedFormControl"></sqx-rich-editor>
</div>
<div *ngSwitchCase="'Markdown'">
<sqx-markdown-editor [formControl]="selectedFormControl" (assetPluginClicked)="assetPluginClicked()"></sqx-markdown-editor>
<sqx-markdown-editor [formControl]="selectedFormControl"></sqx-markdown-editor>
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="selectedFormControl">

13
src/Squidex/app/features/content/pages/content/content-field.component.ts

@ -7,13 +7,12 @@
import { Component, Input, OnInit } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import {
AppLanguageDto,
FieldDto,
fieldInvariant
} from 'shared';
} from '@app/shared';
@Component({
selector: 'sqx-content-field',
@ -36,12 +35,6 @@ export class ContentFieldComponent implements OnInit {
public selectedFormControl: AbstractControl;
public selectedLanguage: AppLanguageDto;
constructor(
private readonly router: Router,
private readonly route: ActivatedRoute
) {
}
public ngOnInit() {
const masterLanguage = this.languages[0];
@ -60,9 +53,5 @@ export class ContentFieldComponent implements OnInit {
this.selectedFormControl = this.fieldForm.controls[language.iso2Code];
this.selectedLanguage = language;
}
public assetPluginClicked() {
this.router.navigate(['assets'], { relativeTo: this.route });
}
}

40
src/Squidex/app/features/content/pages/content/content-history.component.html

@ -1,29 +1,21 @@
<sqx-panel desiredWidth="16rem">
<div class="panel-header">
<div class="panel-title-row">
<h3 class="panel-title">Activity</h3>
</div>
<a class="panel-close" sqxParentLink>
<i class="icon-close"></i>
</a>
</div>
<sqx-panel desiredWidth="16rem" isBlank="true">
<ng-container title>
Activity
</ng-container>
<div class="panel-main">
<div class="panel-content panel-content-blank">
<div *ngFor="let event of events | async" class="event">
<div class="event-left">
<img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:null" [attr.src]="event.actor | sqxUserPictureRef" />
<ng-container content>
<div *ngFor="let event of events | async" class="event">
<div class="event-left">
<img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:null" [attr.src]="event.actor | sqxUserPictureRef" />
</div>
<div class="event-main">
<div class="event-message">
<span class="event-actor user-ref">{{event.actor | sqxUserNameRef:null}}</span> <span [innerHTML]="format(event.message) | async"></span>
</div>
<div class="event-main">
<div class="event-message">
<span class="event-actor user-ref">{{event.actor | sqxUserNameRef:null}}</span> <span [innerHTML]="format(event.message) | async"></span>
</div>
<div class="event-created">{{event.created | sqxFromNow}}</div>
<div class="event-created">{{event.created | sqxFromNow}}</div>
<a class="event-load" (click)="loadVersion(event.version)">Load this Version</a>
</div>
</div>
<a class="event-load" (click)="loadVersion(event.version)">Load this Version</a>
</div>
</div>
</div>
</ng-container>
</sqx-panel>

2
src/Squidex/app/features/content/pages/content/content-history.component.ts

@ -16,7 +16,7 @@ import {
HistoryEventDto,
HistoryService,
UsersProviderService
} from 'shared';
} from '@app/shared';
import { ContentVersionSelected } from './../messages';

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

@ -1,70 +1,60 @@
<sqx-title message="{app} | {schema} | Content" parameter1="app" parameter2="schema" [value1]="ctx.appName" [value2]="schema?.displayName"></sqx-title>
<form [formGroup]="contentForm" (ngSubmit)="saveAndPublish()">
<sqx-panel desiredWidth="53rem">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right" *ngIf="!content || content.status !== 'Archived'">
<span *ngIf="isNewMode">
<button type="button" class="btn btn-secondary" (click)="saveAsDraft()" title="CTRL + S">
Save as Draft
</button>
<sqx-panel desiredWidth="53rem" showSidebar="true">
<ng-container title>
<ng-container *ngIf="isNewMode">
New Content
</ng-container>
<ng-container *ngIf="!isNewMode && content.status !== 'Archived'">
Edit Content
</ng-container>
<ng-container *ngIf="!isNewMode && content.status === 'Archived'">
Show Content
</ng-container>
</ng-container>
<button type="submit" class="btn btn-primary">
Save and Publish
</button>
</span>
<span *ngIf="!isNewMode">
<button type="submit" class="btn btn-primary" title="CTRL + S">
Save
</button>
</span>
<sqx-shortcut keys="ctrl+s" (trigger)="saveAndPublish()"></sqx-shortcut>
</div>
<h3 class="panel-title" *ngIf="isNewMode">
New Content
</h3>
<h3 class="panel-title" *ngIf="!isNewMode && content.status !== 'Archived'">
Edit Content
</h3>
<h3 class="panel-title" *ngIf="!isNewMode && content.status === 'Archived'">
Show Content
</h3>
</div>
<ng-container menu>
<ng-container *ngIf="isNewMode; else notNew">
<button type="button" class="btn btn-secondary" (click)="saveAsDraft()" title="CTRL + S">
Save as Draft
</button>
<a class="panel-close" sqxParentLink>
<i class="icon-close"></i>
</a>
</div>
<button type="submit" class="btn btn-primary">
Save and Publish
</button>
</ng-container>
<ng-template #notNew>
<button type="submit" class="btn btn-primary" title="CTRL + S">
Save
</button>
</ng-template>
<div class="panel-main">
<div class="panel-content panel-content-scroll">
<div class="panel-alert panel-alert-danger" *ngIf="contentOld">
<div class="float-right">
<a class="force" (click)="showLatest()">View latest</a>
</div>
Viewing <strong>{{content.lastModifiedBy | sqxUserNameRef:null}}'s</strong> changes of {{content.lastModified | sqxShortDate}}.
</div>
<sqx-shortcut keys="ctrl+s" (trigger)="saveAndPublish()"></sqx-shortcut>
</ng-container>
<div *ngFor="let field of schema.fields">
<sqx-content-field [field]="field" [fieldForm]="contentForm.controls[field.name]" [languages]="languages" [contentFormSubmitted]="contentFormSubmitted"></sqx-content-field>
<ng-container content>
<div class="panel-alert panel-alert-danger" *ngIf="contentOld">
<div class="float-right">
<a class="force" (click)="showLatest()">View latest</a>
</div>
Viewing <strong>{{content.lastModifiedBy | sqxUserNameRef:null}}'s</strong> changes of {{content.lastModified | sqxShortDate}}.
</div>
<div class="panel-sidebar">
<a class="panel-link" routerLink="history" routerLinkActive="active" #linkHistory *ngIf="!isNewMode">
<i class="icon-time"></i>
</a>
<a class="panel-link" routerLink="assets" routerLinkActive="active">
<i class="icon-media"></i>
</a>
<sqx-onboarding-tooltip id="history" [for]="linkHistory" position="leftTop" after="120000" *ngIf="!isNewMode">
The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time.
</sqx-onboarding-tooltip>
<div *ngFor="let field of schema.fields">
<sqx-content-field [field]="field" [fieldForm]="contentForm.controls[field.name]" [languages]="languages" [contentFormSubmitted]="contentFormSubmitted"></sqx-content-field>
</div>
</div>
</ng-container>
<ng-container sidebar>
<a class="panel-link" routerLink="history" routerLinkActive="active" #linkHistory *ngIf="!isNewMode">
<i class="icon-time"></i>
</a>
<sqx-onboarding-tooltip id="history" [for]="linkHistory" position="leftTop" after="120000" *ngIf="!isNewMode">
The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time.
</sqx-onboarding-tooltip>
</ng-container>
</sqx-panel>
</form>

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

@ -28,7 +28,7 @@ import {
fieldInvariant,
SchemaDetailsDto,
Version
} from 'shared';
} from '@app/shared';
@Component({
selector: 'sqx-content-page',

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

@ -1,208 +1,184 @@
<sqx-title message="{app} | {schema} | Contents" parameter1="app" parameter2="schema" [value1]="ctx.appName" [value2]="schema?.displayName"></sqx-title>
<sqx-panel [desiredWidth]="isReadOnly ? '40rem' : '60rem'">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Contents (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+g" (trigger)="newButton.click()" *ngIf="!isReadOnly"></sqx-shortcut>
<form class="form-inline" (ngSubmit)="search()">
<input class="form-control form-control-expandable" #inputFind [formControl]="contentsFilter" placeholder="Search for content" />
<a class="expand-search" (click)="searchModal.toggle()" #archive>
<i class="icon-caret-down"></i>
</a>
</form>
<sqx-onboarding-tooltip id="contentArchive" [for]="archive" position="bottomRight" after="60000">
Click this icon to show the advanced search menu and to show the archive!
</sqx-onboarding-tooltip>
<sqx-onboarding-tooltip id="contentFind" [for]="inputFind" position="bottomRight" after="120000">
Search for content using full text search over all fields and languages!
</sqx-onboarding-tooltip>
<div class="dropdown-menu" *sqxModalView="searchModal" [sqxModalTarget]="inputFind">
<sqx-search-form
[canArchive]="!isReadOnly"
(queryChanged)="contentsFilter.setValue($event, { emitEvent: false })"
[query]="contentsFilter.value"
(archivedChanged)="updateArchive($event)"
[archived]="isArchive">
</sqx-search-form>
</div>
<span *ngIf="!isReadOnly && languages.length > 1">
<sqx-language-selector class="languages-buttons" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages"></sqx-language-selector>
</span>
<button *ngIf="!isReadOnly" class="btn btn-success" #newButton routerLink="new" title="New Content (CTRL + SHIFT + G)">
<i class="icon-plus"></i> New
</button>
</div>
<sqx-panel [desiredWidth]="isReadOnly ? '40rem' : '60rem'" contentClass="grid">
<ng-container title>
<ng-container *ngIf="!isReadOnly && !isArchive">
Contents
</ng-container>
<ng-container *ngIf="isArchive">
Archive
</ng-container>
<ng-container *ngIf="isReadOnly">
References
</ng-container>
</ng-container>
<ng-container menu>
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Contents (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+g" (trigger)="newButton.click()" *ngIf="!isReadOnly"></sqx-shortcut>
<form class="form-inline" (ngSubmit)="search()">
<input class="form-control form-control-expandable" #inputFind [formControl]="contentsFilter" placeholder="Search for content" />
<a class="expand-search" (click)="searchModal.toggle()" #archive>
<i class="icon-caret-down"></i>
</a>
</form>
<sqx-onboarding-tooltip id="contentArchive" [for]="archive" position="bottomRight" after="60000">
Click this icon to show the advanced search menu and to show the archive!
</sqx-onboarding-tooltip>
<sqx-onboarding-tooltip id="contentFind" [for]="inputFind" position="bottomRight" after="120000">
Search for content using full text search over all fields and languages!
</sqx-onboarding-tooltip>
<div class="dropdown-menu" *sqxModalView="searchModal" [sqxModalTarget]="inputFind">
<sqx-search-form
[canArchive]="!isReadOnly"
(queryChanged)="contentsFilter.setValue($event, { emitEvent: false })"
[query]="contentsFilter.value"
(archivedChanged)="updateArchive($event)"
[archived]="isArchive">
</sqx-search-form>
</div>
<ng-container *ngIf="!isReadOnly && languages.length > 1">
<sqx-language-selector class="languages-buttons" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages"></sqx-language-selector>
</ng-container>
<button *ngIf="!isReadOnly" class="btn btn-success" #newButton routerLink="new" title="New Content (CTRL + SHIFT + G)">
<i class="icon-plus"></i> New
</button>
</ng-container>
<ng-container content>
<div class="grid-header">
<table class="table table-items table-fixed" *ngIf="contentItems">
<thead>
<tr>
<th class="cell-select" *ngIf="!isReadOnly">
<input type="checkbox" class="form-control" [ngModel]="isAllSelected" (ngModelChange)="selectAll($event)" />
</th>
<th class="cell-auto" *ngFor="let field of contentFields">
<span class="field">{{field.displayName}}</span>
</th>
<th class="cell-time">
Updated
</th>
<th class="cell-user">
By
</th>
<th class="cell-actions" *ngIf="!isReadOnly">
Actions
</th>
</tr>
</thead>
</table>
</div>
<div class="selection" *ngIf="selectionCount > 0">
{{selectionCount}} items selected:&nbsp;&nbsp;
<h3 class="panel-title" *ngIf="!isReadOnly && !isArchive">
Contents
</h3>
<button class="btn btn-secondary" (click)="publishSelected()" *ngIf="canPublish">
Publish
</button>
<h3 class="panel-title" *ngIf="isArchive">
<button class="btn btn-secondary" (click)="unpublishSelected()" *ngIf="canUnpublish">
Unpublish
</button>
<button class="btn btn-secondary" (click)="archiveSelected()" *ngIf="!isArchive">
Archive
</h3>
<h3 class="panel-title" *ngIf="isReadOnly">
References
</h3>
</button>
<button class="btn btn-secondary" (click)="restoreSelected()" *ngIf="isArchive">
Restore
</button>
<button class="btn btn-danger"
(sqxConfirmClick)="deleteSelected()"
confirmTitle="Delete content"
confirmText="Do you really want to delete the selected content items?">
Delete
</button>
</div>
<a class="panel-close" sqxParentLink>
<i class="icon-close"></i>
</a>
</div>
<div class="panel-main">
<div class="panel-content grid">
<div class="grid-header">
<table class="table table-items table-fixed" *ngIf="contentItems">
<thead>
<tr>
<th class="cell-select" *ngIf="!isReadOnly">
<input type="checkbox" class="form-control" [ngModel]="isAllSelected" (ngModelChange)="selectAll($event)" />
</th>
<th class="cell-auto" *ngFor="let field of contentFields">
<span class="field">{{field.displayName}}</span>
</th>
<th class="cell-time">
Updated
</th>
<th class="cell-user">
By
</th>
<th class="cell-actions" *ngIf="!isReadOnly">
Actions
</th>
</tr>
</thead>
<div class="grid-content">
<div sqxIgnoreScrollbar>
<table class="table table-items table-fixed" *ngIf="contentItems" >
<tbody *ngIf="!isReadOnly">
<ng-template ngFor let-content [ngForOf]="contentItems" [ngForTrackBy]="trackByContent">
<tr [sqxContent]="content" [routerLink]="[content.id]" routerLinkActive="active"
[language]="languageSelected"
[schemaFields]="contentFields"
[schema]="schema"
[selected]="isItemSelected(content)"
(selectedChange)="selectItem(content, $event)"
(unpublishing)="unpublishContent(content)"
(publishing)="publishContent(content)"
(archiving)="archiveContent(content)"
(restoring)="restoreContent(content)"
(deleting)="deleteContent(content)"
(saved)="onContentSaved(content, $event)"></tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
<tbody *ngIf="isReadOnly">
<ng-template ngFor let-content [ngForOf]="contentItems">
<tr [sqxContent]="content" dnd-draggable [dragData]="dropData(content)"
[language]="languageSelected"
[schemaFields]="contentFields"
[schema]="schema"
isReadOnly="true"></tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
</table>
</div>
</div>
<div class="selection" *ngIf="selectionCount > 0">
{{selectionCount}} items selected:&nbsp;&nbsp;
<button class="btn btn-secondary" (click)="publishSelected()" *ngIf="canPublish">
Publish
</button>
<button class="btn btn-secondary" (click)="unpublishSelected()" *ngIf="canUnpublish">
Unpublish
</button>
<button class="btn btn-secondary" (click)="archiveSelected()" *ngIf="!isArchive">
Archive
</button>
<button class="btn btn-secondary" (click)="restoreSelected()" *ngIf="isArchive">
Restore
</button>
<button class="btn btn-danger"
(sqxConfirmClick)="deleteSelected()"
confirmTitle="Delete content"
confirmText="Do you really want to delete the selected content items?">
Delete
</button>
</div>
<div class="grid-content">
<div sqxIgnoreScrollbar>
<table class="table table-items table-fixed" *ngIf="contentItems" >
<tbody *ngIf="!isReadOnly">
<ng-template ngFor let-content [ngForOf]="contentItems" [ngForTrackBy]="trackBy">
<tr [sqxContent]="content" [routerLink]="[content.id]" routerLinkActive="active"
[language]="languageSelected"
[schemaFields]="contentFields"
[schema]="schema"
[selected]="isItemSelected(content)"
(selectedChange)="selectItem(content, $event)"
(unpublishing)="unpublishContent(content)"
(publishing)="publishContent(content)"
(archiving)="archiveContent(content)"
(restoring)="restoreContent(content)"
(deleting)="deleteContent(content)"
(saved)="onContentSaved(content, $event)"></tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
<tbody *ngIf="isReadOnly">
<ng-template ngFor let-content [ngForOf]="contentItems">
<tr [sqxContent]="content" dnd-draggable [dragData]="dropData(content)"
[language]="languageSelected"
[schemaFields]="contentFields"
[schema]="schema"
isReadOnly="true"></tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
</table>
</div>
</div>
<div class="grid-footer clearfix" *ngIf="contentsPager.numberOfItems > 0">
<div class="float-right pagination">
<span class="pagination-text">{{contentsPager.itemFirst}}-{{contentsPager.itemLast}} of {{contentsPager.numberOfItems}}</span>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!contentsPager.canGoPrev" (click)="goPrev()">
<i class="icon-angle-left"></i>
</button>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!contentsPager.canGoNext" (click)="goNext()">
<i class="icon-angle-right"></i>
</button>
</div>
</div>
<div class="grid-footer">
<sqx-pager [pager]="contentsPager"></sqx-pager>
</div>
</div>
</ng-container>
</sqx-panel>
<router-outlet></router-outlet>
<div class="modal" *sqxModalView="dueTimeDialog;onRoot:true">
<div class="modal-backdrop"></div>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{dueTimeAction}} content item(s)</h4>
<ng-container *sqxModalView="dueTimeDialog;onRoot:true">
<sqx-modal-dialog (close)="cancelStatusChange()">
<ng-container title>
{{dueTimeAction}} content item(s)
</ng-container>
<ng-container content>
<div class="form-check">
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Immediately" id="immediately">
<label class="form-check-label" for="immediately">
{{dueTimeAction}} content item(s) immediately.
</label>
</div>
<div class="modal-body">
<div class="form-check">
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Immediately" id="immediately">
<label class="form-check-label" for="immediately">
{{dueTimeAction}} content item(s) immediately.
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Scheduled" id="scheduled">
<label class="form-check-label" for="scheduled">
{{dueTimeAction}} content item(s) at a later point date and time.
</label>
</div>
<sqx-date-time-editor [disabled]="dueTimeMode === 'Immediately'" mode="DateTime" hideClear="true" [(ngModel)]="dueTime"></sqx-date-time-editor>
<div class="form-check">
<input class="form-check-input" type="radio" [(ngModel)]="dueTimeMode" value="Scheduled" id="scheduled">
<label class="form-check-label" for="scheduled">
{{dueTimeAction}} content item(s) at a later point date and time.
</label>
</div>
<div class="modal-footer">
<div class="clearfix">
<button type="button" class="float-left btn btn-secondary" (click)="cancelStatusChange()">Cancel</button>
<button type="button" class="float-right btn btn-primary" [disabled]="dueTimeMode === 'Scheduled' && !dueTime" (click)="confirmStatusChange()">Confirm</button>
</div>
</div>
</div>
</div>
</div>
<sqx-date-time-editor [disabled]="dueTimeMode === 'Immediately'" mode="DateTime" hideClear="true" [(ngModel)]="dueTime"></sqx-date-time-editor>
</ng-container>
<ng-container footer>
<button type="button" class="float-left btn btn-secondary" (click)="cancelStatusChange()">Cancel</button>
<button type="button" class="float-right btn btn-primary" [disabled]="dueTimeMode === 'Scheduled' && !dueTime" (click)="confirmStatusChange()">Confirm</button>
</ng-container>
</sqx-modal-dialog>
</ng-container>

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

@ -29,7 +29,7 @@ import {
Pager,
SchemaDetailsDto,
Versioned
} from 'shared';
} from '@app/shared';
@Component({
selector: 'sqx-contents-page',
@ -361,7 +361,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
this.ctx.bus.emit(new ContentRemoved(content));
}
public trackBy(content: ContentDto): string {
public trackByContent(content: ContentDto): string {
return content.id;
}

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

@ -5,13 +5,14 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
@Component({
selector: 'sqx-search-form',
styleUrls: ['./search-form.component.scss'],
templateUrl: './search-form.component.html'
templateUrl: './search-form.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SearchFormComponent implements OnChanges {
private queryValue = '';

2
src/Squidex/app/features/content/pages/messages.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ContentDto } from 'shared';
import { ContentDto } from '@app/shared';
export class ContentCreated {
constructor(

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

@ -1,35 +1,27 @@
<sqx-title message="{app} | Schemas" parameter1="app" [value1]="ctx.appName"></sqx-title>
<sqx-panel theme="dark" desiredWidth="16rem">
<div class="panel-header">
<div class="panel-title-row">
<h3 class="panel-title">Schemas</h3>
</div>
<a class="panel-close" sqxParentLink isLazyLoaded="true">
<i class="icon-close"></i>
</a>
<sqx-panel theme="dark" desiredWidth="16rem" showSecondHeader="true">
<ng-container title>
Schemas
</ng-container>
<div class="panel-header-row">
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<ng-container secondHeader>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<div class="search-form">
<input class="form-control form-control-dark" #inputFind [formControl]="schemasFilter" placeholder="Search for schemas" />
<div class="search-form">
<input class="form-control form-control-dark" #inputFind [formControl]="schemasFilter" placeholder="Search for schemas" />
<i class="icon-search"></i>
</div>
<i class="icon-search"></i>
</div>
</div>
</ng-container>
<div class="panel-main">
<div class="panel-content">
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column">
<li class="nav-item" *ngFor="let schema of schemasFiltered | async">
<a class="nav-link" [routerLink]="schema.name" routerLinkActive="active">{{schema.displayName}} <i class="icon-angle-right"></i></a>
</li>
</ul>
</div>
</div>
<ng-container content>
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column">
<li class="nav-item" *ngFor="let schema of schemasFiltered | async">
<a class="nav-link" [routerLink]="schema.name" routerLinkActive="active">{{schema.displayName}} <i class="icon-angle-right"></i></a>
</li>
</ul>
</ng-container>
</sqx-panel>
<router-outlet></router-outlet>

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

@ -13,7 +13,7 @@ import {
AppContext,
SchemaDto,
SchemasService
} from 'shared';
} from '@app/shared';
@Component({
selector: 'sqx-schemas-page',

15
src/Squidex/app/features/content/shared/assets-editor.component.html

@ -1,8 +1,8 @@
<div class="assets-container" [class.disabled]="isDisabled">
<div class="row">
<div class="col-4 drop-area-container">
<div class="drop-area" dnd-droppable (onDropSuccess)="onAssetDropped($event.dragData)" [allowDrop]="canDrop()" (sqxFileDrop)="addFiles($event)" routerLink="assets">
Drop files or assets here to add them.
<div class="drop-area" (sqxFileDrop)="addFiles($event)" (click)="selectorModal.show()">
Drop files here to add them.
</div>
</div>
@ -10,9 +10,12 @@
(failed)="onAssetFailed(file)"
(loaded)="onAssetLoaded(file, $event)">
</sqx-asset>
<sqx-asset class="col-4" *ngFor="let asset of oldAssets" [asset]="asset" [closeMode]="true"
(closing)="onAssetRemoving($event)"
(updated)="onAssetUpdated($event)">
<sqx-asset class="col-4" *ngFor="let asset of oldAssets" [asset]="asset" removeMode="true" isDisabled="true"
(removing)="onAssetRemoving($event)">
</sqx-asset>
</div>
</div>
</div>
<ng-container *sqxModalView="selectorModal;onRoot:true;closeAuto:false">
<sqx-assets-selector (selected)="onAssetsSelected($event)"></sqx-assets-selector>
</ng-container>

2
src/Squidex/app/features/content/shared/assets-editor.component.scss

@ -27,7 +27,7 @@
font-size: 1.2rem;
font-weight: normal;
text-align: center;
padding: 3rem 2rem;
padding: 3.5rem 2rem;
color: darken($color-border, 30%);
}

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

@ -7,18 +7,17 @@
// tslint:disable:prefer-for-of
import { Component, forwardRef, OnDestroy, OnInit } from '@angular/core';
import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subscription } from 'rxjs';
import {
AppContext,
AppsState,
AssetDto,
AssetsService,
AssetUpdated,
ModalView,
ImmutableArray,
Types
} from 'shared';
} from '@app/shared';
export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AssetsEditorComponent), multi: true
@ -29,46 +28,33 @@ export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
styleUrls: ['./assets-editor.component.scss'],
templateUrl: './assets-editor.component.html',
providers: [
AppContext,
SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR
]
})
export class AssetsEditorComponent implements ControlValueAccessor, OnDestroy, OnInit {
private assetUpdatedSubscription: Subscription;
export class AssetsEditorComponent implements ControlValueAccessor {
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
public selectorModal = new ModalView();
public newAssets = ImmutableArray.empty<File>();
public oldAssets = ImmutableArray.empty<AssetDto>();
public isDisabled = false;
constructor(public readonly ctx: AppContext,
constructor(
private readonly appsState: AppsState,
private readonly assetsService: AssetsService
) {
}
public ngOnDestroy() {
this.assetUpdatedSubscription.unsubscribe();
}
public ngOnInit() {
this.assetUpdatedSubscription =
this.ctx.bus.of(AssetUpdated)
.subscribe(event => {
if (event.sender !== this) {
this.oldAssets = this.oldAssets.replaceBy('id', event.assetDto);
}
});
}
public writeValue(value: string[]) {
this.oldAssets = ImmutableArray.empty<AssetDto>();
if (Types.isArrayOfString(value) && value.length > 0) {
const assetIds: string[] = value;
this.assetsService.getAssets(this.ctx.appName, 0, 0, undefined, value)
this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, value)
.subscribe(dtos => {
this.oldAssets = ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)).filter(a => !!a).map(a => a!));
});
@ -93,20 +79,16 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnDestroy, O
}
}
public canDrop() {
const component = this;
return (dragData: any) => {
return !component.isDisabled && dragData instanceof AssetDto && !component.oldAssets.find(a => a.id === dragData.id);
};
}
public onAssetDropped(asset: AssetDto) {
if (asset) {
this.oldAssets = this.oldAssets.pushFront(asset);
public onAssetsSelected(assets: AssetDto[]) {
for (let asset of assets) {
this.oldAssets = this.oldAssets.push(asset);
}
if (assets.length > 0) {
this.updateValue();
}
this.selectorModal.hide();
}
public onAssetRemoving(asset: AssetDto) {
@ -124,10 +106,6 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnDestroy, O
this.updateValue();
}
public onAssetUpdated(asset: AssetDto) {
this.ctx.bus.emit(new AssetUpdated(asset, this));
}
public onAssetFailed(file: File) {
this.newAssets = this.newAssets.remove(file);
}

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

@ -56,21 +56,21 @@
</div>
</td>
<td class="cell-time" (click)="shouldStop($event)">
<span *ngIf="!content.scheduledTo">
<ng-container *ngIf="!content.scheduledTo">
<span class="content-status content-status-{{content.status | lowercase}}" #statusIcon>
<i class="icon-circle"></i>
</span>
<sqx-tooltip [target]="statusIcon">{{content.status}}</sqx-tooltip>
</span>
</ng-container>
<span *ngIf="content.scheduledTo">
<ng-container *ngIf="content.scheduledTo">
<span class="content-status content-status-{{content.scheduledTo | lowercase}}" #statusIcon>
<i class="icon-clock"></i>
</span>
<sqx-tooltip [target]="statusIcon">Will be set to '{{content.scheduledTo}}' at {{content.scheduledAt | sqxFullDateTime}}</sqx-tooltip>
</span>
</ng-container>
<small class="item-modified">{{content.lastModified | sqxFromNow}}</small>
</td>

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

@ -20,7 +20,7 @@ import {
SchemaDto,
Types,
Versioned
} from 'shared';
} from '@app/shared';
/* tslint:disable:component-selector */

2
src/Squidex/app/features/content/shared/references-editor.component.ts

@ -21,7 +21,7 @@ import {
SchemaDetailsDto,
SchemasService,
Types
} from 'shared';
} from '@app/shared';
export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ReferencesEditorComponent), multi: true

2
src/Squidex/app/features/dashboard/module.ts

@ -9,7 +9,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ChartModule } from 'angular2-chartjs';
import { SqxFrameworkModule, SqxSharedModule } from 'shared';
import { SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import {
DashboardPageComponent

2
src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts

@ -18,7 +18,7 @@ import {
HistoryService,
UsagesService,
UsersProviderService
} from 'shared';
} from '@app/shared';
declare var _urq: any;

2
src/Squidex/app/features/rules/module.ts

@ -12,7 +12,7 @@ import {
HelpComponent,
SqxFrameworkModule,
SqxSharedModule
} from 'shared';
} from '@app/shared';
import {
AlgoliaActionComponent,

185
src/Squidex/app/features/rules/pages/events/rule-events-page.component.html

@ -1,116 +1,97 @@
<sqx-title message="{app} | Rules Events" parameter1="app" [value1]="ctx.appName"></sqx-title>
<sqx-panel desiredWidth="63rem">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Events (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<ng-container title>
Events
</ng-container>
<ng-container menu>
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Events (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
</div>
<h3 class="panel-title">Events</h3>
</div>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
</ng-container>
<ng-container content>
<table class="table table-items table-fixed">
<thead>
<tr>
<th class="cell-label">
Status
</th>
<th class="cell-40">
Event
</th>
<th class="cell-60">
Description
</th>
<th class="cell-time">
Created
</th>
<th class="cell-actions">
<a class="panel-close" sqxParentLink isLazyLoaded="true">
<i class="icon-close"></i>
</a>
</div>
</th>
</tr>
</thead>
<div class="panel-main">
<div class="panel-content panel-content-scroll">
<table class="table table-items table-fixed">
<thead>
<tr>
<th class="cell-label">
Status
</th>
<th class="cell-40">
Event
</th>
<th class="cell-60">
Description
</th>
<th class="cell-time">
Created
</th>
<th class="cell-actions">
</th>
<tbody>
<ng-template ngFor let-event [ngForOf]="eventsItems">
<tr [class.expanded]="selectedEventId === event.id">
<td class="cell-label">
<span class="badge badge-pill badge-{{getBadgeClass(event.jobResult)}}">{{event.jobResult}}</span>
</td>
<td class="cell-40">
<span class="truncate">{{event.eventName}}</span>
</td>
<td class="cell-60">
<span class="truncate">{{event.description}}</span>
</td>
<td class="cell-time">
<small class="item-modified">{{event.created | sqxFromNow}}</small>
</td>
<td class="cell-actions">
<button type="button" class="btn btn-secondary table-items-edit-button" [class.active]="selectedEventId === event.id" (click)="selectEvent(event.id)">
<i class="icon-settings"></i>
</button>
</td>
</tr>
</thead>
<tbody>
<ng-template ngFor let-event [ngForOf]="eventsItems">
<tr [class.expanded]="selectedEventId === event.id">
<td class="cell-label">
<span class="badge badge-pill badge-{{getBadgeClass(event.jobResult)}}">{{event.jobResult}}</span>
</td>
<td class="cell-40">
<span class="truncate">{{event.eventName}}</span>
</td>
<td class="cell-60">
<span class="truncate">{{event.description}}</span>
</td>
<td class="cell-time">
<small class="item-modified">{{event.created | sqxFromNow}}</small>
</td>
<td class="cell-actions">
<button type="button" class="btn btn-secondary table-items-edit-button" [class.active]="selectedEventId === event.id" (click)="selectEvent(event.id)">
<i class="icon-settings"></i>
</button>
</td>
</tr>
<tr *ngIf="selectedEventId === event.id">
<td colspan="5">
<div class="event-header">
<h3>Last Invocation</h3>
<tr *ngIf="selectedEventId === event.id">
<td colspan="5">
<div class="event-header">
<h3>Last Invocation</h3>
</div>
<div class="row event-stats">
<div class="col-3">
<span class="badge badge-pill badge-{{getBadgeClass(event.result)}}">{{event.result}}</span>
</div>
<div class="row event-stats">
<div class="col-3">
<span class="badge badge-pill badge-{{getBadgeClass(event.result)}}">{{event.result}}</span>
</div>
<div class="col-3">
Attempts: {{event.numCalls}}
</div>
<div class="col-3">
Next: <span *ngIf="event.nextAttempt">{{event.nextAttempt | sqxFromNow}}</span>
</div>
<div class="col-3 text-right">
<button class="btn btn-success btn-sm" (click)="enqueueEvent(event)">
Enqueue
</button>
</div>
<div class="col-3">
Attempts: {{event.numCalls}}
</div>
<div class="row">
<div class="col-12">
<textarea class="event-dump form-control" readonly>{{event.lastDump}}</textarea>
</div>
<div class="col-3">
Next: <ng-container *ngIf="event.nextAttempt">{{event.nextAttempt | sqxFromNow}}</ng-container>
</div>
</td>
</tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
</table>
<div class="clearfix" *ngIf="eventsPager.numberOfItems > 0">
<div class="float-right pagination">
<span class="pagination-text">{{eventsPager.itemFirst}}-{{eventsPager.itemLast}} of {{eventsPager.numberOfItems}}</span>
<div class="col-3 text-right">
<button class="btn btn-success btn-sm" (click)="enqueueEvent(event)">
Enqueue
</button>
</div>
</div>
<div class="row">
<div class="col-12">
<textarea class="event-dump form-control" readonly>{{event.lastDump}}</textarea>
</div>
</div>
</td>
</tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
</table>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!eventsPager.canGoPrev" (click)="goPrev()">
<i class="icon-angle-left"></i>
</button>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!eventsPager.canGoNext" (click)="goNext()">
<i class="icon-angle-right"></i>
</button>
</div>
</div>
</div>
</div>
<sqx-pager [pager]="eventsPager"></sqx-pager>
</ng-container>
</sqx-panel>
<router-outlet></router-outlet>

2
src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts

@ -13,7 +13,7 @@ import {
Pager,
RuleEventDto,
RulesService
} from 'shared';
} from '@app/shared';
@Component({
selector: 'sqx-rule-events-page',

2
src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.html

@ -1,4 +1,4 @@
<h3 class="rule-title">Populate index in algolia with content</h3>
<h3 class="wizard-title">Populate index in algolia with content</h3>
<form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row">

9
src/Squidex/app/features/rules/pages/rules/actions/algolia-action.component.ts

@ -26,15 +26,18 @@ export class AlgoliaActionComponent implements OnInit {
appId: ['',
[
Validators.required
]],
]
],
apiKey: ['',
[
Validators.required
]],
]
],
indexName: ['$SCHEMA_NAME',
[
Validators.required
]]
]
]
});
constructor(

2
src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.html

@ -1,4 +1,4 @@
<h3 class="rule-title">Send event payload to Azure Storage Queue</h3>
<h3 class="wizard-title">Send event payload to Azure Storage Queue</h3>
<form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row">

8
src/Squidex/app/features/rules/pages/rules/actions/azure-queue-action.component.ts

@ -8,7 +8,7 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { ValidatorsEx } from 'shared';
import { ValidatorsEx } from '@app/shared';
@Component({
selector: 'sqx-azure-queue-action',
@ -28,12 +28,14 @@ export class AzureQueueActionComponent implements OnInit {
connectionString: ['',
[
Validators.required
]],
]
],
queue: ['squidex',
[
Validators.required,
ValidatorsEx.pattern('[a-z][a-z0-9]{2,}(\-[a-z0-9]+)*', 'Name must be a valid azure queue name.')
]]
]
]
});
constructor(

2
src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.html

@ -1,4 +1,4 @@
<h3 class="rule-title">Populate index in ElasticSearch with content</h3>
<h3 class="wizard-title">Populate index in ElasticSearch with content</h3>
<form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row">

9
src/Squidex/app/features/rules/pages/rules/actions/elastic-search-action.component.ts

@ -26,15 +26,18 @@ export class ElasticSearchActionComponent implements OnInit {
host: ['',
[
Validators.required
]],
]
],
indexName: ['$APP_NAME',
[
Validators.required
]],
]
],
indexType: ['$SCHEMA_NAME',
[
// Validators.required
]],
]
],
username: '',
password: ''
});

2
src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.html

@ -1,4 +1,4 @@
<h3 class="rule-title">Purge cache entries in Fastly</h3>
<h3 class="wizard-title">Purge cache entries in Fastly</h3>
<form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row">

6
src/Squidex/app/features/rules/pages/rules/actions/fastly-action.component.ts

@ -26,11 +26,13 @@ export class FastlyActionComponent implements OnInit {
serviceId: ['',
[
Validators.required
]],
]
],
apiKey: ['',
[
Validators.required
]]
]
]
});
constructor(

2
src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.html

@ -1,4 +1,4 @@
<h3 class="rule-title">Send custom text to an incoming webhook in Slack</h3>
<h3 class="wizard-title">Send custom text to an incoming webhook in Slack</h3>
<form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row">

5
src/Squidex/app/features/rules/pages/rules/actions/slack-action.component.ts

@ -26,8 +26,9 @@ export class SlackActionComponent implements OnInit {
webhookUrl: ['',
[
Validators.required
]],
text: ['']
]
],
text: ''
});
constructor(

2
src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.html

@ -1,4 +1,4 @@
<h3 class="rule-title">Send event payload to webhook</h3>
<h3 class="wizard-title">Send event payload to webhook</h3>
<form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row">

5
src/Squidex/app/features/rules/pages/rules/actions/webhook-action.component.ts

@ -26,8 +26,9 @@ export class WebhookActionComponent implements OnInit {
url: ['',
[
Validators.required
]],
sharedSecret: ['']
]
],
sharedSecret: ''
});
constructor(

237
src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html

@ -1,127 +1,122 @@
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" *ngIf="mode === 'EditTrigger'">
Edit Trigger
</h4>
<h4 class="modal-title" *ngIf="mode === 'EditAction'">
Edit Action
</h4>
<h4 class="modal-title" *ngIf="mode === 'Wizard' && step === 1">
Step 1 of 4: Select Trigger
</h4>
<h4 class="modal-title" *ngIf="mode === 'Wizard' && step === 2">
Step 2 of 4: Configure Trigger
</h4>
<h4 class="modal-title" *ngIf="mode === 'Wizard' && step === 3">
Step 3 of 4: Select Action
</h4>
<h4 class="modal-title" *ngIf="mode === 'Wizard' && step === 4">
Step 4 of 4: Configure Action
</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="cancel()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<sqx-modal-dialog large="true" (close)="cancel()">
<ng-container title>
<ng-container *ngIf="mode === 'EditTrigger'">
Edit Trigger
</ng-container>
<ng-container *ngIf="mode === 'EditAction'">
Edit Action
</ng-container>
<ng-container *ngIf="mode === 'Wizard' && step === 1">
Step 1 of 4: Select Trigger
</ng-container>
<ng-container *ngIf="mode === 'Wizard' && step === 2">
Step 2 of 4: Configure Trigger
</ng-container>
<ng-container *ngIf="mode === 'Wizard' && step === 3">
Step 3 of 4: Select Action
</ng-container>
<ng-container *ngIf="mode === 'Wizard' && step === 4">
Step 4 of 4: Configure Action
</ng-container>
</ng-container>
<div class="modal-body">
<div *ngIf="step === 1">
<span *ngFor="let trigger of ruleTriggers | sqxKeys" class="rule-element rule-element-{{trigger}}" (click)="selectTriggerType(trigger)">
<span class="rule-element-icon">
<i class="icon-trigger-{{trigger}}"></i>
</span>
<span class="rule-element-text">
{{ruleTriggers[trigger].name}}
</span>
<ng-container content>
<ng-container *ngIf="step === 1">
<span *ngFor="let trigger of ruleTriggers | sqxKeys" class="rule-element rule-element-{{trigger}}" (click)="selectTriggerType(trigger)">
<span class="rule-element-icon">
<i class="icon-trigger-{{trigger}}"></i>
</span>
</div>
<div *ngIf="step === 2 && schemas" class="modal-form">
<div [ngSwitch]="triggerType">
<div *ngSwitchCase="'AssetChanged'">
<sqx-asset-changed-trigger #triggerControl
[trigger]="trigger"
(triggerChanged)="selectTrigger($event)">
</sqx-asset-changed-trigger>
</div>
<div *ngSwitchCase="'ContentChanged'">
<sqx-content-changed-trigger #triggerControl
[schemas]="schemas"
[trigger]="trigger"
(triggerChanged)="selectTrigger($event)">
</sqx-content-changed-trigger>
</div>
</div>
</div>
<span class="rule-element-text">
{{ruleTriggers[trigger].name}}
</span>
</span>
</ng-container>
<div *ngIf="step === 3">
<span *ngFor="let action of ruleActions | sqxKeys" class="rule-element rule-element-{{action}}" (click)="selectActionType(action)">
<span class="rule-element-icon">
<i class="icon-action-{{action}}"></i>
</span>
<span class="rule-element-text">
{{ruleActions[action].name}}
</span>
<ng-container *ngIf="step === 2 && schemas">
<ng-container [ngSwitch]="triggerType">
<ng-container *ngSwitchCase="'AssetChanged'">
<sqx-asset-changed-trigger #triggerControl
[trigger]="trigger"
(triggerChanged)="selectTrigger($event)">
</sqx-asset-changed-trigger>
</ng-container>
<ng-container *ngSwitchCase="'ContentChanged'">
<sqx-content-changed-trigger #triggerControl
[schemas]="schemas"
[trigger]="trigger"
(triggerChanged)="selectTrigger($event)">
</sqx-content-changed-trigger>
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngIf="step === 3">
<span *ngFor="let action of ruleActions | sqxKeys" class="rule-element rule-element-{{action}}" (click)="selectActionType(action)">
<span class="rule-element-icon">
<i class="icon-action-{{action}}"></i>
</span>
<span class="rule-element-text">
{{ruleActions[action].name}}
</span>
</div>
</span>
</ng-container>
<ng-container *ngIf="step === 4">
<ng-container [ngSwitch]="actionType">
<ng-container *ngSwitchCase="'Algolia'">
<sqx-algolia-action #actionControl
[action]="action"
(actionChanged)="selectAction($event)">
</sqx-algolia-action>
</ng-container>
<ng-container *ngSwitchCase="'AzureQueue'">
<sqx-azure-queue-action #actionControl
[action]="action"
(actionChanged)="selectAction($event)">
</sqx-azure-queue-action>
</ng-container>
<ng-container *ngSwitchCase="'ElasticSearch'">
<sqx-elastic-search-action #actionControl
[action]="action"
(actionChanged)="selectAction($event)">
</sqx-elastic-search-action>
</ng-container>
<ng-container *ngSwitchCase="'Fastly'">
<sqx-fastly-action #actionControl
[action]="action"
(actionChanged)="selectAction($event)">
</sqx-fastly-action>
</ng-container>
<ng-container *ngSwitchCase="'Slack'">
<sqx-slack-action #actionControl
[action]="action"
(actionChanged)="selectAction($event)">
</sqx-slack-action>
</ng-container>
<ng-container *ngSwitchCase="'Webhook'">
<sqx-webhook-action #actionControl
[action]="action"
(actionChanged)="selectAction($event)">
</sqx-webhook-action>
</ng-container>
</ng-container>
</ng-container>
</ng-container>
<ng-container footer>
<ng-container *ngIf="mode === 'Wizard' && step === 2">
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button>
<button type="submit" class="float-right btn btn-primary" (click)="triggerControl.save()">Next</button>
</ng-container>
<div *ngIf="step === 4" class="modal-form">
<div [ngSwitch]="actionType">
<div *ngSwitchCase="'Algolia'">
<sqx-algolia-action #actionControl
[action]="action"
(actionChanged)="selectAction($event)">
</sqx-algolia-action>
</div>
<div *ngSwitchCase="'AzureQueue'">
<sqx-azure-queue-action #actionControl
[action]="action"
(actionChanged)="selectAction($event)">
</sqx-azure-queue-action>
</div>
<div *ngSwitchCase="'ElasticSearch'">
<sqx-elastic-search-action #actionControl
[action]="action"
(actionChanged)="selectAction($event)">
</sqx-elastic-search-action>
</div>
<div *ngSwitchCase="'Fastly'">
<sqx-fastly-action #actionControl
[action]="action"
(actionChanged)="selectAction($event)">
</sqx-fastly-action>
</div>
<div *ngSwitchCase="'Slack'">
<sqx-slack-action #actionControl
[action]="action"
(actionChanged)="selectAction($event)">
</sqx-slack-action>
</div>
<div *ngSwitchCase="'Webhook'">
<sqx-webhook-action #actionControl
[action]="action"
(actionChanged)="selectAction($event)">
</sqx-webhook-action>
</div>
</div>
</div>
</div>
<div class="modal-footer" *ngIf="step === 2 || step === 4">
<div class="clearfix" *ngIf="mode === 'Wizard' && step === 2">
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button>
<button type="submit" class="float-right btn btn-primary" (click)="triggerControl.save()">Next</button>
</div>
<div class="clearfix" *ngIf="mode !== 'Wizard' && step === 2">
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button>
<button type="submit" class="float-right btn btn-primary" (click)="triggerControl.save()">Save</button>
</div>
<ng-container *ngIf="mode !== 'Wizard' && step === 2">
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button>
<button type="submit" class="float-right btn btn-primary" (click)="triggerControl.save()">Save</button>
</ng-container>
<div class="clearfix" *ngIf="step === 4">
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button>
<button type="submit" class="float-right btn btn-primary" (click)="actionControl.save()">Save</button>
</div>
</div>
</div>
</div>
<ng-container *ngIf="step === 4">
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button>
<button type="submit" class="float-right btn btn-primary" (click)="actionControl.save()">Save</button>
</ng-container>
</ng-container>
</sqx-modal-dialog>

6
src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts

@ -11,14 +11,13 @@ import {
AppContext,
CreateRuleDto,
DateTime,
fadeAnimation,
ruleActions,
ruleTriggers,
RuleDto,
RulesService,
SchemaDto,
UpdateRuleDto
} from 'shared';
} from '@app/shared';
export const MODE_WIZARD = 'Wizard';
export const MODE_EDIT_TRIGGER = 'EditTrigger';
@ -30,9 +29,6 @@ export const MODE_EDIT_ACTION = 'EditAction';
templateUrl: './rule-wizard.component.html',
providers: [
AppContext
],
animations: [
fadeAnimation
]
})
export class RuleWizardComponent implements OnInit {

166
src/Squidex/app/features/rules/pages/rules/rules-page.component.html

@ -1,107 +1,97 @@
<sqx-title message="{app} | Rules" parameter1="app" [value1]="ctx.appName"></sqx-title>
<sqx-panel desiredWidth="54rem">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<sqx-panel desiredWidth="54rem" showSidebar="true">
<ng-container title>
Rules
</ng-container>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut>
<button class="btn btn-success" #buttonNew (click)="createNew()" title="New Rule (CTRL + M)">
<i class="icon-plus"></i> New
</button>
</div>
<ng-container menu>
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<h3 class="panel-title">Rules</h3>
</div>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut>
<a class="panel-close" sqxParentLink isLazyLoaded="true">
<i class="icon-close"></i>
</a>
</div>
<button class="btn btn-success" #buttonNew (click)="createNew()" title="New Rule (CTRL + M)">
<i class="icon-plus"></i> New
</button>
</ng-container>
<div class="panel-main">
<div class="panel-content panel-content-scroll">
<div class="table-items-row table-items-row-empty" *ngIf="rules && rules.length === 0">
No Rule created yet.
</div>
<ng-container content>
<div class="table-items-row table-items-row-empty" *ngIf="rules && rules.length === 0">
No Rule created yet.
</div>
<table class="table table-items table-fixed" *ngIf="rules && rules.length > 0">
<tbody>
<ng-template ngFor let-rule [ngForOf]="rules">
<tr>
<td class="cell-separator">
<h3>If</h3>
</td>
<td class="cell-auto">
<span class="rule-element rule-element-{{rule.triggerType}}" (click)="editTrigger(rule)">
<span class="rule-element-icon">
<i class="icon-trigger-{{rule.triggerType}}"></i>
</span>
<span class="rule-element-text">
{{ruleTriggers[rule.triggerType].name}}
</span>
<table class="table table-items table-fixed" *ngIf="rules && rules.length > 0">
<tbody>
<ng-template ngFor let-rule [ngForOf]="rules">
<tr>
<td class="cell-separator">
<h3>If</h3>
</td>
<td class="cell-auto">
<span class="rule-element rule-element-{{rule.triggerType}}" (click)="editTrigger(rule)">
<span class="rule-element-icon">
<i class="icon-trigger-{{rule.triggerType}}"></i>
</span>
</td>
<td class="cell-separator">
<h3>then</h3>
</td>
<td class="cell-auto">
<span class="rule-element rule-element-{{rule.actionType}}" (click)="editAction(rule)">
<span class="rule-element-icon">
<i class="icon-action-{{rule.actionType}}"></i>
</span>
<span class="rule-element-text">
{{ruleActions[rule.actionType].name}}
</span>
<span class="rule-element-text">
{{ruleTriggers[rule.triggerType].name}}
</span>
</td>
<td class="cell-actions">
<sqx-toggle [ngModel]="rule.isEnabled" (ngModelChange)="toggleRule(rule)"></sqx-toggle>
</td>
<td class="cell-actions">
<button type="button" class="btn btn-link btn-danger"
(sqxConfirmClick)="deleteRule(rule)"
confirmTitle="Delete rule"
confirmText="Do you really want to delete the rule?">
<i class="icon-bin2"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
</table>
</div>
</span>
</td>
<td class="cell-separator">
<h3>then</h3>
</td>
<td class="cell-auto">
<span class="rule-element rule-element-{{rule.actionType}}" (click)="editAction(rule)">
<span class="rule-element-icon">
<i class="icon-action-{{rule.actionType}}"></i>
</span>
<span class="rule-element-text">
{{ruleActions[rule.actionType].name}}
</span>
</span>
</td>
<td class="cell-actions">
<sqx-toggle [ngModel]="rule.isEnabled" (ngModelChange)="toggleRule(rule)"></sqx-toggle>
</td>
<td class="cell-actions">
<button type="button" class="btn btn-link btn-danger"
(sqxConfirmClick)="deleteRule(rule)"
confirmTitle="Delete rule"
confirmText="Do you really want to delete the rule?">
<i class="icon-bin2"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
</table>
</ng-container>
<div class="panel-sidebar">
<a class="panel-link" routerLink="events" routerLinkActive="active" #linkHistory>
<i class="icon-time"></i>
</a>
<a class="panel-link" routerLink="help" routerLinkActive="active" #linkHelp>
<i class="icon-help"></i>
</a>
<sqx-onboarding-tooltip id="help" [for]="linkHelp" position="leftTop" after="180000">
Click the help icon to show a context specific help page. Go to <a href="https://docs.squidex.io" target="_blank">https://docs.squidex.io</a> for the full documentation.
</sqx-onboarding-tooltip>
</div>
</div>
<ng-container sidebar>
<a class="panel-link" routerLink="events" routerLinkActive="active" #linkHistory>
<i class="icon-time"></i>
</a>
<a class="panel-link" routerLink="help" routerLinkActive="active" #linkHelp>
<i class="icon-help"></i>
</a>
<sqx-onboarding-tooltip id="help" [for]="linkHelp" position="leftTop" after="180000">
Click the help icon to show a context specific help page. Go to <a href="https://docs.squidex.io" target="_blank">https://docs.squidex.io</a> for the full documentation.
</sqx-onboarding-tooltip>
</ng-container>
</sqx-panel>
<div class="modal" *sqxModalView="addRuleDialog;onRoot:true;closeAuto:false" @fade>
<div class="modal-backdrop"></div>
<ng-container *sqxModalView="addRuleDialog;onRoot:true;closeAuto:false">
<sqx-rule-wizard [schemas]="schemas" [rule]="wizardRule" [mode]="wizardMode"
(updated)="onRuleUpdated($event)"
(cancelled)="addRuleDialog.hide()"
(created)="onRuleCreated($event)">
</sqx-rule-wizard>
</div>
</ng-container>
<router-outlet></router-outlet>

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

@ -10,7 +10,6 @@ import { Component, OnInit } from '@angular/core';
import {
AppContext,
DateTime,
fadeAnimation,
ImmutableArray,
ModalView,
ruleActions,
@ -19,7 +18,7 @@ import {
RulesService,
SchemaDto,
SchemasService
} from 'shared';
} from '@app/shared';
@Component({
selector: 'sqx-rules-page',
@ -27,9 +26,6 @@ import {
templateUrl: './rules-page.component.html',
providers: [
AppContext
],
animations: [
fadeAnimation
]
})
export class RulesPageComponent implements OnInit {

2
src/Squidex/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html

@ -1,4 +1,4 @@
<h3 class="rule-title">Trigger rule when asset has been...</h3>
<h3 class="wizard-title">Trigger rule when asset has been...</h3>
<form [formGroup]="triggerForm" class="form-horizontal" (ngSubmit)="save()">

2
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.html

@ -1,4 +1,4 @@
<h3 class="rule-title">Trigger rule when an events for a schemas happens</h3>
<h3 class="wizard-title">Trigger rule when an events for a schemas happens</h3>
<table class="table table-middle table-sm table-fixed table-borderless" *ngIf="!handleAll">
<colgroup>

2
src/Squidex/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts

@ -11,7 +11,7 @@ import {
ImmutableArray,
SchemaDto,
Types
} from 'shared';
} from '@app/shared';
export interface TriggerSchemaForm {
schema: SchemaDto;

7
src/Squidex/app/features/schemas/declarations.ts

@ -5,6 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
export * from './guards/schema-must-exist.guard';
export * from './pages/schema/types/assets-ui.component';
export * from './pages/schema/types/assets-validation.component';
export * from './pages/schema/types/boolean-ui.component';
@ -24,7 +26,12 @@ export * from './pages/schema/types/string-validation.component';
export * from './pages/schema/types/tags-ui.component';
export * from './pages/schema/types/tags-validation.component';
export * from './pages/schema/forms/field-form-common.component';
export * from './pages/schema/forms/field-form-ui.component';
export * from './pages/schema/forms/field-form-validation.component';
export * from './pages/schema/field.component';
export * from './pages/schema/field-wizard.component';
export * from './pages/schema/schema-edit-form.component';
export * from './pages/schema/schema-page.component';
export * from './pages/schema/schema-scripts-form.component';

36
src/Squidex/app/features/schemas/guards/schema-must-exist.guard.ts

@ -0,0 +1,36 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { allParams, SchemasState } from '@app/shared';
@Injectable()
export class SchemaMustExistGuard implements CanActivate {
constructor(
private readonly schemasState: SchemasState,
private readonly router: Router
) {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const schemaName = allParams(route)['schemaName'];
const result =
this.schemasState.selectSchema(schemaName)
.do(dto => {
if (!dto) {
this.router.navigate(['/404']);
}
})
.map(u => u !== null);
return result;
}
}

40
src/Squidex/app/features/schemas/module.ts

@ -10,12 +10,9 @@ import { RouterModule, Routes } from '@angular/router';
import { DndModule } from 'ng2-dnd';
import {
HelpComponent,
HistoryComponent,
ResolveSchemaGuard,
SqxFrameworkModule,
SqxSharedModule
} from 'shared';
} from '@app/shared';
import {
FieldComponent,
@ -25,6 +22,10 @@ import {
BooleanValidationComponent,
DateTimeUIComponent,
DateTimeValidationComponent,
FieldFormCommonComponent,
FieldFormUIComponent,
FieldFormValidationComponent,
FieldWizardComponent,
GeolocationUIComponent,
GeolocationValidationComponent,
JsonUIComponent,
@ -35,6 +36,7 @@ import {
ReferencesValidationComponent,
SchemaEditFormComponent,
SchemaFormComponent,
SchemaMustExistGuard,
SchemaPageComponent,
SchemasPageComponent,
SchemaScriptsFormComponent,
@ -49,31 +51,10 @@ const routes: Routes = [
path: '',
component: SchemasPageComponent,
children: [
{
path: ''
},
{
path: ':schemaName',
component: SchemaPageComponent,
resolve: {
schema: ResolveSchemaGuard
},
children: [
{
path: 'history',
component: HistoryComponent,
data: {
channel: 'schemas.{schemaName}'
}
},
{
path: 'help',
component: HelpComponent,
data: {
helpPage: '05-integrated/schemas'
}
}
]
canActivate: [SchemaMustExistGuard]
}]
}
];
@ -85,6 +66,9 @@ const routes: Routes = [
DndModule,
RouterModule.forChild(routes)
],
providers: [
SchemaMustExistGuard
],
declarations: [
FieldComponent,
AssetsUIComponent,
@ -93,6 +77,10 @@ const routes: Routes = [
BooleanValidationComponent,
DateTimeUIComponent,
DateTimeValidationComponent,
FieldFormCommonComponent,
FieldFormUIComponent,
FieldFormValidationComponent,
FieldWizardComponent,
GeolocationUIComponent,
GeolocationValidationComponent,
JsonUIComponent,

25
src/Squidex/app/features/schemas/pages/messages.ts

@ -5,32 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { SchemaDto } from 'shared';
export class SchemaUpdated {
constructor(
public readonly schema: SchemaDto
) {
}
}
export class SchemaCreated {
constructor(
public readonly schema: SchemaDto
) {
}
}
export class SchemaDeleted {
constructor(
public readonly schema: SchemaDto
) {
}
}
export class SchemaCloning {
constructor(
public readonly importing: any
public readonly schema: any
) {
}
}

62
src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html

@ -0,0 +1,62 @@
<form [formGroup]="addFieldForm.form" (ngSubmit)="addField(false)">
<sqx-modal-dialog (close)="complete()" large="true">
<ng-container title>
Create Field
</ng-container>
<ng-container content>
<sqx-form-error [error]="addFieldForm.error | async"></sqx-form-error>
<div class="form-group">
<div class="row">
<div class="col-4 type" *ngFor="let fieldType of fieldTypes">
<label>
<input type="radio" class="radio-input" formControlName="type" value="{{fieldType.type}}" />
<div class="row no-gutters">
<div class="col col-auto">
<div class="type-icon" [class.active]="addFieldForm.form.controls['type'].value === fieldType.type">
<i class="icon-type-{{fieldType.type}}"></i>
</div>
</div>
<div class="col">
<div class="type-title">{{fieldType.type}}</div>
<div class="type-text text-muted">{{fieldType.description}}</div>
</div>
</div>
</label>
</div>
</div>
</div>
<div class="form-group">
<sqx-control-errors for="name" submitOnly="true" [submitted]="addFieldForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control" formControlName="name" maxlength="40" #nameInput placeholder="Enter field name" sqxFocusOnInit />
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isLocalizable" formControlName="isLocalizable" />
<label class="form-check-label" for="isLocalizable">
Localizable
</label>
</div>
<small class="form-text text-muted">
You can the field as localizable. It means that is dependent on the language, for example a city name.
</small>
</div>
</ng-container>
<ng-container footer>
<button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button>
<div class="float-right">
<button class="btn btn-success" (click)="addField(false)">Create and close</button>
<button class="btn btn-success" (click)="addField(true)">Create and new field</button>
</div>
</ng-container>
</sqx-modal-dialog>
</form>

72
src/Squidex/app/features/schemas/pages/schema/field-wizard.component.scss

@ -0,0 +1,72 @@
@import '_vars';
@import '_mixins';
$icon-size: 4.5rem;
.form-group {
margin-bottom: 1.5rem;
}
.nav-link {
cursor: default;
}
.nav-tabs {
margin-bottom: 1rem;
}
.type {
& {
margin-bottom: .5rem;
}
&-title {
font-weight: bold;
}
&-text {
font-size: .9rem;
}
&-icon {
& {
@include border-radius;
height: $icon-size;
color: $color-theme-blue;
cursor: pointer;
border: 1px solid $color-border;
background: transparent;
margin-right: .5rem;
line-height: $icon-size;
font-size: 1.75rem;
font-weight: normal;
text-align: center;
width: $icon-size;
}
.radio-input {
display: none;
}
&.active {
& {
@include box-shadow(0, 0, 10px, .5);
background: $color-theme-blue;
border-color: $color-theme-blue;
color: $color-dark-foreground;
}
&:hover {
color: $color-dark-foreground;
}
}
&:hover {
border-color: $color-border-dark;
}
}
.radio-input {
display: none;
}
}

66
src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts

@ -0,0 +1,66 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import {
AddFieldForm,
fieldTypes,
SchemaDetailsDto,
SchemasState
} from '@app/shared';
@Component({
selector: 'sqx-field-wizard',
styleUrls: ['./field-wizard.component.scss'],
templateUrl: './field-wizard.component.html'
})
export class FieldWizardComponent {
@ViewChild('nameInput')
public nameInput: ElementRef;
@Input()
public schema: SchemaDetailsDto;
@Output()
public completed = new EventEmitter();
public fieldTypes = fieldTypes;
public addFieldForm: AddFieldForm;
constructor(formBuilder: FormBuilder,
private readonly schemasState: SchemasState
) {
this.addFieldForm = new AddFieldForm(formBuilder);
}
public complete() {
this.completed.emit();
}
public addField(next: boolean) {
const value = this.addFieldForm.submit();
if (value) {
this.schemasState.addField(this.schema, value)
.subscribe(dto => {
this.addFieldForm.submitCompleted({ type: fieldTypes[0].type });
if (next) {
this.nameInput.nativeElement.focus();
} else {
this.complete();
}
}, error => {
this.addFieldForm.submitFailed(error);
});
}
}
}

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

@ -5,7 +5,7 @@
<span class="field-name">
<i class="field-icon icon-type-{{field.properties.fieldType}}"></i>
<span [class.field-hidden]="field.isHidden" [attr.title]="field.isHidden ? 'Hidden Field' : 'Visible Field'">{{displayName}}</span>
<span [class.field-hidden]="field.isHidden" [attr.title]="field.isHidden ? 'Hidden Field' : 'Visible Field'">{{field.displayName}}</span>
<span class="field-partitioning" *ngIf="field.isLocalizable">localizable</span>
</span>
</div>
@ -27,26 +27,26 @@
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="dropdown" [sqxModalTarget]="optionsButton" @fade>
<a class="dropdown-item" (click)="enabling.emit()" *ngIf="field.isDisabled" [class.disabled]="field.isLocked">
<a class="dropdown-item" (click)="enableField()" *ngIf="field.isDisabled" [class.disabled]="field.isLocked">
Enable
</a>
<a class="dropdown-item" (click)="disabling.emit()" *ngIf="!field.isDisabled" [class.disabled]="field.isLocked">
<a class="dropdown-item" (click)="disableField()" *ngIf="!field.isDisabled" [class.disabled]="field.isLocked">
Disable
</a>
<a class="dropdown-item" (click)="hiding.emit()" *ngIf="!field.isHidden" [class.disabled]="field.isLocked">
<a class="dropdown-item" (click)="hideField()" *ngIf="!field.isHidden" [class.disabled]="field.isLocked">
Hide
</a>
<a class="dropdown-item" (click)="showing.emit()" *ngIf="field.isHidden" [class.disabled]="field.isLocked">
<a class="dropdown-item" (click)="showField()" *ngIf="field.isHidden" [class.disabled]="field.isLocked">
Show
</a>
<a class="dropdown-item" *ngIf="!field.isLocked"
(sqxConfirmClick)="locking.emit()"
(sqxConfirmClick)="lockField()"
confirmTitle="Lock field"
confirmText="Do you really want to lock the field? Lock fields cannot be deleted or changed.">
Lock
</a>
<a class="dropdown-item dropdown-item-delete" [class.disabled]="field.isLocked"
(sqxConfirmClick)="deleting.emit()"
(sqxConfirmClick)="deleteField()"
confirmTitle="Delete field"
confirmText="Do you really want to delete the field?">
Delete
@ -59,7 +59,7 @@
</div>
<div class="table-items-row-details" *ngIf="isEditing">
<form [formGroup]="editForm" (ngSubmit)="save()" [attr.disabled]="field.isLocked">
<form [formGroup]="editForm.form" (ngSubmit)="save()">
<div class="table-items-row-details-tabs clearfix">
<ul class="nav nav-tabs2">
<li class="nav-item">
@ -80,124 +80,15 @@
</div>
<div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 0">
<div class="form-group row">
<label class="col col-3 col-form-label" for="fieldName">Name</label>
<div class="col col-6">
<input type="text" class="form-control" id="fieldName" readonly [ngModel]="field.name" [ngModelOptions]="{standalone: true}" />
<small class="form-text text-muted">
The name of the field in the API response.
</small>
</div>
</div>
<div class="form-group row">
<label class="col col-3 col-form-label" for="fieldLabel">Label</label>
<div class="col col-6">
<sqx-control-errors for="label" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="fieldLabel" maxlength="100" formControlName="label" />
<small class="form-text text-muted">
Define the display name for the field for documentation and user interfaces.
</small>
</div>
</div>
<div class="form-group row">
<label class="col col-3 col-form-label" for="fieldHints">Hints</label>
<div class="col col-6">
<sqx-control-errors for="hints" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="fieldHints" maxlength="100" formControlName="hints" />
<small class="form-text text-muted">
Define some hints for the user and editor for the field for documentation and user interfaces.
</small>
</div>
</div>
<div class="form-group row">
<div class="col col-9 offset-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="fieldListfield" formControlName="isListField" />
<label class="form-check-label" for="fieldListfield">
List Field
</label>
</div>
<small class="form-text text-muted">
List fields are shown as a column in the content list. If no list field is defined, the first field is shown by default.
</small>
</div>
</div>
<sqx-field-form-common [editForm]="editForm.form" [editFormSubmitted]="editForm.submitted | async" [field]="field"></sqx-field-form-common>
</div>
<div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 1">
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">
<sqx-number-validation [editForm]="editForm" [properties]="field.properties"></sqx-number-validation>
</div>
<div *ngSwitchCase="'String'">
<sqx-string-validation [editForm]="editForm" [properties]="field.properties" [regexSuggestions]="regexSuggestions"></sqx-string-validation>
</div>
<div *ngSwitchCase="'Boolean'">
<sqx-boolean-validation [editForm]="editForm" [properties]="field.properties"></sqx-boolean-validation>
</div>
<div *ngSwitchCase="'DateTime'">
<sqx-date-time-validation [editForm]="editForm" [properties]="field.properties"></sqx-date-time-validation>
</div>
<div *ngSwitchCase="'Geolocation'">
<sqx-geolocation-validation [editForm]="editForm" [properties]="field.properties"></sqx-geolocation-validation>
</div>
<div *ngSwitchCase="'Json'">
<sqx-json-validation [editForm]="editForm" [properties]="field.properties"></sqx-json-validation>
</div>
<div *ngSwitchCase="'Assets'">
<sqx-assets-validation [editForm]="editForm" [properties]="field.properties"></sqx-assets-validation>
</div>
<div *ngSwitchCase="'References'">
<sqx-references-validation [editForm]="editForm" [properties]="field.properties" [schemas]="schemas"></sqx-references-validation>
</div>
<div *ngSwitchCase="'Tags'">
<sqx-tags-validation [editForm]="editForm" [properties]="field.properties"></sqx-tags-validation>
</div>
</div>
<sqx-field-form-validation [patterns]="patterns" [editForm]="editForm.form" [field]="field"></sqx-field-form-validation>
</div>
<div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 2">
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">
<sqx-number-ui [editForm]="editForm" [properties]="field.properties"></sqx-number-ui>
</div>
<div *ngSwitchCase="'String'">
<sqx-string-ui [editForm]="editForm" [properties]="field.properties"></sqx-string-ui>
</div>
<div *ngSwitchCase="'Boolean'">
<sqx-boolean-ui [editForm]="editForm" [properties]="field.properties"></sqx-boolean-ui>
</div>
<div *ngSwitchCase="'DateTime'">
<sqx-date-time-ui [editForm]="editForm" [properties]="field.properties"></sqx-date-time-ui>
</div>
<div *ngSwitchCase="'Geolocation'">
<sqx-geolocation-ui [editForm]="editForm" [properties]="field.properties"></sqx-geolocation-ui>
</div>
<div *ngSwitchCase="'Json'">
<sqx-json-ui [editForm]="editForm" [properties]="field.properties"></sqx-json-ui>
</div>
<div *ngSwitchCase="'Assets'">
<sqx-assets-ui [editForm]="editForm" [properties]="field.properties"></sqx-assets-ui>
</div>
<div *ngSwitchCase="'References'">
<sqx-references-ui [editForm]="editForm" [properties]="field.properties"></sqx-references-ui>
</div>
<div *ngSwitchCase="'Tags'">
<sqx-tags-ui [editForm]="editForm" [properties]="field.properties"></sqx-tags-ui>
</div>
</div>
<sqx-field-form-ui [editForm]="editForm.form" [field]="field"></sqx-field-form-ui>
</div>
</form>
</div>

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

@ -5,18 +5,20 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import {
AppPatternDto,
createProperties,
EditFieldForm,
fadeAnimation,
FieldDto,
FieldPropertiesDto,
ModalView,
SchemaDto
} from 'shared';
SchemaDetailsDto,
SchemasState,
UpdateFieldDto
} from '@app/shared';
@Component({
selector: 'sqx-field',
@ -31,63 +33,31 @@ export class FieldComponent implements OnInit {
public field: FieldDto;
@Input()
public schemas: SchemaDto[];
public patterns: AppPatternDto;
@Input()
public regexSuggestions: AppPatternDto[] = [];
@Output()
public locking = new EventEmitter();
@Output()
public hiding = new EventEmitter();
@Output()
public showing = new EventEmitter();
@Output()
public saving = new EventEmitter<FieldPropertiesDto>();
@Output()
public enabling = new EventEmitter();
@Output()
public disabling = new EventEmitter();
@Output()
public deleting = new EventEmitter();
public schema: SchemaDetailsDto;
public dropdown = new ModalView(false, true);
public isEditing = false;
public selectedTab = 0;
public get displayName() {
return this.field.properties.label && this.field.properties.label.length > 0 ? this.field.properties.label : this.field.name;
}
public editFormSubmitted = false;
public editForm =
this.formBuilder.group({
label: ['',
[
Validators.maxLength(100)
]],
hints: ['',
[
Validators.maxLength(100)
]],
isRequired: [false],
isListField: [false]
});
public editForm: EditFieldForm;
constructor(
private readonly formBuilder: FormBuilder
private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState
) {
}
public ngOnInit() {
this.resetEditForm();
this.editForm = new EditFieldForm(this.formBuilder);
this.editForm.load(this.field.properties);
if (this.field.isLocked) {
this.editForm.form.disable();
}
}
public toggleEditing() {
@ -99,32 +69,46 @@ export class FieldComponent implements OnInit {
}
public cancel() {
this.resetEditForm();
this.editForm.load(this.field);
}
public save() {
this.editFormSubmitted = true;
public deleteField() {
this.schemasState.deleteField(this.schema, this.field).subscribe();
}
if (this.editForm.valid) {
const properties = createProperties(this.field.properties['fieldType'], this.editForm.value);
public enableField() {
this.schemasState.enableField(this.schema, this.field).subscribe();
}
this.emitSaving(properties);
}
public disableField() {
this.schemasState.disableField(this.schema, this.field).subscribe();
}
private emitSaving(properties: FieldPropertiesDto) {
this.saving.emit(properties);
public showField() {
this.schemasState.showField(this.schema, this.field).subscribe();
}
private resetEditForm() {
this.editFormSubmitted = false;
this.editForm.reset(this.field.properties);
public hideField() {
this.schemasState.hideField(this.schema, this.field).subscribe();
}
if (this.field.isLocked) {
this.editForm.disable();
}
public lockField() {
this.schemasState.lockField(this.schema, this.field).subscribe();
}
public save() {
const value = this.editForm.submit();
if (value) {
const properties = createProperties(this.field.properties['fieldType'], value);
this.isEditing = false;
this.schemasState.updateField(this.schema, this.field, new UpdateFieldDto(properties))
.subscribe(() => {
this.editForm.submitCompleted();
}, error => {
this.editForm.submitFailed(error);
});
}
}
}

56
src/Squidex/app/features/schemas/pages/schema/forms/field-form-common.component.html

@ -0,0 +1,56 @@
<div [formGroup]="editForm">
<div class="form-group row" *ngIf="showName">
<label class="col col-3 col-form-label" for="fieldName">Name</label>
<div class="col col-6">
<input type="text" class="form-control" id="fieldName" readonly [ngModel]="field.name" [ngModelOptions]="{standalone: true}" />
<small class="form-text text-muted">
The name of the field in the API response.
</small>
</div>
</div>
<div class="form-group row">
<label class="col col-3 col-form-label" for="fieldLabel">Label</label>
<div class="col col-6">
<sqx-control-errors for="label" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="fieldLabel" maxlength="100" formControlName="label" />
<small class="form-text text-muted">
Define the display name for the field for documentation and user interfaces.
</small>
</div>
</div>
<div class="form-group row">
<label class="col col-3 col-form-label" for="fieldHints">Hints</label>
<div class="col col-6">
<sqx-control-errors for="hints" [submitted]="editFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="fieldHints" maxlength="100" formControlName="hints" />
<small class="form-text text-muted">
Define some hints for the user and editor for the field for documentation and user interfaces.
</small>
</div>
</div>
<div class="form-group row">
<div class="col col-6 offset-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="fieldListfield" formControlName="isListField" />
<label class="form-check-label" for="fieldListfield">
List Field
</label>
</div>
<small class="form-text text-muted">
List fields are shown as a column in the content list.<br />When no list field is defined, the first field is used.
</small>
</div>
</div>
</div>

2
src/Squidex/app/features/schemas/pages/schema/forms/field-form-common.component.scss

@ -0,0 +1,2 @@
@import '_vars';
@import '_mixins';

31
src/Squidex/app/features/schemas/pages/schema/forms/field-form-common.component.ts

@ -0,0 +1,31 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FieldDto } from '@app/shared';
@Component({
selector: 'sqx-field-form-common',
styleUrls: ['field-form-common.component.scss'],
templateUrl: 'field-form-common.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FieldFormCommonComponent {
@Input()
public editForm: FormGroup;
@Input()
public editFormSubmitted = false;
@Input()
public showName = true;
@Input()
public field: FieldDto;
}

29
src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.html

@ -0,0 +1,29 @@
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">
<sqx-number-ui [editForm]="editForm" [properties]="field.properties"></sqx-number-ui>
</div>
<div *ngSwitchCase="'String'">
<sqx-string-ui [editForm]="editForm" [properties]="field.properties"></sqx-string-ui>
</div>
<div *ngSwitchCase="'Boolean'">
<sqx-boolean-ui [editForm]="editForm" [properties]="field.properties"></sqx-boolean-ui>
</div>
<div *ngSwitchCase="'DateTime'">
<sqx-date-time-ui [editForm]="editForm" [properties]="field.properties"></sqx-date-time-ui>
</div>
<div *ngSwitchCase="'Geolocation'">
<sqx-geolocation-ui [editForm]="editForm" [properties]="field.properties"></sqx-geolocation-ui>
</div>
<div *ngSwitchCase="'Json'">
<sqx-json-ui [editForm]="editForm" [properties]="field.properties"></sqx-json-ui>
</div>
<div *ngSwitchCase="'Assets'">
<sqx-assets-ui [editForm]="editForm" [properties]="field.properties"></sqx-assets-ui>
</div>
<div *ngSwitchCase="'References'">
<sqx-references-ui [editForm]="editForm" [properties]="field.properties"></sqx-references-ui>
</div>
<div *ngSwitchCase="'Tags'">
<sqx-tags-ui [editForm]="editForm" [properties]="field.properties"></sqx-tags-ui>
</div>
</div>

2
src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.scss

@ -0,0 +1,2 @@
@import '_vars';
@import '_mixins';

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save