From 1bae1dfcbc4a5f590c51103635006198ae6a91d6 Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Tue, 30 Apr 2019 15:23:38 +0200 Subject: Enforce service limit --- src/stores/FeaturesStore.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/stores/FeaturesStore.js') diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js index e7832088b..1ac05d3b9 100644 --- a/src/stores/FeaturesStore.js +++ b/src/stores/FeaturesStore.js @@ -16,6 +16,7 @@ import workspaces from '../features/workspaces'; import shareFranz from '../features/shareFranz'; import announcements from '../features/announcements'; import settingsWS from '../features/settingsWS'; +import serviceLimit from '../features/serviceLimit'; import { DEFAULT_FEATURES_CONFIG } from '../config'; @@ -75,5 +76,6 @@ export default class FeaturesStore extends Store { shareFranz(this.stores, this.actions); announcements(this.stores, this.actions); settingsWS(this.stores, this.actions); + serviceLimit(this.stores, this.actions); } } -- cgit v1.2.3-70-g09d2 From fbff4ed90b0137088b1bb92e95f32fc1bfa7bb3e Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Thu, 2 May 2019 11:27:23 +0200 Subject: Add custom recipe limitation --- packages/ui/src/badge/ProBadge.tsx | 5 +- src/components/settings/recipes/RecipeItem.js | 2 +- .../settings/recipes/RecipesDashboard.js | 194 +++++++++++++++++---- src/config.js | 1 + src/containers/settings/RecipesScreen.js | 40 ++++- src/features/communityRecipes/index.js | 28 +++ src/features/communityRecipes/store.js | 31 ++++ src/i18n/locales/defaultMessages.json | 178 +++++++++++++++++-- src/i18n/locales/en-US.json | 8 +- .../settings/recipes/RecipesDashboard.json | 116 ++++++++++-- src/stores/FeaturesStore.js | 2 + src/stores/index.js | 2 + src/styles/recipes.scss | 2 +- 13 files changed, 526 insertions(+), 83 deletions(-) create mode 100644 src/features/communityRecipes/index.js create mode 100644 src/features/communityRecipes/store.js (limited to 'src/stores/FeaturesStore.js') diff --git a/packages/ui/src/badge/ProBadge.tsx b/packages/ui/src/badge/ProBadge.tsx index 612e23210..2dad7ef49 100644 --- a/packages/ui/src/badge/ProBadge.tsx +++ b/packages/ui/src/badge/ProBadge.tsx @@ -3,13 +3,14 @@ import classnames from 'classnames'; import React, { Component } from 'react'; import injectStyle from 'react-jss'; -import { Icon, Badge } from '../'; +import { Badge, Icon } from '../'; import { IWithStyle } from '../typings/generic'; interface IProps extends IWithStyle { badgeClasses?: string; iconClasses?: string; inverted?: boolean; + className?: string; } const styles = (theme: Theme) => ({ @@ -37,6 +38,7 @@ class ProBadgeComponent extends Component { badgeClasses, iconClasses, inverted, + className, } = this.props; return ( @@ -46,6 +48,7 @@ class ProBadgeComponent extends Component { classes.badge, inverted && classes.invertedBadge, badgeClasses, + className, ])} > - {recipe.local && ( + {recipe.isDevRecipe && ( dev )} div': { + fontFamily: 'SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace', + }, + }, + actionContainer: { + '& button': { + margin: [0, 10], + }, + }, + devRecipeList: { + marginTop: 20, + height: 'auto', + }, + proBadge: { + marginLeft: '10px !important', + }, +}; + +export default @injectSheet(styles) @observer class RecipesDashboard extends Component { static propTypes = { recipes: MobxPropTypes.arrayOrObservableArray.isRequired, isLoading: PropTypes.bool.isRequired, @@ -55,12 +111,19 @@ export default @observer class RecipesDashboard extends Component { searchRecipes: PropTypes.func.isRequired, resetSearch: PropTypes.func.isRequired, serviceStatus: MobxPropTypes.arrayOrObservableArray.isRequired, - devRecipesCount: PropTypes.number.isRequired, searchNeedle: PropTypes.string, + recipeFilter: PropTypes.string, + recipeDirectory: PropTypes.string.isRequired, + openRecipeDirectory: PropTypes.func.isRequired, + openDevDocs: PropTypes.func.isRequired, + classes: PropTypes.object.isRequired, + isCommunityRecipesPremiumFeature: PropTypes.bool.isRequired, + isUserPremiumUser: PropTypes.bool.isRequired, }; static defaultProps = { searchNeedle: '', + recipeFilter: 'all', } static contextTypes = { @@ -76,11 +139,21 @@ export default @observer class RecipesDashboard extends Component { searchRecipes, resetSearch, serviceStatus, - devRecipesCount, searchNeedle, + recipeFilter, + recipeDirectory, + openRecipeDirectory, + openDevDocs, + classes, + isCommunityRecipesPremiumFeature, + isUserPremiumUser, } = this.props; const { intl } = this.context; + + const communityRecipes = recipes.filter(r => !r.isDevRecipe); + const devRecipes = recipes.filter(r => r.isDevRecipe); + return (
@@ -122,20 +195,14 @@ export default @observer class RecipesDashboard extends Component { > {intl.formatMessage(messages.allRecipes)} - {devRecipesCount > 0 && ( - resetSearch()} - > - {intl.formatMessage(messages.devRecipes)} - {' '} -( - {devRecipesCount} -) - - )} + resetSearch()} + > + {intl.formatMessage(messages.customRecipes)} + {intl.formatMessage(messages.missingService)} {' '} @@ -146,23 +213,78 @@ export default @observer class RecipesDashboard extends Component { {isLoading ? ( ) : ( -
- {hasLoadedRecipes && recipes.length === 0 && ( -

- - - - {intl.formatMessage(messages.nothingFound)} -

+ <> + {recipeFilter === 'dev' && ( + <> +

+ {intl.formatMessage(messages.headlineCustomRecipes)} + {!isUserPremiumUser && ( + + )} +

+
+

+ {intl.formatMessage(messages.customRecipeIntro)} +

+ +
+
+
+ + )} + 0) && isCommunityRecipesPremiumFeature} + > + {recipeFilter === 'dev' && communityRecipes.length > 0 && ( +

{intl.formatMessage(messages.headlineCommunityRecipes)}

+ )} +
+ {hasLoadedRecipes && recipes.length === 0 && recipeFilter !== 'dev'( +

+ + + + {intl.formatMessage(messages.nothingFound)} +

, + )} + {communityRecipes.map(recipe => ( + showAddServiceInterface({ recipeId: recipe.id })} + /> + ))} +
+
+ {recipeFilter === 'dev' && devRecipes.length > 0 && ( +
+

{intl.formatMessage(messages.headlineDevRecipes)}

+
+ {devRecipes.map(recipe => ( + showAddServiceInterface({ recipeId: recipe.id })} + /> + ))} +
+
)} - {recipes.map(recipe => ( - showAddServiceInterface({ recipeId: recipe.id })} - /> - ))} -
+ )}
diff --git a/src/config.js b/src/config.js index 5bc318545..544f94fde 100644 --- a/src/config.js +++ b/src/config.js @@ -67,6 +67,7 @@ export const DEFAULT_WINDOW_OPTIONS = { export const FRANZ_SERVICE_REQUEST = 'https://bit.ly/franz-plugin-docs'; export const FRANZ_TRANSLATION = 'https://bit.ly/franz-translate'; +export const FRANZ_DEV_DOCS = 'http://bit.ly/franz-dev-hub'; export const FILE_SYSTEM_SETTINGS_TYPES = [ 'app', diff --git a/src/containers/settings/RecipesScreen.js b/src/containers/settings/RecipesScreen.js index eda5ae54c..57e879f42 100644 --- a/src/containers/settings/RecipesScreen.js +++ b/src/containers/settings/RecipesScreen.js @@ -1,7 +1,9 @@ +import { remote, shell } from 'electron'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { autorun } from 'mobx'; import { inject, observer } from 'mobx-react'; +import path from 'path'; import RecipePreviewsStore from '../../stores/RecipePreviewsStore'; import RecipeStore from '../../stores/RecipesStore'; @@ -10,6 +12,11 @@ import UserStore from '../../stores/UserStore'; import RecipesDashboard from '../../components/settings/recipes/RecipesDashboard'; import ErrorBoundary from '../../components/util/ErrorBoundary'; +import { FRANZ_DEV_DOCS } from '../../config'; +import { gaEvent } from '../../lib/analytics'; +import { communityRecipesStore } from '../../features/communityRecipes'; + +const { app } = remote; export default @inject('stores', 'actions') @observer class RecipesScreen extends Component { static propTypes = { @@ -67,9 +74,16 @@ export default @inject('stores', 'actions') @observer class RecipesScreen extend render() { const { - recipePreviews, recipes, services, user, + recipePreviews, + recipes, + services, + user, } = this.props.stores; - const { showAddServiceInterface } = this.props.actions.service; + + const { + app: appActions, + service: serviceActions, + } = this.props.actions; const { filter } = this.props.params; let recipeFilter; @@ -77,7 +91,7 @@ export default @inject('stores', 'actions') @observer class RecipesScreen extend if (filter === 'all') { recipeFilter = recipePreviews.all; } else if (filter === 'dev') { - recipeFilter = recipePreviews.dev; + recipeFilter = communityRecipesStore.communityRecipes; } else { recipeFilter = recipePreviews.featured; } @@ -89,6 +103,8 @@ export default @inject('stores', 'actions') @observer class RecipesScreen extend || recipes.installRecipeRequest.isExecuting || recipePreviews.searchRecipePreviewsRequest.isExecuting; + const recipeDirectory = path.join(app.getPath('userData'), 'recipes', 'dev'); + return ( this.searchRecipes(e)} resetSearch={() => this.resetSearch()} searchNeedle={this.state.needle} serviceStatus={services.actionStatus} - devRecipesCount={recipePreviews.dev.length} + recipeFilter={filter} + recipeDirectory={recipeDirectory} + openRecipeDirectory={() => { + shell.openItem(recipeDirectory); + gaEvent('Recipe', 'open-recipe-folder', 'Open Folder'); + }} + openDevDocs={() => { + appActions.openExternalUrl({ url: FRANZ_DEV_DOCS }); + gaEvent('Recipe', 'open-dev-docs', 'Developer Documentation'); + }} + isCommunityRecipesPremiumFeature={communityRecipesStore.isCommunityRecipesPremiumFeature} + isUserPremiumUser={user.isPremium} /> ); @@ -117,6 +144,9 @@ RecipesScreen.wrappedComponent.propTypes = { user: PropTypes.instanceOf(UserStore).isRequired, }).isRequired, actions: PropTypes.shape({ + app: PropTypes.shape({ + openExternalUrl: PropTypes.func.isRequired, + }).isRequired, service: PropTypes.shape({ showAddServiceInterface: PropTypes.func.isRequired, }).isRequired, diff --git a/src/features/communityRecipes/index.js b/src/features/communityRecipes/index.js new file mode 100644 index 000000000..78e87855e --- /dev/null +++ b/src/features/communityRecipes/index.js @@ -0,0 +1,28 @@ +import { reaction } from 'mobx'; +import { CommunityRecipesStore } from './store'; + +const debug = require('debug')('Franz:feature:communityRecipes'); + +export const DEFAULT_SERVICE_LIMIT = 3; + +export const communityRecipesStore = new CommunityRecipesStore(); + +export default function initCommunityRecipes(stores, actions) { + const { features } = stores; + + communityRecipesStore.start(stores, actions); + + // Toggle communityRecipe premium status + reaction( + () => ( + features.features.isCommunityRecipesPremiumFeature + ), + (isPremiumFeature) => { + debug('Community recipes is premium feature: ', isPremiumFeature); + communityRecipesStore.isCommunityRecipesPremiumFeature = isPremiumFeature; + }, + { + fireImmediately: true, + }, + ); +} diff --git a/src/features/communityRecipes/store.js b/src/features/communityRecipes/store.js new file mode 100644 index 000000000..165b753e8 --- /dev/null +++ b/src/features/communityRecipes/store.js @@ -0,0 +1,31 @@ +import { computed, observable } from 'mobx'; +import { FeatureStore } from '../utils/FeatureStore'; + +const debug = require('debug')('Franz:feature:communityRecipes:store'); + +export class CommunityRecipesStore extends FeatureStore { + @observable isCommunityRecipesPremiumFeature = false; + + start(stores, actions) { + debug('start'); + this.stores = stores; + this.actions = actions; + } + + stop() { + debug('stop'); + super.stop(); + } + + @computed get communityRecipes() { + if (!this.stores) return []; + + return this.stores.recipePreviews.dev.map((r) => { + r.isDevRecipe = !!r.author.find(a => a.email === this.stores.user.data.email); + + return r; + }); + } +} + +export default CommunityRecipesStore; diff --git a/src/i18n/locales/defaultMessages.json b/src/i18n/locales/defaultMessages.json index 632eb38fd..9be12d369 100644 --- a/src/i18n/locales/defaultMessages.json +++ b/src/i18n/locales/defaultMessages.json @@ -1411,104 +1411,182 @@ "defaultMessage": "!!!Available Services", "end": { "column": 3, - "line": 18 + "line": 22 }, "file": "src/components/settings/recipes/RecipesDashboard.js", "id": "settings.recipes.headline", "start": { "column": 12, - "line": 15 + "line": 19 } }, { "defaultMessage": "!!!Search service", "end": { "column": 3, - "line": 22 + "line": 26 }, "file": "src/components/settings/recipes/RecipesDashboard.js", "id": "settings.searchService", "start": { "column": 17, - "line": 19 + "line": 23 } }, { "defaultMessage": "!!!Most popular", "end": { "column": 3, - "line": 26 + "line": 30 }, "file": "src/components/settings/recipes/RecipesDashboard.js", "id": "settings.recipes.mostPopular", "start": { "column": 22, - "line": 23 + "line": 27 } }, { "defaultMessage": "!!!All services", "end": { "column": 3, - "line": 30 + "line": 34 }, "file": "src/components/settings/recipes/RecipesDashboard.js", "id": "settings.recipes.all", "start": { "column": 14, - "line": 27 + "line": 31 } }, { - "defaultMessage": "!!!Development", + "defaultMessage": "!!!Custom Services", "end": { "column": 3, - "line": 34 + "line": 38 }, "file": "src/components/settings/recipes/RecipesDashboard.js", - "id": "settings.recipes.dev", + "id": "settings.recipes.custom", "start": { - "column": 14, - "line": 31 + "column": 17, + "line": 35 } }, { "defaultMessage": "!!!Sorry, but no service matched your search term.", "end": { "column": 3, - "line": 38 + "line": 42 }, "file": "src/components/settings/recipes/RecipesDashboard.js", "id": "settings.recipes.nothingFound", "start": { "column": 16, - "line": 35 + "line": 39 } }, { "defaultMessage": "!!!Service successfully added", "end": { "column": 3, - "line": 42 + "line": 46 }, "file": "src/components/settings/recipes/RecipesDashboard.js", "id": "settings.recipes.servicesSuccessfulAddedInfo", "start": { "column": 31, - "line": 39 + "line": 43 } }, { "defaultMessage": "!!!Missing a service?", "end": { "column": 3, - "line": 46 + "line": 50 }, "file": "src/components/settings/recipes/RecipesDashboard.js", "id": "settings.recipes.missingService", "start": { "column": 18, - "line": 43 + "line": 47 + } + }, + { + "defaultMessage": "!!!To add a custom service, copy the recipe folder into:", + "end": { + "column": 3, + "line": 54 + }, + "file": "src/components/settings/recipes/RecipesDashboard.js", + "id": "settings.recipes.customService.intro", + "start": { + "column": 21, + "line": 51 + } + }, + { + "defaultMessage": "!!!Open directory", + "end": { + "column": 3, + "line": 58 + }, + "file": "src/components/settings/recipes/RecipesDashboard.js", + "id": "settings.recipes.customService.openFolder", + "start": { + "column": 14, + "line": 55 + } + }, + { + "defaultMessage": "!!!Developer Documentation", + "end": { + "column": 3, + "line": 62 + }, + "file": "src/components/settings/recipes/RecipesDashboard.js", + "id": "settings.recipes.customService.openDevDocs", + "start": { + "column": 15, + "line": 59 + } + }, + { + "defaultMessage": "!!!Custom Service Recipes", + "end": { + "column": 3, + "line": 66 + }, + "file": "src/components/settings/recipes/RecipesDashboard.js", + "id": "settings.recipes.customService.headline.customRecipes", + "start": { + "column": 25, + "line": 63 + } + }, + { + "defaultMessage": "!!!Community Services", + "end": { + "column": 3, + "line": 70 + }, + "file": "src/components/settings/recipes/RecipesDashboard.js", + "id": "settings.recipes.customService.headline.communityRecipes", + "start": { + "column": 28, + "line": 67 + } + }, + { + "defaultMessage": "!!!Your Development Service Recipes", + "end": { + "column": 3, + "line": 74 + }, + "file": "src/components/settings/recipes/RecipesDashboard.js", + "id": "settings.recipes.customService.headline.devRecipes", + "start": { + "column": 22, + "line": 71 } } ], @@ -3219,6 +3297,37 @@ ], "path": "src/features/announcements/components/AnnouncementScreen.json" }, + { + "descriptors": [ + { + "defaultMessage": "!!!You have added {amount} of {limit} services. Please upgrade your account to add more services.", + "end": { + "column": 3, + "line": 14 + }, + "file": "src/features/communityRecipes/components/LimitReachedInfobox.js", + "id": "feature.serviceLimit.limitReached", + "start": { + "column": 16, + "line": 11 + } + }, + { + "defaultMessage": "!!!Upgrade account", + "end": { + "column": 3, + "line": 18 + }, + "file": "src/features/communityRecipes/components/LimitReachedInfobox.js", + "id": "premiumFeature.button.upgradeAccount", + "start": { + "column": 10, + "line": 15 + } + } + ], + "path": "src/features/communityRecipes/components/LimitReachedInfobox.json" + }, { "descriptors": [ { @@ -3359,6 +3468,37 @@ ], "path": "src/features/shareFranz/Component.json" }, + { + "descriptors": [ + { + "defaultMessage": "!!!You have added {amount} of {limit} services. Please upgrade your account to add more services.", + "end": { + "column": 3, + "line": 14 + }, + "file": "src/features/unofficialRecipes/components/LimitReachedInfobox.js", + "id": "feature.serviceLimit.limitReached", + "start": { + "column": 16, + "line": 11 + } + }, + { + "defaultMessage": "!!!Upgrade account", + "end": { + "column": 3, + "line": 18 + }, + "file": "src/features/unofficialRecipes/components/LimitReachedInfobox.js", + "id": "premiumFeature.button.upgradeAccount", + "start": { + "column": 10, + "line": 15 + } + } + ], + "path": "src/features/unofficialRecipes/components/LimitReachedInfobox.json" + }, { "descriptors": [ { diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 6c2759dcc..8bffea63c 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -181,7 +181,13 @@ "settings.navigation.yourServices": "Your services", "settings.navigation.yourWorkspaces": "Your workspaces", "settings.recipes.all": "All services", - "settings.recipes.dev": "Development", + "settings.recipes.custom": "Custom Services", + "settings.recipes.customService.headline.communityRecipes": "Community Services", + "settings.recipes.customService.headline.customRecipes": "Custom Service Recipes", + "settings.recipes.customService.headline.devRecipes": "Your Development Service Recipes", + "settings.recipes.customService.intro": "To add a custom service, copy the service recipe to:", + "settings.recipes.customService.openDevDocs": "Developer Documentation", + "settings.recipes.customService.openFolder": "Open folder", "settings.recipes.headline": "Available services", "settings.recipes.missingService": "Missing a service?", "settings.recipes.mostPopular": "Most popular", diff --git a/src/i18n/messages/src/components/settings/recipes/RecipesDashboard.json b/src/i18n/messages/src/components/settings/recipes/RecipesDashboard.json index 7d9ed3283..26dcd3da5 100644 --- a/src/i18n/messages/src/components/settings/recipes/RecipesDashboard.json +++ b/src/i18n/messages/src/components/settings/recipes/RecipesDashboard.json @@ -4,11 +4,11 @@ "defaultMessage": "!!!Available Services", "file": "src/components/settings/recipes/RecipesDashboard.js", "start": { - "line": 15, + "line": 19, "column": 12 }, "end": { - "line": 18, + "line": 22, "column": 3 } }, @@ -17,11 +17,11 @@ "defaultMessage": "!!!Search service", "file": "src/components/settings/recipes/RecipesDashboard.js", "start": { - "line": 19, + "line": 23, "column": 17 }, "end": { - "line": 22, + "line": 26, "column": 3 } }, @@ -30,11 +30,11 @@ "defaultMessage": "!!!Most popular", "file": "src/components/settings/recipes/RecipesDashboard.js", "start": { - "line": 23, + "line": 27, "column": 22 }, "end": { - "line": 26, + "line": 30, "column": 3 } }, @@ -43,24 +43,24 @@ "defaultMessage": "!!!All services", "file": "src/components/settings/recipes/RecipesDashboard.js", "start": { - "line": 27, + "line": 31, "column": 14 }, "end": { - "line": 30, + "line": 34, "column": 3 } }, { - "id": "settings.recipes.dev", - "defaultMessage": "!!!Development", + "id": "settings.recipes.custom", + "defaultMessage": "!!!Custom Services", "file": "src/components/settings/recipes/RecipesDashboard.js", "start": { - "line": 31, - "column": 14 + "line": 35, + "column": 17 }, "end": { - "line": 34, + "line": 38, "column": 3 } }, @@ -69,11 +69,11 @@ "defaultMessage": "!!!Sorry, but no service matched your search term.", "file": "src/components/settings/recipes/RecipesDashboard.js", "start": { - "line": 35, + "line": 39, "column": 16 }, "end": { - "line": 38, + "line": 42, "column": 3 } }, @@ -82,11 +82,11 @@ "defaultMessage": "!!!Service successfully added", "file": "src/components/settings/recipes/RecipesDashboard.js", "start": { - "line": 39, + "line": 43, "column": 31 }, "end": { - "line": 42, + "line": 46, "column": 3 } }, @@ -95,11 +95,89 @@ "defaultMessage": "!!!Missing a service?", "file": "src/components/settings/recipes/RecipesDashboard.js", "start": { - "line": 43, + "line": 47, "column": 18 }, "end": { - "line": 46, + "line": 50, + "column": 3 + } + }, + { + "id": "settings.recipes.customService.intro", + "defaultMessage": "!!!To add a custom service, copy the recipe folder into:", + "file": "src/components/settings/recipes/RecipesDashboard.js", + "start": { + "line": 51, + "column": 21 + }, + "end": { + "line": 54, + "column": 3 + } + }, + { + "id": "settings.recipes.customService.openFolder", + "defaultMessage": "!!!Open directory", + "file": "src/components/settings/recipes/RecipesDashboard.js", + "start": { + "line": 55, + "column": 14 + }, + "end": { + "line": 58, + "column": 3 + } + }, + { + "id": "settings.recipes.customService.openDevDocs", + "defaultMessage": "!!!Developer Documentation", + "file": "src/components/settings/recipes/RecipesDashboard.js", + "start": { + "line": 59, + "column": 15 + }, + "end": { + "line": 62, + "column": 3 + } + }, + { + "id": "settings.recipes.customService.headline.customRecipes", + "defaultMessage": "!!!Custom Service Recipes", + "file": "src/components/settings/recipes/RecipesDashboard.js", + "start": { + "line": 63, + "column": 25 + }, + "end": { + "line": 66, + "column": 3 + } + }, + { + "id": "settings.recipes.customService.headline.communityRecipes", + "defaultMessage": "!!!Community Services", + "file": "src/components/settings/recipes/RecipesDashboard.js", + "start": { + "line": 67, + "column": 28 + }, + "end": { + "line": 70, + "column": 3 + } + }, + { + "id": "settings.recipes.customService.headline.devRecipes", + "defaultMessage": "!!!Your Development Service Recipes", + "file": "src/components/settings/recipes/RecipesDashboard.js", + "start": { + "line": 71, + "column": 22 + }, + "end": { + "line": 74, "column": 3 } } diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js index e7832088b..0f54a50af 100644 --- a/src/stores/FeaturesStore.js +++ b/src/stores/FeaturesStore.js @@ -16,6 +16,7 @@ import workspaces from '../features/workspaces'; import shareFranz from '../features/shareFranz'; import announcements from '../features/announcements'; import settingsWS from '../features/settingsWS'; +import communityRecipes from '../features/communityRecipes'; import { DEFAULT_FEATURES_CONFIG } from '../config'; @@ -75,5 +76,6 @@ export default class FeaturesStore extends Store { shareFranz(this.stores, this.actions); announcements(this.stores, this.actions); settingsWS(this.stores, this.actions); + communityRecipes(this.stores, this.actions); } } diff --git a/src/stores/index.js b/src/stores/index.js index 1912418a2..7f89bf1fb 100644 --- a/src/stores/index.js +++ b/src/stores/index.js @@ -12,6 +12,7 @@ import RequestStore from './RequestStore'; import GlobalErrorStore from './GlobalErrorStore'; import { workspaceStore } from '../features/workspaces'; import { announcementsStore } from '../features/announcements'; +import { communityRecipesStore } from '../features/communityRecipes'; export default (api, actions, router) => { const stores = {}; @@ -31,6 +32,7 @@ export default (api, actions, router) => { globalError: new GlobalErrorStore(stores, api, actions), workspaces: workspaceStore, announcements: announcementsStore, + communityRecipes: communityRecipesStore, }); // Initialize all stores Object.keys(stores).forEach((name) => { diff --git a/src/styles/recipes.scss b/src/styles/recipes.scss index 84222e1fe..56a248e98 100644 --- a/src/styles/recipes.scss +++ b/src/styles/recipes.scss @@ -12,7 +12,7 @@ display: flex; flex-flow: row wrap; height: auto; - min-height: 70%; + // min-height: 70%; &.recipes__list--disabled { filter: grayscale(100%); -- cgit v1.2.3-70-g09d2 From d7ed456a7b6f73e046ba3a2ef38eb21f82f06ca4 Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Tue, 30 Jul 2019 11:41:54 +0200 Subject: Make todo layer resizable --- packages/theme/src/themes/default/index.ts | 7 ++ src/actions/index.js | 2 + src/components/layout/AppLayout.js | 16 +--- src/containers/layout/AppLayoutContainer.js | 4 - src/features/todos/actions.js | 10 ++ src/features/todos/components/TodosWebview.js | 129 +++++++++++++++++++++++--- src/features/todos/containers/TodosScreen.js | 45 +++++++++ src/features/todos/index.js | 33 +++++++ src/features/todos/store.js | 86 +++++++++++++++++ src/stores/FeaturesStore.js | 2 + src/styles/layout.scss | 7 +- 11 files changed, 311 insertions(+), 30 deletions(-) create mode 100644 src/features/todos/actions.js create mode 100644 src/features/todos/containers/TodosScreen.js create mode 100644 src/features/todos/index.js create mode 100644 src/features/todos/store.js (limited to 'src/stores/FeaturesStore.js') diff --git a/packages/theme/src/themes/default/index.ts b/packages/theme/src/themes/default/index.ts index 0f02fa3c8..4a49a4de0 100644 --- a/packages/theme/src/themes/default/index.ts +++ b/packages/theme/src/themes/default/index.ts @@ -207,3 +207,10 @@ export const announcements = { background: legacyStyles.themeGrayLightest, }, }; + +// Todos +export const todos = { + dragIndicator: { + background: legacyStyles.themeGrayLight, + }, +}; diff --git a/src/actions/index.js b/src/actions/index.js index fc525afeb..336344d76 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -13,6 +13,7 @@ import settings from './settings'; import requests from './requests'; import announcements from '../features/announcements/actions'; import workspaces from '../features/workspaces/actions'; +import todos from '../features/todos/actions'; const actions = Object.assign({}, { service, @@ -31,4 +32,5 @@ export default Object.assign( defineActions(actions, PropTypes.checkPropTypes), { announcements }, { workspaces }, + { todos }, ); diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js index 7f2f707fb..dbf7d3c21 100644 --- a/src/components/layout/AppLayout.js +++ b/src/components/layout/AppLayout.js @@ -17,7 +17,7 @@ import { isWindows } from '../../environment'; import WorkspaceSwitchingIndicator from '../../features/workspaces/components/WorkspaceSwitchingIndicator'; import { workspaceStore } from '../../features/workspaces'; import AppUpdateInfoBar from '../AppUpdateInfoBar'; -import TodosWebview from '../../features/todos/components/TodosWebview'; +import Todos from '../../features/todos/containers/TodosScreen'; function createMarkup(HTMLString) { return { __html: HTMLString }; @@ -52,7 +52,6 @@ const styles = theme => ({ @injectSheet(styles) @observer class AppLayout extends Component { static propTypes = { - authToken: PropTypes.string.isRequired, classes: PropTypes.object.isRequired, isFullScreen: PropTypes.bool.isRequired, sidebar: PropTypes.element.isRequired, @@ -60,7 +59,6 @@ class AppLayout extends Component { services: PropTypes.element.isRequired, children: PropTypes.element, news: MobxPropTypes.arrayOrObservableArray.isRequired, - // isOnline: PropTypes.bool.isRequired, showServicesUpdatedInfoBar: PropTypes.bool.isRequired, appUpdateIsDownloaded: PropTypes.bool.isRequired, nextAppReleaseVersion: PropTypes.string, @@ -85,7 +83,6 @@ class AppLayout extends Component { render() { const { - authToken, classes, isFullScreen, workspacesDrawer, @@ -129,15 +126,6 @@ class AppLayout extends Component { ))} - {/* {!isOnline && ( - - - {intl.formatMessage(globalMessages.notConnectedToTheInternet)} - - )} */} {!areRequiredRequestsSuccessful && showRequiredRequestsError && ( - + diff --git a/src/containers/layout/AppLayoutContainer.js b/src/containers/layout/AppLayoutContainer.js index 8a48f4924..cf3da71e8 100644 --- a/src/containers/layout/AppLayoutContainer.js +++ b/src/containers/layout/AppLayoutContainer.js @@ -12,7 +12,6 @@ import NewsStore from '../../stores/NewsStore'; import SettingsStore from '../../stores/SettingsStore'; import RequestStore from '../../stores/RequestStore'; import GlobalErrorStore from '../../stores/GlobalErrorStore'; -import UserStore from '../../stores/UserStore'; import { oneOrManyChildElements } from '../../prop-types'; import AppLayout from '../../components/layout/AppLayout'; @@ -40,7 +39,6 @@ export default @inject('stores', 'actions') @observer class AppLayoutContainer e settings, globalError, requests, - user, } = this.props.stores; const { @@ -133,7 +131,6 @@ export default @inject('stores', 'actions') @observer class AppLayoutContainer e return ( ({ root: { background: theme.colorBackground, - height: '100%', - width: 300, - position: 'absolute', - top: 0, - right: -300, + position: 'relative', }, webview: { height: '100%', }, + resizeHandler: { + position: 'absolute', + left: 0, + marginLeft: -5, + width: 10, + zIndex: 400, + cursor: 'col-resize', + }, + dragIndicator: { + position: 'absolute', + left: 0, + width: 5, + zIndex: 400, + background: theme.todos.dragIndicator.background, + }, }); @injectSheet(styles) @observer @@ -24,17 +35,113 @@ class TodosWebview extends Component { static propTypes = { classes: PropTypes.object.isRequired, authToken: PropTypes.string.isRequired, + resize: PropTypes.func.isRequired, + width: PropTypes.number.isRequired, + minWidth: PropTypes.number.isRequired, }; + state = { + isDragging: false, + width: 300, + } + + componentWillMount() { + const { width } = this.props; + + this.setState({ + width, + }); + } + + componentDidMount() { + this.node.addEventListener('mousemove', this.resizePanel.bind(this)); + this.node.addEventListener('mouseup', this.stopResize.bind(this)); + this.node.addEventListener('mouseleave', this.stopResize.bind(this)); + } + + startResize = (event) => { + this.setState({ + isDragging: true, + initialPos: event.clientX, + delta: 0, + }); + } + + resizePanel(e) { + const { minWidth } = this.props; + + const { + isDragging, + initialPos, + } = this.state; + + if (isDragging && Math.abs(e.clientX - window.innerWidth) > minWidth) { + const delta = e.clientX - initialPos; + + this.setState({ + delta, + }); + } + } + + stopResize() { + const { + resize, + minWidth, + } = this.props; + + const { + isDragging, + delta, + width, + } = this.state; + + if (isDragging) { + let newWidth = width + (delta < 0 ? Math.abs(delta) : -Math.abs(delta)); + + if (newWidth < minWidth) { + newWidth = minWidth; + } + + this.setState({ + isDragging: false, + delta: 0, + width: newWidth, + }); + + resize(newWidth); + } + } + render() { const { authToken, classes } = this.props; + const { width, delta, isDragging } = this.state; + return ( -
- -
+ <> +
this.stopResize()} + ref={(node) => { this.node = node; }} + > +
this.startResize(e)} + /> + {isDragging && ( +
+ )} + +
+ ); } } diff --git a/src/features/todos/containers/TodosScreen.js b/src/features/todos/containers/TodosScreen.js new file mode 100644 index 000000000..0759c22db --- /dev/null +++ b/src/features/todos/containers/TodosScreen.js @@ -0,0 +1,45 @@ +import React, { Component } from 'react'; +import { inject, observer } from 'mobx-react'; +import PropTypes from 'prop-types'; + +import TodosWebview from '../components/TodosWebview'; +import ErrorBoundary from '../../../components/util/ErrorBoundary'; +import UserStore from '../../../stores/UserStore'; +import TodoStore from '../store'; +import { TODOS_MIN_WIDTH } from '..'; + +@inject('stores', 'actions') @observer +class TodosScreen extends Component { + static propTypes = { + stores: PropTypes.shape({ + user: PropTypes.instanceOf(UserStore).isRequired, + todos: PropTypes.instanceOf(TodoStore).isRequired, + }).isRequired, + actions: PropTypes.shape({ + todos: PropTypes.shape({ + resize: PropTypes.func.isRequired, + }), + }).isRequired, + }; + + render() { + const { stores, actions } = this.props; + + if (!stores.todos || !stores.todos.isFeatureActive) { + return null; + } + + return ( + + actions.todos.resize({ width })} + /> + + ); + } +} + +export default TodosScreen; diff --git a/src/features/todos/index.js b/src/features/todos/index.js new file mode 100644 index 000000000..0dfd35c78 --- /dev/null +++ b/src/features/todos/index.js @@ -0,0 +1,33 @@ +import { reaction } from 'mobx'; +import TodoStore from './store'; + +const debug = require('debug')('Franz:feature:todos'); + +export const GA_CATEGORY_TODOS = 'Todos'; + +export const DEFAULT_TODOS_WIDTH = 300; +export const TODOS_MIN_WIDTH = 200; + +export const todoStore = new TodoStore(); + +export default function initTodos(stores, actions) { + stores.todos = todoStore; + const { features } = stores; + + // Toggle todos feature + reaction( + () => features.features.isTodosEnabled, + (isEnabled) => { + if (isEnabled) { + debug('Initializing `todos` feature'); + todoStore.start(stores, actions); + } else if (todoStore.isFeatureActive) { + debug('Disabling `todos` feature'); + todoStore.stop(); + } + }, + { + fireImmediately: true, + }, + ); +} diff --git a/src/features/todos/store.js b/src/features/todos/store.js new file mode 100644 index 000000000..e7e13b37f --- /dev/null +++ b/src/features/todos/store.js @@ -0,0 +1,86 @@ +import { + computed, + action, + observable, +} from 'mobx'; +import localStorage from 'mobx-localstorage'; + +import { todoActions } from './actions'; +import { FeatureStore } from '../utils/FeatureStore'; +import { createReactions } from '../../stores/lib/Reaction'; +import { createActionBindings } from '../utils/ActionBinding'; +import { DEFAULT_TODOS_WIDTH, TODOS_MIN_WIDTH } from '.'; + +const debug = require('debug')('Franz:feature:todos:store'); + +export default class TodoStore extends FeatureStore { + @observable isFeatureEnabled = false; + + @observable isFeatureActive = false; + + @computed get width() { + const width = this.settings.width || DEFAULT_TODOS_WIDTH; + + return width < TODOS_MIN_WIDTH ? TODOS_MIN_WIDTH : width; + } + + @computed get settings() { + return localStorage.getItem('todos') || {}; + } + + // ========== PUBLIC API ========= // + + @action start(stores, actions) { + debug('TodoStore::start'); + this.stores = stores; + this.actions = actions; + + // ACTIONS + + this._registerActions(createActionBindings([ + [todoActions.resize, this._resize], + ])); + + // REACTIONS + + this._allReactions = createReactions([ + this._setFeatureEnabledReaction, + ]); + + this._registerReactions(this._allReactions); + + this.isFeatureActive = true; + } + + @action stop() { + super.stop(); + debug('TodoStore::stop'); + this.reset(); + this.isFeatureActive = false; + } + + // ========== PRIVATE METHODS ========= // + + _updateSettings = (changes) => { + localStorage.setItem('todos', { + ...this.settings, + ...changes, + }); + }; + + // Actions + + @action _resize = ({ width }) => { + this._updateSettings({ + width, + }); + }; + + // Reactions + + _setFeatureEnabledReaction = () => { + const { isTodosEnabled } = this.stores.features.features; + + this.isFeatureEnabled = isTodosEnabled; + }; +} diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js index e7832088b..35a050c67 100644 --- a/src/stores/FeaturesStore.js +++ b/src/stores/FeaturesStore.js @@ -16,6 +16,7 @@ import workspaces from '../features/workspaces'; import shareFranz from '../features/shareFranz'; import announcements from '../features/announcements'; import settingsWS from '../features/settingsWS'; +import todos from '../features/todos'; import { DEFAULT_FEATURES_CONFIG } from '../config'; @@ -75,5 +76,6 @@ export default class FeaturesStore extends Store { shareFranz(this.stores, this.actions); announcements(this.stores, this.actions); settingsWS(this.stores, this.actions); + todos(this.stores, this.actions); } } diff --git a/src/styles/layout.scss b/src/styles/layout.scss index 739082445..10027da60 100644 --- a/src/styles/layout.scss +++ b/src/styles/layout.scss @@ -37,10 +37,15 @@ html { overflow: hidden; } .app__content { display: flex; + width: calc(100% + 300px); + } + + .app__main-content { + display: flex; + width: 100%; } .app__service { - // position: relative; display: flex; flex: 1; flex-direction: column; -- cgit v1.2.3-70-g09d2