src/app/shared/widgets/buy/search-attributes/search-attributes.component.ts
Properties |
|
allowRange |
allowRange:
|
Type : boolean
|
Optional |
Enables/disables attribute values range, from and to. @default(false) |
attributeUrl |
attributeUrl:
|
Type : String
|
Optional |
used to get attributes in case of editShowAttributes enabled. |
canEditShownAttributes |
canEditShownAttributes:
|
Type : boolean
|
Optional |
Enables/Disables selecting attributes SelectAttributesDialog. @default(false) |
localstorage-attribute-search |
localstorage-attribute-search:
|
Type : String
|
Optional |
Local storage key for edited attributes and their values. |
localstorage-content-visible |
localstorage-content-visible:
|
Type : String
|
Optional |
Local storage key for search attributes visibility state. |
multiLookupsAllowed |
multiLookupsAllowed:
|
Type : boolean
|
Optional |
Enables/disables multilookup option. @default(false) |
translateAttributeDescriptions |
translateAttributeDescriptions:
|
Type : boolean
|
Optional |
Enables/Disables showing attributes description. @default(false) |
import {
combineLatest as observableCombineLatest,
Subject,
ReplaySubject,
} from "rxjs";
import {
onErrorResumeNext,
takeUntil,
mergeMap,
distinctUntilChanged,
map,
flatMap,
} from "rxjs/operators";
import { Component, OnDestroy } from "@angular/core";
import { WidgetframeService } from "../../widgetframe/widgetframe.service";
import {
getOrDefault,
throwIfUndefined,
WidgetConfig,
} from "../../widget.configuration";
import {
WidgetComponent,
WidgetConfiguration,
WidgetConfigure,
WidgetId,
WidgetInput,
WidgetOutput,
} from "../../widget.metadata";
import { NgUnsubscribe } from "../../../ng-unsubscribe";
import * as uriTemplates_ from "uri-templates";
import { Attribute } from "../../../components/edit-attribute/attribute";
import { EditAttributeService } from "../../../components/edit-attribute/edit-attribute.service";
import { HalService } from "../../../components/hal/index";
import { CustomNotificationService } from "../../../components/notification/customnotification.service";
import {
DeletionMode,
Scope,
} from "../../../components/local-storage/local-storage-constants";
import {
LocalStorageEntry,
LocalStorageService,
} from "../../../components/local-storage/local-storage.service";
import { SelectAttributesDialogComponent } from "../../../components/dialog/selectAttributesDialog.component";
import { AppdataStore } from "../../../components/appdata/appdata.store";
import { deepCopy } from "../../../components/util/util.service";
import { FormControl } from "@angular/forms";
import { DialogService } from "../../../components/dialog";
const uriTemplates = uriTemplates_;
export interface SearchAttributesWidgetConfiguration {
/**
* Enables/Disables selecting attributes SelectAttributesDialog. @default(false)
*/
canEditShownAttributes?: boolean;
/**
* Local storage key for edited attributes and their values.
*/
"localstorage-attribute-search"?: String;
/**
* Local storage key for search attributes visibility state.
*/
"localstorage-content-visible"?: String;
/**
* Enables/disables attribute values range, from and to. @default(false)
*/
allowRange?: boolean;
/**
* Enables/Disables showing attributes description. @default(false)
*/
translateAttributeDescriptions?: boolean;
/**
* Enables/disables multilookup option. @default(false)
*/
multiLookupsAllowed?: boolean;
/**
* used to get attributes in case of editShowAttributes enabled.
*/
attributeUrl?: String;
}
declare var jQuery: any;
@WidgetComponent("nm-search-attributes")
@Component({
selector: "nm-search-attributes",
templateUrl: "./search-attributes.component.html",
styleUrls: ["./search-attributes.component.scss"],
providers: [EditAttributeService],
})
export class SearchAttributesWidgetComponent implements OnDestroy {
public cols: any[];
public attributes: Attribute[];
public attributesorg: Attribute[];
public changedAttributes = <any>{};
public localstorageAttributesEntry: LocalStorageEntry;
public localStorageVisibiltyEntry: LocalStorageEntry;
public canEditShownAttributes = false;
public contentVisible: boolean = false;
public allowRange;
public isCollapsible: boolean = false;
public translateAttributeDescriptions = false;
public currentLocale: string;
public inputLink: string;
/**
* By default, attributes of type 'MULTI_LOOKUP' are converted to type 'LOOKUP', for compatibility with some
* search functions. Setting this value to true will allow multiple values to be selected for these attributes.
*/
public multiLookupsAllowed: boolean;
@WidgetConfiguration()
public configuration: WidgetConfig<SearchAttributesWidgetConfiguration>;
/**
* Reloads attributes for a given locale, and attributes url.
*/
@WidgetInput()
public reload = new Subject<any>();
/**
* Emits an array of all edited attributes and their values.
*/
@WidgetOutput("changedAttributesOutput")
public changedAttributesOutput = new ReplaySubject<any>();
/**
* Emits the attribute that is being edited.
*/
@WidgetOutput("changedValueByUser")
public changedValueByUser = new Subject<any>();
/**
* Resets attributes and local storage values.
*/
@WidgetInput()
public reset = new Subject<any>();
/**
* Will call given uri and select the attributes from the address.
*/
@WidgetInput("uri")
public uri = new ReplaySubject<any>(1);
/**
* Input to set the uri that is used for the editShownAttributes.
*/
@WidgetInput("attributeUrl")
public attributeUrl = new Subject();
/**
* Input of preset array of attribute identifiers, used to get attributes.
*/
@WidgetInput()
public data = new Subject();
@WidgetId()
public _id: string;
public unsubscribe = NgUnsubscribe.create();
private attributesLocalstorageEntry: LocalStorageEntry;
constructor(
public _widgetframeService: WidgetframeService,
public _editAttributeService: EditAttributeService,
public _halService: HalService,
private localStorageService: LocalStorageService,
public _notificationService: CustomNotificationService,
private appStore: AppdataStore,
private dialogService: DialogService
) {}
private selectAttributeUrl;
@WidgetConfigure()
protected configureWidget(configuration: WidgetConfig) {
this.changedAttributes = [];
this.localStorageVisibiltyEntry = this.localStorageService.getLocalStorageEntry(
configuration.configuration["localstorage-content-visible"],
Scope.GLOBAL,
DeletionMode.LOGIN
);
this.localstorageAttributesEntry = this.localStorageService.getLocalStorageEntry(
configuration.configuration["localstorage-attribute-search"],
Scope.GLOBAL,
DeletionMode.LOGIN
);
this.selectAttributeUrl = configuration.configuration.attributeUrl;
this.canEditShownAttributes = getOrDefault(
configuration.configuration.editShownAttributes,
false
);
if (this.canEditShownAttributes) {
this.attributesLocalstorageEntry = this.localStorageService.getLocalStorageEntry(
this._id,
Scope.GLOBAL,
DeletionMode.RESET
);
if (this.attributesLocalstorageEntry.exists()) {
this.attributes = JSON.parse(this.attributesLocalstorageEntry.value);
this.recoverChangedAttributes();
}
}
this.allowRange = getOrDefault(
configuration.configuration["allowRange"],
false
);
this.translateAttributeDescriptions =
configuration.configuration.translateAttributeDescriptions || false;
if (this.localStorageVisibiltyEntry.exists()) {
this.contentVisible = this.localStorageVisibiltyEntry.value === "true";
}
this.multiLookupsAllowed = getOrDefault(
configuration.configuration.multiLookupsAllowed,
false
);
this.data.pipe(takeUntil(this.unsubscribe)).subscribe((data) => {
const missingAttributes = [];
let foundAttributes = [];
Object.keys(data).forEach((key) => {
const attribute = this.attributes.find((att) => att.identifier == key);
if (attribute) {
if (attribute.source && attribute.source[0]) {
attribute.source[0] = {};
}
foundAttributes.push(attribute);
} else {
missingAttributes.push(key);
}
});
if (missingAttributes.length !== 0) {
this.appStore
.getAppdata()
.pipe(
map((data) => data.ipim._links.attributes.href),
map(
(data) =>
data +
'?filter=identifier:in:"' +
missingAttributes.join(",") +
'"'
),
flatMap((data) => this._widgetframeService.getData(data)),
map((data) => data._embedded.attributes)
)
.subscribe((entries) => {
foundAttributes = foundAttributes.concat(entries);
this.recoverFromData(data, foundAttributes);
});
} else {
this.recoverFromData(data, foundAttributes);
}
});
this.reset.pipe(takeUntil(this.unsubscribe)).subscribe((data) => {
this.changedAttributes = [];
this.changedAttributesOutput.next(this.changedAttributes);
if (this.localstorageAttributesEntry.exists()) {
this.localstorageAttributesEntry.clear();
}
if (this.attributesorg) {
this.attributes = jQuery.extend(true, [], this.attributesorg);
} else {
if (this.attributes) {
this.attributes.forEach((attribute) => {
if (attribute.source && attribute.source[0]) {
attribute.source[0] = {};
}
});
this.attributes = jQuery.extend(true, [], this.attributes);
}
}
});
this._editAttributeService
.getAttributeChangedEvent()
.pipe(takeUntil(this.unsubscribe))
.subscribe((attribute) => {
const dashIdx = attribute.identifier.indexOf("-");
// The original attribute that was changed, needed to save the from / to
let originalAttribute;
let from;
if (dashIdx !== -1) {
const rootAttributeName =
dashIdx === -1 ? null : attribute.identifier.substring(0, dashIdx);
const att = this.attributes.find(
(att) => att.identifier === rootAttributeName
);
const sub = attribute.identifier.substring(dashIdx + 1);
originalAttribute = attribute;
attribute = att;
from = sub === "from";
}
let dublicateIndex = this.changedAttributes.findIndex(
(item) => item.identifier == attribute.identifier
);
if (dublicateIndex !== -1) {
this.changedAttributes.splice(dublicateIndex, 1);
const isEmpty = this.checkIsAttributeEmpty(attribute);
// The attribute and all of its fake range child attributes are not empty, we can readd it to the changed attributes
if (!isEmpty) {
this.changedAttributes.push(attribute);
}
} else {
// THere is no duplicate, we are save to add this to the changed attributes
this.changedAttributes.push(attribute);
}
this.localstorageAttributesEntry.value = JSON.stringify(
this.changedAttributes
);
this.changedAttributesOutput.next(this.changedAttributes);
this.changedValueByUser.next(attribute);
});
this.attributeUrl
.pipe(takeUntil(this.unsubscribe))
.subscribe((data) => (this.selectAttributeUrl = data));
let source = observableCombineLatest(
this.uri.asObservable(),
this.reload.asObservable().pipe(distinctUntilChanged()),
function (uri, reloadEvent) {
let template = uriTemplates(uri);
let uriParams = {};
// uriParams["context"] = configuration.configuration["context"];
uriParams["locale"] = reloadEvent;
this.currentLocale = reloadEvent;
return template.fill(uriParams);
}
).pipe(
takeUntil(this.unsubscribe),
mergeMap((href) =>
this._widgetframeService
.getData(href)
.pipe(onErrorResumeNext(this.resetWidget()))
)
);
source.subscribe(
(data) => {
for (const attribute of <any>data["_embedded"]["attributes"]) {
if (!this.multiLookupsAllowed && attribute.type === "MULTI_LOOKUP") {
attribute.type = "LOOKUP";
}
}
this.attributes = jQuery.extend(
true,
[],
data["_embedded"]["attributes"]
);
this.attributesorg = jQuery.extend(
true,
[],
data["_embedded"]["attributes"]
);
this.recoverChangedAttributes();
},
(error) => {
console.log(error);
this.attributes = null;
}
);
this.reload.asObservable().subscribe((data) => {
this.currentLocale = data;
});
}
private recoverFromData(data, attributes: Attribute[]) {
this.attributes = [];
this.changedAttributes = [];
Object.keys(data).forEach((key) => {
// We need to deep copy the object, so that the angular change detection will still work while in OnPush (we need to replace the reference)
const attribute = deepCopy(
attributes.find((attribute) => attribute.identifier == key)
);
attribute.source = [{ value: data[key] }];
this.attributes.push(attribute);
this.changedAttributes.push(attribute);
});
this.changedAttributesOutput.next(this.changedAttributes);
if (this.attributesLocalstorageEntry) {
this.attributesLocalstorageEntry.value = JSON.stringify(this.attributes);
}
this.localstorageAttributesEntry.value = JSON.stringify(
this.changedAttributes
);
}
private checkIsAttributeEmpty(attribute: Attribute): boolean {
const isRootEmpty =
!attribute.source[0].value ||
attribute.source[0].value === "" ||
attribute.source.length === 0;
// The root is not empty, it cant be empty
if (!isRootEmpty) {
return false;
}
// The root is empty and it has no range attributes, it must be empty
if (!attribute.from) {
return true;
}
//Check if the children are empty
else
return (
this.checkIsAttributeEmpty(attribute.from) &&
this.checkIsAttributeEmpty(attribute.to)
);
}
private recoverChangedAttributes() {
if (this.localstorageAttributesEntry.exists()) {
this.changedAttributes = JSON.parse(
this.localstorageAttributesEntry.value
);
for (const changedAttribute of this.changedAttributes) {
let dublicate = this.attributes.filter(
(item) => item.identifier == changedAttribute.identifier
);
if (dublicate.length === 1) {
dublicate[0].source = changedAttribute.source;
if (changedAttribute.from) {
dublicate[0].from = changedAttribute.from;
dublicate[0].to = changedAttribute.to;
dublicate[0].displayRange = changedAttribute.displayRange;
}
}
}
this.changedAttributesOutput.next(this.changedAttributes);
}
}
resetWidget() {
return null;
}
ngOnDestroy() {
this.unsubscribe.destroy();
}
toogleContentVisibility() {
this.contentVisible = !this.contentVisible;
this.localStorageVisibiltyEntry.value = this.contentVisible.toString();
}
public editShownAttributes() {
let dialogRef = this.dialogService.open(SelectAttributesDialogComponent, {
minWidth: "900px",
maxWidth: "900px",
height: "755px",
});
dialogRef.componentInstance.preselectedAttributes = this.attributes || [];
dialogRef.componentInstance.currentLocale = this.currentLocale;
dialogRef.componentInstance.attributeUrl = throwIfUndefined(
this.selectAttributeUrl
);
dialogRef.componentInstance.infoText = "infoText.select.attributes";
dialogRef.afterClosed().subscribe((data) => {
if (data) {
let changedAttributesChanged = false;
var i = this.changedAttributes.length;
// If we have a changed attribute that is no longer editable we need to remove it from the list of changedattributes and output the changed attributes
while (i--) {
const entry = this.changedAttributes[i];
const attribute = data.find(
(val) => val.identifier === entry.identifier
);
if (!attribute) {
this.changedAttributes.splice(i, 1);
changedAttributesChanged = true;
}
}
if (changedAttributesChanged) {
this.changedAttributesOutput.next(this.changedAttributes);
}
if (this.attributes) {
data.forEach((entry) => {
if (!this.multiLookupsAllowed && entry.type === "MULTI_LOOKUP") {
entry.type = "LOOKUP";
}
const value = this.attributes.find(
(att) => att.identifier === entry.identifier
);
if (value) {
entry.source = value.source;
}
});
}
this.attributes = data;
this.attributesLocalstorageEntry.value = JSON.stringify(data);
}
});
}
showLabel(attribute) {
return !(this.allowRange && this.attributeSupportsRange(attribute));
}
private attributeSupportsRange(attribute) {
return attribute.type !== "BOOLEAN";
}
public rangeChange(event, attribute: Attribute) {
attribute.displayRange = event.checked;
if (attribute.from) {
//Make sure that the attribute and the change attribute instance are === so that changes to displayRange will be copied to the changed attribute (needed for tooltip)
} else if (event.checked) {
// This will deep copy the source of the root to from and to! If this is a problem we need to cleanse the source before / while copping
const fromClone: Attribute = JSON.parse(JSON.stringify(attribute));
const toClone = JSON.parse(JSON.stringify(attribute));
fromClone.identifier = fromClone.identifier + "-from";
toClone.identifier = toClone.identifier + "-to";
attribute.from = fromClone;
attribute.to = toClone;
}
// Emit change, since changing the slider changes the search
this.changedAttributesOutput.next(this.changedAttributes);
}
public getChangedAttributesForHeader() {
const result = [];
this.changedAttributes.forEach((value) => {
if (value.displayRange) {
let from;
let to;
if (value.from.source[0] && value.from.source[0].value) {
from = value.from.source[0].value;
}
if (value.to.source[0] && value.to.source[0].value) {
to = value.to.source[0].value;
}
let description = from ? from : "?";
if (to) {
description += " - " + to;
}
const fakeValue = {
description: value.description,
source: [
{
description,
},
],
};
result.push(fakeValue);
} else {
result.push(value);
}
});
return result;
}
}