Browse Source

Small screen improvements (#947)

* Typings for editor SDK.

* Fix line height and cut text.

* Improve integrated documentation for .NET SDK

* Add access token to preview URL.

* Improve layout system for small screens.
pull/949/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
13163426b4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      backend/i18n/frontend_en.json
  2. 4
      backend/i18n/frontend_it.json
  3. 4
      backend/i18n/frontend_nl.json
  4. 4
      backend/i18n/frontend_zh.json
  5. 2
      backend/i18n/source/backend_en.json
  6. 4
      backend/i18n/source/frontend_en.json
  7. 52
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  8. 6
      backend/src/Squidex.Shared/Texts.it.resx
  9. 6
      backend/src/Squidex.Shared/Texts.nl.resx
  10. 6
      backend/src/Squidex.Shared/Texts.resx
  11. 6
      backend/src/Squidex.Shared/Texts.zh.resx
  12. 199
      backend/src/Squidex/wwwroot/scripts/editor-sdk.d.ts
  13. 2
      frontend/src/app/features/administration/pages/users/user-page.component.html
  14. 2
      frontend/src/app/features/content/pages/comments/comments-page.component.html
  15. 2
      frontend/src/app/features/content/pages/content/content-history-page.component.html
  16. 2
      frontend/src/app/features/content/shared/list/content.component.scss
  17. 9
      frontend/src/app/features/content/shared/preview-button.component.ts
  18. 8
      frontend/src/app/features/content/shared/references/reference-item.component.html
  19. 55
      frontend/src/app/features/settings/pages/clients/client-connect-form.component.html
  20. 6
      frontend/src/app/features/settings/pages/clients/client-connect-form.component.scss
  21. 34
      frontend/src/app/features/settings/pages/clients/client-connect-form.component.ts
  22. 2
      frontend/src/app/framework/angular/language-selector.stories.tsx
  23. 17
      frontend/src/app/framework/angular/layout-container.directive.ts
  24. 32
      frontend/src/app/framework/angular/layout.component.html
  25. 106
      frontend/src/app/framework/angular/layout.component.ts
  26. 216
      frontend/src/app/framework/angular/layout.stories.tsx
  27. 2
      frontend/src/app/framework/angular/loader.stories.ts
  28. 4
      frontend/src/app/framework/angular/status-icon.stories.tsx
  29. 1
      frontend/src/app/shared/components/forms/markdown-editor.component.scss
  30. 2
      frontend/src/app/shared/components/help/help.component.html
  31. 2
      frontend/src/app/shared/components/history/history.component.html
  32. 20
      frontend/src/app/theme/_mixins.scss
  33. 70
      frontend/src/app/theme/_panels2.scss
  34. 10
      frontend/src/app/theme/_vars.scss
  35. 6
      frontend/src/config/webpack.config.js

4
backend/i18n/frontend_en.json

@ -188,12 +188,16 @@
"clients.connectWizard.manuallyTokenHint": "Tokens usally expire after 30days, but you can request multiple tokens.",
"clients.connectWizard.postManDocs": "Start with the Postman tutorial in the [Documentation](https://docs.squidex.io/02-documentation/developer-guides/api-overview/postman).",
"clients.connectWizard.sdk": "Connect to your App with SDK",
"clients.connectWizard.sdkDocumentation": "Documentations for the .NET SDK is available: ",
"clients.connectWizard.sdkHelp": "You need another SDK?",
"clients.connectWizard.sdkHelpLink": "Contact us in the Support Forum",
"clients.connectWizard.sdkHint": "Download an SDK and establish a connection to this app.",
"clients.connectWizard.sdkStep1": "Install the .NET SDK",
"clients.connectWizard.sdkStep1Download": "The SDK is available on [nuget](https://www.nuget.org/packages/Squidex.ClientLibrary/)",
"clients.connectWizard.sdkStep2": "Create a client manager",
"clients.connectWizard.sdkStep3": "Optionally: Install the Service Extensions for the SDK",
"clients.connectWizard.sdkStep3Download": "The SDK Extension is available on [nuget](https://www.nuget.org/packages/Squidex.ClientLibrary.ServiceExtensions/)",
"clients.connectWizard.sdkStep4": "Optionally: Register the client manager and all clients",
"clients.connectWizard.step0Title": "Setup client",
"clients.connectWizard.step1Title": "Choose connection method",
"clients.connectWizard.step2Title": "Connect",

4
backend/i18n/frontend_it.json

@ -188,12 +188,16 @@
"clients.connectWizard.manuallyTokenHint": "Solitamente i Token scadono dopo 30 giorni, ma puoi richiedere token multipli.",
"clients.connectWizard.postManDocs": "Per il tutorial Postman inizia da questo link [Documentazione](https://docs.squidex.io/02-documentation/developer-guides/api-overview/postman).",
"clients.connectWizard.sdk": "Connetti la tua APP utilizzando SDK",
"clients.connectWizard.sdkDocumentation": "Documentations for the .NET SDK is available: ",
"clients.connectWizard.sdkHelp": "Hai bisogno di un altro SDK?",
"clients.connectWizard.sdkHelpLink": "Contattaci nel Forum di assistenza",
"clients.connectWizard.sdkHint": "Scarica l'SDK e connetti quest'app.",
"clients.connectWizard.sdkStep1": "Installa .NET SDK",
"clients.connectWizard.sdkStep1Download": "L'SDK è disponibile su [nuget](https://www.nuget.org/packages/Squidex.ClientLibrary/)",
"clients.connectWizard.sdkStep2": "Crea un client manager",
"clients.connectWizard.sdkStep3": "Optionally: Install the Service Extensions for the SDK",
"clients.connectWizard.sdkStep3Download": "The SDK Extension is available on [nuget](https://www.nuget.org/packages/Squidex.ClientLibrary.ServiceExtensions/)",
"clients.connectWizard.sdkStep4": "Optionally: Register the client manager and all clients",
"clients.connectWizard.step0Title": "Setup client",
"clients.connectWizard.step1Title": "Scegli la tipologia di connessione",
"clients.connectWizard.step2Title": "Collega",

4
backend/i18n/frontend_nl.json

@ -188,12 +188,16 @@
"clients.connectWizard.manuallyTokenHint": "Tokens vervallen gewoonlijk na 30 dagen, maar je kunt meerdere tokens aanvragen.",
"clients.connectWizard.postManDocs": "Begin met de Postman-tutorial in de [Documentatie] (https://docs.squidex.io/02-documentation/developer-guides/api-overview/postman).",
"clients.connectWizard.sdk": "Maak verbinding met uw app met SDK",
"clients.connectWizard.sdkDocumentation": "Documentations for the .NET SDK is available: ",
"clients.connectWizard.sdkHelp": "Heb je een andere SDK nodig?",
"clients.connectWizard.sdkHelpLink": "Neem contact met ons op in het ondersteuningsforum",
"clients.connectWizard.sdkHint": "Download een SDK en maak verbinding met deze app.",
"clients.connectWizard.sdkStep1": "Installeer de .NET SDK",
"clients.connectWizard.sdkStep1Download": "De SDK is beschikbaar op [nuget] (https://www.nuget.org/packages/Squidex.ClientLibrary/)",
"clients.connectWizard.sdkStep2": "Maak een klantenbeheerder",
"clients.connectWizard.sdkStep3": "Optionally: Install the Service Extensions for the SDK",
"clients.connectWizard.sdkStep3Download": "The SDK Extension is available on [nuget](https://www.nuget.org/packages/Squidex.ClientLibrary.ServiceExtensions/)",
"clients.connectWizard.sdkStep4": "Optionally: Register the client manager and all clients",
"clients.connectWizard.step0Title": "Client instellen",
"clients.connectWizard.step1Title": "Kies verbindingsmethode",
"clients.connectWizard.step2Title": "Verbinden",

4
backend/i18n/frontend_zh.json

@ -188,12 +188,16 @@
"clients.connectWizard.manuallyTokenHint": "令牌通常会在 30 天后过期,但您可以请求多个令牌。",
"clients.connectWizard.postManDocs": "从 [文档](https://docs.squidex.io/02-documentation/developer-guides/api-overview/postman) 中的 Postman 教程开始。",
"clients.connectWizard.sdk": "使用 SDK 连接到您的应用程序",
"clients.connectWizard.sdkDocumentation": "Documentations for the .NET SDK is available: ",
"clients.connectWizard.sdkHelp": "你需要另一个 SDK?",
"clients.connectWizard.sdkHelpLink": "在支持论坛联系我们",
"clients.connectWizard.sdkHint": "下载 SDK 并建立与此应用程序的连接。",
"clients.connectWizard.sdkStep1": "安装.NET SDK",
"clients.connectWizard.sdkStep1Download": "SDK 可在 [nuget](https://www.nuget.org/packages/Squidex.ClientLibrary/)",
"clients.connectWizard.sdkStep2": "创建客户端管理器",
"clients.connectWizard.sdkStep3": "Optionally: Install the Service Extensions for the SDK",
"clients.connectWizard.sdkStep3Download": "The SDK Extension is available on [nuget](https://www.nuget.org/packages/Squidex.ClientLibrary.ServiceExtensions/)",
"clients.connectWizard.sdkStep4": "Optionally: Register the client manager and all clients",
"clients.connectWizard.step0Title": "设置客户端",
"clients.connectWizard.step1Title": "选择连接方式",
"clients.connectWizard.step2Title": "连接",

2
backend/i18n/source/backend_en.json

@ -215,6 +215,7 @@
"history.apps.clientAdded": "added client {[Id]} to app",
"history.apps.clientRevoked": "revoked client {[Id]}",
"history.apps.clientUpdated": "updated client {[Id]}",
"history.apps.common.updated": "updated general settings",
"history.apps.contributoreAssigned": "assigned {user:[Contributor]} as {[Role]}",
"history.apps.contributoreRemoved": "removed {user:[Contributor]} from app",
"history.apps.created": "created the app.",
@ -231,7 +232,6 @@
"history.apps.roleUpdated": "updated role {[Name]}",
"history.apps.settingsUpdated": "updated UI settings",
"history.apps.transfered": "updated app to client",
"history.apps.updated": "updated general settings",
"history.apps.workflowAdded": "added workflow {[Name]}.",
"history.apps.workflowDeleted": "deleted a workflow.",
"history.apps.workflowUpdated": "updated a workflow.",

4
backend/i18n/source/frontend_en.json

@ -188,12 +188,16 @@
"clients.connectWizard.manuallyTokenHint": "Tokens usally expire after 30days, but you can request multiple tokens.",
"clients.connectWizard.postManDocs": "Start with the Postman tutorial in the [Documentation](https://docs.squidex.io/02-documentation/developer-guides/api-overview/postman).",
"clients.connectWizard.sdk": "Connect to your App with SDK",
"clients.connectWizard.sdkDocumentation": "Documentations for the .NET SDK is available: ",
"clients.connectWizard.sdkHelp": "You need another SDK?",
"clients.connectWizard.sdkHelpLink": "Contact us in the Support Forum",
"clients.connectWizard.sdkHint": "Download an SDK and establish a connection to this app.",
"clients.connectWizard.sdkStep1": "Install the .NET SDK",
"clients.connectWizard.sdkStep1Download": "The SDK is available on [nuget](https://www.nuget.org/packages/Squidex.ClientLibrary/)",
"clients.connectWizard.sdkStep2": "Create a client manager",
"clients.connectWizard.sdkStep3": "Optionally: Install the Service Extensions for the SDK",
"clients.connectWizard.sdkStep3Download": "The SDK Extension is available on [nuget](https://www.nuget.org/packages/Squidex.ClientLibrary.ServiceExtensions/)",
"clients.connectWizard.sdkStep4": "Optionally: Register the client manager and all clients",
"clients.connectWizard.step0Title": "Setup client",
"clients.connectWizard.step1Title": "Choose connection method",
"clients.connectWizard.step2Title": "Connect",

52
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs

@ -80,38 +80,40 @@ public class ContentQueryParser
query.Filter = await GeoQueryTransformer.TransformAsync(query.Filter, context, schema, textIndex);
}
if (!string.IsNullOrWhiteSpace(query.FullText))
if (string.IsNullOrWhiteSpace(query.FullText))
{
if (schema == null)
{
ThrowHelper.InvalidOperationException();
return;
}
return;
}
var textQuery = new TextQuery(query.FullText, 1000)
{
PreferredSchemaId = schema.Id
};
if (schema == null)
{
ThrowHelper.InvalidOperationException();
return;
}
var fullTextIds = await textIndex.SearchAsync(context.App, textQuery, context.Scope());
var fullTextFilter = ClrFilter.Eq("id", "__notfound__");
var textQuery = new TextQuery(query.FullText, 1000)
{
PreferredSchemaId = schema.Id
};
if (fullTextIds?.Any() == true)
{
fullTextFilter = ClrFilter.In("id", fullTextIds.Select(x => x.ToString()).ToList());
}
var fullTextIds = await textIndex.SearchAsync(context.App, textQuery, context.Scope());
var fullTextFilter = ClrFilter.Eq("id", "__notfound__");
if (query.Filter != null)
{
query.Filter = ClrFilter.And(query.Filter, fullTextFilter);
}
else
{
query.Filter = fullTextFilter;
}
if (fullTextIds?.Any() == true)
{
fullTextFilter = ClrFilter.In("id", fullTextIds.Select(x => x.ToString()).ToList());
}
query.FullText = null;
if (query.Filter != null)
{
query.Filter = ClrFilter.And(query.Filter, fullTextFilter);
}
else
{
query.Filter = fullTextFilter;
}
query.FullText = null;
}
private async Task<ClrQuery> ParseClrQueryAsync(Context context, Q q, ISchemaEntity? schema)

6
backend/src/Squidex.Shared/Texts.it.resx

@ -730,6 +730,9 @@
<data name="history.apps.clientUpdated" xml:space="preserve">
<value>Aggiornato client {[Id]}</value>
</data>
<data name="history.apps.common.updated" xml:space="preserve">
<value>updated general settings</value>
</data>
<data name="history.apps.contributoreAssigned" xml:space="preserve">
<value>Associato {user:[Contributor]} al ruolo {[Role]}</value>
</data>
@ -778,9 +781,6 @@
<data name="history.apps.transfered" xml:space="preserve">
<value>updated app to client</value>
</data>
<data name="history.apps.updated" xml:space="preserve">
<value>updated general settings</value>
</data>
<data name="history.apps.workflowAdded" xml:space="preserve">
<value>added workflow {[Name]}.</value>
</data>

6
backend/src/Squidex.Shared/Texts.nl.resx

@ -730,6 +730,9 @@
<data name="history.apps.clientUpdated" xml:space="preserve">
<value>bijgewerkte client {[Id]}</value>
</data>
<data name="history.apps.common.updated" xml:space="preserve">
<value>updated general settings</value>
</data>
<data name="history.apps.contributoreAssigned" xml:space="preserve">
<value>heeft {user:[Contributor]} toegewezen als {[Role]}</value>
</data>
@ -778,9 +781,6 @@
<data name="history.apps.transfered" xml:space="preserve">
<value>updated app to client</value>
</data>
<data name="history.apps.updated" xml:space="preserve">
<value>updated general settings</value>
</data>
<data name="history.apps.workflowAdded" xml:space="preserve">
<value>added workflow {[Name]}.</value>
</data>

6
backend/src/Squidex.Shared/Texts.resx

@ -730,6 +730,9 @@
<data name="history.apps.clientUpdated" xml:space="preserve">
<value>updated client {[Id]}</value>
</data>
<data name="history.apps.common.updated" xml:space="preserve">
<value>updated general settings</value>
</data>
<data name="history.apps.contributoreAssigned" xml:space="preserve">
<value>assigned {user:[Contributor]} as {[Role]}</value>
</data>
@ -778,9 +781,6 @@
<data name="history.apps.transfered" xml:space="preserve">
<value>updated app to client</value>
</data>
<data name="history.apps.updated" xml:space="preserve">
<value>updated general settings</value>
</data>
<data name="history.apps.workflowAdded" xml:space="preserve">
<value>added workflow {[Name]}.</value>
</data>

6
backend/src/Squidex.Shared/Texts.zh.resx

@ -730,6 +730,9 @@
<data name="history.apps.clientUpdated" xml:space="preserve">
<value>更新的客户端 {[Id]}</value>
</data>
<data name="history.apps.common.updated" xml:space="preserve">
<value>updated general settings</value>
</data>
<data name="history.apps.contributoreAssigned" xml:space="preserve">
<value>已分配 {user:[Contributor]} 作为 {[Role]}</value>
</data>
@ -778,9 +781,6 @@
<data name="history.apps.transfered" xml:space="preserve">
<value>updated app to client</value>
</data>
<data name="history.apps.updated" xml:space="preserve">
<value>updated general settings</value>
</data>
<data name="history.apps.workflowAdded" xml:space="preserve">
<value>added workflow {[Name]}.</value>
</data>

199
backend/src/Squidex/wwwroot/scripts/editor-sdk.d.ts

@ -0,0 +1,199 @@
declare class EditorPlugin {
/**
* Get the current context.
*/
getContext(): any;
/**
* Notifies the parent to navigate to the path.
*/
navigate(url: string): void;
/**
* Register an function that is called when the sidebar is initialized.
*
* @param {Function} callback: The callback to invoke.
*/
onInit(callback: () => void): void;
/**
* Register an function that is called whenever the value of the content has changed.
*
* @param {Function} callback: The callback to invoke. Argument 1: Content value (any).
*/
onContentChanged(callback: (content: any) => void): void;
/**
* Clean the editor SDK.
*/
clean(): void;
}
declare class SquidexFormField {
/**
* Get the current value.
*/
getValue(): any;
/**
* Get the current value.
*/
getContext(): any;
/**
* Get the current form value.
*/
getFormValue(): any;
/**
* Get the current field language.
*/
getLanguage(): string | undefined | null;
/**
* Get the current index when the field is an array item.
*/
getIndex(): number | undefined | null;
/**
* Get the disabled state.
*/
isDisabled(): boolean;
/**
* Get the fullscreen state.
*/
isFullscreen(): boolean;
/**
* Get the expanded state.
*/
isExpanded(): boolean;
/**
* Notifies the control container that the editor has been touched.
*/
touched(): void;
/**
* Notifies the parent to navigate to the path.
*
* @param {string} url: The url to navigate to.
*/
navigate(url: string): void;
/**
* Notifies the parent to go to fullscreen mode.
*/
toggleFullscreen(): void;
/**
* Notifies the parent to go to expanded mode.
*/
toggleExpanded(): void;
/**
* Notifies the control container that the value has been changed.
*
* @param {any} newValue: The new field value.
*/
valueChanged(newValue: any): void;
/**
* Shows an info alert.
*
* @param {string} text: The info text.
*/
notifyInfo(text: string): void;
/**
* Shows an error alert.
*
* @param {string} text: error info text.
*/
notifyError(text: string): void;
/**
* Shows an confirm dialog.
*
* @param {string} title The title of the dialog.
* @param {string} text The text of the dialog.
* @param {function} callback The callback to invoke when the dialog is completed or closed.
*/
confirm(title: string, text: string, callback: (result: boolean) => void): void;
/**
* Shows the dialog to pick assets.
*
* @param {function} callback The callback to invoke when the dialog is completed or closed.
*/
pickAssets(callback: (assets: any[]) => void): void;
/**
* Shows the dialog to pick contents.
*
* @param {string} schemas: The list of schema names.
* @param {function} callback The callback to invoke when the dialog is completed or closed.
*/
pickContents(schemas: string[], callback: (assets: any[]) => void): void;
/**
* Register an function that is called when the field is initialized.
*
* @param {Function} callback: The callback to invoke.
*/
onInit(callback: () => void): void;
/**
* Register an function that is called when the field is moved.
*
* @param {Function} callback: The callback to invoke. Argument 1: New position (number).
*/
onMoved(callback: (index: number) => void): void;
/**
* Register an function that is called whenever the field is disabled or enabled.
*
* @param {Function} callback: The callback to invoke. Argument 1: New disabled state (boolean, disabled = true, enabled = false).
*/
onDisabled(callback: (isDisabled: boolean) => void): void;
/**
* Register an function that is called whenever the field language is changed.
*
* @param {Function} callback: The callback to invoke. Argument 1: Language code (string).
*/
onLanguageChanged(callback: (language: string) => void): void;
/**
* Register an function that is called whenever the value of the field has changed.
*
* @param {Function} callback: The callback to invoke. Argument 1: Field value (any).
*/
onValueChanged(callback: (value: any) => void): void;
/**
* Register an function that is called whenever the value of the content has changed.
*
* @param {Function} callback: The callback to invoke. Argument 1: Content value (any).
*/
onFormValueChanged(callback: (value: any) => void): void;
/**
* Register an function that is called whenever the fullscreen mode has changed.
*
* @param {Function} callback: The callback to invoke. Argument 1: Fullscreen state (boolean, fullscreen on = true, fullscreen off = false).
*/
onFullscreen(callback: (isFullscreen: boolean) => void): void;
/**
* Register an function that is called whenever the expanded mode has changed.
*
* @param {Function} callback: The callback to invoke. Argument 1: Expanded state (boolean, expanded on = true, expanded off = false).
*/
onExpanded(callback: (isExpanded: boolean) => void): void;
/**
* Clean the editor SDK.
*/
clean(): void;
}

2
frontend/src/app/features/administration/pages/users/user-page.component.html

@ -2,7 +2,7 @@
<form [formGroup]="userForm.form" (ngSubmit)="save()">
<input style="display: none;" type="password" name="foilautofill">
<sqx-layout layout="simple" [width]="30" [white]="true" [padding]="true" [overflow]="true">
<sqx-layout layout="right" [width]="30" [white]="true" [padding]="true" [overflow]="true">
<ng-container title>
<ng-container *ngIf="usersState.selectedUser | async; else noUserTitle">
<sqx-title message="i18n:users.editPageTitle"></sqx-title>

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

@ -1,3 +1,3 @@
<sqx-layout layout="simple" titleText="i18n:comments.title" [width]="20" [white]="true">
<sqx-layout layout="right" titleText="i18n:comments.title" [width]="20" [white]="true">
<sqx-comments [commentsId]="commentsId | async"></sqx-comments>
</sqx-layout>

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

@ -1,4 +1,4 @@
<sqx-layout layout="simple" titleText="i18n:common.workflow" [width]="20" [white]="true" [overflow]="true" [padding]="true">
<sqx-layout layout="right" titleText="i18n:common.workflow" [width]="20" [white]="true" [overflow]="true" [padding]="true">
<ng-container>
<div class="section mb-4">
<label for="id">{{ 'common.id' | sqxTranslate }}</label>

2
frontend/src/app/features/content/shared/list/content.component.scss

@ -19,7 +19,7 @@ tr {
}
td {
border-bottom: 1px solid $color-border;
border-bottom: .7px solid $color-border;
border-left: 1px solid $color-white;
border-top: 0 !important;
position: relative;

9
frontend/src/app/features/content/shared/preview-button.component.ts

@ -8,7 +8,7 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { ContentDto, interpolate, LocalStoreService, ModalModel, SchemaDto, Settings, StatefulComponent } from '@app/shared';
import { AuthService, ContentDto, interpolate, LocalStoreService, ModalModel, SchemaDto, Settings, StatefulComponent } from '@app/shared';
interface State {
// The name of the selected preview config.
@ -37,6 +37,7 @@ export class PreviewButtonComponent extends StatefulComponent<State> implements
public dropdown = new ModalModel();
constructor(changeDetector: ChangeDetectorRef,
private readonly authService: AuthService,
private readonly localStore: LocalStoreService,
) {
super(changeDetector, {
@ -74,7 +75,11 @@ export class PreviewButtonComponent extends StatefulComponent<State> implements
}
private navigateTo(name: string) {
const url = interpolate(this.schema.previewUrls[name], this.content, 'iv');
const vars = { ...this.content };
vars['accessToken'] = this.authService.user?.accessToken;
const url = interpolate(this.schema.previewUrls[name], vars, 'iv');
window.open(url, '_blank');
}

8
frontend/src/app/features/content/shared/references/reference-item.component.html

@ -49,10 +49,6 @@
<i class="icon-copy"></i>
</button>
<a class="btn btn-text-secondary" target="_blank" [routerLink]="['../..', content.schemaName, content.id]">
<i class="icon-pencil"></i>
</a>
<button type="button" class="btn btn-text-secondary" *ngIf="canRemove"
(sqxConfirmClick)="delete.emit()"
confirmTitle="i18n:contents.removeConfirmTitle"
@ -60,6 +56,10 @@
confirmRememberKey="removeReference">
<i class="icon-close"></i>
</button>
<a class="btn btn-text-secondary" target="_blank" [routerLink]="['../..', content.schemaName, content.id]">
<i class="icon-pencil"></i>
</a>
</div>
</div>
</td>

55
frontend/src/app/features/settings/pages/clients/client-connect-form.component.html

@ -69,7 +69,15 @@
<h5><span class="badge rounded-pill bg-dark">1</span> {{ 'clients.connectWizard.manuallyStep1' | sqxTranslate }}</h5>
<p>
<sqx-code>{{connectHttpText}}</sqx-code>
<sqx-code>
<div ngPreserveWhitespaces>$ curl</div>
<div ngPreserveWhitespaces>-X POST '{{apiUrl.buildUrl('/identity-server/connect/token')}}'</div>
<div ngPreserveWhitespaces>-H 'Content-Type: application/x-www-form-urlencoded'</div>
<div ngPreserveWhitespaces>-d 'grant_type=client_credentials&</div>
<div ngPreserveWhitespaces> client_id={{appName}}:{{client.id}}</div>
<div ngPreserveWhitespaces> client_secret={{client.secret}}</div>
<div ngPreserveWhitespaces> scope=squidex-api'</div>
</sqx-code>
</p>
</div>
@ -77,7 +85,7 @@
<h5><span class="badge rounded-pill bg-dark">2</span> {{ 'clients.connectWizard.manuallyStep2' | sqxTranslate }}</h5>
<p>
<sqx-code>{{connectToken?.accessToken}}</sqx-code>
<sqx-code>{{appToken?.accessToken}}</sqx-code>
</p>
</div>
@ -129,6 +137,12 @@
</div>
</ng-container>
<ng-container *ngSwitchCase="'SDK'">
<div class="section step">
<sqx-form-hint>
{{ 'clients.connectWizard.sdkDocumentation' | sqxTranslate }} <a href="https://docs.squidex.io/02-documentation/software-development-kits/.net-standard" sqxExternalLink>{{ 'common.documentation' | sqxTranslate }}</a>
</sqx-form-hint>
</div>
<div class="section step">
<h5><span class="badge rounded-pill bg-dark">1</span> {{ 'clients.connectWizard.sdkStep1' | sqxTranslate }}</h5>
@ -143,7 +157,42 @@
<h5><span class="badge rounded-pill bg-dark">2</span> {{ 'clients.connectWizard.sdkStep2' | sqxTranslate }}</h5>
<p>
<sqx-code>{{connectLibraryText}}</sqx-code>
<sqx-code>
<div ngPreserveWhitespaces>var clientManager = new SquidexClientManager(new SquidexOptions</div>
<div ngPreserveWhitespaces>{{'{'}}</div>
<div ngPreserveWhitespaces> AppName = {{appName}},</div>
<div ngPreserveWhitespaces> ClientId = {{appName}}:{{client.id}},</div>
<div ngPreserveWhitespaces> ClientSecret = {{client.secret}},</div>
<div ngPreserveWhitespaces> Url = {{apiUrl.value}}</div>
<div ngPreserveWhitespaces>});</div>
</sqx-code>
</p>
</div>
<div class="section step">
<h5><span class="badge rounded-pill bg-dark">3</span> {{ 'clients.connectWizard.sdkStep3' | sqxTranslate }}</h5>
<div [innerHTML]="'clients.connectWizard.sdkStep3Download' | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>
<p>
<sqx-code>dotnet add package Squidex.ClientLibrary.ServiceExtensions</sqx-code>
</p>
</div>
<div class="section step">
<h5><span class="badge rounded-pill bg-dark">4</span> {{ 'clients.connectWizard.sdkStep4' | sqxTranslate }}</h5>
<p>
<sqx-code>
<div ngPreserveWhitespaces>services.AddSquidex(options =></div>
<div ngPreserveWhitespaces>{{'{'}}</div>
<div ngPreserveWhitespaces> options.AppName = {{appName}};</div>
<div ngPreserveWhitespaces> options.ClientId = {{appName}}:{{client.id}};</div>
<div ngPreserveWhitespaces> options.ClientSecret = {{client.secret}};</div>
<div ngPreserveWhitespaces> options.Url = {{apiUrl.value}};</div>
<div ngPreserveWhitespaces>});</div>
</sqx-code>
</p>
</div>

6
frontend/src/app/features/settings/pages/clients/client-connect-form.component.scss

@ -43,4 +43,10 @@
font-size: $font-smallest;
font-weight: normal;
}
}
sqx-code {
pre {
white-space: pre;
}
}

34
frontend/src/app/features/settings/pages/clients/client-connect-form.component.ts

@ -21,10 +21,7 @@ export class ClientConnectFormComponent implements OnInit {
public client!: ClientDto;
public appName!: string;
public connectToken?: AccessTokenDto;
public connectHttpText = '';
public connectLibraryText = '';
public appToken?: AccessTokenDto;
public step = 'Start';
@ -44,13 +41,10 @@ export class ClientConnectFormComponent implements OnInit {
public ngOnInit() {
this.appName = this.appsState.appName;
this.connectHttpText = connectHttpText(this.apiUrl, this.appName, this.client);
this.connectLibraryText = connectLibrary(this.apiUrl, this.appName, this.client);
this.clientsService.createToken(this.appsState.appName, this.client)
.subscribe({
next: dto => {
this.connectToken = dto;
this.appToken = dto;
this.changeDetector.detectChanges();
},
@ -63,26 +57,4 @@ export class ClientConnectFormComponent implements OnInit {
public go(step: string) {
this.step = step;
}
}
function connectHttpText(apiUrl: ApiUrlConfig, app: string, client: { id: string; secret: string }) {
const url = apiUrl.buildUrl('identity-server/connect/token');
return `$ curl
-X POST '${url}'
-H 'Content-Type: application/x-www-form-urlencoded'
-d 'grant_type=client_credentials&
client_id=${app}:${client.id}&
client_secret=${client.secret}&
scope=squidex-api'`;
}
function connectLibrary(apiUrl: ApiUrlConfig, app: string, client: { id: string; secret: string }) {
const url = apiUrl.value;
return `var clientManager = new SquidexClientManager(
"${url}",
"${app}",
"${app}:${client.id}",
"${client.secret}")`;
}
}

2
frontend/src/app/framework/angular/language-selector.stories.tsx

@ -15,7 +15,7 @@ export default {
component: LanguageSelectorComponent,
argTypes: {
size: {
control: 'enum',
control: 'select',
options: [
'sm',
'md',

17
frontend/src/app/framework/angular/layout-container.directive.ts

@ -63,13 +63,14 @@ export class LayoutContainerDirective implements AfterViewInit {
}
let currentSize = 0;
let layoutWidth = this.containerWidth;
let layoutsWidthSpread = 0;
for (const layout of layouts) {
if (layout.desiredWidth > 0) {
const layoutWidth = layout.desiredWidth;
const desiredWidth = layout.computeDesiredWidth(layouts.length, layoutWidth);
layout.measure(`${layoutWidth}rem`);
if (desiredWidth >= 0) {
layout.measure(`${desiredWidth}rem`);
currentSize += layout.renderWidth;
} else {
@ -77,10 +78,12 @@ export class LayoutContainerDirective implements AfterViewInit {
}
}
const spreadWidth = (this.containerWidth - currentSize) / layoutsWidthSpread;
const spreadWidth = (layoutWidth - currentSize) / layoutsWidthSpread;
for (const layout of layouts) {
if (layout.desiredWidth <= 0) {
const desiredWidth = layout.computeDesiredWidth(layouts.length, layoutWidth);
if (desiredWidth < 0) {
layout.measure(`${spreadWidth}px`);
currentSize += layout.renderWidth;
@ -97,8 +100,10 @@ export class LayoutContainerDirective implements AfterViewInit {
currentLayer -= 10;
}
const diff = Math.max(0, currentPosition - this.containerWidth);
const diff = Math.max(0, currentPosition - layoutWidth);
this.renderer.setStyle(this.element.nativeElement, 'overflow-x', diff > 1 ? 'auto' : 'hidden');
this.renderer.setStyle(this.element.nativeElement, 'overflow-y', 'hidden');
this.renderer.setProperty(this.element.nativeElement, 'scrollLeft', diff);
}
}

32
frontend/src/app/framework/angular/layout.component.html

@ -1,8 +1,8 @@
<div class="panel2" #panel>
<ng-container *ngIf="layout === 'simple'">
<div class="panel2-slice simple">
<div class="panel2-header simple" *ngIf="!hideHeader">
<div class="panel2-header-inner simple">
<div class="panel2" #panel [class.minimized]="isMinimized">
<ng-container *ngIf="layout === 'right'">
<div class="panel2-slice right" [style.minWidth]="desiredWidth" [style.maxWidth]="desiredWidth">
<div class="panel2-header right" *ngIf="!hideHeader">
<div class="panel2-header-inner right">
<h3 *ngIf="titleText">
<i class="icon-{{titleIcon}}" *ngIf="titleIcon"></i> {{ titleText | sqxTranslate }}
</h3>
@ -13,9 +13,21 @@
<ng-container *ngTemplateOutlet="menuTemplate"></ng-container>
</div>
</div>
<ng-container *ngIf="route; else noRoute">
<a class="btn panel2-collapse" [routerLink]="['./../']" [queryParamsHandling]="closeQueryParamsHandling" [relativeTo]="route">
<i class="icon-close"></i>
</a>
</ng-container>
<ng-template #noRoute>
<a class="btn panel2-collapse">
<i class="icon-close"></i>
</a>
</ng-template>
</div>
<div class="panel2-main simple">
<div class="panel2-main-inner simple" [class.white]="white" [class.padded]="padding" [class.overflow]="overflow">
<div class="panel2-main right">
<div class="panel2-main-inner right" [class.white]="white" [class.padded]="padding" [class.overflow]="overflow">
<ng-container *ngTemplateOutlet="contentTemplate"></ng-container>
</div>
</div>
@ -23,7 +35,7 @@
</ng-container>
<ng-container *ngIf="layout === 'left'">
<div class="panel2-slice left" [class.collapsed]="isCollapsed">
<div class="panel2-slice left" [class.collapsed]="isCollapsed" [style.minWidth]="desiredWidth" [style.maxWidth]="desiredWidth" (click)="expand($event)">
<div class="panel2-header left" *ngIf="!hideHeader">
<div class="panel2-header-inner left">
<h3 *ngIf="titleText">
@ -37,7 +49,7 @@
</div>
</div>
<button class="btn panel2-collapse" (click)="toggle()">
<button class="btn panel2-collapse" (click)="toggle()" sqxStopClick>
<i class="icon-angle-left"></i>
</button>
@ -75,7 +87,7 @@
</div>
<div class="panel2-slice menu" *ngIf="!hideSidebar">
<div class="panel2-header menu">
<div class="panel2-header menu">
<a class="btn panel2-collapse" [routerLink]="['./']" [queryParamsHandling]="closeQueryParamsHandling" [relativeTo]="route" *ngIf="firstChild | async">
<i class="icon-angle-right"></i>
</a>

106
frontend/src/app/framework/angular/layout.component.ts

@ -7,7 +7,7 @@
/* eslint-disable import/no-cycle */
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core';
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit, Optional, Renderer2, ViewChild } from '@angular/core';
import { ActivatedRoute, NavigationEnd, QueryParamsHandling, Router } from '@angular/router';
import { concat, defer, filter, map, of } from 'rxjs';
import { LayoutContainerDirective } from './layout-container.directive';
@ -36,7 +36,7 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
public titleCollapsed = '';
@Input()
public layout: 'simple' | 'left' | 'main' = 'simple';
public layout: 'left' | 'main' | 'right' = 'main';
@Input()
public width = -1;
@ -69,15 +69,16 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
public panel!: ElementRef<HTMLElement>;
public isCollapsed = false;
public get desiredWidth() {
return this.isCollapsed ? 3 : this.width;
}
public isMinimized = false;
public get desiredInnerWidth() {
return this.innerWidth <= 0 ? '100%' : `${this.innerWidth}rem`;
}
public get desiredWidth() {
return this.width <= 0 ? '100%' : `${this.width}rem`;
}
public get isViewInit() {
return this.isViewInitField;
}
@ -88,22 +89,27 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
public firstChild =
concat(
defer(() => of(!!this.route.firstChild)),
this.router.events.pipe(
defer(() => of(!!this.route?.firstChild)),
this.router?.events.pipe(
filter(event => event instanceof NavigationEnd),
map(() => {
return !!this.route.firstChild;
return !!this.route?.firstChild;
}),
));
) || of({}));
constructor(
private readonly changeDetector: ChangeDetectorRef,
private readonly container: LayoutContainerDirective,
private readonly renderer: Renderer2,
public readonly route: ActivatedRoute,
public readonly router: Router,
@Optional() public readonly route?: ActivatedRoute,
@Optional() public readonly router?: Router,
) {
}
public ngOnDestroy() {
this.container.peek();
}
public ngOnInit() {
this.container.push(this);
}
@ -114,46 +120,76 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
this.container.invalidate();
}
public ngOnDestroy() {
this.container.peek();
public computeDesiredWidth(numberOfLayouts: number, availableWidth: number) {
if (this.layout === 'main') {
return this.width;
}
const isMinimized = availableWidth < 1200 && numberOfLayouts > 1;
if (isMinimized !== this.isMinimized) {
this.isCollapsed = !this.isMinimized;
this.isMinimized = isMinimized;
this.changeDetector.detectChanges();
}
return this.isMinimized || this.isCollapsed ? (this.layout === 'left' ? 3 : 0) : this.width;
}
public measure(size: string) {
if (this.widthPrevious !== size && this.isViewInitField) {
this.widthPrevious = size;
if (!this.isViewInitField || this.widthPrevious === size) {
return;
}
const element = this.panel.nativeElement;
this.widthPrevious = size;
if (element) {
this.renderer.setStyle(element, 'width', size);
const element = this.panel.nativeElement;
if (this.layout === 'main') {
this.renderer.setStyle(element, 'minWidth', `${this.innerWidth + this.innerWidthPadding}rem`);
}
if (!element) {
return;
}
this.widthToRender = element.offsetWidth;
}
this.renderer.setStyle(element, 'width', size);
if (this.layout === 'main') {
this.renderer.setStyle(element, 'minWidth', `${this.innerWidth + this.innerWidthPadding}rem`);
}
this.widthToRender = element.offsetWidth;
}
public arrange(left: any, layer: any) {
if (this.isViewInit) {
const element = this.panel.nativeElement;
if (!this.isViewInitField) {
return;
}
if (element) {
this.renderer.setStyle(element, 'top', '0px');
this.renderer.setStyle(element, 'left', left);
this.renderer.setStyle(element, 'bottom', '0px');
this.renderer.setStyle(element, 'position', 'absolute');
const element = this.panel.nativeElement;
this.renderer.setStyle(element, 'z-index', layer);
}
if (element) {
this.renderer.setStyle(element, 'top', '0px');
this.renderer.setStyle(element, 'left', left);
this.renderer.setStyle(element, 'bottom', '0px');
this.renderer.setStyle(element, 'position', 'absolute');
this.renderer.setStyle(element, 'z-index', layer);
}
}
public toggle() {
this.isCollapsed = !this.isCollapsed;
this.setCollapsed(!this.isCollapsed);
}
this.container.invalidate();
public expand(event: MouseEvent) {
if (event.target?.['nodeName'] !== 'DIV') {
return;
}
this.setCollapsed(false);
}
private setCollapsed(isCollapsed: boolean) {
if (this.isCollapsed !== isCollapsed) {
this.isCollapsed = isCollapsed;
this.container.invalidate();
}
}
}

216
frontend/src/app/framework/angular/layout.stories.tsx

@ -0,0 +1,216 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { moduleMetadata } from '@storybook/angular';
import { Meta, Story } from '@storybook/angular/types-6-0';
import { LayoutComponent, LocalizerService, SqxFrameworkModule } from '@app/framework';
export default {
title: 'Framework/Layout',
component: LayoutComponent,
argTypes: {
titleText: {
control: 'text',
},
titleCollapsed: {
control: 'text',
},
titleIcon: {
control: 'select',
options: [
'',
'help',
'help2',
],
},
layout: {
control: 'select',
options: [
'left',
'main',
'right',
],
defaultValue: 'left',
},
innerWidth: {
control: 'number',
},
},
parameters: {
layout: 'fullscreen',
},
decorators: [
moduleMetadata({
imports: [
BrowserAnimationsModule,
SqxFrameworkModule,
SqxFrameworkModule.forRoot(),
],
providers: [
{ provide: LocalizerService, useValue: new LocalizerService({}) },
],
}),
],
} as Meta;
const Template: Story<LayoutComponent> = (args: LayoutComponent) => ({
props: args,
template: `
<sqx-root-view>
<div sqxLayoutContainer>
<sqx-layout
[layout]="layout"
[innerWidth]="innerWidth"
[titleCollapsed]="titleCollapsed"
[titleIcon]="titleIcon"
[titleText]="titleText">
<div>
<sqx-list-view [innerWidth]="innerWidth + 'rem'">
<div class="card">
<div class="card-body">
Content
</div>
</div>
<div class="card mt-2">
<div class="card-body">
Content
</div>
</div>
<div class="card mt-2">
<div class="card-body">
Content
</div>
</div>
</sqx-list-view>
</div>
</sqx-layout>
</div>
</sqx-root-view>
`,
});
const ComplexTemplate: Story<LayoutComponent> = (args: LayoutComponent) => ({
props: args,
template: `
<sqx-root-view>
<div sqxLayoutContainer>
<sqx-layout titleText="Left" layout="left" width="15">
<div>
<sqx-list-view>
<div class="card">
<div class="card-body">
Content
</div>
</div>
<div class="card mt-2">
<div class="card-body">
Content
</div>
</div>
<div class="card mt-2">
<div class="card-body">
Content
</div>
</div>
</sqx-list-view>
</div>
</sqx-layout>
<sqx-layout layout="main"
[innerWidth]="innerWidth"
[titleCollapsed]="titleCollapsed"
[titleIcon]="titleIcon"
[titleText]="titleText">
<div>
<sqx-list-view [innerWidth]="innerWidth + 'rem'">
<div class="card">
<div class="card-body">
Content
</div>
</div>
<div class="card mt-2">
<div class="card-body">
Content
</div>
</div>
<div class="card mt-2">
<div class="card-body">
Content
</div>
</div>
</sqx-list-view>
</div>
</sqx-layout>
<sqx-layout titleText="Simple" layout="right" width="15">
<div class="p-4">
<div class="card">
<div class="card-body">
Content
</div>
</div>
<div class="card mt-2">
<div class="card-body">
Content
</div>
</div>
<div class="card mt-2">
<div class="card-body">
Content
</div>
</div>
</div>
</sqx-layout>
</div>
</sqx-root-view>
`,
});
export const Empty = Template.bind({});
Empty.args = {
titleText: 'Title',
};
export const Icon = Template.bind({});
Icon.args = {
titleText: 'Title',
titleIcon: 'help',
};
export const InnerWidth = Template.bind({});
InnerWidth.args = {
titleText: 'Title',
titleIcon: '',
innerWidth: 30,
layout: 'main',
};
export const Left = Template.bind({});
Left.args = {
titleText: 'Title',
titleCollapsed: 'I am collapsed',
layout: 'left',
};
export const Right = Template.bind({});
Right.args = {
titleText: 'Title',
titleCollapsed: 'I am collapsed',
layout: 'right',
};
export const Complex = ComplexTemplate.bind({});
Complex.args = {
titleText: 'Main',
titleIcon: 'help',
innerWidth: 20,
};

2
frontend/src/app/framework/angular/loader.stories.ts

@ -17,7 +17,7 @@ export default {
control: 'number',
},
color: {
control: 'enum',
control: 'select',
options: [
'white',
'theme',

4
frontend/src/app/framework/angular/status-icon.stories.tsx

@ -13,7 +13,7 @@ export default {
component: StatusIconComponent,
argTypes: {
status: {
control: 'enum',
control: 'select',
options: [
'Failed',
'Success',
@ -25,7 +25,7 @@ export default {
control: 'text',
},
size: {
control: 'enum',
control: 'select',
options: [
'sm',
'md',

1
frontend/src/app/shared/components/forms/markdown-editor.component.scss

@ -6,7 +6,6 @@ $background: #fff;
:host ::ng-deep {
/* stylelint-disable-next-line selector-class-pattern */
.CodeMirror {
box-sizing: content-box;
height: 300px;
padding-bottom: 10px;
padding-top: 10px;

2
frontend/src/app/shared/components/help/help.component.html

@ -1,4 +1,4 @@
<sqx-layout titleText="i18n:common.help" titleIcon="help2" [width]="20" layout="simple" [white]="true" [padding]="true" [overflow]="true">
<sqx-layout titleText="i18n:common.help" titleIcon="help2" [width]="20" layout="right" [white]="true" [padding]="true" [overflow]="true">
<div class="help">
<div [innerHTML]="helpMarkdown | async | sqxHelpMarkdown"></div>
</div>

2
frontend/src/app/shared/components/history/history.component.html

@ -1,3 +1,3 @@
<sqx-layout layout="simple" titleText="i18n:history.title" [width]="20" titleIcon="time" [white]="true">
<sqx-layout layout="right" titleText="i18n:history.title" [width]="20" titleIcon="time" [white]="true">
<sqx-history-list [events]="events | async"></sqx-history-list>
</sqx-layout>

20
frontend/src/app/theme/_mixins.scss

@ -89,12 +89,28 @@
width: $width;
}
@mixin force-width-important($width) {
// Ensure that we use the minimum width in flex scenarios.
max-width: $width !important;
min-width: $width !important;
// Normal width definition.
width: $width !important;
}
@mixin force-height($height) {
// Normal height definition.
height: $height;
// Ensure that we use the minimum height in flex scenarios.
max-height: $height;
min-height: $height;
// Normal height definition.
height: $height;
}
@mixin force-height-important($height) {
// Ensure that we use the minimum height in flex scenarios.
max-height: $height !important;
min-height: $height !important;
// Normal height definition.
height: $height !important;
}
@mixin caret-top($color, $size: .6rem) {

70
frontend/src/app/theme/_panels2.scss

@ -43,9 +43,35 @@
align-items: stretch;
display: flex;
flex-direction: row;
flex-grow: inherit;
&.minimized {
z-index: 30 !important;
& > .panel2-slice {
&.right {
@include absolute(0, 0, 0);
@include box-shadow-outer(0, -2px, 3px, .13);
}
&.left {
@include box-shadow-outer(0, 2px, 3px, .13);
max-width: none;
}
&.collapsed {
box-shadow: none;
}
}
.panel2-collapse {
display: inline !important;
}
}
&-slice {
align-items: stretch;
background-color: $color-background;
display: flex;
flex-direction: column;
flex-grow: 0;
@ -58,8 +84,12 @@
flex-shrink: 1;
max-width: 100%;
.panel2-sidebar-title {
white-space: nowrap;
}
&.collapsed {
width: 3.25rem !important;
@include force-width-important(3.25rem);
.panel2-sidebar-title {
opacity: 1;
@ -73,15 +103,13 @@
display: none;
}
.panel2-header {
padding-left: 0;
padding-right: 0;
width: 3rem !important;
}
.panel2-collapse {
transform: rotate(180deg);
}
& {
cursor: pointer;
}
}
}
@ -106,7 +134,7 @@
overflow: hidden;
}
&.simple {
&.right {
border: 0;
border-left: 1px solid $color-border;
width: 100%;
@ -177,6 +205,15 @@
@include absolute(50%, null, null, .5rem);
}
}
&.right {
.panel2-collapse {
@include absolute(55%, .5rem, null, null);
display: none;
font-size: .9rem;
font-weight: lighter;
}
}
}
&-collapse {
@ -211,7 +248,7 @@
width: 100%;
}
&.simple {
&.right {
width: 100%;
}
@ -234,8 +271,7 @@
}
&-sidebar-title {
@include absolute(7rem, 1.5rem);
font-size: $font-small;
@include absolute(7rem, 1.75rem);
opacity: 0;
transform: rotate(270deg);
transform-origin: right;
@ -278,8 +314,8 @@
}
.nav-link {
border: 0;
border-radius: $border-radius;
border: 0;
color: lighten($color-text, 20%);
cursor: pointer;
font-size: inherit;
@ -294,17 +330,19 @@
margin-right: 1rem;
}
&:hover,
&:active,
&.active {
background-color: $color-theme-brand-light;
color: $color-theme-brand;
cursor: default;
font-weight: 500;
}
&:hover {
background-color: $color-theme-brand-light;
color: $color-text;
}
&.active {
color: $color-theme-brand;
}
}
.nav-heading {

10
frontend/src/app/theme/_vars.scss

@ -78,14 +78,16 @@ $panel-sidebar: 3.75rem;
$font-small: 85%;
$font-smallest: 80%;
$headings-line-height: 1.3;
$btn-disabled-color: inherit;
$btn-disabled-bg: inherit;
$btn-disabled-border-color: inherit;
$history-dot-size: 10px;
$history-dot-offset-x: -($history-dot-size * .5 + 1px);
$history-dot-sm-size: 6px;
$history-dot-sm-offset-x: -($history-dot-sm-size * .5 + 1px);
$history-dot-size: 10px;
$history-dot-offset-x: -($history-dot-size * .5 + 1px);
$history-dot-sm-size: 6px;
$history-dot-sm-offset-x: -($history-dot-sm-size * .5 + 1px);
$asset-width: 14.5rem;
$asset-folder-height: 4rem;

6
frontend/src/config/webpack.config.js

@ -74,11 +74,11 @@ module.exports = (config, _, options) => {
if (index >= 0) {
config.plugins.splice(index, 1);
}
config.plugins.push(new plugins.MiniCssExtractPlugin({
filename: '[name].css',
}));
/*
* Specifies the name of each output file on disk.
*
@ -93,7 +93,7 @@ module.exports = (config, _, options) => {
*/
config.output.chunkFilename = '[id].[fullhash].chunk.js';
/*
/*
* The filename for assets.
*/
config.output.assetModuleFilename = 'assets/[hash][ext][query]';

Loading…
Cancel
Save