@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.
@@ -0,0 +1,2464 @@
1
+ import * as i4 from '@abp/ng.core';
2
+ import { RestService, CoreModule, EnvironmentService, ListService, LIST_QUERY_DEBOUNCE_TIME, mapEnumToOptions, authGuard, permissionGuard, RouterOutletComponent } from '@abp/ng.core';
3
+ import * as i1 from '@angular/common';
4
+ import { CommonModule } from '@angular/common';
5
+ import * as i0 from '@angular/core';
6
+ import { inject, Injectable, ChangeDetectionStrategy, Component, signal, Input, ElementRef, EventEmitter, ViewChild, HostListener, Output, Pipe } from '@angular/core';
7
+ import * as i1$1 from '@angular/forms';
8
+ import { ReactiveFormsModule, Validators, FormGroup, FormControl, FormArray, FormsModule, UntypedFormGroup, UntypedFormControl } from '@angular/forms';
9
+ import { FieldTypeConfigBase, FieldTypeControlBase, readStringList } from '@dignite/ng.flex-fields';
10
+ import * as i3 from '@ngx-validate/core';
11
+ import { forkJoin, of, tap, map, finalize, shareReplay } from 'rxjs';
12
+ import { catchError } from 'rxjs/operators';
13
+ import * as i2$1 from '@angular/cdk/drag-drop';
14
+ import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
15
+ import * as i2 from '@abp/ng.theme.shared';
16
+ import { ThemeSharedModule } from '@abp/ng.theme.shared';
17
+ import * as i7 from '@abp/ng.components/tree';
18
+ import { TreeModule } from '@abp/ng.components/tree';
19
+ import * as i6 from '@swimlane/ngx-datatable';
20
+
21
+ /**
22
+ * Mirrors `SiteAdminPermissions` (`src/Dignite.Site.Admin.Application.Contracts/Permissions/SiteAdminPermissions.cs`).
23
+ *
24
+ * Note the group name is the bare string `SiteAdmin`, not `Site.Admin` - these are the exact strings the
25
+ * server checks, so they cannot be prefixed further here.
26
+ */
27
+ // A plain enum, not `const enum`: templates reference these through a component property, and a
28
+ // `const enum` is erased at compile time so it has no value to read at runtime.
29
+ var eSitePolicyNames;
30
+ (function (eSitePolicyNames) {
31
+ eSitePolicyNames["Pages"] = "SiteAdmin.Pages";
32
+ eSitePolicyNames["PagesCreate"] = "SiteAdmin.Pages.Create";
33
+ eSitePolicyNames["PagesUpdate"] = "SiteAdmin.Pages.Update";
34
+ eSitePolicyNames["PagesDelete"] = "SiteAdmin.Pages.Delete";
35
+ eSitePolicyNames["ContentTypes"] = "SiteAdmin.ContentTypes";
36
+ eSitePolicyNames["ContentTypesCreate"] = "SiteAdmin.ContentTypes.Create";
37
+ eSitePolicyNames["ContentTypesUpdate"] = "SiteAdmin.ContentTypes.Update";
38
+ eSitePolicyNames["ContentTypesDelete"] = "SiteAdmin.ContentTypes.Delete";
39
+ eSitePolicyNames["Fields"] = "SiteAdmin.Fields";
40
+ eSitePolicyNames["FieldsCreate"] = "SiteAdmin.Fields.Create";
41
+ eSitePolicyNames["FieldsUpdate"] = "SiteAdmin.Fields.Update";
42
+ eSitePolicyNames["FieldsDelete"] = "SiteAdmin.Fields.Delete";
43
+ /** Separate from update on purpose: renaming a field migrates every stored value. */
44
+ eSitePolicyNames["FieldsRename"] = "SiteAdmin.Fields.Rename";
45
+ eSitePolicyNames["FieldGroups"] = "SiteAdmin.FieldGroups";
46
+ eSitePolicyNames["FieldGroupsCreate"] = "SiteAdmin.FieldGroups.Create";
47
+ eSitePolicyNames["FieldGroupsUpdate"] = "SiteAdmin.FieldGroups.Update";
48
+ eSitePolicyNames["FieldGroupsDelete"] = "SiteAdmin.FieldGroups.Delete";
49
+ eSitePolicyNames["Contents"] = "SiteAdmin.Contents";
50
+ eSitePolicyNames["ContentsCreate"] = "SiteAdmin.Contents.Create";
51
+ eSitePolicyNames["ContentsUpdate"] = "SiteAdmin.Contents.Update";
52
+ eSitePolicyNames["ContentsDelete"] = "SiteAdmin.Contents.Delete";
53
+ })(eSitePolicyNames || (eSitePolicyNames = {}));
54
+
55
+ class ContentTypeAdminService {
56
+ constructor() {
57
+ this.restService = inject(RestService);
58
+ this.apiName = 'SiteAdmin';
59
+ this.create = (input, config) => this.restService.request({
60
+ method: 'POST',
61
+ url: '/api/site-admin/content-types',
62
+ body: input,
63
+ }, { apiName: this.apiName, ...config });
64
+ this.delete = (id, config) => this.restService.request({
65
+ method: 'DELETE',
66
+ url: `/api/site-admin/content-types/${id}`,
67
+ }, { apiName: this.apiName, ...config });
68
+ this.findByName = (pageId, name, config) => this.restService.request({
69
+ method: 'GET',
70
+ url: `/api/site-admin/content-types/by-page/${pageId}/by-name`,
71
+ params: { name },
72
+ }, { apiName: this.apiName, ...config });
73
+ this.get = (id, config) => this.restService.request({
74
+ method: 'GET',
75
+ url: `/api/site-admin/content-types/${id}`,
76
+ }, { apiName: this.apiName, ...config });
77
+ this.getList = (config) => this.restService.request({
78
+ method: 'GET',
79
+ url: '/api/site-admin/content-types',
80
+ }, { apiName: this.apiName, ...config });
81
+ this.getListByPage = (pageId, config) => this.restService.request({
82
+ method: 'GET',
83
+ url: `/api/site-admin/content-types/by-page/${pageId}`,
84
+ }, { apiName: this.apiName, ...config });
85
+ this.update = (id, input, config) => this.restService.request({
86
+ method: 'PUT',
87
+ url: `/api/site-admin/content-types/${id}`,
88
+ body: input,
89
+ }, { apiName: this.apiName, ...config });
90
+ }
91
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentTypeAdminService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
92
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentTypeAdminService, providedIn: 'root' }); }
93
+ }
94
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentTypeAdminService, decorators: [{
95
+ type: Injectable,
96
+ args: [{
97
+ providedIn: 'root',
98
+ }]
99
+ }] });
100
+
101
+ /**
102
+ * Configuration of a `Content` field, shaped for `FormBuilder.group()`. Mirrors `ContentConfiguration`
103
+ * (`src/Dignite.FlexFields.Site/Dignite/FlexFields/Site/Content/ContentConfiguration.cs`).
104
+ *
105
+ * The property names are the **stored** configuration keys, not a naming choice.
106
+ */
107
+ class ContentConfiguration {
108
+ constructor() {
109
+ /** Restricts the picker to Content of this content type. `null` = unrestricted. */
110
+ this['Content.ContentTypeId'] = [null];
111
+ this['Content.Multiple'] = [false];
112
+ this['Content.Placeholder'] = [''];
113
+ }
114
+ }
115
+
116
+ /** Designer-side editor for a `Content` field's configuration: which content type to restrict to (if
117
+ * any), single- vs. multi-select, and a placeholder. */
118
+ class ContentConfigComponent extends FieldTypeConfigBase {
119
+ constructor() {
120
+ super(...arguments);
121
+ this.contentTypeAdminService = inject(ContentTypeAdminService);
122
+ this.contentTypes = [];
123
+ }
124
+ configurationDefaults() {
125
+ return new ContentConfiguration();
126
+ }
127
+ ngOnInit() {
128
+ this.contentTypeAdminService.getList().subscribe(result => {
129
+ this.contentTypes = result.items ?? [];
130
+ });
131
+ }
132
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentConfigComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
133
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.2.2", type: ContentConfigComponent, isStandalone: true, selector: "site-content-config", usesInheritance: true, ngImport: i0, template: "<form [formGroup]=\"formEntity\">\n <div formGroupName=\"configuration\">\n <div class=\"mb-3\">\n <label class=\"form-label\">{{ 'FlexFieldsSite::Content:ContentTypeId' | abpLocalization }}</label>\n <select class=\"form-select\" formControlName=\"Content.ContentTypeId\">\n <option [ngValue]=\"null\">{{ 'FlexFieldsSite::Content:AnyContentType' | abpLocalization }}</option>\n <option *ngFor=\"let item of contentTypes\" [ngValue]=\"item.id\">{{ item.displayName }}</option>\n </select>\n </div>\n <div class=\"form-check mb-3\">\n <input\n type=\"checkbox\"\n class=\"form-check-input\"\n id=\"content-config-multiple\"\n formControlName=\"Content.Multiple\"\n />\n <label class=\"form-check-label\" for=\"content-config-multiple\">\n {{ 'FlexFieldsSite::Content:Multiple' | abpLocalization }}\n </label>\n </div>\n <div class=\"mb-3\">\n <label class=\"form-label\">{{ 'FlexFieldsSite::Content:Placeholder' | abpLocalization }}</label>\n <input type=\"text\" class=\"form-control\" formControlName=\"Content.Placeholder\" />\n </div>\n </div>\n</form>\n", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.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$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.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$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1$1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i3.ValidationGroupDirective, selector: "[formGroup],[formGroupName]", exportAs: ["validationGroup"] }, { kind: "directive", type: i3.ValidationDirective, selector: "[formControl],[formControlName]", exportAs: ["validationDirective"] }, { kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "pipe", type: i4.LocalizationPipe, name: "abpLocalization" }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
134
+ }
135
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentConfigComponent, decorators: [{
136
+ type: Component,
137
+ args: [{ selector: 'site-content-config', changeDetection: ChangeDetectionStrategy.Eager, imports: [CoreModule, CommonModule, ReactiveFormsModule], template: "<form [formGroup]=\"formEntity\">\n <div formGroupName=\"configuration\">\n <div class=\"mb-3\">\n <label class=\"form-label\">{{ 'FlexFieldsSite::Content:ContentTypeId' | abpLocalization }}</label>\n <select class=\"form-select\" formControlName=\"Content.ContentTypeId\">\n <option [ngValue]=\"null\">{{ 'FlexFieldsSite::Content:AnyContentType' | abpLocalization }}</option>\n <option *ngFor=\"let item of contentTypes\" [ngValue]=\"item.id\">{{ item.displayName }}</option>\n </select>\n </div>\n <div class=\"form-check mb-3\">\n <input\n type=\"checkbox\"\n class=\"form-check-input\"\n id=\"content-config-multiple\"\n formControlName=\"Content.Multiple\"\n />\n <label class=\"form-check-label\" for=\"content-config-multiple\">\n {{ 'FlexFieldsSite::Content:Multiple' | abpLocalization }}\n </label>\n </div>\n <div class=\"mb-3\">\n <label class=\"form-label\">{{ 'FlexFieldsSite::Content:Placeholder' | abpLocalization }}</label>\n <input type=\"text\" class=\"form-control\" formControlName=\"Content.Placeholder\" />\n </div>\n </div>\n</form>\n" }]
138
+ }] });
139
+
140
+ class ContentAdminService {
141
+ constructor() {
142
+ this.restService = inject(RestService);
143
+ this.apiName = 'SiteAdmin';
144
+ this.create = (input, config) => this.restService.request({
145
+ method: 'POST',
146
+ url: '/api/site-admin/contents',
147
+ body: input,
148
+ }, { apiName: this.apiName, ...config });
149
+ this.delete = (id, config) => this.restService.request({
150
+ method: 'DELETE',
151
+ url: `/api/site-admin/contents/${id}`,
152
+ }, { apiName: this.apiName, ...config });
153
+ this.findBySlug = (pageId, cultureName, slug, config) => this.restService.request({
154
+ method: 'GET',
155
+ url: '/api/site-admin/contents/by-slug',
156
+ params: { pageId, cultureName, slug },
157
+ }, { apiName: this.apiName, ...config });
158
+ this.get = (id, config) => this.restService.request({
159
+ method: 'GET',
160
+ url: `/api/site-admin/contents/${id}`,
161
+ }, { apiName: this.apiName, ...config });
162
+ this.getList = (input, config) => this.restService.request({
163
+ method: 'GET',
164
+ url: '/api/site-admin/contents',
165
+ params: { pageId: input.pageId, cultureName: input.cultureName, contentTypeId: input.contentTypeId, status: input.status, publishedBefore: input.publishedBefore, publishedAfter: input.publishedAfter, filter: input.filter, flexFieldConditions: input.flexFieldConditions, sorting: input.sorting, skipCount: input.skipCount, maxResultCount: input.maxResultCount },
166
+ }, { apiName: this.apiName, ...config });
167
+ this.update = (id, input, config) => this.restService.request({
168
+ method: 'PUT',
169
+ url: `/api/site-admin/contents/${id}`,
170
+ body: input,
171
+ }, { apiName: this.apiName, ...config });
172
+ }
173
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentAdminService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
174
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentAdminService, providedIn: 'root' }); }
175
+ }
176
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentAdminService, decorators: [{
177
+ type: Injectable,
178
+ args: [{
179
+ providedIn: 'root',
180
+ }]
181
+ }] });
182
+
183
+ /**
184
+ * Edits the value of a `Content` field: a picker over other Content, restricted to
185
+ * `Content.ContentTypeId` when set. The value is always a flat array of ids, even in single-select mode
186
+ * - matching the server's `ContentFieldType`, which always stores `List<Guid>`.
187
+ *
188
+ * The picker list is fetched once, in {@link ngOnInit} - `Content.ContentTypeId` is fixed for the
189
+ * lifetime of one rendered control (it belongs to the field's own definition, not something this
190
+ * component's own inputs can change), so there is nothing to react to afterward.
191
+ */
192
+ class ContentControlComponent extends FieldTypeControlBase {
193
+ constructor() {
194
+ super(...arguments);
195
+ this.contentAdminService = inject(ContentAdminService);
196
+ /**
197
+ * A signal, not a plain field: it is filled by an HTTP callback, and this control renders in the content
198
+ * editor's page flow, under the host layout's OnPush ancestors - a plain-field write there never marks
199
+ * them for check, so the options (and a stored selection) would stay blank until an unrelated event.
200
+ */
201
+ this.options = signal([], /* @ts-ignore */
202
+ ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
203
+ }
204
+ get multiple() {
205
+ return !!this.fieldValue?.field.configuration['Content.Multiple'];
206
+ }
207
+ get placeholder() {
208
+ return this.fieldValue?.field.configuration['Content.Placeholder'] ?? '';
209
+ }
210
+ configurationDefaults() {
211
+ return new ContentConfiguration();
212
+ }
213
+ createControl() {
214
+ const validators = [];
215
+ if (this.fieldValue.required) {
216
+ validators.push(Validators.required);
217
+ }
218
+ const stored = readStringList(this.selectedValue).filter(value => value !== '');
219
+ return this.fb.control(this.multiple ? stored : (stored[0] ?? ''), validators);
220
+ }
221
+ ngOnInit() {
222
+ const contentTypeId = this.fieldValue?.field.configuration['Content.ContentTypeId'];
223
+ this.contentAdminService
224
+ .getList({ contentTypeId: contentTypeId || null, skipCount: 0, maxResultCount: 100 })
225
+ .subscribe(result => {
226
+ this.options.set(result.items ?? []);
227
+ });
228
+ }
229
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentControlComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
230
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.2", type: ContentControlComponent, isStandalone: true, selector: "site-content-control", usesInheritance: true, ngImport: i0, template: "<div [formGroup]=\"entityForm\">\n <div [formGroupName]=\"parentField\">\n <div class=\"mb-3\">\n <label class=\"form-label\" *ngIf=\"fieldValue.field.displayName\">\n {{ fieldValue.field.displayName }}\n </label>\n @if (multiple) {\n <select\n class=\"form-select\"\n multiple\n [attr.data-flex-field-name]=\"fieldValue.field.name\"\n [formControlName]=\"fieldValue.field.name\"\n >\n <option *ngFor=\"let item of options()\" [value]=\"item.id\">{{ item.slug }}</option>\n </select>\n } @else {\n <select\n class=\"form-select\"\n [attr.data-flex-field-name]=\"fieldValue.field.name\"\n [formControlName]=\"fieldValue.field.name\"\n >\n <option value=\"\">{{ placeholder }}</option>\n <option *ngFor=\"let item of options()\" [value]=\"item.id\">{{ item.slug }}</option>\n </select>\n }\n <small class=\"form-text text-muted d-block\" *ngIf=\"fieldValue.field.description\">\n {{ fieldValue.field.description }}\n </small>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.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$1.SelectMultipleControlValueAccessor, selector: "select[multiple]:not([ngNoCva])[formControlName],select[multiple]:not([ngNoCva])[formControl],select[multiple]:not([ngNoCva])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1$1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
231
+ }
232
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentControlComponent, decorators: [{
233
+ type: Component,
234
+ args: [{ selector: 'site-content-control', changeDetection: ChangeDetectionStrategy.Eager, imports: [CommonModule, ReactiveFormsModule], template: "<div [formGroup]=\"entityForm\">\n <div [formGroupName]=\"parentField\">\n <div class=\"mb-3\">\n <label class=\"form-label\" *ngIf=\"fieldValue.field.displayName\">\n {{ fieldValue.field.displayName }}\n </label>\n @if (multiple) {\n <select\n class=\"form-select\"\n multiple\n [attr.data-flex-field-name]=\"fieldValue.field.name\"\n [formControlName]=\"fieldValue.field.name\"\n >\n <option *ngFor=\"let item of options()\" [value]=\"item.id\">{{ item.slug }}</option>\n </select>\n } @else {\n <select\n class=\"form-select\"\n [attr.data-flex-field-name]=\"fieldValue.field.name\"\n [formControlName]=\"fieldValue.field.name\"\n >\n <option value=\"\">{{ placeholder }}</option>\n <option *ngFor=\"let item of options()\" [value]=\"item.id\">{{ item.slug }}</option>\n </select>\n }\n <small class=\"form-text text-muted d-block\" *ngIf=\"fieldValue.field.description\">\n {{ fieldValue.field.description }}\n </small>\n </div>\n </div>\n</div>\n" }]
235
+ }] });
236
+
237
+ /**
238
+ * Displays the value of a `Content` field read-only: the referenced Content, resolved by id. An id
239
+ * that no longer resolves (deleted since picked) is silently dropped rather than shown as an error -
240
+ * the same "fail open" stance the server's own `Content.cshtml` partial takes.
241
+ */
242
+ class ContentViewComponent {
243
+ constructor() {
244
+ this.contentAdminService = inject(ContentAdminService);
245
+ this.showInList = false;
246
+ this.value = '';
247
+ /**
248
+ * A signal, not a plain field: it is filled by an HTTP callback, and this view renders in the content
249
+ * list's page flow, under the host layout's OnPush ancestors - a plain-field write there never marks
250
+ * them for check, so the cell would stay at `-` until an unrelated event.
251
+ */
252
+ this.resolved = signal([], /* @ts-ignore */
253
+ ...(ngDevMode ? [{ debugName: "resolved" }] : /* istanbul ignore next */ []));
254
+ }
255
+ get summaryText() {
256
+ const resolved = this.resolved();
257
+ return resolved.length === 0 ? '-' : resolved.map(item => item.slug).join(', ');
258
+ }
259
+ ngOnChanges() {
260
+ const ids = readStringList(this.value).filter(id => id !== '');
261
+ if (ids.length === 0) {
262
+ this.resolved.set([]);
263
+ return;
264
+ }
265
+ forkJoin(ids.map(id => this.contentAdminService.get(id).pipe(catchError(() => of(null))))).subscribe(results => {
266
+ this.resolved.set(results.filter((item) => item !== null));
267
+ });
268
+ }
269
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
270
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.2", type: ContentViewComponent, isStandalone: true, selector: "site-content-view", inputs: { showInList: "showInList", fields: "fields", type: "type", value: "value" }, usesOnChanges: true, ngImport: i0, template: "@if (showInList) {\n {{ summaryText }}\n} @else {\n <div class=\"flex-field flex-field-view flex-field-view-content\">\n <span class=\"flex-field-label\" *ngIf=\"fields?.field?.displayName\">{{ fields.field.displayName }}</span>\n @if (resolved().length === 0) {\n <span class=\"flex-field-value\">-</span>\n } @else {\n <ul class=\"flex-field-value flex-field-value-content\">\n <li *ngFor=\"let item of resolved()\">{{ item.slug }}</li>\n </ul>\n }\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
271
+ }
272
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentViewComponent, decorators: [{
273
+ type: Component,
274
+ args: [{ selector: 'site-content-view', changeDetection: ChangeDetectionStrategy.Eager, imports: [CommonModule], template: "@if (showInList) {\n {{ summaryText }}\n} @else {\n <div class=\"flex-field flex-field-view flex-field-view-content\">\n <span class=\"flex-field-label\" *ngIf=\"fields?.field?.displayName\">{{ fields.field.displayName }}</span>\n @if (resolved().length === 0) {\n <span class=\"flex-field-value\">-</span>\n } @else {\n <ul class=\"flex-field-value flex-field-value-content\">\n <li *ngFor=\"let item of resolved()\">{{ item.slug }}</li>\n </ul>\n }\n </div>\n}\n" }]
275
+ }], propDecorators: { showInList: [{
276
+ type: Input
277
+ }], fields: [{
278
+ type: Input
279
+ }], type: [{
280
+ type: Input
281
+ }], value: [{
282
+ type: Input
283
+ }] } });
284
+
285
+ /** The registration key. Must equal `ContentFieldType.ControlName` on the server. */
286
+ const CONTENT_FIELD_TYPE_NAME = 'Content';
287
+ /**
288
+ * Site's 8th field type, alongside the six the flex-fields library ships and Site's own `Seo` (the
289
+ * 7th). The server registers `ContentFieldType` through DI automatically; the client has no equivalent
290
+ * discovery, so it is registered here and wired in through `provideSite()` (GitHub issue #49).
291
+ *
292
+ * Named "Content" rather than DynamicForms' own "Entry" - Site's domain has no "Entry" concept, only
293
+ * `Content`, so the field type is named after what it actually references.
294
+ *
295
+ * **No search component.** `ContentFieldType.IndexValueType` is `Guid` on the server (so a future
296
+ * filter UI is possible), but no `searchComponent` ships this round - same state CKEditor/the file field
297
+ * are currently in.
298
+ */
299
+ const CONTENT_FIELD_TYPE = {
300
+ name: CONTENT_FIELD_TYPE_NAME,
301
+ displayNameKey: 'FlexFieldsSite::FieldType:Content',
302
+ configComponent: ContentConfigComponent,
303
+ controlComponent: ContentControlComponent,
304
+ viewComponent: ContentViewComponent,
305
+ };
306
+
307
+ /**
308
+ * Site's file containers - the server's `SiteFileContainerNames`. The file library works in these only;
309
+ * a file field names one of them. `images` takes pictures, `attachments` documents only.
310
+ */
311
+ const SITE_FILE_CONTAINER_NAMES = ['images', 'attachments'];
312
+ /** Pictures only (raster formats, no SVG) - what a share image or an image field uses. */
313
+ const SITE_IMAGES_CONTAINER_NAME = 'images';
314
+
315
+ var _a, _b;
316
+ /**
317
+ * The configuration keys of the file field type. **Persisted data**: every field (and every Matrix
318
+ * block's nested configuration) stores them under these names, which date from when the field type was
319
+ * `Dignite.Abp.FlexFields`' - the same keys as the server's `FileFieldConfigurationNames`.
320
+ */
321
+ const FILE_FIELD_CONFIGURATION_NAMES = {
322
+ fileContainerName: 'FileExplorer.FileContainerName',
323
+ uploadFileMultiple: 'FileExplorer.UploadFileMultiple',
324
+ };
325
+ /** Configuration of a file field, shaped for `FormBuilder.group()`. */
326
+ class FileFieldConfiguration {
327
+ constructor() {
328
+ // No fallback container anywhere downstream (the control prompts instead of guessing), so this has to
329
+ // be required here - the point where a misconfiguration is cheapest to catch.
330
+ this[_a] = ['', [Validators.required]];
331
+ this[_b] = [false];
332
+ }
333
+ static { _a = FILE_FIELD_CONFIGURATION_NAMES.fileContainerName, _b = FILE_FIELD_CONFIGURATION_NAMES.uploadFileMultiple; }
334
+ }
335
+
336
+ /** Designer-side editor for a file field's configuration: which of Site's containers, one file or many. */
337
+ class FileConfigComponent extends FieldTypeConfigBase {
338
+ constructor() {
339
+ super(...arguments);
340
+ this.containerNames = SITE_FILE_CONTAINER_NAMES;
341
+ this.names = FILE_FIELD_CONFIGURATION_NAMES;
342
+ }
343
+ configurationDefaults() {
344
+ return new FileFieldConfiguration();
345
+ }
346
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FileConfigComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
347
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.2", type: FileConfigComponent, isStandalone: true, selector: "site-file-config", usesInheritance: true, ngImport: i0, template: "<form [formGroup]=\"formEntity\">\n <div formGroupName=\"configuration\">\n <div class=\"mb-3\">\n <label class=\"form-label\" for=\"siteFileFieldContainerName\">\n {{ 'FlexFieldsSite::File:FileContainerName' | abpLocalization }}\n </label>\n <select class=\"form-select\" id=\"siteFileFieldContainerName\" [formControlName]=\"names.fileContainerName\">\n <option value=\"\"></option>\n @for (containerName of containerNames; track containerName) {\n <option [value]=\"containerName\">{{ containerName }}</option>\n }\n </select>\n </div>\n <div class=\"mb-3\">\n <div class=\"form-check form-check-inline\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n [formControlName]=\"names.uploadFileMultiple\"\n id=\"siteFileFieldUploadFileMultiple\"\n />\n <label class=\"form-check-label\" for=\"siteFileFieldUploadFileMultiple\">\n {{ 'FlexFieldsSite::File:UploadFileMultiple' | abpLocalization }}\n </label>\n </div>\n </div>\n <button type=\"submit\" style=\"display: none\" #submitclick></button>\n </div>\n</form>\n", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.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$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1$1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i3.ValidationGroupDirective, selector: "[formGroup],[formGroupName]", exportAs: ["validationGroup"] }, { kind: "directive", type: i3.ValidationDirective, selector: "[formControl],[formControlName]", exportAs: ["validationDirective"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "pipe", type: i4.LocalizationPipe, name: "abpLocalization" }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
348
+ }
349
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FileConfigComponent, decorators: [{
350
+ type: Component,
351
+ args: [{ selector: 'site-file-config', changeDetection: ChangeDetectionStrategy.Eager, imports: [CoreModule, ReactiveFormsModule], template: "<form [formGroup]=\"formEntity\">\n <div formGroupName=\"configuration\">\n <div class=\"mb-3\">\n <label class=\"form-label\" for=\"siteFileFieldContainerName\">\n {{ 'FlexFieldsSite::File:FileContainerName' | abpLocalization }}\n </label>\n <select class=\"form-select\" id=\"siteFileFieldContainerName\" [formControlName]=\"names.fileContainerName\">\n <option value=\"\"></option>\n @for (containerName of containerNames; track containerName) {\n <option [value]=\"containerName\">{{ containerName }}</option>\n }\n </select>\n </div>\n <div class=\"mb-3\">\n <div class=\"form-check form-check-inline\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n [formControlName]=\"names.uploadFileMultiple\"\n id=\"siteFileFieldUploadFileMultiple\"\n />\n <label class=\"form-check-label\" for=\"siteFileFieldUploadFileMultiple\">\n {{ 'FlexFieldsSite::File:UploadFileMultiple' | abpLocalization }}\n </label>\n </div>\n </div>\n <button type=\"submit\" style=\"display: none\" #submitclick></button>\n </div>\n</form>\n" }]
352
+ }] });
353
+
354
+ class ValidatorsService {
355
+ constructor(toaster, localizationService) {
356
+ this.toaster = toaster;
357
+ this.localizationService = localizationService;
358
+ }
359
+ isCheckForm(input, module = 'AbpValidation') {
360
+ const keys = Object.keys(input);
361
+ for (const element of keys) {
362
+ if (input[element] === false) {
363
+ const displayName = element.charAt(0).toUpperCase() + element.slice(1);
364
+ let info = `"${this.localizationService.instant(`${module}::${displayName}`)}" `;
365
+ if (element.includes('.') && !element.includes('].')) {
366
+ const parts = element.split('.');
367
+ info = `"${this.localizationService.instant(`${module}::${parts[0]}`)}.${this.localizationService.instant(`${module}::${parts[1]}`)}"`;
368
+ }
369
+ if (element.includes('].')) {
370
+ const parts = element.split('].');
371
+ const arrayStart = parts[0].split('[');
372
+ info = `"${this.localizationService.instant(`${module}::${arrayStart[0]}`)}[${arrayStart[1]}].${this.localizationService.instant(`${module}::${parts[1]}`)}"`;
373
+ }
374
+ this.toaster.warn(info + this.localizationService.instant('AbpValidation::ThisFieldIsNotValid.'));
375
+ return true;
376
+ }
377
+ }
378
+ return false;
379
+ }
380
+ getFormValidationStatus(formEntity) {
381
+ const validationStatus = {};
382
+ const traverseForm = (form, prefix = '') => {
383
+ if (form instanceof FormGroup) {
384
+ Object.keys(form.controls).forEach(key => {
385
+ const control = form.controls[key];
386
+ const fullKey = prefix ? `${prefix}.${key}` : key;
387
+ if (control instanceof FormControl) {
388
+ validationStatus[fullKey] = control.valid;
389
+ }
390
+ else if (control instanceof FormArray || control instanceof FormGroup) {
391
+ traverseForm(control, fullKey);
392
+ }
393
+ });
394
+ }
395
+ else {
396
+ form.controls.forEach((control, index) => {
397
+ const fullKey = prefix ? `${prefix}[${index}]` : `[${index}]`;
398
+ if (control instanceof FormControl) {
399
+ validationStatus[fullKey] = control.valid;
400
+ }
401
+ else if (control instanceof FormArray || control instanceof FormGroup) {
402
+ traverseForm(control, fullKey);
403
+ }
404
+ });
405
+ }
406
+ };
407
+ traverseForm(formEntity);
408
+ return validationStatus;
409
+ }
410
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ValidatorsService, deps: [{ token: i2.ToasterService }, { token: i4.LocalizationService }], target: i0.ɵɵFactoryTarget.Injectable }); }
411
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ValidatorsService, providedIn: 'root' }); }
412
+ }
413
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ValidatorsService, decorators: [{
414
+ type: Injectable,
415
+ args: [{
416
+ providedIn: 'root',
417
+ }]
418
+ }], ctorParameters: () => [{ type: i2.ToasterService }, { type: i4.LocalizationService }] });
419
+
420
+ class DirectoryAdminService {
421
+ constructor() {
422
+ this.restService = inject(RestService);
423
+ this.apiName = 'SiteAdmin';
424
+ this.create = (input, config) => this.restService.request({
425
+ method: 'POST',
426
+ url: '/api/site-admin/files/directories',
427
+ body: input,
428
+ }, { apiName: this.apiName, ...config });
429
+ this.delete = (id, config) => this.restService.request({
430
+ method: 'DELETE',
431
+ url: `/api/site-admin/files/directories/${id}`,
432
+ }, { apiName: this.apiName, ...config });
433
+ this.get = (id, config) => this.restService.request({
434
+ method: 'GET',
435
+ url: `/api/site-admin/files/directories/${id}`,
436
+ }, { apiName: this.apiName, ...config });
437
+ this.getList = (input, config) => this.restService.request({
438
+ method: 'GET',
439
+ url: '/api/site-admin/files/directories',
440
+ params: { containerName: input.containerName },
441
+ }, { apiName: this.apiName, ...config });
442
+ this.move = (id, input, config) => this.restService.request({
443
+ method: 'PUT',
444
+ url: `/api/site-admin/files/directories/${id}/move`,
445
+ body: input,
446
+ }, { apiName: this.apiName, ...config });
447
+ this.update = (id, input, config) => this.restService.request({
448
+ method: 'PUT',
449
+ url: `/api/site-admin/files/directories/${id}`,
450
+ body: input,
451
+ }, { apiName: this.apiName, ...config });
452
+ }
453
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: DirectoryAdminService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
454
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: DirectoryAdminService, providedIn: 'root' }); }
455
+ }
456
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: DirectoryAdminService, decorators: [{
457
+ type: Injectable,
458
+ args: [{
459
+ providedIn: 'root',
460
+ }]
461
+ }] });
462
+
463
+ class FileAdminService {
464
+ constructor() {
465
+ this.restService = inject(RestService);
466
+ this.apiName = 'SiteAdmin';
467
+ this.create = (input, config) => this.restService.request({
468
+ method: 'POST',
469
+ url: '/api/site-admin/files',
470
+ params: { containerName: input.containerName, directoryId: input.directoryId },
471
+ body: input.file,
472
+ }, { apiName: this.apiName, ...config });
473
+ this.delete = (id, config) => this.restService.request({
474
+ method: 'DELETE',
475
+ url: `/api/site-admin/files/${id}`,
476
+ }, { apiName: this.apiName, ...config });
477
+ this.get = (id, config) => this.restService.request({
478
+ method: 'GET',
479
+ url: `/api/site-admin/files/${id}`,
480
+ }, { apiName: this.apiName, ...config });
481
+ this.getContainerConfiguration = (containerName, config) => this.restService.request({
482
+ method: 'GET',
483
+ url: `/api/site-admin/files/containers/${containerName}`,
484
+ }, { apiName: this.apiName, ...config });
485
+ this.getList = (input, config) => this.restService.request({
486
+ method: 'GET',
487
+ url: '/api/site-admin/files',
488
+ params: { containerName: input.containerName, directoryId: input.directoryId, creatorId: input.creatorId, filter: input.filter, sorting: input.sorting, skipCount: input.skipCount, maxResultCount: input.maxResultCount },
489
+ }, { apiName: this.apiName, ...config });
490
+ this.update = (id, input, config) => this.restService.request({
491
+ method: 'PUT',
492
+ url: `/api/site-admin/files/${id}`,
493
+ body: input,
494
+ }, { apiName: this.apiName, ...config });
495
+ }
496
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FileAdminService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
497
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FileAdminService, providedIn: 'root' }); }
498
+ }
499
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FileAdminService, decorators: [{
500
+ type: Injectable,
501
+ args: [{
502
+ providedIn: 'root',
503
+ }]
504
+ }] });
505
+
506
+ var index$h = /*#__PURE__*/Object.freeze({
507
+ __proto__: null,
508
+ DirectoryAdminService: DirectoryAdminService,
509
+ FileAdminService: FileAdminService
510
+ });
511
+
512
+ /* eslint-disable @angular-eslint/component-selector */
513
+ const MY_FILES_NODE_KEY = '__site-my-files__';
514
+ const isMyFilesNode = (node) => node?.key === MY_FILES_NODE_KEY;
515
+ class FileDirectoryTreeComponent {
516
+ constructor(directoryAdminService, fb, confirmation, localizationService) {
517
+ this.directoryAdminService = directoryAdminService;
518
+ this.fb = fb;
519
+ this.confirmation = confirmation;
520
+ this.localizationService = localizationService;
521
+ this.hostElement = inject((ElementRef));
522
+ this.directoryTree = [];
523
+ this.expandedNodeKeys = [MY_FILES_NODE_KEY];
524
+ this._fileContainerName = '';
525
+ this.nodeClick = new EventEmitter();
526
+ this.treeNodeData = new EventEmitter();
527
+ this.isMyFilesNode = isMyFilesNode;
528
+ /** Dragged-directory highlight state, tracked globally so it clears even if the drop lands outside the tree. */
529
+ this.isDirectoryDragging = false;
530
+ this.beforeDrop = (arg) => {
531
+ const { pos, dragNode, node } = arg;
532
+ if (![0, 1, -1].includes(pos) || this.isMyFilesNode(dragNode)) {
533
+ return of(false);
534
+ }
535
+ // "My Files" is the virtual root; a directory can't be dropped before or after it.
536
+ if (this.isMyFilesNode(node) && pos !== 0) {
537
+ return of(false);
538
+ }
539
+ const targetParent = node.parentNode;
540
+ // The indent guide to the right of an expanded directory is technically "before its first
541
+ // child", but the intended drop target there is still "the sibling slot after that directory" -
542
+ // not a second way to create a child.
543
+ const isIndentedFirstChildGap = pos === -1 &&
544
+ !!targetParent &&
545
+ !this.isMyFilesNode(targetParent) &&
546
+ targetParent.children?.[0]?.key === node.key;
547
+ if (isIndentedFirstChildGap) {
548
+ const grandParentKey = targetParent.parentNode?.key;
549
+ const parentId = !grandParentKey || grandParentKey === MY_FILES_NODE_KEY ? null : grandParentKey;
550
+ const order = targetParent.origin.order + 1;
551
+ return this.directoryAdminService.move(dragNode.key, { parentId, order }).pipe(tap(() => this.loadDirectoryTree()),
552
+ // Refresh from the server's own reordering instead of letting ng-zorro also apply its
553
+ // "before the first child" move locally.
554
+ map(() => false));
555
+ }
556
+ const parentKey = node.parentNode?.key;
557
+ const parentId = pos === 0
558
+ ? this.isMyFilesNode(node)
559
+ ? null
560
+ : node.key
561
+ : !parentKey || parentKey === MY_FILES_NODE_KEY
562
+ ? null
563
+ : parentKey;
564
+ const order = pos === 0
565
+ ? (node.origin.children || []).length + 1
566
+ : pos === 1
567
+ ? node.origin.order + 1
568
+ : node.origin.order;
569
+ return this.directoryAdminService.move(dragNode.key, { parentId, order }).pipe(tap(() => this.loadDirectoryTree()), map(() => true));
570
+ };
571
+ this.isNodeSelected = (node) => {
572
+ return !!this.selectedTreeNode && node.key === this.selectedTreeNode.key;
573
+ };
574
+ this.directoryFormModalOpen = false;
575
+ this.directoryFormBusy = false;
576
+ this.validatorsService = inject(ValidatorsService);
577
+ }
578
+ set theSelectedTreeNode(v) {
579
+ this.selectedTreeNode = v;
580
+ }
581
+ set fileContainerName(v) {
582
+ if (v) {
583
+ this._fileContainerName = v;
584
+ }
585
+ }
586
+ get fileContainerName() {
587
+ return this._fileContainerName;
588
+ }
589
+ ngAfterContentInit() {
590
+ if (this.fileContainerName) {
591
+ this.loadData();
592
+ }
593
+ }
594
+ loadData() {
595
+ if (this.fileContainerName) {
596
+ this.loadDirectoryTree();
597
+ }
598
+ }
599
+ loadDirectoryTree() {
600
+ this.directoryAdminService
601
+ .getList({ containerName: this.fileContainerName })
602
+ .subscribe(res => {
603
+ const directories = this.buildNodeTree(res.items);
604
+ this.directoryTree = [this.createMyFilesNode(directories)];
605
+ this.treeNodeData.emit(this.directoryTree);
606
+ });
607
+ }
608
+ createMyFilesNode(children) {
609
+ const title = this.localizationService.instant('Site::Files:MyFiles');
610
+ return {
611
+ id: MY_FILES_NODE_KEY,
612
+ key: MY_FILES_NODE_KEY,
613
+ name: title,
614
+ title,
615
+ parentId: null,
616
+ children,
617
+ expanded: this.expandedNodeKeys.includes(MY_FILES_NODE_KEY),
618
+ isLeaf: children.length === 0,
619
+ isVirtualRoot: true,
620
+ };
621
+ }
622
+ /** Reshapes the flat, parent-referencing directory list into the nested tree `abp-tree` renders. */
623
+ buildNodeTree(items, parentId = null) {
624
+ const result = items
625
+ .filter(item => item.parentId === parentId)
626
+ .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
627
+ result.forEach((node, index) => {
628
+ node.title = node.name ?? '';
629
+ node.key = node.id ?? '';
630
+ node.expanded = this.expandedNodeKeys.includes(node.key);
631
+ node.isFirstChildOfDirectory = parentId !== null && index === 0;
632
+ node.children = (node.children ?? []);
633
+ node.children.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
634
+ if (node.children.length > 0) {
635
+ this.buildNodeTree(node.children, node.id);
636
+ }
637
+ });
638
+ return result;
639
+ }
640
+ onDirectoryDragStart(event) {
641
+ if (event.target instanceof Node && this.hostElement.nativeElement.contains(event.target)) {
642
+ this.isDirectoryDragging = true;
643
+ }
644
+ }
645
+ onDirectoryDragEnd() {
646
+ this.isDirectoryDragging = false;
647
+ }
648
+ /**
649
+ * The node icon itself stops propagation via `(click)="$event.stopPropagation()"`, so a click on
650
+ * it never reaches here - no need to distinguish where the click came from.
651
+ */
652
+ activeNode(node) {
653
+ if (this.selectedTreeNode && this.selectedTreeNode.key === node.key)
654
+ return;
655
+ this.selectedTreeNode = node;
656
+ this.nodeClick.emit(node);
657
+ }
658
+ onExpandChange(event) {
659
+ const key = event.node.key;
660
+ this.expandedNodeKeys = this.expandedNodeKeys.includes(key)
661
+ ? this.expandedNodeKeys.filter(k => k !== key)
662
+ : [...this.expandedNodeKeys, key];
663
+ }
664
+ openDirectoryForm(parentNode, editing = false) {
665
+ this.directoryFormModalOpen = true;
666
+ this.directoryForm = this.fb.group({
667
+ containerName: [this.fileContainerName || '', Validators.required],
668
+ name: ['', Validators.required],
669
+ parentId: [
670
+ parentNode && !this.isMyFilesNode(parentNode) ? parentNode.key : null,
671
+ ],
672
+ });
673
+ if (editing && parentNode) {
674
+ this.nodeBeingEdited = parentNode;
675
+ this.directoryForm.patchValue({ name: parentNode.name });
676
+ }
677
+ }
678
+ deleteDirectory(node) {
679
+ this.confirmation
680
+ .warn('Site::Files:DeletionConfirmationMessage', 'Site::Files:DeletionConfirmationTitle', {
681
+ messageLocalizationParams: [node.title],
682
+ })
683
+ .subscribe((status) => {
684
+ if (status !== 'confirm')
685
+ return;
686
+ this.directoryAdminService.delete(node.key).subscribe(() => {
687
+ this.directoryFormModalOpen = false;
688
+ if (this.nodeBeingEdited && this.nodeBeingEdited.key === node.key) {
689
+ this.nodeBeingEdited = undefined;
690
+ }
691
+ this.loadDirectoryTree();
692
+ });
693
+ });
694
+ }
695
+ onDirectoryFormVisibleChange(visible) {
696
+ if (!visible) {
697
+ this.directoryForm = undefined;
698
+ this.nodeBeingEdited = undefined;
699
+ }
700
+ }
701
+ saveDirectoryForm() {
702
+ if (!this.directoryForm)
703
+ return;
704
+ const input = this.directoryForm.value;
705
+ const validationStatus = this.validatorsService.getFormValidationStatus(this.directoryForm);
706
+ if (this.validatorsService.isCheckForm(validationStatus, 'Site'))
707
+ return;
708
+ if (!this.directoryForm.valid || this.directoryFormBusy)
709
+ return;
710
+ this.directoryFormBusy = true;
711
+ const request = this.nodeBeingEdited
712
+ ? this.directoryAdminService.update(this.nodeBeingEdited.key, input)
713
+ : this.directoryAdminService.create(input);
714
+ request.pipe(finalize(() => (this.directoryFormBusy = false))).subscribe(() => {
715
+ this.directoryFormModalOpen = false;
716
+ this.onDirectoryFormVisibleChange(false);
717
+ this.loadDirectoryTree();
718
+ });
719
+ }
720
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FileDirectoryTreeComponent, deps: [{ token: DirectoryAdminService }, { token: i1$1.FormBuilder }, { token: i2.ConfirmationService }, { token: i4.LocalizationService }], target: i0.ɵɵFactoryTarget.Component }); }
721
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.2", type: FileDirectoryTreeComponent, isStandalone: true, selector: "site-file-directory-tree", inputs: { theSelectedTreeNode: "theSelectedTreeNode", fileContainerName: "fileContainerName" }, outputs: { nodeClick: "nodeClick", treeNodeData: "treeNodeData" }, host: { listeners: { "document:dragstart": "onDirectoryDragStart($event)", "document:dragend": "onDirectoryDragEnd()" } }, viewQueries: [{ propertyName: "directoryFormSubmit", first: true, predicate: ["directoryFormSubmit"], descendants: true }], ngImport: i0, template: "<div class=\"file-tree-panel\">\n <abp-tree class=\"mb-2\" [class.directory-dragging]=\"isDirectoryDragging\" [nodes]=\"directoryTree\" (nzExpandChange)=\"onExpandChange($event)\" [draggable]=\"true\"\n [beforeDrop]=\"beforeDrop\" (selectedNodeChange)=\"activeNode($event)\"\n [changeCheckboxWithNode]=\"true\" [isNodeSelected]=\"isNodeSelected\"\n (contextmenu)=\"$event.preventDefault()\">\n\n <ng-template #menu let-node>\n <button type=\"button\" class=\"dropdown-item\" (click.stop)=\"openDirectoryForm(node)\"><i\n class=\"me-1 fa fa-plus\"></i>{{'Site::Files:NewDirectory' | abpLocalization}}</button>\n @if (!isMyFilesNode(node)) {\n <button type=\"button\" class=\"dropdown-item\" (click.stop)=\"openDirectoryForm(node, true)\"><i\n class=\"me-1 fa fa-pencil\"></i>{{'AbpUi::Edit' | abpLocalization}}</button>\n <button type=\"button\" class=\"dropdown-item\" (click.stop)=\"deleteDirectory(node)\"><i\n class=\" me-1 fa fa-remove\"></i>{{'AbpUi::Delete' | abpLocalization}}</button>\n }\n </ng-template>\n\n <ng-template abpTreeNodeTemplate let-node>\n <div class=\"file-tree-node-drop-target\" [class.my-files-node]=\"isMyFilesNode(node)\"\n [class.first-child-of-directory]=\"node.origin?.isFirstChildOfDirectory\">\n <i class=\"fa me-1\" [class.fa-home]=\"isMyFilesNode(node)\" [class.fa-folder]=\"!isMyFilesNode(node)\"\n aria-hidden=\"true\" (click)=\"$event.stopPropagation()\"></i>\n <span>{{node.title}}</span>\n </div>\n </ng-template>\n\n <ng-template abpTreeExpandedIconTemplate let-node let-origin=\"origin\">\n @if (node.children.length>0 && !isMyFilesNode(node)) {\n <i aria-hidden=\"true\">\n <ng-container *ngTemplateOutlet=\"node.isExpanded ? minusIcon : plusIcon\"></ng-container></i>\n }\n </ng-template>\n\n <ng-template #minusIcon>\n <i class=\"fa fa-minus\" aria-hidden=\"true\"></i>\n </ng-template>\n\n <ng-template #plusIcon>\n <i class=\"fa fa-plus\" aria-hidden=\"true\"></i>\n </ng-template>\n </abp-tree>\n</div>\n <abp-modal [(visible)]=\"directoryFormModalOpen\" [busy]=\"directoryFormBusy\"\n (visibleChange)=\"onDirectoryFormVisibleChange($event)\">\n <ng-template #abpHeader>\n <h3>{{nodeBeingEdited?('AbpUi::Edit' | abpLocalization):('Site::Files:NewDirectory' | abpLocalization)}}\n </h3>\n </ng-template>\n <ng-template #abpBody>\n @if (directoryForm) {\n <form class=\"sites-modal-form\" #myForm=\"ngForm\" [formGroup]=\"directoryForm\"\n (keydown.enter)=\"$event.preventDefault()\" (submit)=\"saveDirectoryForm()\">\n <button type=\"submit\" #directoryFormSubmit style=\"display: none\"></button>\n <div class=\"mb-3\">\n <label class=\"form-label\">{{'Site::Files:DirectoryName' | abpLocalization}}</label>\n <input type=\"text\" class=\"form-control\" formControlName=\"name\">\n </div>\n </form>\n } @else {\n <div class=\"text-center\"><i class=\"fa fa-pulse fa-spinner\" aria-hidden=\"true\"></i></div>\n }\n </ng-template>\n <ng-template #abpFooter>\n <button type=\"button\" class=\"btn btn-secondary\" abpClose>{{'AbpUi::Close' | abpLocalization}}</button>\n <abp-button iconClass=\"fa fa-check\" [loading]=\"directoryFormBusy\"\n (click)=\"directoryFormSubmit.nativeElement.click()\">{{'AbpUi::Save' | abpLocalization}}</abp-button>\n </ng-template>\n </abp-modal>\n", styles: [".file-tree-panel abp-tree{display:block}::ng-deep .file-tree-panel abp-tree .ant-tree-node-content-wrapper{padding:0}::ng-deep .file-tree-panel abp-tree .ant-tree-node-content-wrapper .ng-star-inserted[title]{padding:0 10px}::ng-deep .file-tree-panel abp-tree .ant-tree-node-content-wrapper.ant-tree-node-selected{background-color:transparent}::ng-deep .file-tree-panel abp-tree .selected{background-color:var(--lpx-brand)!important;color:var(--bs-white)}.file-tree-node-drop-target{width:100%;border:1px solid transparent;border-radius:.25rem}::ng-deep .file-tree-panel abp-tree.directory-dragging .drag-over nz-tree-drop-indicator,::ng-deep .file-tree-panel abp-tree.directory-dragging .drag-over .ant-tree-drop-indicator{display:none!important}::ng-deep .file-tree-panel abp-tree.directory-dragging .drag-over .file-tree-node-drop-target{border-color:var(--bs-primary);background-color:var(--bs-primary-bg-subtle)}::ng-deep .file-tree-panel abp-tree.directory-dragging .drag-over-gap-top:has(.file-tree-node-drop-target.first-child-of-directory) nz-tree-drop-indicator,::ng-deep .file-tree-panel abp-tree.directory-dragging .drag-over-gap-top:has(.file-tree-node-drop-target.first-child-of-directory) .ant-tree-drop-indicator{left:-20px!important}::ng-deep .file-tree-panel abp-tree:not(.directory-dragging) nz-tree-drop-indicator,::ng-deep .file-tree-panel abp-tree:not(.directory-dragging) .ant-tree-drop-indicator{display:none!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.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$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i3.ValidationGroupDirective, selector: "[formGroup],[formGroupName]", exportAs: ["validationGroup"] }, { kind: "directive", type: i3.ValidationDirective, selector: "[formControl],[formControlName]", exportAs: ["validationDirective"] }, { kind: "directive", type: i4.StopPropagationDirective, selector: "[click.stop]", outputs: ["click.stop"] }, { kind: "ngmodule", type: ThemeSharedModule }, { kind: "component", type: i2.ButtonComponent, selector: "abp-button", inputs: ["buttonId", "buttonClass", "buttonType", "formName", "iconClass", "loading", "disabled", "attributes"], outputs: ["click", "focus", "blur", "abpClick", "abpFocus", "abpBlur"] }, { kind: "component", type: i2.ModalComponent, selector: "abp-modal", inputs: ["visible", "busy", "options", "suppressUnsavedChangesWarning"], outputs: ["visibleChange", "init", "appear", "disappear"] }, { kind: "directive", type: i2.ModalCloseDirective, selector: "[abpClose]" }, { 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 }); }
722
+ }
723
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FileDirectoryTreeComponent, decorators: [{
724
+ type: Component,
725
+ args: [{ selector: 'site-file-directory-tree', changeDetection: ChangeDetectionStrategy.Eager, imports: [CommonModule, FormsModule, ReactiveFormsModule, CoreModule, ThemeSharedModule, TreeModule], template: "<div class=\"file-tree-panel\">\n <abp-tree class=\"mb-2\" [class.directory-dragging]=\"isDirectoryDragging\" [nodes]=\"directoryTree\" (nzExpandChange)=\"onExpandChange($event)\" [draggable]=\"true\"\n [beforeDrop]=\"beforeDrop\" (selectedNodeChange)=\"activeNode($event)\"\n [changeCheckboxWithNode]=\"true\" [isNodeSelected]=\"isNodeSelected\"\n (contextmenu)=\"$event.preventDefault()\">\n\n <ng-template #menu let-node>\n <button type=\"button\" class=\"dropdown-item\" (click.stop)=\"openDirectoryForm(node)\"><i\n class=\"me-1 fa fa-plus\"></i>{{'Site::Files:NewDirectory' | abpLocalization}}</button>\n @if (!isMyFilesNode(node)) {\n <button type=\"button\" class=\"dropdown-item\" (click.stop)=\"openDirectoryForm(node, true)\"><i\n class=\"me-1 fa fa-pencil\"></i>{{'AbpUi::Edit' | abpLocalization}}</button>\n <button type=\"button\" class=\"dropdown-item\" (click.stop)=\"deleteDirectory(node)\"><i\n class=\" me-1 fa fa-remove\"></i>{{'AbpUi::Delete' | abpLocalization}}</button>\n }\n </ng-template>\n\n <ng-template abpTreeNodeTemplate let-node>\n <div class=\"file-tree-node-drop-target\" [class.my-files-node]=\"isMyFilesNode(node)\"\n [class.first-child-of-directory]=\"node.origin?.isFirstChildOfDirectory\">\n <i class=\"fa me-1\" [class.fa-home]=\"isMyFilesNode(node)\" [class.fa-folder]=\"!isMyFilesNode(node)\"\n aria-hidden=\"true\" (click)=\"$event.stopPropagation()\"></i>\n <span>{{node.title}}</span>\n </div>\n </ng-template>\n\n <ng-template abpTreeExpandedIconTemplate let-node let-origin=\"origin\">\n @if (node.children.length>0 && !isMyFilesNode(node)) {\n <i aria-hidden=\"true\">\n <ng-container *ngTemplateOutlet=\"node.isExpanded ? minusIcon : plusIcon\"></ng-container></i>\n }\n </ng-template>\n\n <ng-template #minusIcon>\n <i class=\"fa fa-minus\" aria-hidden=\"true\"></i>\n </ng-template>\n\n <ng-template #plusIcon>\n <i class=\"fa fa-plus\" aria-hidden=\"true\"></i>\n </ng-template>\n </abp-tree>\n</div>\n <abp-modal [(visible)]=\"directoryFormModalOpen\" [busy]=\"directoryFormBusy\"\n (visibleChange)=\"onDirectoryFormVisibleChange($event)\">\n <ng-template #abpHeader>\n <h3>{{nodeBeingEdited?('AbpUi::Edit' | abpLocalization):('Site::Files:NewDirectory' | abpLocalization)}}\n </h3>\n </ng-template>\n <ng-template #abpBody>\n @if (directoryForm) {\n <form class=\"sites-modal-form\" #myForm=\"ngForm\" [formGroup]=\"directoryForm\"\n (keydown.enter)=\"$event.preventDefault()\" (submit)=\"saveDirectoryForm()\">\n <button type=\"submit\" #directoryFormSubmit style=\"display: none\"></button>\n <div class=\"mb-3\">\n <label class=\"form-label\">{{'Site::Files:DirectoryName' | abpLocalization}}</label>\n <input type=\"text\" class=\"form-control\" formControlName=\"name\">\n </div>\n </form>\n } @else {\n <div class=\"text-center\"><i class=\"fa fa-pulse fa-spinner\" aria-hidden=\"true\"></i></div>\n }\n </ng-template>\n <ng-template #abpFooter>\n <button type=\"button\" class=\"btn btn-secondary\" abpClose>{{'AbpUi::Close' | abpLocalization}}</button>\n <abp-button iconClass=\"fa fa-check\" [loading]=\"directoryFormBusy\"\n (click)=\"directoryFormSubmit.nativeElement.click()\">{{'AbpUi::Save' | abpLocalization}}</abp-button>\n </ng-template>\n </abp-modal>\n", styles: [".file-tree-panel abp-tree{display:block}::ng-deep .file-tree-panel abp-tree .ant-tree-node-content-wrapper{padding:0}::ng-deep .file-tree-panel abp-tree .ant-tree-node-content-wrapper .ng-star-inserted[title]{padding:0 10px}::ng-deep .file-tree-panel abp-tree .ant-tree-node-content-wrapper.ant-tree-node-selected{background-color:transparent}::ng-deep .file-tree-panel abp-tree .selected{background-color:var(--lpx-brand)!important;color:var(--bs-white)}.file-tree-node-drop-target{width:100%;border:1px solid transparent;border-radius:.25rem}::ng-deep .file-tree-panel abp-tree.directory-dragging .drag-over nz-tree-drop-indicator,::ng-deep .file-tree-panel abp-tree.directory-dragging .drag-over .ant-tree-drop-indicator{display:none!important}::ng-deep .file-tree-panel abp-tree.directory-dragging .drag-over .file-tree-node-drop-target{border-color:var(--bs-primary);background-color:var(--bs-primary-bg-subtle)}::ng-deep .file-tree-panel abp-tree.directory-dragging .drag-over-gap-top:has(.file-tree-node-drop-target.first-child-of-directory) nz-tree-drop-indicator,::ng-deep .file-tree-panel abp-tree.directory-dragging .drag-over-gap-top:has(.file-tree-node-drop-target.first-child-of-directory) .ant-tree-drop-indicator{left:-20px!important}::ng-deep .file-tree-panel abp-tree:not(.directory-dragging) nz-tree-drop-indicator,::ng-deep .file-tree-panel abp-tree:not(.directory-dragging) .ant-tree-drop-indicator{display:none!important}\n"] }]
726
+ }], ctorParameters: () => [{ type: DirectoryAdminService }, { type: i1$1.FormBuilder }, { type: i2.ConfirmationService }, { type: i4.LocalizationService }], propDecorators: { theSelectedTreeNode: [{
727
+ type: Input
728
+ }], fileContainerName: [{
729
+ type: Input
730
+ }], nodeClick: [{
731
+ type: Output
732
+ }], treeNodeData: [{
733
+ type: Output
734
+ }], onDirectoryDragStart: [{
735
+ type: HostListener,
736
+ args: ['document:dragstart', ['$event']]
737
+ }], onDirectoryDragEnd: [{
738
+ type: HostListener,
739
+ args: ['document:dragend']
740
+ }], directoryFormSubmit: [{
741
+ type: ViewChild,
742
+ args: ['directoryFormSubmit', { static: false }]
743
+ }] } });
744
+
745
+ /** Fallback icon by MIME type (or file extension, for the few types MIME can't distinguish). */
746
+ const IMAGE_TYPE_OPTIONS = [
747
+ { type: 'image/jpeg', icon: '' },
748
+ { type: 'image/webp', icon: '' },
749
+ { type: 'image/gif', icon: '' },
750
+ { type: 'image/png', icon: '' },
751
+ { type: 'image/bmp', icon: '' },
752
+ { type: 'audio/ogg', icon: '' },
753
+ { type: 'video/mp4', icon: '' },
754
+ { type: 'application/pdf', icon: 'fa fa-file-pdf-o' },
755
+ { type: 'application/msword', icon: 'fa fa-file-word-o' },
756
+ {
757
+ type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
758
+ icon: 'fa fa-file-word-o',
759
+ },
760
+ { type: 'application/vnd.ms-powerpoint', icon: 'fa fa-file-powerpoint-o' },
761
+ {
762
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
763
+ icon: 'fa fa-file-powerpoint-o',
764
+ },
765
+ { type: 'application/vnd.ms-excel', icon: 'fa fa-file-excel-o' },
766
+ {
767
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
768
+ icon: 'fa fa-file-excel-o',
769
+ },
770
+ { type: 'application/x-zip-compressed', icon: 'fa fa-file-archive-o' },
771
+ { type: '7z', icon: 'fa fa-file-archive-o' },
772
+ { type: 'text/plain', icon: 'fa fa-file-text-o' },
773
+ { type: '', icon: 'fa fa-file-o' },
774
+ ];
775
+
776
+ /**
777
+ * The path every Site file is served under - the server's `SiteFileUrl.RoutePrefix`. The admin API hands
778
+ * out addresses relative to it (`FileDescriptorDto.url`, `/api/site-public/files/...?__tenant=`), and that
779
+ * relative address is what a field stores: no host is written into content. The public site resolves it
780
+ * against its own origin; this admin UI, served from another origin, shows it from its `SiteAdmin` API
781
+ * host (`SiteFileUrlService`).
782
+ */
783
+ const SITE_FILE_PATH = '/api/site-public/files/';
784
+ /** Whether `url` addresses a file Site serves (and so can be resized), as opposed to an external image. */
785
+ function isSiteFileUrl(url) {
786
+ return !!url && url.toLowerCase().includes(SITE_FILE_PATH);
787
+ }
788
+ /**
789
+ * `url` asked for at `width` x `height` (`?Width=&Height=`, the server crops to fill when both are given),
790
+ * keeping every other query parameter - `__tenant` in particular. Anything that is not a Site file address
791
+ * (a local `blob:` URL, an external image) comes back unchanged, as does a call without a size. Mirrors the
792
+ * server's `SiteFileUrl.Sized`.
793
+ */
794
+ function sizedFileUrl(url, width, height) {
795
+ const value = url ?? '';
796
+ if (!isSiteFileUrl(value) || (!width && !height)) {
797
+ return value;
798
+ }
799
+ const queryStart = value.indexOf('?');
800
+ const base = queryStart < 0 ? value : value.substring(0, queryStart);
801
+ const kept = queryStart < 0
802
+ ? []
803
+ : value
804
+ .substring(queryStart + 1)
805
+ .split('&')
806
+ .filter(parameter => {
807
+ const name = parameter.split('=')[0].toLowerCase();
808
+ return parameter && name !== 'width' && name !== 'height';
809
+ });
810
+ const size = [width ? `Width=${width}` : '', height ? `Height=${height}` : ''].filter(Boolean);
811
+ return `${base}?${[...kept, ...size].join('&')}`;
812
+ }
813
+ /**
814
+ * `url` on `apiBase` when it is a relative Site file address; anything else - an absolute address, a local
815
+ * `blob:`/`data:` URL, an external image - comes back unchanged, as does every address when `apiBase` is
816
+ * empty (an admin UI served from its API's own origin). Mirrors the server's `SiteFileUrl.Absolutize`.
817
+ */
818
+ function absoluteFileUrl(url, apiBase) {
819
+ const value = url ?? '';
820
+ const base = trimBase(apiBase);
821
+ return base && value.toLowerCase().startsWith(SITE_FILE_PATH) ? base + value : value;
822
+ }
823
+ function trimBase(apiBase) {
824
+ return (apiBase ?? '').trim().replace(/\/+$/, '');
825
+ }
826
+
827
+ /**
828
+ * Where this admin UI shows Site's files from. A stored address is relative (`/api/site-public/files/...`,
829
+ * see `SITE_FILE_PATH`), and this UI is served from an origin of its own, so the previews
830
+ * (`site-file-preview`: the file picker and modal, a file field, the SEO share image) put the `SiteAdmin`
831
+ * API's host - `environment.apis.SiteAdmin.url`, where the file library's admin API lives - in front of it.
832
+ * The CKEditor field does the same inside the editor (`siteCKEditorConfigContributor`). Nothing that is
833
+ * saved carries that host.
834
+ */
835
+ class SiteFileUrlService {
836
+ constructor() {
837
+ this.environment = inject(EnvironmentService);
838
+ }
839
+ /** The `SiteAdmin` API's base URL, with no trailing slash; empty when the UI shares its API's origin. */
840
+ get apiBase() {
841
+ // getApiUrl itself, guarded: it dereferences `apis` unchecked, and an environment without them (a test
842
+ // bed, say) means there is no other origin to show files from.
843
+ const apis = this.environment.getEnvironment()?.apis;
844
+ return apis ? (this.environment.getApiUrl('SiteAdmin') ?? '').replace(/\/+$/, '') : '';
845
+ }
846
+ /** A stored file address as this UI loads it - see `absoluteFileUrl`. */
847
+ toDisplay(url) {
848
+ return absoluteFileUrl(url, this.apiBase);
849
+ }
850
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SiteFileUrlService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
851
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SiteFileUrlService, providedIn: 'root' }); }
852
+ }
853
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SiteFileUrlService, decorators: [{
854
+ type: Injectable,
855
+ args: [{ providedIn: 'root' }]
856
+ }] });
857
+
858
+ /**
859
+ * A thumbnail (image) or type icon (anything else) for one file. Site's file containers are publicly
860
+ * readable, so an image is shown straight from its public address (`/api/site-public/files/...`), asked
861
+ * for at the thumbnail's size so the browser does not download the full upload. The address is stored
862
+ * relative; it is shown - and linked - from the `SiteAdmin` API's host (`SiteFileUrlService`).
863
+ */
864
+ class FilePreviewComponent {
865
+ constructor() {
866
+ this.fileUrls = inject(SiteFileUrlService);
867
+ this.width = '100px';
868
+ /** The file's address: the public read endpoint's, or a local `blob:`/`data:` URL for a file not uploaded yet. */
869
+ this.src = '';
870
+ this.type = '';
871
+ this.name = '';
872
+ this.className = '';
873
+ this.isImage = true;
874
+ this.isAudio = false;
875
+ this.isVideo = false;
876
+ this.displaySrc = '';
877
+ /** {@link src} where this UI can reach it - the download link. */
878
+ this.linkHref = '';
879
+ }
880
+ ngOnChanges() {
881
+ this.updateFileType();
882
+ this.linkHref = this.fileUrls.toDisplay(this.src);
883
+ this.displaySrc = this.isImage ? sizedFileUrl(this.linkHref, this.resizeWidth, this.resizeHeight) : this.linkHref;
884
+ }
885
+ updateFileType() {
886
+ const fileName = this.name || '';
887
+ const fileType = this.type || '';
888
+ if (!this.type) {
889
+ this.type = fileName.includes('.7z') ? '7z' : '';
890
+ }
891
+ this.isImage = fileType.includes('image/');
892
+ this.isAudio = fileType.includes('audio/');
893
+ this.isVideo = fileType.includes('video/');
894
+ }
895
+ /** Falls back to a generic file icon when the type has no dedicated one. */
896
+ getFileIconClass() {
897
+ return IMAGE_TYPE_OPTIONS.find(item => item.type === this.type)?.icon || 'fa fa-file-o';
898
+ }
899
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FilePreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
900
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.2", type: FilePreviewComponent, isStandalone: true, selector: "site-file-preview", inputs: { width: "width", src: "src", type: "type", name: "name", className: "className", resizeWidth: "resizeWidth", resizeHeight: "resizeHeight" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"image_box\" [style]=\"{ width: width, maxWidth: '100%' }\">\n <a class=\"file-preview-link\" [href]=\"linkHref\" [download]=\"name\" [attr.aria-label]=\"name\">\n @if (isImage) {\n <div class=\"img_box\" [class]=\"className\">\n <img [src]=\"displaySrc\" [alt]=\"name\" />\n </div>\n }\n @if (isAudio) {\n <i class=\"file-type-icon fa fa-file-audio-o fs-4\" aria-hidden=\"true\"></i>\n }\n @if (isVideo) {\n <i class=\"file-type-icon fa fa-file-video-o fs-4\" aria-hidden=\"true\"></i>\n }\n @if (!(isImage||isAudio||isVideo)) {\n <i class=\"file-type-icon fs-4\" [ngClass]=\"getFileIconClass()\" aria-hidden=\"true\"></i>\n }\n </a>\n</div>\n", styles: [".image_box{text-align:center;position:relative}.image_box img{width:100%}.file-preview-link{display:block;color:inherit;text-decoration:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
901
+ }
902
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FilePreviewComponent, decorators: [{
903
+ type: Component,
904
+ args: [{ selector: 'site-file-preview', changeDetection: ChangeDetectionStrategy.Eager, imports: [CommonModule], template: "<div class=\"image_box\" [style]=\"{ width: width, maxWidth: '100%' }\">\n <a class=\"file-preview-link\" [href]=\"linkHref\" [download]=\"name\" [attr.aria-label]=\"name\">\n @if (isImage) {\n <div class=\"img_box\" [class]=\"className\">\n <img [src]=\"displaySrc\" [alt]=\"name\" />\n </div>\n }\n @if (isAudio) {\n <i class=\"file-type-icon fa fa-file-audio-o fs-4\" aria-hidden=\"true\"></i>\n }\n @if (isVideo) {\n <i class=\"file-type-icon fa fa-file-video-o fs-4\" aria-hidden=\"true\"></i>\n }\n @if (!(isImage||isAudio||isVideo)) {\n <i class=\"file-type-icon fs-4\" [ngClass]=\"getFileIconClass()\" aria-hidden=\"true\"></i>\n }\n </a>\n</div>\n", styles: [".image_box{text-align:center;position:relative}.image_box img{width:100%}.file-preview-link{display:block;color:inherit;text-decoration:none}\n"] }]
905
+ }], propDecorators: { width: [{
906
+ type: Input
907
+ }], src: [{
908
+ type: Input
909
+ }], type: [{
910
+ type: Input
911
+ }], name: [{
912
+ type: Input
913
+ }], className: [{
914
+ type: Input
915
+ }], resizeWidth: [{
916
+ type: Input
917
+ }], resizeHeight: [{
918
+ type: Input
919
+ }] } });
920
+
921
+ const UNITS = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
922
+ class FormatFileSizePipe {
923
+ transform(bytes) {
924
+ let size = bytes;
925
+ let unitIndex = 0;
926
+ while (size >= 1024 && unitIndex < UNITS.length - 1) {
927
+ size /= 1024;
928
+ unitIndex++;
929
+ }
930
+ return `${size.toFixed(1)} ${UNITS[unitIndex]}`;
931
+ }
932
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FormatFileSizePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
933
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.2.2", ngImport: i0, type: FormatFileSizePipe, isStandalone: true, name: "formatFileSize" }); }
934
+ }
935
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FormatFileSizePipe, decorators: [{
936
+ type: Pipe,
937
+ args: [{
938
+ name: 'formatFileSize',
939
+ standalone: true,
940
+ }]
941
+ }] });
942
+
943
+ class FileModalComponent {
944
+ constructor(fileService, toaster, list, confirmation, localizationService) {
945
+ this.fileService = fileService;
946
+ this.toaster = toaster;
947
+ this.list = list;
948
+ this.confirmation = confirmation;
949
+ this.localizationService = localizationService;
950
+ // The file list uses the opposite visual convention for sort arrows.
951
+ // Keep the direction sent by ListService unchanged and only swap the icons.
952
+ this.tableSortIcons = {
953
+ sortAscending: 'datatable-icon-down',
954
+ sortDescending: 'datatable-icon-up',
955
+ };
956
+ this.createDirectoryPermissionName = '';
957
+ this._fileContainerName = '';
958
+ this.multiple = false;
959
+ this.sizeLimit = 1048576;
960
+ this.visibleChange = new EventEmitter();
961
+ this.modalOpen = false;
962
+ /**
963
+ * The two secondary dialogs' open/busy flags are signals, not plain fields: they are written after an
964
+ * HTTP call returns (rename, move), and bound on `<abp-modal>` elements that sit in this component's own
965
+ * view - under the host layout's OnPush ancestors (`abp-router-outlet`, the LeptonX layouts), where a
966
+ * plain-field write in a response callback never marks the view for check, so the dialog would stay open
967
+ * until some unrelated event happened to. What renders *inside* a dialog (header/body/footer) is attached
968
+ * to the application root by `NgbModal` and refreshed on every tick, so the plain fields it reads
969
+ * (`data`, `uploadingFiles`, `isRenaming`, ...) are not affected.
970
+ */
971
+ this.fileNameModalOpen = signal(false, /* @ts-ignore */
972
+ ...(ngDevMode ? [{ debugName: "fileNameModalOpen" }] : /* istanbul ignore next */ []));
973
+ this.hasQueryHook = false;
974
+ /** Flattened directory list from the tree, for the breadcrumb-style path lookups below. */
975
+ this.flattenedDirectories = [];
976
+ this.uploadingFiles = [];
977
+ this.data = {
978
+ items: [],
979
+ totalCount: 0,
980
+ };
981
+ this.filters = {};
982
+ this.selectFilefn = new EventEmitter();
983
+ this.moveModalOpen = signal(false, /* @ts-ignore */
984
+ ...(ngDevMode ? [{ debugName: "moveModalOpen" }] : /* istanbul ignore next */ []));
985
+ this.moveModalBusy = signal(false, /* @ts-ignore */
986
+ ...(ngDevMode ? [{ debugName: "moveModalBusy" }] : /* istanbul ignore next */ []));
987
+ this.selectedFiles = [];
988
+ this.isAllSelected = false;
989
+ this.isRowSelected = (id) => {
990
+ return this.selectedFiles.some(item => item.id === id);
991
+ };
992
+ this.isRenaming = false;
993
+ }
994
+ loadContainerConfiguration() {
995
+ this.fileService.getContainerConfiguration(this.fileContainerName).subscribe(res => {
996
+ this.createDirectoryPermissionName = res?.createDirectoryPermissionName;
997
+ // Reflect the container's actual server-side limit (FileSizeLimitHandler.MaxFileSize) once
998
+ // it's known, instead of leaving the pre-fetch default of 1MB as the permanent client cap.
999
+ if (res?.maxBlobSize > 0) {
1000
+ this.sizeLimit = res.maxBlobSize;
1001
+ }
1002
+ this.scheduleTableRecalculation();
1003
+ });
1004
+ }
1005
+ /** No default: an unconfigured container is a caller bug, not implicitly "Images". */
1006
+ set fileContainerName(v) {
1007
+ if (v) {
1008
+ this._fileContainerName = v;
1009
+ }
1010
+ }
1011
+ get fileContainerName() {
1012
+ return this._fileContainerName;
1013
+ }
1014
+ set limit(v) {
1015
+ this.sizeLimit = v;
1016
+ }
1017
+ set visible(v) {
1018
+ this.modalOpen = v;
1019
+ if (v) {
1020
+ this.loadData();
1021
+ }
1022
+ }
1023
+ onFileNameModalVisibleChange(visible) {
1024
+ this.fileNameModalOpen.set(visible);
1025
+ if (!visible) {
1026
+ this.fileNameForm = undefined;
1027
+ this.editingFileRow = undefined;
1028
+ }
1029
+ }
1030
+ onModalVisibleChange(visible) {
1031
+ if (visible)
1032
+ return;
1033
+ this.modalOpen = false;
1034
+ this.visibleChange.emit(visible);
1035
+ this.createDirectoryPermissionName = '';
1036
+ this.selectedTreeNode = undefined;
1037
+ this.selectedFiles = [];
1038
+ this.uploadingFiles = [];
1039
+ this.onCancelFileNameEdit();
1040
+ }
1041
+ /** Recomputes the table's column widths once the modal has finished its open transition. */
1042
+ onModalInit() {
1043
+ this.scheduleTableRecalculation();
1044
+ }
1045
+ scheduleTableRecalculation() {
1046
+ if (!this.modalOpen)
1047
+ return;
1048
+ if (typeof requestAnimationFrame === 'undefined') {
1049
+ setTimeout(() => this.fileTable?.recalculate());
1050
+ return;
1051
+ }
1052
+ if (this.tableRecalculationFrame !== undefined) {
1053
+ cancelAnimationFrame(this.tableRecalculationFrame);
1054
+ }
1055
+ this.tableRecalculationFrame = requestAnimationFrame(() => {
1056
+ this.tableRecalculationFrame = requestAnimationFrame(() => {
1057
+ this.tableRecalculationFrame = undefined;
1058
+ if (this.modalOpen) {
1059
+ this.fileTable?.recalculate();
1060
+ }
1061
+ });
1062
+ });
1063
+ }
1064
+ modalSave() {
1065
+ if (this.selectedFiles.length === 0)
1066
+ return;
1067
+ const selected = structuredClone(this.selectedFiles);
1068
+ this.selectFilefn.emit(selected);
1069
+ this.onModalVisibleChange(false);
1070
+ }
1071
+ loadData() {
1072
+ if (this.modalOpen && this.fileContainerName) {
1073
+ this.list.maxResultCount = 50;
1074
+ this.loadContainerConfiguration();
1075
+ if (!this.hasQueryHook) {
1076
+ this.hookToQuery();
1077
+ this.hasQueryHook = true;
1078
+ }
1079
+ else {
1080
+ this.list.get();
1081
+ }
1082
+ }
1083
+ }
1084
+ getSelectedDirectoryId() {
1085
+ const key = this.selectedTreeNode?.key;
1086
+ return key && key !== MY_FILES_NODE_KEY ? key : undefined;
1087
+ }
1088
+ onTreeNodeData(nodes) {
1089
+ this.flattenedDirectories = this.flattenTree(nodes);
1090
+ }
1091
+ getDirectoryPath(node) {
1092
+ const path = [];
1093
+ const visitedKeys = new Set();
1094
+ let currentKey = node?.key;
1095
+ while (currentKey && !visitedKeys.has(currentKey)) {
1096
+ visitedKeys.add(currentKey);
1097
+ const currentNode = this.flattenedDirectories.find(item => item.key === currentKey);
1098
+ if (!currentNode)
1099
+ break;
1100
+ path.unshift(currentNode);
1101
+ currentKey = currentNode.parentId ?? undefined;
1102
+ }
1103
+ return path;
1104
+ }
1105
+ getFileDirectoryPath(directoryId) {
1106
+ if (!directoryId)
1107
+ return '';
1108
+ return this.getDirectoryPath({ key: directoryId })
1109
+ .map(node => node.name || node.title)
1110
+ .filter(Boolean)
1111
+ .join(' / ');
1112
+ }
1113
+ flattenTree(nodes) {
1114
+ const result = [];
1115
+ const visit = (items) => {
1116
+ for (const item of items) {
1117
+ result.push(item);
1118
+ if (item.children?.length) {
1119
+ visit(item.children);
1120
+ }
1121
+ }
1122
+ };
1123
+ visit(nodes);
1124
+ return result;
1125
+ }
1126
+ onTreeNodeClick(node) {
1127
+ this.filters.skipCount = 0;
1128
+ this.selectedTreeNode = node;
1129
+ this.list.get();
1130
+ }
1131
+ async onFileInputChange(event) {
1132
+ const input = event.target;
1133
+ const files = Array.from(input.files ?? []);
1134
+ this.uploadingFiles = files;
1135
+ // Uploaded one at a time: concurrent creates against the same container can race inside
1136
+ // the blob storage provider (e.g. Volo.Abp.BlobStoring.Database) and surface as a spurious
1137
+ // AbpDbConcurrencyException (409) on one of the files. Sequential requests avoid that race.
1138
+ for (const file of files) {
1139
+ if (file.size > this.sizeLimit) {
1140
+ this.setUploadStatus(file, 2);
1141
+ continue;
1142
+ }
1143
+ try {
1144
+ await this.uploadFile(file);
1145
+ this.setUploadStatus(file, 1);
1146
+ }
1147
+ catch {
1148
+ this.setUploadStatus(file, 2);
1149
+ }
1150
+ }
1151
+ this.list.get();
1152
+ const allSucceeded = !this.uploadingFiles.some(file => file.status === 2);
1153
+ if (allSucceeded) {
1154
+ setTimeout(() => {
1155
+ this.uploadingFiles = [];
1156
+ }, 4000);
1157
+ }
1158
+ }
1159
+ setUploadStatus(file, status) {
1160
+ for (const uploading of this.uploadingFiles) {
1161
+ if (uploading === file)
1162
+ uploading.status = status;
1163
+ }
1164
+ }
1165
+ uploadFile(file) {
1166
+ return new Promise((resolve, reject) => {
1167
+ const formData = new FormData();
1168
+ formData.append('file', file, file.name);
1169
+ this.fileService
1170
+ .create({
1171
+ file: formData,
1172
+ containerName: this.fileContainerName,
1173
+ directoryId: this.getSelectedDirectoryId() ?? null,
1174
+ })
1175
+ .subscribe({ next: resolve, error: reject });
1176
+ });
1177
+ }
1178
+ hookToQuery() {
1179
+ const getData = (query) => this.fileService.getList({
1180
+ ...query,
1181
+ ...this.filters,
1182
+ containerName: this.fileContainerName,
1183
+ directoryId: this.getSelectedDirectoryId(),
1184
+ });
1185
+ const setData = (list) => {
1186
+ this.data = list;
1187
+ this.selectedFiles = [];
1188
+ this.isAllSelected = false;
1189
+ this.scheduleTableRecalculation();
1190
+ };
1191
+ this.list.hookToQuery(getData).subscribe(setData);
1192
+ }
1193
+ deleteSelectedFiles() {
1194
+ this.confirmation
1195
+ .warn('Site::Files:BatchDeletionConfirmationMessage', 'Site::Files:BatchDeletionConfirmationTitle', {
1196
+ messageLocalizationParams: [String(this.selectedFiles.length)],
1197
+ })
1198
+ .subscribe(async (status) => {
1199
+ if (status !== 'confirm')
1200
+ return;
1201
+ const result = await this.batchDeleteFiles(this.selectedFiles);
1202
+ if (result.success) {
1203
+ this.toaster.success(result.message);
1204
+ }
1205
+ else {
1206
+ this.toaster.error(result.message);
1207
+ }
1208
+ this.list.get();
1209
+ });
1210
+ }
1211
+ async batchDeleteFiles(files) {
1212
+ const results = await Promise.allSettled(files.map(file => new Promise((resolve, reject) => {
1213
+ this.fileService.delete(file.id).subscribe({ next: () => resolve(), error: () => reject(file) });
1214
+ })));
1215
+ const failedCount = results.filter(result => result.status === 'rejected').length;
1216
+ return {
1217
+ success: failedCount === 0,
1218
+ message: failedCount === 0
1219
+ ? this.localizationService.instant('Site::Files:DeletedSuccessfully')
1220
+ : this.localizationService.instant('Site::Files:ItemsFailedToDelete', String(failedCount)),
1221
+ };
1222
+ }
1223
+ openMoveModal() {
1224
+ if (this.selectedFiles.length === 0 || !this.createDirectoryPermissionName)
1225
+ return;
1226
+ this.moveTargetDirectoryNode = undefined;
1227
+ this.moveModalOpen.set(true);
1228
+ }
1229
+ onMoveTargetDirectoryChange(node) {
1230
+ this.moveTargetDirectoryNode = node;
1231
+ }
1232
+ onMoveModalVisibleChange(visible) {
1233
+ this.moveModalOpen.set(visible);
1234
+ if (!visible) {
1235
+ this.moveModalBusy.set(false);
1236
+ this.moveTargetDirectoryNode = undefined;
1237
+ }
1238
+ }
1239
+ async moveSelectedFiles() {
1240
+ const targetNode = this.moveTargetDirectoryNode;
1241
+ if (!targetNode?.key || this.selectedFiles.length === 0 || this.moveModalBusy())
1242
+ return;
1243
+ const targetDirectoryId = isMyFilesNode(targetNode) ? null : targetNode.key;
1244
+ this.moveModalBusy.set(true);
1245
+ const results = await Promise.allSettled(this.selectedFiles.map(file => new Promise((resolve, reject) => {
1246
+ this.fileService
1247
+ .update(file.id, { directoryId: targetDirectoryId })
1248
+ .subscribe({ next: () => resolve(), error: reject });
1249
+ })));
1250
+ this.moveModalBusy.set(false);
1251
+ const failedCount = results.filter(result => result.status === 'rejected').length;
1252
+ if (failedCount > 0) {
1253
+ this.toaster.error(this.localizationService.instant('Site::Files:MoveFailed'));
1254
+ return;
1255
+ }
1256
+ this.toaster.success(this.localizationService.instant('Site::Files:MovedSuccessfully'));
1257
+ this.selectedFiles = [];
1258
+ this.isAllSelected = false;
1259
+ this.list.get();
1260
+ this.moveModalOpen.set(false);
1261
+ }
1262
+ closeFileStatusModal() {
1263
+ this.uploadingFiles = [];
1264
+ }
1265
+ ngOnChanges(changes) {
1266
+ const selectPickerFileChange = changes['selectPickerFile'];
1267
+ if (!selectPickerFileChange) {
1268
+ return;
1269
+ }
1270
+ this.selectedFiles = structuredClone(selectPickerFileChange.currentValue ?? []);
1271
+ }
1272
+ onRowCheckboxChange(event, row, rows) {
1273
+ const checked = event.target.checked;
1274
+ let selected = [...this.selectedFiles];
1275
+ if (this.multiple) {
1276
+ if (checked) {
1277
+ selected.push(row);
1278
+ }
1279
+ else {
1280
+ selected = selected.filter(item => item.id !== row.id);
1281
+ }
1282
+ this.isAllSelected = this.areAllSelected(rows, selected);
1283
+ }
1284
+ else {
1285
+ selected = checked ? [row] : [];
1286
+ }
1287
+ this.selectedFiles = this.removeDuplicatesById(selected);
1288
+ }
1289
+ areAllSelected(rows, selected = []) {
1290
+ if (rows.length === 0)
1291
+ return false;
1292
+ return rows.every(row => selected.some(item => item.id === row.id));
1293
+ }
1294
+ onSelectAllChange(event, rows) {
1295
+ const checked = event.target.checked;
1296
+ this.selectedFiles = checked
1297
+ ? this.removeDuplicatesById([...this.selectedFiles, ...rows])
1298
+ : this.selectedFiles.filter(item => !rows.some(row => row.id === item.id));
1299
+ this.isAllSelected = checked;
1300
+ }
1301
+ removeDuplicatesById(files) {
1302
+ const seenIds = new Set();
1303
+ return files.filter(file => {
1304
+ if (seenIds.has(file.id))
1305
+ return false;
1306
+ seenIds.add(file.id);
1307
+ return true;
1308
+ });
1309
+ }
1310
+ onSubmitFileName() {
1311
+ if (!this.fileNameForm?.valid || this.isRenaming)
1312
+ return;
1313
+ const input = this.fileNameForm.value;
1314
+ this.isRenaming = true;
1315
+ this.fileService
1316
+ .update(input.id, { name: input.fileName })
1317
+ .pipe(finalize(() => (this.isRenaming = false)))
1318
+ .subscribe(() => {
1319
+ const row = this.data.items.find(item => item.id === this.editingFileRow?.id);
1320
+ if (row) {
1321
+ row.name = input.fileName;
1322
+ }
1323
+ this.fileNameForm = undefined;
1324
+ this.editingFileRow = undefined;
1325
+ this.fileNameModalOpen.set(false);
1326
+ this.toaster.success(this.localizationService.instant('Site::Files:SavedSuccessfully'));
1327
+ });
1328
+ }
1329
+ onEditFileName(row) {
1330
+ this.fileNameForm = new FormGroup({
1331
+ fileName: new FormControl(row.name, [Validators.required]),
1332
+ id: new FormControl(row.id, [Validators.required]),
1333
+ });
1334
+ this.editingFileRow = row;
1335
+ this.fileNameModalOpen.set(true);
1336
+ }
1337
+ onCancelFileNameEdit() {
1338
+ this.fileNameModalOpen.set(false);
1339
+ this.editingFileRow = undefined;
1340
+ this.fileNameForm = undefined;
1341
+ }
1342
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FileModalComponent, deps: [{ token: FileAdminService }, { token: i2.ToasterService }, { token: i4.ListService }, { token: i2.ConfirmationService }, { token: i4.LocalizationService }], target: i0.ɵɵFactoryTarget.Component }); }
1343
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.2", type: FileModalComponent, isStandalone: true, selector: "site-file-modal", inputs: { fileContainerName: "fileContainerName", multiple: "multiple", limit: "limit", visible: "visible", selectPickerFile: "selectPickerFile" }, outputs: { visibleChange: "visibleChange", selectFilefn: "selectFilefn" }, providers: [
1344
+ ListService,
1345
+ { provide: LIST_QUERY_DEBOUNCE_TIME, useValue: 500 },
1346
+ ], viewQueries: [{ propertyName: "fileTable", first: true, predicate: ["fileTable"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<abp-modal [(visible)]=\"modalOpen\" (visibleChange)=\"onModalVisibleChange($event)\" (init)=\"onModalInit()\"\n [options]=\"{ size: 'xl', scrollable: false, windowClass: 'file-modal-window' }\">\n <ng-template #abpHeader>\n <h3>{{'Site::Files:Files' | abpLocalization}}</h3>\n </ng-template>\n <ng-template #abpBody>\n @if (modalOpen) {\n <div class=\"row file-modal-body file-modal\">\n @if (createDirectoryPermissionName) {\n <div class=\"col-3\" style=\"overflow: auto; height: calc(100vh - 240px)\">\n <site-file-directory-tree [theSelectedTreeNode]=\"selectedTreeNode\" (nodeClick)=\"onTreeNodeClick($event)\"\n (treeNodeData)=\"onTreeNodeData($event)\"\n [fileContainerName]=\"fileContainerName\"></site-file-directory-tree>\n </div>\n }\n <div [class]=\"createDirectoryPermissionName ? 'col-9 file-browser-content' : 'col-12 file-browser-content'\">\n <div class=\"mb-3 d-flex justify-content-between align-items-center\">\n <div class=\"file-list-actions d-flex align-items-center gap-3\" style=\"flex: 2\">\n @if (selectedFiles.length > 0) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click.stop)=\"deleteSelectedFiles()\"\n [attr.aria-label]=\"'Site::Files:Delete' | abpLocalization\"\n [title]=\"'Site::Files:Delete' | abpLocalization\">\n <i class=\"fa fa-trash\" aria-hidden=\"true\"></i>\n </button>\n @if (createDirectoryPermissionName) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click.stop)=\"openMoveModal()\"\n [attr.aria-label]=\"'Site::Files:Move' | abpLocalization\"\n [title]=\"'Site::Files:Move' | abpLocalization\">\n <i class=\"fa fa-folder-open-o\" aria-hidden=\"true\"></i>\n </button>\n }\n }\n </div>\n <div class=\"input-group form-file\" style=\"flex: 2\">\n <input type=\"file\" class=\"form-control\" #fileUploadInput [multiple]=\"true\" (change)=\"onFileInputChange($event)\" />\n </div>\n </div>\n <div class=\"file-table-container\">\n <ngx-datatable #fileTable class=\"material container-height\" [rows]=\"data.items\" [list]=\"list\"\n [cssClasses]=\"tableSortIcons\" columnMode=\"flex\"\n [headerHeight]=\"50\" [footerHeight]=\"50\" rowHeight=\"auto\" [scrollbarV]=\"true\" [scrollbarVDynamic]=\"true\"\n [virtualization]=\"false\"\n [externalPaging]=\"true\" [count]=\"data.totalCount\">\n <ngx-datatable-column [flexGrow]=\"0.6\" prop=\"id\" [sortable]=\"false\" [draggable]=\"false\"\n [resizeable]=\"false\">\n <ng-template ngx-datatable-header-template let-value=\"value\" let-allRowsSelected=\"allRowsSelected\"\n let-selectFn=\"selectFn\">\n @if (multiple) {\n <input type=\"checkbox\" class=\"form-check-input\" [checked]=\"isAllSelected\"\n (change)=\"onSelectAllChange($event,data.items)\" />\n }\n </ng-template>\n <ng-template ngx-datatable-cell-template let-row=\"row\" let-value=\"value\">\n <input type=\"checkbox\" class=\"form-check-input\" [checked]=\"isRowSelected(value)\"\n (change)=\"onRowCheckboxChange($event,row,data.items)\" />\n </ng-template>\n </ngx-datatable-column>\n <ngx-datatable-column [name]=\"'Site::Files:FileName' | abpLocalization\" prop=\"name\" [sortable]=\"false\"\n [flexGrow]=\"4\">\n <ng-template let-row=\"row\" let-value=\"value\" ngx-datatable-cell-template>\n <div class=\"file-name-cell\">\n <div class=\"file-name-content\">\n @if (getFileDirectoryPath(row.directoryId)) {\n <div class=\"file-directory-path\" [title]=\"getFileDirectoryPath(row.directoryId)\">\n <i class=\"fa fa-folder-o me-1\" aria-hidden=\"true\"></i>{{ getFileDirectoryPath(row.directoryId) }}\n </div>\n }\n <div>{{ value }}</div>\n </div>\n <div class=\"file-name-preview\" [attr.title]=\"row.name\">\n <site-file-preview [width]=\"'48px'\" [src]=\"row.url\" [resizeWidth]=\"96\" [resizeHeight]=\"96\"\n [name]=\"row.name\" [type]=\"row.mimeType\"></site-file-preview>\n </div>\n </div>\n </ng-template>\n </ngx-datatable-column>\n <ngx-datatable-column [name]=\"'Site::Files:FileSize' | abpLocalization\" prop=\"size\" [flexGrow]=\"1.5\">\n <ng-template let-row=\"row\" let-value=\"value\" ngx-datatable-cell-template>\n {{(value|formatFileSize) }}\n </ng-template>\n </ngx-datatable-column>\n <ngx-datatable-column [name]=\"'Site::Files:CreationTime' | abpLocalization\" prop=\"creationTime\"\n [flexGrow]=\"2\">\n <ng-template let-row=\"row\" let-value=\"value\" ngx-datatable-cell-template>\n {{ value | date : 'short' }}\n </ng-template>\n </ngx-datatable-column>\n <ngx-datatable-column [name]=\"'AbpUi::Edit' | abpLocalization\" [flexGrow]=\"1\" [sortable]=\"false\">\n <ng-template let-row=\"row\" ngx-datatable-cell-template>\n <button type=\"button\" class=\"btn btn-sm btn-secondary p-0\" (click.stop)=\"onEditFileName(row)\"\n [attr.aria-label]=\"'AbpUi::Edit' | abpLocalization\">\n <i class=\"fas fa-edit p-2\" aria-hidden=\"true\"></i>\n </button>\n </ng-template>\n </ngx-datatable-column>\n </ngx-datatable>\n </div>\n @if (uploadingFiles.length > 0) {\n <div\n class=\"position-fixed bottom-0 end-0 shadow-lg file-status-modal m-5\" style=\"border-radius: 10px\">\n <div class=\"card p-1\">\n <div class=\"card-header d-flex align-items-center justify-content-between p-0 py-1 border-bottom\">\n <div>{{'Site::Files:UploadFiles' | abpLocalization}}</div>\n <button type=\"button\" class=\"btn-close\" aria-label=\"Close\" (click.stop)=\"closeFileStatusModal()\"></button>\n </div>\n <div class=\"card-body\">\n <table class=\"table\">\n <thead>\n <tr>\n <th scope=\"col\">{{'Site::Files:FileName' | abpLocalization}}</th>\n <th scope=\"col\">{{'Site::Files:FileSize' | abpLocalization}}</th>\n <th scope=\"col\"></th>\n </tr>\n </thead>\n <tbody>\n @for (item of uploadingFiles; track item; let i = $index) {\n <tr>\n <th scope=\"row\">\n <div>{{ item.name }}</div>\n @if (item.size > sizeLimit) {\n <div class=\"form-text text-danger\">\n {{'Site::Files:ExceedsMaximumSize' |\n abpLocalization:(sizeLimit|formatFileSize)}}\n </div>\n }\n </th>\n <td>{{ item.size | formatFileSize }}</td>\n <td>\n @if (item.status === 1) {\n <i class=\"fas fa-check text-primary\"></i>\n }\n @if (item.status === 2) {\n <i class=\"fas fa-times-circle text-danger\"></i>\n }\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n }\n </ng-template>\n <ng-template #abpFooter>\n <button type=\"button\" class=\"btn btn-outline-secondary\" abpClose>{{'AbpUi::Close' | abpLocalization}}</button>\n <abp-button iconClass=\"fa fa-check\" [disabled]=\"selectedFiles.length === 0\" (click)=\"modalSave()\">\n <span>{{'Site::Files:Confirm' | abpLocalization}}</span>\n <span class=\"selected-file-count\" [class.d-none]=\"selectedFiles.length === 0\">\n ({{selectedFiles.length}})\n </span>\n </abp-button>\n </ng-template>\n </abp-modal>\n\n <abp-modal [(visible)]=\"fileNameModalOpen\" (visibleChange)=\"onFileNameModalVisibleChange($event)\">\n <ng-template #abpHeader>\n <h3>{{'AbpUi::Edit' | abpLocalization}} {{'Site::Files:FileName' | abpLocalization}}</h3>\n </ng-template>\n <ng-template #abpBody>\n @if (fileNameForm) {\n <form id=\"file-name-edit-form\" [formGroup]=\"fileNameForm\" (ngSubmit)=\"onSubmitFileName()\">\n <div class=\"mb-3\">\n <label class=\"form-label\" for=\"file-name-edit-input\">\n {{'Site::Files:FileName' | abpLocalization}}\n </label>\n <input id=\"file-name-edit-input\" class=\"form-control\" type=\"text\" formControlName=\"fileName\" />\n </div>\n </form>\n }\n </ng-template>\n <ng-template #abpFooter>\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"onCancelFileNameEdit()\">\n {{'AbpUi::Cancel' | abpLocalization}}\n </button>\n <button type=\"submit\" class=\"btn btn-primary\" form=\"file-name-edit-form\"\n [disabled]=\"isRenaming || fileNameForm?.invalid\">\n @if (isRenaming) {\n <i class=\"fa-spin fa fa-spinner me-1\" aria-hidden=\"true\"></i>\n }\n {{'AbpUi::Save' | abpLocalization}}\n </button>\n </ng-template>\n </abp-modal>\n\n <abp-modal [(visible)]=\"moveModalOpen\" [busy]=\"moveModalBusy()\"\n (visibleChange)=\"onMoveModalVisibleChange($event)\">\n <ng-template #abpHeader>\n <h3>{{'Site::Files:Move' | abpLocalization}}</h3>\n </ng-template>\n <ng-template #abpBody>\n @if (moveModalOpen()) {\n <div class=\"move-directory-tree\">\n <site-file-directory-tree [theSelectedTreeNode]=\"moveTargetDirectoryNode\"\n (nodeClick)=\"onMoveTargetDirectoryChange($event)\" [fileContainerName]=\"fileContainerName\">\n </site-file-directory-tree>\n </div>\n }\n </ng-template>\n <ng-template #abpFooter>\n <button type=\"button\" class=\"btn btn-secondary\" abpClose>\n {{'AbpUi::Cancel' | abpLocalization}}\n </button>\n <button type=\"button\" class=\"btn btn-primary\" [disabled]=\"moveModalBusy() || !moveTargetDirectoryNode\"\n (click)=\"moveSelectedFiles()\">\n @if (moveModalBusy()) {\n <i class=\"fa-spin fa fa-spinner me-1\" aria-hidden=\"true\"></i>\n }\n {{'AbpUi::Save' | abpLocalization}}\n </button>\n </ng-template>\n </abp-modal>\n", styles: ["::ng-deep .file-modal .container-height{height:100%;min-height:0}::ng-deep .file-modal .file-browser-content{display:flex;flex-direction:column;height:calc(100vh - 240px);min-height:0}::ng-deep .file-modal .file-table-container{flex:1 1 auto;min-height:0}::ng-deep .file-modal .file-status-modal{background:#fff}::ng-deep .file-modal .file-status-modal .card-body{max-height:300px;overflow-y:auto}::ng-deep .file-modal .ngx-datatable.material.single-selection .datatable-body-row.active,::ng-deep .file-modal .ngx-datatable.material.single-selection .datatable-body-row.active .datatable-row-group,::ng-deep .file-modal .ngx-datatable.material.multi-selection .datatable-body-row.active,::ng-deep .file-modal .ngx-datatable.material.multi-selection .datatable-body-row.active .datatable-row-group,::ng-deep .file-modal .ngx-datatable.material.multi-click-selection .datatable-body-row.active,::ng-deep .file-modal .ngx-datatable.material.multi-click-selection .datatable-body-row.active .datatable-row-group{background-color:transparent!important}::ng-deep .file-modal .ngx-datatable.fixed-header .datatable-header .datatable-header-inner .datatable-header-cell,::ng-deep .file-modal .ngx-datatable.material .datatable-body .datatable-body-row .datatable-body-cell{overflow:initial;line-break:anywhere}::ng-deep .file-modal input[type=checkbox]{cursor:pointer}::ng-deep .file-modal .file-directory-path{color:var(--bs-secondary-color);font-size:.75rem;font-weight:400;line-height:1.25;margin-bottom:.25rem;overflow-wrap:anywhere}::ng-deep .file-modal .file-name-cell{display:flex;align-items:start;gap:.75rem;min-width:0}::ng-deep .file-modal .file-name-content{flex:1 1 auto;min-width:0}::ng-deep .file-modal .file-name-preview{flex:0 0 48px;width:48px;height:48px;display:flex;align-items:start;justify-content:center;overflow:hidden}::ng-deep .file-modal .file-name-preview ::ng-deep .image_box{width:48px!important;height:48px;display:flex;align-items:center;justify-content:center}::ng-deep .file-modal .file-name-preview ::ng-deep .image_box img{width:100%;height:100%;object-fit:cover}::ng-deep .file-modal .file-name-preview ::ng-deep .image_box a{line-height:1}::ng-deep .file-modal-window .modal-body{padding-bottom:.5rem!important}::ng-deep .file-modal-window .modal-footer{padding-top:.5rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.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$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i3.ValidationGroupDirective, selector: "[formGroup],[formGroupName]", exportAs: ["validationGroup"] }, { kind: "directive", type: i3.ValidationDirective, selector: "[formControl],[formControlName]", exportAs: ["validationDirective"] }, { kind: "directive", type: i4.FormSubmitDirective, selector: "form[ngSubmit][formGroup]", inputs: ["debounce", "notValidateOnSubmit", "markAsDirtyWhenSubmit"], outputs: ["ngSubmit"] }, { kind: "directive", type: i4.StopPropagationDirective, selector: "[click.stop]", outputs: ["click.stop"] }, { kind: "ngmodule", type: ThemeSharedModule }, { kind: "component", type: i2.ButtonComponent, selector: "abp-button", inputs: ["buttonId", "buttonClass", "buttonType", "formName", "iconClass", "loading", "disabled", "attributes"], outputs: ["click", "focus", "blur", "abpClick", "abpFocus", "abpBlur"] }, { kind: "component", type: i2.ModalComponent, selector: "abp-modal", inputs: ["visible", "busy", "options", "suppressUnsavedChangesWarning"], outputs: ["visibleChange", "init", "appear", "disappear"] }, { kind: "directive", type: i2.ModalCloseDirective, selector: "[abpClose]" }, { 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.DataTableColumnHeaderDirective, selector: "[ngx-datatable-header-template]" }, { kind: "directive", type: i6.DataTableColumnCellDirective, selector: "[ngx-datatable-cell-template]" }, { kind: "directive", type: i2.NgxDatatableListDirective, selector: "ngx-datatable[list]", inputs: ["list"], exportAs: ["ngxDatatableList"] }, { kind: "component", type: FileDirectoryTreeComponent, selector: "site-file-directory-tree", inputs: ["theSelectedTreeNode", "fileContainerName"], outputs: ["nodeClick", "treeNodeData"] }, { kind: "component", type: FilePreviewComponent, selector: "site-file-preview", inputs: ["width", "src", "type", "name", "className", "resizeWidth", "resizeHeight"] }, { kind: "pipe", type: i1.DatePipe, name: "date" }, { kind: "pipe", type: i4.LocalizationPipe, name: "abpLocalization" }, { kind: "pipe", type: FormatFileSizePipe, name: "formatFileSize" }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
1347
+ }
1348
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FileModalComponent, decorators: [{
1349
+ type: Component,
1350
+ args: [{ selector: 'site-file-modal', changeDetection: ChangeDetectionStrategy.Eager, imports: [
1351
+ CommonModule,
1352
+ ReactiveFormsModule,
1353
+ CoreModule,
1354
+ ThemeSharedModule,
1355
+ FileDirectoryTreeComponent,
1356
+ FilePreviewComponent,
1357
+ FormatFileSizePipe,
1358
+ ], providers: [
1359
+ ListService,
1360
+ { provide: LIST_QUERY_DEBOUNCE_TIME, useValue: 500 },
1361
+ ], template: "<abp-modal [(visible)]=\"modalOpen\" (visibleChange)=\"onModalVisibleChange($event)\" (init)=\"onModalInit()\"\n [options]=\"{ size: 'xl', scrollable: false, windowClass: 'file-modal-window' }\">\n <ng-template #abpHeader>\n <h3>{{'Site::Files:Files' | abpLocalization}}</h3>\n </ng-template>\n <ng-template #abpBody>\n @if (modalOpen) {\n <div class=\"row file-modal-body file-modal\">\n @if (createDirectoryPermissionName) {\n <div class=\"col-3\" style=\"overflow: auto; height: calc(100vh - 240px)\">\n <site-file-directory-tree [theSelectedTreeNode]=\"selectedTreeNode\" (nodeClick)=\"onTreeNodeClick($event)\"\n (treeNodeData)=\"onTreeNodeData($event)\"\n [fileContainerName]=\"fileContainerName\"></site-file-directory-tree>\n </div>\n }\n <div [class]=\"createDirectoryPermissionName ? 'col-9 file-browser-content' : 'col-12 file-browser-content'\">\n <div class=\"mb-3 d-flex justify-content-between align-items-center\">\n <div class=\"file-list-actions d-flex align-items-center gap-3\" style=\"flex: 2\">\n @if (selectedFiles.length > 0) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click.stop)=\"deleteSelectedFiles()\"\n [attr.aria-label]=\"'Site::Files:Delete' | abpLocalization\"\n [title]=\"'Site::Files:Delete' | abpLocalization\">\n <i class=\"fa fa-trash\" aria-hidden=\"true\"></i>\n </button>\n @if (createDirectoryPermissionName) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" (click.stop)=\"openMoveModal()\"\n [attr.aria-label]=\"'Site::Files:Move' | abpLocalization\"\n [title]=\"'Site::Files:Move' | abpLocalization\">\n <i class=\"fa fa-folder-open-o\" aria-hidden=\"true\"></i>\n </button>\n }\n }\n </div>\n <div class=\"input-group form-file\" style=\"flex: 2\">\n <input type=\"file\" class=\"form-control\" #fileUploadInput [multiple]=\"true\" (change)=\"onFileInputChange($event)\" />\n </div>\n </div>\n <div class=\"file-table-container\">\n <ngx-datatable #fileTable class=\"material container-height\" [rows]=\"data.items\" [list]=\"list\"\n [cssClasses]=\"tableSortIcons\" columnMode=\"flex\"\n [headerHeight]=\"50\" [footerHeight]=\"50\" rowHeight=\"auto\" [scrollbarV]=\"true\" [scrollbarVDynamic]=\"true\"\n [virtualization]=\"false\"\n [externalPaging]=\"true\" [count]=\"data.totalCount\">\n <ngx-datatable-column [flexGrow]=\"0.6\" prop=\"id\" [sortable]=\"false\" [draggable]=\"false\"\n [resizeable]=\"false\">\n <ng-template ngx-datatable-header-template let-value=\"value\" let-allRowsSelected=\"allRowsSelected\"\n let-selectFn=\"selectFn\">\n @if (multiple) {\n <input type=\"checkbox\" class=\"form-check-input\" [checked]=\"isAllSelected\"\n (change)=\"onSelectAllChange($event,data.items)\" />\n }\n </ng-template>\n <ng-template ngx-datatable-cell-template let-row=\"row\" let-value=\"value\">\n <input type=\"checkbox\" class=\"form-check-input\" [checked]=\"isRowSelected(value)\"\n (change)=\"onRowCheckboxChange($event,row,data.items)\" />\n </ng-template>\n </ngx-datatable-column>\n <ngx-datatable-column [name]=\"'Site::Files:FileName' | abpLocalization\" prop=\"name\" [sortable]=\"false\"\n [flexGrow]=\"4\">\n <ng-template let-row=\"row\" let-value=\"value\" ngx-datatable-cell-template>\n <div class=\"file-name-cell\">\n <div class=\"file-name-content\">\n @if (getFileDirectoryPath(row.directoryId)) {\n <div class=\"file-directory-path\" [title]=\"getFileDirectoryPath(row.directoryId)\">\n <i class=\"fa fa-folder-o me-1\" aria-hidden=\"true\"></i>{{ getFileDirectoryPath(row.directoryId) }}\n </div>\n }\n <div>{{ value }}</div>\n </div>\n <div class=\"file-name-preview\" [attr.title]=\"row.name\">\n <site-file-preview [width]=\"'48px'\" [src]=\"row.url\" [resizeWidth]=\"96\" [resizeHeight]=\"96\"\n [name]=\"row.name\" [type]=\"row.mimeType\"></site-file-preview>\n </div>\n </div>\n </ng-template>\n </ngx-datatable-column>\n <ngx-datatable-column [name]=\"'Site::Files:FileSize' | abpLocalization\" prop=\"size\" [flexGrow]=\"1.5\">\n <ng-template let-row=\"row\" let-value=\"value\" ngx-datatable-cell-template>\n {{(value|formatFileSize) }}\n </ng-template>\n </ngx-datatable-column>\n <ngx-datatable-column [name]=\"'Site::Files:CreationTime' | abpLocalization\" prop=\"creationTime\"\n [flexGrow]=\"2\">\n <ng-template let-row=\"row\" let-value=\"value\" ngx-datatable-cell-template>\n {{ value | date : 'short' }}\n </ng-template>\n </ngx-datatable-column>\n <ngx-datatable-column [name]=\"'AbpUi::Edit' | abpLocalization\" [flexGrow]=\"1\" [sortable]=\"false\">\n <ng-template let-row=\"row\" ngx-datatable-cell-template>\n <button type=\"button\" class=\"btn btn-sm btn-secondary p-0\" (click.stop)=\"onEditFileName(row)\"\n [attr.aria-label]=\"'AbpUi::Edit' | abpLocalization\">\n <i class=\"fas fa-edit p-2\" aria-hidden=\"true\"></i>\n </button>\n </ng-template>\n </ngx-datatable-column>\n </ngx-datatable>\n </div>\n @if (uploadingFiles.length > 0) {\n <div\n class=\"position-fixed bottom-0 end-0 shadow-lg file-status-modal m-5\" style=\"border-radius: 10px\">\n <div class=\"card p-1\">\n <div class=\"card-header d-flex align-items-center justify-content-between p-0 py-1 border-bottom\">\n <div>{{'Site::Files:UploadFiles' | abpLocalization}}</div>\n <button type=\"button\" class=\"btn-close\" aria-label=\"Close\" (click.stop)=\"closeFileStatusModal()\"></button>\n </div>\n <div class=\"card-body\">\n <table class=\"table\">\n <thead>\n <tr>\n <th scope=\"col\">{{'Site::Files:FileName' | abpLocalization}}</th>\n <th scope=\"col\">{{'Site::Files:FileSize' | abpLocalization}}</th>\n <th scope=\"col\"></th>\n </tr>\n </thead>\n <tbody>\n @for (item of uploadingFiles; track item; let i = $index) {\n <tr>\n <th scope=\"row\">\n <div>{{ item.name }}</div>\n @if (item.size > sizeLimit) {\n <div class=\"form-text text-danger\">\n {{'Site::Files:ExceedsMaximumSize' |\n abpLocalization:(sizeLimit|formatFileSize)}}\n </div>\n }\n </th>\n <td>{{ item.size | formatFileSize }}</td>\n <td>\n @if (item.status === 1) {\n <i class=\"fas fa-check text-primary\"></i>\n }\n @if (item.status === 2) {\n <i class=\"fas fa-times-circle text-danger\"></i>\n }\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n }\n </ng-template>\n <ng-template #abpFooter>\n <button type=\"button\" class=\"btn btn-outline-secondary\" abpClose>{{'AbpUi::Close' | abpLocalization}}</button>\n <abp-button iconClass=\"fa fa-check\" [disabled]=\"selectedFiles.length === 0\" (click)=\"modalSave()\">\n <span>{{'Site::Files:Confirm' | abpLocalization}}</span>\n <span class=\"selected-file-count\" [class.d-none]=\"selectedFiles.length === 0\">\n ({{selectedFiles.length}})\n </span>\n </abp-button>\n </ng-template>\n </abp-modal>\n\n <abp-modal [(visible)]=\"fileNameModalOpen\" (visibleChange)=\"onFileNameModalVisibleChange($event)\">\n <ng-template #abpHeader>\n <h3>{{'AbpUi::Edit' | abpLocalization}} {{'Site::Files:FileName' | abpLocalization}}</h3>\n </ng-template>\n <ng-template #abpBody>\n @if (fileNameForm) {\n <form id=\"file-name-edit-form\" [formGroup]=\"fileNameForm\" (ngSubmit)=\"onSubmitFileName()\">\n <div class=\"mb-3\">\n <label class=\"form-label\" for=\"file-name-edit-input\">\n {{'Site::Files:FileName' | abpLocalization}}\n </label>\n <input id=\"file-name-edit-input\" class=\"form-control\" type=\"text\" formControlName=\"fileName\" />\n </div>\n </form>\n }\n </ng-template>\n <ng-template #abpFooter>\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"onCancelFileNameEdit()\">\n {{'AbpUi::Cancel' | abpLocalization}}\n </button>\n <button type=\"submit\" class=\"btn btn-primary\" form=\"file-name-edit-form\"\n [disabled]=\"isRenaming || fileNameForm?.invalid\">\n @if (isRenaming) {\n <i class=\"fa-spin fa fa-spinner me-1\" aria-hidden=\"true\"></i>\n }\n {{'AbpUi::Save' | abpLocalization}}\n </button>\n </ng-template>\n </abp-modal>\n\n <abp-modal [(visible)]=\"moveModalOpen\" [busy]=\"moveModalBusy()\"\n (visibleChange)=\"onMoveModalVisibleChange($event)\">\n <ng-template #abpHeader>\n <h3>{{'Site::Files:Move' | abpLocalization}}</h3>\n </ng-template>\n <ng-template #abpBody>\n @if (moveModalOpen()) {\n <div class=\"move-directory-tree\">\n <site-file-directory-tree [theSelectedTreeNode]=\"moveTargetDirectoryNode\"\n (nodeClick)=\"onMoveTargetDirectoryChange($event)\" [fileContainerName]=\"fileContainerName\">\n </site-file-directory-tree>\n </div>\n }\n </ng-template>\n <ng-template #abpFooter>\n <button type=\"button\" class=\"btn btn-secondary\" abpClose>\n {{'AbpUi::Cancel' | abpLocalization}}\n </button>\n <button type=\"button\" class=\"btn btn-primary\" [disabled]=\"moveModalBusy() || !moveTargetDirectoryNode\"\n (click)=\"moveSelectedFiles()\">\n @if (moveModalBusy()) {\n <i class=\"fa-spin fa fa-spinner me-1\" aria-hidden=\"true\"></i>\n }\n {{'AbpUi::Save' | abpLocalization}}\n </button>\n </ng-template>\n </abp-modal>\n", styles: ["::ng-deep .file-modal .container-height{height:100%;min-height:0}::ng-deep .file-modal .file-browser-content{display:flex;flex-direction:column;height:calc(100vh - 240px);min-height:0}::ng-deep .file-modal .file-table-container{flex:1 1 auto;min-height:0}::ng-deep .file-modal .file-status-modal{background:#fff}::ng-deep .file-modal .file-status-modal .card-body{max-height:300px;overflow-y:auto}::ng-deep .file-modal .ngx-datatable.material.single-selection .datatable-body-row.active,::ng-deep .file-modal .ngx-datatable.material.single-selection .datatable-body-row.active .datatable-row-group,::ng-deep .file-modal .ngx-datatable.material.multi-selection .datatable-body-row.active,::ng-deep .file-modal .ngx-datatable.material.multi-selection .datatable-body-row.active .datatable-row-group,::ng-deep .file-modal .ngx-datatable.material.multi-click-selection .datatable-body-row.active,::ng-deep .file-modal .ngx-datatable.material.multi-click-selection .datatable-body-row.active .datatable-row-group{background-color:transparent!important}::ng-deep .file-modal .ngx-datatable.fixed-header .datatable-header .datatable-header-inner .datatable-header-cell,::ng-deep .file-modal .ngx-datatable.material .datatable-body .datatable-body-row .datatable-body-cell{overflow:initial;line-break:anywhere}::ng-deep .file-modal input[type=checkbox]{cursor:pointer}::ng-deep .file-modal .file-directory-path{color:var(--bs-secondary-color);font-size:.75rem;font-weight:400;line-height:1.25;margin-bottom:.25rem;overflow-wrap:anywhere}::ng-deep .file-modal .file-name-cell{display:flex;align-items:start;gap:.75rem;min-width:0}::ng-deep .file-modal .file-name-content{flex:1 1 auto;min-width:0}::ng-deep .file-modal .file-name-preview{flex:0 0 48px;width:48px;height:48px;display:flex;align-items:start;justify-content:center;overflow:hidden}::ng-deep .file-modal .file-name-preview ::ng-deep .image_box{width:48px!important;height:48px;display:flex;align-items:center;justify-content:center}::ng-deep .file-modal .file-name-preview ::ng-deep .image_box img{width:100%;height:100%;object-fit:cover}::ng-deep .file-modal .file-name-preview ::ng-deep .image_box a{line-height:1}::ng-deep .file-modal-window .modal-body{padding-bottom:.5rem!important}::ng-deep .file-modal-window .modal-footer{padding-top:.5rem}\n"] }]
1362
+ }], ctorParameters: () => [{ type: FileAdminService }, { type: i2.ToasterService }, { type: i4.ListService }, { type: i2.ConfirmationService }, { type: i4.LocalizationService }], propDecorators: { fileTable: [{
1363
+ type: ViewChild,
1364
+ args: ['fileTable']
1365
+ }], fileContainerName: [{
1366
+ type: Input
1367
+ }], multiple: [{
1368
+ type: Input
1369
+ }], limit: [{
1370
+ type: Input
1371
+ }], visible: [{
1372
+ type: Input
1373
+ }], visibleChange: [{
1374
+ type: Output
1375
+ }], selectFilefn: [{
1376
+ type: Output
1377
+ }], selectPickerFile: [{
1378
+ type: Input
1379
+ }] } });
1380
+
1381
+ /* eslint-disable @angular-eslint/component-selector */
1382
+ class FilePickerComponent {
1383
+ constructor() {
1384
+ this.multiple = false;
1385
+ /** No default: a caller that never specifies a container has a configuration bug, not an "Images" one. */
1386
+ this.fileContainerName = '';
1387
+ this.selectFormFile = [];
1388
+ this.selectedFileChange = new EventEmitter();
1389
+ /** This component's single source of truth; also passed straight to the modal to reselect what's already picked. */
1390
+ this.fileShowTable = [];
1391
+ this.modalOpen = false;
1392
+ }
1393
+ ngOnChanges(changes) {
1394
+ const selectFormFileChange = changes['selectFormFile'];
1395
+ if (selectFormFileChange?.currentValue?.length > 0) {
1396
+ this.fileShowTable = selectFormFileChange.currentValue;
1397
+ }
1398
+ }
1399
+ onSelectFile(files) {
1400
+ this.fileShowTable = structuredClone(files);
1401
+ this.selectedFileChange.emit(this.fileShowTable);
1402
+ }
1403
+ deleteFileTableItem(index) {
1404
+ this.fileShowTable.splice(index, 1);
1405
+ this.selectedFileChange.emit(this.fileShowTable);
1406
+ }
1407
+ drop(event) {
1408
+ moveItemInArray(this.fileShowTable, event.previousIndex, event.currentIndex);
1409
+ this.selectedFileChange.emit(this.fileShowTable);
1410
+ }
1411
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FilePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1412
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.2", type: FilePickerComponent, isStandalone: true, selector: "site-file-picker", inputs: { multiple: "multiple", fileContainerName: "fileContainerName", selectFormFile: "selectFormFile" }, outputs: { selectedFileChange: "selectedFileChange" }, usesOnChanges: true, ngImport: i0, template: "@if (fileContainerName) {\n <button class=\"btn btn-sm btn-dark soft ms-2 float-end\" type=\"button\"\n (click.stop)=\"modalOpen = true\">{{'Site::Files:SelectFile' | abpLocalization}}</button>\n\n <site-file-modal [(visible)]=\"modalOpen\" [selectPickerFile]=\"fileShowTable\" [multiple]=\"multiple\"\n (selectFilefn)=\"onSelectFile($event)\" [fileContainerName]=\"fileContainerName\"></site-file-modal>\n @if (fileShowTable.length>0) {\n <table class=\"table\" cdkDropList (cdkDropListDropped)=\"drop($event)\">\n <thead>\n <tr>\n <th scope=\"col\" style=\"width: 50px;\">{{'Site::Files:RowNumber' | abpLocalization}}</th>\n <th scope=\"col\">{{'Site::Files:FileName' | abpLocalization}}</th>\n <th scope=\"col\" style=\"width: 120px;\">{{'Site::Files:FileSize' | abpLocalization}}</th>\n <th scope=\"col\" style=\"width: 50px;\"></th>\n </tr>\n </thead>\n <tbody>\n @for (item of fileShowTable; track item; let i = $index) {\n <tr cdkDrag cdkDragLockAxis=\"y\" [cdkDragData]=\"item\">\n <td>{{ i + 1 }}</td>\n <td>\n <div class=\"d-flex\">\n <div class=\"flex-grow-1 file-name-drag-handle\" cdkDragHandle\n [title]=\"'Site::Files:DragFileNameToReorder' | abpLocalization\"\n [attr.aria-label]=\"'Site::Files:DragFileNameToReorder' | abpLocalization\">\n <div>{{ item.name }}</div>\n </div>\n <div class=\"\" [attr.title]=\"item.name\">\n <site-file-preview [width]=\"'60px'\" [src]=\"item.url\" [resizeWidth]=\"120\" [resizeHeight]=\"120\"\n [name]=\"item.name\" [type]=\"item.mimeType\"></site-file-preview>\n </div>\n </div>\n </td>\n <td>{{(item.size)|formatFileSize}}</td>\n <td>\n <button class=\"btn btn-sm btn-outline-secondary p-0\" (click.stop)=\"deleteFileTableItem(i)\">\n <i class=\"fas fa-trash p-2\"></i>\n </button>\n </td>\n </tr>\n }\n </tbody>\n </table>\n } @else {\n <div class=\"no-file-hint text-muted small mt-2\">{{ 'Site::Files:NoFileSelected' | abpLocalization }}</div>\n }\n} @else {\n <div class=\"alert alert-warning py-2 px-3 mb-0\">\n {{ 'Site::Files:ContainerNotConfigured' | abpLocalization }}\n </div>\n}\n", styles: [":host{display:block}.table{border-collapse:collapse;table-layout:fixed}.no-file-hint{clear:both}.file-name-drag-handle{cursor:grab;-webkit-user-select:none;user-select:none}.file-name-drag-handle:active{cursor:grabbing}\n"], dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i4.StopPropagationDirective, selector: "[click.stop]", outputs: ["click.stop"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i2$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i2$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i2$1.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "component", type: FileModalComponent, selector: "site-file-modal", inputs: ["fileContainerName", "multiple", "limit", "visible", "selectPickerFile"], outputs: ["visibleChange", "selectFilefn"] }, { kind: "component", type: FilePreviewComponent, selector: "site-file-preview", inputs: ["width", "src", "type", "name", "className", "resizeWidth", "resizeHeight"] }, { kind: "pipe", type: i4.LocalizationPipe, name: "abpLocalization" }, { kind: "pipe", type: FormatFileSizePipe, name: "formatFileSize" }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
1413
+ }
1414
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FilePickerComponent, decorators: [{
1415
+ type: Component,
1416
+ args: [{ selector: 'site-file-picker', changeDetection: ChangeDetectionStrategy.Eager, imports: [
1417
+ CoreModule,
1418
+ DragDropModule,
1419
+ FileModalComponent,
1420
+ FilePreviewComponent,
1421
+ FormatFileSizePipe,
1422
+ ], template: "@if (fileContainerName) {\n <button class=\"btn btn-sm btn-dark soft ms-2 float-end\" type=\"button\"\n (click.stop)=\"modalOpen = true\">{{'Site::Files:SelectFile' | abpLocalization}}</button>\n\n <site-file-modal [(visible)]=\"modalOpen\" [selectPickerFile]=\"fileShowTable\" [multiple]=\"multiple\"\n (selectFilefn)=\"onSelectFile($event)\" [fileContainerName]=\"fileContainerName\"></site-file-modal>\n @if (fileShowTable.length>0) {\n <table class=\"table\" cdkDropList (cdkDropListDropped)=\"drop($event)\">\n <thead>\n <tr>\n <th scope=\"col\" style=\"width: 50px;\">{{'Site::Files:RowNumber' | abpLocalization}}</th>\n <th scope=\"col\">{{'Site::Files:FileName' | abpLocalization}}</th>\n <th scope=\"col\" style=\"width: 120px;\">{{'Site::Files:FileSize' | abpLocalization}}</th>\n <th scope=\"col\" style=\"width: 50px;\"></th>\n </tr>\n </thead>\n <tbody>\n @for (item of fileShowTable; track item; let i = $index) {\n <tr cdkDrag cdkDragLockAxis=\"y\" [cdkDragData]=\"item\">\n <td>{{ i + 1 }}</td>\n <td>\n <div class=\"d-flex\">\n <div class=\"flex-grow-1 file-name-drag-handle\" cdkDragHandle\n [title]=\"'Site::Files:DragFileNameToReorder' | abpLocalization\"\n [attr.aria-label]=\"'Site::Files:DragFileNameToReorder' | abpLocalization\">\n <div>{{ item.name }}</div>\n </div>\n <div class=\"\" [attr.title]=\"item.name\">\n <site-file-preview [width]=\"'60px'\" [src]=\"item.url\" [resizeWidth]=\"120\" [resizeHeight]=\"120\"\n [name]=\"item.name\" [type]=\"item.mimeType\"></site-file-preview>\n </div>\n </div>\n </td>\n <td>{{(item.size)|formatFileSize}}</td>\n <td>\n <button class=\"btn btn-sm btn-outline-secondary p-0\" (click.stop)=\"deleteFileTableItem(i)\">\n <i class=\"fas fa-trash p-2\"></i>\n </button>\n </td>\n </tr>\n }\n </tbody>\n </table>\n } @else {\n <div class=\"no-file-hint text-muted small mt-2\">{{ 'Site::Files:NoFileSelected' | abpLocalization }}</div>\n }\n} @else {\n <div class=\"alert alert-warning py-2 px-3 mb-0\">\n {{ 'Site::Files:ContainerNotConfigured' | abpLocalization }}\n </div>\n}\n", styles: [":host{display:block}.table{border-collapse:collapse;table-layout:fixed}.no-file-hint{clear:both}.file-name-drag-handle{cursor:grab;-webkit-user-select:none;user-select:none}.file-name-drag-handle:active{cursor:grabbing}\n"] }]
1423
+ }], propDecorators: { multiple: [{
1424
+ type: Input
1425
+ }], fileContainerName: [{
1426
+ type: Input
1427
+ }], selectFormFile: [{
1428
+ type: Input
1429
+ }], selectedFileChange: [{
1430
+ type: Output
1431
+ }] } });
1432
+
1433
+ /** Edits a file field's value: picks one or more files from Site's file library. */
1434
+ class FileControlComponent extends FieldTypeControlBase {
1435
+ get multiple() {
1436
+ return !!this.fieldValue?.field.configuration[FILE_FIELD_CONFIGURATION_NAMES.uploadFileMultiple];
1437
+ }
1438
+ get containerName() {
1439
+ return this.fieldValue?.field.configuration[FILE_FIELD_CONFIGURATION_NAMES.fileContainerName] ?? '';
1440
+ }
1441
+ /** No fallback container: an unconfigured field is a misconfiguration to surface, not paper over. */
1442
+ get isContainerConfigured() {
1443
+ return this.containerName.length > 0;
1444
+ }
1445
+ configurationDefaults() {
1446
+ return new FileFieldConfiguration();
1447
+ }
1448
+ createControl() {
1449
+ const validators = [];
1450
+ if (this.fieldValue.required) {
1451
+ validators.push(Validators.required);
1452
+ }
1453
+ const stored = Array.isArray(this.selectedValue) ? this.selectedValue : [];
1454
+ return this.fb.control(stored, validators);
1455
+ }
1456
+ onSelectedFileChange(files) {
1457
+ this.fieldControl?.setValue(files);
1458
+ }
1459
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FileControlComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
1460
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.2", type: FileControlComponent, isStandalone: true, selector: "site-file-control", usesInheritance: true, ngImport: i0, template: "<div [formGroup]=\"entityForm\">\n <div [formGroupName]=\"parentField\">\n <label class=\"form-label\" *ngIf=\"fieldValue.field.displayName\">\n {{ fieldValue.field.displayName }}\n </label>\n @if (isContainerConfigured) {\n <site-file-picker\n [selectFormFile]=\"fieldControl?.value\"\n [multiple]=\"multiple\"\n [fileContainerName]=\"containerName\"\n (selectedFileChange)=\"onSelectedFileChange($event)\"\n />\n } @else {\n <div class=\"alert alert-warning py-2 px-3 mb-0\">\n {{ 'FlexFieldsSite::File:ContainerNotConfigured' | abpLocalization }}\n </div>\n }\n <small class=\"form-text text-muted d-block\" *ngIf=\"fieldValue.field.description\">\n {{ fieldValue.field.description }}\n </small>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i3.ValidationGroupDirective, selector: "[formGroup],[formGroupName]", exportAs: ["validationGroup"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: FilePickerComponent, selector: "site-file-picker", inputs: ["multiple", "fileContainerName", "selectFormFile"], outputs: ["selectedFileChange"] }, { kind: "pipe", type: i4.LocalizationPipe, name: "abpLocalization" }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
1461
+ }
1462
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FileControlComponent, decorators: [{
1463
+ type: Component,
1464
+ args: [{ selector: 'site-file-control', changeDetection: ChangeDetectionStrategy.Eager, imports: [CommonModule, CoreModule, ReactiveFormsModule, FilePickerComponent], template: "<div [formGroup]=\"entityForm\">\n <div [formGroupName]=\"parentField\">\n <label class=\"form-label\" *ngIf=\"fieldValue.field.displayName\">\n {{ fieldValue.field.displayName }}\n </label>\n @if (isContainerConfigured) {\n <site-file-picker\n [selectFormFile]=\"fieldControl?.value\"\n [multiple]=\"multiple\"\n [fileContainerName]=\"containerName\"\n (selectedFileChange)=\"onSelectedFileChange($event)\"\n />\n } @else {\n <div class=\"alert alert-warning py-2 px-3 mb-0\">\n {{ 'FlexFieldsSite::File:ContainerNotConfigured' | abpLocalization }}\n </div>\n }\n <small class=\"form-text text-muted d-block\" *ngIf=\"fieldValue.field.description\">\n {{ fieldValue.field.description }}\n </small>\n </div>\n</div>\n" }]
1465
+ }] });
1466
+
1467
+ /** Displays a file field's value read-only: the first file's preview, plus a count. */
1468
+ class FileViewComponent {
1469
+ constructor() {
1470
+ /** Renders bare, without the label wrapper, for use inside a table cell. */
1471
+ this.showInList = false;
1472
+ this.value = '';
1473
+ }
1474
+ get files() {
1475
+ return Array.isArray(this.value) ? this.value : [];
1476
+ }
1477
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FileViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1478
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.2", type: FileViewComponent, isStandalone: true, selector: "site-file-view", inputs: { showInList: "showInList", fields: "fields", type: "type", value: "value" }, ngImport: i0, template: "@if (showInList) {\n @if (files.length > 0) {\n <div class=\"d-flex align-items-center\">\n <site-file-preview\n width=\"60px\"\n className=\"overflow-hidden me-1\"\n [src]=\"files[0].url ?? ''\"\n [resizeWidth]=\"120\"\n [resizeHeight]=\"120\"\n [name]=\"files[0].name ?? ''\"\n [type]=\"files[0].mimeType ?? ''\"\n />\n @if (files.length - 1 > 0) {\n <span>+{{ files.length - 1 }}</span>\n }\n </div>\n } @else {\n <span>&mdash;</span>\n }\n} @else {\n <div class=\"mb-3\">\n @if (fields?.field?.displayName) {\n <label class=\"form-label\">{{ fields?.field?.displayName }}</label>\n }\n @if (files.length > 0) {\n <div class=\"d-flex align-items-center\">\n <site-file-preview\n width=\"60px\"\n className=\"overflow-hidden me-1\"\n [src]=\"files[0].url ?? ''\"\n [resizeWidth]=\"120\"\n [resizeHeight]=\"120\"\n [name]=\"files[0].name ?? ''\"\n [type]=\"files[0].mimeType ?? ''\"\n />\n @if (files.length - 1 > 0) {\n <span>+{{ files.length - 1 }}</span>\n }\n </div>\n } @else {\n <div>&mdash;</div>\n }\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "component", type: FilePreviewComponent, selector: "site-file-preview", inputs: ["width", "src", "type", "name", "className", "resizeWidth", "resizeHeight"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
1479
+ }
1480
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FileViewComponent, decorators: [{
1481
+ type: Component,
1482
+ args: [{ selector: 'site-file-view', changeDetection: ChangeDetectionStrategy.Eager, imports: [CoreModule, FilePreviewComponent], template: "@if (showInList) {\n @if (files.length > 0) {\n <div class=\"d-flex align-items-center\">\n <site-file-preview\n width=\"60px\"\n className=\"overflow-hidden me-1\"\n [src]=\"files[0].url ?? ''\"\n [resizeWidth]=\"120\"\n [resizeHeight]=\"120\"\n [name]=\"files[0].name ?? ''\"\n [type]=\"files[0].mimeType ?? ''\"\n />\n @if (files.length - 1 > 0) {\n <span>+{{ files.length - 1 }}</span>\n }\n </div>\n } @else {\n <span>&mdash;</span>\n }\n} @else {\n <div class=\"mb-3\">\n @if (fields?.field?.displayName) {\n <label class=\"form-label\">{{ fields?.field?.displayName }}</label>\n }\n @if (files.length > 0) {\n <div class=\"d-flex align-items-center\">\n <site-file-preview\n width=\"60px\"\n className=\"overflow-hidden me-1\"\n [src]=\"files[0].url ?? ''\"\n [resizeWidth]=\"120\"\n [resizeHeight]=\"120\"\n [name]=\"files[0].name ?? ''\"\n [type]=\"files[0].mimeType ?? ''\"\n />\n @if (files.length - 1 > 0) {\n <span>+{{ files.length - 1 }}</span>\n }\n </div>\n } @else {\n <div>&mdash;</div>\n }\n </div>\n}\n" }]
1483
+ }], propDecorators: { showInList: [{
1484
+ type: Input
1485
+ }], fields: [{
1486
+ type: Input
1487
+ }], type: [{
1488
+ type: Input
1489
+ }], value: [{
1490
+ type: Input
1491
+ }] } });
1492
+
1493
+ /**
1494
+ * The registration key. **Persisted data** (every field definition of this type, Matrix block
1495
+ * configurations included) - unchanged from when the field type was `Dignite.Abp.FlexFields`'; must equal
1496
+ * the server's `FileFieldType.ControlName`.
1497
+ */
1498
+ const FILE_FIELD_TYPE_NAME = 'FileExplorer';
1499
+ /**
1500
+ * The file field type: picks one or more files from Site's file library. Registered by `provideSite()`
1501
+ * like `Seo` and `Content` - the client has no equivalent of the server's DI discovery.
1502
+ *
1503
+ * **No search component.** `FileFieldType.IndexValueType` is null - the value is an array of file
1504
+ * objects with no typed index column to project into.
1505
+ */
1506
+ const FILE_FIELD_TYPE = {
1507
+ name: FILE_FIELD_TYPE_NAME,
1508
+ displayNameKey: 'FlexFieldsSite::FieldType:File',
1509
+ configComponent: FileConfigComponent,
1510
+ controlComponent: FileControlComponent,
1511
+ viewComponent: FileViewComponent,
1512
+ };
1513
+
1514
+ /**
1515
+ * Configuration of a `Seo` field, shaped for `FormBuilder.group()`. Mirrors `SeoConfiguration`
1516
+ * (`src/Dignite.Site.Domain/Seo/SeoConfiguration.cs`).
1517
+ *
1518
+ * The property names are the **stored** configuration keys, not a naming choice.
1519
+ *
1520
+ * Both limits are **advisory**. `SeoFieldType.Validate` does not reject an over-length value, because a
1521
+ * search engine truncates an over-long title or description rather than rejecting it. They are a
1522
+ * threshold for an editing UI (or AI-generated content) to aim for, so the control renders them as a
1523
+ * counter and never as a `Validators.maxLength`.
1524
+ */
1525
+ class SeoConfiguration {
1526
+ constructor() {
1527
+ /** 60 - Google's practical truncation point for a search result title. */
1528
+ this['Seo.MetaTitleCharLimit'] = [60];
1529
+ /** 160 - the conventional SERP snippet length. */
1530
+ this['Seo.MetaDescriptionCharLimit'] = [160];
1531
+ }
1532
+ }
1533
+
1534
+ /**
1535
+ * Designer-side editor for a `Seo` field's configuration: the two advisory character limits.
1536
+ *
1537
+ * They are thresholds the value editor counts against, not validation - see {@link SeoConfiguration}.
1538
+ */
1539
+ class SeoConfigComponent extends FieldTypeConfigBase {
1540
+ configurationDefaults() {
1541
+ return new SeoConfiguration();
1542
+ }
1543
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SeoConfigComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
1544
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.2.2", type: SeoConfigComponent, isStandalone: true, selector: "site-seo-config", usesInheritance: true, ngImport: i0, template: "<form [formGroup]=\"formEntity\">\n <div formGroupName=\"configuration\">\n <div class=\"mb-3\">\n <label class=\"form-label\">{{ 'FlexFieldsSite::Seo:MetaTitleCharLimit' | abpLocalization }}</label>\n <input type=\"number\" class=\"form-control\" formControlName=\"Seo.MetaTitleCharLimit\" min=\"1\" />\n </div>\n <div class=\"mb-3\">\n <label class=\"form-label\">{{ 'FlexFieldsSite::Seo:MetaDescriptionCharLimit' | abpLocalization }}</label>\n <input\n type=\"number\"\n class=\"form-control\"\n formControlName=\"Seo.MetaDescriptionCharLimit\"\n min=\"1\"\n />\n </div>\n <div class=\"alert alert-light border py-2 mb-0\">\n <small class=\"text-muted\">{{ 'FlexFieldsSite::Seo:CharLimitAdvisoryHint' | abpLocalization }}</small>\n </div>\n </div>\n</form>\n", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.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$1.NumberValueAccessor, selector: "input[type=number]:not([ngNoCva])[formControlName],input[type=number]:not([ngNoCva])[formControl],input[type=number]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1$1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i3.ValidationGroupDirective, selector: "[formGroup],[formGroupName]", exportAs: ["validationGroup"] }, { kind: "directive", type: i3.ValidationDirective, selector: "[formControl],[formControlName]", exportAs: ["validationDirective"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "pipe", type: i4.LocalizationPipe, name: "abpLocalization" }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
1545
+ }
1546
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SeoConfigComponent, decorators: [{
1547
+ type: Component,
1548
+ args: [{ selector: 'site-seo-config', changeDetection: ChangeDetectionStrategy.Eager, imports: [CoreModule, ReactiveFormsModule], template: "<form [formGroup]=\"formEntity\">\n <div formGroupName=\"configuration\">\n <div class=\"mb-3\">\n <label class=\"form-label\">{{ 'FlexFieldsSite::Seo:MetaTitleCharLimit' | abpLocalization }}</label>\n <input type=\"number\" class=\"form-control\" formControlName=\"Seo.MetaTitleCharLimit\" min=\"1\" />\n </div>\n <div class=\"mb-3\">\n <label class=\"form-label\">{{ 'FlexFieldsSite::Seo:MetaDescriptionCharLimit' | abpLocalization }}</label>\n <input\n type=\"number\"\n class=\"form-control\"\n formControlName=\"Seo.MetaDescriptionCharLimit\"\n min=\"1\"\n />\n </div>\n <div class=\"alert alert-light border py-2 mb-0\">\n <small class=\"text-muted\">{{ 'FlexFieldsSite::Seo:CharLimitAdvisoryHint' | abpLocalization }}</small>\n </div>\n </div>\n</form>\n" }]
1549
+ }] });
1550
+
1551
+ /**
1552
+ * Where the share image is picked from and the size it is shared at (GitHub issue #72).
1553
+ *
1554
+ * The container mirrors `SiteFileContainerNames.Images` on the server - fixed, not a field setting: the
1555
+ * share image is always a picture, and `images` is the container whose policy is pictures only.
1556
+ * The size mirrors `OpenGraphConsts`: the server emits a file-library `og:image` cropped to exactly
1557
+ * this, so previewing it at the same crop shows the editor what a shared link will look like.
1558
+ */
1559
+ const OG_IMAGE_CONTAINER_NAME = 'images';
1560
+ const OG_IMAGE_WIDTH = 1200;
1561
+ const OG_IMAGE_HEIGHT = 630;
1562
+
1563
+ /**
1564
+ * Edits the value of a `Seo` field.
1565
+ *
1566
+ * Unlike every built-in field type, this binds a **`FormGroup`** rather than a single control: the
1567
+ * value is a composite object, and the group's shape is exactly `SeoFieldValue`, so what the form
1568
+ * produces round-trips back to the server unchanged.
1569
+ *
1570
+ * **No sub-field is individually required, even when the field usage is.** The server's
1571
+ * `SeoFieldType.Validate` only rejects a *missing bundle*; SEO fields are supplementary and the
1572
+ * platform falls back to defaults for whatever is left blank (总体设计 §5.3). Since a `FormGroup`
1573
+ * always produces an object, a required Seo field is satisfied by being rendered at all - adding
1574
+ * `Validators.required` to the parts would enforce something the server does not.
1575
+ *
1576
+ * **The share image is picked, not typed** (GitHub issue #72): the file library's modal, fixed to the
1577
+ * `images` container, uploads or selects one file and its `url` is what `ogImage` stores - still a
1578
+ * plain string, so a value an MCP client wrote (an external address included) shows and can be replaced
1579
+ * or removed the same way.
1580
+ */
1581
+ class SeoControlComponent extends FieldTypeControlBase {
1582
+ constructor() {
1583
+ super(...arguments);
1584
+ this.ogImageContainerName = OG_IMAGE_CONTAINER_NAME;
1585
+ this.ogImageWidth = OG_IMAGE_WIDTH;
1586
+ this.ogImageHeight = OG_IMAGE_HEIGHT;
1587
+ this.ogImagePickerOpen = false;
1588
+ }
1589
+ configurationDefaults() {
1590
+ return new SeoConfiguration();
1591
+ }
1592
+ /**
1593
+ * Untyped on purpose. `FieldTypeControlBase.createControl` is declared to return
1594
+ * `AbstractControl<any, any, any>`, and a typed `FormGroup<{...}>` is not assignable to it - typed
1595
+ * forms fix `TRawValue` to the group's own shape. Every built-in field type returns a single
1596
+ * control, so this is the first one to hit it.
1597
+ */
1598
+ createControl() {
1599
+ const value = this.currentValue();
1600
+ return new UntypedFormGroup({
1601
+ metaTitle: new UntypedFormControl(value.metaTitle ?? ''),
1602
+ metaDescription: new UntypedFormControl(value.metaDescription ?? ''),
1603
+ ogImage: new UntypedFormControl(value.ogImage ?? ''),
1604
+ noIndex: new UntypedFormControl(value.noIndex ?? false),
1605
+ });
1606
+ }
1607
+ /** The bound group, for the template's counters. */
1608
+ get seoGroup() {
1609
+ return this.fieldControl;
1610
+ }
1611
+ /** The stored share image address, or '' when there is none. */
1612
+ get ogImage() {
1613
+ return String(this.seoGroup?.get('ogImage')?.value ?? '');
1614
+ }
1615
+ onOgImagePicked(files) {
1616
+ const url = files[0]?.url;
1617
+ if (url) {
1618
+ this.setOgImage(url);
1619
+ }
1620
+ }
1621
+ removeOgImage() {
1622
+ this.setOgImage('');
1623
+ }
1624
+ get metaTitleLength() {
1625
+ return String(this.seoGroup?.get('metaTitle')?.value ?? '').length;
1626
+ }
1627
+ get metaDescriptionLength() {
1628
+ return String(this.seoGroup?.get('metaDescription')?.value ?? '').length;
1629
+ }
1630
+ get metaTitleCharLimit() {
1631
+ return Number(this.fieldValue?.field.configuration['Seo.MetaTitleCharLimit'] ?? 60);
1632
+ }
1633
+ get metaDescriptionCharLimit() {
1634
+ return Number(this.fieldValue?.field.configuration['Seo.MetaDescriptionCharLimit'] ?? 160);
1635
+ }
1636
+ setOgImage(url) {
1637
+ const control = this.seoGroup?.get('ogImage');
1638
+ control?.setValue(url);
1639
+ control?.markAsDirty();
1640
+ }
1641
+ /**
1642
+ * A fresh value is a live object; one that has round-tripped through storage arrives as parsed JSON.
1643
+ * Anything else - the base class seeds `''` when there is no value - means "nothing stored yet".
1644
+ */
1645
+ currentValue() {
1646
+ const value = this.selectedValue;
1647
+ return value && typeof value === 'object' ? value : {};
1648
+ }
1649
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SeoControlComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
1650
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.2", type: SeoControlComponent, isStandalone: true, selector: "site-seo-control", usesInheritance: true, ngImport: i0, template: "<div [formGroup]=\"entityForm\">\n <div [formGroupName]=\"parentField\">\n <fieldset class=\"mb-3\" [formGroupName]=\"fieldValue.field.name\">\n <legend class=\"form-label h6\" *ngIf=\"fieldValue.field.displayName\">\n {{ fieldValue.field.displayName }}\n </legend>\n <small class=\"form-text text-muted d-block mb-2\" *ngIf=\"fieldValue.field.description\">\n {{ fieldValue.field.description }}\n </small>\n\n <div class=\"mb-3\">\n <label class=\"form-label\" [for]=\"fieldValue.field.name + '-meta-title'\">\n {{ 'FlexFieldsSite::Seo:MetaTitle' | abpLocalization }}\n </label>\n <input\n type=\"text\"\n class=\"form-control\"\n formControlName=\"metaTitle\"\n [id]=\"fieldValue.field.name + '-meta-title'\"\n />\n <small\n class=\"form-text d-block\"\n [class.text-muted]=\"metaTitleLength <= metaTitleCharLimit\"\n [class.text-warning]=\"metaTitleLength > metaTitleCharLimit\"\n >\n {{ metaTitleLength }} / {{ metaTitleCharLimit }}\n <ng-container *ngIf=\"metaTitleLength > metaTitleCharLimit\">\n \u2014 {{ 'FlexFieldsSite::Seo:OverCharLimitHint' | abpLocalization }}\n </ng-container>\n </small>\n </div>\n\n <div class=\"mb-3\">\n <label class=\"form-label\" [for]=\"fieldValue.field.name + '-meta-description'\">\n {{ 'FlexFieldsSite::Seo:MetaDescription' | abpLocalization }}\n </label>\n <textarea\n class=\"form-control\"\n formControlName=\"metaDescription\"\n rows=\"3\"\n [id]=\"fieldValue.field.name + '-meta-description'\"\n ></textarea>\n <small\n class=\"form-text d-block\"\n [class.text-muted]=\"metaDescriptionLength <= metaDescriptionCharLimit\"\n [class.text-warning]=\"metaDescriptionLength > metaDescriptionCharLimit\"\n >\n {{ metaDescriptionLength }} / {{ metaDescriptionCharLimit }}\n <ng-container *ngIf=\"metaDescriptionLength > metaDescriptionCharLimit\">\n \u2014 {{ 'FlexFieldsSite::Seo:OverCharLimitHint' | abpLocalization }}\n </ng-container>\n </small>\n </div>\n\n <div class=\"mb-3\">\n <label class=\"form-label d-block\">\n {{ 'FlexFieldsSite::Seo:OgImage' | abpLocalization }}\n </label>\n @if (ogImage) {\n <div class=\"mb-2\">\n <site-file-preview\n width=\"240px\"\n type=\"image/*\"\n [src]=\"ogImage\"\n [name]=\"'FlexFieldsSite::Seo:OgImage' | abpLocalization\"\n [resizeWidth]=\"ogImageWidth\"\n [resizeHeight]=\"ogImageHeight\"\n ></site-file-preview>\n </div>\n }\n <div>\n <button type=\"button\" class=\"btn btn-sm btn-outline-primary\" (click)=\"ogImagePickerOpen = true\">\n <i class=\"fa fa-image me-1\" aria-hidden=\"true\"></i>\n {{ 'FlexFieldsSite::Seo:SelectOgImage' | abpLocalization }}\n </button>\n @if (ogImage) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary ms-2\" (click)=\"removeOgImage()\">\n <i class=\"fa fa-trash me-1\" aria-hidden=\"true\"></i>\n {{ 'FlexFieldsSite::Seo:RemoveOgImage' | abpLocalization }}\n </button>\n }\n </div>\n <small class=\"form-text text-muted d-block\">\n {{ 'FlexFieldsSite::Seo:OgImageHint' | abpLocalization }}\n </small>\n <site-file-modal\n [(visible)]=\"ogImagePickerOpen\"\n [multiple]=\"false\"\n [fileContainerName]=\"ogImageContainerName\"\n (selectFilefn)=\"onOgImagePicked($event)\"\n ></site-file-modal>\n </div>\n\n <div class=\"form-check\">\n <input\n type=\"checkbox\"\n class=\"form-check-input\"\n formControlName=\"noIndex\"\n [id]=\"fieldValue.field.name + '-no-index'\"\n />\n <label class=\"form-check-label\" [for]=\"fieldValue.field.name + '-no-index'\">\n {{ 'FlexFieldsSite::Seo:NoIndex' | abpLocalization }}\n </label>\n <small class=\"form-text text-muted d-block\">\n {{ 'FlexFieldsSite::Seo:NoIndexHint' | abpLocalization }}\n </small>\n </div>\n </fieldset>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$1.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$1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1$1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i3.ValidationGroupDirective, selector: "[formGroup],[formGroupName]", exportAs: ["validationGroup"] }, { kind: "directive", type: i3.ValidationDirective, selector: "[formControl],[formControlName]", exportAs: ["validationDirective"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: FileModalComponent, selector: "site-file-modal", inputs: ["fileContainerName", "multiple", "limit", "visible", "selectPickerFile"], outputs: ["visibleChange", "selectFilefn"] }, { kind: "component", type: FilePreviewComponent, selector: "site-file-preview", inputs: ["width", "src", "type", "name", "className", "resizeWidth", "resizeHeight"] }, { kind: "pipe", type: i4.LocalizationPipe, name: "abpLocalization" }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
1651
+ }
1652
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SeoControlComponent, decorators: [{
1653
+ type: Component,
1654
+ args: [{ selector: 'site-seo-control', changeDetection: ChangeDetectionStrategy.Eager, imports: [CoreModule, ReactiveFormsModule, FileModalComponent, FilePreviewComponent], template: "<div [formGroup]=\"entityForm\">\n <div [formGroupName]=\"parentField\">\n <fieldset class=\"mb-3\" [formGroupName]=\"fieldValue.field.name\">\n <legend class=\"form-label h6\" *ngIf=\"fieldValue.field.displayName\">\n {{ fieldValue.field.displayName }}\n </legend>\n <small class=\"form-text text-muted d-block mb-2\" *ngIf=\"fieldValue.field.description\">\n {{ fieldValue.field.description }}\n </small>\n\n <div class=\"mb-3\">\n <label class=\"form-label\" [for]=\"fieldValue.field.name + '-meta-title'\">\n {{ 'FlexFieldsSite::Seo:MetaTitle' | abpLocalization }}\n </label>\n <input\n type=\"text\"\n class=\"form-control\"\n formControlName=\"metaTitle\"\n [id]=\"fieldValue.field.name + '-meta-title'\"\n />\n <small\n class=\"form-text d-block\"\n [class.text-muted]=\"metaTitleLength <= metaTitleCharLimit\"\n [class.text-warning]=\"metaTitleLength > metaTitleCharLimit\"\n >\n {{ metaTitleLength }} / {{ metaTitleCharLimit }}\n <ng-container *ngIf=\"metaTitleLength > metaTitleCharLimit\">\n \u2014 {{ 'FlexFieldsSite::Seo:OverCharLimitHint' | abpLocalization }}\n </ng-container>\n </small>\n </div>\n\n <div class=\"mb-3\">\n <label class=\"form-label\" [for]=\"fieldValue.field.name + '-meta-description'\">\n {{ 'FlexFieldsSite::Seo:MetaDescription' | abpLocalization }}\n </label>\n <textarea\n class=\"form-control\"\n formControlName=\"metaDescription\"\n rows=\"3\"\n [id]=\"fieldValue.field.name + '-meta-description'\"\n ></textarea>\n <small\n class=\"form-text d-block\"\n [class.text-muted]=\"metaDescriptionLength <= metaDescriptionCharLimit\"\n [class.text-warning]=\"metaDescriptionLength > metaDescriptionCharLimit\"\n >\n {{ metaDescriptionLength }} / {{ metaDescriptionCharLimit }}\n <ng-container *ngIf=\"metaDescriptionLength > metaDescriptionCharLimit\">\n \u2014 {{ 'FlexFieldsSite::Seo:OverCharLimitHint' | abpLocalization }}\n </ng-container>\n </small>\n </div>\n\n <div class=\"mb-3\">\n <label class=\"form-label d-block\">\n {{ 'FlexFieldsSite::Seo:OgImage' | abpLocalization }}\n </label>\n @if (ogImage) {\n <div class=\"mb-2\">\n <site-file-preview\n width=\"240px\"\n type=\"image/*\"\n [src]=\"ogImage\"\n [name]=\"'FlexFieldsSite::Seo:OgImage' | abpLocalization\"\n [resizeWidth]=\"ogImageWidth\"\n [resizeHeight]=\"ogImageHeight\"\n ></site-file-preview>\n </div>\n }\n <div>\n <button type=\"button\" class=\"btn btn-sm btn-outline-primary\" (click)=\"ogImagePickerOpen = true\">\n <i class=\"fa fa-image me-1\" aria-hidden=\"true\"></i>\n {{ 'FlexFieldsSite::Seo:SelectOgImage' | abpLocalization }}\n </button>\n @if (ogImage) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary ms-2\" (click)=\"removeOgImage()\">\n <i class=\"fa fa-trash me-1\" aria-hidden=\"true\"></i>\n {{ 'FlexFieldsSite::Seo:RemoveOgImage' | abpLocalization }}\n </button>\n }\n </div>\n <small class=\"form-text text-muted d-block\">\n {{ 'FlexFieldsSite::Seo:OgImageHint' | abpLocalization }}\n </small>\n <site-file-modal\n [(visible)]=\"ogImagePickerOpen\"\n [multiple]=\"false\"\n [fileContainerName]=\"ogImageContainerName\"\n (selectFilefn)=\"onOgImagePicked($event)\"\n ></site-file-modal>\n </div>\n\n <div class=\"form-check\">\n <input\n type=\"checkbox\"\n class=\"form-check-input\"\n formControlName=\"noIndex\"\n [id]=\"fieldValue.field.name + '-no-index'\"\n />\n <label class=\"form-check-label\" [for]=\"fieldValue.field.name + '-no-index'\">\n {{ 'FlexFieldsSite::Seo:NoIndex' | abpLocalization }}\n </label>\n <small class=\"form-text text-muted d-block\">\n {{ 'FlexFieldsSite::Seo:NoIndexHint' | abpLocalization }}\n </small>\n </div>\n </fieldset>\n </div>\n</div>\n" }]
1655
+ }] });
1656
+
1657
+ /**
1658
+ * Displays the value of a `Seo` field read-only.
1659
+ *
1660
+ * In a list cell there is room for one thing, so it shows the meta title and flags `noindex` - the one
1661
+ * part of the bundle with a platform-recognized consequence.
1662
+ */
1663
+ class SeoViewComponent {
1664
+ constructor() {
1665
+ this.ogImageWidth = OG_IMAGE_WIDTH;
1666
+ this.ogImageHeight = OG_IMAGE_HEIGHT;
1667
+ /** Renders bare, without the label wrapper, for use inside a table cell. */
1668
+ this.showInList = false;
1669
+ this.value = '';
1670
+ }
1671
+ get seo() {
1672
+ return this.value && typeof this.value === 'object' ? this.value : {};
1673
+ }
1674
+ get hasValue() {
1675
+ const { metaTitle, metaDescription, ogImage, noIndex } = this.seo;
1676
+ return !!(metaTitle || metaDescription || ogImage || noIndex);
1677
+ }
1678
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SeoViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1679
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.2", type: SeoViewComponent, isStandalone: true, selector: "site-seo-view", inputs: { showInList: "showInList", fields: "fields", type: "type", value: "value" }, ngImport: i0, template: "@if (showInList) {\n @if (hasValue) {\n <span>{{ seo.metaTitle || ('FlexFieldsSite::Seo:NoMetaTitle' | abpLocalization) }}</span>\n @if (seo.noIndex) {\n <span class=\"badge bg-warning text-dark ms-1\">{{ 'FlexFieldsSite::Seo:NoIndex' | abpLocalization }}</span>\n }\n } @else {\n ---\n }\n} @else {\n <div class=\"mb-3\">\n <label class=\"form-label\" *ngIf=\"fields?.field?.displayName\">\n {{ fields.field.displayName }}\n </label>\n <dl class=\"row mb-0\">\n <dt class=\"col-sm-4\">{{ 'FlexFieldsSite::Seo:MetaTitle' | abpLocalization }}</dt>\n <dd class=\"col-sm-8\">{{ seo.metaTitle || '---' }}</dd>\n <dt class=\"col-sm-4\">{{ 'FlexFieldsSite::Seo:MetaDescription' | abpLocalization }}</dt>\n <dd class=\"col-sm-8\">{{ seo.metaDescription || '---' }}</dd>\n <dt class=\"col-sm-4\">{{ 'FlexFieldsSite::Seo:OgImage' | abpLocalization }}</dt>\n <dd class=\"col-sm-8\">\n @if (seo.ogImage) {\n <site-file-preview\n width=\"160px\"\n type=\"image/*\"\n [src]=\"seo.ogImage\"\n [name]=\"'FlexFieldsSite::Seo:OgImage' | abpLocalization\"\n [resizeWidth]=\"ogImageWidth\"\n [resizeHeight]=\"ogImageHeight\"\n ></site-file-preview>\n } @else {\n ---\n }\n </dd>\n <dt class=\"col-sm-4\">{{ 'FlexFieldsSite::Seo:NoIndex' | abpLocalization }}</dt>\n <dd class=\"col-sm-8\">\n {{ (seo.noIndex ? 'AbpUi::Yes' : 'AbpUi::No') | abpLocalization }}\n </dd>\n </dl>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: FilePreviewComponent, selector: "site-file-preview", inputs: ["width", "src", "type", "name", "className", "resizeWidth", "resizeHeight"] }, { kind: "pipe", type: i4.LocalizationPipe, name: "abpLocalization" }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
1680
+ }
1681
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SeoViewComponent, decorators: [{
1682
+ type: Component,
1683
+ args: [{ selector: 'site-seo-view', changeDetection: ChangeDetectionStrategy.Eager, imports: [CoreModule, FilePreviewComponent], template: "@if (showInList) {\n @if (hasValue) {\n <span>{{ seo.metaTitle || ('FlexFieldsSite::Seo:NoMetaTitle' | abpLocalization) }}</span>\n @if (seo.noIndex) {\n <span class=\"badge bg-warning text-dark ms-1\">{{ 'FlexFieldsSite::Seo:NoIndex' | abpLocalization }}</span>\n }\n } @else {\n ---\n }\n} @else {\n <div class=\"mb-3\">\n <label class=\"form-label\" *ngIf=\"fields?.field?.displayName\">\n {{ fields.field.displayName }}\n </label>\n <dl class=\"row mb-0\">\n <dt class=\"col-sm-4\">{{ 'FlexFieldsSite::Seo:MetaTitle' | abpLocalization }}</dt>\n <dd class=\"col-sm-8\">{{ seo.metaTitle || '---' }}</dd>\n <dt class=\"col-sm-4\">{{ 'FlexFieldsSite::Seo:MetaDescription' | abpLocalization }}</dt>\n <dd class=\"col-sm-8\">{{ seo.metaDescription || '---' }}</dd>\n <dt class=\"col-sm-4\">{{ 'FlexFieldsSite::Seo:OgImage' | abpLocalization }}</dt>\n <dd class=\"col-sm-8\">\n @if (seo.ogImage) {\n <site-file-preview\n width=\"160px\"\n type=\"image/*\"\n [src]=\"seo.ogImage\"\n [name]=\"'FlexFieldsSite::Seo:OgImage' | abpLocalization\"\n [resizeWidth]=\"ogImageWidth\"\n [resizeHeight]=\"ogImageHeight\"\n ></site-file-preview>\n } @else {\n ---\n }\n </dd>\n <dt class=\"col-sm-4\">{{ 'FlexFieldsSite::Seo:NoIndex' | abpLocalization }}</dt>\n <dd class=\"col-sm-8\">\n {{ (seo.noIndex ? 'AbpUi::Yes' : 'AbpUi::No') | abpLocalization }}\n </dd>\n </dl>\n </div>\n}\n" }]
1684
+ }], propDecorators: { showInList: [{
1685
+ type: Input
1686
+ }], fields: [{
1687
+ type: Input
1688
+ }], type: [{
1689
+ type: Input
1690
+ }], value: [{
1691
+ type: Input
1692
+ }] } });
1693
+
1694
+ /** The registration key. Must equal `SeoFieldNames.FieldTypeName` on the server. */
1695
+ const SEO_FIELD_TYPE_NAME = 'Seo';
1696
+ /**
1697
+ * Site's seventh field type, alongside the six the flex-fields library ships. The server registers
1698
+ * `SeoFieldType` through DI automatically; the client has no equivalent discovery, so it is registered
1699
+ * here and wired in through `provideSite()`.
1700
+ *
1701
+ * This is not optional wiring: `seo` is a platform-preset seeded field, so a fresh install already has
1702
+ * one. Without this registration `FieldTypeResolver.get('Seo')` throws and both the field designer and
1703
+ * the content editor break on first use.
1704
+ *
1705
+ * **No search component.** `SeoFieldType.IndexValueType` is null - the value is a composite object with
1706
+ * no typed index column to project into - so a `Seo` field can never be searched and must not offer a
1707
+ * filter control.
1708
+ *
1709
+ * `displayNameKey` reads `FlexFieldsSite::...`, not `Site::...`: `SeoFieldType` moved from
1710
+ * `Dignite.Site.Domain` into `Dignite.FlexFields.Site` alongside Content/Matrix/Table (GitHub issue
1711
+ * #49), and its `LocalizationResource` moved with it.
1712
+ */
1713
+ const SEO_FIELD_TYPE = {
1714
+ name: SEO_FIELD_TYPE_NAME,
1715
+ displayNameKey: 'FlexFieldsSite::FieldType:Seo',
1716
+ configComponent: SeoConfigComponent,
1717
+ controlComponent: SeoControlComponent,
1718
+ viewComponent: SeoViewComponent,
1719
+ };
1720
+
1721
+ /**
1722
+ * Creates a short-lived object URL for a Blob so previews can reference the
1723
+ * browser-managed blob instead of holding a base64 copy in memory. Callers own
1724
+ * the returned URL and must revoke it (URL.revokeObjectURL) when it is no longer
1725
+ * needed to avoid leaking the underlying blob.
1726
+ */
1727
+ class ObjectUrlService {
1728
+ get(file) {
1729
+ return URL.createObjectURL(file);
1730
+ }
1731
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ObjectUrlService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1732
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ObjectUrlService, providedIn: 'root' }); }
1733
+ }
1734
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ObjectUrlService, decorators: [{
1735
+ type: Injectable,
1736
+ args: [{
1737
+ providedIn: 'root',
1738
+ }]
1739
+ }] });
1740
+
1741
+ /* eslint-disable @angular-eslint/component-selector */
1742
+ class FileUploadComponent {
1743
+ constructor(formatFileSizePipe, objectUrlService, fileAdminService) {
1744
+ this.formatFileSizePipe = formatFileSizePipe;
1745
+ this.objectUrlService = objectUrlService;
1746
+ this.fileAdminService = fileAdminService;
1747
+ this._fileContainerName = '';
1748
+ this.multiple = true;
1749
+ this.fileDataChange = new EventEmitter();
1750
+ /**
1751
+ * A signal, not a plain field: the container's real limit arrives in an HTTP callback, and a plain-field
1752
+ * write there never marks a host's OnPush ancestors for check, so the "exceeds the maximum size" hint
1753
+ * would keep showing the pre-fetch 1MB default until an unrelated event.
1754
+ */
1755
+ this.sizeLimit = signal(1048576, /* @ts-ignore */
1756
+ ...(ngDevMode ? [{ debugName: "sizeLimit" }] : /* istanbul ignore next */ []));
1757
+ this.filesTableData = [];
1758
+ this.previewObjectUrls = new Set();
1759
+ this.deletedUploadedFiles = [];
1760
+ }
1761
+ /**
1762
+ * No default: an unconfigured container is not silently treated as any particular one - the
1763
+ * upload input just doesn't render until a container is set (see the template).
1764
+ */
1765
+ set fileContainerName(v) {
1766
+ this._fileContainerName = v ?? '';
1767
+ if (!v)
1768
+ return;
1769
+ // Reflects the container's actual server-side limit once known, replacing the pre-fetch
1770
+ // default of 1MB below.
1771
+ this.fileAdminService.getContainerConfiguration(v).subscribe(res => {
1772
+ if (res?.maxBlobSize > 0) {
1773
+ this.sizeLimit.set(res.maxBlobSize);
1774
+ }
1775
+ });
1776
+ }
1777
+ get fileContainerName() {
1778
+ return this._fileContainerName;
1779
+ }
1780
+ /** Already-uploaded files to show pre-populated in the table, e.g. when editing an existing record. */
1781
+ set fileData(files) {
1782
+ if (files?.length > 0) {
1783
+ void this.addFiles(files);
1784
+ }
1785
+ }
1786
+ set limit(v) {
1787
+ this.sizeLimit.set(v);
1788
+ }
1789
+ async onFileInputChange(event) {
1790
+ const input = event.target;
1791
+ await this.addFiles(Array.from(input.files ?? []));
1792
+ this.emitChange();
1793
+ }
1794
+ deleteFileTableItem(index, item) {
1795
+ this.filesTableData.splice(index, 1);
1796
+ this.releasePreviewUrl(item.src);
1797
+ if (item.id) {
1798
+ this.deletedUploadedFiles.push(item);
1799
+ }
1800
+ this.emitChange();
1801
+ }
1802
+ async addFiles(files) {
1803
+ for (const file of files) {
1804
+ file.fileSize = this.formatFileSizePipe.transform(file.size);
1805
+ // Use a browser-managed object URL instead of retaining a base64 copy in memory.
1806
+ if (!file.src && file instanceof Blob) {
1807
+ const objectUrl = this.objectUrlService.get(file);
1808
+ file.src = objectUrl;
1809
+ this.previewObjectUrls.add(objectUrl);
1810
+ }
1811
+ }
1812
+ this.filesTableData.push(...files);
1813
+ }
1814
+ emitChange() {
1815
+ const theFilesToBeUploaded = this.filesTableData.filter(item => !item.id);
1816
+ const isSubmit = !this.filesTableData.some(item => item.size > this.sizeLimit());
1817
+ this.fileDataChange.emit({
1818
+ theFilesToBeUploaded,
1819
+ deleteTheUploadedFiles: this.deletedUploadedFiles,
1820
+ isSubmit,
1821
+ });
1822
+ }
1823
+ ngOnDestroy() {
1824
+ for (const objectUrl of this.previewObjectUrls) {
1825
+ URL.revokeObjectURL(objectUrl);
1826
+ }
1827
+ this.previewObjectUrls.clear();
1828
+ }
1829
+ releasePreviewUrl(url) {
1830
+ if (!url || !this.previewObjectUrls.delete(url)) {
1831
+ return;
1832
+ }
1833
+ URL.revokeObjectURL(url);
1834
+ }
1835
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FileUploadComponent, deps: [{ token: FormatFileSizePipe }, { token: ObjectUrlService }, { token: FileAdminService }], target: i0.ɵɵFactoryTarget.Component }); }
1836
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.2", type: FileUploadComponent, isStandalone: true, selector: "site-file-upload", inputs: { fileContainerName: "fileContainerName", multiple: "multiple", fileData: "fileData", limit: "limit" }, outputs: { fileDataChange: "fileDataChange" }, providers: [FormatFileSizePipe], viewQueries: [{ propertyName: "fileUploadInput", first: true, predicate: ["fileUploadInput"], descendants: true, static: true }], ngImport: i0, template: "@if (fileContainerName) {\n <div class=\"input-group mb-3\">\n <input type=\"file\" class=\"form-control\" #fileUploadInput [multiple]=\"multiple\" (change)=\"onFileInputChange($event)\">\n </div>\n\n @if (filesTableData.length>0) {\n <table class=\"table\">\n <thead>\n <tr>\n <th scope=\"col\">{{'Site::Files:FileName' | abpLocalization}}</th>\n <th scope=\"col\" style=\"width: 120px;\">{{'Site::Files:FileSize' | abpLocalization}}</th>\n <th scope=\"col\" style=\"width: 50px;\"></th>\n </tr>\n </thead>\n <tbody>\n @for (item of filesTableData; track item; let i = $index) {\n <tr>\n <td>\n <div class=\"d-flex\">\n <div class=\"flex-grow-1\">\n {{item.name}}\n @if (item.size > sizeLimit()) {\n <div class=\"form-text text-danger\">\n {{'Site::Files:ExceedsMaximumSize' |\n abpLocalization:(sizeLimit()|formatFileSize)}}</div>\n }\n </div>\n <div class=\"\" [attr.title]=\"item.name\">\n <site-file-preview [width]=\"'60px'\" [src]=\"item.src\" [name]=\"item.name\" [type]=\"item.type\"></site-file-preview>\n </div>\n </div>\n </td>\n <td>{{item.fileSize}}</td>\n <td>\n <button class=\"btn btn-sm p-0 btn-outline-secondary \" (click.stop)=\"deleteFileTableItem(i,item)\">\n <i class=\"fas fa-trash p-2\"></i>\n </button>\n </td>\n </tr>\n }\n </tbody>\n </table>\n }\n} @else {\n <div class=\"alert alert-warning py-2 px-3 mb-0\">\n {{ 'Site::Files:ContainerNotConfigured' | abpLocalization }}\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i4.StopPropagationDirective, selector: "[click.stop]", outputs: ["click.stop"] }, { kind: "component", type: FilePreviewComponent, selector: "site-file-preview", inputs: ["width", "src", "type", "name", "className", "resizeWidth", "resizeHeight"] }, { kind: "pipe", type: i4.LocalizationPipe, name: "abpLocalization" }, { kind: "pipe", type: FormatFileSizePipe, name: "formatFileSize" }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
1837
+ }
1838
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FileUploadComponent, decorators: [{
1839
+ type: Component,
1840
+ args: [{ selector: 'site-file-upload', changeDetection: ChangeDetectionStrategy.Eager, imports: [CoreModule, FilePreviewComponent, FormatFileSizePipe], providers: [FormatFileSizePipe], template: "@if (fileContainerName) {\n <div class=\"input-group mb-3\">\n <input type=\"file\" class=\"form-control\" #fileUploadInput [multiple]=\"multiple\" (change)=\"onFileInputChange($event)\">\n </div>\n\n @if (filesTableData.length>0) {\n <table class=\"table\">\n <thead>\n <tr>\n <th scope=\"col\">{{'Site::Files:FileName' | abpLocalization}}</th>\n <th scope=\"col\" style=\"width: 120px;\">{{'Site::Files:FileSize' | abpLocalization}}</th>\n <th scope=\"col\" style=\"width: 50px;\"></th>\n </tr>\n </thead>\n <tbody>\n @for (item of filesTableData; track item; let i = $index) {\n <tr>\n <td>\n <div class=\"d-flex\">\n <div class=\"flex-grow-1\">\n {{item.name}}\n @if (item.size > sizeLimit()) {\n <div class=\"form-text text-danger\">\n {{'Site::Files:ExceedsMaximumSize' |\n abpLocalization:(sizeLimit()|formatFileSize)}}</div>\n }\n </div>\n <div class=\"\" [attr.title]=\"item.name\">\n <site-file-preview [width]=\"'60px'\" [src]=\"item.src\" [name]=\"item.name\" [type]=\"item.type\"></site-file-preview>\n </div>\n </div>\n </td>\n <td>{{item.fileSize}}</td>\n <td>\n <button class=\"btn btn-sm p-0 btn-outline-secondary \" (click.stop)=\"deleteFileTableItem(i,item)\">\n <i class=\"fas fa-trash p-2\"></i>\n </button>\n </td>\n </tr>\n }\n </tbody>\n </table>\n }\n} @else {\n <div class=\"alert alert-warning py-2 px-3 mb-0\">\n {{ 'Site::Files:ContainerNotConfigured' | abpLocalization }}\n </div>\n}\n" }]
1841
+ }], ctorParameters: () => [{ type: FormatFileSizePipe }, { type: ObjectUrlService }, { type: FileAdminService }], propDecorators: { fileContainerName: [{
1842
+ type: Input
1843
+ }], multiple: [{
1844
+ type: Input
1845
+ }], fileData: [{
1846
+ type: Input
1847
+ }], fileDataChange: [{
1848
+ type: Output
1849
+ }], limit: [{
1850
+ type: Input
1851
+ }], fileUploadInput: [{
1852
+ type: ViewChild,
1853
+ args: ['fileUploadInput', { static: true }]
1854
+ }] } });
1855
+
1856
+ const IMAGE_TYPES = ['imageBlock', 'imageInline'];
1857
+ /**
1858
+ * Lets a CKEditor field work with Site's images on the `SiteAdmin` API's host while storing them relative.
1859
+ * A stored address is relative (`<img src="/api/site-public/files/...">`, see `SITE_FILE_PATH`), and this
1860
+ * admin UI is served from another origin, where it does not resolve.
1861
+ *
1862
+ * So the editor's **model** holds the absolute address, and only the data the editor hands out is
1863
+ * relative: a plugin converts an image's `src` (`imageBlock`/`imageInline`) on the way in (upcast:
1864
+ * `setData()`, the initial value, a paste) from relative to absolute, and on the way out (data downcast:
1865
+ * `getData()` - the form value that is saved, the Source view, a copy) from absolute back to relative.
1866
+ * Everything CKEditor itself does with the model's `src` then just works: the editing view shows it, and
1867
+ * `ImageUtils.setImageNaturalSizeAttributes` reads an uploaded image's size from it (`width`/`height`).
1868
+ * HTML and Markdown fields alike: both load images into the same model. Links (`<a href>`) are left alone.
1869
+ * The upload provider hands the editor the absolute address for the same reason.
1870
+ *
1871
+ * Registered by `provideSite()` under `CKEDITOR_CONFIG_CONTRIBUTORS` (with `useFactory`, so it can inject);
1872
+ * the pattern is the one in `@dignite/ng.flex-fields-ckeditor`'s README, "Customizing the editor
1873
+ * configuration".
1874
+ */
1875
+ function siteCKEditorConfigContributor() {
1876
+ const fileUrls = inject(SiteFileUrlService);
1877
+ return config => {
1878
+ const apiBase = fileUrls.apiBase;
1879
+ if (!apiBase) {
1880
+ // The UI shares its API's origin: a relative address already loads.
1881
+ return;
1882
+ }
1883
+ const absolutePrefix = apiBase + SITE_FILE_PATH;
1884
+ const toModel = (src) => (src.startsWith(SITE_FILE_PATH) ? apiBase + src : src);
1885
+ const toData = (src) => (src.startsWith(absolutePrefix) ? src.substring(apiBase.length) : src);
1886
+ // A CKEditor 5 plugin can be a plain function of the editor.
1887
+ function SiteFileAddresses(editor) {
1888
+ // In: the image plugin creates the model image, src included, in its own `element:img` converter
1889
+ // (`upcastImg`, normal priority; a <figure> converts its <img> through the same event). This listener
1890
+ // runs after it ('low') and corrects the src of the image it produced.
1891
+ editor.conversion.for('upcast').add(upcastDispatcher => {
1892
+ upcastDispatcher.on('element:img', (evt, data, conversionApi) => {
1893
+ if (!data.modelRange) {
1894
+ return;
1895
+ }
1896
+ for (const item of data.modelRange.getItems({ shallow: true })) {
1897
+ if (!IMAGE_TYPES.some(type => item.is('element', type))) {
1898
+ continue;
1899
+ }
1900
+ const src = item.getAttribute('src');
1901
+ if (typeof src === 'string' && src !== toModel(src)) {
1902
+ conversionApi.writer.setAttribute('src', toModel(src), item);
1903
+ }
1904
+ }
1905
+ }, { priority: 'low' });
1906
+ });
1907
+ // Out: the image plugin's own src converter (`downcastImageAttribute`, registered for both downcast
1908
+ // pipelines) writes the model's src as it is. In the data pipeline only, this one runs first ('high')
1909
+ // and consumes the change, so that one skips it; the editing view keeps the absolute address.
1910
+ editor.conversion.for('dataDowncast').add(downcastDispatcher => {
1911
+ for (const imageType of IMAGE_TYPES) {
1912
+ downcastDispatcher.on(`attribute:src:${imageType}`, (evt, data, conversionApi) => {
1913
+ if (!conversionApi.consumable.consume(data.item, evt.name)) {
1914
+ return;
1915
+ }
1916
+ const view = conversionApi.mapper.toViewElement(data.item);
1917
+ const img = view && editor.plugins.get('ImageUtils').findViewImgElement(view);
1918
+ if (img) {
1919
+ const src = data.attributeNewValue ?? '';
1920
+ conversionApi.writer.setAttribute('src', toData(src), img);
1921
+ }
1922
+ }, { priority: 'high' });
1923
+ }
1924
+ });
1925
+ }
1926
+ // The casts to Emitter above: both dispatchers are Emitters (EmitterMixin), but this workspace compiles
1927
+ // without strictNullChecks, under which ckeditor5's EmitterMixinConstructor<undefined> resolves to the
1928
+ // wrong branch and their types lose `on`.
1929
+ config.extraPlugins = [...(config.extraPlugins ?? []), SiteFileAddresses];
1930
+ };
1931
+ }
1932
+
1933
+ /**
1934
+ * Lets the read-only rich-text view (`ff-ckeditor-view`: a content list cell, say) show Site's images on the
1935
+ * `SiteAdmin` API's host. The stored HTML holds relative addresses (`<img src="/api/site-public/files/...">`,
1936
+ * see `SITE_FILE_PATH`), which do not resolve when this admin UI is served from another origin. The editor
1937
+ * itself is handled by `siteCKEditorConfigContributor`; this is its counterpart for the view, which has no
1938
+ * editor to convert in.
1939
+ *
1940
+ * Only `src` is rewritten, in the double- and single-quoted spellings; links (`<a href>`) are left alone, as
1941
+ * in the editor. The result is for display only: the view never hands HTML back, so nothing saved carries the
1942
+ * host. When the UI shares its API's origin (`apiBase` empty) the HTML comes back untouched.
1943
+ *
1944
+ * Registered by `provideSite()` under `CKEDITOR_DISPLAY_CONTRIBUTORS` (with `useFactory`, so it can inject);
1945
+ * the pattern is the one in `@dignite/ng.flex-fields-ckeditor`'s README, "Customizing the read-only view".
1946
+ */
1947
+ function siteCKEditorDisplayContributor() {
1948
+ const fileUrls = inject(SiteFileUrlService);
1949
+ const relativeSrc = new RegExp(String.raw `(\ssrc\s*=\s*["'])(${escapeRegExp(SITE_FILE_PATH)})`, 'gi');
1950
+ return html => {
1951
+ const apiBase = fileUrls.apiBase;
1952
+ return apiBase ? html.replace(relativeSrc, (_match, prefix, path) => prefix + apiBase + path) : html;
1953
+ };
1954
+ }
1955
+ function escapeRegExp(value) {
1956
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1957
+ }
1958
+
1959
+ /**
1960
+ * Uploads an image inserted into a CKEditor field into Site's file library and hands the editor the
1961
+ * file's public address to embed - the same `/api/site-public/files/...` address a file field stores, so
1962
+ * the image is served to visitors like any other site file. Registered as the `CKEDITOR_UPLOAD_PROVIDER`
1963
+ * by `provideSite()`; the field's `CKEditor.ImagesContainerName` names the container (normally
1964
+ * `images`).
1965
+ *
1966
+ * The editor gets the address on the `SiteAdmin` API's host: its model holds absolute addresses, so it can
1967
+ * show the image and read its size (`width`/`height`); `siteCKEditorConfigContributor` turns it back into
1968
+ * the relative address the server handed out when the editor's data is read and saved.
1969
+ */
1970
+ class SiteCKEditorUploadProvider {
1971
+ constructor() {
1972
+ this.fileAdminService = inject(FileAdminService);
1973
+ this.fileUrls = inject(SiteFileUrlService);
1974
+ }
1975
+ upload(file, containerName) {
1976
+ const body = new FormData();
1977
+ body.append('file', file, file.name);
1978
+ return this.fileAdminService
1979
+ .create({
1980
+ containerName,
1981
+ directoryId: null,
1982
+ // The generated proxy sends `file` as the request body; FormData makes it multipart.
1983
+ file: body,
1984
+ })
1985
+ .pipe(map(result => {
1986
+ if (!result?.url) {
1987
+ throw new Error('The upload succeeded but the server returned no file address.');
1988
+ }
1989
+ return this.fileUrls.toDisplay(result.url);
1990
+ }));
1991
+ }
1992
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SiteCKEditorUploadProvider, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1993
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SiteCKEditorUploadProvider, providedIn: 'root' }); }
1994
+ }
1995
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SiteCKEditorUploadProvider, decorators: [{
1996
+ type: Injectable,
1997
+ args: [{ providedIn: 'root' }]
1998
+ }] });
1999
+
2000
+ // Site's file library UI: the picker a file field and the SEO share image use, the browsing modal behind
2001
+ // it, and the pieces they are built from. There is no stand-alone file-management screen - files are
2002
+ // managed where they are used.
2003
+
2004
+ var FlexFieldQueryOperator;
2005
+ (function (FlexFieldQueryOperator) {
2006
+ FlexFieldQueryOperator[FlexFieldQueryOperator["Equals"] = 0] = "Equals";
2007
+ FlexFieldQueryOperator[FlexFieldQueryOperator["NotEquals"] = 1] = "NotEquals";
2008
+ FlexFieldQueryOperator[FlexFieldQueryOperator["Contains"] = 2] = "Contains";
2009
+ FlexFieldQueryOperator[FlexFieldQueryOperator["GreaterThan"] = 3] = "GreaterThan";
2010
+ FlexFieldQueryOperator[FlexFieldQueryOperator["GreaterThanOrEqual"] = 4] = "GreaterThanOrEqual";
2011
+ FlexFieldQueryOperator[FlexFieldQueryOperator["LessThan"] = 5] = "LessThan";
2012
+ FlexFieldQueryOperator[FlexFieldQueryOperator["LessThanOrEqual"] = 6] = "LessThanOrEqual";
2013
+ FlexFieldQueryOperator[FlexFieldQueryOperator["In"] = 7] = "In";
2014
+ })(FlexFieldQueryOperator || (FlexFieldQueryOperator = {}));
2015
+ const flexFieldQueryOperatorOptions = mapEnumToOptions(FlexFieldQueryOperator);
2016
+
2017
+ var FlexFieldValueType;
2018
+ (function (FlexFieldValueType) {
2019
+ FlexFieldValueType[FlexFieldValueType["String"] = 0] = "String";
2020
+ FlexFieldValueType[FlexFieldValueType["Number"] = 1] = "Number";
2021
+ FlexFieldValueType[FlexFieldValueType["DateTime"] = 2] = "DateTime";
2022
+ FlexFieldValueType[FlexFieldValueType["Boolean"] = 3] = "Boolean";
2023
+ FlexFieldValueType[FlexFieldValueType["Guid"] = 4] = "Guid";
2024
+ })(FlexFieldValueType || (FlexFieldValueType = {}));
2025
+ const flexFieldValueTypeOptions = mapEnumToOptions(FlexFieldValueType);
2026
+
2027
+ var index$g = /*#__PURE__*/Object.freeze({
2028
+ __proto__: null,
2029
+ get FlexFieldQueryOperator () { return FlexFieldQueryOperator; },
2030
+ get FlexFieldValueType () { return FlexFieldValueType; },
2031
+ flexFieldQueryOperatorOptions: flexFieldQueryOperatorOptions,
2032
+ flexFieldValueTypeOptions: flexFieldValueTypeOptions
2033
+ });
2034
+
2035
+ var index$f = /*#__PURE__*/Object.freeze({
2036
+ __proto__: null,
2037
+ FlexFields: index$g
2038
+ });
2039
+
2040
+ var index$e = /*#__PURE__*/Object.freeze({
2041
+ __proto__: null,
2042
+ ContentTypeAdminService: ContentTypeAdminService
2043
+ });
2044
+
2045
+ var index$d = /*#__PURE__*/Object.freeze({
2046
+ __proto__: null,
2047
+ ContentAdminService: ContentAdminService
2048
+ });
2049
+
2050
+ class FieldAdminService {
2051
+ constructor() {
2052
+ this.restService = inject(RestService);
2053
+ this.apiName = 'SiteAdmin';
2054
+ this.create = (input, config) => this.restService.request({
2055
+ method: 'POST',
2056
+ url: '/api/site-admin/fields',
2057
+ body: input,
2058
+ }, { apiName: this.apiName, ...config });
2059
+ this.delete = (id, config) => this.restService.request({
2060
+ method: 'DELETE',
2061
+ url: `/api/site-admin/fields/${id}`,
2062
+ }, { apiName: this.apiName, ...config });
2063
+ this.findByName = (name, config) => this.restService.request({
2064
+ method: 'GET',
2065
+ url: '/api/site-admin/fields/by-name',
2066
+ params: { name },
2067
+ }, { apiName: this.apiName, ...config });
2068
+ this.get = (id, config) => this.restService.request({
2069
+ method: 'GET',
2070
+ url: `/api/site-admin/fields/${id}`,
2071
+ }, { apiName: this.apiName, ...config });
2072
+ this.getFieldTypes = (config) => this.restService.request({
2073
+ method: 'GET',
2074
+ url: '/api/site-admin/fields/field-types',
2075
+ }, { apiName: this.apiName, ...config });
2076
+ this.getList = (input, config) => this.restService.request({
2077
+ method: 'GET',
2078
+ url: '/api/site-admin/fields',
2079
+ params: { filter: input.filter },
2080
+ }, { apiName: this.apiName, ...config });
2081
+ this.rename = (id, input, config) => this.restService.request({
2082
+ method: 'POST',
2083
+ url: `/api/site-admin/fields/${id}/rename`,
2084
+ body: input,
2085
+ }, { apiName: this.apiName, ...config });
2086
+ this.update = (id, input, config) => this.restService.request({
2087
+ method: 'PUT',
2088
+ url: `/api/site-admin/fields/${id}`,
2089
+ body: input,
2090
+ }, { apiName: this.apiName, ...config });
2091
+ }
2092
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FieldAdminService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2093
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FieldAdminService, providedIn: 'root' }); }
2094
+ }
2095
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: FieldAdminService, decorators: [{
2096
+ type: Injectable,
2097
+ args: [{
2098
+ providedIn: 'root',
2099
+ }]
2100
+ }] });
2101
+
2102
+ var index$c = /*#__PURE__*/Object.freeze({
2103
+ __proto__: null,
2104
+ FieldAdminService: FieldAdminService
2105
+ });
2106
+
2107
+ class PageAdminService {
2108
+ constructor() {
2109
+ this.restService = inject(RestService);
2110
+ this.apiName = 'SiteAdmin';
2111
+ this.create = (input, config) => this.restService.request({
2112
+ method: 'POST',
2113
+ url: '/api/site-admin/pages',
2114
+ body: input,
2115
+ }, { apiName: this.apiName, ...config });
2116
+ this.delete = (id, config) => this.restService.request({
2117
+ method: 'DELETE',
2118
+ url: `/api/site-admin/pages/${id}`,
2119
+ }, { apiName: this.apiName, ...config });
2120
+ this.findByName = (name, config) => this.restService.request({
2121
+ method: 'GET',
2122
+ url: '/api/site-admin/pages/by-name',
2123
+ params: { name },
2124
+ }, { apiName: this.apiName, ...config });
2125
+ this.get = (id, config) => this.restService.request({
2126
+ method: 'GET',
2127
+ url: `/api/site-admin/pages/${id}`,
2128
+ }, { apiName: this.apiName, ...config });
2129
+ this.getList = (input, config) => this.restService.request({
2130
+ method: 'GET',
2131
+ url: '/api/site-admin/pages',
2132
+ params: { isActive: input.isActive, filter: input.filter },
2133
+ }, { apiName: this.apiName, ...config });
2134
+ this.update = (id, input, config) => this.restService.request({
2135
+ method: 'PUT',
2136
+ url: `/api/site-admin/pages/${id}`,
2137
+ body: input,
2138
+ }, { apiName: this.apiName, ...config });
2139
+ }
2140
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: PageAdminService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2141
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: PageAdminService, providedIn: 'root' }); }
2142
+ }
2143
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: PageAdminService, decorators: [{
2144
+ type: Injectable,
2145
+ args: [{
2146
+ providedIn: 'root',
2147
+ }]
2148
+ }] });
2149
+
2150
+ var index$b = /*#__PURE__*/Object.freeze({
2151
+ __proto__: null,
2152
+ PageAdminService: PageAdminService
2153
+ });
2154
+
2155
+ class SiteSchemaAdminService {
2156
+ constructor() {
2157
+ this.restService = inject(RestService);
2158
+ this.apiName = 'SiteAdmin';
2159
+ this.get = (config) => this.restService.request({
2160
+ method: 'GET',
2161
+ url: '/api/site-admin/schema',
2162
+ }, { apiName: this.apiName, ...config });
2163
+ }
2164
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SiteSchemaAdminService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2165
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SiteSchemaAdminService, providedIn: 'root' }); }
2166
+ }
2167
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SiteSchemaAdminService, decorators: [{
2168
+ type: Injectable,
2169
+ args: [{
2170
+ providedIn: 'root',
2171
+ }]
2172
+ }] });
2173
+
2174
+ var index$a = /*#__PURE__*/Object.freeze({
2175
+ __proto__: null,
2176
+ SiteSchemaAdminService: SiteSchemaAdminService
2177
+ });
2178
+
2179
+ var index$9 = /*#__PURE__*/Object.freeze({
2180
+ __proto__: null,
2181
+ ContentTypes: index$e,
2182
+ Contents: index$d,
2183
+ Fields: index$c,
2184
+ Files: index$h,
2185
+ Pages: index$b,
2186
+ Schema: index$a
2187
+ });
2188
+
2189
+ var index$8 = /*#__PURE__*/Object.freeze({
2190
+ __proto__: null
2191
+ });
2192
+
2193
+ var ContentStatus;
2194
+ (function (ContentStatus) {
2195
+ ContentStatus[ContentStatus["Draft"] = 0] = "Draft";
2196
+ ContentStatus[ContentStatus["Published"] = 1] = "Published";
2197
+ ContentStatus[ContentStatus["Archived"] = 2] = "Archived";
2198
+ })(ContentStatus || (ContentStatus = {}));
2199
+ const contentStatusOptions = mapEnumToOptions(ContentStatus);
2200
+
2201
+ var index$7 = /*#__PURE__*/Object.freeze({
2202
+ __proto__: null,
2203
+ get ContentStatus () { return ContentStatus; },
2204
+ contentStatusOptions: contentStatusOptions
2205
+ });
2206
+
2207
+ var index$6 = /*#__PURE__*/Object.freeze({
2208
+ __proto__: null
2209
+ });
2210
+
2211
+ var index$5 = /*#__PURE__*/Object.freeze({
2212
+ __proto__: null
2213
+ });
2214
+
2215
+ var index$4 = /*#__PURE__*/Object.freeze({
2216
+ __proto__: null,
2217
+ Admin: index$9,
2218
+ ContentTypes: index$8,
2219
+ Contents: index$7,
2220
+ Fields: index$6,
2221
+ Pages: index$5
2222
+ });
2223
+
2224
+ var index$3 = /*#__PURE__*/Object.freeze({
2225
+ __proto__: null,
2226
+ Abp: index$f,
2227
+ Site: index$4
2228
+ });
2229
+
2230
+ var index$2 = /*#__PURE__*/Object.freeze({
2231
+ __proto__: null
2232
+ });
2233
+
2234
+ var index$1 = /*#__PURE__*/Object.freeze({
2235
+ __proto__: null,
2236
+ Content: index$2
2237
+ });
2238
+
2239
+ var index = /*#__PURE__*/Object.freeze({
2240
+ __proto__: null,
2241
+ Abp: index$1
2242
+ });
2243
+
2244
+ /**
2245
+ * Flattens a content-list query into the shape ASP.NET Core's model binder actually reads.
2246
+ *
2247
+ * `GET /api/site-admin/contents` binds `GetContentListInput` from the query string, and that input
2248
+ * carries `FlexFieldConditions: List<FlexFieldQueryCondition>` - a collection of complex objects.
2249
+ * ASP.NET Core binds those only from indexed keys (`flexFieldConditions[0].fieldId=...`), whereas the
2250
+ * generated proxy hands the array straight to `RestService`, which builds an `HttpParams` via
2251
+ * `{ fromObject }`. Angular stringifies each element, so the server receives
2252
+ * `flexFieldConditions=[object Object]` and every condition is silently dropped - no error, just
2253
+ * unfiltered results. Hence this translation rather than using the generated `getList`.
2254
+ *
2255
+ * The flex-fields demo does not need an equivalent because its list endpoint is a POST with a body
2256
+ * (`/api/app/product/search`); Site's is a GET, so the conditions have to survive the query string.
2257
+ */
2258
+ function toContentListParams(input) {
2259
+ const { flexFieldConditions, ...rest } = input;
2260
+ const params = { ...rest };
2261
+ (flexFieldConditions ?? []).forEach((condition, index) => {
2262
+ const prefix = `flexFieldConditions[${index}]`;
2263
+ params[`${prefix}.fieldId`] = condition.fieldId;
2264
+ params[`${prefix}.fieldName`] = condition.fieldName;
2265
+ params[`${prefix}.operator`] = condition.operator;
2266
+ params[`${prefix}.value`] = condition.value;
2267
+ params[`${prefix}.valueType`] = condition.valueType;
2268
+ });
2269
+ return params;
2270
+ }
2271
+
2272
+ /**
2273
+ * Remembers the content list's filters, search conditions and paging across a round trip to the
2274
+ * create/edit page and back.
2275
+ *
2276
+ * `ContentsComponent` is destroyed on every route change - editing a content is a full page
2277
+ * (`content-editor.component.ts`), not a dialog - so without this, `filters`, the applied flex-field
2278
+ * search and the `ListService` it owns would all reset to their defaults every time an admin finishes
2279
+ * editing and returns to the list.
2280
+ *
2281
+ * In-memory rather than the URL: `flexFieldConditions` covers arbitrary per-content-type fields, and
2282
+ * round-tripping that shape through query params would need close to as much code as the search feature
2283
+ * itself. Losing the snapshot on a hard refresh is an acceptable trade for an admin filter, unlike a
2284
+ * shareable resource link.
2285
+ */
2286
+ class ContentListStateService {
2287
+ save(state) {
2288
+ this.snapshot = state;
2289
+ }
2290
+ restore() {
2291
+ return this.snapshot;
2292
+ }
2293
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentListStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2294
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentListStateService, providedIn: 'root' }); }
2295
+ }
2296
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentListStateService, decorators: [{
2297
+ type: Injectable,
2298
+ args: [{ providedIn: 'root' }]
2299
+ }] });
2300
+
2301
+ /**
2302
+ * Stands in for `ContentAdminService.getList` alone. Everything else on the generated proxy is used
2303
+ * directly - only the list call needs `flexFieldConditions` flattened before it reaches `RestService`
2304
+ * (see {@link toContentListParams}). Kept here rather than patched into `proxy/` so that re-running
2305
+ * `abp generate-proxy` cannot quietly revert the fix.
2306
+ */
2307
+ class ContentQueryService {
2308
+ constructor() {
2309
+ this.restService = inject(RestService);
2310
+ this.apiName = 'SiteAdmin';
2311
+ this.getList = (input, config) => this.restService.request({
2312
+ method: 'GET',
2313
+ url: '/api/site-admin/contents',
2314
+ params: toContentListParams(input),
2315
+ }, { apiName: this.apiName, ...config });
2316
+ }
2317
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentQueryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2318
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentQueryService, providedIn: 'root' }); }
2319
+ }
2320
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: ContentQueryService, decorators: [{
2321
+ type: Injectable,
2322
+ args: [{ providedIn: 'root' }]
2323
+ }] });
2324
+
2325
+ /**
2326
+ * The reference data every screen needs but none of them owns: the site's enabled languages, the whole
2327
+ * field library, and which field types can be indexed. Each is fetched once and replayed; call the
2328
+ * matching `invalidate*` after a write that would change it.
2329
+ *
2330
+ * **On the schema endpoint.** `GET /api/site-admin/schema` is keyed by *name* all the way down -
2331
+ * `SiteSchemaContentTypeDto` and `SiteSchemaFieldDto` carry no id. That shape exists for MCP, which
2332
+ * addresses everything by name. The admin UI cannot resolve a content type's fields through it, because
2333
+ * a `ContentDto` identifies its type only by `contentTypeId` (a Guid) and there is nothing to join a
2334
+ * Guid to a name on. So the schema is used here for site-level settings only; field resolution goes
2335
+ * through {@link getFieldsById}, joined against `ContentTypeDto.fields[].fieldId`.
2336
+ */
2337
+ class SiteReferenceDataService {
2338
+ constructor() {
2339
+ this.schemaService = inject(SiteSchemaAdminService);
2340
+ this.fieldService = inject(FieldAdminService);
2341
+ this.contentTypeService = inject(ContentTypeAdminService);
2342
+ }
2343
+ /** Site-level settings. `enabledLanguages[0]` is the default language. */
2344
+ getSchema() {
2345
+ this.schemaCache ??= this.schemaService
2346
+ .get()
2347
+ .pipe(shareReplay({ bufferSize: 1, refCount: false }));
2348
+ return this.schemaCache;
2349
+ }
2350
+ /** The whole field library keyed by id - `ContentTypeFieldDto` carries only `fieldId`. */
2351
+ getFieldsById() {
2352
+ this.fieldsCache ??= this.fieldService
2353
+ .getList({})
2354
+ .pipe(map(result => new Map((result.items ?? []).map(field => [field.id, field]))), shareReplay({ bufferSize: 1, refCount: false }));
2355
+ return this.fieldsCache;
2356
+ }
2357
+ /**
2358
+ * Whether a field type has a query-index slot at all, keyed by field type name. Straight from the
2359
+ * server: the answer is `IFieldType.IndexValueType`, and the Angular flex-fields library deliberately
2360
+ * does not restate it. A type that answers `false` can never be searched however its `searchable` flag
2361
+ * is set - `FlexFieldIndexManagerBase.GetIndexableFieldsAsync` skips it regardless.
2362
+ */
2363
+ getIndexableByFieldType() {
2364
+ this.indexableCache ??= this.fieldService
2365
+ .getFieldTypes()
2366
+ .pipe(map(result => new Map((result.items ?? []).map(type => [type.name ?? '', type.indexable ?? true]))), shareReplay({ bufferSize: 1, refCount: false }));
2367
+ return this.indexableCache;
2368
+ }
2369
+ /**
2370
+ * Every content type, across every page, keyed by id - `ContentDto` carries only `contentTypeId`, and
2371
+ * `ContentTypeAdminService.getListByPage` alone cannot resolve a name for a row whose page is not the
2372
+ * one currently selected in a filter.
2373
+ */
2374
+ getContentTypesById() {
2375
+ this.contentTypesCache ??= this.contentTypeService
2376
+ .getList()
2377
+ .pipe(map(result => new Map((result.items ?? []).map(type => [type.id, type]))), shareReplay({ bufferSize: 1, refCount: false }));
2378
+ return this.contentTypesCache;
2379
+ }
2380
+ invalidateFields() {
2381
+ this.fieldsCache = undefined;
2382
+ }
2383
+ invalidateSchema() {
2384
+ this.schemaCache = undefined;
2385
+ }
2386
+ invalidateContentTypes() {
2387
+ this.contentTypesCache = undefined;
2388
+ }
2389
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SiteReferenceDataService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2390
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SiteReferenceDataService, providedIn: 'root' }); }
2391
+ }
2392
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.2", ngImport: i0, type: SiteReferenceDataService, decorators: [{
2393
+ type: Injectable,
2394
+ args: [{ providedIn: 'root' }]
2395
+ }] });
2396
+
2397
+ /**
2398
+ * No `admin` path segment (unlike Dignite.Cms's `/cms/admin/...`): the public tier has no Angular UI,
2399
+ * so every route here is an admin route and the extra segment would carry no information.
2400
+ *
2401
+ * The parent deliberately has no `pathMatch: 'full'`. It did while this library was a stub, which
2402
+ * would have stopped any child route from ever matching.
2403
+ */
2404
+ const SITE_ROUTES = [
2405
+ {
2406
+ path: '',
2407
+ component: RouterOutletComponent,
2408
+ canActivate: [authGuard, permissionGuard],
2409
+ children: [
2410
+ {
2411
+ path: '',
2412
+ pathMatch: 'full',
2413
+ redirectTo: 'contents',
2414
+ },
2415
+ {
2416
+ path: 'contents',
2417
+ loadComponent: () => import('./dignite-ng.site-contents.component-DKuel8w-.mjs').then(c => c.ContentsComponent),
2418
+ data: { requiredPolicy: eSitePolicyNames.Contents },
2419
+ },
2420
+ {
2421
+ // Editing a content is a page, not a dialog: the left half is a form as long as the content
2422
+ // type defines, which a dialog could only scroll.
2423
+ path: 'contents/create',
2424
+ loadComponent: () => import('./dignite-ng.site-content-editor.component-iFWDeYJ9.mjs').then(c => c.ContentEditorComponent),
2425
+ data: { requiredPolicy: eSitePolicyNames.ContentsCreate },
2426
+ },
2427
+ {
2428
+ path: 'contents/:id/edit',
2429
+ loadComponent: () => import('./dignite-ng.site-content-editor.component-iFWDeYJ9.mjs').then(c => c.ContentEditorComponent),
2430
+ data: { requiredPolicy: eSitePolicyNames.ContentsUpdate },
2431
+ },
2432
+ {
2433
+ path: 'pages',
2434
+ loadComponent: () => import('./dignite-ng.site-pages.component-S9BEgC4W.mjs').then(c => c.PagesComponent),
2435
+ data: { requiredPolicy: eSitePolicyNames.Pages },
2436
+ },
2437
+ {
2438
+ path: 'pages/:pageId/content-types',
2439
+ loadComponent: () => import('./dignite-ng.site-content-types.component-D2jMSSq6.mjs').then(c => c.ContentTypesComponent),
2440
+ data: { requiredPolicy: eSitePolicyNames.ContentTypes },
2441
+ },
2442
+ {
2443
+ path: 'fields',
2444
+ loadComponent: () => import('./dignite-ng.site-fields.component-By_YyfIx.mjs').then(c => c.FieldsComponent),
2445
+ data: { requiredPolicy: eSitePolicyNames.Fields },
2446
+ },
2447
+ ],
2448
+ },
2449
+ ];
2450
+
2451
+ /*
2452
+ * Public API Surface of @dignite/ng.site
2453
+ *
2454
+ * Screen components are intentionally absent: every one of them is reached through `SITE_ROUTES`'
2455
+ * `loadComponent`, and exporting them here would pull all of them into whatever chunk imports this
2456
+ * barrel, undoing the lazy loading.
2457
+ */
2458
+
2459
+ /**
2460
+ * Generated bundle index. Do not edit.
2461
+ */
2462
+
2463
+ export { sizedFileUrl as $, FormatFileSizePipe as A, SEO_FIELD_TYPE as B, ContentAdminService as C, SEO_FIELD_TYPE_NAME as D, SITE_FILE_CONTAINER_NAMES as E, FlexFieldQueryOperator as F, SITE_FILE_PATH as G, SITE_IMAGES_CONTAINER_NAME as H, IMAGE_TYPE_OPTIONS as I, SITE_ROUTES as J, SeoConfigComponent as K, SeoConfiguration as L, MY_FILES_NODE_KEY as M, SeoControlComponent as N, ObjectUrlService as O, PageAdminService as P, SeoViewComponent as Q, SiteCKEditorUploadProvider as R, SiteReferenceDataService as S, SiteFileUrlService as T, index as U, ValidatorsService as V, absoluteFileUrl as W, isMyFilesNode as X, isSiteFileUrl as Y, siteCKEditorConfigContributor as Z, siteCKEditorDisplayContributor as _, ContentQueryService as a, toContentListParams as a0, ContentListStateService as b, ContentTypeAdminService as c, ContentStatus as d, eSitePolicyNames as e, FlexFieldValueType as f, FieldAdminService as g, CONTENT_FIELD_TYPE as h, CONTENT_FIELD_TYPE_NAME as i, ContentConfigComponent as j, ContentConfiguration as k, ContentControlComponent as l, ContentViewComponent as m, index$3 as n, FILE_FIELD_CONFIGURATION_NAMES as o, FILE_FIELD_TYPE as p, FILE_FIELD_TYPE_NAME as q, FileConfigComponent as r, FileControlComponent as s, FileDirectoryTreeComponent as t, FileFieldConfiguration as u, FileModalComponent as v, FilePickerComponent as w, FilePreviewComponent as x, FileUploadComponent as y, FileViewComponent as z };
2464
+ //# sourceMappingURL=dignite-ng.site-dignite-ng.site-CsKxqjY9.mjs.map