From e6da59b728bf44342428531a2c7e4024829234ed Mon Sep 17 00:00:00 2001 From: Dominik Guzei Date: Mon, 14 Jan 2019 19:01:46 +0100 Subject: setup logic to display workspace edit page --- src/actions/index.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/actions/index.js') diff --git a/src/actions/index.js b/src/actions/index.js index 59acabb0b..a406af50a 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -11,6 +11,7 @@ import payment from './payment'; import news from './news'; import settings from './settings'; import requests from './requests'; +import workspace from './workspace'; const actions = Object.assign({}, { service, @@ -23,6 +24,7 @@ const actions = Object.assign({}, { news, settings, requests, + workspace, }); export default defineActions(actions, PropTypes.checkPropTypes); -- cgit v1.2.3-70-g09d2 From 90399cc608b93cc185b0ee1c9b79e98cfafb8bc1 Mon Sep 17 00:00:00 2001 From: Dominik Guzei Date: Tue, 12 Feb 2019 14:59:58 +0100 Subject: consolidate workspace feature for further development --- src/actions/index.js | 2 +- src/actions/workspace.js | 8 --- src/app.js | 4 +- .../settings/workspaces/WorkspaceItem.js | 42 ------------ .../settings/workspaces/WorkspacesDashboard.js | 63 ------------------ src/containers/settings/EditWorkspaceScreen.js | 54 ---------------- src/containers/settings/WorkspacesScreen.js | 37 ----------- src/features/workspaces/actions.js | 8 +++ .../workspaces/components/WorkspaceItem.js | 42 ++++++++++++ .../workspaces/components/WorkspacesDashboard.js | 63 ++++++++++++++++++ .../workspaces/containers/EditWorkspaceScreen.js | 75 ++++++++++++++++++++++ .../workspaces/containers/WorkspacesScreen.js | 37 +++++++++++ src/features/workspaces/models/Workspace.js | 25 ++++++++ src/features/workspaces/store.js | 2 +- .../workspaces/styles/workspaces-table.scss | 53 +++++++++++++++ src/i18n/locales/de.json | 3 + src/i18n/locales/en-US.json | 1 + src/models/Workspace.js | 25 -------- src/styles/main.scss | 2 +- src/styles/workspace-table.scss | 53 --------------- 20 files changed, 312 insertions(+), 287 deletions(-) delete mode 100644 src/actions/workspace.js delete mode 100644 src/components/settings/workspaces/WorkspaceItem.js delete mode 100644 src/components/settings/workspaces/WorkspacesDashboard.js delete mode 100644 src/containers/settings/EditWorkspaceScreen.js delete mode 100644 src/containers/settings/WorkspacesScreen.js create mode 100644 src/features/workspaces/actions.js create mode 100644 src/features/workspaces/components/WorkspaceItem.js create mode 100644 src/features/workspaces/components/WorkspacesDashboard.js create mode 100644 src/features/workspaces/containers/EditWorkspaceScreen.js create mode 100644 src/features/workspaces/containers/WorkspacesScreen.js create mode 100644 src/features/workspaces/models/Workspace.js create mode 100644 src/features/workspaces/styles/workspaces-table.scss delete mode 100644 src/models/Workspace.js delete mode 100644 src/styles/workspace-table.scss (limited to 'src/actions/index.js') diff --git a/src/actions/index.js b/src/actions/index.js index a406af50a..45e6da515 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -11,7 +11,7 @@ import payment from './payment'; import news from './news'; import settings from './settings'; import requests from './requests'; -import workspace from './workspace'; +import workspace from '../features/workspaces/actions'; const actions = Object.assign({}, { service, diff --git a/src/actions/workspace.js b/src/actions/workspace.js deleted file mode 100644 index ab07a96c0..000000000 --- a/src/actions/workspace.js +++ /dev/null @@ -1,8 +0,0 @@ -import PropTypes from 'prop-types'; -import Workspace from '../models/Workspace'; - -export default { - edit: { - workspace: PropTypes.instanceOf(Workspace).isRequired, - }, -}; diff --git a/src/app.js b/src/app.js index 320ba679f..d3b540f62 100644 --- a/src/app.js +++ b/src/app.js @@ -39,8 +39,8 @@ import PricingScreen from './containers/auth/PricingScreen'; import InviteScreen from './containers/auth/InviteScreen'; import AuthLayoutContainer from './containers/auth/AuthLayoutContainer'; import SubscriptionPopupScreen from './containers/subscription/SubscriptionPopupScreen'; -import WorkspacesScreen from './containers/settings/WorkspacesScreen'; -import EditWorkspaceScreen from './containers/settings/EditWorkspaceScreen'; +import WorkspacesScreen from './features/workspaces/containers/WorkspacesScreen'; +import EditWorkspaceScreen from './features/workspaces/containers/EditWorkspaceScreen'; // Add Polyfills smoothScroll.polyfill(); diff --git a/src/components/settings/workspaces/WorkspaceItem.js b/src/components/settings/workspaces/WorkspaceItem.js deleted file mode 100644 index 088d61433..000000000 --- a/src/components/settings/workspaces/WorkspaceItem.js +++ /dev/null @@ -1,42 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { intlShape } from 'react-intl'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import Workspace from '../../../models/Workspace'; - -// const messages = defineMessages({}); - -@observer -class WorkspaceItem extends Component { - static propTypes = { - workspace: PropTypes.instanceOf(Workspace).isRequired, - onItemClick: PropTypes.func.isRequired, - }; - - static contextTypes = { - intl: intlShape, - }; - - render() { - const { workspace, onItemClick } = this.props; - // const { intl } = this.context; - - return ( - - onItemClick(workspace)} - > - {workspace.name} - - - ); - } -} - -export default WorkspaceItem; diff --git a/src/components/settings/workspaces/WorkspacesDashboard.js b/src/components/settings/workspaces/WorkspacesDashboard.js deleted file mode 100644 index a5bb18cb7..000000000 --- a/src/components/settings/workspaces/WorkspacesDashboard.js +++ /dev/null @@ -1,63 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; -import { defineMessages, intlShape } from 'react-intl'; - -import Loader from '../../ui/Loader'; -import WorkspaceItem from './WorkspaceItem'; - -const messages = defineMessages({ - headline: { - id: 'settings.workspaces.headline', - defaultMessage: '!!!Your workspaces', - }, - noServicesAdded: { - id: 'settings.workspaces.noWorkspacesAdded', - defaultMessage: '!!!You haven\'t added any workspaces yet.', - }, -}); - -@observer -class WorkspacesDashboard extends Component { - static propTypes = { - workspaces: MobxPropTypes.arrayOrObservableArray.isRequired, - isLoading: PropTypes.bool.isRequired, - onWorkspaceClick: PropTypes.func.isRequired, - }; - - static contextTypes = { - intl: intlShape, - }; - - render() { - const { workspaces, isLoading, onWorkspaceClick } = this.props; - const { intl } = this.context; - - return ( -
-
-

{intl.formatMessage(messages.headline)}

-
-
- {isLoading ? ( - - ) : ( - - - {workspaces.map(workspace => ( - onWorkspaceClick(w)} - /> - ))} - -
- )} -
-
- ); - } -} - -export default WorkspacesDashboard; diff --git a/src/containers/settings/EditWorkspaceScreen.js b/src/containers/settings/EditWorkspaceScreen.js deleted file mode 100644 index 665b405bd..000000000 --- a/src/containers/settings/EditWorkspaceScreen.js +++ /dev/null @@ -1,54 +0,0 @@ -import React, { Component } from 'react'; -import { inject, observer } from 'mobx-react'; -import { defineMessages, intlShape } from 'react-intl'; -import Form from '../../lib/Form'; -import ErrorBoundary from '../../components/util/ErrorBoundary'; -import { gaPage } from '../../lib/analytics'; -import { state } from '../../features/workspaces/state'; - -const messages = defineMessages({ - name: { - id: 'settings.workspace.form.name', - defaultMessage: '!!!Name', - }, -}); - -@inject('stores', 'actions') @observer -class EditWorkspaceScreen extends Component { - static contextTypes = { - intl: intlShape, - }; - - componentDidMount() { - gaPage('Settings/Workspace/Edit'); - } - - prepareForm(workspace) { - const { intl } = this.context; - const config = { - fields: { - name: { - label: intl.formatMessage(messages.name), - placeholder: intl.formatMessage(messages.name), - value: workspace.name, - }, - }, - }; - return new Form(config); - } - - render() { - const { workspaceBeingEdited } = state; - if (!workspaceBeingEdited) return null; - - // const form = this.prepareForm(workspaceBeingEdited); - - return ( - -
{workspaceBeingEdited.name}
-
- ); - } -} - -export default EditWorkspaceScreen; diff --git a/src/containers/settings/WorkspacesScreen.js b/src/containers/settings/WorkspacesScreen.js deleted file mode 100644 index 5e91f7673..000000000 --- a/src/containers/settings/WorkspacesScreen.js +++ /dev/null @@ -1,37 +0,0 @@ -import React, { Component } from 'react'; -import { inject, observer } from 'mobx-react'; -import PropTypes from 'prop-types'; -import { gaPage } from '../../lib/analytics'; -import { state } from '../../features/workspaces/state'; -import WorkspacesDashboard from '../../components/settings/workspaces/WorkspacesDashboard'; -import ErrorBoundary from '../../components/util/ErrorBoundary'; - -@inject('actions') @observer -class WorkspacesScreen extends Component { - static propTypes = { - actions: PropTypes.shape({ - workspace: PropTypes.shape({ - edit: PropTypes.func.isRequired, - }), - }).isRequired, - }; - - componentDidMount() { - gaPage('Settings/Workspaces Dashboard'); - } - - render() { - const { workspace } = this.props.actions; - return ( - - workspace.edit({ workspace: w })} - /> - - ); - } -} - -export default WorkspacesScreen; diff --git a/src/features/workspaces/actions.js b/src/features/workspaces/actions.js new file mode 100644 index 000000000..30866af96 --- /dev/null +++ b/src/features/workspaces/actions.js @@ -0,0 +1,8 @@ +import PropTypes from 'prop-types'; +import Workspace from './models/Workspace'; + +export default { + edit: { + workspace: PropTypes.instanceOf(Workspace).isRequired, + }, +}; diff --git a/src/features/workspaces/components/WorkspaceItem.js b/src/features/workspaces/components/WorkspaceItem.js new file mode 100644 index 000000000..b2c2a4830 --- /dev/null +++ b/src/features/workspaces/components/WorkspaceItem.js @@ -0,0 +1,42 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { intlShape } from 'react-intl'; +import { observer } from 'mobx-react'; +import classnames from 'classnames'; +import Workspace from '../models/Workspace'; + +// const messages = defineMessages({}); + +@observer +class WorkspaceItem extends Component { + static propTypes = { + workspace: PropTypes.instanceOf(Workspace).isRequired, + onItemClick: PropTypes.func.isRequired, + }; + + static contextTypes = { + intl: intlShape, + }; + + render() { + const { workspace, onItemClick } = this.props; + // const { intl } = this.context; + + return ( + + onItemClick(workspace)} + > + {workspace.name} + + + ); + } +} + +export default WorkspaceItem; diff --git a/src/features/workspaces/components/WorkspacesDashboard.js b/src/features/workspaces/components/WorkspacesDashboard.js new file mode 100644 index 000000000..2a8b3a5ee --- /dev/null +++ b/src/features/workspaces/components/WorkspacesDashboard.js @@ -0,0 +1,63 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; + +import Loader from '../../../components/ui/Loader'; +import WorkspaceItem from './WorkspaceItem'; + +const messages = defineMessages({ + headline: { + id: 'settings.workspaces.headline', + defaultMessage: '!!!Your workspaces', + }, + noServicesAdded: { + id: 'settings.workspaces.noWorkspacesAdded', + defaultMessage: '!!!You haven\'t added any workspaces yet.', + }, +}); + +@observer +class WorkspacesDashboard extends Component { + static propTypes = { + workspaces: MobxPropTypes.arrayOrObservableArray.isRequired, + isLoading: PropTypes.bool.isRequired, + onWorkspaceClick: PropTypes.func.isRequired, + }; + + static contextTypes = { + intl: intlShape, + }; + + render() { + const { workspaces, isLoading, onWorkspaceClick } = this.props; + const { intl } = this.context; + + return ( +
+
+

{intl.formatMessage(messages.headline)}

+
+
+ {isLoading ? ( + + ) : ( + + + {workspaces.map(workspace => ( + onWorkspaceClick(w)} + /> + ))} + +
+ )} +
+
+ ); + } +} + +export default WorkspacesDashboard; diff --git a/src/features/workspaces/containers/EditWorkspaceScreen.js b/src/features/workspaces/containers/EditWorkspaceScreen.js new file mode 100644 index 000000000..d8c52f586 --- /dev/null +++ b/src/features/workspaces/containers/EditWorkspaceScreen.js @@ -0,0 +1,75 @@ +import React, { Component } from 'react'; +import { inject, observer } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; +import { Link } from 'react-router'; +import Form from '../../../lib/Form'; +import ErrorBoundary from '../../../components/util/ErrorBoundary'; +import { gaPage } from '../../../lib/analytics'; +import { state } from '../state'; + +const messages = defineMessages({ + name: { + id: 'settings.workspace.form.name', + defaultMessage: '!!!Name', + }, + yourWorkspaces: { + id: 'settings.workspace.form.yourWorkspaces', + defaultMessage: '!!!Your workspaces', + }, +}); + +@inject('stores', 'actions') @observer +class EditWorkspaceScreen extends Component { + static contextTypes = { + intl: intlShape, + }; + + componentDidMount() { + gaPage('Settings/Workspace/Edit'); + } + + prepareForm(workspace) { + const { intl } = this.context; + const config = { + fields: { + name: { + label: intl.formatMessage(messages.name), + placeholder: intl.formatMessage(messages.name), + value: workspace.name, + }, + }, + }; + return new Form(config); + } + + render() { + const { intl } = this.context; + const { workspaceBeingEdited } = state; + if (!workspaceBeingEdited) return null; + + // const form = this.prepareForm(workspaceBeingEdited); + + return ( + +
+
+ + + {intl.formatMessage(messages.yourWorkspaces)} + + + + + {workspaceBeingEdited.name} + +
+
+ test +
+
+
+ ); + } +} + +export default EditWorkspaceScreen; diff --git a/src/features/workspaces/containers/WorkspacesScreen.js b/src/features/workspaces/containers/WorkspacesScreen.js new file mode 100644 index 000000000..f129edec5 --- /dev/null +++ b/src/features/workspaces/containers/WorkspacesScreen.js @@ -0,0 +1,37 @@ +import React, { Component } from 'react'; +import { inject, observer } from 'mobx-react'; +import PropTypes from 'prop-types'; +import { gaPage } from '../../../lib/analytics'; +import { state } from '../state'; +import WorkspacesDashboard from '../components/WorkspacesDashboard'; +import ErrorBoundary from '../../../components/util/ErrorBoundary'; + +@inject('actions') @observer +class WorkspacesScreen extends Component { + static propTypes = { + actions: PropTypes.shape({ + workspace: PropTypes.shape({ + edit: PropTypes.func.isRequired, + }), + }).isRequired, + }; + + componentDidMount() { + gaPage('Settings/Workspaces Dashboard'); + } + + render() { + const { workspace } = this.props.actions; + return ( + + workspace.edit({ workspace: w })} + /> + + ); + } +} + +export default WorkspacesScreen; diff --git a/src/features/workspaces/models/Workspace.js b/src/features/workspaces/models/Workspace.js new file mode 100644 index 000000000..ede2710dc --- /dev/null +++ b/src/features/workspaces/models/Workspace.js @@ -0,0 +1,25 @@ +import { observable } from 'mobx'; + +export default class Workspace { + id = null; + + @observable name = null; + + @observable order = null; + + @observable services = []; + + @observable userId = null; + + constructor(data) { + if (!data.id) { + throw Error('Workspace requires Id'); + } + + this.id = data.id; + this.name = data.name; + this.order = data.order; + this.services = data.services; + this.userId = data.userId; + } +} diff --git a/src/features/workspaces/store.js b/src/features/workspaces/store.js index aab66708b..ea61cec31 100644 --- a/src/features/workspaces/store.js +++ b/src/features/workspaces/store.js @@ -1,7 +1,7 @@ import { observable, reaction } from 'mobx'; import Store from '../../stores/lib/Store'; import CachedRequest from '../../stores/lib/CachedRequest'; -import Workspace from '../../models/Workspace'; +import Workspace from './models/Workspace'; import { matchRoute } from '../../helpers/routing-helpers'; const debug = require('debug')('Franz:feature:workspaces'); diff --git a/src/features/workspaces/styles/workspaces-table.scss b/src/features/workspaces/styles/workspaces-table.scss new file mode 100644 index 000000000..6d0e7b4f5 --- /dev/null +++ b/src/features/workspaces/styles/workspaces-table.scss @@ -0,0 +1,53 @@ +@import '../../../styles/config'; + +.theme__dark .workspace-table { + .workspace-table__column-info .mdi { color: $dark-theme-gray-lightest; } + + .workspace-table__row { + border-bottom: 1px solid $dark-theme-gray-darker; + + &:hover { background: $dark-theme-gray-darker; } + &.workspace-table__row--disabled { color: $dark-theme-gray; } + } +} + +.workspace-table { + width: 100%; + + .workspace-table__toggle { + width: 60px; + + .franz-form__field { + margin-bottom: 0; + } + } + + .workspace-table__column-action { + width: 40px + } + + .workspace-table__column-info { + width: 40px; + + .mdi { + color: $theme-gray-light; + display: block; + font-size: 18px; + } + } + + .workspace-table__row { + border-bottom: 1px solid $theme-gray-lightest; + + &:hover { + cursor: initial; + background: $theme-gray-lightest; + } + + &.workspace-table__row--disabled { + color: $theme-gray-light; + } + } + + td { padding: 10px; } +} diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index b5abb56d4..5cb8c6bd3 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -158,6 +158,7 @@ "settings.navigation.logout" : "Abmelden", "settings.navigation.settings" : "Einstellungen", "settings.navigation.yourServices" : "Deine Dienste", + "settings.navigation.yourWorkspaces": "Deine Workspaces", "settings.recipes.all" : "Alle Dienste", "settings.recipes.dev" : "Entwicklung", "settings.recipes.headline" : "Verfügbare Dienste", @@ -216,6 +217,8 @@ "settings.services.tooltip.isMuted" : "Alle Töne sind deaktiviert", "settings.services.tooltip.notificationsDisabled" : "Benachrichtigungen deaktiviert", "settings.services.updatedInfo" : "Deine Änderungen wurden gespeichert", + "settings.workspaces.headline": "Deine Workspaces", + "settings.workspace.form.yourWorkspaces": "Deine Workspaces", "settings.user.form.accountType.company" : "Firma", "settings.user.form.accountType.individual" : "Einzelperson", "settings.user.form.accountType.label" : "Konto-Typ", diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 1652b5585..9b323e323 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -197,6 +197,7 @@ "settings.user.form.accountType.non-profit": "Non-Profit", "settings.user.form.accountType.company": "Company", "settings.workspaces.headline": "Your workspaces", + "settings.workspace.form.yourWorkspaces": "Your workspaces", "subscription.type.free": "free", "subscription.type.month": "month", "subscription.type.year": "year", diff --git a/src/models/Workspace.js b/src/models/Workspace.js deleted file mode 100644 index ede2710dc..000000000 --- a/src/models/Workspace.js +++ /dev/null @@ -1,25 +0,0 @@ -import { observable } from 'mobx'; - -export default class Workspace { - id = null; - - @observable name = null; - - @observable order = null; - - @observable services = []; - - @observable userId = null; - - constructor(data) { - if (!data.id) { - throw Error('Workspace requires Id'); - } - - this.id = data.id; - this.name = data.name; - this.order = data.order; - this.services = data.services; - this.userId = data.userId; - } -} diff --git a/src/styles/main.scss b/src/styles/main.scss index 30f43532f..a941d89d0 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -30,7 +30,7 @@ $mdi-font-path: '../node_modules/mdi/fonts'; @import './content-tabs.scss'; @import './invite.scss'; @import './title-bar.scss'; -@import './workspace-table.scss'; +@import '../features/workspaces/styles/workspaces-table'; // form @import './input.scss'; diff --git a/src/styles/workspace-table.scss b/src/styles/workspace-table.scss deleted file mode 100644 index 05ebfa629..000000000 --- a/src/styles/workspace-table.scss +++ /dev/null @@ -1,53 +0,0 @@ -@import './config.scss'; - -.theme__dark .workspace-table { - .workspace-table__column-info .mdi { color: $dark-theme-gray-lightest; } - - .workspace-table__row { - border-bottom: 1px solid $dark-theme-gray-darker; - - &:hover { background: $dark-theme-gray-darker; } - &.workspace-table__row--disabled { color: $dark-theme-gray; } - } -} - -.workspace-table { - width: 100%; - - .workspace-table__toggle { - width: 60px; - - .franz-form__field { - margin-bottom: 0; - } - } - - .workspace-table__column-action { - width: 40px - } - - .workspace-table__column-info { - width: 40px; - - .mdi { - color: $theme-gray-light; - display: block; - font-size: 18px; - } - } - - .workspace-table__row { - border-bottom: 1px solid $theme-gray-lightest; - - &:hover { - cursor: initial; - background: $theme-gray-lightest; - } - - &.workspace-table__row--disabled { - color: $theme-gray-light; - } - } - - td { padding: 10px; } -} -- cgit v1.2.3-70-g09d2 From 739ef2e8a2dec94c3e10c3d26d797fe759fac7aa Mon Sep 17 00:00:00 2001 From: Dominik Guzei Date: Fri, 1 Mar 2019 14:25:44 +0100 Subject: finish workspaces mvp --- src/actions/index.js | 8 ++- src/actions/lib/actions.js | 29 +++++---- src/components/layout/Sidebar.js | 2 +- src/components/services/tabs/Tabbar.js | 4 +- src/features/workspaces/actions.js | 9 ++- .../workspaces/containers/EditWorkspaceScreen.js | 12 ++-- .../workspaces/containers/WorkspacesScreen.js | 10 +-- src/features/workspaces/index.js | 40 +++++++++++- src/features/workspaces/state.js | 6 +- src/features/workspaces/store.js | 37 +++++++---- src/i18n/locales/de.json | 5 +- src/i18n/locales/en-US.json | 5 +- src/lib/Menu.js | 71 +++++++++++++++++++++- src/stores/ServicesStore.js | 12 ++-- 14 files changed, 193 insertions(+), 57 deletions(-) (limited to 'src/actions/index.js') diff --git a/src/actions/index.js b/src/actions/index.js index 45e6da515..00f843cd6 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -11,7 +11,7 @@ import payment from './payment'; import news from './news'; import settings from './settings'; import requests from './requests'; -import workspace from '../features/workspaces/actions'; +import workspaces from '../features/workspaces/actions'; const actions = Object.assign({}, { service, @@ -24,7 +24,9 @@ const actions = Object.assign({}, { news, settings, requests, - workspace, }); -export default defineActions(actions, PropTypes.checkPropTypes); +export default Object.assign( + defineActions(actions, PropTypes.checkPropTypes), + { workspaces }, +); diff --git a/src/actions/lib/actions.js b/src/actions/lib/actions.js index 499018d70..6571e9441 100644 --- a/src/actions/lib/actions.js +++ b/src/actions/lib/actions.js @@ -1,18 +1,23 @@ +export const createActionsFromDefinitions = (actionDefinitions, validate) => { + const actions = {}; + Object.keys(actionDefinitions).forEach((actionName) => { + const action = (params) => { + const schema = actionDefinitions[actionName]; + validate(schema, params, actionName); + action.notify(params); + }; + actions[actionName] = action; + action.listeners = []; + action.listen = listener => action.listeners.push(listener); + action.notify = params => action.listeners.forEach(listener => listener(params)); + }); + return actions; +}; + export default (definitions, validate) => { const newActions = {}; Object.keys(definitions).forEach((scopeName) => { - newActions[scopeName] = {}; - Object.keys(definitions[scopeName]).forEach((actionName) => { - const action = (params) => { - const schema = definitions[scopeName][actionName]; - validate(schema, params, actionName); - action.notify(params); - }; - newActions[scopeName][actionName] = action; - action.listeners = []; - action.listen = listener => action.listeners.push(listener); - action.notify = params => action.listeners.forEach(listener => listener(params)); - }); + newActions[scopeName] = createActionsFromDefinitions(definitions[scopeName], validate); }); return newActions; }; diff --git a/src/components/layout/Sidebar.js b/src/components/layout/Sidebar.js index 609a3b604..fcc5b0001 100644 --- a/src/components/layout/Sidebar.js +++ b/src/components/layout/Sidebar.js @@ -31,7 +31,7 @@ export default @observer class Sidebar extends Component { openSettings: PropTypes.func.isRequired, toggleMuteApp: PropTypes.func.isRequired, isAppMuted: PropTypes.bool.isRequired, - } + }; static contextTypes = { intl: intlShape, diff --git a/src/components/services/tabs/Tabbar.js b/src/components/services/tabs/Tabbar.js index dd5c2140f..5e8260ad0 100644 --- a/src/components/services/tabs/Tabbar.js +++ b/src/components/services/tabs/Tabbar.js @@ -19,7 +19,7 @@ export default @observer class TabBar extends Component { updateService: PropTypes.func.isRequired, showMessageBadgeWhenMutedSetting: PropTypes.bool.isRequired, showMessageBadgesEvenWhenMuted: PropTypes.bool.isRequired, - } + }; onSortEnd = ({ oldIndex, newIndex }) => { const { @@ -45,7 +45,7 @@ export default @observer class TabBar extends Component { redirect: false, }); } - } + }; disableService({ serviceId }) { this.toggleService({ serviceId, isEnabled: false }); diff --git a/src/features/workspaces/actions.js b/src/features/workspaces/actions.js index 84de2b011..25246de09 100644 --- a/src/features/workspaces/actions.js +++ b/src/features/workspaces/actions.js @@ -1,7 +1,8 @@ import PropTypes from 'prop-types'; import Workspace from './models/Workspace'; +import { createActionsFromDefinitions } from '../../actions/lib/actions'; -export default { +export default createActionsFromDefinitions({ edit: { workspace: PropTypes.instanceOf(Workspace).isRequired, }, @@ -14,4 +15,8 @@ export default { update: { workspace: PropTypes.instanceOf(Workspace).isRequired, }, -}; + activate: { + workspace: PropTypes.instanceOf(Workspace).isRequired, + }, + deactivate: {}, +}, PropTypes.checkPropTypes); diff --git a/src/features/workspaces/containers/EditWorkspaceScreen.js b/src/features/workspaces/containers/EditWorkspaceScreen.js index 790b8a0fe..1b13bc2d4 100644 --- a/src/features/workspaces/containers/EditWorkspaceScreen.js +++ b/src/features/workspaces/containers/EditWorkspaceScreen.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import ErrorBoundary from '../../../components/util/ErrorBoundary'; import EditWorkspaceForm from '../components/EditWorkspaceForm'; -import { state } from '../state'; +import { workspacesState } from '../state'; import ServicesStore from '../../../stores/ServicesStore'; import Workspace from '../models/Workspace'; @@ -22,23 +22,23 @@ class EditWorkspaceScreen extends Component { }; onDelete = () => { - const { workspaceBeingEdited } = state; + const { workspaceBeingEdited } = workspacesState; const { actions } = this.props; if (!workspaceBeingEdited) return null; - actions.workspace.delete({ workspace: workspaceBeingEdited }); + actions.workspaces.delete({ workspace: workspaceBeingEdited }); }; onSave = (values) => { - const { workspaceBeingEdited } = state; + const { workspaceBeingEdited } = workspacesState; const { actions } = this.props; const workspace = new Workspace( Object.assign({}, workspaceBeingEdited, values), ); - actions.workspace.update({ workspace }); + actions.workspaces.update({ workspace }); }; render() { - const { workspaceBeingEdited } = state; + const { workspaceBeingEdited } = workspacesState; const { stores } = this.props; if (!workspaceBeingEdited) return null; return ( diff --git a/src/features/workspaces/containers/WorkspacesScreen.js b/src/features/workspaces/containers/WorkspacesScreen.js index b89cbcf67..94e714255 100644 --- a/src/features/workspaces/containers/WorkspacesScreen.js +++ b/src/features/workspaces/containers/WorkspacesScreen.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import { inject, observer } from 'mobx-react'; import PropTypes from 'prop-types'; -import { state } from '../state'; +import { workspacesState } from '../state'; import WorkspacesDashboard from '../components/WorkspacesDashboard'; import ErrorBoundary from '../../../components/util/ErrorBoundary'; @@ -20,10 +20,10 @@ class WorkspacesScreen extends Component { return ( actions.workspace.create(data)} - onWorkspaceClick={w => actions.workspace.edit({ workspace: w })} + workspaces={workspacesState.workspaces} + isLoading={workspacesState.isLoading} + onCreateWorkspaceSubmit={data => actions.workspaces.create(data)} + onWorkspaceClick={w => actions.workspaces.edit({ workspace: w })} /> ); diff --git a/src/features/workspaces/index.js b/src/features/workspaces/index.js index 50ac3b414..8091f49fc 100644 --- a/src/features/workspaces/index.js +++ b/src/features/workspaces/index.js @@ -1,14 +1,28 @@ -import { reaction } from 'mobx'; +import { reaction, runInAction } from 'mobx'; import WorkspacesStore from './store'; import api from './api'; -import { state, resetState } from './state'; +import { workspacesState, resetState } from './state'; const debug = require('debug')('Franz:feature:workspaces'); let store = null; +export const filterServicesByActiveWorkspace = (services) => { + const { isFeatureActive, activeWorkspace } = workspacesState; + if (isFeatureActive && activeWorkspace) { + return services.filter(s => activeWorkspace.services.includes(s.id)); + } + return services; +}; + +export const getActiveWorkspaceServices = (services) => { + return filterServicesByActiveWorkspace(services); +}; + export default function initWorkspaces(stores, actions) { const { features, user } = stores; + + // Toggle workspace feature reaction( () => ( features.features.isWorkspaceEnabled && ( @@ -18,10 +32,12 @@ export default function initWorkspaces(stores, actions) { (isEnabled) => { if (isEnabled) { debug('Initializing `workspaces` feature'); - store = new WorkspacesStore(stores, api, actions, state); + store = new WorkspacesStore(stores, api, actions, workspacesState); store.initialize(); + runInAction(() => { workspacesState.isFeatureActive = true; }); } else if (store) { debug('Disabling `workspaces` feature'); + runInAction(() => { workspacesState.isFeatureActive = false; }); store.teardown(); store = null; resetState(); // Reset state to default @@ -31,4 +47,22 @@ export default function initWorkspaces(stores, actions) { fireImmediately: true, }, ); + + // Update active service on workspace switches + reaction(() => ({ + isFeatureActive: workspacesState.isFeatureActive, + activeWorkspace: workspacesState.activeWorkspace, + }), ({ isFeatureActive, activeWorkspace }) => { + if (!isFeatureActive) return; + if (activeWorkspace) { + const services = stores.services.allDisplayed; + const activeService = services.find(s => s.isActive); + const workspaceServices = filterServicesByActiveWorkspace(services); + const isActiveServiceInWorkspace = workspaceServices.includes(activeService); + if (!isActiveServiceInWorkspace) { + console.log(workspaceServices[0].id); + actions.service.setActive({ serviceId: workspaceServices[0].id }); + } + } + }); } diff --git a/src/features/workspaces/state.js b/src/features/workspaces/state.js index f938c1470..963b96f81 100644 --- a/src/features/workspaces/state.js +++ b/src/features/workspaces/state.js @@ -1,13 +1,15 @@ import { observable } from 'mobx'; const defaultState = { + activeWorkspace: null, isLoading: false, + isFeatureActive: false, workspaces: [], workspaceBeingEdited: null, }; -export const state = observable(defaultState); +export const workspacesState = observable(defaultState); export function resetState() { - Object.assign(state, defaultState); + Object.assign(workspacesState, defaultState); } diff --git a/src/features/workspaces/store.js b/src/features/workspaces/store.js index 5cccb2ab7..a2997a0d2 100644 --- a/src/features/workspaces/store.js +++ b/src/features/workspaces/store.js @@ -1,8 +1,9 @@ -import { observable, reaction } from 'mobx'; +import { observable, reaction, action } from 'mobx'; import Store from '../../stores/lib/Store'; import CachedRequest from '../../stores/lib/CachedRequest'; import Workspace from './models/Workspace'; import { matchRoute } from '../../helpers/routing-helpers'; +import workspaceActions from './actions'; const debug = require('debug')('Franz:feature:workspaces'); @@ -48,28 +49,30 @@ export default class WorkspacesStore extends Store { }, ); - this.actions.workspace.edit.listen(this._edit); - this.actions.workspace.create.listen(this._create); - this.actions.workspace.delete.listen(this._delete); - this.actions.workspace.update.listen(this._update); + workspaceActions.edit.listen(this._edit); + workspaceActions.create.listen(this._create); + workspaceActions.delete.listen(this._delete); + workspaceActions.update.listen(this._update); + workspaceActions.activate.listen(this._setActiveWorkspace); + workspaceActions.deactivate.listen(this._deactivateActiveWorkspace); } - _setWorkspaces = (workspaces) => { + _getWorkspaceById = id => this.state.workspaces.find(w => w.id === id); + + @action _setWorkspaces = (workspaces) => { debug('setting user workspaces', workspaces.slice()); this.state.workspaces = workspaces.map(data => new Workspace(data)); }; - _setIsLoading = (isLoading) => { + @action _setIsLoading = (isLoading) => { this.state.isLoading = isLoading; }; - _getWorkspaceById = id => this.state.workspaces.find(w => w.id === id); - - _edit = ({ workspace }) => { + @action _edit = ({ workspace }) => { this.stores.router.push(`/settings/workspaces/edit/${workspace.id}`); }; - _create = async ({ name }) => { + @action _create = async ({ name }) => { try { const result = await this.api.createWorkspace(name); const workspace = new Workspace(result); @@ -80,7 +83,7 @@ export default class WorkspacesStore extends Store { } }; - _delete = async ({ workspace }) => { + @action _delete = async ({ workspace }) => { try { await this.api.deleteWorkspace(workspace); this.state.workspaces.remove(workspace); @@ -90,7 +93,7 @@ export default class WorkspacesStore extends Store { } }; - _update = async ({ workspace }) => { + @action _update = async ({ workspace }) => { try { await this.api.updateWorkspace(workspace); const localWorkspace = this.state.workspaces.find(ws => ws.id === workspace.id); @@ -100,4 +103,12 @@ export default class WorkspacesStore extends Store { throw error; } }; + + @action _setActiveWorkspace = ({ workspace }) => { + this.state.activeWorkspace = workspace; + }; + + @action _deactivateActiveWorkspace = () => { + this.state.activeWorkspace = null; + }; } diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 4906070a3..0c1fb8aa6 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -74,6 +74,9 @@ "menu.window" : "Fenster", "menu.window.close" : "Schließen", "menu.window.minimize" : "Minimieren", + "menu.workspaces": "Workspaces", + "menu.workspaces.defaultWorkspace": "All services", + "menu.workspaces.addNewWorkspace": "Add New Workspace", "password.email.label" : "E-Mail Adresse", "password.headline" : "Passwort zurücksetzen", "password.link.login" : "An Deinem Konto anmelden", @@ -224,7 +227,7 @@ "settings.workspace.form.name": "Name", "settings.workspace.form.buttonDelete": "Workspace löschen", "settings.workspace.form.buttonSave": "Workspace speichern", - "settings.workspace.form.servicesInWorkspaceHeadline": "Services in diesem Workspace", + "settings.workspace.form.servicesInWorkspaceHeadline": "Services in diesem Workspace", "settings.user.form.accountType.company" : "Firma", "settings.user.form.accountType.individual" : "Einzelperson", "settings.user.form.accountType.label" : "Konto-Typ", diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 988ac46f2..2a51662a2 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -276,7 +276,10 @@ "menu.app.hideOthers": "Hide Others", "menu.app.unhide": "Unhide", "menu.app.quit": "Quit", - "menu.services.addNewService": "Add New Service...", + "menu.services.addNewService": "Add New Service", + "menu.workspaces": "Workspaces", + "menu.workspaces.defaultWorkspace": "All services", + "menu.workspaces.addNewWorkspace": "Add New Workspace", "validation.required": "{field} is required", "validation.email": "{field} is not valid", "validation.url": "{field} is not a valid URL", diff --git a/src/lib/Menu.js b/src/lib/Menu.js index c378619ad..1560dd285 100644 --- a/src/lib/Menu.js +++ b/src/lib/Menu.js @@ -3,6 +3,8 @@ import { observable, autorun, computed } from 'mobx'; import { defineMessages } from 'react-intl'; import { isMac, ctrlKey, cmdKey } from '../environment'; +import { workspacesState } from '../features/workspaces/state'; +import workspaceActions from '../features/workspaces/actions'; const { app, Menu, dialog } = remote; @@ -179,6 +181,18 @@ const menuItems = defineMessages({ id: 'menu.services.addNewService', defaultMessage: '!!!Add New Service...', }, + workspaces: { + id: 'menu.workspaces', + defaultMessage: '!!!Workspaces', + }, + defaultWorkspace: { + id: 'menu.workspaces.defaultWorkspace', + defaultMessage: '!!!Default', + }, + addNewWorkspace: { + id: 'menu.workspaces.addNewWorkspace', + defaultMessage: '!!!Add New Workspace...', + }, }); function getActiveWebview() { @@ -265,6 +279,10 @@ const _templateFactory = intl => [ label: intl.formatMessage(menuItems.services), submenu: [], }, + { + label: intl.formatMessage(menuItems.workspaces), + submenu: [], + }, { label: intl.formatMessage(menuItems.window), role: 'window', @@ -499,7 +517,9 @@ export default class FranzMenu { } _build() { - const serviceTpl = Object.assign([], this.serviceTpl); // need to clone object so we don't modify computed (cached) object + // need to clone object so we don't modify computed (cached) object + const serviceTpl = Object.assign([], this.serviceTpl); + const workspacesMenu = Object.assign([], this.workspacesMenu); if (window.franz === undefined) { return; @@ -632,7 +652,7 @@ export default class FranzMenu { }, ); - tpl[4].submenu.unshift(about, { + tpl[5].submenu.unshift(about, { type: 'separator', }); } else { @@ -678,6 +698,8 @@ export default class FranzMenu { tpl[3].submenu = serviceTpl; } + tpl[4].submenu = workspacesMenu; + this.currentTemplate = tpl; const menu = Menu.buildFromTemplate(tpl); Menu.setApplicationMenu(menu); @@ -701,6 +723,51 @@ export default class FranzMenu { return []; } + @computed get workspacesMenu() { + const { workspaces, activeWorkspace } = workspacesState; + const { intl } = window.franz; + const menu = []; + + // Add new workspace item: + menu.push({ + label: intl.formatMessage(menuItems.addNewWorkspace), + accelerator: `${cmdKey}+Shift+N`, + click: () => { + this.actions.ui.openSettings({ path: 'workspaces' }); + }, + enabled: this.stores.user.isLoggedIn, + }, { + type: 'separator', + }); + + // Default workspace + menu.push({ + label: intl.formatMessage(menuItems.defaultWorkspace), + accelerator: `${cmdKey}+Alt+1`, + type: 'radio', + checked: !activeWorkspace, + click: () => { + workspaceActions.deactivate(); + }, + }); + + // Workspace items + if (this.stores.user.isLoggedIn) { + workspaces.forEach((workspace, i) => menu.push({ + label: workspace.name, + accelerator: i < 9 ? `${cmdKey}+Alt+${i + 2}` : null, + type: 'radio', + checked: activeWorkspace ? workspace.id === activeWorkspace.id : false, + click: () => { + workspaceActions.activate({ workspace }); + }, + })); + } + + console.log(menu); + return menu; + } + _getServiceName(service) { if (service.name) { return service.name; diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js index c63bef196..a86db8103 100644 --- a/src/stores/ServicesStore.js +++ b/src/stores/ServicesStore.js @@ -2,7 +2,7 @@ import { action, reaction, computed, - observable, + observable, runInAction, } from 'mobx'; import { debounce, remove } from 'lodash'; import ms from 'ms'; @@ -12,6 +12,8 @@ import Request from './lib/Request'; import CachedRequest from './lib/CachedRequest'; import { matchRoute } from '../helpers/routing-helpers'; import { gaEvent } from '../lib/analytics'; +import { workspacesState } from '../features/workspaces/state'; +import { filterServicesByActiveWorkspace, getActiveWorkspaceServices } from '../features/workspaces'; const debug = require('debug')('Franz:ServiceStore'); @@ -98,7 +100,6 @@ export default class ServicesStore extends Store { return observable(services.slice().slice().sort((a, b) => a.order - b.order)); } } - return []; } @@ -107,13 +108,16 @@ export default class ServicesStore extends Store { } @computed get allDisplayed() { - return this.stores.settings.all.app.showDisabledServices ? this.all : this.enabled; + const services = this.stores.settings.all.app.showDisabledServices ? this.all : this.enabled; + return filterServicesByActiveWorkspace(services); } // This is just used to avoid unnecessary rerendering of resource-heavy webviews @computed get allDisplayedUnordered() { + const { showDisabledServices } = this.stores.settings.all.app; const services = this.allServicesRequest.execute().result || []; - return this.stores.settings.all.app.showDisabledServices ? services : services.filter(service => service.isEnabled); + const filteredServices = showDisabledServices ? services : services.filter(service => service.isEnabled); + return getActiveWorkspaceServices(filteredServices); } @computed get filtered() { -- cgit v1.2.3-70-g09d2 From 6fb07bcb716af76ec2e96345f37624d12d0d1af0 Mon Sep 17 00:00:00 2001 From: Dominik Guzei Date: Tue, 12 Mar 2019 21:36:10 +0100 Subject: implements basic release announcement feature --- .eslintrc | 2 +- package-lock.json | 5 ++ package.json | 1 + src/actions/index.js | 6 +- src/actions/service.js | 1 + src/components/layout/AppLayout.js | 4 + src/config.js | 1 + src/containers/layout/AppLayoutContainer.js | 2 + src/features/announcements/Component.js | 77 ++++++++++++++++++ src/features/announcements/actions.js | 8 ++ src/features/announcements/api.js | 19 +++++ src/features/announcements/index.js | 37 +++++++++ src/features/announcements/state.js | 17 ++++ src/features/announcements/store.js | 95 ++++++++++++++++++++++ src/i18n/locales/defaultMessages.json | 95 ++++++++++++++-------- src/i18n/locales/en-US.json | 2 + .../messages/src/components/layout/AppLayout.json | 24 +++--- .../src/features/announcements/Component.json | 15 ++++ src/i18n/messages/src/lib/Menu.json | 53 +++++++----- src/lib/Menu.js | 10 +++ src/stores/FeaturesStore.js | 2 + src/stores/ServicesStore.js | 6 ++ 22 files changed, 416 insertions(+), 66 deletions(-) create mode 100644 src/features/announcements/Component.js create mode 100644 src/features/announcements/actions.js create mode 100644 src/features/announcements/api.js create mode 100644 src/features/announcements/index.js create mode 100644 src/features/announcements/state.js create mode 100644 src/features/announcements/store.js create mode 100644 src/i18n/messages/src/features/announcements/Component.json (limited to 'src/actions/index.js') diff --git a/.eslintrc b/.eslintrc index 743946d35..a4ffd505c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -14,7 +14,7 @@ "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], - "react/forbid-prop-types": 1, + "react/forbid-prop-types": 0, "react/destructuring-assignment": 1, "prefer-destructuring": 1, "no-underscore-dangle": 0, diff --git a/package-lock.json b/package-lock.json index 8499abda9..bc333ae50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12094,6 +12094,11 @@ "object-visit": "^1.0.0" } }, + "marked": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.6.1.tgz", + "integrity": "sha512-+H0L3ibcWhAZE02SKMqmvYsErLo4EAVJxu5h3bHBBDvvjeWXtl92rGUSBYHL2++5Y+RSNgl8dYOAXcYe7lp1fA==" + }, "matchdep": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", diff --git a/package.json b/package.json index 14e0df7ca..4ddc83777 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "hex-to-rgba": "1.0.2", "jsonwebtoken": "^7.4.1", "lodash": "^4.17.4", + "marked": "0.6.1", "mdi": "^1.9.33", "mime-types": "2.1.21", "mobx": "5.7.0", diff --git a/src/actions/index.js b/src/actions/index.js index 59acabb0b..dc1d3b6b2 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -11,6 +11,7 @@ import payment from './payment'; import news from './news'; import settings from './settings'; import requests from './requests'; +import announcements from '../features/announcements/actions'; const actions = Object.assign({}, { service, @@ -25,4 +26,7 @@ const actions = Object.assign({}, { requests, }); -export default defineActions(actions, PropTypes.checkPropTypes); +export default Object.assign( + defineActions(actions, PropTypes.checkPropTypes), + { announcements }, +); diff --git a/src/actions/service.js b/src/actions/service.js index ceaabc31e..ce62560a9 100644 --- a/src/actions/service.js +++ b/src/actions/service.js @@ -5,6 +5,7 @@ export default { setActive: { serviceId: PropTypes.string.isRequired, }, + blurActive: {}, setActiveNext: {}, setActivePrev: {}, showAddServiceInterface: { diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js index 593149e72..2bda91f73 100644 --- a/src/components/layout/AppLayout.js +++ b/src/components/layout/AppLayout.js @@ -13,6 +13,7 @@ import ErrorBoundary from '../util/ErrorBoundary'; // import globalMessages from '../../i18n/globalMessages'; import { isWindows } from '../../environment'; +import AnnouncementScreen from '../../features/announcements/Component'; function createMarkup(HTMLString) { return { __html: HTMLString }; @@ -64,6 +65,7 @@ export default @observer class AppLayout extends Component { areRequiredRequestsLoading: PropTypes.bool.isRequired, darkMode: PropTypes.bool.isRequired, isDelayAppScreenVisible: PropTypes.bool.isRequired, + isAnnouncementVisible: PropTypes.bool.isRequired, }; static defaultProps = { @@ -93,6 +95,7 @@ export default @observer class AppLayout extends Component { areRequiredRequestsLoading, darkMode, isDelayAppScreenVisible, + isAnnouncementVisible, } = this.props; const { intl } = this.context; @@ -166,6 +169,7 @@ export default @observer class AppLayout extends Component { {isDelayAppScreenVisible && ()} + {isAnnouncementVisible && ()} {services} diff --git a/src/config.js b/src/config.js index 479572edb..47d22ca7d 100644 --- a/src/config.js +++ b/src/config.js @@ -41,6 +41,7 @@ export const DEFAULT_FEATURES_CONFIG = { }, isServiceProxyEnabled: false, isServiceProxyPremiumFeature: true, + isAnnouncementsEnabled: true, }; export const DEFAULT_WINDOW_OPTIONS = { diff --git a/src/containers/layout/AppLayoutContainer.js b/src/containers/layout/AppLayoutContainer.js index 5a05ce431..f26e51517 100644 --- a/src/containers/layout/AppLayoutContainer.js +++ b/src/containers/layout/AppLayoutContainer.js @@ -20,6 +20,7 @@ import Services from '../../components/services/content/Services'; import AppLoader from '../../components/ui/AppLoader'; import { state as delayAppState } from '../../features/delayApp'; +import { announcementsState } from '../../features/announcements/state'; export default @inject('stores', 'actions') @observer class AppLayoutContainer extends Component { static defaultProps = { @@ -134,6 +135,7 @@ export default @inject('stores', 'actions') @observer class AppLayoutContainer e areRequiredRequestsLoading={requests.areRequiredRequestsLoading} darkMode={settings.all.app.darkMode} isDelayAppScreenVisible={delayAppState.isDelayAppScreenVisible} + isAnnouncementVisible={announcementsState.isAnnouncementVisible} > {React.Children.count(children) > 0 ? children : null} diff --git a/src/features/announcements/Component.js b/src/features/announcements/Component.js new file mode 100644 index 000000000..5d95f5d84 --- /dev/null +++ b/src/features/announcements/Component.js @@ -0,0 +1,77 @@ +import React, { Component } from 'react'; +import marked from 'marked'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; +import injectSheet from 'react-jss'; +import { themeSidebarWidth } from '@meetfranz/theme/lib/themes/legacy'; +import state from './state'; + +const messages = defineMessages({ + headline: { + id: 'feature.announcements.headline', + defaultMessage: '!!!What\'s new in Franz {version}?', + }, +}); + +const styles = theme => ({ + container: { + background: theme.colorBackground, + position: 'absolute', + top: 0, + zIndex: 140, + width: `calc(100% - ${themeSidebarWidth})`, + display: 'flex', + 'flex-direction': 'column', + 'align-items': 'center', + 'justify-content': 'center', + }, + headline: { + color: theme.colorHeadline, + margin: [25, 0, 40], + 'max-width': 500, + 'text-align': 'center', + 'line-height': '1.3em', + }, + body: { + '& h3': { + fontSize: '24px', + margin: '1.5em 0 1em 0', + }, + '& li': { + marginBottom: '1em', + }, + }, +}); + + +@inject('actions') @injectSheet(styles) @observer +class AnnouncementScreen extends Component { + static propTypes = { + classes: PropTypes.object.isRequired, + }; + + static contextTypes = { + intl: intlShape, + }; + + render() { + const { classes } = this.props; + const { intl } = this.context; + return ( +
+

+ {intl.formatMessage(messages.headline, { version: state.currentVersion })} +

+
+
+ ); + } +} + +export default AnnouncementScreen; diff --git a/src/features/announcements/actions.js b/src/features/announcements/actions.js new file mode 100644 index 000000000..68b262ded --- /dev/null +++ b/src/features/announcements/actions.js @@ -0,0 +1,8 @@ +import PropTypes from 'prop-types'; +import { createActionsFromDefinitions } from '../../actions/lib/actions'; + +export const announcementActions = createActionsFromDefinitions({ + show: {}, +}, PropTypes.checkPropTypes); + +export default announcementActions; diff --git a/src/features/announcements/api.js b/src/features/announcements/api.js new file mode 100644 index 000000000..ec16066a6 --- /dev/null +++ b/src/features/announcements/api.js @@ -0,0 +1,19 @@ +import { remote } from 'electron'; + +const debug = require('debug')('Franz:feature:announcements:api'); + +export default { + async getCurrentVersion() { + debug('getting current version of electron app'); + return Promise.resolve(remote.app.getVersion()); + }, + + async getAnnouncementForVersion(version) { + debug('fetching release announcement from Github'); + const url = `https://api.github.com/repos/meetfranz/franz/releases/tags/v${version}`; + const request = await window.fetch(url, { method: 'GET' }); + if (!request.ok) throw request; + const data = await request.json(); + return data.body; + }, +}; diff --git a/src/features/announcements/index.js b/src/features/announcements/index.js new file mode 100644 index 000000000..5ea74e0af --- /dev/null +++ b/src/features/announcements/index.js @@ -0,0 +1,37 @@ +import { reaction, runInAction } from 'mobx'; +import { AnnouncementsStore } from './store'; +import api from './api'; +import state, { resetState } from './state'; + +const debug = require('debug')('Franz:feature:announcements'); + +let store = null; + +export default function initAnnouncements(stores, actions) { + // const { features } = stores; + + // Toggle workspace feature + reaction( + () => ( + true + // features.features.isAnnouncementsEnabled + ), + (isEnabled) => { + if (isEnabled) { + debug('Initializing `announcements` feature'); + store = new AnnouncementsStore(stores, api, actions, state); + store.initialize(); + runInAction(() => { state.isFeatureActive = true; }); + } else if (store) { + debug('Disabling `announcements` feature'); + runInAction(() => { state.isFeatureActive = false; }); + store.teardown(); + store = null; + resetState(); // Reset state to default + } + }, + { + fireImmediately: true, + }, + ); +} diff --git a/src/features/announcements/state.js b/src/features/announcements/state.js new file mode 100644 index 000000000..81b632253 --- /dev/null +++ b/src/features/announcements/state.js @@ -0,0 +1,17 @@ +import { observable } from 'mobx'; + +const defaultState = { + announcement: null, + currentVersion: null, + lastUsedVersion: null, + isAnnouncementVisible: false, + isFeatureActive: false, +}; + +export const announcementsState = observable(defaultState); + +export function resetState() { + Object.assign(announcementsState, defaultState); +} + +export default announcementsState; diff --git a/src/features/announcements/store.js b/src/features/announcements/store.js new file mode 100644 index 000000000..004a44062 --- /dev/null +++ b/src/features/announcements/store.js @@ -0,0 +1,95 @@ +import { action, observable, reaction } from 'mobx'; +import semver from 'semver'; + +import Request from '../../stores/lib/Request'; +import Store from '../../stores/lib/Store'; + +const debug = require('debug')('Franz:feature:announcements:store'); + +export class AnnouncementsStore extends Store { + @observable getCurrentVersion = new Request(this.api, 'getCurrentVersion'); + + @observable getAnnouncement = new Request(this.api, 'getAnnouncementForVersion'); + + constructor(stores, api, actions, state) { + super(stores, api, actions); + this.state = state; + } + + async setup() { + await this.fetchLastUsedVersion(); + await this.fetchCurrentVersion(); + await this.fetchReleaseAnnouncement(); + this.showAnnouncementIfNotSeenYet(); + + this.actions.announcements.show.listen(this._showAnnouncement.bind(this)); + } + + // ====== PUBLIC ====== + + async fetchLastUsedVersion() { + debug('getting last used version from local storage'); + const lastUsedVersion = window.localStorage.getItem('lastUsedVersion'); + this._setLastUsedVersion(lastUsedVersion == null ? '0.0.0' : lastUsedVersion); + } + + async fetchCurrentVersion() { + debug('getting current version from api'); + const version = await this.getCurrentVersion.execute(); + this._setCurrentVersion(version); + } + + async fetchReleaseAnnouncement() { + debug('getting release announcement from api'); + try { + const announcement = await this.getAnnouncement.execute(this.state.currentVersion); + this._setAnnouncement(announcement); + } catch (error) { + this._setAnnouncement(null); + } + } + + showAnnouncementIfNotSeenYet() { + const { announcement, currentVersion, lastUsedVersion } = this.state; + if (announcement && semver.gt(currentVersion, lastUsedVersion)) { + debug(`${currentVersion} < ${lastUsedVersion}: announcement is shown`); + this._showAnnouncement(); + } else { + debug(`${currentVersion} >= ${lastUsedVersion}: announcement is hidden`); + this._hideAnnouncement(); + } + } + + // ====== PRIVATE ====== + + @action _setCurrentVersion(version) { + debug(`setting current version to ${version}`); + this.state.currentVersion = version; + } + + @action _setLastUsedVersion(version) { + debug(`setting last used version to ${version}`); + this.state.lastUsedVersion = version; + } + + @action _setAnnouncement(announcement) { + debug(`setting announcement to ${announcement}`); + this.state.announcement = announcement; + } + + @action _showAnnouncement() { + this.state.isAnnouncementVisible = true; + this.actions.service.blurActive(); + const dispose = reaction( + () => this.stores.services.active, + () => { + this._hideAnnouncement(); + dispose(); + }, + ); + } + + @action _hideAnnouncement() { + this.state.isAnnouncementVisible = false; + } +} diff --git a/src/i18n/locales/defaultMessages.json b/src/i18n/locales/defaultMessages.json index 0641c510c..fcd24c7ef 100644 --- a/src/i18n/locales/defaultMessages.json +++ b/src/i18n/locales/defaultMessages.json @@ -625,78 +625,78 @@ "defaultMessage": "!!!Your services have been updated.", "end": { "column": 3, - "line": 25 + "line": 26 }, "file": "src/components/layout/AppLayout.js", "id": "infobar.servicesUpdated", "start": { "column": 19, - "line": 22 + "line": 23 } }, { "defaultMessage": "!!!A new update for Franz is available.", "end": { "column": 3, - "line": 29 + "line": 30 }, "file": "src/components/layout/AppLayout.js", "id": "infobar.updateAvailable", "start": { "column": 19, - "line": 26 + "line": 27 } }, { "defaultMessage": "!!!Reload services", "end": { "column": 3, - "line": 33 + "line": 34 }, "file": "src/components/layout/AppLayout.js", "id": "infobar.buttonReloadServices", "start": { "column": 24, - "line": 30 + "line": 31 } }, { "defaultMessage": "!!!Changelog", "end": { "column": 3, - "line": 37 + "line": 38 }, "file": "src/components/layout/AppLayout.js", "id": "infobar.buttonChangelog", "start": { "column": 13, - "line": 34 + "line": 35 } }, { "defaultMessage": "!!!Restart & install update", "end": { "column": 3, - "line": 41 + "line": 42 }, "file": "src/components/layout/AppLayout.js", "id": "infobar.buttonInstallUpdate", "start": { "column": 23, - "line": 38 + "line": 39 } }, { "defaultMessage": "!!!Could not load services and user information", "end": { "column": 3, - "line": 45 + "line": 46 }, "file": "src/components/layout/AppLayout.js", "id": "infobar.requiredRequestsFailed", "start": { "column": 26, - "line": 42 + "line": 43 } } ], @@ -3022,6 +3022,24 @@ ], "path": "src/containers/settings/EditUserScreen.json" }, + { + "descriptors": [ + { + "defaultMessage": "!!!What's new in Franz {version}?", + "end": { + "column": 3, + "line": 14 + }, + "file": "src/features/announcements/Component.js", + "id": "feature.announcements.headline", + "start": { + "column": 12, + "line": 11 + } + } + ], + "path": "src/features/announcements/Component.json" + }, { "descriptors": [ { @@ -3799,133 +3817,146 @@ } }, { - "defaultMessage": "!!!Settings", + "defaultMessage": "!!!What's new in Franz?", "end": { "column": 3, "line": 161 }, "file": "src/lib/Menu.js", + "id": "menu.app.announcement", + "start": { + "column": 16, + "line": 158 + } + }, + { + "defaultMessage": "!!!Settings", + "end": { + "column": 3, + "line": 165 + }, + "file": "src/lib/Menu.js", "id": "menu.app.settings", "start": { "column": 12, - "line": 158 + "line": 162 } }, { "defaultMessage": "!!!Hide", "end": { "column": 3, - "line": 165 + "line": 169 }, "file": "src/lib/Menu.js", "id": "menu.app.hide", "start": { "column": 8, - "line": 162 + "line": 166 } }, { "defaultMessage": "!!!Hide Others", "end": { "column": 3, - "line": 169 + "line": 173 }, "file": "src/lib/Menu.js", "id": "menu.app.hideOthers", "start": { "column": 14, - "line": 166 + "line": 170 } }, { "defaultMessage": "!!!Unhide", "end": { "column": 3, - "line": 173 + "line": 177 }, "file": "src/lib/Menu.js", "id": "menu.app.unhide", "start": { "column": 10, - "line": 170 + "line": 174 } }, { "defaultMessage": "!!!Quit", "end": { "column": 3, - "line": 177 + "line": 181 }, "file": "src/lib/Menu.js", "id": "menu.app.quit", "start": { "column": 8, - "line": 174 + "line": 178 } }, { "defaultMessage": "!!!Add New Service...", "end": { "column": 3, - "line": 181 + "line": 185 }, "file": "src/lib/Menu.js", "id": "menu.services.addNewService", "start": { "column": 17, - "line": 178 + "line": 182 } }, { "defaultMessage": "!!!Activate next service...", "end": { "column": 3, - "line": 185 + "line": 189 }, "file": "src/lib/Menu.js", "id": "menu.services.setNextServiceActive", "start": { "column": 23, - "line": 182 + "line": 186 } }, { "defaultMessage": "!!!Activate previous service...", "end": { "column": 3, - "line": 189 + "line": 193 }, "file": "src/lib/Menu.js", "id": "menu.services.activatePreviousService", "start": { "column": 27, - "line": 186 + "line": 190 } }, { "defaultMessage": "!!!Disable notifications & audio", "end": { "column": 3, - "line": 193 + "line": 197 }, "file": "src/lib/Menu.js", "id": "sidebar.muteApp", "start": { "column": 11, - "line": 190 + "line": 194 } }, { "defaultMessage": "!!!Enable notifications & audio", "end": { "column": 3, - "line": 197 + "line": 201 }, "file": "src/lib/Menu.js", "id": "sidebar.unmuteApp", "start": { "column": 13, - "line": 194 + "line": 198 } } ], diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 7543d38bd..573231c45 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -1,6 +1,7 @@ { "app.errorHandler.action": "Reload", "app.errorHandler.headline": "Something went wrong", + "feature.announcements.headline": "What's new in Franz {version}?", "feature.delayApp.action": "Get a Franz Supporter License", "feature.delayApp.headline": "Please purchase a Franz Supporter License to skip waiting", "feature.delayApp.text": "Franz will continue in {seconds} seconds.", @@ -43,6 +44,7 @@ "login.submit.label": "Sign in", "login.tokenExpired": "Your session expired, please login again.", "menu.app.about": "About Franz", + "menu.app.announcement": "What's new in Franz?", "menu.app.hide": "Hide", "menu.app.hideOthers": "Hide Others", "menu.app.quit": "Quit", diff --git a/src/i18n/messages/src/components/layout/AppLayout.json b/src/i18n/messages/src/components/layout/AppLayout.json index 07603d062..384d4b441 100644 --- a/src/i18n/messages/src/components/layout/AppLayout.json +++ b/src/i18n/messages/src/components/layout/AppLayout.json @@ -4,11 +4,11 @@ "defaultMessage": "!!!Your services have been updated.", "file": "src/components/layout/AppLayout.js", "start": { - "line": 22, + "line": 23, "column": 19 }, "end": { - "line": 25, + "line": 26, "column": 3 } }, @@ -17,11 +17,11 @@ "defaultMessage": "!!!A new update for Franz is available.", "file": "src/components/layout/AppLayout.js", "start": { - "line": 26, + "line": 27, "column": 19 }, "end": { - "line": 29, + "line": 30, "column": 3 } }, @@ -30,11 +30,11 @@ "defaultMessage": "!!!Reload services", "file": "src/components/layout/AppLayout.js", "start": { - "line": 30, + "line": 31, "column": 24 }, "end": { - "line": 33, + "line": 34, "column": 3 } }, @@ -43,11 +43,11 @@ "defaultMessage": "!!!Changelog", "file": "src/components/layout/AppLayout.js", "start": { - "line": 34, + "line": 35, "column": 13 }, "end": { - "line": 37, + "line": 38, "column": 3 } }, @@ -56,11 +56,11 @@ "defaultMessage": "!!!Restart & install update", "file": "src/components/layout/AppLayout.js", "start": { - "line": 38, + "line": 39, "column": 23 }, "end": { - "line": 41, + "line": 42, "column": 3 } }, @@ -69,11 +69,11 @@ "defaultMessage": "!!!Could not load services and user information", "file": "src/components/layout/AppLayout.js", "start": { - "line": 42, + "line": 43, "column": 26 }, "end": { - "line": 45, + "line": 46, "column": 3 } } diff --git a/src/i18n/messages/src/features/announcements/Component.json b/src/i18n/messages/src/features/announcements/Component.json new file mode 100644 index 000000000..18e1b84c5 --- /dev/null +++ b/src/i18n/messages/src/features/announcements/Component.json @@ -0,0 +1,15 @@ +[ + { + "id": "feature.announcements.headline", + "defaultMessage": "!!!What's new in Franz {version}?", + "file": "src/features/announcements/Component.js", + "start": { + "line": 11, + "column": 12 + }, + "end": { + "line": 14, + "column": 3 + } + } +] \ No newline at end of file diff --git a/src/i18n/messages/src/lib/Menu.json b/src/i18n/messages/src/lib/Menu.json index 9314f5cce..0db994871 100644 --- a/src/i18n/messages/src/lib/Menu.json +++ b/src/i18n/messages/src/lib/Menu.json @@ -480,16 +480,29 @@ "column": 3 } }, + { + "id": "menu.app.announcement", + "defaultMessage": "!!!What's new in Franz?", + "file": "src/lib/Menu.js", + "start": { + "line": 158, + "column": 16 + }, + "end": { + "line": 161, + "column": 3 + } + }, { "id": "menu.app.settings", "defaultMessage": "!!!Settings", "file": "src/lib/Menu.js", "start": { - "line": 158, + "line": 162, "column": 12 }, "end": { - "line": 161, + "line": 165, "column": 3 } }, @@ -498,11 +511,11 @@ "defaultMessage": "!!!Hide", "file": "src/lib/Menu.js", "start": { - "line": 162, + "line": 166, "column": 8 }, "end": { - "line": 165, + "line": 169, "column": 3 } }, @@ -511,11 +524,11 @@ "defaultMessage": "!!!Hide Others", "file": "src/lib/Menu.js", "start": { - "line": 166, + "line": 170, "column": 14 }, "end": { - "line": 169, + "line": 173, "column": 3 } }, @@ -524,11 +537,11 @@ "defaultMessage": "!!!Unhide", "file": "src/lib/Menu.js", "start": { - "line": 170, + "line": 174, "column": 10 }, "end": { - "line": 173, + "line": 177, "column": 3 } }, @@ -537,11 +550,11 @@ "defaultMessage": "!!!Quit", "file": "src/lib/Menu.js", "start": { - "line": 174, + "line": 178, "column": 8 }, "end": { - "line": 177, + "line": 181, "column": 3 } }, @@ -550,11 +563,11 @@ "defaultMessage": "!!!Add New Service...", "file": "src/lib/Menu.js", "start": { - "line": 178, + "line": 182, "column": 17 }, "end": { - "line": 181, + "line": 185, "column": 3 } }, @@ -563,11 +576,11 @@ "defaultMessage": "!!!Activate next service...", "file": "src/lib/Menu.js", "start": { - "line": 182, + "line": 186, "column": 23 }, "end": { - "line": 185, + "line": 189, "column": 3 } }, @@ -576,11 +589,11 @@ "defaultMessage": "!!!Activate previous service...", "file": "src/lib/Menu.js", "start": { - "line": 186, + "line": 190, "column": 27 }, "end": { - "line": 189, + "line": 193, "column": 3 } }, @@ -589,11 +602,11 @@ "defaultMessage": "!!!Disable notifications & audio", "file": "src/lib/Menu.js", "start": { - "line": 190, + "line": 194, "column": 11 }, "end": { - "line": 193, + "line": 197, "column": 3 } }, @@ -602,11 +615,11 @@ "defaultMessage": "!!!Enable notifications & audio", "file": "src/lib/Menu.js", "start": { - "line": 194, + "line": 198, "column": 13 }, "end": { - "line": 197, + "line": 201, "column": 3 } } diff --git a/src/lib/Menu.js b/src/lib/Menu.js index 7a60c448f..70f3b2877 100644 --- a/src/lib/Menu.js +++ b/src/lib/Menu.js @@ -155,6 +155,10 @@ const menuItems = defineMessages({ id: 'menu.app.about', defaultMessage: '!!!About Franz', }, + announcement: { + id: 'menu.app.announcement', + defaultMessage: '!!!What\'s new in Franz?', + }, settings: { id: 'menu.app.settings', defaultMessage: '!!!Settings', @@ -589,6 +593,12 @@ export default class FranzMenu { label: intl.formatMessage(menuItems.about), role: 'about', }, + { + label: intl.formatMessage(menuItems.announcement), + click: () => { + this.actions.announcements.show(); + }, + }, { type: 'separator', }, diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js index d2842083c..1c9044b07 100644 --- a/src/stores/FeaturesStore.js +++ b/src/stores/FeaturesStore.js @@ -8,6 +8,7 @@ import spellchecker from '../features/spellchecker'; import serviceProxy from '../features/serviceProxy'; import basicAuth from '../features/basicAuth'; import shareFranz from '../features/shareFranz'; +import announcements from '../features/announcements'; import { DEFAULT_FEATURES_CONFIG } from '../config'; @@ -58,5 +59,6 @@ export default class FeaturesStore extends Store { serviceProxy(this.stores, this.actions); basicAuth(this.stores, this.actions); shareFranz(this.stores, this.actions); + announcements(this.stores, this.actions); } } diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js index 69e616f0c..88b0331bf 100644 --- a/src/stores/ServicesStore.js +++ b/src/stores/ServicesStore.js @@ -35,6 +35,7 @@ export default class ServicesStore extends Store { // Register action handlers this.actions.service.setActive.listen(this._setActive.bind(this)); + this.actions.service.blurActive.listen(this._blurActive.bind(this)); this.actions.service.setActiveNext.listen(this._setActiveNext.bind(this)); this.actions.service.setActivePrev.listen(this._setActivePrev.bind(this)); this.actions.service.showAddServiceInterface.listen(this._showAddServiceInterface.bind(this)); @@ -298,6 +299,11 @@ export default class ServicesStore extends Store { this._focusActiveService(); } + @action _blurActive() { + if (!this.active) return; + this.active.isActive = false; + } + @action _setActiveNext() { const nextIndex = this._wrapIndex(this.allDisplayed.findIndex(service => service.isActive), 1, this.allDisplayed.length); -- cgit v1.2.3-70-g09d2