Browse Source

Merge branch 'master' of github.com:Squidex/squidex

# Conflicts:
#	backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
pull/949/head
Sebastian 4 years ago
parent
commit
2edd606714
  1. 116
      .github/workflows/dev.yml
  2. 141
      .github/workflows/release.yml
  3. 9
      Dockerfile
  4. 52
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  5. 2
      frontend/src/app/features/administration/pages/users/user-page.component.html
  6. 2
      frontend/src/app/features/content/pages/comments/comments-page.component.html
  7. 2
      frontend/src/app/features/content/pages/content/content-history-page.component.html
  8. 2
      frontend/src/app/features/content/shared/list/content.component.scss
  9. 13
      frontend/src/app/features/rules/pages/rules/rule.component.html
  10. 10
      frontend/src/app/features/settings/pages/clients/client.component.html
  11. 7
      frontend/src/app/features/settings/pages/workflows/workflow-step.component.html
  12. 11
      frontend/src/app/framework/angular/avatar.stories.tsx
  13. 63
      frontend/src/app/framework/angular/forms/editable-title.component.html
  14. 46
      frontend/src/app/framework/angular/forms/editable-title.component.scss
  15. 51
      frontend/src/app/framework/angular/forms/editable-title.component.ts
  16. 134
      frontend/src/app/framework/angular/forms/editable-title.stories.ts
  17. 2
      frontend/src/app/framework/angular/language-selector.stories.tsx
  18. 17
      frontend/src/app/framework/angular/layout-container.directive.ts
  19. 32
      frontend/src/app/framework/angular/layout.component.html
  20. 106
      frontend/src/app/framework/angular/layout.component.ts
  21. 216
      frontend/src/app/framework/angular/layout.stories.tsx
  22. 2
      frontend/src/app/framework/angular/loader.stories.ts
  23. 4
      frontend/src/app/framework/angular/status-icon.stories.tsx
  24. 1
      frontend/src/app/shared/components/forms/markdown-editor.component.scss
  25. 2
      frontend/src/app/shared/components/help/help.component.html
  26. 2
      frontend/src/app/shared/components/history/history.component.html
  27. 15
      frontend/src/app/shared/components/schema-category.component.html
  28. 19
      frontend/src/app/shared/components/schema-category.component.scss
  29. 6
      frontend/src/app/shared/components/schema-category.component.ts
  30. 12
      frontend/src/app/shared/state/schemas.state.spec.ts
  31. 27
      frontend/src/app/shared/state/schemas.state.ts
  32. 20
      frontend/src/app/theme/_mixins.scss
  33. 70
      frontend/src/app/theme/_panels2.scss
  34. 6
      frontend/src/config/webpack.config.js

116
.github/workflows/dev.yml

@ -1,5 +1,5 @@
name: Dev
concurrency: dev
concurrency: build
on:
push:
@ -16,94 +16,59 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v3.1.0
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2
- name: Calculate Version
env:
BUILD_NUMBER: ${{ github.run_number }}
run: |
echo "BUILD_NUMBER=$(($BUILD_NUMBER + 6000))" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Cache Docker layers
uses: actions/cache@v3.0.11
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: BUILD
uses: docker/build-push-action@v3.2.0
with:
push: false
load: true
build-args: "SQUIDEX__VERSION=5.0.0-dev-${{ env.BUILD_NUMBER }}"
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
tags: squidex-tmp
- name: Export Image
run: docker save squidex-tmp | gzip > squidex-tmp.tar.gz
- name: Save Image to Cache
uses: actions/cache@v3.0.11
with:
path: squidex-tmp.tar.gz
key: squidex-dev-image-${{ github.sha }}
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
push: true
build-args: "SQUIDEX__RUNTIME__VERSION=7.0.0-dev-${{ env.BUILD_NUMBER }}"
cache-from: type=gha
cache-to: type=gha,mode=max
tags: squidex/squidex-build
test:
needs: build
runs-on: ubuntu-latest
steps:
- name: Calculate Version
env:
BUILD_NUMBER: ${{ github.run_number }}
run: |
echo "BUILD_NUMBER=$(($BUILD_NUMBER + 6000))" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v3.1.0
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2
- name: Get Image From Cache
uses: actions/cache@v3.0.11
with:
path: squidex-tmp.tar.gz
key: squidex-dev-image-${{ github.sha }}
- name: Load Image
run: docker load < squidex-tmp.tar.gz
- name: Pull from Cache
run: docker pull squidex/squidex-build
- name: Replace Image Name1
uses: mikefarah/yq@v4.28.2
with:
cmd: yq e '.services.squidex1.image = "squidex-tmp"' -i backend/tests/docker-compose.yml
cmd: yq e '.services.squidex1.image = "squidex/squidex-build"' -i backend/tests/docker-compose.yml
- name: Replace Image Name2
uses: mikefarah/yq@v4.28.2
with:
cmd: yq e '.services.squidex2.image = "squidex-tmp"' -i backend/tests/docker-compose.yml
cmd: yq e '.services.squidex2.image = "squidex/squidex-build"' -i backend/tests/docker-compose.yml
- name: Replace Image Name3
uses: mikefarah/yq@v4.28.2
with:
cmd: yq e '.services.squidex3.image = "squidex-tmp"' -i backend/tests/docker-compose.yml
cmd: yq e '.services.squidex3.image = "squidex/squidex-build"' -i backend/tests/docker-compose.yml
- name: Start Test
run: docker-compose up -d
@ -166,42 +131,39 @@ jobs:
publish:
needs: test
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
steps:
- name: Checkout
uses: actions/checkout@v3.1.0
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2
- name: Calculate Version
env:
BUILD_NUMBER: ${{ github.run_number }}
run: |
echo "BUILD_NUMBER=$(($BUILD_NUMBER + 6000))" >> $GITHUB_ENV
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
if: github.event_name != 'pull_request'
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Get Image From Cache
if: github.event_name != 'pull_request'
uses: actions/cache@v3.0.11
- name: BUILD
uses: docker/build-push-action@v3.2.0
with:
path: squidex-tmp.tar.gz
key: squidex-dev-image-${{ github.sha }}
- name: Load Image
if: github.event_name != 'pull_request'
run: docker load < squidex-tmp.tar.gz
- name: Rename Tags
if: github.event_name != 'pull_request'
run: |
docker tag squidex-tmp squidex/squidex:dev
docker tag squidex-tmp squidex/squidex:dev-${{ env.BUILD_NUMBER }}
- name: Push Tags
if: github.event_name != 'pull_request'
run: |
docker push squidex/squidex:dev
docker push squidex/squidex:dev-${{ env.BUILD_NUMBER }}
push: true
build-args: "SQUIDEX__RUNTIME__VERSION=7.0.0-dev-${{ env.BUILD_NUMBER }}"
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
squidex/squidex:dev
squidex/squidex:dev-${{ env.BUILD_NUMBER }}

141
.github/workflows/release.yml

@ -1,5 +1,5 @@
name: Release
concurrency: release
concurrency: build
on:
push:
@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v3.1.0
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2
@ -22,71 +22,48 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Cache Docker layers
uses: actions/cache@v3.0.11
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: BUILD
uses: docker/build-push-action@v3.2.0
with:
push: false
load: true
tags: squidex-tmp
build-args: "SQUIDEX__VERSION=${{ env.GITHUB_REF_SLUG }}"
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
- name: Export Image
run: docker save squidex-tmp | gzip > squidex-tmp.tar.gz
- name: Save Image to Cache
uses: actions/cache@v3.0.11
with:
path: squidex-tmp.tar.gz
key: squidex-release-image-${{ github.sha }}
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
push: true
build-args: "SQUIDEX__BUILD__VERSION=${{ env.GITHUB_REF_SLUG }},SQUIDEX__RUNTIME__VERSION=${{ env.GITHUB_REF_SLUG }}"
cache-from: type=gha
cache-to: type=gha,mode=max
tags: squidex/squidex-build
test:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v3.1.0
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2
- name: Get Image From Cache
uses: actions/cache@v3.0.11
with:
path: squidex-tmp.tar.gz
key: squidex-release-image-${{ github.sha }}
- name: Load Image
run: docker load < squidex-tmp.tar.gz
- name: Pull from Cache
run: docker pull squidex/squidex-build
- name: Replace Image Name1
uses: mikefarah/yq@v4.28.2
with:
cmd: yq e '.services.squidex1.image = "squidex-tmp"' -i backend/tests/docker-compose.yml
cmd: yq e '.services.squidex1.image = "squidex/squidex-build"' -i backend/tests/docker-compose.yml
- name: Replace Image Name2
uses: mikefarah/yq@v4.28.2
with:
cmd: yq e '.services.squidex2.image = "squidex-tmp"' -i backend/tests/docker-compose.yml
cmd: yq e '.services.squidex2.image = "squidex/squidex-build"' -i backend/tests/docker-compose.yml
- name: Replace Image Name3
uses: mikefarah/yq@v4.28.2
with:
cmd: yq e '.services.squidex3.image = "squidex-tmp"' -i backend/tests/docker-compose.yml
cmd: yq e '.services.squidex3.image = "squidex/squidex-build"' -i backend/tests/docker-compose.yml
- name: Start Test
run: docker-compose up -d
@ -150,18 +127,15 @@ jobs:
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3.1.0
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Get Major Version
id: version
uses: rishabhgupta/split-by@v1.0.1
uses: rishabhgupta/split-by@v1
with:
string: "${{ env.GITHUB_REF_SLUG }}"
split-by: "."
@ -170,59 +144,64 @@ jobs:
id: normal-version
run: |
if [[ ${{ env.GITHUB_REF_SLUG }} =~ ^[0-9]+\.[0-9]+$ ]]; then
echo ::set-output name=match::true
echo "STABLE_VERSION=true" >> $GITHUB_ENV
fi
- name: Get Image From Cache
uses: actions/cache@v3.0.11
with:
path: squidex-tmp.tar.gz
key: squidex-release-image-${{ github.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Load Image
run: docker load < squidex-tmp.tar.gz
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.2.1
- name: Rename Tags
run: |
docker tag squidex-tmp squidex/squidex:latest
docker tag squidex-tmp squidex/squidex:${{ env.GITHUB_REF_SLUG }}
docker tag squidex-tmp squidex/squidex:${{ steps.version.outputs._0 }}
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Push Version
run: |
docker push squidex/squidex:latest
docker push squidex/squidex:${{ env.GITHUB_REF_SLUG }}
docker push squidex/squidex:${{ steps.version.outputs._0 }}
- name: BUILD
uses: docker/build-push-action@v3.2.0
with:
push: true
build-args: "SQUIDEX__BUILD__VERSION=${{ env.GITHUB_REF_SLUG }},SQUIDEX__RUNTIME__VERSION=${{ env.GITHUB_REF_SLUG }}"
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
squidex/squidex:latest
squidex/squidex:${{ env.GITHUB_REF_SLUG }}
squidex/squidex:${{ steps.version.outputs._0 }}
if: env.STABLE_VERSION == 'true'
- name: Push Latest
run: |
docker push squidex/squidex:latest
if: steps.normal-version.outputs.match == 'true'
- name: BUILD
uses: docker/build-push-action@v3.2.0
with:
push: true
build-args: "SQUIDEX__BUILD__VERSION=${{ env.GITHUB_REF_SLUG }},SQUIDEX__RUNTIME__VERSION=${{ env.GITHUB_REF_SLUG }}"
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
squidex/squidex:${{ env.GITHUB_REF_SLUG }}
squidex/squidex:${{ steps.version.outputs._0 }}
if: env.STABLE_VERSION != 'true'
release:
needs: publish
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v3.1.0
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4.3.2
- name: Get Image From Cache
uses: actions/cache@v3.0.11
with:
path: squidex-tmp.tar.gz
key: squidex-release-image-${{ github.sha }}
- name: Load Image
run: docker load < squidex-tmp.tar.gz
- name: Pull from Cache
run: docker pull squidex/squidex-build
- name: Make directories
run: sudo mkdir /build /release
- name: Create container
run: docker create --name squidex-container squidex-tmp:latest
run: docker create --name squidex-container squidex/squidex-build
- name: Get binaries
run: sudo docker cp squidex-container:/app/. /build

9
Dockerfile

@ -3,7 +3,7 @@
#
FROM mcr.microsoft.com/dotnet/sdk:7.0 as backend
ARG SQUIDEX__VERSION=7.0.0
ARG SQUIDEX__BUILD__VERSION=7.0.0
WORKDIR /src
@ -30,7 +30,7 @@ COPY backend .
RUN dotnet test --no-restore --filter Category!=Dependencies
# Publish
RUN dotnet publish --no-restore src/Squidex/Squidex.csproj --output /build/ --configuration Release -p:version=$SQUIDEX__VERSION
RUN dotnet publish --no-restore src/Squidex/Squidex.csproj --output /build/ --configuration Release -p:version=$SQUIDEX__BUILD__VERSION
# Install tools
RUN dotnet tool install --tool-path /tools dotnet-counters \
@ -70,6 +70,8 @@ RUN cp -a build /build/
#
FROM mcr.microsoft.com/dotnet/aspnet:7.0-bullseye-slim
ARG SQUIDEX__RUNTIME__VERSION=7.0.0
# Curl for debugging and libc-dev for protobuf
RUN apt-get update \
&& apt-get install -y curl libc-dev
@ -89,7 +91,6 @@ COPY --from=frontend /build/ wwwroot/build/
EXPOSE 80
EXPOSE 443
EXPOSE 11111
ENV DIAGNOSTICS__COUNTERSTOOL=/tools/dotnet-counters
ENV DIAGNOSTICS__DUMPTOOL=/tools/dotnet-dump
@ -97,3 +98,5 @@ ENV DIAGNOSTICS__GCDUMPTOOL=/tools/dotnet-gcdump
ENV DIAGNOSTICS__TRACETOOL=/tools/dotnet-trace
ENTRYPOINT ["dotnet", "Squidex.dll"]
ENV EXPOSEDCONFIGURATION__VERSION=$SQUIDEX__RUNTIME__VERSION

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

@ -82,38 +82,40 @@ public class ContentQueryParser
query.Filter = await GeoQueryTransformer.TransformAsync(query.Filter, context, schema, textIndex, ct);
}
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(), ct);
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(), ct);
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,

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;

13
frontend/src/app/features/rules/pages/rules/rule.component.html

@ -2,12 +2,13 @@
<div class="card-header">
<div class="row">
<div class="col col-name">
<sqx-editable-title [disabled]="!rule.canUpdate"
[fallback]="'rules.unnamed' | sqxTranslate"
[name]="rule.name"
(nameChange)="rename($event)"
[maxLength]="60"
[isRequired]="false">
<sqx-editable-title
[disabled]="!rule.canUpdate"
[displayFallback]="'rules.unnamed' | sqxTranslate"
(inputTitleChange)="rename($event)"
[inputTitle]="rule.name"
[inputTitleLength]="60"
[inputTitleRequired]="false" >
</sqx-editable-title>
</div>
<div class="col-auto" [class.invisible]="!rule.canDelete && !rule.canRun">

10
frontend/src/app/features/settings/pages/clients/client.component.html

@ -1,8 +1,12 @@
<div class="card">
<div class="card-header">
<div class="row g-0">
<div class="row g-1">
<div class="col col-name">
<sqx-editable-title [name]="client.name" (nameChange)="rename($event)" [disabled]="!client.canUpdate">
<sqx-editable-title
[disabled]="!client.canUpdate"
(inputTitleChange)="rename($event)"
[inputTitle]="client.name"
[inputTitleLength]="100">
</sqx-editable-title>
</div>
<div class="col-auto">
@ -10,7 +14,7 @@
{{ 'clients.connect' | sqxTranslate }}
</button>
</div>
<div class="col-auto cell-actions">
<div class="col-auto">
<button type="button" class="btn btn-text-danger" [disabled]="!client.canRevoke"
(sqxConfirmClick)="revoke()"
confirmTitle="i18n:clients.deleteConfirmTitle"

7
frontend/src/app/features/settings/pages/workflows/workflow-step.component.html

@ -18,9 +18,10 @@
</div>
<div class="col">
<sqx-editable-title
[name]="step.name"
(nameChange)="changeName($event)"
[disabled]="step.isLocked || disabled">
[disabled]="step.isLocked || disabled"
(inputTitleChange)="changeName($event)"
[inputTitle]="step.name"
[inputTitleLength]="100">
</sqx-editable-title>
</div>
<div class="col">

11
frontend/src/app/framework/angular/avatar.stories.tsx

@ -5,8 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { moduleMetadata } from '@storybook/angular';
import { Meta, Story } from '@storybook/angular/types-6-0';
import { AvatarComponent } from '@app/framework';
import { AvatarComponent, SqxFrameworkModule } from '@app/framework';
export default {
title: 'Framework/Avatar',
@ -22,6 +23,14 @@ export default {
control: 'number',
},
},
decorators: [
moduleMetadata({
imports: [
SqxFrameworkModule,
SqxFrameworkModule.forRoot(),
],
}),
],
} as Meta;
const Template: Story<AvatarComponent> = (args: AvatarComponent) => ({

63
frontend/src/app/framework/angular/forms/editable-title.component.html

@ -1,32 +1,41 @@
<div class="title">
<form *ngIf="renaming; else noRenaming" (ngSubmit)="rename()">
<div class="row g-0">
<div class="col">
<div class="form-group me-2">
<sqx-control-errors for="name"></sqx-control-errors>
<input class="form-control" [formControl]="renameForm" [maxLength]="maxLength" sqxFocusOnInit (keydown)="onKeyDown($event)" spellcheck="false">
</div>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary me-1" [disabled]="!renameForm.valid || !renameForm.dirty">
{{ 'common.save' | sqxTranslate }}
</button>
<button type="button" class="btn btn-text-secondary btn-cancel me-4" (click)="toggleRename()">
<i class="icon-close"></i>
</button>
<form *ngIf="renaming; else noRenaming" (ngSubmit)="rename()">
<div class="row g-1 align-items-center">
<div class="col">
<div class="form-group">
<sqx-control-errors [for]="renameForm"></sqx-control-errors>
<input class="form-control form-control-{{size}}" [formControl]="renameForm" [maxLength]="inputTitleLength" sqxFocusOnInit (keydown)="onKeyDown($event)" spellcheck="false">
</div>
</div>
</form>
<div class="col-auto">
<button type="submit" class="btn btn-primary btn-{{size}}" [disabled]="!renameForm.valid || !renameForm.dirty">
<i class="icon-checkmark"></i>
</button>
</div>
<div class="col-auto" *ngIf="closeButton">
<button type="button" class="btn btn-text-secondary btn-{{size}} btn-cancel" (click)="toggleRename()">
<i class="icon-close"></i>
</button>
</div>
</div>
</form>
<ng-template #noRenaming>
<div class="title-view">
<h3 class="title-name" [class.fallback]="!name" (dblclick)="toggleRename()">
{{name || fallback}}
<ng-template #noRenaming>
<div class="row g-0 align-items-center title-view d-nowrap">
<div class="col">
<h3 class="truncate {{size}}" [class.fallback]="!inputTitle" (dblclick)="toggleRename()">
{{inputTitle || displayFallback}}
</h3>
<i class="title-edit icon-pencil" *ngIf="!disabled" (click)="toggleRename()"></i>
</div>
</ng-template>
</div>
<div class="col-auto title-edit" *ngIf="!disabled" >
<button type="button" class="btn btn-text-secondary btn-{{size}}" (click)="toggleRename()">
<i class="icon-pencil text-decent"></i>
</button>
</div>
<div class="col-auto">
<button type="button" class="btn btn-text-secondary btn-{{size}} btn-placeholder" (click)="toggleRename()">
&nbsp;
</button>
</div>
</div>
</ng-template>

46
frontend/src/app/framework/angular/forms/editable-title.component.scss

@ -1,34 +1,36 @@
@import 'mixins';
@import 'vars';
.title {
@include hover-visible('.title-edit', inline);
position: relative;
&-edit {
@include absolute(0, 0, null, null);
color: darken($color-border-dark, 20%);
cursor: pointer;
padding: .6rem .25rem;
}
:host {
display: block;
}
&-name {
display: inline;
font-size: 1.2rem;
font-weight: normal;
padding-right: 1.75rem;
}
.title-view {
@include hover-visible('.title-edit', block);
&-view {
@include truncate;
border-bottom: 1px solid transparent;
border-top: 0;
padding: .375rem 0;
position: absolute;
.col {
overflow: hidden;
}
}
.btn-placeholder {
padding-left: 0;
padding-right: 0;
width: 0;
}
.row {
flex-wrap: nowrap;
}
h3 {
margin: 0;
&.sm {
font-size: 1rem;
font-weight: normal;
}
&.fallback {
color: $color-text-decent;
}

51
frontend/src/app/framework/angular/forms/editable-title.component.ts

@ -6,42 +6,41 @@
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { UntypedFormControl, Validators } from '@angular/forms';
import { FormControl, ValidatorFn, Validators } from '@angular/forms';
import { Keys } from '@app/framework/internal';
@Component({
selector: 'sqx-editable-title[name]',
selector: 'sqx-editable-title[inputTitle]',
styleUrls: ['./editable-title.component.scss'],
templateUrl: './editable-title.component.html',
})
export class EditableTitleComponent {
@Output()
public nameChange = new EventEmitter<string>();
public inputTitleChange = new EventEmitter<string>();
@Input()
public disabled?: boolean | null;
public inputTitle!: string;
@Input()
public fallback = '';
public inputTitleLength = 20;
@Input()
public name!: string;
public inputTitleRequired = true;
@Input()
public maxLength = 20;
public disabled?: boolean | null;
@Input()
public set isRequired(value: boolean) {
const validator =
value ?
Validators.required :
Validators.nullValidator;
public closeButton = true;
this.renameForm.setValidators(validator);
}
@Input()
public size: 'sm' | 'md' | 'lg' = 'md';
@Input()
public displayFallback = '';
public renaming = false;
public renameForm = new UntypedFormControl();
public renameForm = new FormControl<string>('');
public onKeyDown(event: KeyboardEvent) {
if (Keys.isEscape(event)) {
@ -54,7 +53,21 @@ export class EditableTitleComponent {
return;
}
this.renameForm.setValue(this.name || '');
if (!this.renaming) {
let validators: ValidatorFn[] = [];
if (this.inputTitleLength) {
validators.push(Validators.maxLength(this.inputTitleLength));
}
if (this.inputTitleRequired) {
validators.push(Validators.required);
}
this.renameForm.setValidators(validators);
}
this.renameForm.setValue(this.inputTitle || '');
this.renaming = !this.renaming;
}
@ -64,10 +77,10 @@ export class EditableTitleComponent {
}
if (this.renameForm.valid) {
const name = this.renameForm.value;
const text = this.renameForm.value || '';
this.nameChange.emit(name);
this.name = name;
this.inputTitleChange.emit(text);
this.inputTitle = text;
this.renaming = false;
}

134
frontend/src/app/framework/angular/forms/editable-title.stories.ts

@ -0,0 +1,134 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { moduleMetadata } from '@storybook/angular';
import { Meta, Story } from '@storybook/angular/types-6-0';
import { EditableTitleComponent, LocalizerService, SqxFrameworkModule } from '@app/framework';
export default {
title: 'Framework/EditableTitle',
component: EditableTitleComponent,
argTypes: {
inputTitle: {
control: 'inputTitle',
},
closeButton: {
control: 'boolean',
},
inputTitleRequired: {
control: 'boolean',
},
inputTitleLength: {
control: 'number',
},
size: {
control: 'select',
options: [
'sm',
'md',
'lg',
],
},
},
args: {
closeButton: true,
inputTitleLength: 30,
inputTitleRequired: true,
},
decorators: [
moduleMetadata({
imports: [
BrowserAnimationsModule,
FormsModule,
ReactiveFormsModule,
SqxFrameworkModule,
SqxFrameworkModule.forRoot(),
],
providers: [
{ provide: LocalizerService, useValue: new LocalizerService({}) },
],
}),
],
} as Meta;
const Template: Story<EditableTitleComponent> = (args: EditableTitleComponent) => ({
props: args,
template: `
<div class="card mt-4">
<div class="row" style="flex-wrap: nowrap">
<div class="col-9">
<sqx-editable-title
[closeButton]="closeButton"
[size]="size"
[inputTitle]="inputTitle"
[inputTitleLength]="inputTitleLength"
[inputTitleRequired]="inputTitleRequired">
</sqx-editable-title>
</div>
<div class="col-3">
<button class="btn btn-primary btn-{{size}}">
Button
</button>
</div>
</div>
</div>
`,
});
export const Default = Template.bind({});
Default.args = {
inputTitle: 'My Title',
size: 'md',
};
export const DefaultNoCloseButton = Template.bind({});
DefaultNoCloseButton.args = {
inputTitle: 'My Title',
size: 'md',
closeButton: false,
};
export const Small = Template.bind({});
Small.args = {
inputTitle: 'My Title',
size: 'sm',
};
export const SmallNoCloseButton = Template.bind({});
SmallNoCloseButton.args = {
inputTitle: 'My Title',
size: 'sm',
closeButton: false,
};
export const Large = Template.bind({});
Large.args = {
inputTitle: 'My Title',
size: 'lg',
};
export const LargeNoCloseButton = Template.bind({});
LargeNoCloseButton.args = {
inputTitle: 'My Title',
size: 'lg',
closeButton: false,
};
export const LongTitle = Template.bind({});
LongTitle.args = {
inputTitle: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua',
size: 'md',
};

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>

15
frontend/src/app/shared/components/schema-category.component.html

@ -6,16 +6,21 @@
(cdkDropListDropped)="changeCategory($event)">
<li class="nav-item nav-heading">
<div class="row g-0 align-items-center mb-1">
<div class="row g-1 align-items-center">
<div class="col-auto">
<button type="button" class="btn btn-sm btn-decent btn-text-secondary btn-toggle" (click)="toggle()">
<i [class.icon-caret-right]="isCollapsed" [class.icon-caret-down]="!isCollapsed"></i>
</button>
</div>
<div class="col">
<div class="truncate">
{{schemaCategory.displayName | sqxTranslate}}
</div>
<div class="col pe-1">
<sqx-editable-title
size="sm"
[closeButton]="false"
(inputTitleChange)="changeName($event)"
[inputTitle]="schemaCategory.displayName | sqxTranslate"
[inputTitleLength]="30"
[disabled]="!schemaCategory.name">
</sqx-editable-title>
</div>
<div class="col-auto">
<ng-container *ngIf="schemaCategory.countSchemasInSubtree > 0; else noSchemas">

19
frontend/src/app/shared/components/schema-category.component.scss

@ -3,10 +3,6 @@
$drag-margin: -8px;
.col {
overflow: hidden;
}
.btn {
width: 2rem;
@ -59,6 +55,7 @@ $drag-margin: -8px;
.nav-heading {
margin-left: -1rem;
margin-top: 0;
max-width: none;
}
.nav-collapsed {
@ -69,6 +66,14 @@ $drag-margin: -8px;
margin-top: 1rem;
}
.nav-category {
max-width: 100%;
}
.nav-panel {
max-width: 100%;
}
.nav-item {
align-items: center;
@ -81,12 +86,6 @@ $drag-margin: -8px;
margin-bottom: 1px;
}
.nav-category,
.nav-panel,
sqx-schema-category {
max-width: 100%;
}
.categories {
padding: 0;
padding-left: 1rem;

6
frontend/src/app/shared/components/schema-category.component.ts

@ -65,6 +65,12 @@ export class SchemaCategoryComponent implements OnChanges {
}
}
public changeName(name: string) {
if (name !== this.schemaCategory.displayName) {
this.schemasState.renameCategory(this.schemaCategory.displayName, name);
}
}
public changeCategory(drag: CdkDragDrop<any>) {
if (drag.previousContainer !== drag.container) {
this.schemasState.changeCategory(drag.item.data, this.schemaCategory.name);

12
frontend/src/app/shared/state/schemas.state.spec.ts

@ -474,6 +474,18 @@ describe('SchemasState', () => {
expect(schemasState.snapshot.schemas).toEqual([updated, schema2]);
expect(schemasState.snapshot.selectedSchema).toEqual(updated);
});
it('should update schema with matching category', () => {
const updated = createSchema(1, '_new');
schemasService.setup(x => x.putCategory(app, schema1, { name: 'new-name' }, version))
.returns(() => of(updated)).verifiable();
schemasState.renameCategory('schema-category1', 'new-name').subscribe();
expect(schemasState.snapshot.schemas).toEqual([updated, schema2]);
expect(schemasState.snapshot.selectedSchema).toEqual(updated);
});
});
});

27
frontend/src/app/shared/state/schemas.state.ts

@ -6,7 +6,7 @@
*/
import { Injectable } from '@angular/core';
import { EMPTY, Observable, of } from 'rxjs';
import { EMPTY, forkJoin, Observable, of } from 'rxjs';
import { catchError, finalize, tap } from 'rxjs/operators';
import { DialogService, LoadingState, shareMapSubscribed, shareSubscribed, State, Version } from '@app/framework';
import { AddFieldDto, CreateSchemaDto, FieldDto, FieldRule, NestedFieldDto, RootFieldDto, SchemaDto, SchemasService, UpdateFieldDto, UpdateSchemaDto, UpdateUIFields } from './../services/schemas.service';
@ -186,6 +186,31 @@ export class SchemasState extends State<Snapshot> {
}, 'Category Removed');
}
public renameCategory(oldName: string, name: string) {
const schemas = this.snapshot.schemas.filter(x => x.category === oldName);
return forkJoin(schemas.map(s => this.schemasService.putCategory(this.appName, s, { name }, s.version))).pipe(
tap(updated => {
this.next(s => {
let { schemas, selectedSchema } = s;
for (const schema of updated) {
schemas = schemas.replacedBy('id', schema).sortedByString(x => x.displayName);
selectedSchema =
schema &&
selectedSchema &&
selectedSchema.id === schema.id ?
schema :
selectedSchema;
}
return { ...s, schemas, selectedSchema };
}, 'Updated');
}),
shareSubscribed(this.dialogs));
}
public publish(schema: SchemaDto): Observable<SchemaDto> {
return this.schemasService.publishSchema(this.appName, schema, schema.version).pipe(
tap(updated => {

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 {

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