@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.
|
|
711
|
-
if (!config
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
874
|
-
const value =
|
|
875
|
-
|
|
876
|
-
|
|
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({
|