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 --- .../serviceLimit/components/LimitReachedInfobox.js | 74 ++++++++++++++++++++++ src/features/serviceLimit/index.js | 33 ++++++++++ src/features/serviceLimit/store.js | 41 ++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 src/features/serviceLimit/components/LimitReachedInfobox.js create mode 100644 src/features/serviceLimit/index.js create mode 100644 src/features/serviceLimit/store.js (limited to 'src/features') diff --git a/src/features/serviceLimit/components/LimitReachedInfobox.js b/src/features/serviceLimit/components/LimitReachedInfobox.js new file mode 100644 index 000000000..ee0d7cb27 --- /dev/null +++ b/src/features/serviceLimit/components/LimitReachedInfobox.js @@ -0,0 +1,74 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; +import injectSheet from 'react-jss'; +import { Infobox } from '@meetfranz/ui'; + +import { gaEvent } from '../../../lib/analytics'; + +const messages = defineMessages({ + limitReached: { + id: 'feature.serviceLimit.limitReached', + defaultMessage: '!!!You have added {amount} of {limit} services. Please upgrade your account to add more services.', + }, + action: { + id: 'premiumFeature.button.upgradeAccount', + defaultMessage: '!!!Upgrade account', + }, +}); + +const styles = theme => ({ + container: { + height: 'auto', + background: theme.styleTypes.primary.accent, + color: theme.styleTypes.primary.contrast, + borderRadius: 0, + marginBottom: 0, + + '& button': { + color: theme.styleTypes.primary.contrast, + }, + }, +}); + + +@inject('stores', 'actions') @injectSheet(styles) @observer +class LimitReachedInfobox extends Component { + static propTypes = { + classes: PropTypes.object.isRequired, + stores: PropTypes.object.isRequired, + actions: PropTypes.object.isRequired, + }; + + static contextTypes = { + intl: intlShape, + }; + + render() { + const { classes, stores, actions } = this.props; + const { intl } = this.context; + + const { + serviceLimit, + } = stores; + + if (!serviceLimit.userHasReachedServiceLimit) return null; + + return ( + { + actions.ui.openSettings({ path: 'user' }); + gaEvent('Service Limit', 'upgrade', 'Upgrade account'); + }} + > + {intl.formatMessage(messages.limitReached, { amount: serviceLimit.serviceCount, limit: serviceLimit.serviceLimit })} + + ); + } +} + +export default LimitReachedInfobox; diff --git a/src/features/serviceLimit/index.js b/src/features/serviceLimit/index.js new file mode 100644 index 000000000..76f996195 --- /dev/null +++ b/src/features/serviceLimit/index.js @@ -0,0 +1,33 @@ +import { reaction } from 'mobx'; +import { ServiceLimitStore } from './store'; + +const debug = require('debug')('Franz:feature:serviceLimit'); + +export const DEFAULT_SERVICE_LIMIT = 3; + +let store = null; + +export const serviceLimitStore = new ServiceLimitStore(); + +export default function initServiceLimit(stores, actions) { + const { features } = stores; + + // Toggle serviceLimit feature + reaction( + () => ( + features.features.hasServiceLimit + ), + (isEnabled) => { + if (isEnabled) { + debug('Initializing `serviceLimit` feature'); + store = serviceLimitStore.start(stores, actions); + } else if (store) { + debug('Disabling `serviceLimit` feature'); + serviceLimitStore.stop(); + } + }, + { + fireImmediately: true, + }, + ); +} diff --git a/src/features/serviceLimit/store.js b/src/features/serviceLimit/store.js new file mode 100644 index 000000000..752f71371 --- /dev/null +++ b/src/features/serviceLimit/store.js @@ -0,0 +1,41 @@ +import { computed, observable } from 'mobx'; +import { FeatureStore } from '../utils/FeatureStore'; +import { DEFAULT_SERVICE_LIMIT } from '.'; + +const debug = require('debug')('Franz:feature:serviceLimit:store'); + +export class ServiceLimitStore extends FeatureStore { + @observable isServiceLimitEnabled = false; + + start(stores, actions) { + debug('start'); + this.stores = stores; + this.actions = actions; + + this.isServiceLimitEnabled = true; + } + + stop() { + super.stop(); + + this.isServiceLimitEnabled = false; + } + + @computed get userHasReachedServiceLimit() { + if (!this.isServiceLimitEnabled) return false; + + const { user } = this.stores; + + return !user.isPremium && this.serviceCount >= this.serviceLimit; + } + + @computed get serviceLimit() { + return this.stores.features.features.serviceLimitCount || DEFAULT_SERVICE_LIMIT; + } + + @computed get serviceCount() { + return this.stores.services.all.length; + } +} + +export default ServiceLimitStore; -- cgit v1.2.3-54-g00ecf 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/features') 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-54-g00ecf From 66647005ae23bccf1691a43ea36565cae4ce9518 Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Thu, 13 Jun 2019 15:15:38 +0200 Subject: Improve serviceLimit implementation --- src/components/settings/navigation/SettingsNavigation.js | 8 +++++++- src/features/serviceLimit/components/LimitReachedInfobox.js | 4 ++++ src/features/serviceLimit/index.js | 2 +- src/features/serviceLimit/store.js | 6 +++--- src/i18n/locales/en-US.json | 2 +- 5 files changed, 16 insertions(+), 6 deletions(-) (limited to 'src/features') diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js index df4b3b3b2..4696b82eb 100644 --- a/src/components/settings/navigation/SettingsNavigation.js +++ b/src/components/settings/navigation/SettingsNavigation.js @@ -8,6 +8,7 @@ import Link from '../../ui/Link'; import { workspaceStore } from '../../../features/workspaces'; import UIStore from '../../../stores/UIStore'; import UserStore from '../../../stores/UserStore'; +import { serviceLimitStore } from '../../../features/serviceLimit'; const messages = defineMessages({ availableServices: { @@ -80,7 +81,12 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp > {intl.formatMessage(messages.yourServices)} {' '} - {serviceCount} + + {serviceCount} + {serviceLimitStore.serviceLimit !== 0 && ( + `/${serviceLimitStore.serviceLimit}` + )} + {workspaceStore.isFeatureEnabled ? ( ({ borderRadius: 0, marginBottom: 0, + '& > div': { + marginBottom: 0, + }, + '& button': { color: theme.styleTypes.primary.contrast, }, diff --git a/src/features/serviceLimit/index.js b/src/features/serviceLimit/index.js index 76f996195..92ad8bb98 100644 --- a/src/features/serviceLimit/index.js +++ b/src/features/serviceLimit/index.js @@ -15,7 +15,7 @@ export default function initServiceLimit(stores, actions) { // Toggle serviceLimit feature reaction( () => ( - features.features.hasServiceLimit + features.features.isServiceLimitEnabled ), (isEnabled) => { if (isEnabled) { diff --git a/src/features/serviceLimit/store.js b/src/features/serviceLimit/store.js index 752f71371..9836c5f51 100644 --- a/src/features/serviceLimit/store.js +++ b/src/features/serviceLimit/store.js @@ -24,12 +24,12 @@ export class ServiceLimitStore extends FeatureStore { @computed get userHasReachedServiceLimit() { if (!this.isServiceLimitEnabled) return false; - const { user } = this.stores; - - return !user.isPremium && this.serviceCount >= this.serviceLimit; + return this.serviceLimit !== 0 && this.serviceCount >= this.serviceLimit; } @computed get serviceLimit() { + if (!this.isServiceLimitEnabled || this.stores.features.features.serviceLimitCount === 0) return 0; + return this.stores.features.features.serviceLimitCount || DEFAULT_SERVICE_LIMIT; } diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index a821dbe39..eded7b79b 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -5,7 +5,7 @@ "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.", - "feature.serviceLimit.limitReached": "You have added {amount} of {limit} services. Please upgrade your account to add more services.", + "feature.serviceLimit.limitReached": "You have added {amount} out of {limit} services that are included in your plan. Please upgrade your account to add more services.", "feature.shareFranz.action.email": "Send as email", "feature.shareFranz.action.facebook": "Share on Facebook", "feature.shareFranz.action.twitter": "Share on Twitter", -- cgit v1.2.3-54-g00ecf From 2887eee3935e75640fc45111a904e78496dab62e Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Mon, 17 Jun 2019 16:52:46 +0200 Subject: Rename feature flags --- src/components/settings/recipes/RecipesDashboard.js | 8 ++++---- src/components/settings/services/EditServiceForm.js | 12 ++++++------ src/components/settings/settings/EditSettingsForm.js | 8 +++++--- src/config.js | 6 +++--- src/containers/settings/EditServiceScreen.js | 4 ++-- src/containers/settings/EditSettingsScreen.js | 6 +++--- src/containers/settings/RecipesScreen.js | 2 +- src/features/communityRecipes/index.js | 4 ++-- src/features/communityRecipes/store.js | 2 +- src/features/serviceProxy/index.js | 8 ++++---- src/features/spellchecker/index.js | 8 ++++---- src/features/workspaces/store.js | 6 +++--- src/stores/ServicesStore.js | 4 ++-- 13 files changed, 40 insertions(+), 38 deletions(-) (limited to 'src/features') diff --git a/src/components/settings/recipes/RecipesDashboard.js b/src/components/settings/recipes/RecipesDashboard.js index ed4a429db..75e60b7ec 100644 --- a/src/components/settings/recipes/RecipesDashboard.js +++ b/src/components/settings/recipes/RecipesDashboard.js @@ -118,7 +118,7 @@ export default @injectSheet(styles) @observer class RecipesDashboard extends Com openRecipeDirectory: PropTypes.func.isRequired, openDevDocs: PropTypes.func.isRequired, classes: PropTypes.object.isRequired, - isCommunityRecipesPremiumFeature: PropTypes.bool.isRequired, + isCommunityRecipesIncludedInCurrentPlan: PropTypes.bool.isRequired, }; static defaultProps = { @@ -145,7 +145,7 @@ export default @injectSheet(styles) @observer class RecipesDashboard extends Com openRecipeDirectory, openDevDocs, classes, - isCommunityRecipesPremiumFeature, + isCommunityRecipesIncludedInCurrentPlan, } = this.props; const { intl } = this.context; @@ -218,7 +218,7 @@ export default @injectSheet(styles) @observer class RecipesDashboard extends Com <>

{intl.formatMessage(messages.headlineCustomRecipes)} - {isCommunityRecipesPremiumFeature && ( + {isCommunityRecipesIncludedInCurrentPlan && ( )}

@@ -247,7 +247,7 @@ export default @injectSheet(styles) @observer class RecipesDashboard extends Com )} 0) && isCommunityRecipesPremiumFeature} + condition={(recipeFilter === 'dev' && communityRecipes.length > 0) && isCommunityRecipesIncludedInCurrentPlan} > {recipeFilter === 'dev' && communityRecipes.length > 0 && (

{intl.formatMessage(messages.headlineCommunityRecipes)}

diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js index c089a1582..5cde0db8e 100644 --- a/src/components/settings/services/EditServiceForm.js +++ b/src/components/settings/services/EditServiceForm.js @@ -130,8 +130,8 @@ export default @observer class EditServiceForm extends Component { isSaving: PropTypes.bool.isRequired, isDeleting: PropTypes.bool.isRequired, isProxyFeatureEnabled: PropTypes.bool.isRequired, - isProxyPremiumFeature: PropTypes.bool.isRequired, - isSpellcheckerPremiumFeature: PropTypes.bool.isRequired, + isServiceProxyIncludedInCurrentPlan: PropTypes.bool.isRequired, + isSpellcheckerIncludedInCurrentPlan: PropTypes.bool.isRequired, }; static defaultProps = { @@ -194,8 +194,8 @@ export default @observer class EditServiceForm extends Component { isDeleting, onDelete, isProxyFeatureEnabled, - isProxyPremiumFeature, - isSpellcheckerPremiumFeature, + isServiceProxyIncludedInCurrentPlan, + isSpellcheckerIncludedInCurrentPlan, } = this.props; const { intl } = this.context; @@ -345,7 +345,7 @@ export default @observer class EditServiceForm extends Component {
@@ -355,7 +355,7 @@ export default @observer class EditServiceForm extends Component { {isProxyFeatureEnabled && (
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js index efd453356..6d0811b1b 100644 --- a/src/components/settings/settings/EditSettingsForm.js +++ b/src/components/settings/settings/EditSettingsForm.js @@ -100,7 +100,7 @@ export default @observer class EditSettingsForm extends Component { isClearingAllCache: PropTypes.bool.isRequired, onClearAllCache: PropTypes.func.isRequired, cacheSize: PropTypes.string.isRequired, - isSpellcheckerPremiumFeature: PropTypes.bool.isRequired, + isSpellcheckerIncludedInCurrentPlan: PropTypes.bool.isRequired, }; static contextTypes = { @@ -130,7 +130,7 @@ export default @observer class EditSettingsForm extends Component { isClearingAllCache, onClearAllCache, cacheSize, - isSpellcheckerPremiumFeature, + isSpellcheckerIncludedInCurrentPlan, } = this.props; const { intl } = this.context; @@ -143,6 +143,8 @@ export default @observer class EditSettingsForm extends Component { updateButtonLabelMessage = messages.buttonSearchForUpdate; } + console.log('isSpellcheckerIncludedInCurrentPlan', isSpellcheckerIncludedInCurrentPlan); + return (
@@ -173,7 +175,7 @@ export default @observer class EditSettingsForm extends Component {

{intl.formatMessage(messages.headlineLanguage)}