@maro-tech/card 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,12 +6,12 @@ import { input, inject, DestroyRef, signal, Component, computed, viewChild, view
6
6
  import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
7
7
  import { EventBus, UiButtonComponent, UiToastService, UiModalComponent, UiConfirmService } from '@maro-tech/core';
8
8
  import { DomSanitizer } from '@angular/platform-browser';
9
+ import { RouterLink, ActivatedRoute, Router } from '@angular/router';
9
10
  import Panzoom from '@panzoom/panzoom';
10
11
  import { LucideDownload, LucideDynamicIcon, LucideEye, LucideFileText, LucidePlus, LucideSparkles, LucideTrash } from '@lucide/angular';
11
12
  import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
12
13
  import { map, shareReplay, forkJoin, switchMap, timer, of } from 'rxjs';
13
14
  import { UiTableComponent, UiPaginatorComponent } from '@maro-tech/table';
14
- import { ActivatedRoute, Router, RouterLink } from '@angular/router';
15
15
  import { map as map$1, exhaustMap, catchError, takeWhile, takeUntil, finalize } from 'rxjs/operators';
16
16
  import * as i1$1 from '@angular/forms';
17
17
  import { FormsModule } from '@angular/forms';
@@ -486,6 +486,7 @@ class DetailsGridComponent {
486
486
  ...field,
487
487
  displayValue: this.formatFieldValue(field),
488
488
  imageSrc: this.getImageSrc(field),
489
+ linkUrl: this.getLinkUrl(field),
489
490
  })), ...(ngDevMode ? [{ debugName: "displayFields" }] : /* istanbul ignore next */ []));
490
491
  selectedImageSrc = signal(null, ...(ngDevMode ? [{ debugName: "selectedImageSrc" }] : /* istanbul ignore next */ []));
491
492
  openViewer(imageSrc) {
@@ -560,6 +561,20 @@ class DetailsGridComponent {
560
561
  }
561
562
  return raw;
562
563
  }
564
+ getLinkUrl(field) {
565
+ if (field.type !== 'link' || !field.link)
566
+ return null;
567
+ const data = this.data() || {};
568
+ const url = field.link.replace(/\{\{([a-zA-Z0-9_.-]+)\}\}/g, (_match, key) => {
569
+ const value = key.split('.').reduce((current, segment) => {
570
+ if (current == null || typeof current !== 'object')
571
+ return undefined;
572
+ return current[segment];
573
+ }, data);
574
+ return encodeURIComponent(String(value ?? ''));
575
+ }).trim();
576
+ return url && url !== '/' && !url.endsWith('/') ? url : null;
577
+ }
563
578
  getImageSrc(field) {
564
579
  if (field.type !== 'image')
565
580
  return null;
@@ -591,11 +606,11 @@ class DetailsGridComponent {
591
606
  : { day: '2-digit', month: '2-digit', year: 'numeric' }).format(date);
592
607
  }
593
608
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DetailsGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
594
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: DetailsGridComponent, isStandalone: true, selector: "app-details-grid", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"grid gap-4\" [class.grid-cols-1]=\"!hasExplicitColumns()\" [class.sm:grid-cols-3]=\"!hasExplicitColumns()\" [class.sm:grid-cols-12]=\"hasExplicitColumns()\">\n @for (field of displayFields(); track $index) {\n <div class=\"rounded-2xl border border-slate-100 bg-slate-200 p-4\" [ngClass]=\"getFieldColumnClass(field)\" [class.flex]=\"field.type === 'image'\" [class.flex-col]=\"field.type === 'image'\" [class.min-h-64]=\"field.type === 'image'\">\n <p class=\"text-xs uppercase tracking-[0.18em] text-slate-500\">{{ field.label }}</p>\n @if (field.type === 'image') {\n @if (field.imageSrc) {\n <button type=\"button\" class=\"mt-2 block flex-1 cursor-zoom-in overflow-hidden rounded-xl border border-slate-200 bg-white\" [attr.aria-label]=\"'\u041F\u0435\u0440\u0435\u0433\u043B\u044F\u043D\u0443\u0442\u0438 ' + field.label\" (click)=\"openViewer(field.imageSrc)\">\n <img [src]=\"field.imageSrc\" [alt]=\"field.label\" class=\"h-full w-full object-cover\" />\n </button>\n } @else {\n <p class=\"mt-2 text-base font-semibold text-slate-900\">-</p>\n }\n } @else {\n <p class=\"mt-2 text-base font-semibold text-slate-900\">{{ field.displayValue ?? '-' }}</p>\n }\n </div>\n }\n</div>\n\n@if (selectedImageSrc(); as imageSrc) {\n <app-images-viewer [urls]=\"[imageSrc]\" (closed)=\"closeViewer()\" />\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: ImagesViewerComponent, selector: "app-images-viewer", inputs: ["urls", "initialIndex"], outputs: ["closed"] }] });
609
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: DetailsGridComponent, isStandalone: true, selector: "app-details-grid", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"grid gap-4\" [class.grid-cols-1]=\"!hasExplicitColumns()\" [class.sm:grid-cols-3]=\"!hasExplicitColumns()\" [class.sm:grid-cols-12]=\"hasExplicitColumns()\">\n @for (field of displayFields(); track $index) {\n <div class=\"rounded-2xl border border-slate-100 bg-slate-200 p-4\" [ngClass]=\"getFieldColumnClass(field)\" [class.flex]=\"field.type === 'image'\" [class.flex-col]=\"field.type === 'image'\" [class.min-h-64]=\"field.type === 'image'\">\n <p class=\"text-xs uppercase tracking-[0.18em] text-slate-500\">{{ field.label }}</p>\n @if (field.type === 'image') {\n @if (field.imageSrc) {\n <button type=\"button\" class=\"mt-2 block flex-1 cursor-zoom-in overflow-hidden rounded-xl border border-slate-200 bg-white\" [attr.aria-label]=\"'\u041F\u0435\u0440\u0435\u0433\u043B\u044F\u043D\u0443\u0442\u0438 ' + field.label\" (click)=\"openViewer(field.imageSrc)\">\n <img [src]=\"field.imageSrc\" [alt]=\"field.label\" class=\"h-full w-full object-cover\" />\n </button>\n } @else {\n <p class=\"mt-2 text-base font-semibold text-slate-900\">-</p>\n }\n } @else if (field.type === 'link' && field.linkUrl) {\n <a class=\"mt-2 inline-block text-base font-semibold text-sky-800 underline\" [routerLink]=\"field.linkUrl\">{{ field.displayValue === '-' ? field.label : field.displayValue }}</a>\n } @else {\n <p class=\"mt-2 text-base font-semibold text-slate-900\">{{ field.displayValue ?? '-' }}</p>\n }\n </div>\n }\n</div>\n\n@if (selectedImageSrc(); as imageSrc) {\n <app-images-viewer [urls]=\"[imageSrc]\" (closed)=\"closeViewer()\" />\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: ImagesViewerComponent, selector: "app-images-viewer", inputs: ["urls", "initialIndex"], outputs: ["closed"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }] });
595
610
  }
596
611
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DetailsGridComponent, decorators: [{
597
612
  type: Component,
598
- args: [{ selector: 'app-details-grid', standalone: true, imports: [CommonModule, ImagesViewerComponent], template: "<div class=\"grid gap-4\" [class.grid-cols-1]=\"!hasExplicitColumns()\" [class.sm:grid-cols-3]=\"!hasExplicitColumns()\" [class.sm:grid-cols-12]=\"hasExplicitColumns()\">\n @for (field of displayFields(); track $index) {\n <div class=\"rounded-2xl border border-slate-100 bg-slate-200 p-4\" [ngClass]=\"getFieldColumnClass(field)\" [class.flex]=\"field.type === 'image'\" [class.flex-col]=\"field.type === 'image'\" [class.min-h-64]=\"field.type === 'image'\">\n <p class=\"text-xs uppercase tracking-[0.18em] text-slate-500\">{{ field.label }}</p>\n @if (field.type === 'image') {\n @if (field.imageSrc) {\n <button type=\"button\" class=\"mt-2 block flex-1 cursor-zoom-in overflow-hidden rounded-xl border border-slate-200 bg-white\" [attr.aria-label]=\"'\u041F\u0435\u0440\u0435\u0433\u043B\u044F\u043D\u0443\u0442\u0438 ' + field.label\" (click)=\"openViewer(field.imageSrc)\">\n <img [src]=\"field.imageSrc\" [alt]=\"field.label\" class=\"h-full w-full object-cover\" />\n </button>\n } @else {\n <p class=\"mt-2 text-base font-semibold text-slate-900\">-</p>\n }\n } @else {\n <p class=\"mt-2 text-base font-semibold text-slate-900\">{{ field.displayValue ?? '-' }}</p>\n }\n </div>\n }\n</div>\n\n@if (selectedImageSrc(); as imageSrc) {\n <app-images-viewer [urls]=\"[imageSrc]\" (closed)=\"closeViewer()\" />\n}\n" }]
613
+ args: [{ selector: 'app-details-grid', standalone: true, imports: [CommonModule, ImagesViewerComponent, RouterLink], template: "<div class=\"grid gap-4\" [class.grid-cols-1]=\"!hasExplicitColumns()\" [class.sm:grid-cols-3]=\"!hasExplicitColumns()\" [class.sm:grid-cols-12]=\"hasExplicitColumns()\">\n @for (field of displayFields(); track $index) {\n <div class=\"rounded-2xl border border-slate-100 bg-slate-200 p-4\" [ngClass]=\"getFieldColumnClass(field)\" [class.flex]=\"field.type === 'image'\" [class.flex-col]=\"field.type === 'image'\" [class.min-h-64]=\"field.type === 'image'\">\n <p class=\"text-xs uppercase tracking-[0.18em] text-slate-500\">{{ field.label }}</p>\n @if (field.type === 'image') {\n @if (field.imageSrc) {\n <button type=\"button\" class=\"mt-2 block flex-1 cursor-zoom-in overflow-hidden rounded-xl border border-slate-200 bg-white\" [attr.aria-label]=\"'\u041F\u0435\u0440\u0435\u0433\u043B\u044F\u043D\u0443\u0442\u0438 ' + field.label\" (click)=\"openViewer(field.imageSrc)\">\n <img [src]=\"field.imageSrc\" [alt]=\"field.label\" class=\"h-full w-full object-cover\" />\n </button>\n } @else {\n <p class=\"mt-2 text-base font-semibold text-slate-900\">-</p>\n }\n } @else if (field.type === 'link' && field.linkUrl) {\n <a class=\"mt-2 inline-block text-base font-semibold text-sky-800 underline\" [routerLink]=\"field.linkUrl\">{{ field.displayValue === '-' ? field.label : field.displayValue }}</a>\n } @else {\n <p class=\"mt-2 text-base font-semibold text-slate-900\">{{ field.displayValue ?? '-' }}</p>\n }\n </div>\n }\n</div>\n\n@if (selectedImageSrc(); as imageSrc) {\n <app-images-viewer [urls]=\"[imageSrc]\" (closed)=\"closeViewer()\" />\n}\n" }]
599
614
  }], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }] } });
600
615
 
601
616
  class ImagesComponent {
@@ -707,19 +722,24 @@ class TableComponent {
707
722
  this.emitAction(event.action, event.row);
708
723
  }
709
724
  onCellChange(event) {
710
- const config = this.editableColumns().find((item) => item.key === event.column.key);
711
- if (!config?.api)
725
+ const config = this.editableColumn(event.column.key);
726
+ if (!config)
712
727
  return;
728
+ const editAction = Array.isArray(event.row['actions'])
729
+ ? event.row['actions'].find((action) => String(action.id || '') === 'edit')
730
+ : undefined;
731
+ const formId = String(editAction?.formId || '').trim();
732
+ const api = config.api || (formId
733
+ ? `/api/${encodeURIComponent(formId)}`
734
+ : `/api/crud/${encodeURIComponent(this.tableName())}/${encodeURIComponent(String(event.row['id'] || ''))}`);
713
735
  this.emitAction({
714
736
  id: `edit:${config.key}`,
715
737
  label: 'Зберегти',
716
738
  type: 'api',
717
- api: config.api,
739
+ api,
718
740
  method: config.method || 'PATCH',
719
741
  payload: {
720
- ...event.row,
721
742
  ...(config.payload || {}),
722
- id: event.row['id'],
723
743
  [config.bindKey || config.key]: event.value,
724
744
  },
725
745
  }, event.row);
@@ -783,10 +803,9 @@ class TableComponent {
783
803
  });
784
804
  }
785
805
  setColumns() {
786
- const editable = this.editableColumns();
787
806
  this.columnsSignal.set(this.sourceColumns.filter((column) => !column['hidden']).map((column) => {
788
807
  const key = String(column['id'] || '');
789
- const config = editable.find((item) => item.key === key);
808
+ const config = this.editableConfig(column);
790
809
  return {
791
810
  key,
792
811
  labelKey: String(column['label'] || key),
@@ -796,12 +815,14 @@ class TableComponent {
796
815
  sortable: true,
797
816
  link: column['link'],
798
817
  editable: !!config,
799
- editableCondition: String(this.config?.props?.['editableCondition'] || config?.['condition'] || ''),
818
+ editableCondition: String(config?.['condition'] || ''),
800
819
  editor: config?.editor,
801
820
  bindKey: config?.bindKey || key,
802
821
  options: config?.options || [],
803
822
  data: config?.data,
804
- dataParams: config?.dataParams,
823
+ dataParams: config?.dataParams
824
+ ? resolveTemplateValue(config.dataParams, this.data, { interpolate: true })
825
+ : undefined,
805
826
  placeholder: config?.placeholder,
806
827
  };
807
828
  }));
@@ -870,11 +891,25 @@ class TableComponent {
870
891
  tableName() {
871
892
  return String(this.config?.props?.['table'] || this.config?.props?.['entity'] || '').trim();
872
893
  }
873
- editableColumns() {
874
- const value = this.config?.props?.['editableColumns'];
875
- return Array.isArray(value)
876
- ? value.filter((item) => !!item && typeof item === 'object' && !!String(item.key || '').trim())
877
- : [];
894
+ editableConfig(column) {
895
+ const value = column['editable'];
896
+ if (value === true) {
897
+ const key = String(column['id'] || '');
898
+ return {
899
+ key,
900
+ bindKey: key,
901
+ editor: column['data'] ? 'select' : 'text',
902
+ data: column['data'],
903
+ };
904
+ }
905
+ return value && typeof value === 'object' && !Array.isArray(value)
906
+ ? value
907
+ : undefined;
908
+ }
909
+ editableColumn(key) {
910
+ const column = this.sourceColumns.find((item) => String(item['id'] || '') === key);
911
+ const config = column ? this.editableConfig(column) : undefined;
912
+ return config ? { ...config, key } : undefined;
878
913
  }
879
914
  tableEndpoint(table) {
880
915
  const params = new URLSearchParams({