mirror of https://github.com/Squidex/squidex.git
135 changed files with 2567 additions and 692 deletions
@ -0,0 +1,35 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Apps |
|||
{ |
|||
public sealed class AppImage |
|||
{ |
|||
public string MimeType { get; } |
|||
|
|||
public string Etag { get; } |
|||
|
|||
public AppImage(string mimeType, string etag = null) |
|||
{ |
|||
Guard.NotNullOrEmpty(mimeType, nameof(mimeType)); |
|||
|
|||
MimeType = mimeType; |
|||
|
|||
if (string.IsNullOrWhiteSpace(etag)) |
|||
{ |
|||
Etag = RandomHash.Simple(); |
|||
} |
|||
else |
|||
{ |
|||
Etag = etag; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps.Commands |
|||
{ |
|||
public sealed class RemoveAppImage : AppCommand |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps.Commands |
|||
{ |
|||
public sealed class UpdateApp : AppCommand |
|||
{ |
|||
public string Label { get; set; } |
|||
|
|||
public string Description { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps.Commands |
|||
{ |
|||
public sealed class UploadAppImage : AppCommand |
|||
{ |
|||
public AppImage Image { get; set; } |
|||
|
|||
public Func<Stream> File { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.EventSourcing; |
|||
|
|||
namespace Squidex.Domain.Apps.Events.Apps |
|||
{ |
|||
[EventType(nameof(AppImageRemoved))] |
|||
public sealed class AppImageRemoved : AppEvent |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Infrastructure.EventSourcing; |
|||
|
|||
namespace Squidex.Domain.Apps.Events.Apps |
|||
{ |
|||
[EventType(nameof(AppImageUploaded))] |
|||
public sealed class AppImageUploaded : AppEvent |
|||
{ |
|||
public AppImage Image { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.EventSourcing; |
|||
|
|||
namespace Squidex.Domain.Apps.Events.Apps |
|||
{ |
|||
[EventType(nameof(AppUpdated))] |
|||
public sealed class AppUpdated : AppEvent |
|||
{ |
|||
public string Label { get; set; } |
|||
|
|||
public string Description { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Entities.Apps.Commands; |
|||
using Squidex.Infrastructure.Reflection; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Apps.Models |
|||
{ |
|||
public sealed class UpdateAppDto |
|||
{ |
|||
/// <summary>
|
|||
/// The optional label of your app.
|
|||
/// </summary>
|
|||
public string Label { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The optional description of your app.
|
|||
/// </summary>
|
|||
public string Description { get; set; } |
|||
|
|||
public UpdateApp ToCommand() |
|||
{ |
|||
return SimpleMapper.Map(this, new UpdateApp()); |
|||
} |
|||
} |
|||
} |
|||
@ -1,4 +1,4 @@ |
|||
<sqx-title message="{app} | API | GraphQL" parameter1="app" [value1]="appsState.appName"></sqx-title> |
|||
<sqx-title message="{app} | API | GraphQL" parameter1="app" [value1]="appsState.appDisplayName"></sqx-title> |
|||
|
|||
<sqx-panel desiredWidth="*" minWidth="50rem" isFullSize="true"> |
|||
<div inner #graphiQLContainer></div> |
|||
|
|||
@ -1,6 +1,6 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
|
|||
texarea { |
|||
textarea { |
|||
resize: none; |
|||
} |
|||
@ -1,29 +1,2 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
|
|||
$circle-size: 2.8rem; |
|||
|
|||
.backup-status { |
|||
& { |
|||
@include circle($circle-size); |
|||
line-height: $circle-size + .1rem; |
|||
text-align: center; |
|||
font-size: .4 * $circle-size; |
|||
font-weight: normal; |
|||
background: $color-border; |
|||
color: $color-dark-foreground; |
|||
vertical-align: middle; |
|||
} |
|||
|
|||
&-pending { |
|||
color: inherit; |
|||
} |
|||
|
|||
&-failed { |
|||
background: $color-theme-error; |
|||
} |
|||
|
|||
&-success { |
|||
background: $color-theme-green; |
|||
} |
|||
} |
|||
@import '_mixins'; |
|||
@ -0,0 +1,43 @@ |
|||
<form [formGroup]="importForm.form" (ngSubmit)="import()"> |
|||
<sqx-modal-dialog (close)="emitClose()"> |
|||
<ng-container title> |
|||
Import contributors |
|||
</ng-container> |
|||
|
|||
<ng-container content> |
|||
<div class="content"> |
|||
<ng-container *ngIf="importStatus; else noStatus"> |
|||
<div class="row pb-2" *ngFor="let status of importStatus"> |
|||
<div class="col truncate"> |
|||
{{status.email}} |
|||
</div> |
|||
|
|||
<div class="col-auto"> |
|||
<sqx-status-icon size="sm" |
|||
[status]="status.result" |
|||
[statusText]="status.resultText"> |
|||
</sqx-status-icon> |
|||
</div> |
|||
</div> |
|||
</ng-container> |
|||
|
|||
<ng-template #noStatus> |
|||
<textarea class="form-control content" placeholder="user1@squidex.io;user2.squidex.io" formControlName="import"></textarea> |
|||
</ng-template> |
|||
</div> |
|||
|
|||
<sqx-form-hint> |
|||
<ng-container *ngIf="importForm.numberOfEmails | async; let emails"> |
|||
Emails detected: {{emails}} |
|||
</ng-container> |
|||
|
|||
|
|||
</sqx-form-hint> |
|||
</ng-container> |
|||
|
|||
<ng-container footer> |
|||
<button type="reset" class="float-left btn btn-secondary" (click)="emitClose()">Cancel</button> |
|||
<button type="submit" class="float-right btn btn-success" [disabled]="importStatus || (importForm.hasNoUser | async)">Add Contributors</button> |
|||
</ng-container> |
|||
</sqx-modal-dialog> |
|||
</form> |
|||
@ -0,0 +1,10 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
|
|||
textarea { |
|||
resize: none; |
|||
} |
|||
|
|||
.content { |
|||
min-height: 300px; |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Component, EventEmitter, Output } from '@angular/core'; |
|||
import { FormBuilder } from '@angular/forms'; |
|||
import { empty, of } from 'rxjs'; |
|||
import { catchError, mergeMap, tap } from 'rxjs/operators'; |
|||
|
|||
import { |
|||
ContributorsState, |
|||
ErrorDto, |
|||
ImportContributorsForm |
|||
} from '@app/shared'; |
|||
|
|||
interface ImportStatus { |
|||
email: string; |
|||
result: 'Pending' | 'Failed' | 'Success'; |
|||
resultText: string; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'sqx-import-contributors-dialog', |
|||
styleUrls: ['./import-contributors-dialog.component.scss'], |
|||
templateUrl: './import-contributors-dialog.component.html' |
|||
}) |
|||
export class ImportContributorsDialogComponent { |
|||
@Output() |
|||
public close = new EventEmitter(); |
|||
|
|||
public importForm = new ImportContributorsForm(this.formBuilder); |
|||
public importStatus: ImportStatus[]; |
|||
|
|||
constructor( |
|||
private readonly formBuilder: FormBuilder, |
|||
private readonly contributorsState: ContributorsState |
|||
) { |
|||
} |
|||
|
|||
public import() { |
|||
const contributors = this.importForm.submit(); |
|||
|
|||
if (contributors && contributors.length > 0) { |
|||
this.importStatus = []; |
|||
|
|||
for (let contributor of contributors) { |
|||
this.importStatus.push({ |
|||
email: contributor.contributorId, |
|||
result: 'Pending', |
|||
resultText: 'Pending' |
|||
}); |
|||
} |
|||
|
|||
of(...contributors).pipe( |
|||
mergeMap(c => |
|||
this.contributorsState.assign(c, { silent: true }).pipe( |
|||
tap(created => { |
|||
let status = this.importStatus.find(x => x.email === c.contributorId); |
|||
|
|||
if (status) { |
|||
status.resultText = getSuccess(created); |
|||
status.result = 'Success'; |
|||
} |
|||
}), |
|||
catchError((error: ErrorDto) => { |
|||
let status = this.importStatus.find(x => x.email === c.contributorId); |
|||
|
|||
if (status) { |
|||
status.resultText = getError(error); |
|||
status.result = 'Failed'; |
|||
} |
|||
|
|||
return empty(); |
|||
}) |
|||
), 1) |
|||
).subscribe(); |
|||
} |
|||
} |
|||
|
|||
public emitClose() { |
|||
this.close.emit(); |
|||
} |
|||
} |
|||
|
|||
function getError(error: ErrorDto): string { |
|||
return error.details[0]; |
|||
} |
|||
|
|||
function getSuccess(created: boolean | undefined): string { |
|||
return created ? |
|||
'User has been invited and assigned.' : |
|||
'User has been assigned'; |
|||
} |
|||
@ -1,2 +1,71 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
@import '_mixins'; |
|||
|
|||
.app { |
|||
&-image { |
|||
position: relative; |
|||
min-width: 150px; |
|||
min-height: 150px; |
|||
} |
|||
|
|||
&-image-remove { |
|||
@include absolute(auto, .5rem, .5rem, auto); |
|||
} |
|||
|
|||
&-progress { |
|||
@include absolute(1rem, 1rem, 1rem, 1rem); |
|||
} |
|||
} |
|||
|
|||
@mixin overlay { |
|||
& { |
|||
@include transition(opacity .4s ease); |
|||
@include absolute(0, 0, 0, 0); |
|||
@include opacity(0); |
|||
@include flex-box; |
|||
color: $color-dark-foreground; |
|||
} |
|||
|
|||
&-background { |
|||
@include absolute(0, 0, 0, 0); |
|||
@include opacity(.7); |
|||
background: $color-dark-black; |
|||
} |
|||
} |
|||
|
|||
.upload-button { |
|||
& { |
|||
margin-top: 1rem; |
|||
} |
|||
|
|||
input { |
|||
@include hidden; |
|||
} |
|||
} |
|||
|
|||
.drop-overlay { |
|||
& { |
|||
@include overlay; |
|||
pointer-events: none; |
|||
} |
|||
|
|||
&-text { |
|||
position: absolute; |
|||
font-size: 1.25rem; |
|||
font-weight: lighter; |
|||
} |
|||
} |
|||
|
|||
.drag { |
|||
.drop-overlay { |
|||
@include opacity(1); |
|||
} |
|||
|
|||
.app-image-remove { |
|||
display: none; |
|||
} |
|||
} |
|||
|
|||
.disabled { |
|||
pointer-events: none; |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Component, Input, OnChanges } from '@angular/core'; |
|||
|
|||
import { picasso } from '@app/framework/internal'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-avatar', |
|||
template: ` |
|||
<img |
|||
[style.width]="sizeInPx" |
|||
[style.height]="sizeInPx" |
|||
[attr.src]="imageSource | sqxSafeUrl" |
|||
/> |
|||
` |
|||
}) |
|||
export class AvatarComponent implements OnChanges { |
|||
public imageSource: string; |
|||
public sizeInPx: string; |
|||
|
|||
@Input() |
|||
public identifier: string; |
|||
|
|||
@Input() |
|||
public image: string; |
|||
|
|||
@Input() |
|||
public size = 50; |
|||
|
|||
public ngOnChanges() { |
|||
this.imageSource = this.image || this.createSvg(); |
|||
|
|||
this.sizeInPx = `${this.size}px`; |
|||
} |
|||
|
|||
private createSvg() { |
|||
const svg = picasso(this.identifier); |
|||
|
|||
return `data:image/svg+xml;utf8,${svg}`; |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
<ng-container [ngSwitch]="status"> |
|||
<div *ngSwitchCase="'Failed'" class="status status-failed {{size}}" [title]="statusText"> |
|||
<i class="icon-exclamation"></i> |
|||
</div> |
|||
<div *ngSwitchCase="'Success'" class="status status-success {{size}}" [title]="statusText"> |
|||
<i class="icon-checkmark"></i> |
|||
</div> |
|||
<div *ngSwitchCase="'Completed'" class="status status-success {{size}}" [title]="statusText"> |
|||
<i class="icon-checkmark"></i> |
|||
</div> |
|||
<div *ngSwitchDefault class="status status-pending spin {{size}}" [title]="statusText"> |
|||
<i class="icon-hour-glass"></i> |
|||
</div> |
|||
</ng-container> |
|||
@ -0,0 +1,41 @@ |
|||
@import '_mixins'; |
|||
@import '_vars'; |
|||
|
|||
$circle-size-sm: 1.6rem; |
|||
|
|||
$circle-size-lg: 2.8rem; |
|||
|
|||
.status { |
|||
& { |
|||
text-align: center; |
|||
background: $color-border; |
|||
color: $color-dark-foreground; |
|||
vertical-align: middle; |
|||
} |
|||
|
|||
&.sm { |
|||
@include circle($circle-size-sm); |
|||
line-height: $circle-size-sm + .1rem; |
|||
font-size: .5 * $circle-size-sm; |
|||
font-weight: normal; |
|||
} |
|||
|
|||
&.lg { |
|||
@include circle($circle-size-lg); |
|||
line-height: $circle-size-lg + .1rem; |
|||
font-size: .5 * $circle-size-lg; |
|||
font-weight: normal; |
|||
} |
|||
|
|||
&-pending { |
|||
color: inherit; |
|||
} |
|||
|
|||
&-failed { |
|||
background: $color-theme-error; |
|||
} |
|||
|
|||
&-success { |
|||
background: $color-theme-green; |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-status-icon', |
|||
styleUrls: ['./status-icon.component.scss'], |
|||
templateUrl: './status-icon.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush |
|||
}) |
|||
export class StatusIconComponent { |
|||
@Input() |
|||
public status: 'Started' | 'Failed' | 'Success' | 'Completed' | 'Failed' | 'Pending'; |
|||
|
|||
@Input() |
|||
public statusText: string; |
|||
|
|||
@Input() |
|||
public size: 'lg' | 'sm' = 'lg'; |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
import MersenneTwister from 'mersenne-twister'; |
|||
|
|||
const ALL_COLORS = [ |
|||
'rgb(226,27,12)', |
|||
'rgb(192,19,78)', |
|||
'rgb(125,31,141)', |
|||
'rgb(82,46,146)', |
|||
'rgb(50,65,145)', |
|||
'rgb(11,122,209)', |
|||
'rgb(2,135,195)', |
|||
'rgb(0,150,170)', |
|||
'rgb(0,120,109)', |
|||
'rgb(61,140,64)', |
|||
'rgb(112,162,54)', |
|||
'rgb(174,188,33)', |
|||
'rgb(210,157,0)', |
|||
'rgb(204,122,0)', |
|||
'rgb(231,55,0)' |
|||
]; |
|||
|
|||
const LAYERS = 3; |
|||
|
|||
const RADIUSES = [20, 25, 30, 35, 40, 45, 50]; |
|||
|
|||
const X_CENTERS = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]; |
|||
const Y_CENTERS = [30, 40, 50, 60, 70]; |
|||
|
|||
function hash(str: string) { |
|||
if (str.length === 0) { |
|||
return 0; |
|||
} |
|||
|
|||
let result = 0; |
|||
|
|||
for (let i = 0; i < str.length; i++) { |
|||
result = result * 31 + str.charCodeAt(i); |
|||
result = result % (2 ** 32); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
export function picasso(content: string) { |
|||
const seed = hash(content); |
|||
const rand = new MersenneTwister(seed); |
|||
|
|||
const colors = [...ALL_COLORS]; |
|||
|
|||
const generateColor = () => { |
|||
const idx = Math.floor(colors.length * rand.random()); |
|||
|
|||
return colors.splice(idx, 1)[0]; |
|||
}; |
|||
|
|||
const background = `<rect fill="${generateColor()}" width="100" height="100"/>`; |
|||
|
|||
let shape = ''; |
|||
|
|||
for (let i = 0; i < LAYERS; i++) { |
|||
const rd = RADIUSES[Math.floor(RADIUSES.length * rand.random())]; |
|||
|
|||
const cx = X_CENTERS[Math.floor(X_CENTERS.length * rand.random())]; |
|||
const cy = Y_CENTERS[Math.floor(Y_CENTERS.length * rand.random())]; |
|||
|
|||
const fill = generateColor(); |
|||
|
|||
shape += `<circle r="${rd}" cx="${cx}" cy="${cy}" fill="${fill}" opacity="0."/>`; |
|||
} |
|||
|
|||
return `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">${background}${shape}</svg>`; |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue