src/app/core/dashboard/admin-dashboard/admin-dashboard.component.ts

Metadata

Relationships

Used by

No results matching.

Depends on

Index

Properties
Methods
Inputs

Inputs

dashboardViewId
Type : string
Required :  true

Methods

Async addNewWidget
addNewWidget()
Returns : any
cancel
cancel()
Returns : void
drop
drop(event: CdkDragDrop<DynamicComponentConfig[]>)
Parameters :
Name Type Optional
event CdkDragDrop<DynamicComponentConfig[]> No
Returns : void
Async editWidget
editWidget(widgetConfig: DynamicComponentConfig, idx: number)
Parameters :
Name Type Optional
widgetConfig DynamicComponentConfig No
idx number No
Returns : any
removeWidget
removeWidget(index: number)
Parameters :
Name Type Optional
index number No
Returns : void
Async save
save()
Returns : any

Properties

dashboardConfig
Type : unknown
Default value : linkedSignal(() => structuredClone(this.dashboardViewConfig()?.config ?? { widgets: [] }), )
import {
  CdkDragDrop,
  DragDropModule,
  moveItemInArray,
} from "@angular/cdk/drag-drop";
import { Location } from "@angular/common";
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  input,
  linkedSignal,
} from "@angular/core";
import { FormsModule } from "@angular/forms";
import { MatButtonModule } from "@angular/material/button";
import { MatCardModule } from "@angular/material/card";
import { MatDialog } from "@angular/material/dialog";
import { MatIconModule } from "@angular/material/icon";
import { MatSnackBar } from "@angular/material/snack-bar";
import { MatTooltipModule } from "@angular/material/tooltip";
import { FaIconComponent } from "@fortawesome/angular-fontawesome";
import { firstValueFrom } from "rxjs";
import { WidgetComponentSelectComponent } from "../../admin/admin-entity-details/widget-component-select/widget-component-select.component";
import {
  AdminWidgetDialogComponent,
  AdminWidgetDialogData,
} from "../../admin/admin-widget-dialog/admin-widget-dialog.component";
import { ViewTitleComponent } from "../../common-components/view-title/view-title.component";
import { ConfigService } from "../../config/config.service";
import { DynamicComponentConfig } from "../../config/dynamic-components/dynamic-component-config.interface";
import { DynamicComponentDirective } from "../../config/dynamic-components/dynamic-component.directive";
import { PREFIX_VIEW_CONFIG } from "../../config/dynamic-routing/view-config.interface";
import { DashboardConfig } from "../../dashboard/dashboard/dashboard.component";
import { DashboardWidgetRegistryService } from "../dashboard-widget-registry.service";
import { HintBoxComponent } from "#src/app/core/common-components/hint-box/hint-box.component";
import { IconButtonComponent } from "#src/app/core/common-components/icon-button/icon-button.component";

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-admin-dashboard",
  standalone: true,
  imports: [
    DynamicComponentDirective,
    DragDropModule,
    FaIconComponent,
    FormsModule,
    MatButtonModule,
    MatCardModule,
    MatTooltipModule,
    MatIconModule,
    ViewTitleComponent,
    HintBoxComponent,
    IconButtonComponent,
  ],
  templateUrl: "./admin-dashboard.component.html",
  styleUrls: [
    "./admin-dashboard.component.scss",
    "../../dashboard/dashboard/dashboard.component.scss",
  ],
})
export class AdminDashboardComponent {
  dashboardViewId = input.required<string>();

  private readonly dashboardViewConfigKey = computed(
    () => PREFIX_VIEW_CONFIG + this.dashboardViewId(),
  );

  private readonly dashboardViewConfig = computed(
    () =>
      this.configService.getConfig(this.dashboardViewConfigKey()) as
        DynamicComponentConfig<DashboardConfig> | undefined,
  );

  dashboardConfig = linkedSignal(() =>
    structuredClone(this.dashboardViewConfig()?.config ?? { widgets: [] }),
  );

  private readonly configService = inject(ConfigService);
  private readonly dialog = inject(MatDialog);
  private readonly location = inject(Location);
  private readonly snackBar = inject(MatSnackBar);
  private readonly widgetRegistry = inject(DashboardWidgetRegistryService);

  drop(event: CdkDragDrop<DynamicComponentConfig[]>) {
    if (event.previousContainer === event.container) {
      this.dashboardConfig.update((config) => {
        const widgets = [...config.widgets];
        moveItemInArray(widgets, event.previousIndex, event.currentIndex);
        return { ...config, widgets };
      });
    }
  }

  async editWidget(widgetConfig: DynamicComponentConfig, idx: number) {
    const settingsComponent = this.widgetRegistry.getSettingsComponentForWidget(
      widgetConfig.component,
    );
    if (!settingsComponent) return;

    const updatedConfig = await this.openWidgetSettingsDialog(
      widgetConfig,
      settingsComponent,
    );
    if (updatedConfig) {
      this.dashboardConfig.update((config) => ({
        ...config,
        widgets: config.widgets.map((w, i) => (i === idx ? updatedConfig : w)),
      }));
    }
  }

  private async openWidgetSettingsDialog(
    widgetConfig: DynamicComponentConfig,
    settingsComponent: string,
  ): Promise<DynamicComponentConfig | undefined> {
    const dialogData: AdminWidgetDialogData = {
      widgetConfig: { ...widgetConfig },
      settingsComponent: settingsComponent,
      title: widgetConfig.component,
    };

    const dialogRef = this.dialog.open(AdminWidgetDialogComponent, {
      width: "80vw",
      data: dialogData,
    });

    return firstValueFrom(dialogRef.afterClosed());
  }

  removeWidget(index: number) {
    this.dashboardConfig.update((config) => ({
      ...config,
      widgets: config.widgets.filter((_, i) => i !== index),
    }));
  }

  async addNewWidget() {
    // Open the widget selection dialog for dashboard widgets
    const dialogRef = this.dialog.open(WidgetComponentSelectComponent, {
      width: "400px",
      data: { isDashboard: true },
    });

    const selectedWidget = await firstValueFrom(dialogRef.afterClosed());
    if (!selectedWidget) return;

    this.dashboardConfig.update((config) => ({
      ...config,
      widgets: [...config.widgets, selectedWidget],
    }));

    // Open settings dialog when no config was pre-filled, so user can set required fields
    const hasNoConfig =
      !selectedWidget.config || Object.keys(selectedWidget.config).length === 0;
    if (hasNoConfig) {
      const newIndex = this.dashboardConfig().widgets.length - 1;
      await this.editWidget(selectedWidget, newIndex);
    }
  }

  cancel() {
    this.location.back();
  }

  async save() {
    const currentConfig = this.configService.exportConfig(true);

    const updatedViewConfig: DynamicComponentConfig<DashboardConfig> = {
      component: "Dashboard",
      config: this.dashboardConfig(),
    };

    currentConfig[this.dashboardViewConfigKey()] = updatedViewConfig;

    await this.configService.saveConfig(currentConfig);

    this.snackBar.open(
      $localize`:Save config confirmation message:Dashboard configuration updated`,
      undefined,
      { duration: 4000 },
    );

    this.location.back();
  }
}
<div class="padding-regular flex-row flex-wrap justify-space-between">
  <app-view-title [displayInPlace]="true" i18n>
    Edit Dashboard Widgets
  </app-view-title>

  <div class="flex-row gap-small">
    <button mat-raised-button color="accent" (click)="save()" i18n>Save</button>
    <button mat-stroked-button (click)="cancel()" i18n>Cancel</button>
  </div>
</div>

<app-hint-box i18n>
  Drag and drop the widgets to reorder them. You can configure details of each
  widget by clicking on them. Add additional widgets or remove widgets to fully
  customize the dashboard to your needs. The dashboard layout is shared by all
  users, your changes will be visible to everyone.
</app-hint-box>

<!-- WIDGET PREVIEW AREA -->
<div
  cdkDropList
  [cdkDropListData]="dashboardConfig().widgets"
  (cdkDropListDropped)="drop($event)"
  cdkDropListOrientation="mixed"
  class="widgets-container drop-list"
>
  @for (
    widgetConfig of dashboardConfig().widgets;
    track $index;
    let i = $index
  ) {
    <mat-card
      cdkDrag
      cdkDragBoundary=".overall-container"
      class="widget-preview-card"
    >
      <fa-icon
        icon="grip-vertical"
        size="xl"
        class="widget-action-icon drag-handle"
        matTooltip="You can rearrange the dashboard sections to highlight the most important information first."
        i18n-matTooltip
      ></fa-icon>

      <button
        mat-icon-button
        class="widget-action-icon widget-remove-icon"
        color="black"
        (click)="removeWidget(i)"
        matTooltip="Remove this widget"
        i18n-matTooltip
      >
        <fa-icon icon="times"></fa-icon>
      </button>

      <mat-card-content class="widget-preview-section">
        <!-- Widget Content with Overlay Edit Button -->
        <ng-template [appDynamicComponent]="widgetConfig"></ng-template>

        <!-- Centered Edit Button Overlay -->
        <div class="field-hover-buttons">
          <button
            mat-stroked-button
            color="accent"
            class="field-edit-button"
            (click)="editWidget(widgetConfig, i)"
            i18n
          >
            Edit Widget
          </button>
        </div>
      </mat-card-content>
    </mat-card>
  }

  <!-- DROP AREA: ADD NEW WIDGET -->
  <mat-card class="widget-preview-card drop-area-hint">
    <mat-card-content class="add-widget-content">
      <app-icon-button
        icon="plus-circle"
        buttonType="mat-stroked-button"
        color="accent"
        (buttonClick)="addNewWidget()"
      >
        <span i18n>Add Widget</span>
      </app-icon-button>
    </mat-card-content>
  </mat-card>
</div>

./admin-dashboard.component.scss

@use "variables/sizes";
@use "@angular/material/core/style/elevation" as mat-elevation;
@use "variables/colors";

.header-left {
  display: flex;
  align-items: center;
  gap: 12px;
}

.back-button {
  margin-right: 8px;
}

::ng-deep app-dashboard-widget {
  pointer-events: none;

  /* dim preview of actual widgets to highlight admin elements */
  opacity: 35%;
}

.widget-interaction-blocker {
  position: absolute;
  inset: 0;
  z-index: 10;
}

.widgets-container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
  height: auto;
  overflow: visible;
  grid-auto-rows: unset !important;
}

.widget-preview-card {
  position: relative;
  border: 2px dotted #ccc;
  border-radius: 8px;
  overflow: hidden;

  &:hover {
    border-color: #999;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  }

  &:hover .field-hover-buttons {
    opacity: 1;
  }

  &.drop-area-hint {
    border: 2px dashed #ccc;
    background-color: #f9f9f9;
    min-height: 250px;

    &:hover {
      border-color: #999;
      background-color: #f5f5f5;
    }
  }
}

.widget-action-icon {
  margin: .5em;
}

.widget-preview-section {
  padding: 2.5em;
}

.add-widget-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  color: #666;
  padding: 2rem;
  min-height: 200px;

  span {
    margin-bottom: 1rem;
    font-size: 1rem;
  }
}

.add-widget-button {
  margin-top: 1rem;
}

.drag-handle {
  position: absolute;
  top: 50%;
  left: -8px;
  color: colors.$accent;
  cursor: move;
  z-index: 20;
  transform: translateY(-50%);
  padding: 10px;
  border-radius: 50%;
}

.widget-remove-icon {
  position: absolute;
  top: -8px;
  right: -8px;
  color: black;
  font-size: 20px;
  z-index: 20;
  cursor: pointer;
  padding: 10px;
  border-radius: 50%;

  &:hover {
    color: #ff0000;
  }
}

// Centered Edit Button
.field-hover-buttons {
  opacity: 0;
  transition: opacity 0.2s ease-in-out;
  z-index: 15;

  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);

  display: flex;
  align-items: center;
  justify-content: center;
}

.field-edit-button {
  /* large button to make edit option obvious */
  padding: 2.5em;

  background: white !important;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  border: 1px solid #ddd;

  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
  }
}

.widget-preview-card.widget-remove-hover {
  border-color: rgb(255, 0, 0);
  background-color: rgba(255, 0, 0, 0.05);
}

// Override Material Card styles for consistency
::ng-deep .widget-preview-card {
  .mat-card-header {
    padding: 0;
    margin: 0;
  }

  .mat-card-content {
    margin: 0;
    padding: 0 !important;
    height: 100%;
  }
}

.widget-container:has(.widget-remove-icon:hover) .widget-preview-card {
  border-color: rgb(255, 0, 0) !important;
  background-color: rgba(255, 0, 0, 0.1) !important;
}

::ng-deep .cdk-drag-preview {
  box-sizing: border-box;
  border-radius: 4px;
  box-shadow:
    0 5px 5px -3px rgba(0, 0, 0, 0.2),
    0 8px 10px 1px rgba(0, 0, 0, 0.14),
    0 3px 14px 2px rgba(0, 0, 0, 0.12);
}

::ng-deep .cdk-drag-placeholder {
  opacity: 0;
}

::ng-deep .cdk-drag-animating {
  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}

::ng-deep
  .drop-list.cdk-drop-list-dragging
  .widget-preview-card:not(.cdk-drag-placeholder) {
  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}

::ng-deep .cdk-drop-list-receiving {
  background-color: rgba(0, 0, 0, 0.03);
  border: 2px dashed #ccc;
}

../../dashboard/dashboard/dashboard.component.scss

@use "mixins/grid-layout";
@use "variables/sizes";

/* The minimum size of a widget for big screens */
/* The minimum size on small screens is whatever size is available */
$min-widget-width: 330px;
/* There will only be one column below this screen width, as defined by the media query */
$max-screen-width: $min-widget-width + sizes.$margin-main-view-right +
  sizes.$margin-main-view-left;
$widget-height: 430px;

.widgets-container {
  /* Make all rows be equal in height */
  grid-auto-rows: $widget-height;

  @include grid-layout.adaptive($min-widget-width, $max-screen-width);
}

.grid-item {
  transition: all 1s;
}

.dashboard-menu-btn {
  position: fixed;
  top: 80px; // Adjust based on your header height
  right: 20px;
  z-index: 1000;
  background: white;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  border-radius: 50%;

  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""