aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Stefan Malzner <stefan@adlk.io>2019-05-02 11:27:23 +0200
committerLibravatar Stefan Malzner <stefan@adlk.io>2019-05-02 11:27:23 +0200
commitfbff4ed90b0137088b1bb92e95f32fc1bfa7bb3e (patch)
treedfa0a6c32ce62c605bcdbf5538d9dc03725d1f88
parentUpdate CHANGELOG.md (diff)
downloadferdium-app-fbff4ed90b0137088b1bb92e95f32fc1bfa7bb3e.tar.gz
ferdium-app-fbff4ed90b0137088b1bb92e95f32fc1bfa7bb3e.tar.zst
ferdium-app-fbff4ed90b0137088b1bb92e95f32fc1bfa7bb3e.zip
Add custom recipe limitation
-rw-r--r--packages/ui/src/badge/ProBadge.tsx5
-rw-r--r--src/components/settings/recipes/RecipeItem.js2
-rw-r--r--src/components/settings/recipes/RecipesDashboard.js194
-rw-r--r--src/config.js1
-rw-r--r--src/containers/settings/RecipesScreen.js40
-rw-r--r--src/features/communityRecipes/index.js28
-rw-r--r--src/features/communityRecipes/store.js31
-rw-r--r--src/i18n/locales/defaultMessages.json178
-rw-r--r--src/i18n/locales/en-US.json8
-rw-r--r--src/i18n/messages/src/components/settings/recipes/RecipesDashboard.json116
-rw-r--r--src/stores/FeaturesStore.js2
-rw-r--r--src/stores/index.js2
-rw-r--r--src/styles/recipes.scss2
13 files changed, 526 insertions, 83 deletions
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';
3import React, { Component } from 'react'; 3import React, { Component } from 'react';
4import injectStyle from 'react-jss'; 4import injectStyle from 'react-jss';
5 5
6import { Icon, Badge } from '../'; 6import { Badge, Icon } from '../';
7import { IWithStyle } from '../typings/generic'; 7import { IWithStyle } from '../typings/generic';
8 8
9interface IProps extends IWithStyle { 9interface IProps extends IWithStyle {
10 badgeClasses?: string; 10 badgeClasses?: string;
11 iconClasses?: string; 11 iconClasses?: string;
12 inverted?: boolean; 12 inverted?: boolean;
13 className?: string;
13} 14}
14 15
15const styles = (theme: Theme) => ({ 16const styles = (theme: Theme) => ({
@@ -37,6 +38,7 @@ class ProBadgeComponent extends Component<IProps> {
37 badgeClasses, 38 badgeClasses,
38 iconClasses, 39 iconClasses,
39 inverted, 40 inverted,
41 className,
40 } = this.props; 42 } = this.props;
41 43
42 return ( 44 return (
@@ -46,6 +48,7 @@ class ProBadgeComponent extends Component<IProps> {
46 classes.badge, 48 classes.badge,
47 inverted && classes.invertedBadge, 49 inverted && classes.invertedBadge,
48 badgeClasses, 50 badgeClasses,
51 className,
49 ])} 52 ])}
50 > 53 >
51 <Icon 54 <Icon
diff --git a/src/components/settings/recipes/RecipeItem.js b/src/components/settings/recipes/RecipeItem.js
index 3bb0852b2..12e3775f6 100644
--- a/src/components/settings/recipes/RecipeItem.js
+++ b/src/components/settings/recipes/RecipeItem.js
@@ -19,7 +19,7 @@ export default @observer class RecipeItem extends Component {
19 className="recipe-teaser" 19 className="recipe-teaser"
20 onClick={onClick} 20 onClick={onClick}
21 > 21 >
22 {recipe.local && ( 22 {recipe.isDevRecipe && (
23 <span className="recipe-teaser__dev-badge">dev</span> 23 <span className="recipe-teaser__dev-badge">dev</span>
24 )} 24 )}
25 <img 25 <img
diff --git a/src/components/settings/recipes/RecipesDashboard.js b/src/components/settings/recipes/RecipesDashboard.js
index 00cd725cf..e2e8121e5 100644
--- a/src/components/settings/recipes/RecipesDashboard.js
+++ b/src/components/settings/recipes/RecipesDashboard.js
@@ -4,12 +4,16 @@ import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import { Link } from 'react-router'; 5import { Link } from 'react-router';
6 6
7import { Button, Input } from '@meetfranz/forms';
8import injectSheet from 'react-jss';
9import { H3, H2, ProBadge } from '@meetfranz/ui';
7import SearchInput from '../../ui/SearchInput'; 10import SearchInput from '../../ui/SearchInput';
8import Infobox from '../../ui/Infobox'; 11import Infobox from '../../ui/Infobox';
9import RecipeItem from './RecipeItem'; 12import RecipeItem from './RecipeItem';
10import Loader from '../../ui/Loader'; 13import Loader from '../../ui/Loader';
11import Appear from '../../ui/effects/Appear'; 14import Appear from '../../ui/effects/Appear';
12import { FRANZ_SERVICE_REQUEST } from '../../../config'; 15import { FRANZ_SERVICE_REQUEST } from '../../../config';
16import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer';
13 17
14const messages = defineMessages({ 18const messages = defineMessages({
15 headline: { 19 headline: {
@@ -28,9 +32,9 @@ const messages = defineMessages({
28 id: 'settings.recipes.all', 32 id: 'settings.recipes.all',
29 defaultMessage: '!!!All services', 33 defaultMessage: '!!!All services',
30 }, 34 },
31 devRecipes: { 35 customRecipes: {
32 id: 'settings.recipes.dev', 36 id: 'settings.recipes.custom',
33 defaultMessage: '!!!Development', 37 defaultMessage: '!!!Custom Services',
34 }, 38 },
35 nothingFound: { 39 nothingFound: {
36 id: 'settings.recipes.nothingFound', 40 id: 'settings.recipes.nothingFound',
@@ -44,9 +48,61 @@ const messages = defineMessages({
44 id: 'settings.recipes.missingService', 48 id: 'settings.recipes.missingService',
45 defaultMessage: '!!!Missing a service?', 49 defaultMessage: '!!!Missing a service?',
46 }, 50 },
51 customRecipeIntro: {
52 id: 'settings.recipes.customService.intro',
53 defaultMessage: '!!!To add a custom service, copy the recipe folder into:',
54 },
55 openFolder: {
56 id: 'settings.recipes.customService.openFolder',
57 defaultMessage: '!!!Open directory',
58 },
59 openDevDocs: {
60 id: 'settings.recipes.customService.openDevDocs',
61 defaultMessage: '!!!Developer Documentation',
62 },
63 headlineCustomRecipes: {
64 id: 'settings.recipes.customService.headline.customRecipes',
65 defaultMessage: '!!!Custom Service Recipes',
66 },
67 headlineCommunityRecipes: {
68 id: 'settings.recipes.customService.headline.communityRecipes',
69 defaultMessage: '!!!Community Services',
70 },
71 headlineDevRecipes: {
72 id: 'settings.recipes.customService.headline.devRecipes',
73 defaultMessage: '!!!Your Development Service Recipes',
74 },
47}); 75});
48 76
49export default @observer class RecipesDashboard extends Component { 77const styles = {
78 devRecipeIntroContainer: {
79 textAlign: 'center',
80 width: '100%',
81 height: 'auto',
82 margin: [40, 0],
83 },
84 path: {
85 marginTop: 20,
86
87 '& > div': {
88 fontFamily: 'SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace',
89 },
90 },
91 actionContainer: {
92 '& button': {
93 margin: [0, 10],
94 },
95 },
96 devRecipeList: {
97 marginTop: 20,
98 height: 'auto',
99 },
100 proBadge: {
101 marginLeft: '10px !important',
102 },
103};
104
105export default @injectSheet(styles) @observer class RecipesDashboard extends Component {
50 static propTypes = { 106 static propTypes = {
51 recipes: MobxPropTypes.arrayOrObservableArray.isRequired, 107 recipes: MobxPropTypes.arrayOrObservableArray.isRequired,
52 isLoading: PropTypes.bool.isRequired, 108 isLoading: PropTypes.bool.isRequired,
@@ -55,12 +111,19 @@ export default @observer class RecipesDashboard extends Component {
55 searchRecipes: PropTypes.func.isRequired, 111 searchRecipes: PropTypes.func.isRequired,
56 resetSearch: PropTypes.func.isRequired, 112 resetSearch: PropTypes.func.isRequired,
57 serviceStatus: MobxPropTypes.arrayOrObservableArray.isRequired, 113 serviceStatus: MobxPropTypes.arrayOrObservableArray.isRequired,
58 devRecipesCount: PropTypes.number.isRequired,
59 searchNeedle: PropTypes.string, 114 searchNeedle: PropTypes.string,
115 recipeFilter: PropTypes.string,
116 recipeDirectory: PropTypes.string.isRequired,
117 openRecipeDirectory: PropTypes.func.isRequired,
118 openDevDocs: PropTypes.func.isRequired,
119 classes: PropTypes.object.isRequired,
120 isCommunityRecipesPremiumFeature: PropTypes.bool.isRequired,
121 isUserPremiumUser: PropTypes.bool.isRequired,
60 }; 122 };
61 123
62 static defaultProps = { 124 static defaultProps = {
63 searchNeedle: '', 125 searchNeedle: '',
126 recipeFilter: 'all',
64 } 127 }
65 128
66 static contextTypes = { 129 static contextTypes = {
@@ -76,11 +139,21 @@ export default @observer class RecipesDashboard extends Component {
76 searchRecipes, 139 searchRecipes,
77 resetSearch, 140 resetSearch,
78 serviceStatus, 141 serviceStatus,
79 devRecipesCount,
80 searchNeedle, 142 searchNeedle,
143 recipeFilter,
144 recipeDirectory,
145 openRecipeDirectory,
146 openDevDocs,
147 classes,
148 isCommunityRecipesPremiumFeature,
149 isUserPremiumUser,
81 } = this.props; 150 } = this.props;
82 const { intl } = this.context; 151 const { intl } = this.context;
83 152
153
154 const communityRecipes = recipes.filter(r => !r.isDevRecipe);
155 const devRecipes = recipes.filter(r => r.isDevRecipe);
156
84 return ( 157 return (
85 <div className="settings__main"> 158 <div className="settings__main">
86 <div className="settings__header"> 159 <div className="settings__header">
@@ -122,20 +195,14 @@ export default @observer class RecipesDashboard extends Component {
122 > 195 >
123 {intl.formatMessage(messages.allRecipes)} 196 {intl.formatMessage(messages.allRecipes)}
124 </Link> 197 </Link>
125 {devRecipesCount > 0 && ( 198 <Link
126 <Link 199 to="/settings/recipes/dev"
127 to="/settings/recipes/dev" 200 className="badge"
128 className="badge" 201 activeClassName={`${!searchNeedle ? 'badge--primary' : ''}`}
129 activeClassName={`${!searchNeedle ? 'badge--primary' : ''}`} 202 onClick={() => resetSearch()}
130 onClick={() => resetSearch()} 203 >
131 > 204 {intl.formatMessage(messages.customRecipes)}
132 {intl.formatMessage(messages.devRecipes)} 205 </Link>
133 {' '}
134(
135 {devRecipesCount}
136)
137 </Link>
138 )}
139 <a href={FRANZ_SERVICE_REQUEST} target="_blank" className="link recipes__service-request"> 206 <a href={FRANZ_SERVICE_REQUEST} target="_blank" className="link recipes__service-request">
140 {intl.formatMessage(messages.missingService)} 207 {intl.formatMessage(messages.missingService)}
141 {' '} 208 {' '}
@@ -146,23 +213,78 @@ export default @observer class RecipesDashboard extends Component {
146 {isLoading ? ( 213 {isLoading ? (
147 <Loader /> 214 <Loader />
148 ) : ( 215 ) : (
149 <div className="recipes__list"> 216 <>
150 {hasLoadedRecipes && recipes.length === 0 && ( 217 {recipeFilter === 'dev' && (
151 <p className="align-middle settings__empty-state"> 218 <>
152 <span className="emoji"> 219 <H2>
153 <img src="./assets/images/emoji/dontknow.png" alt="" /> 220 {intl.formatMessage(messages.headlineCustomRecipes)}
154 </span> 221 {!isUserPremiumUser && (
155 {intl.formatMessage(messages.nothingFound)} 222 <ProBadge className={classes.proBadge} />
156 </p> 223 )}
224 </H2>
225 <div className={classes.devRecipeIntroContainer}>
226 <p>
227 {intl.formatMessage(messages.customRecipeIntro)}
228 </p>
229 <Input
230 value={recipeDirectory}
231 className={classes.path}
232 showLabel={false}
233 />
234 <div className={classes.actionContainer}>
235 <Button
236 onClick={openRecipeDirectory}
237 buttonType="secondary"
238 label={intl.formatMessage(messages.openFolder)}
239 />
240 <Button
241 onClick={openDevDocs}
242 buttonType="secondary"
243 label={intl.formatMessage(messages.openDevDocs)}
244 />
245 </div>
246 </div>
247 </>
248 )}
249 <PremiumFeatureContainer
250 condition={(recipeFilter === 'dev' && communityRecipes.length > 0) && isCommunityRecipesPremiumFeature}
251 >
252 {recipeFilter === 'dev' && communityRecipes.length > 0 && (
253 <H3>{intl.formatMessage(messages.headlineCommunityRecipes)}</H3>
254 )}
255 <div className="recipes__list">
256 {hasLoadedRecipes && recipes.length === 0 && recipeFilter !== 'dev'(
257 <p className="align-middle settings__empty-state">
258 <span className="emoji">
259 <img src="./assets/images/emoji/dontknow.png" alt="" />
260 </span>
261 {intl.formatMessage(messages.nothingFound)}
262 </p>,
263 )}
264 {communityRecipes.map(recipe => (
265 <RecipeItem
266 key={recipe.id}
267 recipe={recipe}
268 onClick={() => showAddServiceInterface({ recipeId: recipe.id })}
269 />
270 ))}
271 </div>
272 </PremiumFeatureContainer>
273 {recipeFilter === 'dev' && devRecipes.length > 0 && (
274 <div className={classes.devRecipeList}>
275 <H3>{intl.formatMessage(messages.headlineDevRecipes)}</H3>
276 <div className="recipes__list">
277 {devRecipes.map(recipe => (
278 <RecipeItem
279 key={recipe.id}
280 recipe={recipe}
281 onClick={() => showAddServiceInterface({ recipeId: recipe.id })}
282 />
283 ))}
284 </div>
285 </div>
157 )} 286 )}
158 {recipes.map(recipe => ( 287 </>
159 <RecipeItem
160 key={recipe.id}
161 recipe={recipe}
162 onClick={() => showAddServiceInterface({ recipeId: recipe.id })}
163 />
164 ))}
165 </div>
166 )} 288 )}
167 </div> 289 </div>
168 </div> 290 </div>
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 = {
67 67
68export const FRANZ_SERVICE_REQUEST = 'https://bit.ly/franz-plugin-docs'; 68export const FRANZ_SERVICE_REQUEST = 'https://bit.ly/franz-plugin-docs';
69export const FRANZ_TRANSLATION = 'https://bit.ly/franz-translate'; 69export const FRANZ_TRANSLATION = 'https://bit.ly/franz-translate';
70export const FRANZ_DEV_DOCS = 'http://bit.ly/franz-dev-hub';
70 71
71export const FILE_SYSTEM_SETTINGS_TYPES = [ 72export const FILE_SYSTEM_SETTINGS_TYPES = [
72 'app', 73 '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 @@
1import { remote, shell } from 'electron';
1import React, { Component } from 'react'; 2import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 3import PropTypes from 'prop-types';
3import { autorun } from 'mobx'; 4import { autorun } from 'mobx';
4import { inject, observer } from 'mobx-react'; 5import { inject, observer } from 'mobx-react';
6import path from 'path';
5 7
6import RecipePreviewsStore from '../../stores/RecipePreviewsStore'; 8import RecipePreviewsStore from '../../stores/RecipePreviewsStore';
7import RecipeStore from '../../stores/RecipesStore'; 9import RecipeStore from '../../stores/RecipesStore';
@@ -10,6 +12,11 @@ import UserStore from '../../stores/UserStore';
10 12
11import RecipesDashboard from '../../components/settings/recipes/RecipesDashboard'; 13import RecipesDashboard from '../../components/settings/recipes/RecipesDashboard';
12import ErrorBoundary from '../../components/util/ErrorBoundary'; 14import ErrorBoundary from '../../components/util/ErrorBoundary';
15import { FRANZ_DEV_DOCS } from '../../config';
16import { gaEvent } from '../../lib/analytics';
17import { communityRecipesStore } from '../../features/communityRecipes';
18
19const { app } = remote;
13 20
14export default @inject('stores', 'actions') @observer class RecipesScreen extends Component { 21export default @inject('stores', 'actions') @observer class RecipesScreen extends Component {
15 static propTypes = { 22 static propTypes = {
@@ -67,9 +74,16 @@ export default @inject('stores', 'actions') @observer class RecipesScreen extend
67 74
68 render() { 75 render() {
69 const { 76 const {
70 recipePreviews, recipes, services, user, 77 recipePreviews,
78 recipes,
79 services,
80 user,
71 } = this.props.stores; 81 } = this.props.stores;
72 const { showAddServiceInterface } = this.props.actions.service; 82
83 const {
84 app: appActions,
85 service: serviceActions,
86 } = this.props.actions;
73 87
74 const { filter } = this.props.params; 88 const { filter } = this.props.params;
75 let recipeFilter; 89 let recipeFilter;
@@ -77,7 +91,7 @@ export default @inject('stores', 'actions') @observer class RecipesScreen extend
77 if (filter === 'all') { 91 if (filter === 'all') {
78 recipeFilter = recipePreviews.all; 92 recipeFilter = recipePreviews.all;
79 } else if (filter === 'dev') { 93 } else if (filter === 'dev') {
80 recipeFilter = recipePreviews.dev; 94 recipeFilter = communityRecipesStore.communityRecipes;
81 } else { 95 } else {
82 recipeFilter = recipePreviews.featured; 96 recipeFilter = recipePreviews.featured;
83 } 97 }
@@ -89,6 +103,8 @@ export default @inject('stores', 'actions') @observer class RecipesScreen extend
89 || recipes.installRecipeRequest.isExecuting 103 || recipes.installRecipeRequest.isExecuting
90 || recipePreviews.searchRecipePreviewsRequest.isExecuting; 104 || recipePreviews.searchRecipePreviewsRequest.isExecuting;
91 105
106 const recipeDirectory = path.join(app.getPath('userData'), 'recipes', 'dev');
107
92 return ( 108 return (
93 <ErrorBoundary> 109 <ErrorBoundary>
94 <RecipesDashboard 110 <RecipesDashboard
@@ -97,12 +113,23 @@ export default @inject('stores', 'actions') @observer class RecipesScreen extend
97 addedServiceCount={services.all.length} 113 addedServiceCount={services.all.length}
98 isPremium={user.data.isPremium} 114 isPremium={user.data.isPremium}
99 hasLoadedRecipes={recipePreviews.featuredRecipePreviewsRequest.wasExecuted} 115 hasLoadedRecipes={recipePreviews.featuredRecipePreviewsRequest.wasExecuted}
100 showAddServiceInterface={showAddServiceInterface} 116 showAddServiceInterface={serviceActions.showAddServiceInterface}
101 searchRecipes={e => this.searchRecipes(e)} 117 searchRecipes={e => this.searchRecipes(e)}
102 resetSearch={() => this.resetSearch()} 118 resetSearch={() => this.resetSearch()}
103 searchNeedle={this.state.needle} 119 searchNeedle={this.state.needle}
104 serviceStatus={services.actionStatus} 120 serviceStatus={services.actionStatus}
105 devRecipesCount={recipePreviews.dev.length} 121 recipeFilter={filter}
122 recipeDirectory={recipeDirectory}
123 openRecipeDirectory={() => {
124 shell.openItem(recipeDirectory);
125 gaEvent('Recipe', 'open-recipe-folder', 'Open Folder');
126 }}
127 openDevDocs={() => {
128 appActions.openExternalUrl({ url: FRANZ_DEV_DOCS });
129 gaEvent('Recipe', 'open-dev-docs', 'Developer Documentation');
130 }}
131 isCommunityRecipesPremiumFeature={communityRecipesStore.isCommunityRecipesPremiumFeature}
132 isUserPremiumUser={user.isPremium}
106 /> 133 />
107 </ErrorBoundary> 134 </ErrorBoundary>
108 ); 135 );
@@ -117,6 +144,9 @@ RecipesScreen.wrappedComponent.propTypes = {
117 user: PropTypes.instanceOf(UserStore).isRequired, 144 user: PropTypes.instanceOf(UserStore).isRequired,
118 }).isRequired, 145 }).isRequired,
119 actions: PropTypes.shape({ 146 actions: PropTypes.shape({
147 app: PropTypes.shape({
148 openExternalUrl: PropTypes.func.isRequired,
149 }).isRequired,
120 service: PropTypes.shape({ 150 service: PropTypes.shape({
121 showAddServiceInterface: PropTypes.func.isRequired, 151 showAddServiceInterface: PropTypes.func.isRequired,
122 }).isRequired, 152 }).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 @@
1import { reaction } from 'mobx';
2import { CommunityRecipesStore } from './store';
3
4const debug = require('debug')('Franz:feature:communityRecipes');
5
6export const DEFAULT_SERVICE_LIMIT = 3;
7
8export const communityRecipesStore = new CommunityRecipesStore();
9
10export default function initCommunityRecipes(stores, actions) {
11 const { features } = stores;
12
13 communityRecipesStore.start(stores, actions);
14
15 // Toggle communityRecipe premium status
16 reaction(
17 () => (
18 features.features.isCommunityRecipesPremiumFeature
19 ),
20 (isPremiumFeature) => {
21 debug('Community recipes is premium feature: ', isPremiumFeature);
22 communityRecipesStore.isCommunityRecipesPremiumFeature = isPremiumFeature;
23 },
24 {
25 fireImmediately: true,
26 },
27 );
28}
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 @@
1import { computed, observable } from 'mobx';
2import { FeatureStore } from '../utils/FeatureStore';
3
4const debug = require('debug')('Franz:feature:communityRecipes:store');
5
6export class CommunityRecipesStore extends FeatureStore {
7 @observable isCommunityRecipesPremiumFeature = false;
8
9 start(stores, actions) {
10 debug('start');
11 this.stores = stores;
12 this.actions = actions;
13 }
14
15 stop() {
16 debug('stop');
17 super.stop();
18 }
19
20 @computed get communityRecipes() {
21 if (!this.stores) return [];
22
23 return this.stores.recipePreviews.dev.map((r) => {
24 r.isDevRecipe = !!r.author.find(a => a.email === this.stores.user.data.email);
25
26 return r;
27 });
28 }
29}
30
31export 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 @@
1411 "defaultMessage": "!!!Available Services", 1411 "defaultMessage": "!!!Available Services",
1412 "end": { 1412 "end": {
1413 "column": 3, 1413 "column": 3,
1414 "line": 18 1414 "line": 22
1415 }, 1415 },
1416 "file": "src/components/settings/recipes/RecipesDashboard.js", 1416 "file": "src/components/settings/recipes/RecipesDashboard.js",
1417 "id": "settings.recipes.headline", 1417 "id": "settings.recipes.headline",
1418 "start": { 1418 "start": {
1419 "column": 12, 1419 "column": 12,
1420 "line": 15 1420 "line": 19
1421 } 1421 }
1422 }, 1422 },
1423 { 1423 {
1424 "defaultMessage": "!!!Search service", 1424 "defaultMessage": "!!!Search service",
1425 "end": { 1425 "end": {
1426 "column": 3, 1426 "column": 3,
1427 "line": 22 1427 "line": 26
1428 }, 1428 },
1429 "file": "src/components/settings/recipes/RecipesDashboard.js", 1429 "file": "src/components/settings/recipes/RecipesDashboard.js",
1430 "id": "settings.searchService", 1430 "id": "settings.searchService",
1431 "start": { 1431 "start": {
1432 "column": 17, 1432 "column": 17,
1433 "line": 19 1433 "line": 23
1434 } 1434 }
1435 }, 1435 },
1436 { 1436 {
1437 "defaultMessage": "!!!Most popular", 1437 "defaultMessage": "!!!Most popular",
1438 "end": { 1438 "end": {
1439 "column": 3, 1439 "column": 3,
1440 "line": 26 1440 "line": 30
1441 }, 1441 },
1442 "file": "src/components/settings/recipes/RecipesDashboard.js", 1442 "file": "src/components/settings/recipes/RecipesDashboard.js",
1443 "id": "settings.recipes.mostPopular", 1443 "id": "settings.recipes.mostPopular",
1444 "start": { 1444 "start": {
1445 "column": 22, 1445 "column": 22,
1446 "line": 23 1446 "line": 27
1447 } 1447 }
1448 }, 1448 },
1449 { 1449 {
1450 "defaultMessage": "!!!All services", 1450 "defaultMessage": "!!!All services",
1451 "end": { 1451 "end": {
1452 "column": 3, 1452 "column": 3,
1453 "line": 30 1453 "line": 34
1454 }, 1454 },
1455 "file": "src/components/settings/recipes/RecipesDashboard.js", 1455 "file": "src/components/settings/recipes/RecipesDashboard.js",
1456 "id": "settings.recipes.all", 1456 "id": "settings.recipes.all",
1457 "start": { 1457 "start": {
1458 "column": 14, 1458 "column": 14,
1459 "line": 27 1459 "line": 31
1460 } 1460 }
1461 }, 1461 },
1462 { 1462 {
1463 "defaultMessage": "!!!Development", 1463 "defaultMessage": "!!!Custom Services",
1464 "end": { 1464 "end": {
1465 "column": 3, 1465 "column": 3,
1466 "line": 34 1466 "line": 38
1467 }, 1467 },
1468 "file": "src/components/settings/recipes/RecipesDashboard.js", 1468 "file": "src/components/settings/recipes/RecipesDashboard.js",
1469 "id": "settings.recipes.dev", 1469 "id": "settings.recipes.custom",
1470 "start": { 1470 "start": {
1471 "column": 14, 1471 "column": 17,
1472 "line": 31 1472 "line": 35
1473 } 1473 }
1474 }, 1474 },
1475 { 1475 {
1476 "defaultMessage": "!!!Sorry, but no service matched your search term.", 1476 "defaultMessage": "!!!Sorry, but no service matched your search term.",
1477 "end": { 1477 "end": {
1478 "column": 3, 1478 "column": 3,
1479 "line": 38 1479 "line": 42
1480 }, 1480 },
1481 "file": "src/components/settings/recipes/RecipesDashboard.js", 1481 "file": "src/components/settings/recipes/RecipesDashboard.js",
1482 "id": "settings.recipes.nothingFound", 1482 "id": "settings.recipes.nothingFound",
1483 "start": { 1483 "start": {
1484 "column": 16, 1484 "column": 16,
1485 "line": 35 1485 "line": 39
1486 } 1486 }
1487 }, 1487 },
1488 { 1488 {
1489 "defaultMessage": "!!!Service successfully added", 1489 "defaultMessage": "!!!Service successfully added",
1490 "end": { 1490 "end": {
1491 "column": 3, 1491 "column": 3,
1492 "line": 42 1492 "line": 46
1493 }, 1493 },
1494 "file": "src/components/settings/recipes/RecipesDashboard.js", 1494 "file": "src/components/settings/recipes/RecipesDashboard.js",
1495 "id": "settings.recipes.servicesSuccessfulAddedInfo", 1495 "id": "settings.recipes.servicesSuccessfulAddedInfo",
1496 "start": { 1496 "start": {
1497 "column": 31, 1497 "column": 31,
1498 "line": 39 1498 "line": 43
1499 } 1499 }
1500 }, 1500 },
1501 { 1501 {
1502 "defaultMessage": "!!!Missing a service?", 1502 "defaultMessage": "!!!Missing a service?",
1503 "end": { 1503 "end": {
1504 "column": 3, 1504 "column": 3,
1505 "line": 46 1505 "line": 50
1506 }, 1506 },
1507 "file": "src/components/settings/recipes/RecipesDashboard.js", 1507 "file": "src/components/settings/recipes/RecipesDashboard.js",
1508 "id": "settings.recipes.missingService", 1508 "id": "settings.recipes.missingService",
1509 "start": { 1509 "start": {
1510 "column": 18, 1510 "column": 18,
1511 "line": 43 1511 "line": 47
1512 }
1513 },
1514 {
1515 "defaultMessage": "!!!To add a custom service, copy the recipe folder into:",
1516 "end": {
1517 "column": 3,
1518 "line": 54
1519 },
1520 "file": "src/components/settings/recipes/RecipesDashboard.js",
1521 "id": "settings.recipes.customService.intro",
1522 "start": {
1523 "column": 21,
1524 "line": 51
1525 }
1526 },
1527 {
1528 "defaultMessage": "!!!Open directory",
1529 "end": {
1530 "column": 3,
1531 "line": 58
1532 },
1533 "file": "src/components/settings/recipes/RecipesDashboard.js",
1534 "id": "settings.recipes.customService.openFolder",
1535 "start": {
1536 "column": 14,
1537 "line": 55
1538 }
1539 },
1540 {
1541 "defaultMessage": "!!!Developer Documentation",
1542 "end": {
1543 "column": 3,
1544 "line": 62
1545 },
1546 "file": "src/components/settings/recipes/RecipesDashboard.js",
1547 "id": "settings.recipes.customService.openDevDocs",
1548 "start": {
1549 "column": 15,
1550 "line": 59
1551 }
1552 },
1553 {
1554 "defaultMessage": "!!!Custom Service Recipes",
1555 "end": {
1556 "column": 3,
1557 "line": 66
1558 },
1559 "file": "src/components/settings/recipes/RecipesDashboard.js",
1560 "id": "settings.recipes.customService.headline.customRecipes",
1561 "start": {
1562 "column": 25,
1563 "line": 63
1564 }
1565 },
1566 {
1567 "defaultMessage": "!!!Community Services",
1568 "end": {
1569 "column": 3,
1570 "line": 70
1571 },
1572 "file": "src/components/settings/recipes/RecipesDashboard.js",
1573 "id": "settings.recipes.customService.headline.communityRecipes",
1574 "start": {
1575 "column": 28,
1576 "line": 67
1577 }
1578 },
1579 {
1580 "defaultMessage": "!!!Your Development Service Recipes",
1581 "end": {
1582 "column": 3,
1583 "line": 74
1584 },
1585 "file": "src/components/settings/recipes/RecipesDashboard.js",
1586 "id": "settings.recipes.customService.headline.devRecipes",
1587 "start": {
1588 "column": 22,
1589 "line": 71
1512 } 1590 }
1513 } 1591 }
1514 ], 1592 ],
@@ -3222,6 +3300,37 @@
3222 { 3300 {
3223 "descriptors": [ 3301 "descriptors": [
3224 { 3302 {
3303 "defaultMessage": "!!!You have added {amount} of {limit} services. Please upgrade your account to add more services.",
3304 "end": {
3305 "column": 3,
3306 "line": 14
3307 },
3308 "file": "src/features/communityRecipes/components/LimitReachedInfobox.js",
3309 "id": "feature.serviceLimit.limitReached",
3310 "start": {
3311 "column": 16,
3312 "line": 11
3313 }
3314 },
3315 {
3316 "defaultMessage": "!!!Upgrade account",
3317 "end": {
3318 "column": 3,
3319 "line": 18
3320 },
3321 "file": "src/features/communityRecipes/components/LimitReachedInfobox.js",
3322 "id": "premiumFeature.button.upgradeAccount",
3323 "start": {
3324 "column": 10,
3325 "line": 15
3326 }
3327 }
3328 ],
3329 "path": "src/features/communityRecipes/components/LimitReachedInfobox.json"
3330 },
3331 {
3332 "descriptors": [
3333 {
3225 "defaultMessage": "!!!Please purchase license to skip waiting", 3334 "defaultMessage": "!!!Please purchase license to skip waiting",
3226 "end": { 3335 "end": {
3227 "column": 3, 3336 "column": 3,
@@ -3362,6 +3471,37 @@
3362 { 3471 {
3363 "descriptors": [ 3472 "descriptors": [
3364 { 3473 {
3474 "defaultMessage": "!!!You have added {amount} of {limit} services. Please upgrade your account to add more services.",
3475 "end": {
3476 "column": 3,
3477 "line": 14
3478 },
3479 "file": "src/features/unofficialRecipes/components/LimitReachedInfobox.js",
3480 "id": "feature.serviceLimit.limitReached",
3481 "start": {
3482 "column": 16,
3483 "line": 11
3484 }
3485 },
3486 {
3487 "defaultMessage": "!!!Upgrade account",
3488 "end": {
3489 "column": 3,
3490 "line": 18
3491 },
3492 "file": "src/features/unofficialRecipes/components/LimitReachedInfobox.js",
3493 "id": "premiumFeature.button.upgradeAccount",
3494 "start": {
3495 "column": 10,
3496 "line": 15
3497 }
3498 }
3499 ],
3500 "path": "src/features/unofficialRecipes/components/LimitReachedInfobox.json"
3501 },
3502 {
3503 "descriptors": [
3504 {
3365 "defaultMessage": "!!!Create workspace", 3505 "defaultMessage": "!!!Create workspace",
3366 "end": { 3506 "end": {
3367 "column": 3, 3507 "column": 3,
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 @@
181 "settings.navigation.yourServices": "Your services", 181 "settings.navigation.yourServices": "Your services",
182 "settings.navigation.yourWorkspaces": "Your workspaces", 182 "settings.navigation.yourWorkspaces": "Your workspaces",
183 "settings.recipes.all": "All services", 183 "settings.recipes.all": "All services",
184 "settings.recipes.dev": "Development", 184 "settings.recipes.custom": "Custom Services",
185 "settings.recipes.customService.headline.communityRecipes": "Community Services",
186 "settings.recipes.customService.headline.customRecipes": "Custom Service Recipes",
187 "settings.recipes.customService.headline.devRecipes": "Your Development Service Recipes",
188 "settings.recipes.customService.intro": "To add a custom service, copy the service recipe to:",
189 "settings.recipes.customService.openDevDocs": "Developer Documentation",
190 "settings.recipes.customService.openFolder": "Open folder",
185 "settings.recipes.headline": "Available services", 191 "settings.recipes.headline": "Available services",
186 "settings.recipes.missingService": "Missing a service?", 192 "settings.recipes.missingService": "Missing a service?",
187 "settings.recipes.mostPopular": "Most popular", 193 "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 @@
4 "defaultMessage": "!!!Available Services", 4 "defaultMessage": "!!!Available Services",
5 "file": "src/components/settings/recipes/RecipesDashboard.js", 5 "file": "src/components/settings/recipes/RecipesDashboard.js",
6 "start": { 6 "start": {
7 "line": 15, 7 "line": 19,
8 "column": 12 8 "column": 12
9 }, 9 },
10 "end": { 10 "end": {
11 "line": 18, 11 "line": 22,
12 "column": 3 12 "column": 3
13 } 13 }
14 }, 14 },
@@ -17,11 +17,11 @@
17 "defaultMessage": "!!!Search service", 17 "defaultMessage": "!!!Search service",
18 "file": "src/components/settings/recipes/RecipesDashboard.js", 18 "file": "src/components/settings/recipes/RecipesDashboard.js",
19 "start": { 19 "start": {
20 "line": 19, 20 "line": 23,
21 "column": 17 21 "column": 17
22 }, 22 },
23 "end": { 23 "end": {
24 "line": 22, 24 "line": 26,
25 "column": 3 25 "column": 3
26 } 26 }
27 }, 27 },
@@ -30,11 +30,11 @@
30 "defaultMessage": "!!!Most popular", 30 "defaultMessage": "!!!Most popular",
31 "file": "src/components/settings/recipes/RecipesDashboard.js", 31 "file": "src/components/settings/recipes/RecipesDashboard.js",
32 "start": { 32 "start": {
33 "line": 23, 33 "line": 27,
34 "column": 22 34 "column": 22
35 }, 35 },
36 "end": { 36 "end": {
37 "line": 26, 37 "line": 30,
38 "column": 3 38 "column": 3
39 } 39 }
40 }, 40 },
@@ -43,24 +43,24 @@
43 "defaultMessage": "!!!All services", 43 "defaultMessage": "!!!All services",
44 "file": "src/components/settings/recipes/RecipesDashboard.js", 44 "file": "src/components/settings/recipes/RecipesDashboard.js",
45 "start": { 45 "start": {
46 "line": 27, 46 "line": 31,
47 "column": 14 47 "column": 14
48 }, 48 },
49 "end": { 49 "end": {
50 "line": 30, 50 "line": 34,
51 "column": 3 51 "column": 3
52 } 52 }
53 }, 53 },
54 { 54 {
55 "id": "settings.recipes.dev", 55 "id": "settings.recipes.custom",
56 "defaultMessage": "!!!Development", 56 "defaultMessage": "!!!Custom Services",
57 "file": "src/components/settings/recipes/RecipesDashboard.js", 57 "file": "src/components/settings/recipes/RecipesDashboard.js",
58 "start": { 58 "start": {
59 "line": 31, 59 "line": 35,
60 "column": 14 60 "column": 17
61 }, 61 },
62 "end": { 62 "end": {
63 "line": 34, 63 "line": 38,
64 "column": 3 64 "column": 3
65 } 65 }
66 }, 66 },
@@ -69,11 +69,11 @@
69 "defaultMessage": "!!!Sorry, but no service matched your search term.", 69 "defaultMessage": "!!!Sorry, but no service matched your search term.",
70 "file": "src/components/settings/recipes/RecipesDashboard.js", 70 "file": "src/components/settings/recipes/RecipesDashboard.js",
71 "start": { 71 "start": {
72 "line": 35, 72 "line": 39,
73 "column": 16 73 "column": 16
74 }, 74 },
75 "end": { 75 "end": {
76 "line": 38, 76 "line": 42,
77 "column": 3 77 "column": 3
78 } 78 }
79 }, 79 },
@@ -82,11 +82,11 @@
82 "defaultMessage": "!!!Service successfully added", 82 "defaultMessage": "!!!Service successfully added",
83 "file": "src/components/settings/recipes/RecipesDashboard.js", 83 "file": "src/components/settings/recipes/RecipesDashboard.js",
84 "start": { 84 "start": {
85 "line": 39, 85 "line": 43,
86 "column": 31 86 "column": 31
87 }, 87 },
88 "end": { 88 "end": {
89 "line": 42, 89 "line": 46,
90 "column": 3 90 "column": 3
91 } 91 }
92 }, 92 },
@@ -95,11 +95,89 @@
95 "defaultMessage": "!!!Missing a service?", 95 "defaultMessage": "!!!Missing a service?",
96 "file": "src/components/settings/recipes/RecipesDashboard.js", 96 "file": "src/components/settings/recipes/RecipesDashboard.js",
97 "start": { 97 "start": {
98 "line": 43, 98 "line": 47,
99 "column": 18 99 "column": 18
100 }, 100 },
101 "end": { 101 "end": {
102 "line": 46, 102 "line": 50,
103 "column": 3
104 }
105 },
106 {
107 "id": "settings.recipes.customService.intro",
108 "defaultMessage": "!!!To add a custom service, copy the recipe folder into:",
109 "file": "src/components/settings/recipes/RecipesDashboard.js",
110 "start": {
111 "line": 51,
112 "column": 21
113 },
114 "end": {
115 "line": 54,
116 "column": 3
117 }
118 },
119 {
120 "id": "settings.recipes.customService.openFolder",
121 "defaultMessage": "!!!Open directory",
122 "file": "src/components/settings/recipes/RecipesDashboard.js",
123 "start": {
124 "line": 55,
125 "column": 14
126 },
127 "end": {
128 "line": 58,
129 "column": 3
130 }
131 },
132 {
133 "id": "settings.recipes.customService.openDevDocs",
134 "defaultMessage": "!!!Developer Documentation",
135 "file": "src/components/settings/recipes/RecipesDashboard.js",
136 "start": {
137 "line": 59,
138 "column": 15
139 },
140 "end": {
141 "line": 62,
142 "column": 3
143 }
144 },
145 {
146 "id": "settings.recipes.customService.headline.customRecipes",
147 "defaultMessage": "!!!Custom Service Recipes",
148 "file": "src/components/settings/recipes/RecipesDashboard.js",
149 "start": {
150 "line": 63,
151 "column": 25
152 },
153 "end": {
154 "line": 66,
155 "column": 3
156 }
157 },
158 {
159 "id": "settings.recipes.customService.headline.communityRecipes",
160 "defaultMessage": "!!!Community Services",
161 "file": "src/components/settings/recipes/RecipesDashboard.js",
162 "start": {
163 "line": 67,
164 "column": 28
165 },
166 "end": {
167 "line": 70,
168 "column": 3
169 }
170 },
171 {
172 "id": "settings.recipes.customService.headline.devRecipes",
173 "defaultMessage": "!!!Your Development Service Recipes",
174 "file": "src/components/settings/recipes/RecipesDashboard.js",
175 "start": {
176 "line": 71,
177 "column": 22
178 },
179 "end": {
180 "line": 74,
103 "column": 3 181 "column": 3
104 } 182 }
105 } 183 }
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';
16import shareFranz from '../features/shareFranz'; 16import shareFranz from '../features/shareFranz';
17import announcements from '../features/announcements'; 17import announcements from '../features/announcements';
18import settingsWS from '../features/settingsWS'; 18import settingsWS from '../features/settingsWS';
19import communityRecipes from '../features/communityRecipes';
19 20
20import { DEFAULT_FEATURES_CONFIG } from '../config'; 21import { DEFAULT_FEATURES_CONFIG } from '../config';
21 22
@@ -75,5 +76,6 @@ export default class FeaturesStore extends Store {
75 shareFranz(this.stores, this.actions); 76 shareFranz(this.stores, this.actions);
76 announcements(this.stores, this.actions); 77 announcements(this.stores, this.actions);
77 settingsWS(this.stores, this.actions); 78 settingsWS(this.stores, this.actions);
79 communityRecipes(this.stores, this.actions);
78 } 80 }
79} 81}
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';
12import GlobalErrorStore from './GlobalErrorStore'; 12import GlobalErrorStore from './GlobalErrorStore';
13import { workspaceStore } from '../features/workspaces'; 13import { workspaceStore } from '../features/workspaces';
14import { announcementsStore } from '../features/announcements'; 14import { announcementsStore } from '../features/announcements';
15import { communityRecipesStore } from '../features/communityRecipes';
15 16
16export default (api, actions, router) => { 17export default (api, actions, router) => {
17 const stores = {}; 18 const stores = {};
@@ -31,6 +32,7 @@ export default (api, actions, router) => {
31 globalError: new GlobalErrorStore(stores, api, actions), 32 globalError: new GlobalErrorStore(stores, api, actions),
32 workspaces: workspaceStore, 33 workspaces: workspaceStore,
33 announcements: announcementsStore, 34 announcements: announcementsStore,
35 communityRecipes: communityRecipesStore,
34 }); 36 });
35 // Initialize all stores 37 // Initialize all stores
36 Object.keys(stores).forEach((name) => { 38 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 @@
12 display: flex; 12 display: flex;
13 flex-flow: row wrap; 13 flex-flow: row wrap;
14 height: auto; 14 height: auto;
15 min-height: 70%; 15 // min-height: 70%;
16 16
17 &.recipes__list--disabled { 17 &.recipes__list--disabled {
18 filter: grayscale(100%); 18 filter: grayscale(100%);