src/app/core/admin/admin-menu/admin-menu-item/admin-menu-item.component.ts

Description

Display and edit a single menu item in the admin interface.

Nesting and drag & drop are handled by the surrounding MenuItemListEditorComponent, which renders the whole (nested) menu as one flat list.

Example

Metadata

Relationships

Depends on

Index

Properties
Methods
Inputs
Outputs

Inputs

allowEntityLinks
Type : boolean
Default value : true

Whether entity type links are allowed (false for shortcuts, true for admin menu)

hasSubItems
Type : boolean
Default value : false

whether this item has nested sub-items (which are rows of their own in the editor)

item
Type : MenuItemForAdminUi
Required :  true

Outputs

deleteItem
Type : MenuItemForAdminUi
item
Type : MenuItemForAdminUi

Methods

Async editMenuItem
editMenuItem(item: MenuItemForAdminUi)
Parameters :
Name Type Optional
item MenuItemForAdminUi No
Returns : any
onDelete
onDelete(item: MenuItemForAdminUi)
Parameters :
Name Type Optional
item MenuItemForAdminUi No
Returns : void

Properties

hasNoLinkWarning
Type : unknown
Default value : computed(() => { const item = this.item(); return item ? isManualItemWithoutLink(item) && !this.hasSubItems() : false; })

True when the item has no link and no sub-items, meaning clicking it will have no visible effect.

itemToDisplay
Type : unknown
Default value : computed<MenuItem>(() => { const item = this.item(); if (!item) { return undefined; } const plainItem = this.menuService.generateMenuItemForEntityType(item); delete plainItem.link; delete plainItem.subMenu; return plainItem; })
import {
  Component,
  input,
  computed,
  inject,
  model,
  output,
  ChangeDetectionStrategy,
} from "@angular/core";
import { EntityMenuItem, MenuItem } from "app/core/ui/navigation/menu-item";
import { MenuItemComponent } from "app/core/ui/navigation/menu-item/menu-item.component";
import { FaIconComponent } from "@fortawesome/angular-fontawesome";
import { MatFormFieldModule } from "@angular/material/form-field";
import { FormsModule } from "@angular/forms";
import { MatDialog } from "@angular/material/dialog";
import { MenuService } from "app/core/ui/navigation/menu.service";
import { firstValueFrom } from "rxjs";
import { AdminMenuItemDetailsComponent } from "../admin-menu-item-details/admin-menu-item-details.component";
import {
  isManualItemWithoutLink,
  MenuItemForAdminUi,
} from "../menu-item-for-admin-ui";
import { MatNavList } from "@angular/material/list";
import { MatIconButton } from "@angular/material/button";
import { MatTooltipModule } from "@angular/material/tooltip";

/**
 * Display and edit a single menu item in the admin interface.
 *
 * Nesting and drag & drop are handled by the surrounding
 * {@link MenuItemListEditorComponent}, which renders the whole (nested) menu as one flat list.
 */
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-admin-menu-item",
  imports: [
    MatNavList,
    MenuItemComponent,
    FaIconComponent,
    MatFormFieldModule,
    FormsModule,
    MatIconButton,
    MatTooltipModule,
  ],
  templateUrl: "./admin-menu-item.component.html",
  styleUrls: [
    "./admin-menu-item.component.scss",
    "../../../ui/navigation/menu-item/menu-item.component.scss",
  ],
})
export class AdminMenuItemComponent {
  private readonly dialog = inject(MatDialog);
  private readonly menuService = inject(MenuService);

  item = model.required<MenuItemForAdminUi>();

  /** whether this item has nested sub-items (which are rows of their own in the editor) */
  hasSubItems = input<boolean>(false);

  itemToDisplay = computed<MenuItem>(() => {
    const item = this.item();
    if (!item) {
      return undefined;
    }
    const plainItem = this.menuService.generateMenuItemForEntityType(item);
    delete plainItem.link;
    delete plainItem.subMenu;
    return plainItem;
  });

  /**
   * True when the item has no link and no sub-items,
   * meaning clicking it will have no visible effect.
   */
  hasNoLinkWarning = computed(() => {
    const item = this.item();
    return item ? isManualItemWithoutLink(item) && !this.hasSubItems() : false;
  });

  /** Whether entity type links are allowed (false for shortcuts, true for admin menu) */
  allowEntityLinks = input<boolean>(true);

  deleteItem = output<MenuItemForAdminUi>();

  onDelete(item: MenuItemForAdminUi): void {
    this.deleteItem.emit(item);
  }

  async editMenuItem(item: MenuItemForAdminUi) {
    const updatedItem = await this.openEditDialog(item);
    if (updatedItem) {
      const mergedItem = { ...item, ...updatedItem };
      if ("entityType" in item && !("entityType" in updatedItem)) {
        delete (mergedItem as unknown as EntityMenuItem).entityType;
      }
      // If link was explicitly removed in the dialog (noLinkMode), ensure it is
      // not re-introduced from the original item by the spread above.
      if ("link" in item && !("link" in updatedItem)) {
        delete mergedItem.link;
      }

      this.item.set(mergedItem);
    }
  }

  private async openEditDialog(
    item: MenuItemForAdminUi,
  ): Promise<MenuItemForAdminUi | undefined> {
    const dialogRef = this.dialog.open(AdminMenuItemDetailsComponent, {
      width: "600px",
      data: {
        item: item ? { ...item } : {},
        allowEntityLinks: this.allowEntityLinks(),
      },
    });
    return firstValueFrom(dialogRef.afterClosed());
  }
}
<mat-nav-list class="limit-width">
  @if (item()) {
    <div class="flex-row menu-item-container">
      <div class="flex-row">
        <fa-icon icon="grip-vertical" class="drag-handle"></fa-icon>

        <app-menu-item
          [item]="itemToDisplay()"
          class="menu-item-preview"
          (click)="editMenuItem(item())"
        ></app-menu-item>

        <div class="menu-actions flex-row gap-small align-center">
          @if (hasNoLinkWarning()) {
            <fa-icon
              icon="triangle-exclamation"
              class="color-error"
              i18n-matTooltip
              matTooltip="This item has no link and no sub-items — it will not be accessible by users"
            ></fa-icon>
          }

          <button
            mat-icon-button
            color="accent"
            (click)="editMenuItem(item())"
            i18n-aria-label
            aria-label="Edit menu item"
          >
            <fa-icon aria-hidden="true" icon="pen"></fa-icon>
          </button>

          <button
            mat-icon-button
            (click)="onDelete(item())"
            i18n-aria-label
            aria-label="Remove menu item"
          >
            <fa-icon aria-hidden="true" icon="times"></fa-icon>
          </button>
        </div>
      </div>
    </div>
  }
</mat-nav-list>

./admin-menu-item.component.scss

@use "variables/colors";
@use "variables/sizes";

.menu-item-container {
  min-height: 50px;
  position: relative;
  display: block;
  padding: 8px;
  margin: auto;

  border: 1px solid colors.$border-color;
}

/* Actions area on the right (edit & remove) */
.menu-actions {
  margin-left: auto; /* Push actions to far right */
}

/* Drag handle (left) */
.drag-handle {
  cursor: move;
  color: colors.$accent;
  display: flex;
  align-items: center;
  margin-right: 12px;
  opacity: 0.5;
  transition: opacity 0.2s;
}

.drag-handle:hover {
  opacity: 1;
}

/* Allow <app-menu-item> to grow */
app-menu-item {
  flex-grow: 1;
  display: flex;
  align-items: center;
  min-width: 0; /* Allow flex items to shrink below content size */
  overflow: hidden; /* Prevent overflow from child elements */
}

.limit-width {
  max-width: 400px;

  @media (max-width: 600px) {
    max-width: 100%;
  }
}

../../../ui/navigation/menu-item/menu-item.component.scss

@use "../../../../../styles/variables/sizes";
@use "../../../../../styles/variables/colors";

/* ensures that all icons have the same width */
.nav-icon {
  min-width: sizes.$max-icon-width;
}

.matched-background {
  background-color: colors.$background !important;
}

.submenu {
  padding-left: 20px;
}

.indent-item {
  margin-left: 8px;
  padding-left: 8px;
  width: auto;
  border-end-end-radius: 0;
  border-start-end-radius: 0;
}

.menu-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1;
  min-width: 0;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""