Browse Source

Optimistic concurrency fixes.

pull/65/head
Sebastian Stehle 9 years ago
parent
commit
ccbdcbac41
  1. 2
      src/Squidex/Controllers/Api/Assets/AssetContentController.cs
  2. 4
      src/Squidex/Controllers/Api/Assets/AssetController.cs
  3. 12
      src/Squidex/app/features/assets/pages/asset.component.html
  4. 155
      src/Squidex/app/features/assets/pages/asset.component.scss
  5. 45
      src/Squidex/app/features/assets/pages/asset.component.ts
  6. 9
      src/Squidex/app/features/assets/pages/assets-page.component.html
  7. 26
      src/Squidex/app/features/assets/pages/assets-page.component.ts
  8. 2
      src/Squidex/app/features/schemas/pages/schema/field.component.scss
  9. 7
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  10. 5
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html
  11. 2
      src/Squidex/app/framework/angular/autocomplete.component.html
  12. 4
      src/Squidex/app/framework/angular/autocomplete.component.scss
  13. 2
      src/Squidex/app/framework/angular/autocomplete.component.ts
  14. 2
      src/Squidex/app/framework/angular/json-editor.component.scss
  15. 2
      src/Squidex/app/framework/angular/rich-editor.component.scss
  16. 4
      src/Squidex/app/framework/angular/slider.component.scss
  17. 2
      src/Squidex/app/framework/angular/tag-editor.component.html
  18. 2
      src/Squidex/app/framework/angular/tag-editor.component.scss
  19. 2
      src/Squidex/app/framework/angular/tag-editor.component.ts
  20. 2
      src/Squidex/app/framework/angular/toggle.component.scss
  21. 13
      src/Squidex/app/shared/services/assets.service.ts
  22. 2
      src/Squidex/app/shell/pages/home/home-page.component.scss
  23. 4
      src/Squidex/app/shell/pages/internal/apps-menu.component.html
  24. 2
      src/Squidex/app/shell/pages/internal/apps-menu.component.scss
  25. 8
      src/Squidex/app/shell/pages/internal/apps-menu.component.ts
  26. 2
      src/Squidex/app/shell/pages/internal/internal-area.component.scss
  27. 6
      src/Squidex/app/theme/_bootstrap.scss
  28. 8
      src/Squidex/app/theme/_forms.scss
  29. 2
      src/Squidex/app/theme/_lists.scss
  30. 2
      src/Squidex/app/theme/_panels.scss
  31. 3
      src/Squidex/app/theme/_vars.scss

2
src/Squidex/Controllers/Api/Assets/AssetContentController.cs

@ -90,3 +90,5 @@ namespace Squidex.Controllers.Api.Assets
}
}
}
#pragma warning restore 1573

4
src/Squidex/Controllers/Api/Assets/AssetController.cs

@ -177,8 +177,8 @@ namespace Squidex.Controllers.Api.Assets
/// 404 => Asset or app not found.
/// </returns>
[HttpDelete]
[Route("apps/{app}/schemas/{name}/")]
public async Task<IActionResult> DeleteSchema(string app, Guid id)
[Route("apps/{app}/assets/{id}/")]
public async Task<IActionResult> DeleteAsset(string app, Guid id)
{
await CommandBus.PublishAsync(new DeleteAsset { AssetId = id });

12
src/Squidex/app/features/assets/pages/asset.component.html

@ -3,10 +3,12 @@
<div *ngIf="asset && progress == 0" [@fade]>
<div class="card-block">
<div class="file-preview">
<div class="file-type">{{fileType}}</div>
<span class="file-type">
{{fileType}}
</span>
<div *ngIf="asset.isImage">
<img [attr.src]="previewUrl" sqxHideInvalidImage>
<img [attr.src]="previewUrl" sqxHideInvalidImage (error)="retryLoadingImage()">
</div>
<div *ngIf="!asset.isImage" class="file-icon-container">
<img class="file-icon" [attr.src]="fileIcon" sqxHideInvalidImage>
@ -18,9 +20,13 @@
<a class="file-download" [attr.href]="downloadUrl" target="_blank">
<i class="icon-download"></i>
</a>
<a class="file-delete">
<a class="file-delete" (click)="deleting.emit(asset)">
<i class="icon-delete"></i>
</a>
<span class="file-overlay-type">
{{fileType}}
</span>
<span class="file-user">
<i class="icon-user"></i> {{userName(asset.lastModifiedBy, true) | async}}
</span>

155
src/Squidex/app/features/assets/pages/asset.component.scss

@ -3,8 +3,51 @@
$card-size: 240px;
$color-type-background: #000;
$color-type-foreground: #fff;
@mixin overlay-container {
position: relative;
}
@mixin overlay {
& {
@include transition(opacity.4s ease);
@include absolute(0, 0, 0, 0);
@include opacity(0);
color: $color-dark-foreground;
}
&-background {
@include absolute(0, 0, 0, 0);
@include opacity(.7);
background: $color-dark-black;
}
}
@mixin asset-type {
padding: .1rem .3rem;
text-transform: uppercase;
font-size: .7rem;
font-weight: normal;
cursor: none;
color: $color-dark-foreground;
}
@mixin asset-link {
& {
font-size: 1.1rem;
font-weight: normal;
cursor: pointer;
color: darken($color-dark-foreground, 10%);
}
&:hover {
color: $color-dark-foreground;
}
&:focus,
&:hover {
text-decoration: none;
}
}
:host {
padding-bottom: 1rem;
@ -12,31 +55,21 @@ $color-type-foreground: #fff;
.drop-overlay {
& {
@include transition(opacity.4s ease);
@include absolute(0, 0, 0, 0);
@include opacity(0);
@include overlay;
pointer-events: none;
}
&-text {
@include absolute(0, 0, 0, 0);
@include absolute(40%, 0, 0, 0);
text-align: center;
line-height: 14rem;
font-size: 1.2rem;
font-size: 1.3rem;
font-weight: lighter;
color: $color-type-foreground;
}
&-background {
@include absolute(0, 0, 0, 0);
@include opacity(.7);
background: $color-type-background;
}
}
.card {
& {
position: relative;
@include overlay-container;
height: $card-size;
}
@ -69,7 +102,10 @@ $color-type-foreground: #fff;
}
&-preview {
position: relative;
& {
@include overlay-container;
height: 155px;
}
&:hover {
.file-overlay {
@ -82,23 +118,6 @@ $color-type-foreground: #fff;
}
}
&-icon {
&-container {
background: $color-border;
border: 0;
height: 155px;
line-height: 155px;
text-align: center;
}
}
&-name {
@include truncate;
font-size: 1rem;
font-weight: normal;
line-height: 2rem;
}
&-user {
@include absolute(auto, auto, 1.7rem, .5rem);
}
@ -108,63 +127,47 @@ $color-type-foreground: #fff;
}
&-delete {
& {
@include absolute(.5rem, 1rem, auto, auto);
font-size: 1.1rem;
font-weight: normal;
cursor: pointer;
color: $color-accent-dark;
}
&:focus,
&:hover {
text-decoration: none;
}
@include asset-link;
@include absolute(.5rem, 1rem, auto, auto);
}
&-download {
& {
@include absolute(.5rem, 2.5rem, auto, auto);
font-size: 1.1rem;
font-weight: normal;
cursor: pointer;
color: $color-accent-dark;
}
&:focus,
&:hover {
text-decoration: none;
}
@include asset-link;
@include absolute(.5rem, 2.5rem, auto, auto);
}
&-type {
@include transition(opacity.4s ease);
@include absolute(.5rem, auto, auto, .5rem);
@include border-radius(2px);
@include opacity(.8);
background: $color-type-background;
border: 0;
padding: .1rem .3rem;
text-transform: uppercase;
font-size: .7rem;
@include absolute(.7rem, auto, auto, .5rem);
@include asset-type;
@include border-radius(3px);
background: $color-dark-black;
}
&-name {
@include truncate;
font-size: 1rem;
font-weight: normal;
color: $color-type-foreground;
line-height: 2rem;
}
&-icon-container {
background: $color-border;
border: 0;
line-height: 155px;
text-align: center;
}
&-overlay {
& {
@include transition(opacity.4s ease);
@include absolute(0, 0, 0, 0);
@include opacity(0);
font-size: .9rem;
@include overlay;
font-size: .8rem;
font-weight: normal;
color: $color-accent-dark;
}
&-background {
@include absolute(0, 0, 0, 0);
@include opacity(.7);
background: $color-type-background;
&-type {
@include absolute(.7rem, auto, auto, .5rem);
@include asset-type;
}
}
}

45
src/Squidex/app/features/assets/pages/asset.component.ts

@ -20,7 +20,8 @@ import {
fadeAnimation,
MathHelper,
NotificationService,
UsersProviderService
UsersProviderService,
Version
} from 'shared';
@Component({
@ -32,7 +33,9 @@ import {
]
})
export class AssetComponent extends AppComponentBase implements OnInit {
private assetVersion = MathHelper.guid();
private assetUrlVersion = MathHelper.guid();
private retries = 0;
private version: Version;
@Input()
public initFile: File;
@ -41,7 +44,10 @@ export class AssetComponent extends AppComponentBase implements OnInit {
public asset: AssetDto;
@Output()
public deleting = new EventEmitter();
public loaded = new EventEmitter<AssetDto>();
@Output()
public deleting = new EventEmitter<AssetDto>();
@Output()
public failed = new EventEmitter();
@ -49,11 +55,11 @@ export class AssetComponent extends AppComponentBase implements OnInit {
public progress = 0;
public get previewUrl(): string {
return this.apiUrl.buildUrl(`api/assets/${this.asset.id}/?width=230&height=155&mode=Crop&q=${this.assetVersion}`);
return this.apiUrl.buildUrl(`api/assets/${this.asset.id}/?width=230&height=155&mode=Crop&q=${this.assetUrlVersion}`);
}
public get downloadUrl(): string {
return this.apiUrl.buildUrl(`api/assets/${this.asset.id}/?q=${this.assetVersion}`);
return this.apiUrl.buildUrl(`api/assets/${this.asset.id}/?q=${this.assetUrlVersion}`);
}
public get fileType(): string {
@ -102,7 +108,7 @@ export class AssetComponent extends AppComponentBase implements OnInit {
const me = `subject:${this.authService.user!.id}`;
const asset = new AssetDto(
this.asset.id,
result.id,
me, me,
DateTime.now(),
DateTime.now(),
@ -115,9 +121,12 @@ export class AssetComponent extends AppComponentBase implements OnInit {
result.version);
this.asset = asset;
this.assetVersion = MathHelper.guid();
this.assetUrlVersion = MathHelper.guid();
this.version = result.version;
this.progress = 0;
}, 2000);
this.loaded.emit(asset);
}, 500);
} else {
this.progress = result;
}
@ -126,13 +135,15 @@ export class AssetComponent extends AppComponentBase implements OnInit {
this.notifyError(error);
});
} else {
this.version = this.asset.version;
}
}
public updateFile(files: FileList) {
if (files.length === 1) {
this.appName()
.switchMap(app => this.assetsService.replaceFile(app, this.asset.id, files[0]))
.switchMap(app => this.assetsService.replaceFile(app, this.asset.id, files[0], this.version))
.subscribe(result => {
if (result instanceof AssetReplacedDto) {
setTimeout(() => {
@ -149,10 +160,12 @@ export class AssetComponent extends AppComponentBase implements OnInit {
result.pixelWidth,
result.pixelHeight,
result.version);
this.asset = asset;
this.assetVersion = MathHelper.guid();
this.assetUrlVersion = MathHelper.guid();
this.progress = 0;
}, 2000);
this.retries = 0;
}, 500);
} else {
this.progress = result;
}
@ -163,6 +176,16 @@ export class AssetComponent extends AppComponentBase implements OnInit {
});
}
}
public retryLoadingImage() {
this.retries++;
if (this.retries <= 10) {
setTimeout(() => {
this.assetUrlVersion = MathHelper.guid();
}, this.retries * 1000);
}
}
}
function fileSize(b: number) {

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

@ -36,8 +36,13 @@
</div>
<div class="row">
<sqx-asset class="col-3" *ngFor="let file of newFiles" [initFile]="file" (failed)="removeFile(file)"></sqx-asset>
<sqx-asset class="col-3" *ngFor="let asset of assetsItems" [asset]="asset"></sqx-asset>
<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)">
</sqx-asset>
</div>
<div class="clearfix" *ngIf="assetsPager.numberOfItems > 0">

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

@ -66,6 +66,28 @@ export class AssetsPageComponent extends AppComponentBase implements OnInit {
});
}
public onAssetDeleting(asset: AssetDto) {
this.appName()
.switchMap(app => this.assetsService.deleteAsset(app, asset.id, asset.version))
.subscribe(dtos => {
this.assetsItems = this.assetsItems.filter(x => x.id !== asset.id);
this.assetsPager = this.assetsPager.decrementCount();
}, error => {
this.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 onAssetFailed(file: File) {
this.newFiles = this.newFiles.remove(file);
}
public goNext() {
this.assetsPager = this.assetsPager.goNext();
@ -78,10 +100,6 @@ export class AssetsPageComponent extends AppComponentBase implements OnInit {
this.load();
}
public removeFile(file: File) {
this.newFiles = this.newFiles.remove(file);
}
public addFiles(files: FileList) {
for (let i = 0; i < files.length; i++) {
this.newFiles = this.newFiles.pushFront(files[i]);

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

@ -36,7 +36,7 @@ $field-header: #e7ebef;
&.active {
background: $color-theme-blue;
border-color: $color-theme-blue;
color: $color-accent-dark;
color: $color-dark-foreground;
}
}

7
src/Squidex/app/features/schemas/pages/schema/schema-page.component.html

@ -114,12 +114,7 @@
</div>
<div class="modal-body">
<sqx-schema-edit-form
[appName]="appName() | async"
[schema]="schemaProperties"
[version]="version"
(saved)="onSchemaSaved($event)"
(cancelled)="editSchemaDialog.hide()"></sqx-schema-edit-form>
<sqx-schema-edit-form [appName]="appName() | async" [schema]="schemaProperties" [version]="version" (saved)="onSchemaSaved($event)" (cancelled)="editSchemaDialog.hide()"></sqx-schema-edit-form>
</div>
</div>
</div>

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

@ -62,10 +62,7 @@
</div>
<div class="modal-body">
<sqx-schema-form
[appName]="appName() | async"
(created)="onSchemaCreated($event)"
(cancelled)="addSchemaDialog.hide()"></sqx-schema-form>
<sqx-schema-form [appName]="appName() | async" (created)="onSchemaCreated($event)" (cancelled)="addSchemaDialog.hide()"></sqx-schema-form>
</div>
</div>
</div>

2
src/Squidex/app/framework/angular/autocomplete.component.html

@ -1,5 +1,5 @@
<span>
<input type="text" class="form-control" (blur)="blur()" [attr.name]="inputName" (keydown)="onKeyDown($event)" (blur)="onBlur()"
<input type="text" class="form-control" (blur)="blur()" [attr.name]="inputName" (keydown)="onKeyDown($event)" (blur)="markTouched()"
[formControl]="queryInput"
autocomplete="off"
autocorrect="off"

4
src/Squidex/app/framework/angular/autocomplete.component.scss

@ -15,7 +15,7 @@ $color-input-border: rgba(0, 0, 0, .15);
width: 18rem;
max-height: 12rem;
border: 1px solid $color-input-border;
background: $color-accent-dark;
background: $color-dark-foreground;
padding: .3rem 0;
overflow-y: auto;
}
@ -29,7 +29,7 @@ $color-input-border: rgba(0, 0, 0, .15);
}
&.active {
color: $color-accent-dark;
color: $color-dark-foreground;
background: $color-theme-blue;
}

2
src/Squidex/app/framework/angular/autocomplete.component.ts

@ -115,7 +115,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
});
}
public onBlur() {
public markTouched() {
this.touchedCallback();
}

2
src/Squidex/app/framework/angular/json-editor.component.scss

@ -2,7 +2,7 @@
@import '_vars';
.editor {
background: $color-accent-dark;
background: $color-dark-foreground;
border: 1px solid $color-input;
height: 30rem;
}

2
src/Squidex/app/framework/angular/rich-editor.component.scss

@ -2,7 +2,7 @@
@import '_vars';
.editor {
background: $color-accent-dark;
background: $color-dark-foreground;
border: 1px solid $color-input;
height: 30rem;
}

4
src/Squidex/app/framework/angular/slider.component.scss

@ -15,7 +15,7 @@ $thumb-margin: ($thumb-size - $bar-height) * .5;
margin-bottom: 1.25rem;
margin-top: .25rem;
margin-right: $thumb-size * .5;
background: $color-accent-dark;
background: $color-dark-foreground;
height: $bar-height;
}
@ -31,7 +31,7 @@ $thumb-margin: ($thumb-size - $bar-height) * .5;
width: $thumb-size;
height: $thumb-size;
border: 1px solid $color-control;
background: $color-accent-dark;
background: $color-dark-foreground;
margin-top: -$thumb-margin;
margin-left: -$thumb-size * .5;
}

2
src/Squidex/app/framework/angular/tag-editor.component.html

@ -1,4 +1,4 @@
<input type="text" class="form-control" [attr.name]="inputName" (keydown)="onKeyDown($event)" (blur)="onBlur()"
<input type="text" class="form-control" [attr.name]="inputName" (keydown)="onKeyDown($event)" (blur)="markTouched()"
[formControl]="addInput"
autocomplete="off"
autocorrect="off"

2
src/Squidex/app/framework/angular/tag-editor.component.scss

@ -10,7 +10,7 @@
& {
@include border-radius(.8rem);
display: inline-block;
color: $color-accent-dark;
color: $color-dark-foreground;
margin-right: .4rem;
margin-bottom: .3rem;
min-height: 1.6rem;

2
src/Squidex/app/framework/angular/tag-editor.component.ts

@ -104,7 +104,7 @@ export class TagEditorComponent implements ControlValueAccessor {
this.updateItems([...this.items.slice(0, index), ...this.items.splice(index + 1)]);
}
public onBlur() {
public markTouched() {
this.touchedCallback();
}

2
src/Squidex/app/framework/angular/toggle.component.scss

@ -11,7 +11,7 @@ $toggle-button-size: $toggle-height - .3rem;
@include box-shadow(0, 2px, 2px, .2);
@include absolute($toggle-height * .5, auto, auto, $toggle-width * .5);
@include transition(left .3s ease);
background: $color-accent-dark;
background: $color-dark-foreground;
border: 0;
margin-left: -$toggle-button-size * .5;
margin-top: -$toggle-button-size * .5;

13
src/Squidex/app/shared/services/assets.service.ts

@ -170,7 +170,7 @@ export class AssetsService {
});
}
public replaceFile(appName: string, id: string, file: File): Observable<number | AssetReplacedDto> {
public replaceFile(appName: string, id: string, file: File, version?: Version): Observable<number | AssetReplacedDto> {
return new Observable<number | AssetReplacedDto>(subscriber => {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}/content`);
@ -179,6 +179,10 @@ export class AssetsService {
'Authorization': `${this.authService.user.user.token_type} ${this.authService.user.user.access_token}`
});
if (version && version.value.length > 0) {
headers.append('If-Match', version.value);
}
content.append('file', file);
this.http.withUploadProgressListener(progress => subscriber.next(progress.percentage))
@ -203,4 +207,11 @@ export class AssetsService {
});
});
}
public deleteAsset(appName: string, id: string, version: Version): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/${id}`);
return this.authService.authDelete(url, version)
.catchError('Failed to delete asset. Please reload.');
}
}

2
src/Squidex/app/shell/pages/home/home-page.component.scss

@ -50,7 +50,7 @@ $color-google-dark: #af2c1a;
&-button {
& {
@include border-radius(1.5rem);
color: $color-accent-dark;
color: $color-dark-foreground;
cursor: pointer;
margin-top: 4rem;
height: 3rem;

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

@ -36,9 +36,7 @@
</div>
<div class="modal-body">
<sqx-app-form
(created)="onAppCreationCompleted($event)"
(cancelled)="onAppCreationCancelled()"></sqx-app-form>
<sqx-app-form (created)="modalDialog.hide()" (cancelled)="modalDialog.hide()"></sqx-app-form>
</div>
</div>
</div>

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

@ -9,7 +9,7 @@
@include opacity(.95);
@include no-selection;
@include border-radius;
color: $color-accent-dark;
color: $color-dark-foreground;
cursor: pointer;
border: 0;
background: $color-theme-blue-dark;

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

@ -56,14 +56,6 @@ export class AppsMenuComponent implements OnInit, OnDestroy {
this.appsStore.selectedApp.subscribe(selectedApp => this.appName = selectedApp ? selectedApp.name : FALLBACK_NAME);
}
public onAppCreationCancelled() {
this.modalDialog.hide();
}
public onAppCreationCompleted(app: AppDto) {
this.modalDialog.hide();
}
public createApp() {
this.modalMenu.hide();
this.modalDialog.show();

2
src/Squidex/app/shell/pages/internal/internal-area.component.scss

@ -33,7 +33,7 @@
margin-top: .625rem;
font-size: .8rem;
font-weight: normal;
color: $color-accent-dark;
color: $color-dark-foreground;
cursor: pointer;
}

6
src/Squidex/app/theme/_bootstrap.scss

@ -61,7 +61,7 @@ body {
&.dropdown-item {
&:active {
color: $color-accent-dark;
color: $color-dark-foreground;
}
}
}
@ -208,11 +208,11 @@ body {
@include box-shadow(0, 0, 10px, .5);
background: $color-theme-blue;
border-color: $color-theme-blue;
color: $color-accent-dark;
color: $color-dark-foreground;
}
&:hover {
color: $color-accent-dark;
color: $color-dark-foreground;
}
}

8
src/Squidex/app/theme/_forms.scss

@ -32,7 +32,7 @@
& {
@include absolute(auto, auto, .4rem, 0);
@include border-radius(2px);
color: $color-accent-dark;
color: $color-dark-foreground;
cursor: none;
display: inline-block;
font-size: .9rem;
@ -68,7 +68,7 @@ select {
.form-error {
@include border-radius(4px);
@include truncate;
color: $color-accent-dark;
color: $color-dark-foreground;
margin-top: .25rem;
margin-bottom: .5rem;
padding: .5rem;
@ -106,10 +106,10 @@ select {
.form-control-dark {
& {
@include transition(background-color .3s ease);
@include placeholder-color(darken($color-accent-dark, 30%));
@include placeholder-color(darken($color-dark-foreground, 30%));
background: $color-dark2-control;
border: 1px solid $color-dark2-control;
color: darken($color-accent-dark, 20%);
color: darken($color-dark-foreground, 20%);
}
&:focus {

2
src/Squidex/app/theme/_lists.scss

@ -62,7 +62,7 @@
&.active {
background: $color-theme-blue;
border-color: $color-theme-blue;
color: $color-accent-dark;
color: $color-dark-foreground;
}
}
}

2
src/Squidex/app/theme/_panels.scss

@ -111,7 +111,7 @@
}
&.active {
color: $color-accent-dark;
color: $color-dark-foreground;
border: 0;
background: $color-theme-blue;
}

3
src/Squidex/app/theme/_vars.scss

@ -50,7 +50,8 @@ $color-table-header: #a0a0a0;
$color-modal-header-background: #2e3842;
$color-modal-header-foreground: #6a7681;
$color-accent-dark: #fff;
$color-dark-black: #000;
$color-dark-foreground: #fff;
$size-navbar-height: 3.25rem;
$size-sidebar-width: 7rem;

Loading…
Cancel
Save