@dignite/ng.site 0.0.0-stage → 1.0.1

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.
@@ -0,0 +1,604 @@
1
+ import * as i4 from '@abp/ng.core';
2
+ import { CoreModule, ListService, LIST_QUERY_DEBOUNCE_TIME, PermissionDirective } from '@abp/ng.core';
3
+ import * as i2$1 from '@abp/ng.theme.shared';
4
+ import { ConfirmationService, ToasterService, Confirmation, ThemeSharedModule } from '@abp/ng.theme.shared';
5
+ import * as i0 from '@angular/core';
6
+ import { inject, ElementRef, EventEmitter, HostListener, Output, Input, ChangeDetectionStrategy, Component, signal } from '@angular/core';
7
+ import * as i1 from '@angular/forms';
8
+ import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
9
+ import * as i2 from '@angular/router';
10
+ import { RouterLink } from '@angular/router';
11
+ import { FlexFieldSearchComponent, FlexFieldViewComponent } from '@dignite/ng.flex-fields';
12
+ import { forkJoin, switchMap, of, map } from 'rxjs';
13
+ import { C as ContentAdminService, a as ContentQueryService, b as ContentListStateService, c as ContentTypeAdminService, P as PageAdminService, S as SiteReferenceDataService, e as eSitePolicyNames, d as ContentStatus, F as FlexFieldQueryOperator, f as FlexFieldValueType } from './dignite-ng.site-dignite-ng.site-CsKxqjY9.mjs';
14
+ import * as i7 from '@abp/ng.components/tree';
15
+ import { TreeAdapter, TreeModule } from '@abp/ng.components/tree';
16
+ import * as i3 from '@ngx-validate/core';
17
+ import * as i6 from '@swimlane/ngx-datatable';
18
+
19
+ /**
20
+ * Picks one page, laid out as the page hierarchy rather than a flat list - a child page's name alone
21
+ * rarely says which section it sits under.
22
+ *
23
+ * Built like flex-fields' `ff-tree-search` - a `.form-select`-styled toggle over an `abp-tree` panel -
24
+ * because both render in the same content-list filter bar and should look and behave alike. That
25
+ * component's node renderer (`ff-tree-picker-nodes`) is not exported, hence `abp-tree` directly.
26
+ */
27
+ class PageTreeSelectComponent {
28
+ constructor() {
29
+ this.elementRef = inject(ElementRef);
30
+ /** Emits only on an actual change, like a native `<select>`'s `change`. */
31
+ this.valueChange = new EventEmitter();
32
+ this.nodes = [];
33
+ this.expandedKeys = [];
34
+ this.isOpen = false;
35
+ this.nodesByKey = new Map();
36
+ }
37
+ /** Every page, flat - the hierarchy is rebuilt from `parentId`. */
38
+ set pages(pages) {
39
+ const ids = new Set(pages.map(page => page.id));
40
+ // `TreeAdapter` silently drops a node whose parent is not in the list, taking its whole subtree
41
+ // with it; such a page shows at the root instead.
42
+ const list = pages.map(page => ({
43
+ ...page,
44
+ id: page.id,
45
+ parentId: page.parentId && ids.has(page.parentId) ? page.parentId : null,
46
+ }));
47
+ this.nodes = new TreeAdapter(list).getTree();
48
+ this.nodesByKey = new Map();
49
+ this.index(this.nodes);
50
+ // Opens fully expanded, the same as the page form's parent picker (`nzDefaultExpandAll`).
51
+ this.expandedKeys = [...this.nodesByKey.values()].filter(node => !node.isLeaf).map(node => node.key);
52
+ }
53
+ /**
54
+ * Passed to `abp-tree` as `selectedNode`, whose default `isNodeSelected` matches on `id` - which a
55
+ * `TreeNode` carries as well as its entity. A stable reference from {@link nodesByKey}, so the
56
+ * OnPush tree only re-renders when the selection really changes.
57
+ */
58
+ get selectedNode() {
59
+ return this.value ? this.nodesByKey.get(this.value) : undefined;
60
+ }
61
+ toggle() {
62
+ this.isOpen = !this.isOpen;
63
+ }
64
+ /** `abp-tree` emits the clicked node's entity, i.e. the page. */
65
+ select(page) {
66
+ this.isOpen = false;
67
+ this.emit(page.id);
68
+ }
69
+ clear(event) {
70
+ // Keeps the click from reaching the toggle underneath it.
71
+ event.stopPropagation();
72
+ this.emit(null);
73
+ }
74
+ closeOnOutsideClick(event) {
75
+ if (this.isOpen && !event.composedPath().includes(this.elementRef.nativeElement)) {
76
+ this.isOpen = false;
77
+ }
78
+ }
79
+ close() {
80
+ this.isOpen = false;
81
+ }
82
+ emit(pageId) {
83
+ if (pageId !== (this.value ?? null)) {
84
+ this.valueChange.emit(pageId);
85
+ }
86
+ }
87
+ index(nodes) {
88
+ for (const node of nodes) {
89
+ this.nodesByKey.set(node.key, node);
90
+ this.index(node.children);
91
+ }
92
+ }
93
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: PageTreeSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
94
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.2", type: PageTreeSelectComponent, isStandalone: true, selector: "site-page-tree-select", inputs: { pages: "pages", value: "value" }, outputs: { valueChange: "valueChange" }, host: { listeners: { "document:click": "closeOnOutsideClick($event)", "keydown.escape": "close()" } }, ngImport: i0, template: `
95
+ <div class="page-tree-select">
96
+ <button
97
+ type="button"
98
+ class="form-select text-start page-tree-select-toggle"
99
+ [class.text-muted]="!selectedNode"
100
+ aria-haspopup="tree"
101
+ [attr.aria-expanded]="isOpen"
102
+ (click)="toggle()"
103
+ >
104
+ {{ selectedNode?.title ?? '--' }}
105
+ </button>
106
+ @if (selectedNode) {
107
+ <button
108
+ type="button"
109
+ class="btn btn-link page-tree-select-clear"
110
+ [title]="'AbpUi::Clear' | abpLocalization"
111
+ (click)="clear($event)"
112
+ >
113
+ <i class="fa fa-times" aria-hidden="true"></i>
114
+ </button>
115
+ }
116
+
117
+ @if (isOpen) {
118
+ <div class="page-tree-select-menu">
119
+ <abp-tree
120
+ [nodes]="nodes"
121
+ [(expandedKeys)]="expandedKeys"
122
+ [selectedNode]="selectedNode"
123
+ (selectedNodeChange)="select($event)"
124
+ >
125
+ <ng-template abpTreeNodeTemplate let-node>
126
+ <span class="page-tree-select-node">{{ node.title }}</span>
127
+ </ng-template>
128
+ <ng-template abpTreeExpandedIconTemplate let-node>
129
+ <i
130
+ class="fa"
131
+ [class.fa-minus]="node.isExpanded"
132
+ [class.fa-plus]="!node.isExpanded"
133
+ aria-hidden="true"
134
+ ></i>
135
+ </ng-template>
136
+ </abp-tree>
137
+ </div>
138
+ }
139
+ </div>
140
+ `, isInline: true, styles: [".page-tree-select{position:relative}.page-tree-select-toggle{height:auto;min-height:calc(1.5em + 1.35rem + 2px);padding-right:3.75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.page-tree-select-clear{position:absolute;top:50%;right:2rem;display:inline-flex;align-items:center;height:100%;padding:0 .375rem;line-height:1;color:var(--bs-secondary-color, #6c757d);text-decoration:none;transform:translateY(-50%);z-index:2}.page-tree-select-menu{position:absolute;z-index:1050;width:100%;max-height:18rem;margin-top:.25rem;padding:.375rem;overflow:auto;color:var(--bs-body-color);background-color:var(--lpx-content-bg, var(--bs-secondary-bg, #fff));border:1px solid var(--bs-border-color, rgba(0, 0, 0, .12));border-radius:.375rem;box-shadow:0 .125rem .25rem #00000013}.page-tree-select-node{display:inline-block;min-height:1.75rem;padding:.125rem .375rem;border-radius:.25rem}:host ::ng-deep .ant-tree{background:transparent!important}:host ::ng-deep .ant-tree-node-content-wrapper,:host ::ng-deep .ant-tree-node-content-wrapper:hover{background-color:transparent!important}:host ::ng-deep .ant-tree-node-content-wrapper>.selected .page-tree-select-node{background-color:var(--lpx-brand, var(--bs-primary));color:var(--bs-white, #fff)}:host ::ng-deep .ant-tree-node-content-wrapper:hover>div:not(.selected) .page-tree-select-node{background-color:var(--bs-secondary-bg, #f5f5f5)}\n"], dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "ngmodule", type: TreeModule }, { kind: "component", type: i7.TreeComponent, selector: "abp-tree", inputs: ["noAnimation", "draggable", "checkable", "checkStrictly", "checkedKeys", "nodes", "expandedKeys", "selectedNode", "changeCheckboxWithNode", "isNodeSelected", "beforeDrop"], outputs: ["checkedKeysChange", "expandedKeysChange", "selectedNodeChange", "dropOver", "nzExpandChange"] }, { kind: "directive", type: i7.TreeNodeTemplateDirective, selector: "[abpTreeNodeTemplate],[abp-tree-node-template]" }, { kind: "directive", type: i7.ExpandedIconTemplateDirective, selector: "[abpTreeExpandedIconTemplate],[abp-tree-expanded-icon-template]" }, { kind: "pipe", type: i4.LocalizationPipe, name: "abpLocalization" }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
141
+ }
142
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: PageTreeSelectComponent, decorators: [{
143
+ type: Component,
144
+ args: [{ selector: 'site-page-tree-select', changeDetection: ChangeDetectionStrategy.Eager, imports: [CoreModule, TreeModule], template: `
145
+ <div class="page-tree-select">
146
+ <button
147
+ type="button"
148
+ class="form-select text-start page-tree-select-toggle"
149
+ [class.text-muted]="!selectedNode"
150
+ aria-haspopup="tree"
151
+ [attr.aria-expanded]="isOpen"
152
+ (click)="toggle()"
153
+ >
154
+ {{ selectedNode?.title ?? '--' }}
155
+ </button>
156
+ @if (selectedNode) {
157
+ <button
158
+ type="button"
159
+ class="btn btn-link page-tree-select-clear"
160
+ [title]="'AbpUi::Clear' | abpLocalization"
161
+ (click)="clear($event)"
162
+ >
163
+ <i class="fa fa-times" aria-hidden="true"></i>
164
+ </button>
165
+ }
166
+
167
+ @if (isOpen) {
168
+ <div class="page-tree-select-menu">
169
+ <abp-tree
170
+ [nodes]="nodes"
171
+ [(expandedKeys)]="expandedKeys"
172
+ [selectedNode]="selectedNode"
173
+ (selectedNodeChange)="select($event)"
174
+ >
175
+ <ng-template abpTreeNodeTemplate let-node>
176
+ <span class="page-tree-select-node">{{ node.title }}</span>
177
+ </ng-template>
178
+ <ng-template abpTreeExpandedIconTemplate let-node>
179
+ <i
180
+ class="fa"
181
+ [class.fa-minus]="node.isExpanded"
182
+ [class.fa-plus]="!node.isExpanded"
183
+ aria-hidden="true"
184
+ ></i>
185
+ </ng-template>
186
+ </abp-tree>
187
+ </div>
188
+ }
189
+ </div>
190
+ `, styles: [".page-tree-select{position:relative}.page-tree-select-toggle{height:auto;min-height:calc(1.5em + 1.35rem + 2px);padding-right:3.75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.page-tree-select-clear{position:absolute;top:50%;right:2rem;display:inline-flex;align-items:center;height:100%;padding:0 .375rem;line-height:1;color:var(--bs-secondary-color, #6c757d);text-decoration:none;transform:translateY(-50%);z-index:2}.page-tree-select-menu{position:absolute;z-index:1050;width:100%;max-height:18rem;margin-top:.25rem;padding:.375rem;overflow:auto;color:var(--bs-body-color);background-color:var(--lpx-content-bg, var(--bs-secondary-bg, #fff));border:1px solid var(--bs-border-color, rgba(0, 0, 0, .12));border-radius:.375rem;box-shadow:0 .125rem .25rem #00000013}.page-tree-select-node{display:inline-block;min-height:1.75rem;padding:.125rem .375rem;border-radius:.25rem}:host ::ng-deep .ant-tree{background:transparent!important}:host ::ng-deep .ant-tree-node-content-wrapper,:host ::ng-deep .ant-tree-node-content-wrapper:hover{background-color:transparent!important}:host ::ng-deep .ant-tree-node-content-wrapper>.selected .page-tree-select-node{background-color:var(--lpx-brand, var(--bs-primary));color:var(--bs-white, #fff)}:host ::ng-deep .ant-tree-node-content-wrapper:hover>div:not(.selected) .page-tree-select-node{background-color:var(--bs-secondary-bg, #f5f5f5)}\n"] }]
191
+ }], propDecorators: { pages: [{
192
+ type: Input
193
+ }], value: [{
194
+ type: Input
195
+ }], valueChange: [{
196
+ type: Output
197
+ }], closeOnOutsideClick: [{
198
+ type: HostListener,
199
+ args: ['document:click', ['$event']]
200
+ }], close: [{
201
+ type: HostListener,
202
+ args: ['keydown.escape']
203
+ }] } });
204
+
205
+ class ContentsComponent {
206
+ constructor() {
207
+ this.contentService = inject(ContentAdminService);
208
+ this.contentQuery = inject(ContentQueryService);
209
+ this.listState = inject(ContentListStateService);
210
+ this.contentTypeService = inject(ContentTypeAdminService);
211
+ this.pageService = inject(PageAdminService);
212
+ this.referenceData = inject(SiteReferenceDataService);
213
+ this.fb = inject(FormBuilder);
214
+ this.confirmation = inject(ConfirmationService);
215
+ this.toaster = inject(ToasterService);
216
+ this.list = inject(ListService);
217
+ this.policies = eSitePolicyNames;
218
+ this.ContentStatus = ContentStatus;
219
+ this.statuses = [ContentStatus.Draft, ContentStatus.Published, ContentStatus.Archived];
220
+ // Everything the template reads that changes in an HTTP callback is a signal, not a plain field: the
221
+ // host layout's ancestors (`abp-router-outlet`, the LeptonX layouts) are OnPush, and a plain-field
222
+ // write in a response callback never marks them for check, so the list would stay empty until some
223
+ // unrelated event happened to.
224
+ this.data = signal({ items: [], totalCount: 0 }, /* @ts-ignore */
225
+ ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
226
+ this.filters = {};
227
+ this.pages = signal([], /* @ts-ignore */
228
+ ...(ngDevMode ? [{ debugName: "pages" }] : /* istanbul ignore next */ []));
229
+ this.languages = signal([], /* @ts-ignore */
230
+ ...(ngDevMode ? [{ debugName: "languages" }] : /* istanbul ignore next */ []));
231
+ this.contentTypes = signal([], /* @ts-ignore */
232
+ ...(ngDevMode ? [{ debugName: "contentTypes" }] : /* istanbul ignore next */ []));
233
+ /**
234
+ * Every content type across every page, for resolving a row's display name. Deliberately separate
235
+ * from {@link contentTypes}, which is scoped to the selected page and drives the filter dropdown and
236
+ * field arrangements - a row's own page is not necessarily the one currently filtered on.
237
+ */
238
+ this.contentTypesById = signal(new Map(), /* @ts-ignore */
239
+ ...(ngDevMode ? [{ debugName: "contentTypesById" }] : /* istanbul ignore next */ []));
240
+ this.fieldsById = new Map();
241
+ /** Each content type's arrangement, joined with the field library. Keyed by content type id. */
242
+ this.arrangementByContentType = new Map();
243
+ /** Union of `showInList` fields across the page's content types, deduped by field. */
244
+ this.columns = signal([], /* @ts-ignore */
245
+ ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
246
+ /** Union of searchable fields, deduped by field. */
247
+ this.searchFields = signal([], /* @ts-ignore */
248
+ ...(ngDevMode ? [{ debugName: "searchFields" }] : /* istanbul ignore next */ []));
249
+ // --- stable-reference caches --------------------------------------------------------------------
250
+ //
251
+ // `ff-flex-field-*` decide whether to re-render by comparing their `fields` input's *object
252
+ // reference*. Binding `[fields]="someMethodCall(row, column)"` in a template calls that method - and
253
+ // allocates a new object - on every change-detection pass. The child sees a changed input, tears
254
+ // down and recreates its dynamic sub-component, which itself triggers change detection: Angular
255
+ // reports `NG0103: Infinite change detection` and the page never finishes rendering. So both caches
256
+ // hold stable references, rebuilt only when the underlying data actually changes.
257
+ /** Per field definition, for the search bar. Keyed by field name. */
258
+ this.definitionValues = signal(new Map(), /* @ts-ignore */
259
+ ...(ngDevMode ? [{ debugName: "definitionValues" }] : /* istanbul ignore next */ []));
260
+ /** Per row then field name, for the list cells. */
261
+ this.rowValues = signal(new Map(), /* @ts-ignore */
262
+ ...(ngDevMode ? [{ debugName: "rowValues" }] : /* istanbul ignore next */ []));
263
+ this.searchForm = this.fb.group({ flexFieldSearch: this.fb.group({}) });
264
+ this.appliedConditions = [];
265
+ /** Raw `flexFieldSearch` value restored from {@link ContentListStateService}, keyed by field name. */
266
+ this.searchValues = {};
267
+ const cached = this.listState.restore();
268
+ if (cached) {
269
+ this.filters = cached.filters;
270
+ this.appliedConditions = cached.appliedConditions;
271
+ this.searchValues = cached.searchValues;
272
+ this.list.page = cached.page;
273
+ this.list.sortKey = cached.sortKey;
274
+ this.list.sortOrder = cached.sortOrder;
275
+ this.list.maxResultCount = cached.maxResultCount;
276
+ }
277
+ this.pageService.getList({}).subscribe(result => {
278
+ this.pages.set(result.items ?? []);
279
+ });
280
+ // Restores the content-type dropdown, columns and search fields for a restored `pageId` - a no-op
281
+ // when there is none, same as a fresh visit.
282
+ this.fetchContentTypes().subscribe();
283
+ forkJoin({
284
+ schema: this.referenceData.getSchema(),
285
+ fields: this.referenceData.getFieldsById(),
286
+ contentTypes: this.referenceData.getContentTypesById(),
287
+ }).subscribe(({ schema, fields, contentTypes }) => {
288
+ // The schema projection is name-keyed, so it is used only for the site-level settings; field
289
+ // resolution goes through the Guid-keyed library below.
290
+ this.languages.set(schema.enabledLanguages ?? []);
291
+ this.fieldsById = fields;
292
+ this.contentTypesById.set(contentTypes);
293
+ this.rebuildArrangements();
294
+ });
295
+ // Waits for the schema before querying: on a first visit the reference data is uncached and
296
+ // routinely outlasts `ListService`'s 300ms debounce, so the first query would otherwise go out
297
+ // with no `cultureName` and list every language under a dropdown already showing one.
298
+ const getData = (query) => this.referenceData.getSchema().pipe(switchMap(schema => {
299
+ this.filters.cultureName ??=
300
+ schema.defaultLanguage ?? schema.enabledLanguages?.[0] ?? null;
301
+ // Snapshot on every real query, not just on `applyFilters()` - this is the one point
302
+ // every filter, search and paging change already funnels through.
303
+ this.listState.save({
304
+ filters: this.filters,
305
+ appliedConditions: this.appliedConditions,
306
+ searchValues: this.searchForm.get('flexFieldSearch')?.value ?? {},
307
+ page: this.list.page,
308
+ sortKey: this.list.sortKey,
309
+ sortOrder: this.list.sortOrder,
310
+ maxResultCount: this.list.maxResultCount,
311
+ });
312
+ return this.contentQuery.getList({
313
+ ...query,
314
+ ...this.filters,
315
+ flexFieldConditions: this.appliedConditions,
316
+ });
317
+ }));
318
+ const setData = (result) => {
319
+ this.data.set(result);
320
+ this.rebuildRowValues();
321
+ };
322
+ this.list.hookToQuery(getData).subscribe(setData);
323
+ }
324
+ // --- filters ------------------------------------------------------------------------------------
325
+ onPageChange(pageId) {
326
+ this.filters.pageId = pageId;
327
+ this.filters.contentTypeId = null;
328
+ this.clearSearch();
329
+ this.loadContentTypes();
330
+ }
331
+ applyFilters() {
332
+ this.appliedConditions = this.buildConditions();
333
+ this.list.page = 0;
334
+ this.list.get();
335
+ }
336
+ /**
337
+ * Resets each existing control rather than replacing the `flexFieldSearch` group.
338
+ *
339
+ * Every `ff-flex-field-search` keeps its own reference to the group it registered its control on
340
+ * (`FieldTypeControlBase.valuesGroup`), captured when its inputs were first set. Swapping in a new
341
+ * group would not change the identity of anything those children can see - `searchForm` itself is
342
+ * unchanged - so none of them would re-register. They would keep writing into the old, detached
343
+ * group while `buildConditions()` read the new, permanently empty one.
344
+ */
345
+ clearSearch() {
346
+ const group = this.searchForm.get('flexFieldSearch');
347
+ Object.values(group.controls).forEach(control => control.reset(''));
348
+ this.appliedConditions = [];
349
+ this.searchValues = {};
350
+ }
351
+ clearFiltersAndSearch() {
352
+ const cultureName = this.filters.cultureName;
353
+ this.filters = { cultureName };
354
+ this.clearSearch();
355
+ this.contentTypes.set([]);
356
+ this.rebuildArrangements();
357
+ this.applyFilters();
358
+ }
359
+ loadContentTypes() {
360
+ this.fetchContentTypes().subscribe(() => this.applyFilters());
361
+ }
362
+ /** Loads `contentTypes` for the current `filters.pageId` and rebuilds the derived arrangements. */
363
+ fetchContentTypes() {
364
+ if (!this.filters.pageId) {
365
+ this.contentTypes.set([]);
366
+ this.rebuildArrangements();
367
+ return of(undefined);
368
+ }
369
+ return this.contentTypeService.getListByPage(this.filters.pageId).pipe(map(result => {
370
+ this.contentTypes.set(result.items ?? []);
371
+ this.rebuildArrangements();
372
+ }));
373
+ }
374
+ // --- arrangement, columns, search fields ---------------------------------------------------------
375
+ /**
376
+ * Recomputes everything derived from "which content types are in play": the per-type arrangements,
377
+ * the column union and the searchable union. Runs on page change and whenever the field library
378
+ * arrives, never per change-detection pass.
379
+ */
380
+ rebuildArrangements() {
381
+ this.arrangementByContentType = new Map(this.contentTypes().map(contentType => [
382
+ contentType.id,
383
+ (contentType.fields ?? [])
384
+ .map(entry => this.toArrangedField(entry.fieldId, entry))
385
+ .filter((field) => !!field)
386
+ .sort((a, b) => a.order - b.order),
387
+ ]));
388
+ this.rebuildColumns();
389
+ this.rebuildSearchFields();
390
+ this.rebuildRowValues();
391
+ }
392
+ toArrangedField(fieldId, usage) {
393
+ const definition = fieldId ? this.fieldsById.get(fieldId) : undefined;
394
+ if (!definition) {
395
+ return undefined;
396
+ }
397
+ const fieldData = {
398
+ id: definition.id,
399
+ name: definition.name,
400
+ displayName: usage.displayName || definition.displayName,
401
+ description: definition.description ?? undefined,
402
+ fieldTypeName: definition.fieldTypeName,
403
+ configuration: (definition.configuration ?? {}),
404
+ };
405
+ return {
406
+ fieldId: definition.id,
407
+ name: definition.name,
408
+ displayName: fieldData.displayName,
409
+ fieldTypeName: definition.fieldTypeName,
410
+ required: !!usage.required,
411
+ searchable: !!usage.searchable,
412
+ showInList: !!usage.showInList,
413
+ order: usage.order ?? 0,
414
+ fieldData,
415
+ };
416
+ }
417
+ /**
418
+ * A field earns a column when a content type marks it `showInList` - that flag is the whole answer.
419
+ *
420
+ * No capability check on top of it: whether a field type ships a view component is not a reason to
421
+ * drop a column the arrangement explicitly asked for. Silently omitting it would leave the editor
422
+ * looking at a setting that appears to do nothing.
423
+ */
424
+ rebuildColumns() {
425
+ const columns = new Map();
426
+ for (const [contentTypeId, fields] of this.arrangementByContentType) {
427
+ for (const field of fields) {
428
+ if (!field.showInList) {
429
+ continue;
430
+ }
431
+ const existing = columns.get(field.fieldId);
432
+ if (existing) {
433
+ existing.contentTypeIds.add(contentTypeId);
434
+ }
435
+ else {
436
+ columns.set(field.fieldId, {
437
+ fieldId: field.fieldId,
438
+ name: field.name,
439
+ displayName: field.displayName,
440
+ fieldTypeName: field.fieldTypeName,
441
+ contentTypeIds: new Set([contentTypeId]),
442
+ });
443
+ }
444
+ }
445
+ }
446
+ this.columns.set([...columns.values()]);
447
+ }
448
+ /**
449
+ * A field is filterable when a content type marks it `searchable` - again, that flag is the whole
450
+ * answer. The field arrangement editor already refuses to set it on a type the server reports as
451
+ * non-indexable, so the check belongs there, where it can explain itself, rather than here where it
452
+ * would quietly drop a filter someone had configured.
453
+ *
454
+ * `ff-flex-field-search` renders nothing for a type with no search component (`DateTime` today); that
455
+ * is the library's documented behaviour, not something to pre-empt by hiding the field.
456
+ */
457
+ rebuildSearchFields() {
458
+ const fields = new Map();
459
+ for (const arranged of this.arrangementByContentType.values()) {
460
+ for (const field of arranged) {
461
+ if (fields.has(field.fieldId) || !field.searchable) {
462
+ continue;
463
+ }
464
+ fields.set(field.fieldId, field);
465
+ }
466
+ }
467
+ const searchFields = [...fields.values()];
468
+ this.searchFields.set(searchFields);
469
+ this.definitionValues.set(new Map(searchFields.map(field => [
470
+ field.name,
471
+ { field: field.fieldData, required: field.required, searchable: true, value: undefined },
472
+ ])));
473
+ }
474
+ rebuildRowValues() {
475
+ this.rowValues.set(new Map((this.data().items ?? []).map(content => [
476
+ content.id,
477
+ new Map((this.arrangementByContentType.get(content.contentTypeId ?? '') ?? []).map(field => [
478
+ field.name,
479
+ {
480
+ field: field.fieldData,
481
+ required: field.required,
482
+ searchable: field.searchable,
483
+ value: content.fieldValues?.[field.name],
484
+ },
485
+ ])),
486
+ ])));
487
+ }
488
+ /** The stable value for one cell, or `undefined` when this row's type does not use the field. */
489
+ rowValueOf(content, column) {
490
+ return this.rowValues().get(content.id)?.get(column.name);
491
+ }
492
+ /** The stable value for one search widget. See {@link definitionValues}. */
493
+ definitionValueOf(field) {
494
+ return this.definitionValues().get(field.name);
495
+ }
496
+ /** What one search widget should redisplay on restore - see {@link ContentListStateService}. */
497
+ selectedValueOf(field) {
498
+ return this.searchValues[field.name] ?? '';
499
+ }
500
+ // --- search conditions ---------------------------------------------------------------------------
501
+ /**
502
+ * Turns what each `ff-flex-field-search` wrote into the conditions the server understands. The
503
+ * mapping depends on each field type's storage shape, not on anything generic the search form knows.
504
+ */
505
+ buildConditions() {
506
+ const raw = (this.searchForm.get('flexFieldSearch')?.value ?? {});
507
+ return this.searchFields().flatMap(field => this.toConditions(field, raw[field.name]));
508
+ }
509
+ toConditions(field, value) {
510
+ switch (field.fieldTypeName) {
511
+ case 'Text':
512
+ return this.isBlank(value)
513
+ ? []
514
+ : [
515
+ this.condition(field, FlexFieldQueryOperator.Contains, String(value), FlexFieldValueType.String),
516
+ ];
517
+ case 'Number': {
518
+ // `ff-number-search` writes one "min-max" string; the executor composes two conditions on the
519
+ // same field as a range rather than a conflict.
520
+ const range = typeof value === 'string' ? value.split('-') : [];
521
+ if (range.length !== 2 || range.some(part => part === '')) {
522
+ return [];
523
+ }
524
+ return [
525
+ this.condition(field, FlexFieldQueryOperator.GreaterThanOrEqual, range[0], FlexFieldValueType.Number),
526
+ this.condition(field, FlexFieldQueryOperator.LessThanOrEqual, range[1], FlexFieldValueType.Number),
527
+ ];
528
+ }
529
+ case 'Boolean':
530
+ // A plain `<option [value]>` binding always stringifies, so this arrives as "true"/"false".
531
+ return value === 'true' || value === 'false'
532
+ ? [this.condition(field, FlexFieldQueryOperator.Equals, value, FlexFieldValueType.Boolean)]
533
+ : [];
534
+ case 'Select':
535
+ case 'Tree': {
536
+ const values = Array.isArray(value) ? value : this.isBlank(value) ? [] : [value];
537
+ return values.length === 0
538
+ ? []
539
+ : [
540
+ this.condition(field, FlexFieldQueryOperator.In, values.join(','), FlexFieldValueType.String),
541
+ ];
542
+ }
543
+ default:
544
+ return [];
545
+ }
546
+ }
547
+ /** Both `fieldId` and `fieldName` are required by the server - always send the pair. */
548
+ condition(field, operator, value, valueType) {
549
+ return { fieldId: field.fieldId, fieldName: field.name, operator, value, valueType };
550
+ }
551
+ isBlank(value) {
552
+ return value === null || value === undefined || value === '';
553
+ }
554
+ // --- display helpers -----------------------------------------------------------------------------
555
+ statusKeyOf(status) {
556
+ return `Site::Enum:ContentStatus:${ContentStatus[status ?? ContentStatus.Draft]}`;
557
+ }
558
+ contentTypeNameOf(contentTypeId) {
559
+ return (contentTypeId ? this.contentTypesById().get(contentTypeId)?.displayName : undefined) ?? '---';
560
+ }
561
+ pageNameOf(pageId) {
562
+ return this.pages().find(page => page.id === pageId)?.displayName ?? '---';
563
+ }
564
+ // --- create / edit -------------------------------------------------------------------------------
565
+ //
566
+ // Editing happens on its own page (`/site/contents/create`, `/site/contents/:id/edit`), so this
567
+ // component only links there. Deleting stays here because it needs no form.
568
+ /** Creating needs a page picked, since a content type belongs to one. */
569
+ get canCreate() {
570
+ return !!this.filters.pageId && this.contentTypes().length > 0;
571
+ }
572
+ delete(content) {
573
+ this.confirmation
574
+ .warn('AbpUi::ItemWillBeDeletedMessage', 'AbpUi::AreYouSure', {
575
+ messageLocalizationParams: [content.slug || this.pageNameOf(content.pageId)],
576
+ })
577
+ .subscribe((status) => {
578
+ if (status === Confirmation.Status.confirm) {
579
+ this.contentService.delete(content.id).subscribe(() => {
580
+ this.list.get();
581
+ this.toaster.success('AbpUi::SuccessfullyDeleted');
582
+ });
583
+ }
584
+ });
585
+ }
586
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
587
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.2", type: ContentsComponent, isStandalone: true, selector: "site-contents", providers: [ListService, { provide: LIST_QUERY_DEBOUNCE_TIME, useValue: 300 }], ngImport: i0, template: "<div class=\"card mb-3\">\n <div class=\"card-body\">\n <div class=\"row align-items-end g-2 mb-3\">\n <div class=\"col-md-3\">\n <label class=\"form-label\">{{ 'Site::Menu:Pages' | abpLocalization }}</label>\n <site-page-tree-select\n [pages]=\"pages()\"\n [value]=\"filters.pageId\"\n (valueChange)=\"onPageChange($event)\"\n ></site-page-tree-select>\n </div>\n <div class=\"col-md-2\">\n <label class=\"form-label\">{{ 'Site::DisplayName:Content.ContentType' | abpLocalization }}</label>\n <select\n class=\"form-select\"\n [(ngModel)]=\"filters.contentTypeId\"\n (change)=\"applyFilters()\"\n [disabled]=\"!filters.pageId\"\n >\n <option [ngValue]=\"null\">--</option>\n @for (contentType of contentTypes(); track contentType.id) {\n <option [ngValue]=\"contentType.id\">{{ contentType.displayName }}</option>\n }\n </select>\n </div>\n <div class=\"col-md-2\">\n <label class=\"form-label\">{{ 'Site::DisplayName:Content.CultureName' | abpLocalization }}</label>\n <select class=\"form-select\" [(ngModel)]=\"filters.cultureName\" (change)=\"applyFilters()\">\n @for (language of languages(); track language) {\n <option [ngValue]=\"language\">{{ language }}</option>\n }\n </select>\n </div>\n <div class=\"col-md-2\">\n <label class=\"form-label\">{{ 'Site::DisplayName:Content.Status' | abpLocalization }}</label>\n <select class=\"form-select\" [(ngModel)]=\"filters.status\" (change)=\"applyFilters()\">\n <option [ngValue]=\"null\">--</option>\n @for (status of statuses; track status) {\n <option [ngValue]=\"status\">{{ statusKeyOf(status) | abpLocalization }}</option>\n }\n </select>\n </div>\n <div class=\"col-md-3\">\n <label class=\"form-label\">{{ 'AbpUi::Search' | abpLocalization }}</label>\n <input\n type=\"text\"\n class=\"form-control\"\n [(ngModel)]=\"filters.filter\"\n (keyup.enter)=\"applyFilters()\"\n [placeholder]=\"'AbpUi::Search' | abpLocalization\"\n />\n </div>\n </div>\n\n @if (searchFields().length > 0) {\n <hr />\n <h6>{{ 'Site::FilterByFieldValue' | abpLocalization }}</h6>\n <form [formGroup]=\"searchForm\">\n <div class=\"row g-2\">\n @for (field of searchFields(); track field.fieldId) {\n <!-- No label here: each search widget renders the field's own label. -->\n <div class=\"col-md-3\">\n <ff-flex-field-search\n [fields]=\"definitionValueOf(field)\"\n [selected]=\"selectedValueOf(field)\"\n [entity]=\"searchForm\"\n parentFieldName=\"flexFieldSearch\"\n ></ff-flex-field-search>\n </div>\n }\n </div>\n </form>\n }\n\n <div class=\"mt-3\">\n <button type=\"button\" class=\"btn btn-primary me-2\" (click)=\"applyFilters()\">\n <i class=\"fa fa-search me-1\"></i>{{ 'AbpUi::Search' | abpLocalization }}\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"clearFiltersAndSearch()\">\n {{ 'AbpUi::Clear' | abpLocalization }}\n </button>\n </div>\n </div>\n</div>\n\n<div class=\"card\">\n <div class=\"card-body\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <h5 class=\"mb-0\">{{ 'Site::Contents' | abpLocalization }}</h5>\n <a\n *abpPermission=\"policies.ContentsCreate\"\n class=\"btn btn-primary\"\n [class.disabled]=\"!canCreate\"\n [routerLink]=\"canCreate ? ['/site/contents/create'] : null\"\n [queryParams]=\"{ pageId: filters.pageId, cultureName: filters.cultureName }\"\n >\n <i class=\"fa fa-plus me-1\"></i>{{ 'Site::NewContent' | abpLocalization }}\n </a>\n </div>\n\n @if (!filters.pageId) {\n <p class=\"text-muted\">{{ 'Site::PageFilterRequiredForFieldColumns' | abpLocalization }}</p>\n }\n\n <!-- `[list]` drives paging through `ListService` (server-side: `skipCount`/`maxResultCount`) and\n renders the localized total, loading spinner and empty message; `default` is ABP's stock\n table look, including the horizontal scrollbar a wide field-column union needs. Every column\n is unsortable, keeping the server's default `PublishTime desc, Id desc` - a deterministic\n order, so pages never overlap or skip rows. Field columns could not sort anyway: `sorting`\n is a dynamic-LINQ order over entity properties, and field values live in JSON. -->\n <ngx-datatable default [rows]=\"data().items\" [count]=\"data().totalCount\" [list]=\"list\">\n <ngx-datatable-column\n [name]=\"'Site::DisplayName:Content.Slug' | abpLocalization\"\n [sortable]=\"false\"\n >\n <ng-template let-row=\"row\" ngx-datatable-cell-template>\n {{ row.slug || ('Site::SlugIsPageOwnAddress' | abpLocalization) }}\n </ng-template>\n </ngx-datatable-column>\n <ngx-datatable-column\n [name]=\"'Site::DisplayName:Content.ContentType' | abpLocalization\"\n [sortable]=\"false\"\n >\n <ng-template let-row=\"row\" ngx-datatable-cell-template>\n {{ contentTypeNameOf(row.contentTypeId) }}\n </ng-template>\n </ngx-datatable-column>\n <ngx-datatable-column\n [name]=\"'Site::DisplayName:Content.Status' | abpLocalization\"\n [sortable]=\"false\"\n >\n <ng-template let-row=\"row\" ngx-datatable-cell-template>\n {{ statusKeyOf(row.status) | abpLocalization }}\n </ng-template>\n </ngx-datatable-column>\n @for (column of columns(); track column.fieldId) {\n <ngx-datatable-column [name]=\"column.displayName\" [sortable]=\"false\">\n <ng-template let-row=\"row\" ngx-datatable-cell-template>\n <!-- Columns are the union across the page's content types, so a row whose type does not\n use this field has nothing to show here. -->\n @if (rowValueOf(row, column); as fieldValue) {\n <ff-flex-field-view\n [showInList]=\"true\"\n [type]=\"column.fieldTypeName\"\n [fields]=\"fieldValue\"\n [value]=\"fieldValue.value\"\n ></ff-flex-field-view>\n } @else {\n ---\n }\n </ng-template>\n </ngx-datatable-column>\n }\n <ngx-datatable-column\n [name]=\"'Site::DisplayName:Content.PublishTime' | abpLocalization\"\n [sortable]=\"false\"\n >\n <ng-template let-row=\"row\" ngx-datatable-cell-template>\n {{ row.publishTime | shortDateTime }}\n </ng-template>\n </ngx-datatable-column>\n <ngx-datatable-column\n [name]=\"'AbpUi::Actions' | abpLocalization\"\n [sortable]=\"false\"\n [width]=\"130\"\n [maxWidth]=\"130\"\n >\n <ng-template let-row=\"row\" ngx-datatable-cell-template>\n <a\n *abpPermission=\"policies.ContentsUpdate\"\n class=\"btn btn-sm btn-light me-1\"\n [routerLink]=\"['/site/contents', row.id, 'edit']\"\n [title]=\"'AbpUi::Edit' | abpLocalization\"\n >\n <i class=\"fa fa-pencil\"></i>\n </a>\n <button\n *abpPermission=\"policies.ContentsDelete\"\n type=\"button\"\n class=\"btn btn-sm btn-light\"\n (click)=\"delete(row)\"\n [title]=\"'AbpUi::Delete' | abpLocalization\"\n >\n <i class=\"fa fa-trash text-danger\"></i>\n </button>\n </ng-template>\n </ngx-datatable-column>\n </ngx-datatable>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple]):not([ngNoCva])[formControlName],select:not([multiple]):not([ngNoCva])[formControl],select:not([multiple]):not([ngNoCva])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i3.ValidationGroupDirective, selector: "[formGroup],[formGroupName]", exportAs: ["validationGroup"] }, { kind: "directive", type: i4.PermissionDirective, selector: "[abpPermission]", inputs: ["abpPermission", "abpPermissionRunChangeDetection"] }, { kind: "ngmodule", type: ThemeSharedModule }, { kind: "component", type: i6.DatatableComponent, selector: "ngx-datatable", inputs: ["targetMarkerTemplate", "rows", "groupRowsBy", "groupedRows", "columns", "selected", "scrollbarV", "scrollbarVDynamic", "scrollbarH", "rowHeight", "columnMode", "headerHeight", "footerHeight", "externalPaging", "externalSorting", "limit", "count", "offset", "loadingIndicator", "ghostLoadingIndicator", "selectionType", "reorderable", "swapColumns", "sortType", "sorts", "cssClasses", "messages", "rowClass", "selectCheck", "displayCheck", "groupExpansionDefault", "trackByProp", "selectAllRowsOnPage", "virtualization", "treeFromRelation", "treeToRelation", "summaryRow", "summaryHeight", "summaryPosition", "disableRowCheck", "rowDraggable", "enableClearingSortState", "rowIdentity"], outputs: ["scroll", "activate", "select", "sort", "page", "reorder", "resize", "tableContextmenu", "treeAction", "rowDragEvents"] }, { kind: "directive", type: i6.DataTableColumnDirective, selector: "ngx-datatable-column", inputs: ["name", "prop", "bindAsUnsafeHtml", "frozenLeft", "frozenRight", "flexGrow", "resizeable", "comparator", "pipe", "sortable", "draggable", "canAutoResize", "minWidth", "width", "maxWidth", "checkboxable", "headerCheckboxable", "headerClass", "cellClass", "isTreeColumn", "treeLevelIndent", "summaryFunc", "summaryTemplate", "cellTemplate", "headerTemplate", "treeToggleTemplate", "ghostCellTemplate"] }, { kind: "directive", type: i6.DataTableColumnCellDirective, selector: "[ngx-datatable-cell-template]" }, { kind: "directive", type: i2$1.NgxDatatableListDirective, selector: "ngx-datatable[list]", inputs: ["list"], exportAs: ["ngxDatatableList"] }, { kind: "directive", type: i2$1.NgxDatatableDefaultDirective, selector: "ngx-datatable[default]", inputs: ["class"], exportAs: ["ngxDatatableDefault"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: FlexFieldSearchComponent, selector: "ff-flex-field-search", inputs: ["fields", "entity", "parentFieldName", "selected"] }, { kind: "component", type: FlexFieldViewComponent, selector: "ff-flex-field-view", inputs: ["showInList", "fields", "type", "value"] }, { kind: "component", type: PageTreeSelectComponent, selector: "site-page-tree-select", inputs: ["pages", "value"], outputs: ["valueChange"] }, { kind: "pipe", type: i4.LocalizationPipe, name: "abpLocalization" }, { kind: "pipe", type: i4.ShortDateTimePipe, name: "shortDateTime" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
588
+ }
589
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentsComponent, decorators: [{
590
+ type: Component,
591
+ args: [{ selector: 'site-contents', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
592
+ CoreModule,
593
+ ThemeSharedModule,
594
+ ReactiveFormsModule,
595
+ PermissionDirective,
596
+ RouterLink,
597
+ FlexFieldSearchComponent,
598
+ FlexFieldViewComponent,
599
+ PageTreeSelectComponent,
600
+ ], providers: [ListService, { provide: LIST_QUERY_DEBOUNCE_TIME, useValue: 300 }], template: "<div class=\"card mb-3\">\n <div class=\"card-body\">\n <div class=\"row align-items-end g-2 mb-3\">\n <div class=\"col-md-3\">\n <label class=\"form-label\">{{ 'Site::Menu:Pages' | abpLocalization }}</label>\n <site-page-tree-select\n [pages]=\"pages()\"\n [value]=\"filters.pageId\"\n (valueChange)=\"onPageChange($event)\"\n ></site-page-tree-select>\n </div>\n <div class=\"col-md-2\">\n <label class=\"form-label\">{{ 'Site::DisplayName:Content.ContentType' | abpLocalization }}</label>\n <select\n class=\"form-select\"\n [(ngModel)]=\"filters.contentTypeId\"\n (change)=\"applyFilters()\"\n [disabled]=\"!filters.pageId\"\n >\n <option [ngValue]=\"null\">--</option>\n @for (contentType of contentTypes(); track contentType.id) {\n <option [ngValue]=\"contentType.id\">{{ contentType.displayName }}</option>\n }\n </select>\n </div>\n <div class=\"col-md-2\">\n <label class=\"form-label\">{{ 'Site::DisplayName:Content.CultureName' | abpLocalization }}</label>\n <select class=\"form-select\" [(ngModel)]=\"filters.cultureName\" (change)=\"applyFilters()\">\n @for (language of languages(); track language) {\n <option [ngValue]=\"language\">{{ language }}</option>\n }\n </select>\n </div>\n <div class=\"col-md-2\">\n <label class=\"form-label\">{{ 'Site::DisplayName:Content.Status' | abpLocalization }}</label>\n <select class=\"form-select\" [(ngModel)]=\"filters.status\" (change)=\"applyFilters()\">\n <option [ngValue]=\"null\">--</option>\n @for (status of statuses; track status) {\n <option [ngValue]=\"status\">{{ statusKeyOf(status) | abpLocalization }}</option>\n }\n </select>\n </div>\n <div class=\"col-md-3\">\n <label class=\"form-label\">{{ 'AbpUi::Search' | abpLocalization }}</label>\n <input\n type=\"text\"\n class=\"form-control\"\n [(ngModel)]=\"filters.filter\"\n (keyup.enter)=\"applyFilters()\"\n [placeholder]=\"'AbpUi::Search' | abpLocalization\"\n />\n </div>\n </div>\n\n @if (searchFields().length > 0) {\n <hr />\n <h6>{{ 'Site::FilterByFieldValue' | abpLocalization }}</h6>\n <form [formGroup]=\"searchForm\">\n <div class=\"row g-2\">\n @for (field of searchFields(); track field.fieldId) {\n <!-- No label here: each search widget renders the field's own label. -->\n <div class=\"col-md-3\">\n <ff-flex-field-search\n [fields]=\"definitionValueOf(field)\"\n [selected]=\"selectedValueOf(field)\"\n [entity]=\"searchForm\"\n parentFieldName=\"flexFieldSearch\"\n ></ff-flex-field-search>\n </div>\n }\n </div>\n </form>\n }\n\n <div class=\"mt-3\">\n <button type=\"button\" class=\"btn btn-primary me-2\" (click)=\"applyFilters()\">\n <i class=\"fa fa-search me-1\"></i>{{ 'AbpUi::Search' | abpLocalization }}\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"clearFiltersAndSearch()\">\n {{ 'AbpUi::Clear' | abpLocalization }}\n </button>\n </div>\n </div>\n</div>\n\n<div class=\"card\">\n <div class=\"card-body\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <h5 class=\"mb-0\">{{ 'Site::Contents' | abpLocalization }}</h5>\n <a\n *abpPermission=\"policies.ContentsCreate\"\n class=\"btn btn-primary\"\n [class.disabled]=\"!canCreate\"\n [routerLink]=\"canCreate ? ['/site/contents/create'] : null\"\n [queryParams]=\"{ pageId: filters.pageId, cultureName: filters.cultureName }\"\n >\n <i class=\"fa fa-plus me-1\"></i>{{ 'Site::NewContent' | abpLocalization }}\n </a>\n </div>\n\n @if (!filters.pageId) {\n <p class=\"text-muted\">{{ 'Site::PageFilterRequiredForFieldColumns' | abpLocalization }}</p>\n }\n\n <!-- `[list]` drives paging through `ListService` (server-side: `skipCount`/`maxResultCount`) and\n renders the localized total, loading spinner and empty message; `default` is ABP's stock\n table look, including the horizontal scrollbar a wide field-column union needs. Every column\n is unsortable, keeping the server's default `PublishTime desc, Id desc` - a deterministic\n order, so pages never overlap or skip rows. Field columns could not sort anyway: `sorting`\n is a dynamic-LINQ order over entity properties, and field values live in JSON. -->\n <ngx-datatable default [rows]=\"data().items\" [count]=\"data().totalCount\" [list]=\"list\">\n <ngx-datatable-column\n [name]=\"'Site::DisplayName:Content.Slug' | abpLocalization\"\n [sortable]=\"false\"\n >\n <ng-template let-row=\"row\" ngx-datatable-cell-template>\n {{ row.slug || ('Site::SlugIsPageOwnAddress' | abpLocalization) }}\n </ng-template>\n </ngx-datatable-column>\n <ngx-datatable-column\n [name]=\"'Site::DisplayName:Content.ContentType' | abpLocalization\"\n [sortable]=\"false\"\n >\n <ng-template let-row=\"row\" ngx-datatable-cell-template>\n {{ contentTypeNameOf(row.contentTypeId) }}\n </ng-template>\n </ngx-datatable-column>\n <ngx-datatable-column\n [name]=\"'Site::DisplayName:Content.Status' | abpLocalization\"\n [sortable]=\"false\"\n >\n <ng-template let-row=\"row\" ngx-datatable-cell-template>\n {{ statusKeyOf(row.status) | abpLocalization }}\n </ng-template>\n </ngx-datatable-column>\n @for (column of columns(); track column.fieldId) {\n <ngx-datatable-column [name]=\"column.displayName\" [sortable]=\"false\">\n <ng-template let-row=\"row\" ngx-datatable-cell-template>\n <!-- Columns are the union across the page's content types, so a row whose type does not\n use this field has nothing to show here. -->\n @if (rowValueOf(row, column); as fieldValue) {\n <ff-flex-field-view\n [showInList]=\"true\"\n [type]=\"column.fieldTypeName\"\n [fields]=\"fieldValue\"\n [value]=\"fieldValue.value\"\n ></ff-flex-field-view>\n } @else {\n ---\n }\n </ng-template>\n </ngx-datatable-column>\n }\n <ngx-datatable-column\n [name]=\"'Site::DisplayName:Content.PublishTime' | abpLocalization\"\n [sortable]=\"false\"\n >\n <ng-template let-row=\"row\" ngx-datatable-cell-template>\n {{ row.publishTime | shortDateTime }}\n </ng-template>\n </ngx-datatable-column>\n <ngx-datatable-column\n [name]=\"'AbpUi::Actions' | abpLocalization\"\n [sortable]=\"false\"\n [width]=\"130\"\n [maxWidth]=\"130\"\n >\n <ng-template let-row=\"row\" ngx-datatable-cell-template>\n <a\n *abpPermission=\"policies.ContentsUpdate\"\n class=\"btn btn-sm btn-light me-1\"\n [routerLink]=\"['/site/contents', row.id, 'edit']\"\n [title]=\"'AbpUi::Edit' | abpLocalization\"\n >\n <i class=\"fa fa-pencil\"></i>\n </a>\n <button\n *abpPermission=\"policies.ContentsDelete\"\n type=\"button\"\n class=\"btn btn-sm btn-light\"\n (click)=\"delete(row)\"\n [title]=\"'AbpUi::Delete' | abpLocalization\"\n >\n <i class=\"fa fa-trash text-danger\"></i>\n </button>\n </ng-template>\n </ngx-datatable-column>\n </ngx-datatable>\n </div>\n</div>\n" }]
601
+ }], ctorParameters: () => [] });
602
+
603
+ export { ContentsComponent };
604
+ //# sourceMappingURL=dignite-ng.site-contents.component-DKuel8w-.mjs.map