From 58feb07119eb3abdccba9bbfa5adfbf7cef5ee78 Mon Sep 17 00:00:00 2001 From: turt2live Date: Sun, 28 May 2017 22:51:04 -0600 Subject: [PATCH] Support vector's RSS bot. Adds #13 This has a side effect of adding #23 as well. A more performant caching method is probably needed (as this doesn't cache at all). --- config/integrations/rssbot.yaml | 3 +- package.json | 1 + src/DimensionApi.js | 33 +++++++++ .../generic_types/IntegrationStub.js | 9 +++ src/integration/impl/rss/RSSBot.js | 15 ++++- .../impl/rss/StubbedRssBackbone.js | 17 +++++ src/integration/impl/rss/VectorRssBackbone.js | 36 +++++++++- .../impl/simple_bot/SimpleBotFactory.js | 2 +- src/scalar/VectorScalarClient.js | 21 +++++- web/app/app.module.ts | 12 +++- web/app/configs/config.component.scss | 32 +++++++++ web/app/configs/rss/rss-config.component.html | 38 +++++++++++ web/app/configs/rss/rss-config.component.scss | 9 +++ web/app/configs/rss/rss-config.component.ts | 67 +++++++++++++++++++ .../integration/integration.component.html | 1 + .../integration/integration.component.scss | 4 ++ web/app/integration/integration.component.ts | 26 ++++++- web/app/riot/riot.component.html | 9 ++- web/app/riot/riot.component.ts | 35 +++++----- web/app/shared/api.service.ts | 9 ++- web/app/shared/integration.service.ts | 38 +++++++++++ web/app/shared/models/integration.ts | 6 ++ web/style/app.scss | 20 ++++++ 23 files changed, 412 insertions(+), 31 deletions(-) create mode 100644 web/app/configs/config.component.scss create mode 100644 web/app/configs/rss/rss-config.component.html create mode 100644 web/app/configs/rss/rss-config.component.scss create mode 100644 web/app/configs/rss/rss-config.component.ts create mode 100644 web/app/shared/integration.service.ts diff --git a/config/integrations/rssbot.yaml b/config/integrations/rssbot.yaml index 194ce8b..b0ae410 100644 --- a/config/integrations/rssbot.yaml +++ b/config/integrations/rssbot.yaml @@ -5,5 +5,4 @@ name: "RSS Bot" about: "Tracks any Atom/RSS feed and sends new items into this room" avatar: "/img/avatars/rssbot.png" upstream: - type: "vector" - id: "rssbot" \ No newline at end of file + type: "vector" \ No newline at end of file diff --git a/package.json b/package.json index 901a7c8..ba5757a 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@angularclass/hmr-loader": "^3.0.2", "@ng-bootstrap/ng-bootstrap": "^1.0.0-alpha.22", "@types/node": "^7.0.18", + "angular2-modal": "^2.0.3", "angular2-template-loader": "^0.6.2", "angular2-toaster": "^4.0.0", "angular2-ui-switch": "^1.2.0", diff --git a/src/DimensionApi.js b/src/DimensionApi.js index 8e2e6e0..d85ac3d 100644 --- a/src/DimensionApi.js +++ b/src/DimensionApi.js @@ -24,6 +24,7 @@ class DimensionApi { app.get("/api/v1/dimension/integrations/:roomId", this._getIntegrations.bind(this)); app.delete("/api/v1/dimension/integrations/:roomId/:type/:integrationType", this._removeIntegration.bind(this)); + app.put("/api/v1/dimension/integrations/:roomId/:type/:integrationType/state", this._updateIntegrationState.bind(this)); } _getIntegration(integrationConfig, roomId, scalarToken) { @@ -103,6 +104,38 @@ class DimensionApi { res.status(500).send({error: err.message}); }); } + + _updateIntegrationState(req, res) { + var roomId = req.params.roomId; + var scalarToken = req.body.scalar_token; + var type = req.params.type; + var integrationType = req.params.integrationType; + + if (!roomId || !scalarToken || !type || !integrationType) { + res.status(400).send({error: "Missing room, integration type, type, or token"}); + return; + } + + var integrationConfig = Integrations.byType[type][integrationType]; + if (!integrationConfig) { + res.status(400).send({error: "Unknown integration"}); + return; + } + + log.info("DimensionApi", "Update state requested for " + type + " (" + integrationType + ") in room " + roomId); + + this._db.checkToken(scalarToken).then(() => { + return this._getIntegration(integrationConfig, roomId, scalarToken); + }).then(integration => { + return integration.updateState(req.body.state); + }).then(newState => { + res.status(200).send(newState); + }).catch(err => { + log.error("DimensionApi", err); + console.error(err); + res.status(500).send({error: err.message}); + }); + } } module.exports = new DimensionApi(); diff --git a/src/integration/generic_types/IntegrationStub.js b/src/integration/generic_types/IntegrationStub.js index ddda32e..d867532 100644 --- a/src/integration/generic_types/IntegrationStub.js +++ b/src/integration/generic_types/IntegrationStub.js @@ -30,6 +30,15 @@ class IntegrationStub { removeFromRoom(roomId) { throw new Error("Not implemented"); } + + /** + * Updates the state information for this integration. The data passed is an implementation detail. + * @param {*} newState the new state + * @returns {Promise<*>} resolves when completed, with the new state of the integration + */ + updateState(newState) { + return Promise.resolve({}); + } } module.exports = IntegrationStub; diff --git a/src/integration/impl/rss/RSSBot.js b/src/integration/impl/rss/RSSBot.js index 5a9959e..f3384b2 100644 --- a/src/integration/impl/rss/RSSBot.js +++ b/src/integration/impl/rss/RSSBot.js @@ -22,8 +22,16 @@ class RSSBot extends ComplexBot { /*override*/ getState() { + var response = { + feeds: [], + immutableFeeds: [] + }; return this._backbone.getFeeds().then(feeds => { - return {feeds: feeds}; + response.feeds = feeds; + return this._backbone.getImmutableFeeds(); + }).then(feeds => { + response.immutableFeeds = feeds; + return response; }); } @@ -31,6 +39,11 @@ class RSSBot extends ComplexBot { removeFromRoom(roomId) { return this._backbone.removeFromRoom(roomId); } + + /*override*/ + updateState(newState) { + return this._backbone.setFeeds(newState.feeds).then(() => this.getState()); + } } module.exports = RSSBot; \ No newline at end of file diff --git a/src/integration/impl/rss/StubbedRssBackbone.js b/src/integration/impl/rss/StubbedRssBackbone.js index 0870ad3..6e51205 100644 --- a/src/integration/impl/rss/StubbedRssBackbone.js +++ b/src/integration/impl/rss/StubbedRssBackbone.js @@ -25,6 +25,23 @@ class StubbedRssBackbone { throw new Error("Not implemented"); } + /** + * Sets the new feeds for this backbone + * @param {string[]} newFeeds the new feed URLs + * @returns {Promise<>} resolves when complete + */ + setFeeds(newFeeds) { + throw new Error("Not implemented"); + } + + /** + * Gets the immutable feeds for this backbone + * @returns {Promise<{url:string,ownerId:string}>} resolves to the collection of immutable feeds + */ + getImmutableFeeds() { + throw new Error("Not implemented"); + } + /** * Removes the bot from the given room * @param {string} roomId the room ID to remove the bot from diff --git a/src/integration/impl/rss/VectorRssBackbone.js b/src/integration/impl/rss/VectorRssBackbone.js index 0584e1c..d4a37f6 100644 --- a/src/integration/impl/rss/VectorRssBackbone.js +++ b/src/integration/impl/rss/VectorRssBackbone.js @@ -18,6 +18,7 @@ class VectorRssBackbone extends StubbedRssBackbone { this._roomId = roomId; this._scalarToken = upstreamScalarToken; this._info = null; + this._otherFeeds = []; } /*override*/ @@ -35,15 +36,46 @@ class VectorRssBackbone extends StubbedRssBackbone { }); } + /*override*/ + setFeeds(newFeeds) { + var feedConfig = {}; + for (var feed of newFeeds) feedConfig[feed] = {}; + + return VectorScalarClient.configureIntegration("rssbot", this._scalarToken, { + feeds: feedConfig, + room_id: this._roomId + }); + } + + /*override*/ + getImmutableFeeds() { + return (this._info ? Promise.resolve() : this._getInfo()).then(() => { + return this._otherFeeds; + }); + } + _getInfo() { - return VectorScalarClient.getIntegration("rssbot", this._roomId, this._scalarToken).then(info => { + return VectorScalarClient.getIntegrationsForRoom(this._roomId, this._scalarToken).then(integrations => { + this._otherFeeds = []; + for (var integration of integrations) { + if (integration.self) continue; // skip - we're not looking for ones we know about + if (integration.type == "rssbot") { + var urls = _.keys(integration.config.feeds); + for (var url of urls) { + this._otherFeeds.push({url: url, ownerId: integration.user_id}); + } + } + } + + return VectorScalarClient.getIntegration("rssbot", this._roomId, this._scalarToken); + }).then(info => { this._info = info; }); } /*override*/ removeFromRoom(roomId) { - return VectorScalarClient.removeIntegration(this._config.upstream.id, roomId, this._upstreamToken); + return VectorScalarClient.removeIntegration("rssbot", roomId, this._scalarToken); } } diff --git a/src/integration/impl/simple_bot/SimpleBotFactory.js b/src/integration/impl/simple_bot/SimpleBotFactory.js index a99068b..65d9678 100644 --- a/src/integration/impl/simple_bot/SimpleBotFactory.js +++ b/src/integration/impl/simple_bot/SimpleBotFactory.js @@ -6,7 +6,7 @@ module.exports = (db, integrationConfig, roomId, scalarToken) => { if (integrationConfig.upstream) { if (integrationConfig.upstream.type !== "vector") throw new Error("Unsupported upstream"); return db.getUpstreamToken(scalarToken).then(upstreamToken => { - var backbone = new VectorSimpleBackbone(roomId, upstreamToken); + var backbone = new VectorSimpleBackbone(integrationConfig, upstreamToken); return new SimpleBot(integrationConfig, backbone); }); } else if (integrationConfig.hosted) { diff --git a/src/scalar/VectorScalarClient.js b/src/scalar/VectorScalarClient.js index 4c1f05d..2a44aa5 100644 --- a/src/scalar/VectorScalarClient.js +++ b/src/scalar/VectorScalarClient.js @@ -54,7 +54,7 @@ class VectorScalarClient { * @return {Promise<>} resolves when completed */ configureIntegration(type, scalarToken, config) { - return this._do("POST", "/integrations/"+type+"/configureService", {scalar_token:scalarToken}, config).then((response, body) => { + return this._do("POST", "/integrations/" + type + "/configureService", {scalar_token: scalarToken}, config).then((response, body) => { if (response.statusCode !== 200) { log.error("VectorScalarClient", response.body); return Promise.reject(response.body); @@ -64,6 +64,23 @@ class VectorScalarClient { }); } + /** + * Gets all of the integrations currently in a room + * @param {string} roomId the room ID + * @param {string} scalarToken the scalar token to use + * @returns {Promise<*[]>} resolves a collection of integrations + */ + getIntegrationsForRoom(roomId, scalarToken) { + return this._do("POST", "/integrations", {scalar_token: scalarToken}, {RoomId: roomId}).then((response, body) => { + if (response.statusCode !== 200) { + log.error("VectorScalarClient", response.body); + return Promise.reject(response.body); + } + + return response.body.integrations; + }); + } + /** * Gets information on * @param {string} type the type to lookup @@ -72,7 +89,7 @@ class VectorScalarClient { * @return {Promise<{bot_user_id:string,integrations:[]}>} resolves to the integration information */ getIntegration(type, roomId, scalarToken) { - return this._do("POST", "/integrations/"+type,{scalar_token:scalarToken}, {room_id:roomId}).then((response, body) => { + return this._do("POST", "/integrations/" + type, {scalar_token: scalarToken}, {room_id: roomId}).then((response, body) => { if (response.statusCode !== 200) { log.error("VectorScalarClient", response.body); return Promise.reject(response.body); diff --git a/web/app/app.module.ts b/web/app/app.module.ts index fc2d104..3757b08 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -15,6 +15,10 @@ import { ToasterModule } from "angular2-toaster"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { IntegrationComponent } from "./integration/integration.component"; import { ScalarCloseComponent } from "./riot/scalar-close/scalar-close.component"; +import { IntegrationService } from "./shared/integration.service"; +import { BootstrapModalModule } from "angular2-modal/plugins/bootstrap"; +import { ModalModule } from "angular2-modal"; +import { RssConfigComponent } from "./configs/rss/rss-config.component"; @NgModule({ imports: [ @@ -26,6 +30,8 @@ import { ScalarCloseComponent } from "./riot/scalar-close/scalar-close.component UiSwitchModule, ToasterModule, BrowserAnimationsModule, + ModalModule.forRoot(), + BootstrapModalModule, ], declarations: [ AppComponent, @@ -33,17 +39,21 @@ import { ScalarCloseComponent } from "./riot/scalar-close/scalar-close.component RiotComponent, IntegrationComponent, ScalarCloseComponent, + RssConfigComponent, // Vendor ], providers: [ ApiService, ScalarService, + IntegrationService, // Vendor ], bootstrap: [AppComponent], - entryComponents: [] + entryComponents: [ + RssConfigComponent, + ] }) export class AppModule { constructor(public appRef: ApplicationRef) { diff --git a/web/app/configs/config.component.scss b/web/app/configs/config.component.scss new file mode 100644 index 0000000..ce39ba5 --- /dev/null +++ b/web/app/configs/config.component.scss @@ -0,0 +1,32 @@ +// 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; +} \ No newline at end of file diff --git a/web/app/configs/rss/rss-config.component.html b/web/app/configs/rss/rss-config.component.html new file mode 100644 index 0000000..6ff1044 --- /dev/null +++ b/web/app/configs/rss/rss-config.component.html @@ -0,0 +1,38 @@ +
+ +
+ +

Configure RSS/Atom Feeds

+
+
+
+
+
+
+ + + + +
+
+
+ {{ feed }} + +
+
+
Feeds from other users in the room
+
+
+ {{ feed.url }} (added by {{ feed.ownerId }}) +
+
+
+
+
\ No newline at end of file diff --git a/web/app/configs/rss/rss-config.component.scss b/web/app/configs/rss/rss-config.component.scss new file mode 100644 index 0000000..c1fafce --- /dev/null +++ b/web/app/configs/rss/rss-config.component.scss @@ -0,0 +1,9 @@ +// component styles are encapsulated and only applied to their components +.feed-list { + margin-top: 5px; +} + +.other-feeds-title { + margin-top: 25px; + margin-bottom: 0; +} \ No newline at end of file diff --git a/web/app/configs/rss/rss-config.component.ts b/web/app/configs/rss/rss-config.component.ts new file mode 100644 index 0000000..a24e837 --- /dev/null +++ b/web/app/configs/rss/rss-config.component.ts @@ -0,0 +1,67 @@ +import { Component } from "@angular/core"; +import { RSSIntegration } from "../../shared/models/integration"; +import { ModalComponent, DialogRef } from "angular2-modal"; +import { ConfigModalContext } from "../../integration/integration.component"; +import { ToasterService } from "angular2-toaster"; +import { ApiService } from "../../shared/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 { + + public integration: RSSIntegration; + + public isUpdating = false; + public feedUrl = ""; + + private roomId: string; + private scalarToken: string; + + constructor(public dialog: DialogRef, + private toaster: ToasterService, + private api: ApiService) { + this.integration = 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"); + } + + let feedCopy = JSON.parse(JSON.stringify(this.integration.feeds)); + feedCopy.push(this.feedUrl); + this.updateFeeds(feedCopy); + } + + 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; + 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; + }); + } +} diff --git a/web/app/integration/integration.component.html b/web/app/integration/integration.component.html index 6d9ca0e..893e59a 100644 --- a/web/app/integration/integration.component.html +++ b/web/app/integration/integration.component.html @@ -8,6 +8,7 @@
+
diff --git a/web/app/integration/integration.component.scss b/web/app/integration/integration.component.scss index d905d92..0b55b7a 100644 --- a/web/app/integration/integration.component.scss +++ b/web/app/integration/integration.component.scss @@ -31,4 +31,8 @@ display: inline-block; vertical-align: top; margin-left: 5px; +} + +.config-icon { + cursor: pointer; } \ No newline at end of file diff --git a/web/app/integration/integration.component.ts b/web/app/integration/integration.component.ts index efd6d6f..635aaae 100644 --- a/web/app/integration/integration.component.ts +++ b/web/app/integration/integration.component.ts @@ -1,5 +1,14 @@ -import { Component, Input, Output, EventEmitter } from "@angular/core"; +import { Component, Input, Output, EventEmitter, ViewContainerRef } from "@angular/core"; import { Integration } from "../shared/models/integration"; +import { Overlay, overlayConfigFactory } from "angular2-modal"; +import { Modal, BSModalContext } from "angular2-modal/plugins/bootstrap"; +import { IntegrationService } from "../shared/integration.service"; + +export class ConfigModalContext extends BSModalContext { + public integration: Integration; + public roomId: string; + public scalarToken: string; +} @Component({ selector: 'my-integration', @@ -9,13 +18,26 @@ import { Integration } from "../shared/models/integration"; export class IntegrationComponent { @Input() integration: Integration; + @Input() roomId: string; + @Input() scalarToken: string; @Output() updated: EventEmitter = new EventEmitter(); - constructor() { + constructor(overlay: Overlay, vcRef: ViewContainerRef, public modal: Modal) { + overlay.defaultViewContainer = vcRef; } public update(): void { this.integration.isEnabled = !this.integration.isEnabled; this.updated.emit(); } + + public configureIntegration(): void { + this.modal.open(IntegrationService.getConfigComponent(this.integration), overlayConfigFactory({ + integration: this.integration, + roomId: this.roomId, + scalarToken: this.scalarToken, + isBlocking: false, + size: 'lg' + }, BSModalContext)); + } } diff --git a/web/app/riot/riot.component.html b/web/app/riot/riot.component.html index 60b9d24..8f8cdca 100644 --- a/web/app/riot/riot.component.html +++ b/web/app/riot/riot.component.html @@ -8,9 +8,14 @@

Manage Integrations

-

Turn on anything you like. If an integration has some special configuration, it will have a configuration icon next to it.

+

Turn on anything you like. If an integration has some special configuration, it will have a configuration + icon next to it.

- +
\ No newline at end of file diff --git a/web/app/riot/riot.component.ts b/web/app/riot/riot.component.ts index c236bda..9b61638 100644 --- a/web/app/riot/riot.component.ts +++ b/web/app/riot/riot.component.ts @@ -4,6 +4,8 @@ import { ApiService } from "../shared/api.service"; import { ScalarService } from "../shared/scalar.service"; import { ToasterService } from "angular2-toaster"; import { Integration } from "../shared/models/integration"; +import { IntegrationService } from "../shared/integration.service"; +import * as _ from "lodash"; @Component({ selector: 'my-riot', @@ -16,8 +18,7 @@ export class RiotComponent { public integrations: Integration[] = []; public loading = true; public roomId: string; - - private scalarToken: string; + public scalarToken: string; constructor(private activatedRoute: ActivatedRoute, private api: ApiService, @@ -41,7 +42,7 @@ export class RiotComponent { private init() { this.api.getIntegrations(this.roomId, this.scalarToken).then(integrations => { - this.integrations = integrations; + this.integrations = _.filter(integrations, i => IntegrationService.isSupported(i)); let promises = integrations.map(b => this.updateIntegrationState(b)); return Promise.all(promises); }).then(() => this.loading = false).catch(err => { @@ -51,6 +52,8 @@ export class RiotComponent { } private updateIntegrationState(integration: Integration) { + integration.hasConfig = IntegrationService.hasConfig(integration); + return this.scalar.getMembershipState(this.roomId, integration.userId).then(payload => { integration.isBroken = false; @@ -74,21 +77,19 @@ export class RiotComponent { promise = this.api.removeIntegration(this.roomId, integration.type, integration.integrationType, this.scalarToken); } else promise = this.scalar.inviteUser(this.roomId, integration.userId); - promise - .then(() => { - if (integration.isEnabled) - this.toaster.pop('success', integration.name + " was invited to the room"); - else this.toaster.pop('success', integration.name + " was removed from the room"); - }) - .catch(err => { - let errorMessage = "Could not update integration status"; + promise.then(() => { + if (integration.isEnabled) + this.toaster.pop('success', integration.name + " was invited to the room"); + else this.toaster.pop('success', integration.name + " was removed from the room"); + }).catch(err => { + let errorMessage = "Could not update integration status"; - if (err.json) { - errorMessage = err.json().error; - } else errorMessage = err.response.error.message; + if (err.json) { + errorMessage = err.json().error; + } else errorMessage = err.response.error.message; - integration.isEnabled = !integration.isEnabled; - this.toaster.pop("error", errorMessage); - }); + integration.isEnabled = !integration.isEnabled; + this.toaster.pop("error", errorMessage); + }); } } diff --git a/web/app/shared/api.service.ts b/web/app/shared/api.service.ts index 6c20de2..52f8dc4 100644 --- a/web/app/shared/api.service.ts +++ b/web/app/shared/api.service.ts @@ -18,7 +18,14 @@ export class ApiService { } removeIntegration(roomId: string, type: string, integrationType: string, scalarToken: string): Promise { - return this.http.delete("/api/v1/dimension/integrations/" + roomId + "/" + type + "/" + integrationType, {params: {scalar_token: scalarToken}}) + 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 { + 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(); } } diff --git a/web/app/shared/integration.service.ts b/web/app/shared/integration.service.ts new file mode 100644 index 0000000..6f8c93f --- /dev/null +++ b/web/app/shared/integration.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from "@angular/core"; +import { Integration } from "./models/integration"; +import { RssConfigComponent } from "../configs/rss/rss-config.component"; +import { ContainerContent } from "angular2-modal"; + +@Injectable() +export class IntegrationService { + + private static supportedTypeMap = { + "bot": true, + "complex-bot": { + "rss": true + } + }; + + private static components = { + "complex-bot": { + "rss": RssConfigComponent + } + }; + + static isSupported(integration: Integration): boolean { + if (IntegrationService.supportedTypeMap[integration.type] === true) return true; + if (!IntegrationService.supportedTypeMap[integration.type]) return false; + return IntegrationService.supportedTypeMap[integration.type][integration.integrationType] === true; + } + + static hasConfig(integration: Integration): boolean { + return integration.type !== "bot"; + } + + static getConfigComponent(integration: Integration): ContainerContent { + return IntegrationService.components[integration.type][integration.integrationType]; + } + + constructor() { + } +} diff --git a/web/app/shared/models/integration.ts b/web/app/shared/models/integration.ts index 9aaea91..e4a809b 100644 --- a/web/app/shared/models/integration.ts +++ b/web/app/shared/models/integration.ts @@ -7,4 +7,10 @@ export interface Integration { about: string; // nullable isEnabled: boolean; isBroken: boolean; + hasConfig: boolean; } + +export interface RSSIntegration extends Integration { + feeds: string[]; + immutableFeeds: {url: string, ownerId: string}[]; +} \ No newline at end of file diff --git a/web/style/app.scss b/web/style/app.scss index 3bc4100..2c4be9a 100644 --- a/web/style/app.scss +++ b/web/style/app.scss @@ -7,3 +7,23 @@ body { padding: 0; color: #222; } + +// HACK: Work around dialog not showing up +// ref: https://github.com/shlomiassaf/angular2-modal/issues/280 +.fade.in { + opacity: 1; +} + +.modal.in .modal-dialog { + -webkit-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); +} + +.modal-backdrop.in { + opacity: 0.5; +} + +button { + cursor: pointer; +} \ No newline at end of file