mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-08-31 06:02:09 +00:00
Remove legacy configurations for now
This commit is contained in:
parent
111423cc7d
commit
7fd674a8ff
@ -26,7 +26,6 @@ import { SpinnerComponent } from "./elements/spinner/spinner.component";
|
||||
import { BreadcrumbsModule } from "ng2-breadcrumbs";
|
||||
import { RiotHomeComponent } from "./riot/riot-home/home.component";
|
||||
import { IntegrationBagComponent } from "./integration-bag/integration-bag.component";
|
||||
import { IntegrationComponent } from "./integration/integration.component";
|
||||
import { ScalarServerApiService } from "./shared/services/scalar-server-api.service";
|
||||
import { DimensionApiService } from "./shared/services/dimension-api.service";
|
||||
import { AdminApiService } from "./shared/services/admin-api.service";
|
||||
@ -53,7 +52,6 @@ import { ConfigScreenWidgetComponent } from "./configs/widget/config_screen/conf
|
||||
AppComponent,
|
||||
HomeComponent,
|
||||
RiotComponent,
|
||||
IntegrationComponent,
|
||||
IntegrationBagComponent,
|
||||
PageHeaderComponent,
|
||||
SpinnerComponent,
|
||||
|
@ -1,63 +0,0 @@
|
||||
<div class="config-wrapper">
|
||||
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
|
||||
<div class="config-header">
|
||||
<img [src]="integration.avatar">
|
||||
<h4>Configure CircleCI hooks</h4>
|
||||
</div>
|
||||
<div class="config-content">
|
||||
<form (submit)="addRepository()" novalidate name="addRepoForm">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="margin-bottom: 12px;">
|
||||
<h6>.circleci/config.yml configuration</h6>
|
||||
The following will need to be added to your .circleci/config.yml file:
|
||||
<pre class="yaml">{{ circleYaml }}</pre>
|
||||
</div>
|
||||
<div class="col-md-8" style="margin-bottom: 12px;">
|
||||
<h6>Your CircleCI hooks</h6>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control"
|
||||
placeholder="owner/repo-name"
|
||||
[(ngModel)]="repoKey" name="repoKey"
|
||||
[disabled]="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
|
||||
<i class="fa fa-plus-circle"></i> Add Repository
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 removable" *ngFor="let repo of integration.repoTemplates trackById">
|
||||
{{ repo.repoKey }}
|
||||
<button type="button" class="btn btn-outline-info btn-sm" (click)="editTemplate(repo.repoKey)"
|
||||
style="margin-top: -5px;" [disabled]="isUpdating" *ngIf="!isTemplateToggled(repo.repoKey)">
|
||||
<i class="fa fa-pencil"></i> Edit
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeRepository(repo.repoKey)"
|
||||
style="margin-top: -5px;" [disabled]="isUpdating">
|
||||
<i class="fa fa-times"></i> Remove Repository
|
||||
</button>
|
||||
<div *ngIf="isTemplateToggled(repo.repoKey)">
|
||||
<textarea [(ngModel)]="repo.newTemplate" name="template-{{repo.repoKey}}"
|
||||
style="width: 100%; height: 100px; margin-top: 5px;"></textarea>
|
||||
<button type="button" class="btn btn-primary btn-sm" (click)="saveTemplate(repo.repoKey)">Save
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline btn-sm" (click)="toggleTemplate(repo.repoKey)">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12" *ngIf="integration.immutableRepoTemplates.length > 0">
|
||||
<h6 class="other-items-title">Hooks from other users in the room</h6>
|
||||
</div>
|
||||
<div class="col-md-12 list" *ngFor="let repo of integration.immutableRepoTemplates trackById">
|
||||
{{ repo.repoKey }} <span class="text-muted">(added by {{ repo.ownerId }})</span>
|
||||
<button type="button" class="btn btn-outline-info btn-sm" (click)="toggleTemplate(repo.repoKey)"
|
||||
style="margin-top: -5px;" [disabled]="isUpdating">
|
||||
{{ isTemplateToggled(repo.repoKey) ? "Hide" : "Show" }} Template
|
||||
</button>
|
||||
<pre *ngIf="isTemplateToggled(repo.repoKey)">{{ repo.template }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -1,19 +0,0 @@
|
||||
// component styles are encapsulated and only applied to their components
|
||||
.list {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.removable {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.other-items-title {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.yaml {
|
||||
border: 1px solid #ccc;
|
||||
background: #eee;
|
||||
padding: 5px;
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { CircleCiIntegration } from "../../shared/models/legacyintegration";
|
||||
import { ModalComponent, DialogRef } from "ngx-modialog";
|
||||
import { ConfigModalContext } from "../../integration/integration.component";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
import { ApiService } from "../../shared/services/legacy/api.service";
|
||||
|
||||
@Component({
|
||||
selector: "my-circleci-config",
|
||||
templateUrl: "./circleci-config.component.html",
|
||||
styleUrls: ["./circleci-config.component.scss", "./../config.component.scss"],
|
||||
})
|
||||
export class CircleCiConfigComponent implements ModalComponent<ConfigModalContext> {
|
||||
|
||||
public integration: CircleCiIntegration;
|
||||
|
||||
public isUpdating = false;
|
||||
public repoKey = "";
|
||||
public repoTemplate = "";
|
||||
public circleYaml = "";
|
||||
|
||||
private roomId: string;
|
||||
private scalarToken: string;
|
||||
private knownRepos: string[] = [];
|
||||
private visibleTemplates = [];
|
||||
|
||||
constructor(public dialog: DialogRef<ConfigModalContext>,
|
||||
private toaster: ToasterService,
|
||||
private api: ApiService) {
|
||||
this.integration = <CircleCiIntegration>dialog.context.integration;
|
||||
this.roomId = dialog.context.roomId;
|
||||
this.scalarToken = dialog.context.scalarToken;
|
||||
|
||||
this.circleYaml = "notify:\n webhooks:\n - url: " + this.integration.webhookUrl;
|
||||
|
||||
this.calculateKnownRepos();
|
||||
this.reset();
|
||||
}
|
||||
|
||||
private calculateKnownRepos() {
|
||||
for (let repo of this.integration.repoTemplates)
|
||||
this.knownRepos.push(repo.repoKey);
|
||||
for (let immutableRepo of this.integration.immutableRepoTemplates)
|
||||
this.knownRepos.push(immutableRepo.repoKey);
|
||||
}
|
||||
|
||||
public toggleTemplate(repoKey: string) {
|
||||
let idx = this.visibleTemplates.indexOf(repoKey);
|
||||
if (idx === -1) this.visibleTemplates.push(repoKey);
|
||||
else this.visibleTemplates.splice(idx, 1);
|
||||
}
|
||||
|
||||
public isTemplateToggled(repoKey: string) {
|
||||
return this.visibleTemplates.indexOf(repoKey) !== -1;
|
||||
}
|
||||
|
||||
public editTemplate(repoKey: string) {
|
||||
this.toggleTemplate(repoKey);
|
||||
let repoConfig = this.integration.repoTemplates.find(r => r.repoKey === repoKey);
|
||||
repoConfig.newTemplate = repoConfig.template;
|
||||
}
|
||||
|
||||
public saveTemplate(repoKey: string) {
|
||||
let repoConfig = this.integration.repoTemplates.find(r => r.repoKey === repoKey);
|
||||
repoConfig.template = repoConfig.newTemplate;
|
||||
this.updateTemplates().then(() => this.toggleTemplate(repoKey));
|
||||
}
|
||||
|
||||
public addRepository() {
|
||||
if (!this.repoKey || this.repoKey.trim().length === 0) {
|
||||
this.toaster.pop("warning", "Please enter a repository");
|
||||
return;
|
||||
}
|
||||
if (this.knownRepos.indexOf(this.repoKey) !== -1) {
|
||||
this.toaster.pop("error", "Repository " + this.repoKey + " is already being tracked");
|
||||
return;
|
||||
}
|
||||
|
||||
this.integration.repoTemplates.push({repoKey: this.repoKey, template: this.repoTemplate, newTemplate: ""});
|
||||
this.updateTemplates().then(() => this.reset());
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.repoKey = "";
|
||||
this.repoTemplate = "%{build_num}#%{build_num} (%{branch} - %{commit} : %{committer_name}): %{outcome}\n Build details : %{build_url}\n";
|
||||
}
|
||||
|
||||
public removeRepository(repoKey: string) {
|
||||
for (let i = 0; i < this.integration.repoTemplates.length; i++) {
|
||||
if (this.integration.repoTemplates[i].repoKey === repoKey) {
|
||||
this.integration.repoTemplates.splice(i, 1);
|
||||
this.updateTemplates().then(() => this.reset());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.toaster.pop("error", "Could not find target repository");
|
||||
}
|
||||
|
||||
public updateTemplates() {
|
||||
this.isUpdating = true;
|
||||
return this.api.updateIntegrationState(this.roomId, this.integration.type, this.integration.integrationType, this.scalarToken, {
|
||||
repoTemplates: this.integration.repoTemplates
|
||||
}).then(response => {
|
||||
this.integration.repoTemplates = response.repoTemplates;
|
||||
this.integration.immutableRepoTemplates = response.immutableRepoTemplates;
|
||||
this.calculateKnownRepos();
|
||||
this.isUpdating = false;
|
||||
this.toaster.pop("success", "Repositories updated");
|
||||
}).catch(err => {
|
||||
this.toaster.pop("error", err.json().error);
|
||||
console.error(err);
|
||||
this.isUpdating = false;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
// shared styling for all config screens
|
||||
.config-wrapper {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
.config-header {
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 14px;
|
||||
border-bottom: 1px solid #dadada;
|
||||
}
|
||||
|
||||
.config-header h4 {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.config-header img {
|
||||
margin-right: 7px;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 35px;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
float: right;
|
||||
margin: -17px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.config-content {
|
||||
display: block;
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
<div class="config-wrapper">
|
||||
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
|
||||
<div class="config-header">
|
||||
<img src="/img/avatars/irc.png">
|
||||
<h4>Configure IRC Bridge</h4>
|
||||
</div>
|
||||
<div class="config-content">
|
||||
<div class="alert alert-info">
|
||||
Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will
|
||||
join the channel to ensure it exists and has operators available.
|
||||
</div>
|
||||
<div *ngIf="!opsLoaded">
|
||||
<h6>Add a channel</h6>
|
||||
<div class="row form-group" style="margin-bottom: 0.1rem;">
|
||||
<label class="col-sm-2 col-form-label">Network</label>
|
||||
<div class="col-md-4">
|
||||
<select class="form-control form-control-sm" [(ngModel)]="newChannel.network" [disabled]="loadingOps">
|
||||
<option *ngFor="let network of integration.availableNetworks" [ngValue]="network.id">
|
||||
{{ network.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row form-group" style="margin-bottom: 0.1rem;">
|
||||
<label class="col-sm-2 col-form-label">Channel</label>
|
||||
<div class="col-md-4 input-group input-group-sm">
|
||||
<div class="input-group-addon">#</div>
|
||||
<input type="text" class="form-control form-control-sm" [(ngModel)]="newChannel.channel" [disabled]="loadingOps">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p *ngIf="opsError" class="text-danger">{{ opsError }}</p>
|
||||
<button type="button" class="btn btn-primary btn-sm" (click)="checkOps()" [disabled]="loadingOps">
|
||||
Next <i class="fa fa-arrow-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="opsLoaded">
|
||||
<h6>Add a channel</h6>
|
||||
<div class="row form-group" style="margin-bottom: 0.1rem;">
|
||||
<label class="col-sm-2 col-form-label">Operator</label>
|
||||
<div class="col-md-4">
|
||||
<select class="form-control form-control-sm" [(ngModel)]="newChannel.op" [disabled]="addingChannel">
|
||||
<option *ngFor="let op of channelOps" [ngValue]="op">{{ op }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>A request to bridge #{{ newChannel.channel }} on {{ getNewChannelNetworkName() }} will be sent to {{ newChannel.op }}. Once they accept, the channel will show up below.</p>
|
||||
<button type="button" class="btn btn-primary btn-sm" (click)="addChannel()" [disabled]="addingChannel">
|
||||
<i class="fa fa-plus-circle"></i> Request Bridge
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 40px;">
|
||||
<div class="col-md-12">
|
||||
<h6>Linked channels</h6>
|
||||
<p *ngIf="channelLinks.length <= 0">No linked channels.</p>
|
||||
</div>
|
||||
<div class="col-md-12" *ngFor="let link of channelLinks">
|
||||
<strong>{{ link.displayName }}</strong>
|
||||
<div *ngFor="let channel of link.channels" style="padding-left: 10px;">
|
||||
{{ channel }}
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" (click)="removeChannelLink(link, channel)" [disabled]="link.beingRemoved.indexOf(channel) !== -1">
|
||||
<i class="fa fa-times"></i> Unlink
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1 +0,0 @@
|
||||
// component styles are encapsulated and only applied to their components
|
@ -1,168 +0,0 @@
|
||||
import { Component, OnDestroy } from "@angular/core";
|
||||
import { IRCIntegration } from "../../shared/models/legacyintegration";
|
||||
import { ModalComponent, DialogRef } from "ngx-modialog";
|
||||
import { ConfigModalContext } from "../../integration/integration.component";
|
||||
import { IrcApiService } from "../../shared/services/legacy/irc-api.service";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
import { IntervalObservable } from "rxjs/observable/IntervalObservable";
|
||||
import { ApiService } from "../../shared/services/legacy/api.service";
|
||||
import { Subscription } from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: "my-irc-config",
|
||||
templateUrl: "./irc-config.component.html",
|
||||
styleUrls: ["./irc-config.component.scss", "./../config.component.scss"],
|
||||
})
|
||||
export class IrcConfigComponent implements ModalComponent<ConfigModalContext>, OnDestroy {
|
||||
|
||||
public integration: IRCIntegration;
|
||||
public loadingOps = false;
|
||||
public opsLoaded = false;
|
||||
public addingChannel = false;
|
||||
public newChannel = {
|
||||
network: "",
|
||||
channel: "",
|
||||
op: ""
|
||||
};
|
||||
public channelOps: string[];
|
||||
public opsError: string = null;
|
||||
public channelLinks: ChannelLink[];
|
||||
|
||||
private roomId: string;
|
||||
private scalarToken: string;
|
||||
private stateTimer: Subscription;
|
||||
|
||||
constructor(public dialog: DialogRef<ConfigModalContext>,
|
||||
private ircApi: IrcApiService,
|
||||
private toaster: ToasterService,
|
||||
private api: ApiService) {
|
||||
this.integration = <IRCIntegration>dialog.context.integration;
|
||||
this.roomId = dialog.context.roomId;
|
||||
this.scalarToken = dialog.context.scalarToken;
|
||||
|
||||
this.newChannel.network = this.integration.availableNetworks[0].id;
|
||||
this.buildChannelLinks();
|
||||
|
||||
this.stateTimer = IntervalObservable.create(5000).subscribe(() => {
|
||||
this.api.getIntegrationState(this.roomId, this.integration.type, this.integration.integrationType, this.scalarToken)
|
||||
.then(state => {
|
||||
for (let key in state) {
|
||||
this.integration[key] = state[key];
|
||||
}
|
||||
this.buildChannelLinks();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.stateTimer.unsubscribe();
|
||||
}
|
||||
|
||||
public checkOps(): void {
|
||||
if (this.newChannel.channel.trim().length === 0) {
|
||||
this.toaster.pop("warning", "Please enter a channel name");
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingOps = true;
|
||||
this.ircApi.getChannelOps(this.roomId, this.newChannel.network, this.newChannel.channel, this.scalarToken).then(ops => {
|
||||
this.channelOps = ops;
|
||||
if (this.channelOps.length === 0) {
|
||||
this.opsError = "No channel operators available";
|
||||
} else {
|
||||
this.newChannel.op = this.channelOps[0];
|
||||
this.loadingOps = false;
|
||||
this.opsLoaded = true;
|
||||
}
|
||||
}).catch(err => {
|
||||
this.toaster.pop("error", err.json().error);
|
||||
console.error(err);
|
||||
this.loadingOps = false;
|
||||
});
|
||||
}
|
||||
|
||||
public getNewChannelNetworkName(): string {
|
||||
for (let network of this.integration.availableNetworks) {
|
||||
if (network.id === this.newChannel.network) {
|
||||
return network.name;
|
||||
}
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
public addChannel(): void {
|
||||
this.addingChannel = true;
|
||||
this.ircApi.linkChannel(
|
||||
this.roomId,
|
||||
this.newChannel.network,
|
||||
this.newChannel.channel,
|
||||
this.newChannel.op,
|
||||
this.scalarToken)
|
||||
.then(() => {
|
||||
this.newChannel = {
|
||||
network: this.integration.availableNetworks[0].id,
|
||||
channel: "",
|
||||
op: ""
|
||||
};
|
||||
this.channelOps = [];
|
||||
this.addingChannel = false;
|
||||
this.opsLoaded = false;
|
||||
|
||||
this.toaster.pop("success", "Channel bridge requested");
|
||||
})
|
||||
.catch(err => {
|
||||
this.toaster.pop("error", err.json().error);
|
||||
console.error(err);
|
||||
this.addingChannel = false;
|
||||
});
|
||||
}
|
||||
|
||||
private buildChannelLinks(): void {
|
||||
this.channelLinks = [];
|
||||
|
||||
for (let network in this.integration.channels) {
|
||||
if (this.integration.channels[network].length <= 0) continue;
|
||||
|
||||
let displayName = "Unknown Network";
|
||||
for (let parentNetwork of this.integration.availableNetworks) {
|
||||
if (parentNetwork.id === network) {
|
||||
displayName = parentNetwork.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.channelLinks.push({
|
||||
displayName: displayName,
|
||||
network: network,
|
||||
channels: this.integration.channels[network],
|
||||
beingRemoved: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public removeChannelLink(link: ChannelLink, channel: string): void {
|
||||
link.beingRemoved.push(channel);
|
||||
this.ircApi.unlinkChannel(this.roomId, link.network, channel.substring(1), this.scalarToken).then(() => {
|
||||
this.toaster.pop("success", "Channel " + channel + " unlinked");
|
||||
|
||||
link.channels.splice(link.channels.indexOf(channel), 1);
|
||||
link.beingRemoved.splice(link.beingRemoved.indexOf(channel), 1);
|
||||
|
||||
if (link.channels.length === 0) {
|
||||
this.channelLinks.splice(this.channelLinks.indexOf(link), 1);
|
||||
}
|
||||
}).catch(err => {
|
||||
this.toaster.pop("error", err.json().error);
|
||||
console.error(err);
|
||||
|
||||
link.beingRemoved.splice(link.beingRemoved.indexOf(channel), 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface ChannelLink {
|
||||
displayName: string;
|
||||
channels: string[];
|
||||
network: string;
|
||||
beingRemoved: string[];
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<div class="config-wrapper">
|
||||
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
|
||||
<div class="config-header">
|
||||
<img [src]="integration.avatar">
|
||||
<h4>Configure RSS/Atom Feeds</h4>
|
||||
</div>
|
||||
<div class="config-content">
|
||||
<form (submit)="addFeed()" novalidate name="addFeedForm">
|
||||
<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"
|
||||
[(ngModel)]="feedUrl" name="feedUrl"
|
||||
[disabled]="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
|
||||
<i class="fa fa-plus-circle"></i> Add Feed
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 removable-feed" *ngFor="let feed of integration.feeds trackById">
|
||||
<a [href]="feed" rel="noopener" target="_blank">{{ feed }}</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeFeed(feed)"
|
||||
style="margin-top: -5px;" [disabled]="isUpdating">
|
||||
<i class="fa fa-times"></i> Remove Feed
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-12" *ngIf="integration.immutableFeeds.length > 0">
|
||||
<h6 class="other-feeds-title">Feeds from other users in the room</h6>
|
||||
</div>
|
||||
<div class="col-md-12 feed-list" *ngFor="let feed of integration.immutableFeeds trackById">
|
||||
<a [href]="feed.url" rel="noopener" target="_blank">{{ feed.url }}</a> <span class="text-muted">(added by {{ feed.ownerId }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -1,13 +0,0 @@
|
||||
// component styles are encapsulated and only applied to their components
|
||||
.feed-list {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.removable-feed {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.other-feeds-title {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 0;
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { RSSIntegration } from "../../shared/models/legacyintegration";
|
||||
import { ModalComponent, DialogRef } from "ngx-modialog";
|
||||
import { ConfigModalContext } from "../../integration/integration.component";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
import { ApiService } from "../../shared/services/legacy/api.service";
|
||||
|
||||
@Component({
|
||||
selector: "my-rss-config",
|
||||
templateUrl: "./rss-config.component.html",
|
||||
styleUrls: ["./rss-config.component.scss", "./../config.component.scss"],
|
||||
})
|
||||
export class RssConfigComponent implements ModalComponent<ConfigModalContext> {
|
||||
|
||||
public integration: RSSIntegration;
|
||||
|
||||
public isUpdating = false;
|
||||
public feedUrl = "";
|
||||
|
||||
private roomId: string;
|
||||
private scalarToken: string;
|
||||
|
||||
constructor(public dialog: DialogRef<ConfigModalContext>,
|
||||
private toaster: ToasterService,
|
||||
private api: ApiService) {
|
||||
this.integration = <RSSIntegration>dialog.context.integration;
|
||||
this.roomId = dialog.context.roomId;
|
||||
this.scalarToken = dialog.context.scalarToken;
|
||||
}
|
||||
|
||||
public addFeed() {
|
||||
if (!this.feedUrl || this.feedUrl.trim().length === 0) {
|
||||
this.toaster.pop("warning", "Please enter a feed URL");
|
||||
return;
|
||||
}
|
||||
if (this.integration.feeds.indexOf(this.feedUrl) !== -1) {
|
||||
this.toaster.pop("error", "This feed has already been added");
|
||||
return;
|
||||
}
|
||||
|
||||
let feedCopy = JSON.parse(JSON.stringify(this.integration.feeds));
|
||||
feedCopy.push(this.feedUrl);
|
||||
this.updateFeeds(feedCopy).then(() => this.feedUrl = "");
|
||||
}
|
||||
|
||||
public removeFeed(feedUrl) {
|
||||
let feedCopy = JSON.parse(JSON.stringify(this.integration.feeds));
|
||||
const idx = feedCopy.indexOf(feedUrl);
|
||||
feedCopy.splice(idx, 1);
|
||||
this.updateFeeds(feedCopy);
|
||||
}
|
||||
|
||||
private updateFeeds(newFeeds) {
|
||||
this.isUpdating = true;
|
||||
return this.api.updateIntegrationState(this.roomId, this.integration.type, this.integration.integrationType, this.scalarToken, {
|
||||
feeds: newFeeds
|
||||
}).then(response => {
|
||||
this.integration.feeds = response.feeds;
|
||||
this.integration.immutableFeeds = response.immutableFeeds;
|
||||
this.isUpdating = false;
|
||||
this.toaster.pop("success", "Feeds updated");
|
||||
}).catch(err => {
|
||||
this.toaster.pop("error", err.json().error);
|
||||
console.error(err);
|
||||
this.isUpdating = false;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
<div class="config-wrapper">
|
||||
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
|
||||
<div class="config-header">
|
||||
<img [src]="integration.avatar">
|
||||
<h4>Configure Travis CI hooks</h4>
|
||||
</div>
|
||||
<div class="config-content">
|
||||
<form (submit)="addRepository()" novalidate name="addRepoForm">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="margin-bottom: 12px;">
|
||||
<h6>.travis.yml configuration</h6>
|
||||
The following will need to be added to your .travis.yml file:
|
||||
<pre class="yaml">{{ travisYaml }}</pre>
|
||||
</div>
|
||||
<div class="col-md-8" style="margin-bottom: 12px;">
|
||||
<h6>Your Travis CI hooks</h6>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control"
|
||||
placeholder="owner/repo-name"
|
||||
[(ngModel)]="repoKey" name="repoKey"
|
||||
[disabled]="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
|
||||
<i class="fa fa-plus-circle"></i> Add Repository
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 removable" *ngFor="let repo of integration.repoTemplates trackById">
|
||||
{{ repo.repoKey }}
|
||||
<button type="button" class="btn btn-outline-info btn-sm" (click)="editTemplate(repo.repoKey)"
|
||||
style="margin-top: -5px;" [disabled]="isUpdating" *ngIf="!isTemplateToggled(repo.repoKey)">
|
||||
<i class="fa fa-pencil"></i> Edit
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeRepository(repo.repoKey)"
|
||||
style="margin-top: -5px;" [disabled]="isUpdating">
|
||||
<i class="fa fa-times"></i> Remove Repository
|
||||
</button>
|
||||
<div *ngIf="isTemplateToggled(repo.repoKey)">
|
||||
<textarea [(ngModel)]="repo.newTemplate" name="template-{{repo.repoKey}}"
|
||||
style="width: 100%; height: 100px; margin-top: 5px;"></textarea>
|
||||
<button type="button" class="btn btn-primary btn-sm" (click)="saveTemplate(repo.repoKey)">Save
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline btn-sm" (click)="toggleTemplate(repo.repoKey)">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12" *ngIf="integration.immutableRepoTemplates.length > 0">
|
||||
<h6 class="other-items-title">Hooks from other users in the room</h6>
|
||||
</div>
|
||||
<div class="col-md-12 list" *ngFor="let repo of integration.immutableRepoTemplates trackById">
|
||||
{{ repo.repoKey }} <span class="text-muted">(added by {{ repo.ownerId }})</span>
|
||||
<button type="button" class="btn btn-outline-info btn-sm" (click)="toggleTemplate(repo.repoKey)"
|
||||
style="margin-top: -5px;" [disabled]="isUpdating">
|
||||
{{ isTemplateToggled(repo.repoKey) ? "Hide" : "Show" }} Template
|
||||
</button>
|
||||
<pre *ngIf="isTemplateToggled(repo.repoKey)">{{ repo.template }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -1,19 +0,0 @@
|
||||
// component styles are encapsulated and only applied to their components
|
||||
.list {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.removable {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.other-items-title {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.yaml {
|
||||
border: 1px solid #ccc;
|
||||
background: #eee;
|
||||
padding: 5px;
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { TravisCiIntegration } from "../../shared/models/legacyintegration";
|
||||
import { ModalComponent, DialogRef } from "ngx-modialog";
|
||||
import { ConfigModalContext } from "../../integration/integration.component";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
import { ApiService } from "../../shared/services/legacy/api.service";
|
||||
|
||||
@Component({
|
||||
selector: "my-travisci-config",
|
||||
templateUrl: "./travisci-config.component.html",
|
||||
styleUrls: ["./travisci-config.component.scss", "./../config.component.scss"],
|
||||
})
|
||||
export class TravisCiConfigComponent implements ModalComponent<ConfigModalContext> {
|
||||
|
||||
public integration: TravisCiIntegration;
|
||||
|
||||
public isUpdating = false;
|
||||
public repoKey = "";
|
||||
public repoTemplate = "";
|
||||
public travisYaml = "";
|
||||
|
||||
private roomId: string;
|
||||
private scalarToken: string;
|
||||
private knownRepos: string[] = [];
|
||||
private visibleTemplates = [];
|
||||
|
||||
constructor(public dialog: DialogRef<ConfigModalContext>,
|
||||
private toaster: ToasterService,
|
||||
private api: ApiService) {
|
||||
this.integration = <TravisCiIntegration>dialog.context.integration;
|
||||
this.roomId = dialog.context.roomId;
|
||||
this.scalarToken = dialog.context.scalarToken;
|
||||
|
||||
this.travisYaml = "notifications:\n webhooks:\n urls:\n - " + this.integration.webhookUrl + "\n on_success: change # always | never | change\n on_failure: always\n on_start: never";
|
||||
|
||||
this.calculateKnownRepos();
|
||||
this.reset();
|
||||
}
|
||||
|
||||
private calculateKnownRepos() {
|
||||
for (let repo of this.integration.repoTemplates)
|
||||
this.knownRepos.push(repo.repoKey);
|
||||
for (let immutableRepo of this.integration.immutableRepoTemplates)
|
||||
this.knownRepos.push(immutableRepo.repoKey);
|
||||
}
|
||||
|
||||
public toggleTemplate(repoKey: string) {
|
||||
let idx = this.visibleTemplates.indexOf(repoKey);
|
||||
if (idx === -1) this.visibleTemplates.push(repoKey);
|
||||
else this.visibleTemplates.splice(idx, 1);
|
||||
}
|
||||
|
||||
public isTemplateToggled(repoKey: string) {
|
||||
return this.visibleTemplates.indexOf(repoKey) !== -1;
|
||||
}
|
||||
|
||||
public editTemplate(repoKey: string) {
|
||||
this.toggleTemplate(repoKey);
|
||||
let repoConfig = this.integration.repoTemplates.find(r => r.repoKey === repoKey);
|
||||
repoConfig.newTemplate = repoConfig.template;
|
||||
}
|
||||
|
||||
public saveTemplate(repoKey: string) {
|
||||
let repoConfig = this.integration.repoTemplates.find(r => r.repoKey === repoKey);
|
||||
repoConfig.template = repoConfig.newTemplate;
|
||||
this.updateTemplates().then(() => this.toggleTemplate(repoKey));
|
||||
}
|
||||
|
||||
public addRepository() {
|
||||
if (!this.repoKey || this.repoKey.trim().length === 0) {
|
||||
this.toaster.pop("warning", "Please enter a repository");
|
||||
return;
|
||||
}
|
||||
if (this.knownRepos.indexOf(this.repoKey) !== -1) {
|
||||
this.toaster.pop("error", "Repository " + this.repoKey + " is already being tracked");
|
||||
return;
|
||||
}
|
||||
|
||||
this.integration.repoTemplates.push({repoKey: this.repoKey, template: this.repoTemplate, newTemplate: ""});
|
||||
this.updateTemplates().then(() => this.reset());
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.repoKey = "";
|
||||
this.repoTemplate = "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}\n Change view : %{compare_url}\n Build details : %{build_url}\n";
|
||||
}
|
||||
|
||||
public removeRepository(repoKey: string) {
|
||||
for (let i = 0; i < this.integration.repoTemplates.length; i++) {
|
||||
if (this.integration.repoTemplates[i].repoKey === repoKey) {
|
||||
this.integration.repoTemplates.splice(i, 1);
|
||||
this.updateTemplates().then(() => this.reset());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.toaster.pop("error", "Could not find target repository");
|
||||
}
|
||||
|
||||
public updateTemplates() {
|
||||
this.isUpdating = true;
|
||||
return this.api.updateIntegrationState(this.roomId, this.integration.type, this.integration.integrationType, this.scalarToken, {
|
||||
repoTemplates: this.integration.repoTemplates
|
||||
}).then(response => {
|
||||
this.integration.repoTemplates = response.repoTemplates;
|
||||
this.integration.immutableRepoTemplates = response.immutableRepoTemplates;
|
||||
this.calculateKnownRepos();
|
||||
this.isUpdating = false;
|
||||
this.toaster.pop("success", "Repositories updated");
|
||||
}).catch(err => {
|
||||
this.toaster.pop("error", err.json().error);
|
||||
console.error(err);
|
||||
this.isUpdating = false;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
<div class="config-wrapper">
|
||||
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
|
||||
<div class="config-header">
|
||||
<img [src]="integration.avatar">
|
||||
<h4>Configure custom widgets</h4>
|
||||
</div>
|
||||
<div class="config-content" *ngIf="isLoading">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p><i class="fa fa-circle-notch fa-spin"></i> Loading widgets...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-content" *ngIf="!isLoading">
|
||||
<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="Custom widget URL"
|
||||
[(ngModel)]="newWidget.dimension.newUrl" name="newWidgetUrl"
|
||||
[disabled]="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
|
||||
<i class="fa fa-plus-circle"></i> Add Widget
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
|
||||
{{ widget.name || widget.url }} <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 Widget
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeWidget(widget)"
|
||||
style="margin-top: -5px;" [disabled]="isUpdating">
|
||||
<i class="fa fa-times"></i> Remove Widget
|
||||
</button>
|
||||
<div *ngIf="isWidgetToggled(widget)">
|
||||
<label>
|
||||
Widget Name
|
||||
<input type="text" class="form-control"
|
||||
placeholder="Custom Widget"
|
||||
[(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.dimension.newUrl" name="widget-url-{{widget.id}}"
|
||||
[disabled]="isUpdating">
|
||||
</label>
|
||||
<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)">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -1,4 +0,0 @@
|
||||
// component styles are encapsulated and only applied to their components
|
||||
.widget-item {
|
||||
margin-top: 3px;
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { DialogRef, ModalComponent } from "ngx-modialog";
|
||||
import { WidgetComponent } from "../widget.component";
|
||||
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
|
||||
import { ConfigModalContext } from "../../../integration/integration.component";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
import { WIDGET_CUSTOM } from "../../../shared/models/widget";
|
||||
|
||||
@Component({
|
||||
selector: "my-customwidget-config",
|
||||
templateUrl: "custom_widget-config.component.html",
|
||||
styleUrls: ["custom_widget-config.component.scss", "./../../config.component.scss"],
|
||||
})
|
||||
export class CustomWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> {
|
||||
|
||||
constructor(public dialog: DialogRef<ConfigModalContext>,
|
||||
toaster: ToasterService,
|
||||
scalarService: ScalarClientApiService,
|
||||
window: Window) {
|
||||
super(
|
||||
window,
|
||||
toaster,
|
||||
scalarService,
|
||||
dialog.context.roomId,
|
||||
dialog.context.integration,
|
||||
dialog.context.integrationId,
|
||||
WIDGET_CUSTOM,
|
||||
"Custom Widget",
|
||||
"generic" // wrapper
|
||||
);
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
<div class="config-wrapper">
|
||||
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
|
||||
<div class="config-header">
|
||||
<img [src]="integration.avatar">
|
||||
<h4>Configure Etherpad widgets</h4>
|
||||
</div>
|
||||
<div class="config-content" *ngIf="isLoading">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p><i class="fa fa-circle-notch fa-spin"></i> Loading widgets...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-content" *ngIf="!isLoading">
|
||||
<form (submit)="validateAndAddWidget()" 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="Etherpad name or URL"
|
||||
[(ngModel)]="newWidget.dimension.newUrl" name="newWidgetUrl"
|
||||
[disabled]="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
|
||||
<i class="fa fa-plus-circle"></i> Add Widget
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
|
||||
{{ widget.name || widget.url }} <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 Widget
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeWidget(widget)"
|
||||
style="margin-top: -5px;" [disabled]="isUpdating">
|
||||
<i class="fa fa-times"></i> Remove Widget
|
||||
</button>
|
||||
<div *ngIf="isWidgetToggled(widget)">
|
||||
<label>
|
||||
Pad Name
|
||||
<input type="text" class="form-control"
|
||||
placeholder="Etherpad Widget"
|
||||
[(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.dimension.newUrl" name="widget-url-{{widget.id}}"
|
||||
[disabled]="isUpdating">
|
||||
</label>
|
||||
<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)">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -1,4 +0,0 @@
|
||||
// component styles are encapsulated and only applied to their components
|
||||
.widget-item {
|
||||
margin-top: 3px;
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { DialogRef, ModalComponent } from "ngx-modialog";
|
||||
import { WidgetComponent } from "../widget.component";
|
||||
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
|
||||
import { ConfigModalContext } from "../../../integration/integration.component";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
import { WIDGET_ETHERPAD } from "../../../shared/models/widget";
|
||||
import { EtherpadWidgetIntegration } from "../../../shared/models/legacyintegration";
|
||||
|
||||
@Component({
|
||||
selector: "my-etherpadwidget-config",
|
||||
templateUrl: "etherpad-config.component.html",
|
||||
styleUrls: ["etherpad-config.component.scss", "./../../config.component.scss"],
|
||||
})
|
||||
export class EtherpadWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> {
|
||||
|
||||
private widgetOpts: EtherpadWidgetIntegration;
|
||||
|
||||
constructor(public dialog: DialogRef<ConfigModalContext>,
|
||||
toaster: ToasterService,
|
||||
scalarService: ScalarClientApiService,
|
||||
window: Window) {
|
||||
super(
|
||||
window,
|
||||
toaster,
|
||||
scalarService,
|
||||
dialog.context.roomId,
|
||||
dialog.context.integration,
|
||||
dialog.context.integrationId,
|
||||
WIDGET_ETHERPAD,
|
||||
"Etherpad Widget",
|
||||
"generic", // wrapper
|
||||
"etherpad" // scalar wrapper
|
||||
);
|
||||
|
||||
this.widgetOpts = <EtherpadWidgetIntegration>dialog.context.integration;
|
||||
}
|
||||
|
||||
public validateAndAddWidget() {
|
||||
if (this.newWidget.dimension.newUrl.startsWith("http://") || this.newWidget.dimension.newUrl.startsWith("https://")) {
|
||||
this.newWidget.dimension.newName = "Etherpad";
|
||||
} else {
|
||||
this.newWidget.dimension.newName = this.newWidget.dimension.newUrl;
|
||||
this.newWidget.dimension.newUrl = this.generatePadUrl(this.newWidget.dimension.newName);
|
||||
}
|
||||
|
||||
this.addWidget();
|
||||
}
|
||||
|
||||
private generatePadUrl(forName: string): string {
|
||||
let template = "https://demo.riot.im/etherpad/p/$roomId_$padName";
|
||||
if (this.widgetOpts && this.widgetOpts.defaultUrl) {
|
||||
template = this.widgetOpts.defaultUrl;
|
||||
}
|
||||
|
||||
template = template.replace("$roomId", encodeURIComponent(this.roomId));
|
||||
template = template.replace("$padName", encodeURIComponent(forName));
|
||||
|
||||
return template;
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
<div class="config-wrapper">
|
||||
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
|
||||
<div class="config-header">
|
||||
<img [src]="integration.avatar">
|
||||
<h4>Configure Google Calendars</h4>
|
||||
</div>
|
||||
<div class="config-content" *ngIf="isLoading">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p><i class="fa fa-circle-notch fa-spin"></i> Loading widgets...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-content" *ngIf="!isLoading">
|
||||
<form (submit)="validateAndAddWidget()" 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="Calendar ID, example: en.uk#holiday@group.v.calendar.google.com"
|
||||
[(ngModel)]="newWidget.dimension.newData.src" name="newWidgetUrl"
|
||||
[disabled]="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
|
||||
<i class="fa fa-plus-circle"></i> Add Widget
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
|
||||
{{ widget.name || widget.url }} <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 Widget
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeWidget(widget)"
|
||||
style="margin-top: -5px;" [disabled]="isUpdating">
|
||||
<i class="fa fa-times"></i> Remove Widget
|
||||
</button>
|
||||
<div *ngIf="isWidgetToggled(widget)">
|
||||
<label class="col-md-8" style="padding-left: 0; margin-left: 0;">
|
||||
Shared Calendar ID
|
||||
<input type="text" class="form-control"
|
||||
placeholder="en.uk#holiday@group.v.calendar.google.com"
|
||||
[(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)">
|
||||
Save
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline btn-sm" (click)="toggleWidget(widget)">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -1,4 +0,0 @@
|
||||
// component styles are encapsulated and only applied to their components
|
||||
.widget-item {
|
||||
margin-top: 3px;
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { DialogRef, ModalComponent } from "ngx-modialog";
|
||||
import { WidgetComponent } from "../widget.component";
|
||||
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
|
||||
import { ConfigModalContext } from "../../../integration/integration.component";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
import { EditableWidget, WIDGET_GOOGLE_CALENDAR } from "../../../shared/models/widget";
|
||||
|
||||
@Component({
|
||||
selector: "my-googlecalendarwidget-config",
|
||||
templateUrl: "googlecalendar-config.component.html",
|
||||
styleUrls: ["googlecalendar-config.component.scss", "./../../config.component.scss"],
|
||||
})
|
||||
export class GoogleCalendarWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> {
|
||||
|
||||
constructor(public dialog: DialogRef<ConfigModalContext>,
|
||||
toaster: ToasterService,
|
||||
scalarService: ScalarClientApiService,
|
||||
window: Window) {
|
||||
super(
|
||||
window,
|
||||
toaster,
|
||||
scalarService,
|
||||
dialog.context.roomId,
|
||||
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 onWidgetsDiscovered() {
|
||||
for (const widget of this.widgets) {
|
||||
if (widget.data.dimSrc) {
|
||||
// Convert legacy Dimension widgets to use src
|
||||
widget.data.src = widget.data.dimSrc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public validateAndAddWidget() {
|
||||
this.setCalendarUrl(this.newWidget);
|
||||
this.addWidget();
|
||||
}
|
||||
|
||||
public validateAndSaveWidget(widget: EditableWidget) {
|
||||
this.setCalendarUrl(widget);
|
||||
this.saveWidget(widget);
|
||||
}
|
||||
|
||||
private setCalendarUrl(widget: EditableWidget) {
|
||||
const encodedId = encodeURIComponent(widget.dimension.newData.src);
|
||||
widget.dimension.newUrl = window.location.origin + "/widget/gcal?calendarId=" + encodedId;
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
<div class="config-wrapper">
|
||||
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
|
||||
<div class="config-header">
|
||||
<img [src]="integration.avatar">
|
||||
<h4>Configure embedded Google Docs</h4>
|
||||
</div>
|
||||
<div class="config-content" *ngIf="isLoading">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p><i class="fa fa-circle-notch fa-spin"></i> Loading widgets...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-content" *ngIf="!isLoading">
|
||||
<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)]="newWidget.dimension.newUrl" name="newWidgetUrl"
|
||||
[disabled]="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
|
||||
<i class="fa fa-plus-circle"></i> Add Widget
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
|
||||
{{ widget.name || widget.url }} <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 Widget
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeWidget(widget)"
|
||||
style="margin-top: -5px;" [disabled]="isUpdating">
|
||||
<i class="fa fa-times"></i> Remove Widget
|
||||
</button>
|
||||
<div *ngIf="isWidgetToggled(widget)">
|
||||
<label class="col-md-8" style="padding-left: 0; margin-left: 0;">
|
||||
Google Doc
|
||||
<input type="text" class="form-control"
|
||||
placeholder="Link to Google Doc"
|
||||
[(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
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline btn-sm" (click)="toggleWidget(widget)">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -1,4 +0,0 @@
|
||||
// component styles are encapsulated and only applied to their components
|
||||
.widget-item {
|
||||
margin-top: 3px;
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { DialogRef, ModalComponent } from "ngx-modialog";
|
||||
import { WidgetComponent } from "../widget.component";
|
||||
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
|
||||
import { ConfigModalContext } from "../../../integration/integration.component";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
import { WIDGET_GOOGLE_DOCS } from "../../../shared/models/widget";
|
||||
|
||||
@Component({
|
||||
selector: "my-googledocswidget-config",
|
||||
templateUrl: "googledocs-config.component.html",
|
||||
styleUrls: ["googledocs-config.component.scss", "./../../config.component.scss"],
|
||||
})
|
||||
export class GoogleDocsWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> {
|
||||
|
||||
constructor(public dialog: DialogRef<ConfigModalContext>,
|
||||
toaster: ToasterService,
|
||||
scalarService: ScalarClientApiService,
|
||||
window: Window) {
|
||||
super(
|
||||
window,
|
||||
toaster,
|
||||
scalarService,
|
||||
dialog.context.roomId,
|
||||
dialog.context.integration,
|
||||
dialog.context.integrationId,
|
||||
WIDGET_GOOGLE_DOCS,
|
||||
"Google Docs",
|
||||
"generic", // wrapper
|
||||
"googleDocs" // scalar wrapper
|
||||
);
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
<div class="config-wrapper">
|
||||
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
|
||||
<div class="config-header">
|
||||
<img [src]="integration.avatar">
|
||||
<h4>Configure Jitsi Conferences</h4>
|
||||
</div>
|
||||
<div class="config-content" *ngIf="isLoading">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p><i class="fa fa-circle-notch fa-spin"></i> Loading widgets...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-content" *ngIf="!isLoading">
|
||||
<form (submit)="validateAndAddWidget()" novalidate name="addForm">
|
||||
<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://{{ newWidget.dimension.newData.domain }}/</span>
|
||||
<input type="text" class="form-control"
|
||||
placeholder="MyConferenceName"
|
||||
[(ngModel)]="newWidget.dimension.newData.conferenceId" name="newWidgetName"
|
||||
[disabled]="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
|
||||
<i class="fa fa-plus-circle"></i> Add
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
|
||||
{{ 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
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeWidget(widget)"
|
||||
style="margin-top: -5px;" [disabled]="isUpdating">
|
||||
<i class="fa fa-times"></i> Remove
|
||||
</button>
|
||||
<div *ngIf="isWidgetToggled(widget)">
|
||||
<label class="col-md-8" style="padding-left: 0; margin-left: 0;">
|
||||
Conference URL
|
||||
<input type="text" class="form-control"
|
||||
placeholder="https://jitsi.riot.im/MyConference"
|
||||
[(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)">
|
||||
Save
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline btn-sm" (click)="toggleWidget(widget)">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -1,4 +0,0 @@
|
||||
// component styles are encapsulated and only applied to their components
|
||||
.widget-item {
|
||||
margin-top: 3px;
|
||||
}
|
@ -1,140 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { DialogRef, ModalComponent } from "ngx-modialog";
|
||||
import { WidgetComponent } from "../widget.component";
|
||||
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
|
||||
import { ConfigModalContext } from "../../../integration/integration.component";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
import { EditableWidget, WIDGET_JITSI } from "../../../shared/models/widget";
|
||||
import { JitsiWidgetIntegration } from "../../../shared/models/legacyintegration";
|
||||
import * as gobyInit from "goby";
|
||||
import * as url from "url";
|
||||
|
||||
const goby = gobyInit.init({
|
||||
// Converts words to a url-safe name
|
||||
// Ie: "hello world how-are you" becomes "HelloWorldHowAreYou"
|
||||
decorator: parts => parts.map(p => p ? p.split('-').map(p2 => p2 ? p2[0].toUpperCase() + p2.substring(1).toLowerCase() : '').join('') : '').join(''),
|
||||
});
|
||||
|
||||
@Component({
|
||||
selector: "my-jitsi-config",
|
||||
templateUrl: "jitsi-config.component.html",
|
||||
styleUrls: ["jitsi-config.component.scss", "./../../config.component.scss"],
|
||||
})
|
||||
export class JitsiWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> {
|
||||
|
||||
public integration: JitsiWidgetIntegration;
|
||||
|
||||
constructor(public dialog: DialogRef<ConfigModalContext>,
|
||||
toaster: ToasterService,
|
||||
scalarService: ScalarClientApiService,
|
||||
window: Window) {
|
||||
super(
|
||||
window,
|
||||
toaster,
|
||||
scalarService,
|
||||
dialog.context.roomId,
|
||||
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;
|
||||
}
|
||||
|
||||
protected onNewWidgetPrepared() {
|
||||
this.newWidget.dimension.newData.conferenceId = this.generateConferenceId();
|
||||
this.newWidget.dimension.newData.domain = this.integration.jitsiDomain;
|
||||
this.newWidget.dimension.newData.isAudioConf = false;
|
||||
}
|
||||
|
||||
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"];
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
if (!this.newWidget.dimension.newData.conferenceId) {
|
||||
this.toaster.pop("warning", "Please enter a conference name");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setJitsiUrl(this.newWidget);
|
||||
this.addWidget();
|
||||
}
|
||||
|
||||
public validateAndSaveWidget(widget: EditableWidget) {
|
||||
if (!widget.dimension.newData.conferenceUrl) {
|
||||
this.toaster.pop("warning", "Please enter a conference URL");
|
||||
return;
|
||||
}
|
||||
|
||||
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 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: {
|
||||
// TODO: Use templating when mobile riot supports it
|
||||
"confId": conferenceId, // named confId for compatibility with mobile clients
|
||||
"domain": domain,
|
||||
"isAudioConf": isAudioConf,
|
||||
"displayName": "$matrix_display_name",
|
||||
"avatarUrl": "$matrix_avatar_url",
|
||||
"userId": "$matrix_user_id",
|
||||
},
|
||||
});
|
||||
widgetQueryString = this.decodeParams(widgetQueryString, Object.keys(widget.dimension.newData).map(k => "$" + k));
|
||||
|
||||
widget.dimension.newUrl = window.location.origin + "/widgets/jitsi" + widgetQueryString;
|
||||
widget.dimension.newData.conferenceUrl = "https://" + domain + "/" + conferenceId;
|
||||
}
|
||||
|
||||
private generateConferenceId() {
|
||||
return goby.generate(["adj", "pre", "suf"]);
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
<div class="config-wrapper">
|
||||
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
|
||||
<div class="config-header">
|
||||
<img [src]="integration.avatar">
|
||||
<h4>Configure Twitch Livestream widgets</h4>
|
||||
</div>
|
||||
<div class="config-content" *ngIf="isLoading">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p><i class="fa fa-circle-notch fa-spin"></i> Loading widgets...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-content" *ngIf="!isLoading">
|
||||
<form (submit)="validateAndAddWidget()" 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="Twitch Channel Name"
|
||||
[(ngModel)]="newWidget.dimension.newData.channelName" name="newWidgetUrl"
|
||||
[disabled]="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
|
||||
<i class="fa fa-plus-circle"></i> Add Widget
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
|
||||
{{ widget.name || widget.url }} <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 Widget
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeWidget(widget)"
|
||||
style="margin-top: -5px;" [disabled]="isUpdating">
|
||||
<i class="fa fa-times"></i> Remove Widget
|
||||
</button>
|
||||
<div *ngIf="isWidgetToggled(widget)">
|
||||
<label>
|
||||
Widget Name
|
||||
<input type="text" class="form-control"
|
||||
placeholder="Twitch Widget"
|
||||
[(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.dimension.newData.channelName" name="widget-url-{{widget.id}}"
|
||||
[disabled]="isUpdating">
|
||||
</label>
|
||||
<button type="button" class="btn btn-primary btn-sm" (click)="validateAndSaveWidget(widget)">
|
||||
Save
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline btn-sm" (click)="toggleWidget(widget)">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -1,4 +0,0 @@
|
||||
// component styles are encapsulated and only applied to their components
|
||||
.widget-item {
|
||||
margin-top: 3px;
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { DialogRef, ModalComponent } from "ngx-modialog";
|
||||
import { WidgetComponent } from "../widget.component";
|
||||
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
|
||||
import { ConfigModalContext } from "../../../integration/integration.component";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
import { EditableWidget, WIDGET_TWITCH } from "../../../shared/models/widget";
|
||||
|
||||
@Component({
|
||||
selector: "my-twitchwidget-config",
|
||||
templateUrl: "twitch-config.component.html",
|
||||
styleUrls: ["twitch-config.component.scss", "./../../config.component.scss"],
|
||||
})
|
||||
export class TwitchWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> {
|
||||
|
||||
constructor(public dialog: DialogRef<ConfigModalContext>,
|
||||
toaster: ToasterService,
|
||||
scalarService: ScalarClientApiService,
|
||||
window: Window) {
|
||||
super(
|
||||
window,
|
||||
toaster,
|
||||
scalarService,
|
||||
dialog.context.roomId,
|
||||
dialog.context.integration,
|
||||
dialog.context.integrationId,
|
||||
WIDGET_TWITCH,
|
||||
"Twitch Widget",
|
||||
"video", // wrapper
|
||||
"twitch" // scalar wrapper
|
||||
);
|
||||
}
|
||||
|
||||
protected onNewWidgetPrepared() {
|
||||
this.newWidget.dimension.newData.channelName = "";
|
||||
}
|
||||
|
||||
protected onWidgetsDiscovered() {
|
||||
for (const widget of this.widgets) {
|
||||
// Convert dimChannelName to channelName
|
||||
if (!widget.data.channelName) {
|
||||
widget.data.channelName = widget.data.dimChannelName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public validateAndAddWidget() {
|
||||
if (!this.newWidget.dimension.newData.channelName) {
|
||||
this.toaster.pop("warning", "Please enter a Twitch Livestream channel name");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setTwitchUrl(this.newWidget);
|
||||
this.addWidget();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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,395 +0,0 @@
|
||||
import { ScalarClientApiService } from "../../shared/services/scalar-client-api.service";
|
||||
import { convertScalarWidgetsToDtos, EditableWidget } from "../../shared/models/widget";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
import { LegacyIntegration } from "../../shared/models/legacyintegration";
|
||||
|
||||
const SCALAR_WIDGET_LINKS = [
|
||||
"https://scalar-staging.riot.im/scalar/api/widgets/__TYPE__.html?url=",
|
||||
"https://scalar-staging.vector.im/scalar/api/widgets/__TYPE__.html?url=",
|
||||
"https://scalar-develop.riot.im/scalar/api/widgets/__TYPE__.html?url=",
|
||||
"https://demo.riot.im/scalar/api/widgets/__TYPE__.html?url=",
|
||||
];
|
||||
|
||||
export class WidgetComponent {
|
||||
|
||||
public isLoading = true;
|
||||
public isUpdating = false;
|
||||
public widgets: EditableWidget[];
|
||||
public newWidget: EditableWidget;
|
||||
|
||||
private toggledWidgetIds: string[] = [];
|
||||
private scalarWrapperUrls: string[] = [];
|
||||
private wrapperUrl = "";
|
||||
|
||||
constructor(window: Window,
|
||||
protected toaster: ToasterService,
|
||||
protected scalarApi: ScalarClientApiService,
|
||||
public roomId: string,
|
||||
public integration: LegacyIntegration,
|
||||
editWidgetId: string,
|
||||
private widgetIds: string[],
|
||||
private defaultName: string,
|
||||
private wrapperId = "generic",
|
||||
private scalarWrapperId = null) {
|
||||
this.isLoading = true;
|
||||
this.isUpdating = false;
|
||||
|
||||
if (wrapperId) {
|
||||
this.wrapperUrl = window.location.origin + "/widgets/" + wrapperId + "?url=";
|
||||
|
||||
if (!scalarWrapperId) scalarWrapperId = wrapperId;
|
||||
for (let widgetLink of SCALAR_WIDGET_LINKS) {
|
||||
this.scalarWrapperUrls.push(widgetLink.replace("__TYPE__", scalarWrapperId));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// See if we should request editing a particular widget
|
||||
if (editWidgetId) {
|
||||
for (let widget of this.widgets) {
|
||||
if (widget.id === editWidgetId) {
|
||||
console.log("Requesting edit for " + widget.id);
|
||||
this.editWidget(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 ? widget.data.title : null,
|
||||
newData: JSON.parse(JSON.stringify(widget.data || "{}")),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 => convertScalarWidgetsToDtos(resp))
|
||||
.then(widgets => widgets.filter(w => types.indexOf(w.type) !== -1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
for (let scalarUrl of urls) {
|
||||
if (url.startsWith(scalarUrl)) {
|
||||
return decodeURIComponent(url.substring(scalarUrl.length));
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
encodedURL = this.decodeParams(encodedURL, customVars);
|
||||
|
||||
return encodedURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.onWidgetBeforeAdd();
|
||||
return this.scalarApi.setWidget(this.roomId, this.newWidget)
|
||||
.then(() => this.widgets.push(this.newWidget))
|
||||
.then(() => {
|
||||
this.isUpdating = false;
|
||||
this.onWidgetAfterAdd();
|
||||
this.prepareNewWidget();
|
||||
this.toaster.pop("success", "Widget added!");
|
||||
})
|
||||
.catch(err => {
|
||||
this.isUpdating = false;
|
||||
console.error(err);
|
||||
this.toaster.pop("error", err.json().error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
this.packWidget(widget);
|
||||
|
||||
this.isUpdating = true;
|
||||
this.onWidgetBeforeEdit(widget);
|
||||
return this.scalarApi.setWidget(this.roomId, widget)
|
||||
.then(() => this.toggleWidget(widget))
|
||||
.then(() => {
|
||||
this.isUpdating = false;
|
||||
this.onWidgetAfterEdit(widget);
|
||||
this.toaster.pop("success", "Widget updated!");
|
||||
})
|
||||
.catch(err => {
|
||||
this.isUpdating = false;
|
||||
console.error(err);
|
||||
this.toaster.pop("error", err.json().error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.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.isUpdating = false;
|
||||
console.error(err);
|
||||
this.toaster.pop("error", err.json().error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts a widget in the edit state.
|
||||
* @param {EditableWidget} widget
|
||||
*/
|
||||
public editWidget(widget: EditableWidget) {
|
||||
this.toggleWidget(widget, "edit");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
<div class="config-wrapper">
|
||||
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
|
||||
<div class="config-header">
|
||||
<img [src]="integration.avatar">
|
||||
<h4>Configure video widgets</h4>
|
||||
</div>
|
||||
<div class="config-content" *ngIf="isLoading">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p><i class="fa fa-circle-notch fa-spin"></i> Loading widgets...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-content" *ngIf="!isLoading">
|
||||
<form (submit)="validateAndAddWidget()" 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="YouTube, Vimeo, or DailyMotion video URL"
|
||||
[(ngModel)]="newWidget.dimension.newUrl" name="newWidgetUrl"
|
||||
[disabled]="isUpdating">
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
|
||||
<i class="fa fa-plus-circle"></i> Add Widget
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
|
||||
{{ widget.name || widget.url }} <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 Widget
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeWidget(widget)"
|
||||
style="margin-top: -5px;" [disabled]="isUpdating">
|
||||
<i class="fa fa-times"></i> Remove Widget
|
||||
</button>
|
||||
<div *ngIf="isWidgetToggled(widget)">
|
||||
<label>
|
||||
Widget Name
|
||||
<input type="text" class="form-control"
|
||||
placeholder="YouTube Widget"
|
||||
[(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.dimension.newUrl" name="widget-url-{{widget.id}}"
|
||||
[disabled]="isUpdating">
|
||||
</label>
|
||||
<button type="button" class="btn btn-primary btn-sm" (click)="validateAndSaveWidget(widget)">
|
||||
Save
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline btn-sm" (click)="toggleWidget(widget)">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -1,4 +0,0 @@
|
||||
// component styles are encapsulated and only applied to their components
|
||||
.widget-item {
|
||||
margin-top: 3px;
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { DialogRef, ModalComponent } from "ngx-modialog";
|
||||
import { WidgetComponent } from "../widget.component";
|
||||
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
|
||||
import { ConfigModalContext } from "../../../integration/integration.component";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
import { EditableWidget, WIDGET_YOUTUBE } from "../../../shared/models/widget";
|
||||
import * as embed from "embed-video";
|
||||
import * as $ from "jquery";
|
||||
|
||||
@Component({
|
||||
selector: "my-youtubewidget-config",
|
||||
templateUrl: "youtube-config.component.html",
|
||||
styleUrls: ["youtube-config.component.scss", "./../../config.component.scss"],
|
||||
})
|
||||
export class YoutubeWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> {
|
||||
|
||||
constructor(public dialog: DialogRef<ConfigModalContext>,
|
||||
toaster: ToasterService,
|
||||
scalarService: ScalarClientApiService,
|
||||
window: Window) {
|
||||
super(
|
||||
window,
|
||||
toaster,
|
||||
scalarService,
|
||||
dialog.context.roomId,
|
||||
dialog.context.integration,
|
||||
dialog.context.integrationId,
|
||||
WIDGET_YOUTUBE,
|
||||
"Youtube Widget",
|
||||
"video", // wrapper
|
||||
"youtube" // scalar wrapper
|
||||
);
|
||||
}
|
||||
|
||||
public validateAndAddWidget() {
|
||||
const url = this.getRealVideoUrl(this.newWidget.dimension.newUrl);
|
||||
if (!url) {
|
||||
this.toaster.pop("warning", "Please enter a YouTube, Vimeo, or DailyMotion video URL");
|
||||
return;
|
||||
}
|
||||
|
||||
this.newWidget.dimension.newUrl = url;
|
||||
this.addWidget();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
widget.dimension.newUrl = url;
|
||||
this.saveWidget(widget);
|
||||
}
|
||||
|
||||
private getRealVideoUrl(url) {
|
||||
const embedCode = embed(url);
|
||||
if (!embedCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// HACK: Grab the video URL from the iframe embed code
|
||||
url = $(embedCode).attr("src");
|
||||
if (url.startsWith("//")) url = "https:" + url;
|
||||
|
||||
return url;
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { LegacyIntegration } from "../shared/models/legacyintegration";
|
||||
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
|
||||
import { Integration } from "../shared/models/integration";
|
||||
|
||||
@ -10,7 +9,7 @@ import { Integration } from "../shared/models/integration";
|
||||
})
|
||||
export class IntegrationBagComponent {
|
||||
|
||||
@Input() integrations: LegacyIntegration[];
|
||||
@Input() integrations: Integration[];
|
||||
@Output() integrationClicked: EventEmitter<Integration> = new EventEmitter<Integration>();
|
||||
|
||||
constructor(private sanitizer: DomSanitizer) {
|
||||
|
@ -1,5 +0,0 @@
|
||||
<div class="integration">
|
||||
<img class="integration-avatar" [src]="getSafeUrl(integration.avatar)"/>
|
||||
<div class="integration-name">{{ integration.name }}</div>
|
||||
<div class="integration-description">{{ integration.about }}</div>
|
||||
</div>
|
@ -1,31 +0,0 @@
|
||||
// component styles are encapsulated and only applied to their components
|
||||
.integration {
|
||||
border: 1px solid #eee;
|
||||
border-radius: 3px;
|
||||
margin-left: 7px;
|
||||
margin-right: 7px;
|
||||
padding: 5px;
|
||||
width: calc(325px - 14px);
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.integration .integration-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.integration .integration-name {
|
||||
display: inline-block;
|
||||
font-size: 1.1em;
|
||||
font-weight: 100;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.integration .integration-description {
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
margin-left: 55px;
|
||||
color: #999;
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { LegacyIntegration } from "../shared/models/legacyintegration";
|
||||
import { BSModalContext } from "ngx-modialog/plugins/bootstrap";
|
||||
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
|
||||
|
||||
export class ConfigModalContext extends BSModalContext {
|
||||
public integration: LegacyIntegration;
|
||||
public roomId: string;
|
||||
public userId: string;
|
||||
public scalarToken: string;
|
||||
public integrationId: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "my-integration",
|
||||
templateUrl: "./integration.component.html",
|
||||
styleUrls: ["./integration.component.scss"],
|
||||
})
|
||||
export class IntegrationComponent {
|
||||
|
||||
@Input() integration: LegacyIntegration;
|
||||
@Output() selected: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
constructor(private sanitizer: DomSanitizer) {
|
||||
}
|
||||
|
||||
public getSafeUrl(url: string): SafeResourceUrl {
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
export interface LegacyIntegration {
|
||||
// These are from the server
|
||||
type: string;
|
||||
integrationType: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
about: string; // nullable
|
||||
supportsEncryptedRooms: boolean;
|
||||
requirements: any; // nullable
|
||||
|
||||
// These are set in the UI
|
||||
isSupported: boolean;
|
||||
notSupportedReason: string;
|
||||
hasAdditionalConfig: boolean;
|
||||
isEnabled: boolean; // for the flip-a-bit integrations
|
||||
isUpdating: boolean;
|
||||
}
|
||||
|
||||
export interface RSSIntegration extends LegacyIntegration {
|
||||
feeds: string[];
|
||||
immutableFeeds: { url: string, ownerId: string }[];
|
||||
}
|
||||
|
||||
export interface TravisCiIntegration extends LegacyIntegration {
|
||||
repoTemplates: { repoKey: string, template: string, newTemplate: string }[]; // newTemplate is local
|
||||
immutableRepoTemplates: { repoKey: string, template: string, ownerId: string }[];
|
||||
webhookUrl: string; // immutable
|
||||
}
|
||||
|
||||
export interface CircleCiIntegration extends LegacyIntegration {
|
||||
repoTemplates: { repoKey: string, template: string, newTemplate: string }[]; // newTemplate is local
|
||||
immutableRepoTemplates: { repoKey: string, template: string, ownerId: string }[];
|
||||
webhookUrl: string; // immutable
|
||||
}
|
||||
|
||||
export interface IRCIntegration extends LegacyIntegration {
|
||||
availableNetworks: { name: string, id: string }[];
|
||||
channels: { [networkId: string]: string[] };
|
||||
}
|
||||
|
||||
export interface EtherpadWidgetIntegration extends LegacyIntegration {
|
||||
defaultUrl: string;
|
||||
}
|
||||
|
||||
export interface JitsiWidgetIntegration extends LegacyIntegration {
|
||||
jitsiDomain: string;
|
||||
scriptUrl: string
|
||||
}
|
@ -2,6 +2,7 @@ import { Injectable } from "@angular/core";
|
||||
import { Http } from "@angular/http";
|
||||
import { AuthedApi } from "./AuthedApi";
|
||||
import { DimensionIntegrationsResponse } from "../models/dimension_responses";
|
||||
import { Widget } from "../models/integration";
|
||||
|
||||
@Injectable()
|
||||
export class DimensionApiService extends AuthedApi {
|
||||
@ -12,4 +13,13 @@ export class DimensionApiService extends AuthedApi {
|
||||
public getIntegrations(roomId: string): Promise<DimensionIntegrationsResponse> {
|
||||
return this.authedGet("/api/v1/dimension/integrations/room/" + roomId).map(r => r.json()).toPromise();
|
||||
}
|
||||
|
||||
public getWidget(type: string): Promise<Widget> {
|
||||
return this.http.get("/api/v1/dimension/widget/" + type).map(r => r.json()).toPromise();
|
||||
}
|
||||
|
||||
public isEmbeddable(url: string): Promise<any> { // 200 = success, anything else = error
|
||||
return this.http.get("/api/v1/dimension/widgets/embeddable", {params: {url: url}})
|
||||
.map(r => r.json()).toPromise();
|
||||
}
|
||||
}
|
||||
|
@ -1,53 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Http } from "@angular/http";
|
||||
import { LegacyIntegration } from "../../models/legacyintegration";
|
||||
|
||||
@Injectable()
|
||||
export class ApiService {
|
||||
constructor(private http: Http) {
|
||||
}
|
||||
|
||||
checkScalarToken(scalarToken: string): Promise<boolean> {
|
||||
return this.http.get("/api/v1/scalar/checkToken", {params: {scalar_token: scalarToken}})
|
||||
.map(res => res.status === 200).toPromise();
|
||||
}
|
||||
|
||||
getTokenOwner(scalarToken: String): Promise<string> {
|
||||
return this.http.get("/api/v1/dimension/whoami", {params: {scalar_token: scalarToken}})
|
||||
.map(res => res.status === 200 ? res.json()["userId"] : null).toPromise();
|
||||
}
|
||||
|
||||
getIntegrations(roomId: string, scalarToken: string): Promise<LegacyIntegration[]> {
|
||||
return this.http.get("/api/v1/dimension/integrations/" + roomId, {params: {scalar_token: scalarToken}})
|
||||
.map(res => res.json()).toPromise();
|
||||
}
|
||||
|
||||
removeIntegration(roomId: string, type: string, integrationType: string, scalarToken: string): Promise<any> {
|
||||
const url = "/api/v1/dimension/integrations/" + roomId + "/" + type + "/" + integrationType;
|
||||
return this.http.delete(url, {params: {scalar_token: scalarToken}})
|
||||
.map(res => res.json()).toPromise();
|
||||
}
|
||||
|
||||
updateIntegrationState(roomId: string, type: string, integrationType: string, scalarToken: string, newState: any): Promise<any> {
|
||||
const url = "/api/v1/dimension/integrations/" + roomId + "/" + type + "/" + integrationType + "/state";
|
||||
return this.http.put(url, {scalar_token: scalarToken, state: newState})
|
||||
.map(res => res.json()).toPromise();
|
||||
}
|
||||
|
||||
getIntegrationState(roomId: string, type: string, integrationType: string, scalarToken: string): Promise<any> {
|
||||
const url = "/api/v1/dimension/integrations/" + roomId + "/" + type + "/" + integrationType + "/state";
|
||||
return this.http.get(url, {params: {scalar_token: scalarToken}})
|
||||
.map(res => res.json()).toPromise();
|
||||
}
|
||||
|
||||
isEmbeddable(checkUrl: string): Promise<any> {
|
||||
const url = "/api/v1/dimension/widgets/embeddable";
|
||||
return this.http.get(url, {params: {url: checkUrl}})
|
||||
.map(res => res.json()).toPromise();
|
||||
}
|
||||
|
||||
getIntegration(type: string, integrationType: string): Promise<LegacyIntegration> {
|
||||
const url = "/api/v1/dimension/integration/" + type + "/" + integrationType;
|
||||
return this.http.get(url).map(res => res.json()).toPromise();
|
||||
}
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { LegacyIntegration } from "../../models/legacyintegration";
|
||||
import { RssConfigComponent } from "../../../configs-old/rss/rss-config.component";
|
||||
import { ContainerContent } from "ngx-modialog";
|
||||
import { IrcConfigComponent } from "../../../configs-old/irc/irc-config.component";
|
||||
import { TravisCiConfigComponent } from "../../../configs-old/travisci/travisci-config.component";
|
||||
import { CustomWidgetConfigComponent } from "../../../configs-old/widget/custom_widget/custom_widget-config.component";
|
||||
import { YoutubeWidgetConfigComponent } from "../../../configs-old/widget/youtube/youtube-config.component";
|
||||
import { TwitchWidgetConfigComponent } from "../../../configs-old/widget/twitch/twitch-config.component";
|
||||
import { EtherpadWidgetConfigComponent } from "../../../configs-old/widget/etherpad/etherpad-config.component";
|
||||
import { JitsiWidgetConfigComponent } from "../../../configs-old/widget/jitsi/jitsi-config.component";
|
||||
import {
|
||||
WIDGET_CUSTOM, WIDGET_ETHERPAD, WIDGET_GOOGLE_CALENDAR, WIDGET_GOOGLE_DOCS, WIDGET_JITSI, WIDGET_TWITCH,
|
||||
WIDGET_YOUTUBE
|
||||
} from "../../models/widget";
|
||||
import { GoogleDocsWidgetConfigComponent } from "../../../configs-old/widget/googledocs/googledocs-config.component";
|
||||
import { GoogleCalendarWidgetConfigComponent } from "../../../configs-old/widget/googlecalendar/googlecalendar-config.component";
|
||||
import { CircleCiConfigComponent } from "../../../configs-old/circleci/circleci-config.component";
|
||||
|
||||
@Injectable()
|
||||
export class LegacyIntegrationService {
|
||||
|
||||
private static supportedIntegrationsMap = {
|
||||
"bot": {}, // empty == supported
|
||||
"complex-bot": {
|
||||
"rss": {
|
||||
component: RssConfigComponent,
|
||||
},
|
||||
"travisci": {
|
||||
component: TravisCiConfigComponent,
|
||||
},
|
||||
"circleci": {
|
||||
component: CircleCiConfigComponent,
|
||||
},
|
||||
},
|
||||
"bridge": {
|
||||
"irc": {
|
||||
component: IrcConfigComponent,
|
||||
},
|
||||
},
|
||||
"widget": {
|
||||
"customwidget": {
|
||||
component: CustomWidgetConfigComponent,
|
||||
types: WIDGET_CUSTOM,
|
||||
},
|
||||
"youtube": {
|
||||
component: YoutubeWidgetConfigComponent,
|
||||
types: WIDGET_YOUTUBE
|
||||
},
|
||||
"etherpad": {
|
||||
component: EtherpadWidgetConfigComponent,
|
||||
types: WIDGET_ETHERPAD,
|
||||
},
|
||||
"twitch": {
|
||||
component: TwitchWidgetConfigComponent,
|
||||
types: WIDGET_TWITCH,
|
||||
},
|
||||
"jitsi": {
|
||||
component: JitsiWidgetConfigComponent,
|
||||
types: WIDGET_JITSI,
|
||||
},
|
||||
"googledocs": {
|
||||
component: GoogleDocsWidgetConfigComponent,
|
||||
types: WIDGET_GOOGLE_DOCS,
|
||||
},
|
||||
"googlecalendar": {
|
||||
component: GoogleCalendarWidgetConfigComponent,
|
||||
types: WIDGET_GOOGLE_CALENDAR,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static getAllConfigComponents(): ContainerContent[] {
|
||||
const components = [];
|
||||
|
||||
for (const iType of Object.keys(LegacyIntegrationService.supportedIntegrationsMap)) {
|
||||
for (const iiType of Object.keys(LegacyIntegrationService.supportedIntegrationsMap[iType])) {
|
||||
const component = LegacyIntegrationService.supportedIntegrationsMap[iType][iiType].component;
|
||||
if (component) components.push(component);
|
||||
}
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
static isSupported(integration: LegacyIntegration): boolean {
|
||||
const forType = LegacyIntegrationService.supportedIntegrationsMap[integration.type];
|
||||
if (!forType) return false;
|
||||
|
||||
if (Object.keys(forType).length === 0) return true;
|
||||
|
||||
return forType[integration.integrationType]; // has sub type
|
||||
}
|
||||
|
||||
static hasConfig(integration: LegacyIntegration): boolean {
|
||||
return integration.type !== "bot";
|
||||
}
|
||||
|
||||
static getConfigComponent(integration: LegacyIntegration): ContainerContent {
|
||||
return LegacyIntegrationService.supportedIntegrationsMap[integration.type][integration.integrationType].component;
|
||||
}
|
||||
|
||||
static getIntegrationForScreen(screen: string): { type: string, integrationType: string } {
|
||||
for (const iType of Object.keys(LegacyIntegrationService.supportedIntegrationsMap)) {
|
||||
for (const iiType of Object.keys(LegacyIntegrationService.supportedIntegrationsMap[iType])) {
|
||||
const integrationTypes = LegacyIntegrationService.supportedIntegrationsMap[iType][iiType].types;
|
||||
const integrationScreens = integrationTypes.map(t => "type_" + t);
|
||||
if (integrationScreens.includes(screen)) return {type: iType, integrationType: iiType};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Http } from "@angular/http";
|
||||
|
||||
@Injectable()
|
||||
export class IrcApiService {
|
||||
constructor(private http: Http) {
|
||||
}
|
||||
|
||||
getChannelOps(roomId: string, networkId: string, channel: string, scalarToken: string): Promise<string[]> {
|
||||
return this.http.get("/api/v1/irc/" + roomId + "/ops/" + networkId + "/" + channel, {params: {scalar_token: scalarToken}})
|
||||
.map(res => res.json()).toPromise();
|
||||
}
|
||||
|
||||
linkChannel(roomId: string, networkId: string, channel: string, op: string, scalarToken: string): Promise<any> {
|
||||
return this.http.put("/api/v1/irc/" + roomId + "/channels/" + networkId + "/" + channel, {}, {
|
||||
params: {
|
||||
scalar_token: scalarToken,
|
||||
op: op
|
||||
}
|
||||
}).map(res => res.json()).toPromise();
|
||||
}
|
||||
|
||||
unlinkChannel(roomId: string, networkId: string, channel: string, scalarToken: string): Promise<any> {
|
||||
return this.http.delete("/api/v1/irc/" + roomId + "/channels/" + networkId + "/" + channel, {params: {scalar_token: scalarToken}})
|
||||
.map(res => res.json()).toPromise();
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ApiService } from "../../shared/services/legacy/api.service";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
|
||||
import { DimensionApiService } from "../../shared/services/dimension-api.service";
|
||||
|
||||
@Component({
|
||||
selector: "my-generic-widget-wrapper",
|
||||
@ -14,7 +14,7 @@ export class GenericWidgetWrapperComponent {
|
||||
public canEmbed = false;
|
||||
public embedUrl: SafeUrl = null;
|
||||
|
||||
constructor(api: ApiService, activatedRoute: ActivatedRoute, sanitizer: DomSanitizer) {
|
||||
constructor(api: DimensionApiService, activatedRoute: ActivatedRoute, sanitizer: DomSanitizer) {
|
||||
let params: any = activatedRoute.snapshot.queryParams;
|
||||
|
||||
api.isEmbeddable(params.url).then(result => {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import * as $ from "jquery";
|
||||
import { ApiService } from "../../shared/services/legacy/api.service";
|
||||
import { JitsiWidgetIntegration } from "../../shared/models/legacyintegration";
|
||||
import { DimensionApiService } from "../../shared/services/dimension-api.service";
|
||||
import { JitsiWidget } from "../../shared/models/integration";
|
||||
|
||||
declare var JitsiMeetExternalAPI: any;
|
||||
|
||||
@ -22,7 +22,7 @@ export class JitsiWidgetWrapperComponent implements OnInit {
|
||||
private userId: string;
|
||||
private jitsiApiObj: any;
|
||||
|
||||
constructor(activatedRoute: ActivatedRoute, private api: ApiService) {
|
||||
constructor(activatedRoute: ActivatedRoute, private api: DimensionApiService) {
|
||||
let params: any = activatedRoute.snapshot.queryParams;
|
||||
|
||||
this.domain = params.domain;
|
||||
@ -33,9 +33,9 @@ export class JitsiWidgetWrapperComponent implements OnInit {
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.api.getIntegration("widget", "jitsi").then(integration => {
|
||||
const widget = <JitsiWidgetIntegration>integration;
|
||||
$.getScript(widget.scriptUrl);
|
||||
this.api.getWidget("jitsi").then(integration => {
|
||||
const widget = <JitsiWidget>integration;
|
||||
$.getScript(widget.options.scriptUrl);
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user