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:
parent
fd5e367146
commit
9ff1443878
19 changed files with 515 additions and 316 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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)">
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)">
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)">
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)">
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue