@dignite/ng.site 0.0.0-stage → 1.0.0
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.
- package/README.md +123 -2
- package/fesm2022/dignite-ng.site-config.mjs +87 -0
- package/fesm2022/dignite-ng.site-config.mjs.map +1 -0
- package/fesm2022/dignite-ng.site-content-editor.component-iFWDeYJ9.mjs +782 -0
- package/fesm2022/dignite-ng.site-content-editor.component-iFWDeYJ9.mjs.map +1 -0
- package/fesm2022/dignite-ng.site-content-types.component-D2jMSSq6.mjs +324 -0
- package/fesm2022/dignite-ng.site-content-types.component-D2jMSSq6.mjs.map +1 -0
- package/fesm2022/dignite-ng.site-contents.component-DKuel8w-.mjs +604 -0
- package/fesm2022/dignite-ng.site-contents.component-DKuel8w-.mjs.map +1 -0
- package/fesm2022/dignite-ng.site-dignite-ng.site-CsKxqjY9.mjs +2464 -0
- package/fesm2022/dignite-ng.site-dignite-ng.site-CsKxqjY9.mjs.map +1 -0
- package/fesm2022/dignite-ng.site-fields.component-By_YyfIx.mjs +409 -0
- package/fesm2022/dignite-ng.site-fields.component-By_YyfIx.mjs.map +1 -0
- package/fesm2022/dignite-ng.site-pages.component-S9BEgC4W.mjs +235 -0
- package/fesm2022/dignite-ng.site-pages.component-S9BEgC4W.mjs.map +1 -0
- package/fesm2022/dignite-ng.site.mjs +2 -0
- package/fesm2022/dignite-ng.site.mjs.map +1 -0
- package/package.json +48 -3
- package/types/dignite-ng.site-config.d.ts +14 -0
- package/types/dignite-ng.site.d.ts +1409 -0
|
@@ -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
|