Browse Source

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

Ui logic rework
pull/272/head
Sebastian Stehle 9 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 # 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 COPY src/Squidex/package.json /tmp/package.json

27
Dockerfile.build

@ -1,30 +1,7 @@
FROM microsoft/aspnetcore-build:2.0.0 FROM squidex/aspnetcore-build-phantomjs-chromium:2.0.3-jessie-fix1 as builder
# 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
COPY src/Squidex/package.json /tmp/package.json COPY src/Squidex/package.json /tmp/package.json
RUN cd /tmp \ RUN cd /tmp \
&& npm install \ && npm install \
&& npm rebuild node-sass && npm rebuild node-sass

28
libs/Dockerfile

@ -6,7 +6,7 @@ RUN apt-get update \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Install official PhantomJS release # Install official PhantomJS release
RUN set -x \ RUN set -x \
&& apt-get update \ && apt-get update \
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
@ -22,4 +22,28 @@ RUN set -x \
&& apt-get clean all \ && apt-get clean all \
&& rm -rf /tmp/* /var/lib/apt/lists/* && 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 public sealed class OrleansEventNotifier : IEventNotifier
{ {
private readonly IGrainFactory factory;
private readonly Lazy<IEventConsumerManagerGrain> eventConsumerManagerGrain; private readonly Lazy<IEventConsumerManagerGrain> eventConsumerManagerGrain;
public OrleansEventNotifier(IGrainFactory factory) public OrleansEventNotifier(IGrainFactory factory)

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

@ -65,12 +65,20 @@ module.exports = function (config) {
*/ */
singleRun: true, 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 * Run with chrome because phantom js does not provide all types, e.g. DragEvent
* *
* available browser launchers: https://npmjs.org/browse/keyword/karma-launcher * available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
*/ */
browsers: ['PhantomJS'] browsers: ['ChromeCustom']
}; };
config.set(_config); config.set(_config);

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

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

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

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

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

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

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

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

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

@ -5,8 +5,16 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * 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/event-consumers/event-consumers-page.component';
export * from './pages/users/user-page.component'; export * from './pages/users/user-page.component';
export * from './pages/users/users-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 { RouterModule, Routes } from '@angular/router';
import { import {
ResolveUserGuard, SqxSharedModule,
SqxFrameworkModule, SqxFrameworkModule
SqxSharedModule } from '@app/shared';
} from 'shared';
import { import {
AdministrationAreaComponent, AdministrationAreaComponent,
EventConsumersPageComponent, EventConsumersPageComponent,
EventConsumersService,
UnsetUserGuard,
UserMustExistGuard,
UserPageComponent, UserPageComponent,
UsersPageComponent UsersPageComponent,
UsersService,
UsersState
} from './declarations'; } from './declarations';
const routes: Routes = [ const routes: Routes = [
@ -39,14 +43,13 @@ const routes: Routes = [
children: [ children: [
{ {
path: 'new', path: 'new',
component: UserPageComponent component: UserPageComponent,
canActivate: [UnsetUserGuard]
}, },
{ {
path: ':userId', path: ':userId',
component: UserPageComponent, component: UserPageComponent,
resolve: { canActivate: [UserMustExistGuard]
user: ResolveUserGuard
}
} }
] ]
} }
@ -58,8 +61,8 @@ const routes: Routes = [
@NgModule({ @NgModule({
imports: [ imports: [
SqxFrameworkModule,
SqxSharedModule, SqxSharedModule,
SqxFrameworkModule,
RouterModule.forChild(routes) RouterModule.forChild(routes)
], ],
declarations: [ declarations: [
@ -67,6 +70,13 @@ const routes: Routes = [
EventConsumersPageComponent, EventConsumersPageComponent,
UserPageComponent, UserPageComponent,
UsersPageComponent UsersPageComponent
],
providers: [
EventConsumersService,
UnsetUserGuard,
UserMustExistGuard,
UsersService,
UsersState
] ]
}) })
export class SqxFeatureAdministrationModule { } 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-title message="Event Consumers"></sqx-title>
<sqx-panel theme="light" desiredWidth="50rem"> <sqx-panel theme="light" desiredWidth="50rem">
<div class="panel-header"> <ng-container title>
<div class="panel-title-row"> Consumers
<div class="float-right"> </ng-container>
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Event Consumers (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> <ng-container menu>
</div> <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> <sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
</div> </ng-container>
<a class="panel-close" sqxParentLink isLazyLoaded="true"> <ng-container content>
<i class="icon-close"></i> <table class="table table-items table-fixed">
</a> <thead>
</div> <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"> <tbody>
<div class="panel-content panel-content-scroll"> <ng-template ngFor let-eventConsumer [ngForOf]="eventConsumers">
<table class="table table-items table-fixed"> <tr [class.faulted]="eventConsumer.error && eventConsumer.error.length > 0">
<thead> <td class="auto-auto">
<tr> <span class="truncate">
<th class="cell-auto"> <i class="faulted-icon icon icon-bug" (click)="showError(eventConsumer)" [class.hidden]="!eventConsumer.error || eventConsumer.error.length === 0"></i>
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>
{{eventConsumer.name}} {{eventConsumer.name}}
</span> </span>
</td> </td>
<td class="cell-auto-right"> <td class="cell-auto-right">
<span>{{eventConsumer.position}}</span> <span>{{eventConsumer.position}}</span>
</td> </td>
<td class="cell-actions-lg"> <td class="cell-actions-lg">
<button class="btn btn-link" (click)="reset(eventConsumer)" *ngIf="!eventConsumer.isResetting" title="Reset Event Consumer"> <button class="btn btn-link" (click)="reset(eventConsumer)" *ngIf="!eventConsumer.isResetting" title="Reset Event Consumer">
<i class="icon icon-reset"></i> <i class="icon icon-reset"></i>
</button> </button>
<button class="btn btn-link" (click)="start(eventConsumer)" *ngIf="eventConsumer.isStopped" title="Start Event Consumer"> <button class="btn btn-link" (click)="start(eventConsumer)" *ngIf="eventConsumer.isStopped" title="Start Event Consumer">
<i class="icon icon-play"></i> <i class="icon icon-play"></i>
</button> </button>
<button class="btn btn-link" (click)="stop(eventConsumer)" *ngIf="!eventConsumer.isStopped" title="Stop Event Consumer"> <button class="btn btn-link" (click)="stop(eventConsumer)" *ngIf="!eventConsumer.isStopped" title="Stop Event Consumer">
<i class="icon icon-pause"></i> <i class="icon icon-pause"></i>
</button> </button>
</td> </td>
</tr> </tr>
<tr class="spacer"></tr> <tr class="spacer"></tr>
</ng-template> </ng-template>
</tbody> </tbody>
</table> </table>
</div> </ng-container>
</div>
</sqx-panel> </sqx-panel>
<div class="modal" *sqxModalView="eventConsumerErrorDialog;onRoot:true" @fade> <sqx-modal-dialog *sqxModalView="eventConsumerErrorDialog;onRoot:true" (close)="eventConsumerErrorDialog.hide()">
<div class="modal-backdrop"></div> <ng-container #title>
<div class="modal-dialog"> Error
<div class="modal-content"> </ng-container>
<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>
<div class="modal-body"> <ng-container #content>
<textarea readonly class="form-control error-message">{{eventConsumerError}}</textarea> <textarea readonly class="form-control error-message">{{eventConsumerError}}</textarea>
</div> </ng-container>
</div> </sqx-modal-dialog>
</div>
</div>

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 { Observable, Subscription } from 'rxjs';
import { import {
AppContext, DialogService,
EventConsumerDto,
EventConsumersService,
fadeAnimation,
ImmutableArray, ImmutableArray,
ModalView ModalView
} from 'shared'; } from '@app/shared';
import { EventConsumerDto, EventConsumersService } from './../../services/event-consumers.service';
@Component({ @Component({
selector: 'sqx-event-consumers-page', selector: 'sqx-event-consumers-page',
styleUrls: ['./event-consumers-page.component.scss'], styleUrls: ['./event-consumers-page.component.scss'],
templateUrl: './event-consumers-page.component.html', templateUrl: './event-consumers-page.component.html'
providers: [
AppContext
],
animations: [
fadeAnimation
]
}) })
export class EventConsumersPageComponent implements OnDestroy, OnInit { export class EventConsumersPageComponent implements OnDestroy, OnInit {
private subscription: Subscription; private subscription: Subscription;
@ -35,7 +28,8 @@ export class EventConsumersPageComponent implements OnDestroy, OnInit {
public eventConsumerError = ''; public eventConsumerError = '';
public eventConsumers = ImmutableArray.empty<EventConsumerDto>(); public eventConsumers = ImmutableArray.empty<EventConsumerDto>();
constructor(public readonly ctx: AppContext, constructor(
private readonly dialogs: DialogService,
private readonly eventConsumersService: EventConsumersService private readonly eventConsumersService: EventConsumersService
) { ) {
} }
@ -59,11 +53,11 @@ export class EventConsumersPageComponent implements OnDestroy, OnInit {
this.eventConsumers = ImmutableArray.of(dtos); this.eventConsumers = ImmutableArray.of(dtos);
if (showInfo) { if (showInfo) {
this.ctx.notifyInfo('Event Consumers reloaded.'); this.dialogs.notifyInfo('Event Consumers reloaded.');
} }
}, error => { }, error => {
if (showError) { if (showError) {
this.ctx.notifyError(error); this.dialogs.notifyError(error);
} }
}); });
} }
@ -71,27 +65,27 @@ export class EventConsumersPageComponent implements OnDestroy, OnInit {
public start(consumer: EventConsumerDto) { public start(consumer: EventConsumerDto) {
this.eventConsumersService.startEventConsumer(consumer.name) this.eventConsumersService.startEventConsumer(consumer.name)
.subscribe(() => { .subscribe(() => {
this.eventConsumers = this.eventConsumers.replaceBy('name', consumer.start()); this.eventConsumers = this.eventConsumers.replaceBy('name', start(consumer));
}, error => { }, error => {
this.ctx.notifyError(error); this.dialogs.notifyError(error);
}); });
} }
public stop(consumer: EventConsumerDto) { public stop(consumer: EventConsumerDto) {
this.eventConsumersService.stopEventConsumer(consumer.name) this.eventConsumersService.stopEventConsumer(consumer.name)
.subscribe(() => { .subscribe(() => {
this.eventConsumers = this.eventConsumers.replaceBy('name', consumer.stop()); this.eventConsumers = this.eventConsumers.replaceBy('name', stop(consumer));
}, error => { }, error => {
this.ctx.notifyError(error); this.dialogs.notifyError(error);
}); });
} }
public reset(consumer: EventConsumerDto) { public reset(consumer: EventConsumerDto) {
this.eventConsumersService.resetEventConsumer(consumer.name) this.eventConsumersService.resetEventConsumer(consumer.name)
.subscribe(() => { .subscribe(() => {
this.eventConsumers = this.eventConsumers.replaceBy('name', consumer.reset()); this.eventConsumers = this.eventConsumers.replaceBy('name', reset(consumer));
}, error => { }, 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> <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"/> <input style="display:none" type="password" name="foilautofill"/>
<sqx-panel desiredWidth="26rem"> <sqx-panel desiredWidth="26rem" isBlank="true">
<div class="panel-header"> <ng-container title>
<div class="panel-title-row"> <ng-container *ngIf="usersState.selectedUser | async; else noUser">
<div class="float-right"> Edit User
<button type="submit" class="btn btn-primary" title="CTRL + S"> </ng-container>
Save
</button> <ng-template #noUser>
</div> New User
</ng-template>
<sqx-shortcut keys="ctrl+s" (trigger)="save()"></sqx-shortcut> </ng-container>
<h3 class="panel-title" *ngIf="isNewMode"> <ng-container menu>
New User <button type="submit" class="btn btn-primary" title="CTRL + S">
</h3> Save
<h3 class="panel-title" *ngIf="!isNewMode"> </button>
Edit User
</h3>
</div>
<sqx-shortcut keys="ctrl+s" (trigger)="save()"></sqx-shortcut> <sqx-shortcut keys="ctrl+s" (trigger)="save()"></sqx-shortcut>
</ng-container>
<a class="panel-close" sqxParentLink> <ng-container content>
<i class="icon-close"></i> <sqx-form-error [error]="userForm.error | async"></sqx-form-error>
</a>
</div>
<div class="panel-main"> <div class="form-group">
<div class="panel-content panel-content-blank"> <label for="email">Email</label>
<div *ngIf="userFormError">
<div class="form-alert form-alert-error" [innerHTML]="userFormError"></div>
</div>
<div class="form-group"> <sqx-control-errors for="email" [submitted]="userForm.submitted | async"></sqx-control-errors>
<label for="email">Email</label>
<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>
<input type="email" class="form-control" id="email" maxlength="100" formControlName="email" autocomplete="false" /> <div class="form-group">
</div> <label for="displayName">Display Name</label>
<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" /> <input type="text" class="form-control" id="displayName" maxlength="100" formControlName="displayName" autocomplete="false" />
</div> </div>
<div class="form-group form-group-password" [class.hidden]="user.id === ctx.userId"> <div class="form-group form-group-password" [class.hidden]="usersState.isCurrentUser | async">
<div class="form-group"> <div class="form-group">
<label for="password">Password</label> <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" /> <input type="password" class="form-control" id="password" maxlength="100" formControlName="password" autocomplete="false" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">Confirm Password</label> <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" /> <input type="password" class="form-control" id="passwordConfirm" maxlength="100" formControlName="passwordConfirm" autocomplete="false" />
</div>
</div> </div>
</div> </div>
</div> </ng-container>
</sqx-panel> </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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { import { UserDto } from './../../services/users.service';
AppContext, import { UserForm, UsersState } from './../../state/users.state';
UserDto,
UserManagementService,
ValidatorsEx
} from 'shared';
import { UserCreated, UserUpdated } from './../messages';
@Component({ @Component({
selector: 'sqx-user-page', selector: 'sqx-user-page',
styleUrls: ['./user-page.component.scss'], styleUrls: ['./user-page.component.scss'],
templateUrl: './user-page.component.html', templateUrl: './user-page.component.html'
providers: [
AppContext
]
}) })
export class UserPageComponent implements OnInit { export class UserPageComponent implements OnDestroy, OnInit {
public user: UserDto; private selectedUserSubscription: Subscription;
private user?: UserDto;
public userFormSubmitted = false;
public userForm: FormGroup;
public userFormError = '';
public isCurrentUser = false; public userForm: UserForm;
public isNewMode = false;
constructor(public readonly ctx: AppContext, constructor(formBuilder: FormBuilder,
private readonly formBuilder: FormBuilder, public readonly usersState: UsersState,
private readonly router: Router, private readonly route: ActivatedRoute,
private readonly userManagementService: UserManagementService private readonly router: Router
) { ) {
this.userForm = new UserForm(formBuilder);
} }
public ngOnInit() { public ngOnDestroy() {
this.ctx.route.data.map(d => d.user) this.selectedUserSubscription.unsubscribe();
.subscribe((user: UserDto) => { }
this.user = user;
this.setupAndPopulateForm(); public ngOnInit() {
}); this.selectedUserSubscription =
this.usersState.selectedUser
.subscribe(user => {
this.user = user;
this.userForm.load(user);
});
} }
public save() { public save() {
this.userFormSubmitted = true; const value = this.userForm.submit();
if (this.userForm.valid) {
this.userForm.disable();
const requestDto = this.userForm.value;
if (this.isNewMode) { if (value) {
this.userManagementService.postUser(requestDto) if (this.user) {
.subscribe(created => { this.usersState.updateUser(this.user, value)
this.user = .subscribe(user => {
new UserDto( this.userForm.submitCompleted();
created.id,
requestDto.email,
requestDto.displayName,
false);
this.ctx.notifyInfo('User created successfully.');
this.emitUserCreated(this.user);
this.back();
}, error => { }, error => {
this.resetUserForm(error.displayMessage); this.userForm.submitFailed(error);
}); });
} else { } else {
this.userManagementService.putUser(this.user.id, requestDto) this.usersState.createUser(value)
.subscribe(() => { .subscribe(user => {
this.user = this.back();
this.user.update(
requestDto.email,
requestDto.displayMessage);
this.ctx.notifyInfo('User saved successfully.');
this.emitUserUpdated(this.user);
this.resetUserForm();
}, error => { }, error => {
this.resetUserForm(error.displayMessage); this.userForm.submitFailed(error);
}); });
} }
} }
} }
private back() { private back() {
this.router.navigate(['../'], { relativeTo: this.ctx.route, replaceUrl: true }); this.router.navigate(['../'], { relativeTo: this.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;
} }
} }

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

@ -1,107 +1,90 @@
<sqx-title message="User Management"></sqx-title> <sqx-title message="User Management"></sqx-title>
<sqx-panel desiredWidth="50rem"> <sqx-panel desiredWidth="50rem" contentClass="grid">
<div class="panel-header"> <ng-container title>
<div class="panel-title-row"> Users
<div class="float-right"> </ng-container>
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Users (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> <ng-container menu>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> <button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Users (CTRL + SHIFT + R)">
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut> <i class="icon-reset"></i> Refresh
</button>
<form class="form-inline" (ngSubmit)="search()"> <sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
<input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" /> <sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
</form> <sqx-shortcut keys="ctrl+shift+n" (trigger)="buttonNew.click()"></sqx-shortcut>
<button class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + M)"> <form class="form-inline" (ngSubmit)="search()">
<i class="icon-plus"></i> New <input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" />
</button> </form>
</div>
<h3 class="panel-title">Users</h3>
</div>
<a class="panel-close" sqxParentLink isLazyLoaded="true"> <button class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + N)">
<i class="icon-close"></i> <i class="icon-plus"></i> New
</a> </button>
</div> </ng-container>
<div class="panel-main"> <ng-container content>
<div class="panel-content grid"> <div class="grid-header">
<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"> <table class="table table-items table-fixed">
<thead> <tbody>
<tr> <ng-template ngFor let-user [ngForOf]="usersState.users | async" [ngForTrackBy]="trackByUser">
<th class="cell-user"> <tr [routerLink]="user.id" routerLinkActive="active">
&nbsp; <td class="cell-user">
</th> <img class="user-picture" [attr.title]="user.name" [attr.src]="user | sqxUserDtoPicture" />
<th class="cell-auto"> </td>
Name <td class="cell-auto">
</th> <span class="user-name table-cell">{{user.displayName}}</span>
<th class="cell-auto"> </td>
Email <td class="cell-auto">
</th> <span class="user-email table-cell">{{user.email}}</span>
<th class="cell-actions"> </td>
Actions <td class="cell-actions">
</th> <ng-container *ngIf="user.id !== authState.user?.id">
</tr> <button class="btn btn-link" (click)="lock(user); $event.stopPropagation();" *ngIf="!user.isLocked" title="Lock User">
</thead> <i class="icon icon-unlocked"></i>
</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;
</button> </button>
</td> <button class="btn btn-link" (click)="unlock(user); $event.stopPropagation();" *ngIf="user.isLocked" title="Unlock User">
</tr> <i class="icon icon-lock"></i>
<tr class="spacer"></tr> </button>
</ng-template> </ng-container>
</tbody> <button *ngIf="user.id === authState.user?.id" class="btn btn-link invisible">
</table> &nbsp;
</div> </button>
</td>
</tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
</table>
</div> </div>
</div>
<div class="grid-footer clearfix" *ngIf="usersPager.numberOfItems > 0"> <div class="grid-footer">
<div class="float-right pagination"> <sqx-pager [pager]="usersState.usersPager | async" (prev)="goPrev()" (next)="goNext()"></sqx-pager>
<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> </div>
</div> </ng-container>
</sqx-panel> </sqx-panel>
<router-outlet></router-outlet> <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. * 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 { FormControl } from '@angular/forms';
import { Subscription } from 'rxjs';
import { import { AuthService } from '@app/shared';
AppContext,
ImmutableArray,
Pager,
UserDto,
UserManagementService
} from 'shared';
import { UserCreated, UserUpdated } from './../messages'; import { UserDto } from './../../services/users.service';
import { UsersState } from './../../state/users.state';
@Component({ @Component({
selector: 'sqx-users-page', selector: 'sqx-users-page',
styleUrls: ['./users-page.component.scss'], styleUrls: ['./users-page.component.scss'],
templateUrl: './users-page.component.html', templateUrl: './users-page.component.html',
providers: [ changeDetection: ChangeDetectionStrategy.OnPush
AppContext
]
}) })
export class UsersPageComponent implements OnDestroy, OnInit { export class UsersPageComponent implements OnInit {
private userCreatedSubscription: Subscription;
private userUpdatedSubscription: Subscription;
public usersItems = ImmutableArray.empty<UserDto>();
public usersPager = new Pager(0);
public usersFilter = new FormControl(); public usersFilter = new FormControl();
public usersQuery = '';
constructor(public readonly ctx: AppContext, constructor(
private readonly userManagementService: UserManagementService public readonly authState: AuthService,
public readonly usersState: UsersState
) { ) {
} }
public ngOnDestroy() {
this.userCreatedSubscription.unsubscribe();
this.userUpdatedSubscription.unsubscribe();
}
public ngOnInit() { 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(); this.load();
} }
public search() { public search() {
this.usersPager = new Pager(0); this.usersState.search(this.usersFilter.value).subscribe();
this.usersQuery = this.usersFilter.value;
this.load();
} }
public load(showInfo = false) { public load(notify = false) {
this.userManagementService.getUsers(this.usersPager.pageSize, this.usersPager.skip, this.usersQuery) this.usersState.loadUsers(notify).subscribe();
.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 lock(user: UserDto) { public lock(user: UserDto) {
this.userManagementService.lockUser(user.id) this.usersState.lockUser(user).subscribe();
.subscribe(() => {
this.usersItems = this.usersItems.replaceBy('id', user.lock());
}, error => {
this.ctx.notifyError(error);
});
} }
public unlock(user: UserDto) { public unlock(user: UserDto) {
this.userManagementService.unlockUser(user.id) this.usersState.unlockUser(user).subscribe();
.subscribe(() => {
this.usersItems = this.usersItems.replaceBy('id', user.unlock());
}, error => {
this.ctx.notifyError(error);
});
} }
public goNext() { public goPrev() {
this.usersPager = this.usersPager.goNext(); this.usersState.goPrev().subscribe();
this.load();
} }
public goPrev() { public goNext() {
this.usersPager = this.usersPager.goPrev(); 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 { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing'; import { inject, TestBed } from '@angular/core/testing';
import { import { ApiUrlConfig } from '@app/framework';
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();
expect(consumer_2.isResetting).toBeTruthy(); import { EventConsumerDto, EventConsumersService } from './event-consumers.service';
});
});
describe('EventConsumersService', () => { describe('EventConsumersService', () => {
beforeEach(() => { 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 { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; 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 { export class EventConsumerDto {
constructor( constructor(
@ -22,18 +22,6 @@ export class EventConsumerDto {
public readonly position: string 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() @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"> <sqx-panel theme="dark" desiredWidth="12rem">
<div class="panel-header"> <ng-container title>
<div class="panel-title-row"> API
<h3 class="panel-title">API</h3> </ng-container>
</div>
<a class="panel-close" sqxParentLink isLazyLoaded="true"> <ng-container content>
<i class="icon-close"></i> <ul class="nav nav-panel nav-dark flex-column">
</a> <li class="nav-item">
</div> <a class="nav-link" routerLink="graphql" routerLinkActive="active">
GraphQL
<div class="panel-main"> <i class="icon-angle-right"></i>
<div class="panel-content"> </a>
<ul class="nav nav-panel nav-dark flex-column"> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="graphql" routerLinkActive="active"> <a class="nav-link" href="/api/content/{{appsState.appName}}/docs" target="_blank">
GraphQL Swagger
<i class="icon-angle-right"></i> </a>
</a> </li>
</li> </ul>
<li class="nav-item"> </ng-container>
<a class="nav-link" href="/api/content/{{ctx.appName}}/docs" target="_blank">
Swagger
</a>
</li>
</ul>
</div>
</div>
</sqx-panel> </sqx-panel>
<router-outlet></router-outlet> <router-outlet></router-outlet>

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

@ -7,19 +7,16 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { AppContext } from 'shared'; import { AppsState } from '@app/shared';
@Component({ @Component({
selector: 'sqx-api-area', selector: 'sqx-api-area',
styleUrls: ['./api-area.component.scss'], styleUrls: ['./api-area.component.scss'],
templateUrl: './api-area.component.html', templateUrl: './api-area.component.html'
providers: [
AppContext
]
}) })
export class ApiAreaComponent { export class ApiAreaComponent {
constructor( 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 { import {
SqxFrameworkModule, SqxFrameworkModule,
SqxSharedModule SqxSharedModule
} from 'shared'; } from '@app/shared';
import { import {
ApiAreaComponent, 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="*"> <sqx-panel desiredWidth="*" isFullSize="true">
<div #graphiQLContainer></div> <div inner #graphiQLContainer></div>
</sqx-panel> </sqx-panel>

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

@ -4,11 +4,13 @@
@import '~graphiql/graphiql'; @import '~graphiql/graphiql';
.graphiql-container { .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 // sass-lint:disable class-name-format
& .editorWrap { & .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 */ /* tslint:disable:use-view-encapsulation */
import { import {
AppContext, AppsState,
GraphQlService, GraphQlService,
LocalStoreService LocalStoreService
} from 'shared'; } from '@app/shared';
@Component({ @Component({
selector: 'sqx-graphql-page', selector: 'sqx-graphql-page',
styleUrls: ['./graphql-page.component.scss'], styleUrls: ['./graphql-page.component.scss'],
templateUrl: './graphql-page.component.html', templateUrl: './graphql-page.component.html',
providers: [
AppContext
],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class GraphQLPageComponent implements OnInit { export class GraphQLPageComponent implements OnInit {
@ViewChild('graphiQLContainer') @ViewChild('graphiQLContainer')
public graphiQLContainer: ElementRef; public graphiQLContainer: ElementRef;
constructor(public readonly ctx: AppContext, constructor(
public readonly appsState: AppsState,
private readonly graphQlService: GraphQlService, private readonly graphQlService: GraphQlService,
private readonly localStoreService: LocalStoreService private readonly localStoreService: LocalStoreService
) { ) {
@ -56,7 +54,7 @@ export class GraphQLPageComponent implements OnInit {
} }
private request(params: any) { 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 { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { SqxFrameworkModule, SqxSharedModule } from 'shared'; import { SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import { import {
AppsPageComponent, AppsPageComponent,

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

@ -1,7 +1,7 @@
<sqx-title message="Apps"></sqx-title> <sqx-title message="Apps"></sqx-title>
<div class="apps-section"> <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"> <div class="subtext">
Welcome to Squidex. Welcome to Squidex.
@ -49,7 +49,9 @@
<div class="card-text"> <div class="card-text">
<div>Start with our ready to use blog.</div> <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> </div>
</div> </div>
@ -64,34 +66,18 @@
<div class="card-text"> <div class="card-text">
<div>Create your profile page.</div> <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> </div>
</div> </div>
<div class="modal" *sqxModalView="addAppDialog;onRoot:true" @fade> <ng-container *sqxModalView="addAppDialog;onRoot:true">
<div class="modal-backdrop"></div> <sqx-app-form [template]="appTemplate"
<div class="modal-dialog"> (completed)="addAppDialog.hide()">
<div class="modal-content"> </sqx-app-form>
<div class="modal-header"> </ng-container>
<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>
<sqx-onboarding-dialog [modalView]="onboardingModal"></sqx-onboarding-dialog> <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 { Subscription } from 'rxjs';
import { import {
AppContext,
AppDto, AppDto,
AppsStoreService, AppsState,
fadeAnimation, AuthService,
ImmutableArray,
ModalView, ModalView,
OnboardingService OnboardingService
} from 'shared'; } from '@app/shared';
@Component({ @Component({
selector: 'sqx-apps-page', selector: 'sqx-apps-page',
styleUrls: ['./apps-page.component.scss'], styleUrls: ['./apps-page.component.scss'],
templateUrl: './apps-page.component.html', templateUrl: './apps-page.component.html'
providers: [
AppContext
],
animations: [
fadeAnimation
]
}) })
export class AppsPageComponent implements OnDestroy, OnInit { export class AppsPageComponent implements OnDestroy, OnInit {
private appsSubscription: Subscription; private appsSubscription: Subscription;
public addAppDialog = new ModalView(); public addAppDialog = new ModalView();
public apps: AppDto[];
public template = ''; public apps: ImmutableArray<AppDto>;
public appTemplate = '';
public onboardingModal = new ModalView(); public onboardingModal = new ModalView();
constructor( constructor(
public readonly ctx: AppContext, public readonly appsState: AppsState,
private readonly appsStore: AppsStoreService, public readonly authState: AuthService,
private readonly onboardingService: OnboardingService private readonly onboardingService: OnboardingService
) { ) {
} }
@ -51,7 +45,7 @@ export class AppsPageComponent implements OnDestroy, OnInit {
public ngOnInit() { public ngOnInit() {
this.appsSubscription = this.appsSubscription =
this.appsStore.apps this.appsState.apps
.subscribe(apps => { .subscribe(apps => {
if (apps.length === 0 && this.onboardingService.shouldShow('dialog')) { if (apps.length === 0 && this.onboardingService.shouldShow('dialog')) {
this.onboardingService.disable('dialog'); this.onboardingService.disable('dialog');
@ -63,7 +57,7 @@ export class AppsPageComponent implements OnDestroy, OnInit {
} }
public createNewApp(template: string) { public createNewApp(template: string) {
this.template = template; this.appTemplate = template;
this.addAppDialog.show(); 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> <ng-container *sqxModalView="modalView;onRoot:true;closeAuto:false">
<div class="modal-backdrop"></div> <sqx-modal-dialog showHeader="false">
<div class="modal-dialog"> <ng-container content>
<div class="modal-content">
<a class="header-right modal-close" (click)="modalView.hide()">Skip Tour</a> <a class="header-right modal-close" (click)="modalView.hide()">Skip Tour</a>
<div class="onboarding-step" *ngIf="step === 0"> <div class="onboarding-step" *ngIf="step === 0">
@ -145,6 +144,6 @@
</div> </div>
</div> </div>
</div> </div>
</div> </ng-container>
</div> </sqx-modal-dialog>
</div> </ng-container>

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

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

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

@ -7,9 +7,8 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { DndModule } from 'ng2-dnd';
import { SqxFrameworkModule, SqxSharedModule } from 'shared'; import { SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import { import {
AssetsPageComponent AssetsPageComponent
@ -26,7 +25,6 @@ const routes: Routes = [
imports: [ imports: [
SqxFrameworkModule, SqxFrameworkModule,
SqxSharedModule, SqxSharedModule,
DndModule,
RouterModule.forChild(routes) RouterModule.forChild(routes)
], ],
declarations: [ 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-title message="{app} | Assets" parameter1="app" [value1]="appsState.appName"></sqx-title>
<sqx-panel desiredWidth="60rem"> <sqx-panel desiredWidth="*">
<div class="panel-header"> <ng-container title>
<div class="panel-title-row"> Assets
<div class="float-right"> </ng-container>
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh <ng-container menu>
</button> <button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> </button>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
<form class="form-inline" (ngSubmit)="search()"> <sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<input class="form-control" #inputFind [formControl]="assetsFilter" placeholder="Search for assets" />
</form> <form class="form-inline" (ngSubmit)="search()">
</div> <input class="form-control" #inputFind [formControl]="assetsFilter" placeholder="Search for assets" />
</form>
<h3 class="panel-title">Assets</h3> </ng-container>
</div>
<ng-container content>
<a class="panel-close" sqxParentLink isLazyLoaded="true"> <sqx-assets-list [state]="assetsState"></sqx-assets-list>
<i class="icon-close"></i> </ng-container>
</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-panel> </sqx-panel>

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

@ -1,50 +1,2 @@
@import '_vars'; @import '_vars';
@import '_mixins'; @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;
}

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

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

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

@ -16,7 +16,7 @@ import {
ResolvePublishedSchemaGuard, ResolvePublishedSchemaGuard,
SqxFrameworkModule, SqxFrameworkModule,
SqxSharedModule SqxSharedModule
} from 'shared'; } from '@app/shared';
import { import {
AssetsEditorComponent, AssetsEditorComponent,
@ -50,10 +50,6 @@ const routes: Routes = [
component: ContentPageComponent, component: ContentPageComponent,
canDeactivate: [CanDeactivateGuard], canDeactivate: [CanDeactivateGuard],
children: [ children: [
{
path: 'assets',
loadChildren: './../assets/module#SqxFeatureAssetsModule'
},
{ {
path: 'references/:schemaName/:language', path: 'references/:schemaName/:language',
component: ContentsPageComponent, component: ContentsPageComponent,
@ -90,10 +86,6 @@ const routes: Routes = [
resolve: { resolve: {
schema: ResolvePublishedSchemaGuard 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> <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> <div class="languages-buttons" #buttonLanguages>
<sqx-language-selector size="sm" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages"></sqx-language-selector> <sqx-language-selector size="sm" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages"></sqx-language-selector>
</div> </div>
@ -13,7 +13,7 @@
<sqx-onboarding-tooltip id="languages" [for]="buttonLanguages" position="topRight" after="120000"> <sqx-onboarding-tooltip id="languages" [for]="buttonLanguages" position="topRight" after="120000">
Please remember to check all languages when you see validation errors. Please remember to check all languages when you see validation errors.
</sqx-onboarding-tooltip> </sqx-onboarding-tooltip>
</div> </ng-container>
<sqx-control-errors [for]="selectedFormControl" [fieldName]="field.displayName" [submitted]="contentFormSubmitted"></sqx-control-errors> <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> <textarea class="form-control" [id]="selectedFormName"[formControl]="selectedFormControl" rows="5" [placeholder]="field.displayPlaceholder"></textarea>
</div> </div>
<div *ngSwitchCase="'RichText'"> <div *ngSwitchCase="'RichText'">
<sqx-rich-editor [formControl]="selectedFormControl" (assetPluginClicked)="assetPluginClicked()"></sqx-rich-editor> <sqx-rich-editor [formControl]="selectedFormControl"></sqx-rich-editor>
</div> </div>
<div *ngSwitchCase="'Markdown'"> <div *ngSwitchCase="'Markdown'">
<sqx-markdown-editor [formControl]="selectedFormControl" (assetPluginClicked)="assetPluginClicked()"></sqx-markdown-editor> <sqx-markdown-editor [formControl]="selectedFormControl"></sqx-markdown-editor>
</div> </div>
<div *ngSwitchCase="'Dropdown'"> <div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControl]="selectedFormControl"> <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 { Component, Input, OnInit } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms'; import { AbstractControl, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { import {
AppLanguageDto, AppLanguageDto,
FieldDto, FieldDto,
fieldInvariant fieldInvariant
} from 'shared'; } from '@app/shared';
@Component({ @Component({
selector: 'sqx-content-field', selector: 'sqx-content-field',
@ -36,12 +35,6 @@ export class ContentFieldComponent implements OnInit {
public selectedFormControl: AbstractControl; public selectedFormControl: AbstractControl;
public selectedLanguage: AppLanguageDto; public selectedLanguage: AppLanguageDto;
constructor(
private readonly router: Router,
private readonly route: ActivatedRoute
) {
}
public ngOnInit() { public ngOnInit() {
const masterLanguage = this.languages[0]; const masterLanguage = this.languages[0];
@ -60,9 +53,5 @@ export class ContentFieldComponent implements OnInit {
this.selectedFormControl = this.fieldForm.controls[language.iso2Code]; this.selectedFormControl = this.fieldForm.controls[language.iso2Code];
this.selectedLanguage = language; 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"> <sqx-panel desiredWidth="16rem" isBlank="true">
<div class="panel-header"> <ng-container title>
<div class="panel-title-row"> Activity
<h3 class="panel-title">Activity</h3> </ng-container>
</div>
<a class="panel-close" sqxParentLink>
<i class="icon-close"></i>
</a>
</div>
<div class="panel-main"> <ng-container content>
<div class="panel-content panel-content-blank"> <div *ngFor="let event of events | async" class="event">
<div *ngFor="let event of events | async" class="event"> <div class="event-left">
<div class="event-left"> <img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:null" [attr.src]="event.actor | sqxUserPictureRef" />
<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>
<div class="event-main"> <div class="event-created">{{event.created | sqxFromNow}}</div>
<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>
<a class="event-load" (click)="loadVersion(event.version)">Load this Version</a> <a class="event-load" (click)="loadVersion(event.version)">Load this Version</a>
</div> </div>
</div>
</div> </div>
</div> </ng-container>
</sqx-panel> </sqx-panel>

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

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

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

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

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

@ -29,7 +29,7 @@ import {
Pager, Pager,
SchemaDetailsDto, SchemaDetailsDto,
Versioned Versioned
} from 'shared'; } from '@app/shared';
@Component({ @Component({
selector: 'sqx-contents-page', selector: 'sqx-contents-page',
@ -361,7 +361,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
this.ctx.bus.emit(new ContentRemoved(content)); this.ctx.bus.emit(new ContentRemoved(content));
} }
public trackBy(content: ContentDto): string { public trackByContent(content: ContentDto): string {
return content.id; 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. * 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'; import { FormBuilder } from '@angular/forms';
@Component({ @Component({
selector: 'sqx-search-form', selector: 'sqx-search-form',
styleUrls: ['./search-form.component.scss'], styleUrls: ['./search-form.component.scss'],
templateUrl: './search-form.component.html' templateUrl: './search-form.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SearchFormComponent implements OnChanges { export class SearchFormComponent implements OnChanges {
private queryValue = ''; private queryValue = '';

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

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ContentDto } from 'shared'; import { ContentDto } from '@app/shared';
export class ContentCreated { export class ContentCreated {
constructor( 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-title message="{app} | Schemas" parameter1="app" [value1]="ctx.appName"></sqx-title>
<sqx-panel theme="dark" desiredWidth="16rem"> <sqx-panel theme="dark" desiredWidth="16rem" showSecondHeader="true">
<div class="panel-header"> <ng-container title>
<div class="panel-title-row"> Schemas
<h3 class="panel-title">Schemas</h3> </ng-container>
</div>
<a class="panel-close" sqxParentLink isLazyLoaded="true">
<i class="icon-close"></i>
</a>
<div class="panel-header-row"> <ng-container secondHeader>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<div class="search-form"> <div class="search-form">
<input class="form-control form-control-dark" #inputFind [formControl]="schemasFilter" placeholder="Search for schemas" /> <input class="form-control form-control-dark" #inputFind [formControl]="schemasFilter" placeholder="Search for schemas" />
<i class="icon-search"></i> <i class="icon-search"></i>
</div>
</div> </div>
</div> </ng-container>
<div class="panel-main"> <ng-container content>
<div class="panel-content"> <ul class="nav nav-panel nav-dark nav-dark-bordered flex-column">
<ul class="nav nav-panel nav-dark nav-dark-bordered flex-column"> <li class="nav-item" *ngFor="let schema of schemasFiltered | async">
<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>
<a class="nav-link" [routerLink]="schema.name" routerLinkActive="active">{{schema.displayName}} <i class="icon-angle-right"></i></a> </li>
</li> </ul>
</ul> </ng-container>
</div>
</div>
</sqx-panel> </sqx-panel>
<router-outlet></router-outlet> <router-outlet></router-outlet>

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

@ -13,7 +13,7 @@ import {
AppContext, AppContext,
SchemaDto, SchemaDto,
SchemasService SchemasService
} from 'shared'; } from '@app/shared';
@Component({ @Component({
selector: 'sqx-schemas-page', 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="assets-container" [class.disabled]="isDisabled">
<div class="row"> <div class="row">
<div class="col-4 drop-area-container"> <div class="col-4 drop-area-container">
<div class="drop-area" dnd-droppable (onDropSuccess)="onAssetDropped($event.dragData)" [allowDrop]="canDrop()" (sqxFileDrop)="addFiles($event)" routerLink="assets"> <div class="drop-area" (sqxFileDrop)="addFiles($event)" (click)="selectorModal.show()">
Drop files or assets here to add them. Drop files here to add them.
</div> </div>
</div> </div>
@ -10,9 +10,12 @@
(failed)="onAssetFailed(file)" (failed)="onAssetFailed(file)"
(loaded)="onAssetLoaded(file, $event)"> (loaded)="onAssetLoaded(file, $event)">
</sqx-asset> </sqx-asset>
<sqx-asset class="col-4" *ngFor="let asset of oldAssets" [asset]="asset" [closeMode]="true" <sqx-asset class="col-4" *ngFor="let asset of oldAssets" [asset]="asset" removeMode="true" isDisabled="true"
(closing)="onAssetRemoving($event)" (removing)="onAssetRemoving($event)">
(updated)="onAssetUpdated($event)">
</sqx-asset> </sqx-asset>
</div> </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-size: 1.2rem;
font-weight: normal; font-weight: normal;
text-align: center; text-align: center;
padding: 3rem 2rem; padding: 3.5rem 2rem;
color: darken($color-border, 30%); 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 // 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 { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subscription } from 'rxjs';
import { import {
AppContext, AppsState,
AssetDto, AssetDto,
AssetsService, AssetsService,
AssetUpdated, ModalView,
ImmutableArray, ImmutableArray,
Types Types
} from 'shared'; } from '@app/shared';
export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = { export const SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AssetsEditorComponent), multi: true 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'], styleUrls: ['./assets-editor.component.scss'],
templateUrl: './assets-editor.component.html', templateUrl: './assets-editor.component.html',
providers: [ providers: [
AppContext,
SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR
] ]
}) })
export class AssetsEditorComponent implements ControlValueAccessor, OnDestroy, OnInit { export class AssetsEditorComponent implements ControlValueAccessor {
private assetUpdatedSubscription: Subscription;
private callChange = (v: any) => { /* NOOP */ }; private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ }; private callTouched = () => { /* NOOP */ };
public selectorModal = new ModalView();
public newAssets = ImmutableArray.empty<File>(); public newAssets = ImmutableArray.empty<File>();
public oldAssets = ImmutableArray.empty<AssetDto>(); public oldAssets = ImmutableArray.empty<AssetDto>();
public isDisabled = false; public isDisabled = false;
constructor(public readonly ctx: AppContext, constructor(
private readonly appsState: AppsState,
private readonly assetsService: AssetsService 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[]) { public writeValue(value: string[]) {
this.oldAssets = ImmutableArray.empty<AssetDto>(); this.oldAssets = ImmutableArray.empty<AssetDto>();
if (Types.isArrayOfString(value) && value.length > 0) { if (Types.isArrayOfString(value) && value.length > 0) {
const assetIds: string[] = value; 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 => { .subscribe(dtos => {
this.oldAssets = ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)).filter(a => !!a).map(a => a!)); 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() { public onAssetsSelected(assets: AssetDto[]) {
const component = this; for (let asset of assets) {
this.oldAssets = this.oldAssets.push(asset);
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);
if (assets.length > 0) {
this.updateValue(); this.updateValue();
} }
this.selectorModal.hide();
} }
public onAssetRemoving(asset: AssetDto) { public onAssetRemoving(asset: AssetDto) {
@ -124,10 +106,6 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnDestroy, O
this.updateValue(); this.updateValue();
} }
public onAssetUpdated(asset: AssetDto) {
this.ctx.bus.emit(new AssetUpdated(asset, this));
}
public onAssetFailed(file: File) { public onAssetFailed(file: File) {
this.newAssets = this.newAssets.remove(file); this.newAssets = this.newAssets.remove(file);
} }

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

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

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

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

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

@ -21,7 +21,7 @@ import {
SchemaDetailsDto, SchemaDetailsDto,
SchemasService, SchemasService,
Types Types
} from 'shared'; } from '@app/shared';
export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = { export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ReferencesEditorComponent), multi: true 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 { RouterModule, Routes } from '@angular/router';
import { ChartModule } from 'angular2-chartjs'; import { ChartModule } from 'angular2-chartjs';
import { SqxFrameworkModule, SqxSharedModule } from 'shared'; import { SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import { import {
DashboardPageComponent DashboardPageComponent

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

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

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

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

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

@ -13,7 +13,7 @@ import {
Pager, Pager,
RuleEventDto, RuleEventDto,
RulesService RulesService
} from 'shared'; } from '@app/shared';
@Component({ @Component({
selector: 'sqx-rule-events-page', 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()"> <form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row"> <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: ['', appId: ['',
[ [
Validators.required Validators.required
]], ]
],
apiKey: ['', apiKey: ['',
[ [
Validators.required Validators.required
]], ]
],
indexName: ['$SCHEMA_NAME', indexName: ['$SCHEMA_NAME',
[ [
Validators.required Validators.required
]] ]
]
}); });
constructor( 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()"> <form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row"> <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 { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms'; import { FormBuilder, Validators } from '@angular/forms';
import { ValidatorsEx } from 'shared'; import { ValidatorsEx } from '@app/shared';
@Component({ @Component({
selector: 'sqx-azure-queue-action', selector: 'sqx-azure-queue-action',
@ -28,12 +28,14 @@ export class AzureQueueActionComponent implements OnInit {
connectionString: ['', connectionString: ['',
[ [
Validators.required Validators.required
]], ]
],
queue: ['squidex', queue: ['squidex',
[ [
Validators.required, Validators.required,
ValidatorsEx.pattern('[a-z][a-z0-9]{2,}(\-[a-z0-9]+)*', 'Name must be a valid azure queue name.') ValidatorsEx.pattern('[a-z][a-z0-9]{2,}(\-[a-z0-9]+)*', 'Name must be a valid azure queue name.')
]] ]
]
}); });
constructor( 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()"> <form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row"> <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: ['', host: ['',
[ [
Validators.required Validators.required
]], ]
],
indexName: ['$APP_NAME', indexName: ['$APP_NAME',
[ [
Validators.required Validators.required
]], ]
],
indexType: ['$SCHEMA_NAME', indexType: ['$SCHEMA_NAME',
[ [
// Validators.required // Validators.required
]], ]
],
username: '', username: '',
password: '' 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()"> <form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row"> <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: ['', serviceId: ['',
[ [
Validators.required Validators.required
]], ]
],
apiKey: ['', apiKey: ['',
[ [
Validators.required Validators.required
]] ]
]
}); });
constructor( 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()"> <form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row"> <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: ['', webhookUrl: ['',
[ [
Validators.required Validators.required
]], ]
text: [''] ],
text: ''
}); });
constructor( 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()"> <form [formGroup]="actionForm" class="form-horizontal" (ngSubmit)="save()">
<div class="form-group row"> <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: ['', url: ['',
[ [
Validators.required Validators.required
]], ]
sharedSecret: [''] ],
sharedSecret: ''
}); });
constructor( constructor(

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

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

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

@ -11,14 +11,13 @@ import {
AppContext, AppContext,
CreateRuleDto, CreateRuleDto,
DateTime, DateTime,
fadeAnimation,
ruleActions, ruleActions,
ruleTriggers, ruleTriggers,
RuleDto, RuleDto,
RulesService, RulesService,
SchemaDto, SchemaDto,
UpdateRuleDto UpdateRuleDto
} from 'shared'; } from '@app/shared';
export const MODE_WIZARD = 'Wizard'; export const MODE_WIZARD = 'Wizard';
export const MODE_EDIT_TRIGGER = 'EditTrigger'; export const MODE_EDIT_TRIGGER = 'EditTrigger';
@ -30,9 +29,6 @@ export const MODE_EDIT_ACTION = 'EditAction';
templateUrl: './rule-wizard.component.html', templateUrl: './rule-wizard.component.html',
providers: [ providers: [
AppContext AppContext
],
animations: [
fadeAnimation
] ]
}) })
export class RuleWizardComponent implements OnInit { 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-title message="{app} | Rules" parameter1="app" [value1]="ctx.appName"></sqx-title>
<sqx-panel desiredWidth="54rem"> <sqx-panel desiredWidth="54rem" showSidebar="true">
<div class="panel-header"> <ng-container title>
<div class="panel-title-row"> Rules
<div class="float-right"> </ng-container>
<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> <ng-container menu>
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut> <button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
<button class="btn btn-success" #buttonNew (click)="createNew()" title="New Rule (CTRL + M)"> </button>
<i class="icon-plus"></i> New
</button>
</div>
<h3 class="panel-title">Rules</h3> <sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
</div> <sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut>
<a class="panel-close" sqxParentLink isLazyLoaded="true"> <button class="btn btn-success" #buttonNew (click)="createNew()" title="New Rule (CTRL + M)">
<i class="icon-close"></i> <i class="icon-plus"></i> New
</a> </button>
</div> </ng-container>
<div class="panel-main"> <ng-container content>
<div class="panel-content panel-content-scroll"> <div class="table-items-row table-items-row-empty" *ngIf="rules && rules.length === 0">
<div class="table-items-row table-items-row-empty" *ngIf="rules && rules.length === 0"> No Rule created yet.
No Rule created yet. </div>
</div>
<table class="table table-items table-fixed" *ngIf="rules && rules.length > 0"> <table class="table table-items table-fixed" *ngIf="rules && rules.length > 0">
<tbody> <tbody>
<ng-template ngFor let-rule [ngForOf]="rules"> <ng-template ngFor let-rule [ngForOf]="rules">
<tr> <tr>
<td class="cell-separator"> <td class="cell-separator">
<h3>If</h3> <h3>If</h3>
</td> </td>
<td class="cell-auto"> <td class="cell-auto">
<span class="rule-element rule-element-{{rule.triggerType}}" (click)="editTrigger(rule)"> <span class="rule-element rule-element-{{rule.triggerType}}" (click)="editTrigger(rule)">
<span class="rule-element-icon"> <span class="rule-element-icon">
<i class="icon-trigger-{{rule.triggerType}}"></i> <i class="icon-trigger-{{rule.triggerType}}"></i>
</span>
<span class="rule-element-text">
{{ruleTriggers[rule.triggerType].name}}
</span>
</span> </span>
</td> <span class="rule-element-text">
<td class="cell-separator"> {{ruleTriggers[rule.triggerType].name}}
<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> </span>
</td> </span>
<td class="cell-actions"> </td>
<sqx-toggle [ngModel]="rule.isEnabled" (ngModelChange)="toggleRule(rule)"></sqx-toggle> <td class="cell-separator">
</td> <h3>then</h3>
<td class="cell-actions"> </td>
<button type="button" class="btn btn-link btn-danger" <td class="cell-auto">
(sqxConfirmClick)="deleteRule(rule)" <span class="rule-element rule-element-{{rule.actionType}}" (click)="editAction(rule)">
confirmTitle="Delete rule" <span class="rule-element-icon">
confirmText="Do you really want to delete the rule?"> <i class="icon-action-{{rule.actionType}}"></i>
<i class="icon-bin2"></i> </span>
</button> <span class="rule-element-text">
</td> {{ruleActions[rule.actionType].name}}
</tr> </span>
<tr class="spacer"></tr> </span>
</ng-template> </td>
</tbody> <td class="cell-actions">
</table> <sqx-toggle [ngModel]="rule.isEnabled" (ngModelChange)="toggleRule(rule)"></sqx-toggle>
</div> </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"> <ng-container sidebar>
<a class="panel-link" routerLink="events" routerLinkActive="active" #linkHistory> <a class="panel-link" routerLink="events" routerLinkActive="active" #linkHistory>
<i class="icon-time"></i> <i class="icon-time"></i>
</a> </a>
<a class="panel-link" routerLink="help" routerLinkActive="active" #linkHelp> <a class="panel-link" routerLink="help" routerLinkActive="active" #linkHelp>
<i class="icon-help"></i> <i class="icon-help"></i>
</a> </a>
<sqx-onboarding-tooltip id="help" [for]="linkHelp" position="leftTop" after="180000"> <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. 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> </sqx-onboarding-tooltip>
</div> </ng-container>
</div>
</sqx-panel> </sqx-panel>
<div class="modal" *sqxModalView="addRuleDialog;onRoot:true;closeAuto:false" @fade> <ng-container *sqxModalView="addRuleDialog;onRoot:true;closeAuto:false">
<div class="modal-backdrop"></div>
<sqx-rule-wizard [schemas]="schemas" [rule]="wizardRule" [mode]="wizardMode" <sqx-rule-wizard [schemas]="schemas" [rule]="wizardRule" [mode]="wizardMode"
(updated)="onRuleUpdated($event)" (updated)="onRuleUpdated($event)"
(cancelled)="addRuleDialog.hide()" (cancelled)="addRuleDialog.hide()"
(created)="onRuleCreated($event)"> (created)="onRuleCreated($event)">
</sqx-rule-wizard> </sqx-rule-wizard>
</div> </ng-container>
<router-outlet></router-outlet> <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 { import {
AppContext, AppContext,
DateTime, DateTime,
fadeAnimation,
ImmutableArray, ImmutableArray,
ModalView, ModalView,
ruleActions, ruleActions,
@ -19,7 +18,7 @@ import {
RulesService, RulesService,
SchemaDto, SchemaDto,
SchemasService SchemasService
} from 'shared'; } from '@app/shared';
@Component({ @Component({
selector: 'sqx-rules-page', selector: 'sqx-rules-page',
@ -27,9 +26,6 @@ import {
templateUrl: './rules-page.component.html', templateUrl: './rules-page.component.html',
providers: [ providers: [
AppContext AppContext
],
animations: [
fadeAnimation
] ]
}) })
export class RulesPageComponent implements OnInit { 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()"> <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"> <table class="table table-middle table-sm table-fixed table-borderless" *ngIf="!handleAll">
<colgroup> <colgroup>

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

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

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

@ -5,6 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * 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-ui.component';
export * from './pages/schema/types/assets-validation.component'; export * from './pages/schema/types/assets-validation.component';
export * from './pages/schema/types/boolean-ui.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-ui.component';
export * from './pages/schema/types/tags-validation.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.component';
export * from './pages/schema/field-wizard.component';
export * from './pages/schema/schema-edit-form.component'; export * from './pages/schema/schema-edit-form.component';
export * from './pages/schema/schema-page.component'; export * from './pages/schema/schema-page.component';
export * from './pages/schema/schema-scripts-form.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 { DndModule } from 'ng2-dnd';
import { import {
HelpComponent,
HistoryComponent,
ResolveSchemaGuard,
SqxFrameworkModule, SqxFrameworkModule,
SqxSharedModule SqxSharedModule
} from 'shared'; } from '@app/shared';
import { import {
FieldComponent, FieldComponent,
@ -25,6 +22,10 @@ import {
BooleanValidationComponent, BooleanValidationComponent,
DateTimeUIComponent, DateTimeUIComponent,
DateTimeValidationComponent, DateTimeValidationComponent,
FieldFormCommonComponent,
FieldFormUIComponent,
FieldFormValidationComponent,
FieldWizardComponent,
GeolocationUIComponent, GeolocationUIComponent,
GeolocationValidationComponent, GeolocationValidationComponent,
JsonUIComponent, JsonUIComponent,
@ -35,6 +36,7 @@ import {
ReferencesValidationComponent, ReferencesValidationComponent,
SchemaEditFormComponent, SchemaEditFormComponent,
SchemaFormComponent, SchemaFormComponent,
SchemaMustExistGuard,
SchemaPageComponent, SchemaPageComponent,
SchemasPageComponent, SchemasPageComponent,
SchemaScriptsFormComponent, SchemaScriptsFormComponent,
@ -49,31 +51,10 @@ const routes: Routes = [
path: '', path: '',
component: SchemasPageComponent, component: SchemasPageComponent,
children: [ children: [
{
path: ''
},
{ {
path: ':schemaName', path: ':schemaName',
component: SchemaPageComponent, component: SchemaPageComponent,
resolve: { canActivate: [SchemaMustExistGuard]
schema: ResolveSchemaGuard
},
children: [
{
path: 'history',
component: HistoryComponent,
data: {
channel: 'schemas.{schemaName}'
}
},
{
path: 'help',
component: HelpComponent,
data: {
helpPage: '05-integrated/schemas'
}
}
]
}] }]
} }
]; ];
@ -85,6 +66,9 @@ const routes: Routes = [
DndModule, DndModule,
RouterModule.forChild(routes) RouterModule.forChild(routes)
], ],
providers: [
SchemaMustExistGuard
],
declarations: [ declarations: [
FieldComponent, FieldComponent,
AssetsUIComponent, AssetsUIComponent,
@ -93,6 +77,10 @@ const routes: Routes = [
BooleanValidationComponent, BooleanValidationComponent,
DateTimeUIComponent, DateTimeUIComponent,
DateTimeValidationComponent, DateTimeValidationComponent,
FieldFormCommonComponent,
FieldFormUIComponent,
FieldFormValidationComponent,
FieldWizardComponent,
GeolocationUIComponent, GeolocationUIComponent,
GeolocationValidationComponent, GeolocationValidationComponent,
JsonUIComponent, JsonUIComponent,

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

@ -5,32 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * 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 { export class SchemaCloning {
constructor( 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"> <span class="field-name">
<i class="field-icon icon-type-{{field.properties.fieldType}}"></i> <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 class="field-partitioning" *ngIf="field.isLocalizable">localizable</span>
</span> </span>
</div> </div>
@ -27,26 +27,26 @@
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </button>
<div class="dropdown-menu" *sqxModalView="dropdown" [sqxModalTarget]="optionsButton" @fade> <div class="dropdown-menu" *sqxModalView="dropdown" [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 Enable
</a> </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 Disable
</a> </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 Hide
</a> </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 Show
</a> </a>
<a class="dropdown-item" *ngIf="!field.isLocked" <a class="dropdown-item" *ngIf="!field.isLocked"
(sqxConfirmClick)="locking.emit()" (sqxConfirmClick)="lockField()"
confirmTitle="Lock field" confirmTitle="Lock field"
confirmText="Do you really want to lock the field? Lock fields cannot be deleted or changed."> confirmText="Do you really want to lock the field? Lock fields cannot be deleted or changed.">
Lock Lock
</a> </a>
<a class="dropdown-item dropdown-item-delete" [class.disabled]="field.isLocked" <a class="dropdown-item dropdown-item-delete" [class.disabled]="field.isLocked"
(sqxConfirmClick)="deleting.emit()" (sqxConfirmClick)="deleteField()"
confirmTitle="Delete field" confirmTitle="Delete field"
confirmText="Do you really want to delete the field?"> confirmText="Do you really want to delete the field?">
Delete Delete
@ -59,7 +59,7 @@
</div> </div>
<div class="table-items-row-details" *ngIf="isEditing"> <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"> <div class="table-items-row-details-tabs clearfix">
<ul class="nav nav-tabs2"> <ul class="nav nav-tabs2">
<li class="nav-item"> <li class="nav-item">
@ -80,124 +80,15 @@
</div> </div>
<div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 0"> <div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 0">
<div class="form-group row"> <sqx-field-form-common [editForm]="editForm.form" [editFormSubmitted]="editForm.submitted | async" [field]="field"></sqx-field-form-common>
<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>
</div> </div>
<div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 1"> <div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 1">
<div [ngSwitch]="field.properties.fieldType"> <sqx-field-form-validation [patterns]="patterns" [editForm]="editForm.form" [field]="field"></sqx-field-form-validation>
<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>
</div> </div>
<div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 2"> <div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 2">
<div [ngSwitch]="field.properties.fieldType"> <sqx-field-form-ui [editForm]="editForm.form" [field]="field"></sqx-field-form-ui>
<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>
</div> </div>
</form> </form>
</div> </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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { import {
AppPatternDto, AppPatternDto,
createProperties, createProperties,
EditFieldForm,
fadeAnimation, fadeAnimation,
FieldDto, FieldDto,
FieldPropertiesDto,
ModalView, ModalView,
SchemaDto SchemaDetailsDto,
} from 'shared'; SchemasState,
UpdateFieldDto
} from '@app/shared';
@Component({ @Component({
selector: 'sqx-field', selector: 'sqx-field',
@ -31,63 +33,31 @@ export class FieldComponent implements OnInit {
public field: FieldDto; public field: FieldDto;
@Input() @Input()
public schemas: SchemaDto[]; public patterns: AppPatternDto;
@Input() @Input()
public regexSuggestions: AppPatternDto[] = []; public schema: SchemaDetailsDto;
@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 dropdown = new ModalView(false, true); public dropdown = new ModalView(false, true);
public isEditing = false; public isEditing = false;
public selectedTab = 0; public selectedTab = 0;
public get displayName() { public editForm: EditFieldForm;
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]
});
constructor( constructor(
private readonly formBuilder: FormBuilder private readonly formBuilder: FormBuilder,
private readonly schemasState: SchemasState
) { ) {
} }
public ngOnInit() { 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() { public toggleEditing() {
@ -99,32 +69,46 @@ export class FieldComponent implements OnInit {
} }
public cancel() { public cancel() {
this.resetEditForm(); this.editForm.load(this.field);
} }
public save() { public deleteField() {
this.editFormSubmitted = true; this.schemasState.deleteField(this.schema, this.field).subscribe();
}
if (this.editForm.valid) { public enableField() {
const properties = createProperties(this.field.properties['fieldType'], this.editForm.value); 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) { public showField() {
this.saving.emit(properties); this.schemasState.showField(this.schema, this.field).subscribe();
} }
private resetEditForm() { public hideField() {
this.editFormSubmitted = false; this.schemasState.hideField(this.schema, this.field).subscribe();
this.editForm.reset(this.field.properties); }
if (this.field.isLocked) { public lockField() {
this.editForm.disable(); 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