Browse Source

feat(admin-ui): Implement new detail view layout

Michael Bromley 2 years ago
parent
commit
4057a5852c
36 changed files with 504 additions and 328 deletions
  1. 7 1
      packages/admin-ui/src/lib/catalog/src/catalog.module.ts
  2. 3 2
      packages/admin-ui/src/lib/catalog/src/catalog.routes.ts
  3. 5 0
      packages/admin-ui/src/lib/catalog/src/components/collection-contents/collection-contents.component.html
  4. 203 175
      packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.html
  5. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/action-bar-items/action-bar-items.component.html
  6. 0 1
      packages/admin-ui/src/lib/core/src/shared/components/action-bar-items/action-bar-items.component.scss
  7. 0 2
      packages/admin-ui/src/lib/core/src/shared/components/action-bar/action-bar.component.scss
  8. 14 2
      packages/admin-ui/src/lib/core/src/shared/components/action-bar/action-bar.component.ts
  9. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/affixed-input/affixed-input.component.html
  10. 6 2
      packages/admin-ui/src/lib/core/src/shared/components/affixed-input/affixed-input.component.scss
  11. 4 0
      packages/admin-ui/src/lib/core/src/shared/components/card/card.component.html
  12. 18 0
      packages/admin-ui/src/lib/core/src/shared/components/card/card.component.scss
  13. 11 0
      packages/admin-ui/src/lib/core/src/shared/components/card/card.component.ts
  14. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/configurable-input/configurable-input.component.html
  15. 20 23
      packages/admin-ui/src/lib/core/src/shared/components/form-field/form-field.component.html
  16. 16 75
      packages/admin-ui/src/lib/core/src/shared/components/form-field/form-field.component.scss
  17. 7 1
      packages/admin-ui/src/lib/core/src/shared/components/page-block/page-block.component.html
  18. 38 4
      packages/admin-ui/src/lib/core/src/shared/components/page-block/page-block.component.scss
  19. 5 2
      packages/admin-ui/src/lib/core/src/shared/components/page-block/page-block.component.ts
  20. 6 0
      packages/admin-ui/src/lib/core/src/shared/components/page-detail-layout/page-detail-layout.component.html
  21. 8 0
      packages/admin-ui/src/lib/core/src/shared/components/page-detail-layout/page-detail-layout.component.scss
  22. 9 0
      packages/admin-ui/src/lib/core/src/shared/components/page-detail-layout/page-detail-layout.component.ts
  23. 11 0
      packages/admin-ui/src/lib/core/src/shared/components/page-detail-layout/page-detail-sidebar.component.ts
  24. 12 0
      packages/admin-ui/src/lib/core/src/shared/components/page-entity-info/page-entity-info.component.html
  25. 18 0
      packages/admin-ui/src/lib/core/src/shared/components/page-entity-info/page-entity-info.component.scss
  26. 11 0
      packages/admin-ui/src/lib/core/src/shared/components/page-entity-info/page-entity-info.component.ts
  27. 1 3
      packages/admin-ui/src/lib/core/src/shared/components/page/page.component.html
  28. 6 0
      packages/admin-ui/src/lib/core/src/shared/components/page/page.component.ts
  29. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/rich-text-editor.component.html
  30. 5 3
      packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/rich-text-editor.component.scss
  31. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/tabbed-custom-fields/tabbed-custom-fields.component.html
  32. 3 0
      packages/admin-ui/src/lib/core/src/shared/components/tabbed-custom-fields/tabbed-custom-fields.component.scss
  33. 16 2
      packages/admin-ui/src/lib/static/styles/global/_buttons.scss
  34. 24 23
      packages/admin-ui/src/lib/static/styles/global/_forms.scss
  35. 9 0
      packages/admin-ui/src/lib/static/styles/global/_overrides.scss
  36. 3 2
      packages/admin-ui/src/lib/static/styles/theme/default.scss

+ 7 - 1
packages/admin-ui/src/lib/catalog/src/catalog.module.ts

@@ -135,10 +135,16 @@ export class CatalogModule {
         });
         pageService.registerPageTab({
             location: 'collection-list',
-            tab: _('catalog.facets'),
+            tab: _('catalog.collections'),
             route: '',
             component: CollectionListComponent,
         });
+        pageService.registerPageTab({
+            location: 'collection-detail',
+            tab: _('catalog.collection'),
+            route: '',
+            component: CollectionDetailComponent,
+        });
         pageService.registerPageTab({
             location: 'asset-list',
             tab: _('catalog.assets'),

+ 3 - 2
packages/admin-ui/src/lib/catalog/src/catalog.routes.ts

@@ -97,11 +97,12 @@ export const createRoutes = (pageService: PageService): Route[] => [
     },
     {
         path: 'collections/:id',
-        component: CollectionDetailComponent,
+        component: PageComponent,
         resolve: createResolveData(CollectionResolver),
-        canDeactivate: [CanDeactivateDetailGuard],
         data: {
+            locationId: 'collection-detail',
             breadcrumb: collectionBreadcrumb,
+            canDeactivate: [CanDeactivateDetailGuard],
         },
         children: pageService.getPageTabRoutes('collection-detail'),
     },

+ 5 - 0
packages/admin-ui/src/lib/catalog/src/components/collection-contents/collection-contents.component.html

@@ -1,5 +1,10 @@
 <div class="table-wrapper">
     <div class="progress loop" [class.visible]="isLoading"></div>
+    <div class="header-title-row">
+        <ng-container
+            *ngTemplateOutlet="headerTemplate; context: { $implicit: contentsTotalItems$ | async }"
+        ></ng-container>
+    </div>
     <vdr-data-table-2
         id="collection-contents"
         [class.loading]="isLoading"

+ 203 - 175
packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.html

@@ -1,199 +1,227 @@
-<vdr-action-bar>
-    <vdr-ab-left>
-        <vdr-entity-info [entity]="entity$ | async"></vdr-entity-info>
-        <vdr-language-selector
-            [disabled]="isNew$ | async"
-            [availableLanguageCodes]="availableLanguages$ | async"
-            [currentLanguageCode]="languageCode$ | async"
-            (languageCodeChange)="setLanguage($event)"
-        ></vdr-language-selector>
-    </vdr-ab-left>
-
-    <vdr-ab-right>
-        <vdr-action-bar-items locationId="collection-detail"></vdr-action-bar-items>
-        <button
-            class="btn btn-primary"
-            *ngIf="isNew$ | async; else updateButton"
-            (click)="create()"
-            [disabled]="detailForm.invalid || detailForm.pristine"
-        >
-            {{ 'common.create' | translate }}
-        </button>
-        <ng-template #updateButton>
+<vdr-page-block>
+    <vdr-action-bar>
+        <vdr-ab-left>
+            <vdr-language-selector
+                [disabled]="isNew$ | async"
+                [availableLanguageCodes]="availableLanguages$ | async"
+                [currentLanguageCode]="languageCode$ | async"
+                (languageCodeChange)="setLanguage($event)"
+            ></vdr-language-selector>
+        </vdr-ab-left>
+        <vdr-ab-right>
+            <vdr-action-bar-items locationId="collection-detail"></vdr-action-bar-items>
             <button
-                *vdrIfPermissions="updatePermission"
                 class="btn btn-primary"
-                (click)="save()"
-                [disabled]="(detailForm.invalid || detailForm.pristine) && !assetsChanged()"
+                *ngIf="isNew$ | async; else updateButton"
+                (click)="create()"
+                [disabled]="detailForm.invalid || detailForm.pristine"
             >
-                {{ 'common.update' | translate }}
+                {{ 'common.create' | translate }}
             </button>
-        </ng-template>
-    </vdr-ab-right>
-</vdr-action-bar>
+            <ng-template #updateButton>
+                <button
+                    *vdrIfPermissions="updatePermission"
+                    class="btn btn-primary"
+                    (click)="save()"
+                    [disabled]="(detailForm.invalid || detailForm.pristine) && !assetsChanged()"
+                >
+                    {{ 'common.update' | translate }}
+                </button>
+            </ng-template></vdr-ab-right
+        >
+    </vdr-action-bar>
+</vdr-page-block>
 <form class="form" [formGroup]="detailForm" *ngIf="entity$ | async as collection">
+    <vdr-page-detail-layout>
+        <vdr-page-detail-sidebar>
+            <vdr-card>
+                <vdr-page-entity-info
+                    *ngIf="entity$ | async as entity"
+                    [entity]="entity"
+                ></vdr-page-entity-info>
+            </vdr-card>
+            <vdr-card>
+                <vdr-form-field [label]="'catalog.visibility' | translate" for="visibility">
+                    <clr-toggle-wrapper>
+                        <input
+                            type="checkbox"
+                            clrToggle
+                            formControlName="visible"
+                            id="visibility"
+                            [vdrDisabled]="!(updatePermission | hasPermission)"
+                        />
+                        <label class="visible-toggle">
+                            <ng-container *ngIf="detailForm.value.visible; else private">{{
+                                'catalog.public' | translate
+                            }}</ng-container>
+                            <ng-template #private>{{ 'catalog.private' | translate }}</ng-template>
+                        </label>
+                    </clr-toggle-wrapper>
+                </vdr-form-field>
+            </vdr-card>
+        </vdr-page-detail-sidebar>
 
-    <nav role="navigation">
-        <ul class="collection-breadcrumbs">
-            <li *ngFor="let breadcrumb of collection.breadcrumbs; let isFirst = first; let isLast = last">
-                <a [routerLink]="['/catalog/collections']" *ngIf="isFirst">{{ 'catalog.root-collection' | translate }}</a>
-                <a [routerLink]="['/catalog/collections', breadcrumb.id]" *ngIf="!isFirst && !isLast">{{ breadcrumb.name | translate }}</a>
-                <ng-container *ngIf="isLast">{{ breadcrumb.name | translate }}</ng-container>
-            </li>
-        </ul>
-    </nav>
-    <div class="clr-row">
-        <div class="clr-col">
-            <vdr-form-field [label]="'catalog.visibility' | translate" for="visibility">
-                <clr-toggle-wrapper>
+        <vdr-page-block
+            ><nav role="navigation">
+                <ul class="collection-breadcrumbs">
+                    <li
+                        *ngFor="
+                            let breadcrumb of (entity$ | async)?.breadcrumbs;
+                            let isFirst = first;
+                            let isLast = last
+                        "
+                    >
+                        <a [routerLink]="['/catalog/collections']" *ngIf="isFirst">{{
+                            'catalog.root-collection' | translate
+                        }}</a>
+                        <a
+                            [routerLink]="['/catalog/collections', breadcrumb.id]"
+                            *ngIf="!isFirst && !isLast"
+                            >{{ breadcrumb.name | translate }}</a
+                        >
+                        <ng-container *ngIf="isLast">{{ breadcrumb.name | translate }}</ng-container>
+                    </li>
+                </ul>
+            </nav>
+        </vdr-page-block>
+
+        <vdr-page-block>
+            <vdr-card>
+                <vdr-form-field [label]="'common.name' | translate" for="name">
                     <input
-                        type="checkbox"
-                        clrToggle
-                        formControlName="visible"
-                        id="visibility"
-                        [vdrDisabled]="!(updatePermission | hasPermission)"
+                        id="name"
+                        type="text"
+                        formControlName="name"
+                        [readonly]="!(updatePermission | hasPermission)"
+                        (input)="updateSlug($event.target.value)"
                     />
-                    <label class="visible-toggle">
-                        <ng-container *ngIf="detailForm.value.visible; else private">{{
-                            'catalog.public' | translate
-                            }}</ng-container>
-                        <ng-template #private>{{ 'catalog.private' | translate }}</ng-template>
-                    </label>
-                </clr-toggle-wrapper>
-            </vdr-form-field>
-            <vdr-form-field [label]="'common.name' | translate" for="name">
-                <input
-                    id="name"
-                    type="text"
-                    formControlName="name"
-                    [readonly]="!(updatePermission | hasPermission)"
-                    (input)="updateSlug($event.target.value)"
-                />
-            </vdr-form-field>
-            <vdr-form-field
-                [label]="'catalog.slug' | translate"
-                for="slug"
-                [errors]="{ pattern: ('catalog.slug-pattern-error' | translate) }"
+                </vdr-form-field>
+                <vdr-form-field
+                    [label]="'catalog.slug' | translate"
+                    for="slug"
+                    [errors]="{ pattern: ('catalog.slug-pattern-error' | translate) }"
+                >
+                    <input
+                        id="slug"
+                        type="text"
+                        formControlName="slug"
+                        [readonly]="!(updatePermission | hasPermission)"
+                    />
+                </vdr-form-field>
+                <vdr-form-field [label]="'common.description' | translate" for="slug">
+                    <vdr-rich-text-editor
+                        formControlName="description"
+                        [readonly]="!(updatePermission | hasPermission)"
+                    ></vdr-rich-text-editor>
+                </vdr-form-field>
+            </vdr-card>
+            <vdr-card
+                formGroupName="customFields"
+                *ngIf="customFields.length"
+                [title]="'common.custom-fields' | translate"
             >
-                <input
-                    id="slug"
-                    type="text"
-                    formControlName="slug"
-                    [readonly]="!(updatePermission | hasPermission)"
-                />
-            </vdr-form-field>
-            <vdr-rich-text-editor
-                formControlName="description"
-                [readonly]="!(updatePermission | hasPermission)"
-                [label]="'common.description' | translate"
-            ></vdr-rich-text-editor>
-
-            <section formGroupName="customFields" *ngIf="customFields.length">
-                <label>{{ 'common.custom-fields' | translate }}</label>
                 <vdr-tabbed-custom-fields
                     entityName="Collection"
                     [customFields]="customFields"
                     [customFieldsFormGroup]="detailForm.get(['customFields'])"
                     [readonly]="!(updatePermission | hasPermission)"
                 ></vdr-tabbed-custom-fields>
-            </section>
+            </vdr-card>
             <vdr-custom-detail-component-host
                 locationId="collection-detail"
                 [entity$]="entity$"
                 [detailForm]="detailForm"
             ></vdr-custom-detail-component-host>
-        </div>
-        <div class="clr-col-md-auto">
-            <vdr-assets
-                [assets]="collection.assets"
-                [featuredAsset]="collection.featuredAsset"
-                [updatePermissions]="updatePermission"
-                (change)="assetChanges = $event"
-            ></vdr-assets>
-        </div>
-    </div>
-    <div class="clr-row">
-        <div class="clr-col">
-            <label>{{ 'catalog.filters' | translate }}</label>
-            <vdr-form-field [label]="'catalog.filter-inheritance' | translate" for="inheritFilters">
-                <clr-toggle-wrapper>
-                    <input
-                        type="checkbox"
-                        clrToggle
-                        formControlName="inheritFilters"
-                        id="inheritFilters"
-                        [vdrDisabled]="!(updatePermission | hasPermission)"
-                    />
-                    <label class="visible-toggle">
-                        <ng-container *ngIf="detailForm.value.inheritFilters; else noInherit">{{
-                            'catalog.inherit-filters-from-parent' | translate
-                        }}</ng-container>
-                        <ng-template #noInherit>{{
-                            'catalog.do-not-inherit-filters' | translate
-                        }}</ng-template>
-                    </label>
-                </clr-toggle-wrapper>
-            </vdr-form-field>
-            <div formArrayName="filters">
-                <ng-container *ngFor="let filter of filters; index as i; trackBy:trackByFn">
-                    <vdr-configurable-input
-                        (remove)="removeFilter(i)"
-                        [position]="i"
-                        [operation]="filter"
-                        [operationDefinition]="getFilterDefinition(filter)"
-                        [formControlName]="i"
-                        [readonly]="!(updatePermission | hasPermission)"
-                    ></vdr-configurable-input>
-                </ng-container>
-            </div>
-
-            <div *vdrIfPermissions="updatePermission">
-                <vdr-dropdown>
-                    <button class="btn btn-outline" vdrDropdownTrigger>
-                        <clr-icon shape="plus"></clr-icon>
-                        {{ 'marketing.add-condition' | translate }}
-                    </button>
-                    <vdr-dropdown-menu vdrPosition="bottom-left">
-                        <button
-                            *ngFor="let filter of allFilters"
-                            type="button"
-                            vdrDropdownItem
-                            (click)="addFilter(filter)"
-                        >
-                            {{ filter.description }}
-                        </button>
-                    </vdr-dropdown-menu>
-                </vdr-dropdown>
-            </div>
-        </div>
-        <div class="clr-col">
-            <vdr-collection-contents
-                [collectionId]="id"
-                [parentId]="parentId$ | async"
-                [updatedFilters]="updatedFilters$ | async"
-                [inheritFilters]="inheritFilters$ | async"
-                [previewUpdatedFilters]="livePreview"
-                #collectionContents
-            >
-                <ng-template let-count>
-                    <div class="contents-title">
-                        {{ 'catalog.collection-contents' | translate }} ({{
-                        'common.results-count' | translate: {count: count}
-                        }})
-                    </div>
-                    <clr-checkbox-wrapper [class.disabled]="detailForm.get('filters')?.pristine">
+            <vdr-card [title]="'common.assets' | translate">
+                <vdr-assets
+                    [assets]="collection.assets"
+                    [featuredAsset]="collection.featuredAsset"
+                    [updatePermissions]="updatePermission"
+                    (change)="assetChanges = $event"
+                ></vdr-assets>
+            </vdr-card>
+            <vdr-card [title]="'catalog.filters' | translate">
+                <vdr-form-field [label]="'catalog.filter-inheritance' | translate" for="inheritFilters">
+                    <clr-toggle-wrapper>
                         <input
                             type="checkbox"
-                            clrCheckbox
-                            [ngModelOptions]="{ standalone: true }"
-                            [disabled]="detailForm.get('filters')?.pristine"
-                            [ngModel]="livePreview"
-                            (ngModelChange)="toggleLivePreview()"
+                            clrToggle
+                            formControlName="inheritFilters"
+                            id="inheritFilters"
+                            [vdrDisabled]="!(updatePermission | hasPermission)"
                         />
-                        <label>{{ 'catalog.live-preview-contents' | translate }}</label>
-                    </clr-checkbox-wrapper>
-                </ng-template>
-            </vdr-collection-contents>
-        </div>
-    </div>
+                        <label class="visible-toggle">
+                            <ng-container *ngIf="detailForm.value.inheritFilters; else noInherit">{{
+                                'catalog.inherit-filters-from-parent' | translate
+                            }}</ng-container>
+                            <ng-template #noInherit>{{
+                                'catalog.do-not-inherit-filters' | translate
+                            }}</ng-template>
+                        </label>
+                    </clr-toggle-wrapper>
+                </vdr-form-field>
+                <div formArrayName="filters" class="mt-4">
+                    <ng-container *ngFor="let filter of filters; index as i; trackBy: trackByFn">
+                        <vdr-configurable-input
+                            (remove)="removeFilter(i)"
+                            [position]="i"
+                            [operation]="filter"
+                            [operationDefinition]="getFilterDefinition(filter)"
+                            [formControlName]="i"
+                            [readonly]="!(updatePermission | hasPermission)"
+                        ></vdr-configurable-input>
+                    </ng-container>
+                </div>
+                <div *vdrIfPermissions="updatePermission">
+                    <vdr-dropdown>
+                        <button class="btn btn-outline" vdrDropdownTrigger>
+                            <clr-icon shape="plus"></clr-icon>
+                            <span>{{ 'marketing.add-condition' | translate }}</span>
+                            <clr-icon shape="ellipsis-vertical"></clr-icon>
+                        </button>
+                        <vdr-dropdown-menu vdrPosition="bottom-left">
+                            <button
+                                *ngFor="let filter of allFilters"
+                                type="button"
+                                vdrDropdownItem
+                                (click)="addFilter(filter)"
+                            >
+                                {{ filter.description }}
+                            </button>
+                        </vdr-dropdown-menu>
+                    </vdr-dropdown>
+                </div>
+            </vdr-card>
+
+            <vdr-card [title]="'common.contents' | translate">
+                <vdr-collection-contents
+                    [collectionId]="id"
+                    [parentId]="parentId$ | async"
+                    [updatedFilters]="updatedFilters$ | async"
+                    [inheritFilters]="inheritFilters$ | async"
+                    [previewUpdatedFilters]="livePreview"
+                    #collectionContents
+                >
+                    <ng-template let-count>
+                        <div class="contents-title">
+                            {{ 'catalog.collection-contents' | translate }} ({{
+                                'common.results-count' | translate : { count: count }
+                            }})
+                        </div>
+                        <clr-checkbox-wrapper [class.disabled]="detailForm.get('filters')?.pristine">
+                            <input
+                                type="checkbox"
+                                clrCheckbox
+                                [ngModelOptions]="{ standalone: true }"
+                                [disabled]="detailForm.get('filters')?.pristine"
+                                [ngModel]="livePreview"
+                                (ngModelChange)="toggleLivePreview()"
+                            />
+                            <label>{{ 'catalog.live-preview-contents' | translate }}</label>
+                        </clr-checkbox-wrapper>
+                    </ng-template>
+                </vdr-collection-contents>
+            </vdr-card>
+        </vdr-page-block>
+    </vdr-page-detail-layout>
 </form>

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/action-bar-items/action-bar-items.component.html

@@ -1,4 +1,4 @@
-<vdr-ui-extension-point [locationId]="locationId" api="actionBar" [leftPx]="-24" [topPx]="-6">
+<vdr-ui-extension-point [locationId]="locationId" api="actionBar">
     <ng-container *ngFor="let item of items$ | async">
         <button
             *vdrIfPermissions="item.requiresPermission"

+ 0 - 1
packages/admin-ui/src/lib/core/src/shared/components/action-bar-items/action-bar-items.component.scss

@@ -1,4 +1,3 @@
 :host {
     display: inline-block;
-    min-height: 36px;
 }

+ 0 - 2
packages/admin-ui/src/lib/core/src/shared/components/action-bar/action-bar.component.scss

@@ -4,9 +4,7 @@
     display: flex;
     justify-content: space-between;
     align-items: baseline;
-    background-color: var(--color-component-bg-100);
     z-index: 25;
-    border-bottom: 1px solid var(--color-component-border-200);
 
     > .grow {
         flex: 1;

+ 14 - 2
packages/admin-ui/src/lib/core/src/shared/components/action-bar/action-bar.component.ts

@@ -2,7 +2,9 @@ import { Component, ContentChild, Input, OnInit } from '@angular/core';
 
 @Component({
     selector: 'vdr-ab-left',
-    template: ` <ng-content></ng-content> `,
+    template: `
+        <ng-content></ng-content>
+    `,
 })
 export class ActionBarLeftComponent {
     @Input() grow = false;
@@ -10,7 +12,17 @@ export class ActionBarLeftComponent {
 
 @Component({
     selector: 'vdr-ab-right',
-    template: ` <ng-content></ng-content> `,
+    template: `
+        <ng-content></ng-content>
+    `,
+    styles: [
+        `
+            :host {
+                display: flex;
+                align-items: center;
+            }
+        `,
+    ],
 })
 export class ActionBarRightComponent {
     @Input() grow = false;

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/affixed-input/affixed-input.component.html

@@ -1,4 +1,4 @@
-<div [class.has-prefix]="!!prefix" [class.has-suffix]="!!suffix">
+<div [class.has-prefix]="!!prefix" [class.has-suffix]="!!suffix" class="input-wrapper">
     <ng-content></ng-content>
 </div>
 <div class="affix prefix" *ngIf="prefix">{{ prefix }}</div>

+ 6 - 2
packages/admin-ui/src/lib/core/src/shared/components/affixed-input/affixed-input.component.scss

@@ -1,6 +1,10 @@
 
 :host {
-    display: inline-flex;
+    display: flex;
+}
+
+.input-wrapper {
+    flex: 1;
 }
 
 .affix {
@@ -17,7 +21,7 @@
 
 ::ng-deep .has-prefix input {
     border-top-left-radius: 0 !important;
-    border-bottom-left-radius: 0 !important;
+    border-bottom-left-radius: 0 !important
 }
 
 .prefix {

+ 4 - 0
packages/admin-ui/src/lib/core/src/shared/components/card/card.component.html

@@ -0,0 +1,4 @@
+<div class="card-container">
+    <div *ngIf="title" class="title">{{ title }}</div>
+    <ng-content></ng-content>
+</div>

+ 18 - 0
packages/admin-ui/src/lib/core/src/shared/components/card/card.component.scss

@@ -0,0 +1,18 @@
+:host {
+    display: block;
+}
+.card-container {
+    border: 1px solid var(--color-card-border);
+    border-radius: var(--border-radius);
+    padding: calc(var(--space-unit) * 2);
+    box-shadow: 0px 2px 4px 0px rgba(0,0,0,0.1);
+}
+
+.title {
+    font-size: var(--font-size-base);
+    margin-bottom: calc(var(--space-unit) * 2);
+}
+
+::ng-deep vdr-card + vdr-card {
+    margin-top: calc(var(--space-unit) * 2);
+}

+ 11 - 0
packages/admin-ui/src/lib/core/src/shared/components/card/card.component.ts

@@ -0,0 +1,11 @@
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+
+@Component({
+    selector: 'vdr-card',
+    templateUrl: './card.component.html',
+    styleUrls: ['./card.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class CardComponent {
+    @Input() title: string;
+}

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/configurable-input/configurable-input.component.html

@@ -21,7 +21,7 @@
         </form>
     </div>
     <div class="card-footer" *ngIf="!readonly && removable">
-        <button class="btn btn-sm btn-link btn-warning" (click)="remove.emit(operation)">
+        <button class="button-small warning" (click)="remove.emit(operation)">
             <clr-icon shape="times"></clr-icon>
             {{ 'common.remove' | translate }}
         </button>

+ 20 - 23
packages/admin-ui/src/lib/core/src/shared/components/form-field/form-field.component.html

@@ -3,31 +3,28 @@
     [class.no-label]="!label"
     [class.clr-error]="formFieldControl?.formControlName?.invalid"
 >
-    <label *ngIf="label" [for]="for" class="clr-control-label">
+    <label *ngIf="label" [for]="for" class="">
         {{ label }}
-        <vdr-help-tooltip *ngIf="tooltip" [content]="tooltip"></vdr-help-tooltip>
     </label>
-    <label
-        [for]="for"
-        aria-haspopup="true"
-        role="tooltip"
+    <div *ngIf="tooltip">
+        {{ tooltip }}
+    </div>
+    <div
+        class="input-row"
+        [class.has-toggle]="readOnlyToggle"
         [class.invalid]="formFieldControl?.touched && !formFieldControl?.valid"
-        class="tooltip tooltip-validation tooltip-sm tooltip-top-left"
     >
-        <div class="input-row" [class.has-toggle]="readOnlyToggle">
-            <ng-content></ng-content>
-            <button
-                *ngIf="readOnlyToggle"
-                type="button"
-                [disabled]="!isReadOnly"
-                [title]="'common.edit-field' | translate"
-                class="btn btn-icon edit-button"
-                (click)="setReadOnly(false)"
-            >
-                <clr-icon shape="edit"></clr-icon>
-            </button>
-        </div>
-        <div class="clr-subtext" *ngIf="getErrorMessage()">{{ getErrorMessage() }}</div>
-        <span class="tooltip-content">{{ label }} is required.</span>
-    </label>
+        <ng-content></ng-content>
+        <button
+            *ngIf="readOnlyToggle"
+            type="button"
+            [disabled]="!isReadOnly"
+            [title]="'common.edit-field' | translate"
+            class="btn btn-icon edit-button"
+            (click)="setReadOnly(false)"
+        >
+            <clr-icon shape="edit"></clr-icon>
+        </button>
+    </div>
+    <div class="error-message" *ngIf="getErrorMessage()">{{ getErrorMessage() }}</div>
 </div>

+ 16 - 75
packages/admin-ui/src/lib/core/src/shared/components/form-field/form-field.component.scss

@@ -1,85 +1,26 @@
-
 :host {
     display: block;
-
-    .form-group > label:first-child {
-        top: 6px;
-    }
-    .form-group > label:nth-of-type(2) {
-        flex: 1;
-        max-width: 20rem;
-
-        ::ng-deep > *:not(.tooltip-content) {
-            width: 100%;
-        }
-    }
-    .form-group .tooltip-validation {
-        height: initial;
-    }
-    .form-group.no-label {
-        margin: -6px 0 0 0;
-        padding: 0;
-        justify-content: center;
-        > label {
-            position: relative;
-            justify-content: center;
-        }
-        .input-row {
-            justify-content: center;
-        }
-    }
-    .input-row {
-        display: flex;
-        ::ng-deep input {
-            flex: 1;
-            &[disabled] {
-                background-color: var(--color-component-bg-200);
-            }
-        }
-        button.edit-button {
-            margin: 0;
-            border-radius: 0 3px 3px 0;
-        }
-
-        &.has-toggle {
-            ::ng-deep input {
-                border-top-right-radius: 0 !important;
-                border-bottom-right-radius: 0 !important;
-                border-right: none;
-            }
-        }
-        ::ng-deep clr-toggle-wrapper {
-            margin-top: 8px;
-        }
-    }
 }
 
-.tooltip.tooltip-validation.invalid::before {
-    position: absolute;
-    content: '';
-    height: .666667rem;
-    width: .666667rem;
-    top: .125rem;
-    right: .25rem;
-    background-image: url(data:image/svg+xml;charset=utf8,%3Csvg%20version%3D%221.1%22%20viewBox%3D%225%205%2026%2026%22%20preserveAspectRatio%3D%22xMidYMid%20meet%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3Cdefs%3E%3Cstyle%3E.clr-i-outline%7Bfill%3A%23a32100%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Ctitle%3Eexclamation-circle-line%3C%2Ftitle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20class%3D%22clr-i-outline%20clr-i-outline-path-1%22%20d%3D%22M18%2C6A12%2C12%2C0%2C1%2C0%2C30%2C18%2C12%2C12%2C0%2C0%2C0%2C18%2C6Zm0%2C22A10%2C10%2C0%2C1%2C1%2C28%2C18%2C10%2C10%2C0%2C0%2C1%2C18%2C28Z%22%3E%3C%2Fpath%3E%3Cpath%20class%3D%22clr-i-outline%20clr-i-outline-path-2%22%20d%3D%22M18%2C20.07a1.3%2C1.3%2C0%2C0%2C1-1.3-1.3v-6a1.3%2C1.3%2C0%2C1%2C1%2C2.6%2C0v6A1.3%2C1.3%2C0%2C0%2C1%2C18%2C20.07Z%22%3E%3C%2Fpath%3E%3Ccircle%20class%3D%22clr-i-outline%20clr-i-outline-path-3%22%20cx%3D%2217.95%22%20cy%3D%2223.02%22%20r%3D%221.5%22%3E%3C%2Fcircle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fsvg%3E);
-    background-repeat: no-repeat;
-    background-size: contain;
-    vertical-align: middle;
-    margin: 0;
+::ng-deep vdr-form-field + vdr-form-field {
+    margin-top: calc(var(--space-unit) * 2);
 }
 
-
-element.style {
-}
-.tooltip.tooltip-sm > .tooltip-content, .tooltip .tooltip-content.tooltip-sm {
-    width: 5rem;
+.form-group {
+    label {
+        font-size: var(--font-size-sm);
+        color: var(--color-weight-800);
+    }
 }
-
-.tooltip:hover > .tooltip-content {
-    right: 12px;
-    margin-bottom: 0;
+.input-row {
+    display: flex;
 }
 
-.tooltip:not(.invalid):hover > .tooltip-content {
-    display: none;
+::ng-deep .input-row {
+    input,
+    select,
+    textarea,
+    vdr-rich-text-editor {
+        width: 100%;
+    }
 }

+ 7 - 1
packages/admin-ui/src/lib/core/src/shared/components/page-block/page-block.component.html

@@ -1 +1,7 @@
-<div class="page-block"><ng-content></ng-content></div>
+<div class="page-block-container">
+    <div *ngIf="title || description" class="page-block-meta">
+        <div class="page-block-title">{{ title }}</div>
+        <div class="page-block-description">{{ description }}</div>
+    </div>
+    <div class="page-block"><ng-content></ng-content></div>
+</div>

+ 38 - 4
packages/admin-ui/src/lib/core/src/shared/components/page-block/page-block.component.scss

@@ -1,9 +1,43 @@
-@import "variables";
+@import 'variables';
 
 :host {
-    margin-top: var(--space-unit);
     display: block;
-}
-.page-block {
     margin-left: var(--surface-margin-left);
+    margin-right: var(--space-unit);
+    margin-top: var(--space-unit);
+    max-width: var(--layout-content-max-width);
+}
+
+.page-block-container {
+    display: flex;
+}
+
+.page-block-meta {
+    width: 300px;
+}
+
+.page-block-title {
+    font-size: var(--font-size-base);
+    color: var(--color-weight-700);
+    margin-bottom: calc(var(--space-unit) * 2);
+}
+.page-block-description {
+    font-size: var(--font-size-lg);
+    color: var(--color-weight-600);
+}
+
+.page-block,
+.page-block-row {
+    flex: 1;
+}
+.page-block-row {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    grid-gap: calc(var(--space-unit) * 2);
+}
+
+@media screen and (max-width: $breakpoint-small) {
+    .page-block-row {
+        grid-template-columns: 1fr;
+    }
 }

+ 5 - 2
packages/admin-ui/src/lib/core/src/shared/components/page-block/page-block.component.ts

@@ -1,4 +1,4 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
 
 @Component({
     selector: 'vdr-page-block',
@@ -6,4 +6,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
     styleUrls: ['./page-block.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class PageBlockComponent {}
+export class PageBlockComponent {
+    @Input() title: string | undefined;
+    @Input() description: string | undefined;
+}

+ 6 - 0
packages/admin-ui/src/lib/core/src/shared/components/page-detail-layout/page-detail-layout.component.html

@@ -0,0 +1,6 @@
+<div class="main">
+    <ng-content></ng-content>
+</div>
+<div class="sidebar">
+    <ng-content select="vdr-page-detail-sidebar"></ng-content>
+</div>

+ 8 - 0
packages/admin-ui/src/lib/core/src/shared/components/page-detail-layout/page-detail-layout.component.scss

@@ -0,0 +1,8 @@
+:host {
+    display: grid;
+    grid-template-columns: 3fr 1fr;
+}
+
+.sidebar {
+    padding: var(--space-unit);
+}

+ 9 - 0
packages/admin-ui/src/lib/core/src/shared/components/page-detail-layout/page-detail-layout.component.ts

@@ -0,0 +1,9 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+
+@Component({
+    selector: 'vdr-page-detail-layout',
+    templateUrl: './page-detail-layout.component.html',
+    styleUrls: ['./page-detail-layout.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class PageDetailLayoutComponent {}

+ 11 - 0
packages/admin-ui/src/lib/core/src/shared/components/page-detail-layout/page-detail-sidebar.component.ts

@@ -0,0 +1,11 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+
+@Component({
+    selector: 'vdr-page-detail-sidebar',
+    template: `
+        <ng-content></ng-content>
+    `,
+    styles: [``],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class PageDetailSidebarComponent {}

+ 12 - 0
packages/admin-ui/src/lib/core/src/shared/components/page-entity-info/page-entity-info.component.html

@@ -0,0 +1,12 @@
+<div class="property">
+    <div class="prop-label">{{ 'common.ID' | translate }}:</div>
+    <div class="value">{{ entity.id }}</div>
+</div>
+<div class="property" *ngIf="entity.createdAt">
+    <div class="prop-label">{{ 'common.created-at' | translate }}:</div>
+    <div class="value">{{ entity.createdAt | localeDate : 'short' }}</div>
+</div>
+<div class="property" *ngIf="entity.updatedAt">
+    <div class="prop-label">{{ 'common.updated-at' | translate }}:</div>
+    <div class="value">{{ entity.updatedAt | localeDate : 'short' }}</div>
+</div>

+ 18 - 0
packages/admin-ui/src/lib/core/src/shared/components/page-entity-info/page-entity-info.component.scss

@@ -0,0 +1,18 @@
+:host {
+    gap: calc(var(--space-unit) * 2);
+}
+.property {
+    display: flex;
+    flex-wrap: wrap;
+    column-gap: 2px;
+    line-height: 1em;
+    &:not(:last-of-type) { margin-bottom: calc(var(--space-unit) * 1);}
+}
+.prop-label {
+    font-size: var(--font-size-xs);
+    color: var(--color-weight-400);
+}
+.value {
+    font-size: var(--font-size-xs);
+    color: var(--color-weight-600);
+}

+ 11 - 0
packages/admin-ui/src/lib/core/src/shared/components/page-entity-info/page-entity-info.component.ts

@@ -0,0 +1,11 @@
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+
+@Component({
+    selector: 'vdr-page-entity-info',
+    templateUrl: './page-entity-info.component.html',
+    styleUrls: ['./page-entity-info.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class PageEntityInfoComponent {
+    @Input() entity: { id: string; createdAt?: string; updatedAt?: string };
+}

+ 1 - 3
packages/admin-ui/src/lib/core/src/shared/components/page/page.component.html

@@ -1,7 +1,5 @@
 <vdr-page-header>
-    <vdr-page-title>
-        <vdr-action-bar-items [locationId]="locationId"></vdr-action-bar-items>
-    </vdr-page-title>
+    <vdr-page-title></vdr-page-title>
     <vdr-page-header-description *ngIf="description">{{ description }}</vdr-page-header-description>
     <vdr-page-header-tabs *ngIf="headerTabs.length > 1" [tabs]="headerTabs"></vdr-page-header-tabs>
 </vdr-page-header>

+ 6 - 0
packages/admin-ui/src/lib/core/src/shared/components/page/page.component.ts

@@ -1,5 +1,7 @@
 import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
 import { ActivatedRoute } from '@angular/router';
+import { Observable, of, switchMap } from 'rxjs';
+import { map } from 'rxjs/operators';
 import { PageLocationId } from '../../../common/component-registry-types';
 import { HeaderTab } from '../page-header-tabs/page-header-tabs.component';
 import { PageService } from '../../../providers/page/page.service';
@@ -14,6 +16,7 @@ export class PageComponent {
     headerTabs: HeaderTab[] = [];
     @Input() protected locationId: PageLocationId;
     @Input() protected description: string;
+    entity$: Observable<{ id: string; createdAt?: string; updatedAt?: string } | undefined>;
     constructor(private route: ActivatedRoute, private pageService: PageService) {
         this.locationId = this.route.snapshot.data.locationId;
         this.description = this.route.snapshot.data.description ?? '';
@@ -23,5 +26,8 @@ export class PageComponent {
             icon: tab.tabIcon,
             route: tab.route ? [tab.route] : ['./'],
         }));
+        this.entity$ = this.route.data.pipe(
+            switchMap(data => (data.entity as Observable<any>) ?? of(undefined)),
+        );
     }
 }

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/rich-text-editor.component.html

@@ -1,3 +1,3 @@
-<label class="clr-control-label">{{ label }}</label>
+<label *ngIf="label" class="rich-text-label">{{ label }}</label>
 <div #editor></div>
 <vdr-context-menu [editorMenuElement]="menuElement"></vdr-context-menu>

+ 5 - 3
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/rich-text-editor.component.scss

@@ -3,7 +3,6 @@
 
 :host {
     display: block;
-    max-width: 710px;
     margin-bottom: 0.5rem;
 
     &.readonly {
@@ -13,10 +12,13 @@
     }
 }
 
+label.rich-text-label {
+    font-size: var(--font-size-sm);
+    color: var(--font-weight-700);
+}
+
 ::ng-deep .ProseMirror-menubar {
     position: sticky;
-    top: 24px;
-    margin-top: 6px;
     border: 1px solid var(--color-component-border-200);
     border-bottom: none;
     background-color: var(--color-component-bg-200);

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/tabbed-custom-fields/tabbed-custom-fields.component.html

@@ -9,7 +9,7 @@
                 }}
             </button>
             <clr-tab-content *clrIfActive>
-                <div class="mt-4">
+                <div class="mt-2">
                     <ng-container *ngFor="let customField of group.customFields">
                         <vdr-custom-field-control
                             *ngIf="customFieldIsSet(customField.name)"

+ 3 - 0
packages/admin-ui/src/lib/core/src/shared/components/tabbed-custom-fields/tabbed-custom-fields.component.scss

@@ -0,0 +1,3 @@
+vdr-custom-field-control + vdr-custom-field-control {
+    margin-top: calc(var(--space-unit) * 2);
+}

+ 16 - 2
packages/admin-ui/src/lib/static/styles/global/_buttons.scss

@@ -1,4 +1,5 @@
-.button, .btn {
+.button,
+.btn {
     display: flex;
     flex-direction: row;
     justify-content: flex-end;
@@ -23,6 +24,18 @@
         background-color: var(--color-weight-100);
         color: var(--color-weight-800);
     }
+
+    &.primary,
+    &.btn-primary {
+        &:not(:disabled) {
+            background-color: var(--color-primary-700);
+            color: white;
+            &:hover {
+                background-color: var(--color-primary-800);
+                color: white;
+            }
+        }
+    }
 }
 
 .button-ghost {
@@ -44,7 +57,8 @@
         color: var(--color-primary-700);
     }
 }
-a.button-ghost:link, a.button-ghost:visited {
+a.button-ghost:link,
+a.button-ghost:visited {
     color: var(--color-weight-700);
 }
 a.button-ghost:hover {

+ 24 - 23
packages/admin-ui/src/lib/static/styles/global/_forms.scss

@@ -7,25 +7,6 @@ form {
     padding-top: 0.5rem;
 }
 
-.form-group {
-    display: flex;
-    flex-wrap: wrap;
-    position: relative;
-    padding-left: 9.5rem;
-    margin-bottom: 0.5rem;
-    font-size: 0.65rem;
-    letter-spacing: normal;
-    line-height: 1rem;
-
-    label:first-child {
-        position: absolute;
-        width: 8.5rem;
-        left: 0;
-        top: 0.25rem;
-        margin: 0;
-    }
-}
-
 .form .form-block,
 form .form-block {
     margin: 0.5rem 0 1.5rem;
@@ -42,30 +23,41 @@ textarea,
             background-size: 0% 100%;
         }
     }
+    &[readonly] {
+        border-color: var(--color-weight-200);
+    }
     font-weight: 400;
     &::placeholder {
         color: var(--color-weight-400);
     }
+
+    &.ng-invalid {
+        color: var(--color-error-700);
+        border-color: var(--color-error-300);
+    }
 }
 
 input,
+textarea,
 select {
     border-radius: var(--border-radius-input) !important;
-    border: 1px solid var(--color-grey-300) !important;
+    border: 1px solid var(--color-weight-300);
     padding: 5px !important;
     height: initial !important;
     transition: background-color 0.2s, box-shadow 0.2s !important;
     &:focus {
-        border-color: var(--color-primary-500) !important;
+        border-color: var(--color-primary-500);
         box-shadow: 0 0 1px 1px var(--color-primary-100);
         outline: none;
     }
+    &[readonly]:focus {
+        border-color: var(--color-weight-400);
+    }
 }
 
 textarea {
-    border-color: var(--clr-forms-border-color);
     &:focus {
-        border-color: var(--color-primary-500) !important;
+        border-color: var(--color-primary-500);
         outline: none;
     }
 }
@@ -99,6 +91,15 @@ clr-input-container.expand {
     }
 }
 
+.clr-toggle-wrapper .clr-control-label {
+    display: inline-block;
+    font-size: var(--font-size-sm);
+}
+
+.clr-checkbox-wrapper .clr-control-label {
+    display: inline-block;
+}
+
 .tooltip.tooltip-validation::before {
     top: 10px !important;
 }

+ 9 - 0
packages/admin-ui/src/lib/static/styles/global/_overrides.scss

@@ -84,3 +84,12 @@ button.icon-button {
 .cdk-overlay-container {
     z-index: 1050;
 }
+
+.card {
+    margin-top: 0;
+}
+
+// clr tabs
+.btn.btn-link.nav-link {
+    background-color: transparent;
+}

+ 3 - 2
packages/admin-ui/src/lib/static/styles/theme/default.scss

@@ -111,7 +111,7 @@
     --color-left-nav-text-hover: var(--color-primary-700);
 
     // Layout
-    --layout-content-max-width: 1600px;
+    --layout-content-max-width: 1400px;
     --left-nav-width: 0px;
     --surface-width: 100vw;
     --surface-margin-left: 4px;
@@ -144,7 +144,7 @@
 
     // Border radius
     --clr-global-borderradius: 4px;
-    --border-radius-sm: div(var(--clr-global-borderradius), 2);
+    --border-radius-sm: calc(var(--clr-global-borderradius) / 2);
     --border-radius: var(--clr-global-borderradius);
     --border-radius-lg: calc(var(--space-unit) * 3);
     --border-radius-img: var(--clr-global-borderradius);
@@ -174,6 +174,7 @@
 
     // Component-specific colors
     --color-top-bar-bg: white;
+    --color-card-border: var(--color-weight-200);
 
     --color-icon-button: var(--color-grey-600);
     --color-form-input-bg: white;