Refactor how widgets are created/edited in the frontend

Creating and editing widgets is now done against the `dimension` object on a widget. This special object is used to translate the widget between the dirty and persisted states.
This commit is contained in:
Travis Ralston 2017-12-13 22:44:20 -07:00
parent fd5e367146
commit 9ff1443878
19 changed files with 515 additions and 316 deletions

View file

@ -18,7 +18,7 @@
<div class="input-group input-group-sm">
<input type="text" class="form-control"
placeholder="Custom widget URL"
[(ngModel)]="newWidgetUrl" name="newWidgetUrl"
[(ngModel)]="newWidget.dimension.newUrl" name="newWidgetUrl"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
@ -42,14 +42,14 @@
Widget Name
<input type="text" class="form-control"
placeholder="Custom Widget"
[(ngModel)]="widget.newName" name="widget-name-{{widget.id}}"
[(ngModel)]="widget.dimension.newName" name="widget-name-{{widget.id}}"
[disabled]="isUpdating">
</label>
<label>
Widget URL
<input type="text" class="form-control"
placeholder="Custom widget URL"
[(ngModel)]="widget.newUrl" name="widget-url-{{widget.id}}"
[(ngModel)]="widget.dimension.newUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating">
</label>
<button type="button" class="btn btn-primary btn-sm" (click)="saveWidget(widget)">Save

View file

@ -4,7 +4,7 @@ import { WidgetComponent } from "../widget.component";
import { ScalarService } from "../../../shared/scalar.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { WIDGET_DIM_CUSTOM, WIDGET_SCALAR_CUSTOM } from "../../../shared/models/widget";
import { WIDGET_CUSTOM } from "../../../shared/models/widget";
@Component({
selector: "my-customwidget-config",
@ -18,14 +18,13 @@ export class CustomWidgetConfigComponent extends WidgetComponent implements Moda
scalarService: ScalarService,
window: Window) {
super(
window,
toaster,
scalarService,
dialog.context.roomId,
window,
WIDGET_DIM_CUSTOM,
WIDGET_SCALAR_CUSTOM,
dialog.context.integration,
dialog.context.integrationId,
WIDGET_CUSTOM,
"Custom Widget",
"generic" // wrapper
);

View file

@ -18,7 +18,7 @@
<div class="input-group input-group-sm">
<input type="text" class="form-control"
placeholder="Etherpad name or URL"
[(ngModel)]="newWidgetUrl" name="newWidgetUrl"
[(ngModel)]="newWidget.dimension.newUrl" name="newWidgetUrl"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
@ -42,17 +42,17 @@
Pad Name
<input type="text" class="form-control"
placeholder="Etherpad Widget"
[(ngModel)]="widget.newName" name="widget-name-{{widget.id}}"
[(ngModel)]="widget.dimension.newName" name="widget-name-{{widget.id}}"
[disabled]="isUpdating">
</label>
<label>
Pad URL
<input type="text" class="form-control"
placeholder="https://your-pad-url"
[(ngModel)]="widget.newUrl" name="widget-url-{{widget.id}}"
[(ngModel)]="widget.dimension.newUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating">
</label>
<button type="button" class="btn btn-primary btn-sm" (click)="validateAndSaveWidget(widget)">
<button type="button" class="btn btn-primary btn-sm" (click)="saveWidget(widget)">
Save
</button>
<button type="button" class="btn btn-outline btn-sm" (click)="toggleWidget(widget)">

View file

@ -4,7 +4,7 @@ import { WidgetComponent } from "../widget.component";
import { ScalarService } from "../../../shared/scalar.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { Widget, WIDGET_DIM_ETHERPAD, WIDGET_SCALAR_ETHERPAD } from "../../../shared/models/widget";
import { WIDGET_ETHERPAD } from "../../../shared/models/widget";
import { EtherpadWidgetIntegration } from "../../../shared/models/integration";
@Component({
@ -21,14 +21,13 @@ export class EtherpadWidgetConfigComponent extends WidgetComponent implements Mo
scalarService: ScalarService,
window: Window) {
super(
window,
toaster,
scalarService,
dialog.context.roomId,
window,
WIDGET_DIM_ETHERPAD,
WIDGET_SCALAR_ETHERPAD,
dialog.context.integration,
dialog.context.integrationId,
WIDGET_ETHERPAD,
"Etherpad Widget",
"generic", // wrapper
"etherpad" // scalar wrapper
@ -38,20 +37,14 @@ export class EtherpadWidgetConfigComponent extends WidgetComponent implements Mo
}
public validateAndAddWidget() {
if (this.newWidgetUrl.startsWith("http://") || this.newWidgetUrl.startsWith("https://")) {
this.newWidgetName = "Etherpad";
if (this.newWidget.dimension.newUrl.startsWith("http://") || this.newWidget.dimension.newUrl.startsWith("https://")) {
this.newWidget.dimension.newName = "Etherpad";
} else {
this.newWidgetName = this.newWidgetUrl;
this.newWidgetUrl = this.generatePadUrl(this.newWidgetName);
this.newWidget.dimension.newName = this.newWidget.dimension.newUrl;
this.newWidget.dimension.newUrl = this.generatePadUrl(this.newWidget.dimension.newName);
}
this.addWidget({dimOriginalUrl: this.newWidgetUrl});
}
public validateAndSaveWidget(widget: Widget) {
if (!widget.data) widget.data = {};
widget.data.dimOriginalUrl = widget.newUrl;
this.saveWidget(widget);
this.addWidget();
}
private generatePadUrl(forName: string): string {

View file

@ -18,7 +18,7 @@
<div class="input-group input-group-sm">
<input type="text" class="form-control"
placeholder="Calendar ID, example: en.uk#holiday@group.v.calendar.google.com"
[(ngModel)]="newWidgetUrl" name="newWidgetUrl"
[(ngModel)]="newWidget.dimension.newData.src" name="newWidgetUrl"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
@ -42,7 +42,7 @@
Shared Calendar ID
<input type="text" class="form-control"
placeholder="en.uk#holiday@group.v.calendar.google.com"
[(ngModel)]="widget.data.dimSrc" name="widget-url-{{widget.id}}"
[(ngModel)]="widget.dimension.newData.src" name="widget-url-{{widget.id}}"
[disabled]="isUpdating">
</label>
<button type="button" class="btn btn-primary btn-sm" (click)="validateAndSaveWidget(widget)">

View file

@ -4,7 +4,7 @@ import { WidgetComponent } from "../widget.component";
import { ScalarService } from "../../../shared/scalar.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { Widget, WIDGET_DIM_GOOGLE_CALENDAR, WIDGET_SCALAR_GOOGLE_CALENDAR } from "../../../shared/models/widget";
import { EditableWidget, WIDGET_GOOGLE_CALENDAR } from "../../../shared/models/widget";
@Component({
selector: "my-googlecalendarwidget-config",
@ -18,54 +18,40 @@ export class GoogleCalendarWidgetConfigComponent extends WidgetComponent impleme
scalarService: ScalarService,
window: Window) {
super(
window,
toaster,
scalarService,
dialog.context.roomId,
window,
WIDGET_DIM_GOOGLE_CALENDAR,
WIDGET_SCALAR_GOOGLE_CALENDAR,
dialog.context.integration,
dialog.context.integrationId,
WIDGET_GOOGLE_CALENDAR,
"Google Calendar",
"", // we intentionally don't specify the wrapper so we can control the behaviour
"googleCalendar" // scalar wrapper
);
}
protected finishParsing(widget: Widget) {
if (!widget.data) widget.data = {};
if (widget.data.src) {
// Scalar widget
widget.data.dimSrc = widget.data.src;
widget.data.dimOriginalSrc = widget.data.src;
protected onWidgetsDiscovered() {
for (const widget of this.widgets) {
if (widget.data.dimSrc) {
// Convert legacy Dimension widgets to use src
widget.data.src = widget.data.dimSrc;
}
}
return widget;
}
public validateAndAddWidget() {
const calendarConfig = this.getCalendarConfig(this.newWidgetUrl);
this.newWidgetUrl = calendarConfig.url;
this.addWidget(calendarConfig.data);
this.setCalendarUrl(this.newWidget);
this.addWidget();
}
public validateAndSaveWidget(widget: Widget) {
const calendarConfig = this.getCalendarConfig(this.newWidgetUrl);
widget.newUrl = calendarConfig.url;
widget.data = calendarConfig.data;
public validateAndSaveWidget(widget: EditableWidget) {
this.setCalendarUrl(widget);
this.saveWidget(widget);
}
private getCalendarConfig(calendarId: string): { url: string, data: any } {
return {
url: window.location.origin + "/widgets/gcal?calendarId=" + encodeURIComponent(calendarId),
data: {
dimSrc: calendarId,
dimOriginalSrc: calendarId,
},
};
private setCalendarUrl(widget: EditableWidget) {
const encodedId = encodeURIComponent(widget.dimension.newData.src);
widget.dimension.newUrl = window.location.origin + "/widget/gcal?calendarId=" + encodedId;
}
}

View file

@ -12,13 +12,13 @@
</div>
</div>
<div class="config-content" *ngIf="!isLoading">
<form (submit)="validateAndAddWidget()" novalidate name="addForm">
<form (submit)="addWidget()" novalidate name="addForm">
<div class="row">
<div class="col-md-8" style="margin-bottom: 12px;">
<div class="input-group input-group-sm">
<input type="text" class="form-control"
placeholder="Link to Google Doc"
[(ngModel)]="newWidgetUrl" name="newWidgetUrl"
[(ngModel)]="newWidget.dimension.newUrl" name="newWidgetUrl"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
@ -42,10 +42,10 @@
Google Doc
<input type="text" class="form-control"
placeholder="Link to Google Doc"
[(ngModel)]="widget.newUrl" name="widget-url-{{widget.id}}"
[(ngModel)]="widget.dimension.newUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating">
</label>
<button type="button" class="btn btn-primary btn-sm" (click)="validateAndSaveWidget(widget)">
<button type="button" class="btn btn-primary btn-sm" (click)="saveWidget(widget)">
Save
</button>
<button type="button" class="btn btn-outline btn-sm" (click)="toggleWidget(widget)">

View file

@ -4,7 +4,7 @@ import { WidgetComponent } from "../widget.component";
import { ScalarService } from "../../../shared/scalar.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { Widget, WIDGET_DIM_GOOGLE_DOCS, WIDGET_SCALAR_GOOGLE_DOCS } from "../../../shared/models/widget";
import { WIDGET_GOOGLE_DOCS } from "../../../shared/models/widget";
@Component({
selector: "my-googledocswidget-config",
@ -18,27 +18,16 @@ export class GoogleDocsWidgetConfigComponent extends WidgetComponent implements
scalarService: ScalarService,
window: Window) {
super(
window,
toaster,
scalarService,
dialog.context.roomId,
window,
WIDGET_DIM_GOOGLE_DOCS,
WIDGET_SCALAR_GOOGLE_DOCS,
dialog.context.integration,
dialog.context.integrationId,
WIDGET_GOOGLE_DOCS,
"Google Docs",
"generic", // wrapper
"googleDocs" // scalar wrapper
);
}
public validateAndAddWidget() {
// We don't actually validate anything here, but we could
this.addWidget({dimOriginalUrl: this.newWidgetUrl});
}
public validateAndSaveWidget(widget: Widget) {
// We don't actually validate anything here, but we could
this.saveWidget(widget);
}
}

View file

@ -16,10 +16,10 @@
<div class="row">
<div class="col-md-8" style="margin-bottom: 12px;">
<div class="input-group input-group-sm">
<span class="input-group-addon">https://{{ integration.jitsiDomain }}/</span>
<span class="input-group-addon">https://{{ newWidget.dimension.newData.domain }}/</span>
<input type="text" class="form-control"
placeholder="MyConferenceName"
[(ngModel)]="newWidgetName" name="newWidgetName"
[(ngModel)]="newWidget.dimension.newData.conferenceId" name="newWidgetName"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
@ -29,7 +29,7 @@
</div>
</div>
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
{{ widget.data.dimOriginalConferenceUrl }} <span class="text-muted" *ngIf="widget.ownerId">(added by {{ widget.ownerId }})</span>
{{ widget.data.conferenceUrl }} <span class="text-muted" *ngIf="widget.ownerId">(added by {{ widget.ownerId }})</span>
<button type="button" class="btn btn-outline-info btn-sm" (click)="editWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-pencil"></i> Edit
@ -43,7 +43,7 @@
Conference URL
<input type="text" class="form-control"
placeholder="https://jitsi.riot.im/MyConference"
[(ngModel)]="widget.data.dimConferenceUrl" name="widget-url-{{widget.id}}"
[(ngModel)]="widget.dimension.newData.conferenceUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating">
</label>
<button type="button" class="btn btn-primary btn-sm" (click)="validateAndSaveWidget(widget)">

View file

@ -4,7 +4,7 @@ import { WidgetComponent } from "../widget.component";
import { ScalarService } from "../../../shared/scalar.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { Widget, WIDGET_DIM_JITSI, WIDGET_SCALAR_JITSI } from "../../../shared/models/widget";
import { EditableWidget, WIDGET_JITSI } from "../../../shared/models/widget";
import { JitsiWidgetIntegration } from "../../../shared/models/integration";
import * as gobyInit from "goby";
import * as url from "url";
@ -29,86 +29,109 @@ export class JitsiWidgetConfigComponent extends WidgetComponent implements Modal
scalarService: ScalarService,
window: Window) {
super(
window,
toaster,
scalarService,
dialog.context.roomId,
window,
WIDGET_DIM_JITSI,
WIDGET_SCALAR_JITSI,
dialog.context.integration,
dialog.context.integrationId,
WIDGET_JITSI,
"Jitsi Video Conference",
"" // we intentionally don't specify the wrapper so we can control the behaviour
);
this.integration = <JitsiWidgetIntegration>dialog.context.integration;
this.newWidgetName = this.generateConferenceId();
}
protected finishParsing(widget: Widget): Widget {
const parsedUrl = url.parse(widget.url, true);
const conferenceId = parsedUrl.query["confId"];
protected onNewWidgetPrepared() {
this.newWidget.dimension.newData.conferenceId = this.generateConferenceId();
this.newWidget.dimension.newData.domain = this.integration.jitsiDomain;
this.newWidget.dimension.newData.isAudioConf = false;
}
if (!widget.data) widget.data = {};
protected onWidgetsDiscovered() {
for (const widget of this.widgets) {
const parsedUrl = url.parse(widget.url, true);
const conferenceId = parsedUrl.query["conferenceId"];
const confId = parsedUrl.query["confId"];
const domain = parsedUrl.query["domain"];
let isAudioConf = parsedUrl.query["isAudioConf"];
if (conferenceId) {
// It's a scalar widget
widget.data.dimOriginalConferenceUrl = "https://jitsi.riot.im/" + conferenceId;
widget.data.dimConferenceUrl = widget.data.dimOriginalConferenceUrl;
// Convert isAudioConf to boolean
if (isAudioConf === "true") isAudioConf = true;
else if (isAudioConf === "false") isAudioConf = false;
else if (isAudioConf && isAudioConf[0] === '$') isAudioConf = widget.data[isAudioConf];
else isAudioConf = false; // default
if (conferenceId) {
// It's a legacy Dimension widget
widget.data.conferenceId = conferenceId;
} else widget.data.conferenceId = confId;
if (domain) widget.data.domain = domain;
else widget.data.domain = "jitsi.riot.im";
widget.data.isAudioConf = isAudioConf;
if (!widget.data.conferenceUrl) {
widget.data.conferenceUrl = "https://" + widget.data.domain + "/" + widget.data.conferenceId;
}
}
return widget;
}
protected beforeEdit(widget: Widget) {
if (!widget.data) widget.data = {};
widget.data.dimConferenceUrl = widget.data.dimOriginalConferenceUrl;
protected onWidgetPreparedForEdit(widget: EditableWidget) {
if (!widget.dimension.newData.conferenceUrl) {
const conferenceId = widget.dimension.newData.conferenceId;
const domain = widget.dimension.newData.domain;
widget.dimension.newData.conferenceUrl = "https://" + domain + "/" + conferenceId;
}
}
public validateAndAddWidget() {
const jitsiConfig = this.getJitsiConfig(this.integration.jitsiDomain, this.newWidgetName);
if (!this.newWidget.dimension.newData.conferenceId) {
this.toaster.pop("warning", "Please enter a conference name");
return;
}
this.newWidgetUrl = jitsiConfig.url;
this.newWidgetName = "Jitsi Video Conference";
this.addWidget(jitsiConfig.data);
this.setJitsiUrl(this.newWidget);
this.addWidget();
}
public validateAndSaveWidget(widget: Widget) {
const jitsiUrl = url.parse(widget.data.dimConferenceUrl);
const jitsiConfig = this.getJitsiConfig(jitsiUrl.host, jitsiUrl.path.substring(1));
public validateAndSaveWidget(widget: EditableWidget) {
if (!widget.dimension.newData.conferenceUrl) {
this.toaster.pop("warning", "Please enter a conference URL");
return;
}
widget.newUrl = jitsiConfig.url;
widget.data = jitsiConfig.data;
const jitsiUrl = url.parse(widget.dimension.newData.conferenceUrl);
widget.dimension.newData.domain = jitsiUrl.host;
widget.dimension.newData.conferenceId = jitsiUrl.path.substring(1);
widget.dimension.newData.isAudioConf = false;
this.setJitsiUrl(widget);
this.saveWidget(widget);
}
private getJitsiConfig(domain: string, conferenceId: string): { url: string, data: any } {
const conferenceUrl = "https://" + domain + "/" + encodeURIComponent(conferenceId);
const data = {
dimOriginalConferenceUrl: conferenceUrl,
dimConferenceUrl: conferenceUrl,
};
private setJitsiUrl(widget: EditableWidget) {
const conferenceId = widget.dimension.newData.conferenceId;
const domain = widget.dimension.newData.domain;
const isAudioConf = widget.dimension.newData.isAudioConf;
let widgetQueryString = url.format({
query: {
//"scriptUrl": this.integration.scriptUrl, // handled in wrapper
// TODO: Use templating when mobile riot supports it
"confId": conferenceId, // named confId for compatibility with mobile clients
"domain": domain,
"conferenceId": conferenceId,
"isAudioConf": isAudioConf,
"displayName": "$matrix_display_name",
"avatarUrl": "$matrix_avatar_url",
"userId": "$matrix_user_id",
},
});
widgetQueryString = this.unformatParams(widgetQueryString, data);
widgetQueryString = this.decodeParams(widgetQueryString, Object.keys(widget.dimension.newData).map(k => "$" + k));
return {
url: window.location.origin + "/widgets/jitsi" + widgetQueryString,
data: data,
};
}
protected widgetAdded() {
this.newWidgetName = this.generateConferenceId();
widget.dimension.newUrl = window.location.origin + "/widgets/jitsi" + widgetQueryString;
widget.dimension.newData.conferenceUrl = "https://" + domain + "/" + conferenceId;
}
private generateConferenceId() {

View file

@ -18,7 +18,7 @@
<div class="input-group input-group-sm">
<input type="text" class="form-control"
placeholder="Twitch Channel Name"
[(ngModel)]="newWidgetUrl" name="newWidgetUrl"
[(ngModel)]="newWidget.dimension.newData.channelName" name="newWidgetUrl"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
@ -42,14 +42,14 @@
Widget Name
<input type="text" class="form-control"
placeholder="Twitch Widget"
[(ngModel)]="widget.newName" name="widget-name-{{widget.id}}"
[(ngModel)]="widget.dimension.newName" name="widget-name-{{widget.id}}"
[disabled]="isUpdating">
</label>
<label>
Channel Name
<input type="text" class="form-control"
placeholder="Twitch Channel Name"
[(ngModel)]="widget.data.newDimChannelName" name="widget-url-{{widget.id}}"
[(ngModel)]="widget.dimension.newData.channelName" name="widget-url-{{widget.id}}"
[disabled]="isUpdating">
</label>
<button type="button" class="btn btn-primary btn-sm" (click)="validateAndSaveWidget(widget)">

View file

@ -4,7 +4,7 @@ import { WidgetComponent } from "../widget.component";
import { ScalarService } from "../../../shared/scalar.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { Widget, WIDGET_DIM_TWITCH, WIDGET_SCALAR_TWITCH } from "../../../shared/models/widget";
import { EditableWidget, WIDGET_TWITCH } from "../../../shared/models/widget";
@Component({
selector: "my-twitchwidget-config",
@ -18,54 +18,55 @@ export class TwitchWidgetConfigComponent extends WidgetComponent implements Moda
scalarService: ScalarService,
window: Window) {
super(
window,
toaster,
scalarService,
dialog.context.roomId,
window,
WIDGET_DIM_TWITCH,
WIDGET_SCALAR_TWITCH,
dialog.context.integration,
dialog.context.integrationId,
WIDGET_TWITCH,
"Twitch Widget",
"video", // wrapper
"twitch" // scalar wrapper
);
}
public validateAndAddWidget() {
// Replace channel name with path to embedable Twitch Player
const url = "https://player.twitch.tv/?channel=" + this.newWidgetUrl;
// TODO Somehow Validate if it is a valid Username
if (!url) {
this.toaster.pop("warning", "Please enter a Twitch Livestream Channel Name");
return;
}
const originalUrl = this.newWidgetUrl;
this.newWidgetUrl = url;
this.addWidget({dimChannelName: originalUrl});
protected onNewWidgetPrepared() {
this.newWidget.dimension.newData.channelName = "";
}
public validateAndSaveWidget(widget: Widget) {
const url = "https://player.twitch.tv/?channel=" + widget.data.dimChannelName;
protected onWidgetsDiscovered() {
for (const widget of this.widgets) {
// Convert dimChannelName to channelName
if (!widget.data.channelName) {
widget.data.channelName = widget.data.dimChannelName;
}
}
}
// TODO Somehow Validate if it is a valid Username
if (!url) {
this.toaster.pop("warning", "Please enter a Twitch Livestream Channel Name");
public validateAndAddWidget() {
if (!this.newWidget.dimension.newData.channelName) {
this.toaster.pop("warning", "Please enter a Twitch Livestream channel name");
return;
}
if (!widget.data) widget.data = {};
this.setTwitchUrl(this.newWidget);
this.addWidget();
}
widget.newUrl = url;
widget.data.dimChannelName = widget.data.newDimChannelName;
public validateAndSaveWidget(widget: EditableWidget) {
if (!widget.dimension.newData.channelName) {
this.toaster.pop("warning", "Please enter a Twitch Livestream channel name");
return;
}
this.setTwitchUrl(widget);
this.saveWidget(widget);
}
public editWidget(widget: Widget) {
widget.data.newDimChannelName = widget.data.dimChannelName;
super.editWidget(widget);
private setTwitchUrl(widget: EditableWidget) {
// TODO: This should use templating when mobile riot supports it
widget.dimension.newUrl = "https://player.twitch.tv/?channel=" + widget.dimension.newData.channelName;
}
}

View file

@ -1,5 +1,5 @@
import { ScalarService } from "../../shared/scalar.service";
import { ScalarToWidgets, Widget } from "../../shared/models/widget";
import { convertScalarWidgetsToDtos, EditableWidget } from "../../shared/models/widget";
import { ToasterService } from "angular2-toaster";
import { Integration } from "../../shared/models/integration";
@ -14,25 +14,23 @@ export class WidgetComponent {
public isLoading = true;
public isUpdating = false;
public widgets: Widget[];
public newWidgetUrl: string = "";
public newWidgetName: string = "";
public widgets: EditableWidget[];
public newWidget: EditableWidget;
private toggledWidgetIds: string[] = [];
private wrapperUrl = "";
private scalarWrapperUrls: string[] = [];
private wrapperUrl = "";
constructor(protected toaster: ToasterService,
constructor(window: Window,
protected toaster: ToasterService,
protected scalarApi: ScalarService,
protected roomId: string,
window: Window,
private primaryWidgetType: string,
alternateWidgetType: string,
public roomId: string,
public integration: Integration,
requestedEditId: string,
editWidgetId: string,
private widgetIds: string[],
private defaultName: string,
wrapperId = "generic",
scalarWrapperId = null) {
private wrapperId = "generic",
private scalarWrapperId = null) {
this.isLoading = true;
this.isUpdating = false;
@ -45,20 +43,21 @@ export class WidgetComponent {
}
}
this.getWidgetsOfType(primaryWidgetType, alternateWidgetType).then(widgets => {
this.prepareNewWidget();
this.getWidgetsOfType(widgetIds).then(widgets => {
this.widgets = widgets;
for (let widget of this.widgets) {
this.unpackWidget(widget);
}
this.onWidgetsDiscovered();
this.isLoading = false;
this.isUpdating = false;
// Unwrap URLs for easy-editing
for (let widget of this.widgets) {
this.setWidgetUrl(widget);
}
// See if we should request editing a particular widget
if (requestedEditId) {
if (editWidgetId) {
for (let widget of this.widgets) {
if (widget.id === requestedEditId) {
if (widget.id === editWidgetId) {
console.log("Requesting edit for " + widget.id);
this.editWidget(widget);
}
@ -67,37 +66,94 @@ export class WidgetComponent {
});
}
protected finishParsing(widget: Widget) {
// We don't actually need to do anything
return widget;
/**
* Populates the Dimension-specific fields of the widget, overwriting any values that
* were there previously.
* @param {EditableWidget} widget The widget to unpack.
*/
private unpackWidget(widget: EditableWidget) {
widget.dimension = {
newUrl: this.unwrapUrl(widget.url),
newName: widget.name,
newTitle: widget.data.title,
newData: JSON.parse(JSON.stringify(widget.data || "{}")),
};
}
protected widgetAdded() {
// Meant to be overridden
/**
* Packs a widget, converting the Dimension-specific parameters into a widget. This will
* overwrite any values in the widget which need to be committed to from the Dimension
* fields.
* @param {EditableWidget} widget The widget to pack.
*/
private packWidget(widget: EditableWidget) {
// The widget already has an ID and type, we just need to fill in the bits
widget.name = widget.dimension.newName || this.defaultName;
widget.data = widget.dimension.newData || {};
widget.url = this.wrapUrl(widget.dimension.newUrl, Object.keys(widget.data).map(k => "$" + k));
widget.type = this.widgetIds[0]; // always set the type to be the latest type
// Populate our stuff
widget.data["dimension:app:metadata"] = {
inRoomId: this.roomId,
wrapperUrlBase: this.wrapperUrl,
wrapperId: this.wrapperId,
scalarWrapperId: this.scalarWrapperId,
integration: {
type: this.integration.type,
integrationType: this.integration.integrationType,
},
lastUpdatedTs: new Date().getTime(),
};
// Set the title to an appropriate value. Empty strings are valid titles, but other falsey
// values should be considered as "no title".
if (widget.dimension.newTitle || widget.dimension.newTitle === "")
widget.data["title"] = widget.dimension.newTitle;
else widget.data["title"] = undefined;
}
protected beforeEdit(widget: Widget) {
// Meant to be overridden - we do some silly logic here to make typescript happy
// noinspection SillyAssignmentJS
widget.url = widget.url;
/**
* Builds a new widget and assigns it to the newWidget property. This also expands the
* Dimension-specific fields for the widget with reasonable defaults.
*/
protected prepareNewWidget() {
this.newWidget = <EditableWidget>{
id: "dimension-" + this.widgetIds[0] + "-" + (new Date().getTime()),
type: this.widgetIds[0],
name: this.defaultName,
url: window.location.origin,
//ownerId: this.userId, // we don't have a user id
dimension: {
newUrl: "",
newName: "",
newTitle: null, // by default we don't offer a title (empty strings are valid titles)
newData: {},
},
};
this.onNewWidgetPrepared();
}
private getWidgetsOfType(type: string, altType: string): Promise<Widget[]> {
/**
* Gets widgets from the Scalar API. The widgets returned from here are not unpacked and
* may not have the Dimension-specific fields set.
* @param {string[]} types The widget types to look for
* @return {Promise<EditableWidget[]>} The widgets discovered
*/
private getWidgetsOfType(types: string[]): Promise<EditableWidget[]> {
return this.scalarApi.getWidgets(this.roomId)
.then(resp => ScalarToWidgets(resp))
.then(widgets => {
let filtered: Widget[] = [];
for (let widget of widgets) {
if (widget.type === type || (altType && widget.type === altType))
filtered.push(widget);
}
return filtered.map(w => this.finishParsing(w));
});
.then(resp => convertScalarWidgetsToDtos(resp))
.then(widgets => widgets.filter(w => types.indexOf(w.type) !== -1));
}
private getWrappedUrl(url: string): string {
/**
* Gets the actual URL from known wrappers. If the URL is found to be not wrapped, or no
* wrappers are applicable, then the given URL is returned.
* @param {string} url The URL to unwrap
* @return {string} The unwrapped URL
*/
private unwrapUrl(url: string): string {
console.log(this.scalarWrapperUrls);
if (!this.wrapperUrl) return url;
const urls = [this.wrapperUrl].concat(this.scalarWrapperUrls);
@ -106,122 +162,234 @@ export class WidgetComponent {
return decodeURIComponent(url.substring(scalarUrl.length));
}
}
return url;
}
private wrapUrl(url: string): string {
/**
* Wraps the URL with an appropriate wrapper. If no wrapper is defined for this component
* then the given URL is returned.
* @param {string} url The URL to wrap
* @param {string[]} customVars The values in the URL which should not be encoded
* @return {string} The wrapped URL
*/
private wrapUrl(url: string, customVars: string[]): string {
if (!this.wrapperUrl) return url;
let encodedURL = this.wrapperUrl + encodeURIComponent(url);
// TODO: Decode data parameters
encodedURL = this.unformatParams(encodedURL);
encodedURL = this.decodeParams(encodedURL, customVars);
return encodedURL;
}
protected unformatParams(encodedUrl: string, additionalData: any = {}):string {
encodedUrl = encodedUrl.replace(encodeURIComponent("$matrix_user_id"), "$matrix_user_id");
encodedUrl = encodedUrl.replace(encodeURIComponent("$matrix_room_id"), "$matrix_room_id");
encodedUrl = encodedUrl.replace(encodeURIComponent("$matrix_display_name"), "$matrix_display_name");
encodedUrl = encodedUrl.replace(encodeURIComponent("$matrix_avatar_url"), "$matrix_avatar_url");
for (const key of Object.keys(additionalData)) {
encodedUrl = encodedUrl.replace(encodeURIComponent("$" + key), "$" + key);
/**
* Decodes the given URL to make the parameters safe for interpretation by clients. The variables
* specified in the widget spec will be decoded automatically.
* @param {string} encodedURL The encoded URL
* @param {string[]} customVars The custom variables to decode
* @returns {string} The URL with variables decoded accordingly
*/
protected decodeParams(encodedURL: string, customVars: string[]): string {
encodedURL = encodedURL.replace(encodeURIComponent("$matrix_user_id"), "$matrix_user_id");
encodedURL = encodedURL.replace(encodeURIComponent("$matrix_room_id"), "$matrix_room_id");
encodedURL = encodedURL.replace(encodeURIComponent("$matrix_display_name"), "$matrix_display_name");
encodedURL = encodedURL.replace(encodeURIComponent("$matrix_avatar_url"), "$matrix_avatar_url");
for (const key of customVars) {
encodedURL = encodedURL.replace(encodeURIComponent(key), key);
}
return encodedUrl;
return encodedURL;
}
private setWidgetUrl(widget: Widget) {
widget.url = this.getWrappedUrl(widget.url);
// Use the Dimension-specific URL override if one is present
if (widget.data && widget.data.dimOriginalUrl) {
widget.url = widget.data.dimOriginalUrl;
}
}
public addWidget(data: any = null) {
let constructedWidget: Widget = {
id: "dimension-" + this.primaryWidgetType + "-" + (new Date().getTime()),
url: this.wrapUrl(this.newWidgetUrl),
type: this.primaryWidgetType,
name: this.newWidgetName || this.defaultName,
};
if (data) constructedWidget.data = data;
/**
* Adds the widget stored in newWidget to the room.
* @returns {Promise<*>} Resolves when the widget has been added and newWidget is populated
* with a new widget.
*/
public addWidget(): Promise<any> {
this.packWidget(this.newWidget);
this.isUpdating = true;
this.scalarApi.setWidget(this.roomId, constructedWidget)
.then(() => this.widgets.push(constructedWidget))
.then(() => this.setWidgetUrl(constructedWidget))
this.onWidgetBeforeAdd();
return this.scalarApi.setWidget(this.roomId, this.newWidget)
.then(() => this.widgets.push(this.newWidget))
.then(() => {
this.isUpdating = false;
this.newWidgetUrl = "";
this.newWidgetName = "";
this.onWidgetAfterAdd();
this.prepareNewWidget();
this.toaster.pop("success", "Widget added!");
this.widgetAdded();
})
.catch(err => {
this.toaster.pop("error", err.json().error);
console.error(err);
this.isUpdating = false;
console.error(err);
this.toaster.pop("error", err.json().error);
});
}
public saveWidget(widget: Widget) {
if (widget.newUrl.trim().length === 0) {
/**
* Saves a widget, persisting the changes to the room.
* @param {EditableWidget} widget The widget to save.
* @returns {Promise<any>} Resolves when the widget has been updated in the room.
*/
public saveWidget(widget: EditableWidget): Promise<any> {
if (!widget.dimension.newUrl || widget.dimension.newUrl.trim().length === 0) {
this.toaster.pop("warning", "Please enter a URL for the widget");
return;
}
widget.name = widget.newName || this.defaultName;
widget.url = this.wrapUrl(widget.newUrl);
this.packWidget(widget);
this.isUpdating = true;
this.scalarApi.setWidget(this.roomId, widget)
this.onWidgetBeforeEdit(widget);
return this.scalarApi.setWidget(this.roomId, widget)
.then(() => this.toggleWidget(widget))
.then(() => {
this.isUpdating = false;
widget.url = this.getWrappedUrl(widget.url); // for easier editing
this.onWidgetAfterEdit(widget);
this.toaster.pop("success", "Widget updated!");
})
.catch(err => {
this.toaster.pop("error", err.json().error);
console.error(err);
this.isUpdating = false;
console.error(err);
this.toaster.pop("error", err.json().error);
});
}
public removeWidget(widget: Widget) {
/**
* Removes a widget from the room
* @param {EditableWidget} widget The widget to remove
* @returns {Promise<*>} Resolves when the widget has been removed.
*/
public removeWidget(widget: EditableWidget): Promise<any> {
this.isUpdating = true;
this.scalarApi.deleteWidget(this.roomId, widget)
this.onWidgetBeforeDelete(widget);
return this.scalarApi.deleteWidget(this.roomId, widget)
.then(() => this.widgets.splice(this.widgets.indexOf(widget), 1))
.then(() => {
this.isUpdating = false;
this.onWidgetAfterDelete(widget);
this.toaster.pop("success", "Widget deleted!");
})
.catch(err => {
this.toaster.pop("error", err.json().error);
console.error(err);
this.isUpdating = false;
console.error(err);
this.toaster.pop("error", err.json().error);
});
}
public editWidget(widget: Widget) {
widget.newName = widget.name || this.defaultName;
widget.newUrl = widget.url;
this.beforeEdit(widget);
this.toggleWidget(widget);
/**
* Puts a widget in the edit state.
* @param {EditableWidget} widget
*/
public editWidget(widget: EditableWidget) {
this.toggleWidget(widget, "edit");
}
public toggleWidget(widget: Widget) {
/**
* Toggles a widget between the "edit" and "canceled" state. If a targetState is
* defined, the widget is forced into that state.
* @param {EditableWidget} widget The widget to set the state of.
* @param {"edit"|"cancel"|null} targetState The target state, optional
*/
public toggleWidget(widget: EditableWidget, targetState: "edit" | "cancel" | null = null) {
let idx = this.toggledWidgetIds.indexOf(widget.id);
if (idx === -1) this.toggledWidgetIds.push(widget.id);
if (targetState === null) targetState = idx === -1 ? "edit" : "cancel";
if (targetState === "edit") {
this.unpackWidget(widget);
this.onWidgetPreparedForEdit(widget);
if (idx === -1) this.toggledWidgetIds.push(widget.id);
}
else this.toggledWidgetIds.splice(idx, 1);
}
public isWidgetToggled(widget: Widget) {
/**
* Determines if a widget is in the edit state
* @param {EditableWidget} widget The widget to check
* @returns {boolean} true if the widget is in the edit state
*/
public isWidgetToggled(widget: EditableWidget) {
return this.toggledWidgetIds.indexOf(widget.id) !== -1;
}
// Component hooks below here
// ------------------------------------------------------------------
/**
* Called when a new widget has been created in the newWidget field
*/
protected onNewWidgetPrepared(): void {
// Component hook
}
/**
* Called after all the widgets have been discovered and unpacked for the room.
*/
protected onWidgetsDiscovered(): void {
// Component hook
}
/**
* Called before the widget is added to the room, but after the Dimension-specific
* settings have been copied over to the primary fields.
*/
protected onWidgetBeforeAdd(): void {
// Component hook
}
/**
* Called after the widget has been added to the room, but before the newWidget field
* has been set to a new widget.
*/
protected onWidgetAfterAdd(): void {
// Component hook
}
/**
* Called when the given widget has been asked to be prepared for editing. At this point
* the widget is not being persisted to the room, it is just updating the EditingWidget's
* properties for the user's ability to edit it.
* @param {EditableWidget} _widget The widget that has been prepared for editing
*/
protected onWidgetPreparedForEdit(_widget: EditableWidget): void {
// Component hook
}
/**
* Called before the given widget has been updated in the room, but after the
* Dimension-specific settings have been copied over to the primary fields.
* This is not called for widgets being deleted.
* @param {EditableWidget} _widget The widget about to be edited
*/
protected onWidgetBeforeEdit(_widget: EditableWidget): void {
// Component hook
}
/**
* Called after a given widget has been updated in the room. This is not called for
* widgets being deleted.
* @param {EditableWidget} _widget The widget that has been updated.
*/
protected onWidgetAfterEdit(_widget: EditableWidget): void {
// Component hook
}
/**
* Called before the given widget has been removed from the room. No changes to the
* widget have been made at this point.
* @param {EditableWidget} _widget The widget about to be deleted.
*/
protected onWidgetBeforeDelete(_widget: EditableWidget): void {
// Component hook
}
/**
* Called after a given widget has been deleted from the room. The widget will be in
* the deleted state and will no longer be tracked anywhere on the component.
* @param {EditableWidget} _widget The widget that has been deleted.
*/
protected onWidgetAfterDelete(_widget: EditableWidget): void {
// Component hook
}
}

View file

@ -18,7 +18,7 @@
<div class="input-group input-group-sm">
<input type="text" class="form-control"
placeholder="YouTube, Vimeo, or DailyMotion video URL"
[(ngModel)]="newWidgetUrl" name="newWidgetUrl"
[(ngModel)]="newWidget.dimension.newUrl" name="newWidgetUrl"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
@ -42,14 +42,14 @@
Widget Name
<input type="text" class="form-control"
placeholder="YouTube Widget"
[(ngModel)]="widget.newName" name="widget-name-{{widget.id}}"
[(ngModel)]="widget.dimension.newName" name="widget-name-{{widget.id}}"
[disabled]="isUpdating">
</label>
<label>
Video URL
<input type="text" class="form-control"
placeholder="YouTube, Vimeo, or DailyMotion video URL"
[(ngModel)]="widget.newUrl" name="widget-url-{{widget.id}}"
[(ngModel)]="widget.dimension.newUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating">
</label>
<button type="button" class="btn btn-primary btn-sm" (click)="validateAndSaveWidget(widget)">

View file

@ -4,7 +4,7 @@ import { WidgetComponent } from "../widget.component";
import { ScalarService } from "../../../shared/scalar.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { Widget, WIDGET_DIM_YOUTUBE, WIDGET_SCALAR_YOUTUBE } from "../../../shared/models/widget";
import { EditableWidget, WIDGET_YOUTUBE } from "../../../shared/models/widget";
import * as embed from "embed-video";
import * as $ from "jquery";
@ -20,14 +20,13 @@ export class YoutubeWidgetConfigComponent extends WidgetComponent implements Mod
scalarService: ScalarService,
window: Window) {
super(
window,
toaster,
scalarService,
dialog.context.roomId,
window,
WIDGET_DIM_YOUTUBE,
WIDGET_SCALAR_YOUTUBE,
dialog.context.integration,
dialog.context.integrationId,
WIDGET_YOUTUBE,
"Youtube Widget",
"video", // wrapper
"youtube" // scalar wrapper
@ -35,37 +34,34 @@ export class YoutubeWidgetConfigComponent extends WidgetComponent implements Mod
}
public validateAndAddWidget() {
const url = this.getSafeUrl(this.newWidgetUrl);
const url = this.getRealVideoUrl(this.newWidget.dimension.newUrl);
if (!url) {
this.toaster.pop("warning", "Please enter a YouTube, Vimeo, or DailyMotion video URL");
return;
}
const originalUrl = this.newWidgetUrl;
this.newWidgetUrl = url;
this.addWidget({dimOriginalUrl: originalUrl});
this.newWidget.dimension.newUrl = url;
this.addWidget();
}
public validateAndSaveWidget(widget: Widget) {
const url = this.getSafeUrl(widget.newUrl);
public validateAndSaveWidget(widget: EditableWidget) {
const url = this.getRealVideoUrl(widget.dimension.newUrl);
if (!url) {
this.toaster.pop("warning", "Please enter a YouTube, Vimeo, or DailyMotion video URL");
return;
}
if (!widget.data) widget.data = {};
widget.data.dimOriginalUrl = widget.newUrl;
widget.newUrl = url;
widget.dimension.newUrl = url;
this.saveWidget(widget);
}
private getSafeUrl(url) {
private getRealVideoUrl(url) {
const embedCode = embed(url);
if (!embedCode) {
return null;
}
// HACK: Grab the video URL from the iframe
// HACK: Grab the video URL from the iframe embed code
url = $(embedCode).attr("src");
if (url.startsWith("//")) url = "https:" + url;

View file

@ -7,6 +7,7 @@ import { IntegrationService } from "../shared/integration.service";
export class ConfigModalContext extends BSModalContext {
public integration: Integration;
public roomId: string;
public userId: string;
public scalarToken: string;
public integrationId: string;
}
@ -43,6 +44,6 @@ export class IntegrationComponent {
}
public canHaveErrors(integration: Integration): boolean {
return integration.type === 'bridge' || integration.type === "widget";
return integration.type === "bridge" || integration.type === "widget";
}
}

View file

@ -10,11 +10,8 @@ import { TwitchWidgetConfigComponent } from "../configs/widget/twitch/twitch-con
import { EtherpadWidgetConfigComponent } from "../configs/widget/etherpad/etherpad-config.component";
import { JitsiWidgetConfigComponent } from "../configs/widget/jitsi/jitsi-config.component";
import {
WIDGET_DIM_CUSTOM,
WIDGET_DIM_ETHERPAD, WIDGET_DIM_GOOGLE_CALENDAR, WIDGET_DIM_GOOGLE_DOCS,
WIDGET_DIM_JITSI,
WIDGET_DIM_TWITCH,
WIDGET_DIM_YOUTUBE
WIDGET_CUSTOM, WIDGET_ETHERPAD, WIDGET_GOOGLE_CALENDAR, WIDGET_GOOGLE_DOCS, WIDGET_JITSI, WIDGET_TWITCH,
WIDGET_YOUTUBE
} from "./models/widget";
import { GoogleDocsWidgetConfigComponent } from "../configs/widget/googledocs/googledocs-config.component";
import { GoogleCalendarWidgetConfigComponent } from "../configs/widget/googlecalendar/googlecalendar-config.component";
@ -40,31 +37,31 @@ export class IntegrationService {
"widget": {
"customwidget": {
component: CustomWidgetConfigComponent,
screenId: "type_" + WIDGET_DIM_CUSTOM,
types: WIDGET_CUSTOM,
},
"youtube": {
component: YoutubeWidgetConfigComponent,
screenId: "type_" + WIDGET_DIM_YOUTUBE,
types: WIDGET_YOUTUBE
},
"etherpad": {
component: EtherpadWidgetConfigComponent,
screenId: "type_" + WIDGET_DIM_ETHERPAD,
types: WIDGET_ETHERPAD,
},
"twitch": {
component: TwitchWidgetConfigComponent,
screenId: "type_" + WIDGET_DIM_TWITCH,
types: WIDGET_TWITCH,
},
"jitsi": {
component: JitsiWidgetConfigComponent,
screenId: "type_" + WIDGET_DIM_JITSI,
types: WIDGET_JITSI,
},
"googledocs": {
component: GoogleDocsWidgetConfigComponent,
screenId: "type_" + WIDGET_DIM_GOOGLE_DOCS,
types: WIDGET_GOOGLE_DOCS,
},
"googlecalendar": {
component: GoogleCalendarWidgetConfigComponent,
screenId: "type_" + WIDGET_DIM_GOOGLE_CALENDAR,
types: WIDGET_GOOGLE_CALENDAR,
},
},
};
@ -102,8 +99,9 @@ export class IntegrationService {
static getIntegrationForScreen(screen: string): { type: string, integrationType: string } {
for (const iType of Object.keys(IntegrationService.supportedIntegrationsMap)) {
for (const iiType of Object.keys(IntegrationService.supportedIntegrationsMap[iType])) {
const iScreen = IntegrationService.supportedIntegrationsMap[iType][iiType].screenId;
if (screen === iScreen) return {type: iType, integrationType: iiType};
const integrationTypes = IntegrationService.supportedIntegrationsMap[iType][iiType].types;
const integrationScreens = integrationTypes.map(t => "type_" + t);
if (integrationScreens.includes(screen)) return {type: iType, integrationType: iiType};
}
}

View file

@ -1,39 +1,84 @@
import { WidgetsResponse } from "./scalar_responses";
// Scalar's widget types (known)
export const WIDGET_SCALAR_CUSTOM = "customwidget";
export const WIDGET_SCALAR_ETHERPAD = "etherpad";
export const WIDGET_SCALAR_GOOGLE_DOCS = "googledocs";
export const WIDGET_SCALAR_GOOGLE_CALENDAR = "googlecalendar";
export const WIDGET_SCALAR_JITSI = "jitsi";
export const WIDGET_SCALAR_YOUTUBE = "youtube";
export const WIDGET_SCALAR_GRAFANA = "grafana";
// Placeholder until Scalar supports twitch
export const WIDGET_SCALAR_TWITCH = "";
export const WIDGET_CUSTOM = ["customwidget", "dimension-customwidget"].reverse();
export const WIDGET_ETHERPAD = ["etherpad", "dimension-etherpad"].reverse();
export const WIDGET_GOOGLE_DOCS = ["googledocs", "dimension-googledocs"].reverse();
export const WIDGET_GOOGLE_CALENDAR = ["googlecalendar", "dimension-googlecalendar"].reverse();
export const WIDGET_JITSI = ["jitsi", "dimension-jitsi"].reverse();
export const WIDGET_YOUTUBE = ["youtube", "dimension-youtube"].reverse();
export const WIDGET_GRAFANA = ["grafana", "dimension-grafana"].reverse();
export const WIDGET_TWITCH = ["twitch", "dimension-twitch"].reverse();
// Dimension has its own set of types to ensure that we don't conflict with Scalar
export const WIDGET_DIM_CUSTOM = "dimension-customwidget";
export const WIDGET_DIM_YOUTUBE = "dimension-youtube";
export const WIDGET_DIM_TWITCH = "dimension-twitch";
export const WIDGET_DIM_ETHERPAD = "dimension-etherpad";
export const WIDGET_DIM_JITSI = "dimension-jitsi";
export const WIDGET_DIM_GOOGLE_DOCS = "dimension-googledocs";
export const WIDGET_DIM_GOOGLE_CALENDAR = "dimension-googlecalendar";
export interface Widget {
export interface EditableWidget {
/**
* The widget ID. This is generated.
*/
id: string;
/**
* The type of widget. Normally one of the WIDGET_* values.
*/
type: string;
/**
* The wrapped URL for the widget.
*/
url: string;
/**
* The name of the widget.
*/
name?: string;
/**
* The raw data for the widget. For parsed data, refer to the "dimension" property.
*/
data?: any;
/**
* The user ID that created this widget.
*/
ownerId?: string;
// used only in ui
newName?: string;
newUrl?: string;
/**
* The Dimension-related settings for the widget, such as the metadata and state
* information for editing/creating this widget.
*/
dimension?: {
/**
* The new name for the widget. Used when editing/creating the widget.
*/
newName: string;
/**
* The new title for the widget. This may be null to have the client try and
* determine a title for this widget. An empty string will prevent the client
* from determining a title. Used when editing or creating the widget.
*/
newTitle: string;
/**
* The new URL for the widget (may end up being wrapped). Used when editing or
* creating the widget.
*/
newUrl: string;
/**
* The new data for the widget. Should not be falsey, but may be empty. Used
* when editing/creating the widget.
*/
newData: any;
};
}
export function ScalarToWidgets(scalarResponse: WidgetsResponse): Widget[] {
/**
* Converts a series of widgets (as given to us by the Scalar API) to widgets which
* can be passed around safely. This only converts the properties to the Widget interface
* and will not populate any custom fields, including the "dimension" field.
* @param {WidgetsResponse} scalarResponse The scalar widgets
* @return {EditableWidget[]} The Dimension widgets
*/
export function convertScalarWidgetsToDtos(scalarResponse: WidgetsResponse): EditableWidget[] {
let widgets = [];
for (let event of scalarResponse.response) {

View file

@ -7,7 +7,7 @@ import {
ScalarSuccessResponse,
WidgetsResponse
} from "./models/scalar_responses";
import { Widget } from "./models/widget";
import { EditableWidget } from "./models/widget";
@Injectable()
export class ScalarService {
@ -49,7 +49,7 @@ export class ScalarService {
});
}
public setWidget(roomId: string, widget: Widget): Promise<ScalarSuccessResponse> {
public setWidget(roomId: string, widget: EditableWidget): Promise<ScalarSuccessResponse> {
return this.callAction("set_widget", {
room_id: roomId,
widget_id: widget.id,
@ -60,7 +60,7 @@ export class ScalarService {
});
}
public deleteWidget(roomId: string, widget: Widget): Promise<ScalarSuccessResponse> {
public deleteWidget(roomId: string, widget: EditableWidget): Promise<ScalarSuccessResponse> {
return this.callAction("set_widget", {
room_id: roomId,
widget_id: widget.id,