scrivito
Version:
Scrivito is a professional, yet easy to use SaaS Enterprise Content Management Service, built for digital agencies and medium to large businesses. It is completely maintenance-free, cost-effective, and has unprecedented performance and security.
388 lines (313 loc) • 10.2 kB
text/typescript
import mapValues from 'lodash-es/mapValues';
import { ComparisonRange, ObjSpaceId, WidgetJson } from 'scrivito_sdk/client';
import { ArgumentError, ScrivitoError, camelCase } from 'scrivito_sdk/common';
import {
Modification,
failIfPerformanceConstraint,
getWidgetModification,
} from 'scrivito_sdk/data';
import * as AttributeSerializer from 'scrivito_sdk/models/attribute_serializer';
import {
ContentValueOrConnection,
ContentValueProvider,
NormalizedBasicAttributeDict,
NormalizedBasicAttributesWithUnknownValues,
NormalizedUnknownAttributeValue,
getContentValue,
getContentValueOrConnection,
isWidgetAttributeValueAndType,
isWidgetlistAttributeValueAndType,
normalizeAttributes,
persistWidgets,
serializeAttributes,
} from 'scrivito_sdk/models/basic_attribute_content';
import {
BasicAttributeValue,
CmsAttributeType,
} from 'scrivito_sdk/models/basic_attribute_types';
import { BasicField } from 'scrivito_sdk/models/basic_field';
import { BasicObj } from 'scrivito_sdk/models/basic_obj';
import { TypeInfo } from 'scrivito_sdk/models/type_info';
import { withBatchedUpdates } from 'scrivito_sdk/state';
export interface BasicWidgetAttributes {
[key: string]: unknown;
}
export interface SerializedWidgetAttributes {
[key: string]: unknown;
}
export type DidPersistCallback = (widget: BasicWidget) => void;
export class BasicWidget implements ContentValueProvider {
static build(id: string, obj: BasicObj): BasicWidget {
return new BasicWidget(id, obj);
}
static newWithSerializedAttributes(
attributes: SerializedWidgetAttributes
): BasicWidget {
const unserializedAttributes: NormalizedBasicAttributeDict = {};
const serializedAttributes: SerializedWidgetAttributes = {};
Object.keys(attributes).forEach((name) => {
const value = attributes[name];
if (name === '_obj_class') {
unserializedAttributes._objClass = [value as string];
return;
}
if (Array.isArray(value)) {
const [type, maybeWidgetData] = value;
if (type === 'widget') {
const widgetData = maybeWidgetData as SerializedWidgetAttributes;
const newWidget = BasicWidget.newWithSerializedAttributes(widgetData);
const attrName = camelCase(name);
unserializedAttributes[attrName] = [newWidget, ['widget']];
return;
}
if (type === 'widgetlist') {
const widgetData = maybeWidgetData as SerializedWidgetAttributes[];
const newWidgets = widgetData.map((serializedWidget) => {
return BasicWidget.newWithSerializedAttributes(serializedWidget);
});
const attrName = camelCase(name);
unserializedAttributes[attrName] = [newWidgets, ['widgetlist']];
return;
}
}
serializedAttributes[name] = value;
});
return new BasicWidget(
undefined,
undefined,
unserializedAttributes,
serializedAttributes
);
}
static create(attributes: BasicWidgetAttributes): BasicWidget {
return new BasicWidget(
undefined,
undefined,
normalizeAttributes(attributes)
);
}
static createWithUnknownValues(
attributes: NormalizedBasicAttributesWithUnknownValues
): BasicWidget {
return new BasicWidget(undefined, undefined, attributes);
}
private readonly attributesToBeSaved?: AttributesToBeSaved;
private onDidPersistCallback?: DidPersistCallback;
constructor(
id: undefined,
obj: undefined,
attributes: NormalizedBasicAttributesWithUnknownValues,
preserializedAttributes?: SerializedWidgetAttributes
);
constructor(
id: string,
obj: BasicObj,
attributes?: undefined,
preserializedAttributes?: undefined
);
constructor(
private _id?: string,
private _obj?: BasicObj,
attributesToBeSaved?: NormalizedBasicAttributesWithUnknownValues,
private readonly preserializedAttributes?: SerializedWidgetAttributes
) {
if (!_obj) {
if (attributesToBeSaved && isAttributesToBeSaved(attributesToBeSaved)) {
this.attributesToBeSaved = attributesToBeSaved;
} else {
throw new ArgumentError(
'Please provide a widget class as the "_objClass" property.'
);
}
}
}
id(): string {
this.failIfNotPersisted();
return this._id!;
}
objClass(): string {
if (this.isPersisted()) {
return this.getAttributeData('_obj_class')!;
}
const [objClass] = this.attributesToBeSaved!._objClass;
return objClass;
}
obj(): BasicObj {
this.failIfNotPersisted();
return this._obj!;
}
objSpaceId(): ObjSpaceId {
return this.obj().objSpaceId();
}
widget(id: string): BasicWidget | null {
return this.obj().widget(id);
}
modification([from, to]: ComparisonRange): Modification {
return getWidgetModification(from, to, this.obj().id(), this.id());
}
get<Type extends CmsAttributeType>(
attributeName: string,
typeInfo: TypeInfo<Type>
): BasicAttributeValue<Type> {
return getContentValue(this, attributeName, typeInfo);
}
getValueOrConnection<Type extends CmsAttributeType>(
attributeName: string,
typeInfo: TypeInfo<Type>
): ContentValueOrConnection<Type> {
return getContentValueOrConnection(this, attributeName, typeInfo);
}
container(): BasicObj | BasicWidget {
failIfPerformanceConstraint(
'for performance reasons, avoid this method when rendering'
);
const containingField = this.containingField();
return containingField ? containingField.getContainer() : this.obj();
}
update(attributes: BasicWidgetAttributes): void {
const normalizedAttributes = normalizeAttributes(attributes);
this.updateWithUnknownValues(normalizedAttributes);
}
updateWithUnknownValues(
attributes: NormalizedBasicAttributesWithUnknownValues
): void {
withBatchedUpdates(() => {
persistWidgets(this.obj(), attributes);
const patch = AttributeSerializer.serialize(attributes);
this.updateSelf(patch);
});
}
insertBefore(widget: BasicWidget): void {
widget.obj().insertWidget(this, { before: widget });
}
insertAfter(widget: BasicWidget): void {
widget.obj().insertWidget(this, { after: widget });
}
delete(): void {
this.obj().deleteWidget(this);
}
copy(): BasicWidget {
if (this.isPersisted()) {
return this.copyPersisted();
}
return this.copyUnpersisted();
}
persistInObjIfNecessary(obj: BasicObj): void {
if (this.isPersisted()) return;
const normalizedAttributes = this.attributesToBeSaved!;
persistWidgets(obj, normalizedAttributes);
const patch = {
...AttributeSerializer.serialize(normalizedAttributes),
...this.preserializedAttributes,
};
this._obj = obj;
this._id = obj.generateWidgetId();
this.updateSelf(patch);
this.executeDidPersistCallback();
}
isPersisted(): boolean {
return !!this._obj;
}
onDidPersist(callback: DidPersistCallback): void {
if (this.isPersisted()) {
throw new ScrivitoError(
'Cannot call "onDidPersist" of an already persisted widget'
);
}
this.onDidPersistCallback = callback;
}
// For test purpose only.
hasOnDidPersistCallback(): boolean {
return !!this.onDidPersistCallback;
}
finishSaving(): Promise<void> {
return this.obj().finishSaving();
}
equals(other: unknown): boolean {
return (
other instanceof BasicWidget &&
this.id() === other.id() &&
this.obj().equals(other.obj())
);
}
containingField():
| BasicField<'widget'>
| BasicField<'widgetlist'>
| undefined {
return this.obj().fieldContainingWidget(this);
}
toPrettyPrint(): string {
return `[object ${this.objClass()} id="${this.id()}" objId="${this.obj().id()}"]`;
}
getAttributeData<Key extends keyof WidgetJson & string>(
attributeName: Key
): WidgetJson[Key] | undefined {
return this.obj().getWidgetAttribute(this.id(), attributeName);
}
getData(): WidgetJson | undefined {
return this.obj().getWidgetData(this.id());
}
// For test purpose only.
getAttributesToBeSaved(): AttributesToBeSaved | undefined {
return this.attributesToBeSaved;
}
private failIfNotPersisted() {
if (!this.isPersisted()) {
throw new ScrivitoError(
'Can not access a new widget before it has been saved.'
);
}
}
private updateSelf(patch: SerializedWidgetAttributes) {
const widgetPoolPatch = { _widgetPool: [{ [this.id()]: patch }] };
this.obj().update(widgetPoolPatch);
}
private executeDidPersistCallback() {
if (this.onDidPersistCallback) {
this.onDidPersistCallback(this);
delete this.onDidPersistCallback;
}
}
private copyPersisted() {
const serializedAttributes = serializeAttributes(this);
return BasicWidget.newWithSerializedAttributes(serializedAttributes);
}
private copyUnpersisted() {
const copy = new BasicWidget(
undefined,
undefined,
mapValues(this.attributesToBeSaved, copyNormalizedValue)
);
if (this.onDidPersistCallback) {
copy.onDidPersist(this.onDidPersistCallback);
}
return copy;
}
}
function copyNormalizedValue(
valueAndType: NormalizedUnknownAttributeValue
): NormalizedUnknownAttributeValue {
if (isWidgetAttributeValueAndType(valueAndType)) {
const [widget, typeInfo] = valueAndType;
return [widget.copy(), typeInfo];
}
if (isWidgetlistAttributeValueAndType(valueAndType)) {
const [value, typeInfo] = valueAndType;
const widgets = Array.isArray(value) ? value : [value];
return [widgets.map((widget) => widget.copy()), typeInfo];
}
// typescript doesn't preserve "tuple-ness" for a copied tuple
return valueAndType.slice(0) as typeof valueAndType;
}
interface AttributesToBeSaved
extends NormalizedBasicAttributesWithUnknownValues {
_objClass: [string];
}
function isAttributesToBeSaved(
attributes: NormalizedBasicAttributesWithUnknownValues
): attributes is AttributesToBeSaved {
const value = attributes._objClass;
if (!value) return false;
const [objClass] = value;
return typeof objClass === 'string';
}