aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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.js192
-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.json116
-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, 462 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 862bd4a27..ed4a429db 100644
--- a/src/components/settings/recipes/RecipesDashboard.js
+++ b/src/components/settings/recipes/RecipesDashboard.js
@@ -4,6 +4,9 @@ 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';
@@ -11,6 +14,7 @@ import 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';
13import LimitReachedInfobox from '../../../features/serviceLimit/components/LimitReachedInfobox'; 16import LimitReachedInfobox from '../../../features/serviceLimit/components/LimitReachedInfobox';
17import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer';
14 18
15const messages = defineMessages({ 19const messages = defineMessages({
16 headline: { 20 headline: {
@@ -29,9 +33,9 @@ const messages = defineMessages({
29 id: 'settings.recipes.all', 33 id: 'settings.recipes.all',
30 defaultMessage: '!!!All services', 34 defaultMessage: '!!!All services',
31 }, 35 },
32 devRecipes: { 36 customRecipes: {
33 id: 'settings.recipes.dev', 37 id: 'settings.recipes.custom',
34 defaultMessage: '!!!Development', 38 defaultMessage: '!!!Custom Services',
35 }, 39 },
36 nothingFound: { 40 nothingFound: {
37 id: 'settings.recipes.nothingFound', 41 id: 'settings.recipes.nothingFound',
@@ -45,9 +49,61 @@ const messages = defineMessages({
45 id: 'settings.recipes.missingService', 49 id: 'settings.recipes.missingService',
46 defaultMessage: '!!!Missing a service?', 50 defaultMessage: '!!!Missing a service?',
47 }, 51 },
52 customRecipeIntro: {
53 id: 'settings.recipes.customService.intro',
54 defaultMessage: '!!!To add a custom service, copy the recipe folder into:',
55 },
56 openFolder: {
57 id: 'settings.recipes.customService.openFolder',
58 defaultMessage: '!!!Open directory',
59 },
60 openDevDocs: {
61 id: 'settings.recipes.customService.openDevDocs',
62 defaultMessage: '!!!Developer Documentation',
63 },
64 headlineCustomRecipes: {
65 id: 'settings.recipes.customService.headline.customRecipes',
66 defaultMessage: '!!!Custom Service Recipes',
67 },
68 headlineCommunityRecipes: {
69 id: 'settings.recipes.customService.headline.communityRecipes',
70 defaultMessage: '!!!Community Services',
71 },
72 headlineDevRecipes: {
73 id: 'settings.recipes.customService.headline.devRecipes',
74 defaultMessage: '!!!Your Development Service Recipes',
75 },
48}); 76});
49 77
50export default @observer class RecipesDashboard extends Component { 78const styles = {
79 devRecipeIntroContainer: {
80 textAlign: 'center',
81 width: '100%',
82 height: 'auto',
83 margin: [40, 0],
84 },
85 path: {
86 marginTop: 20,
87
88 '& > div': {
89 fontFamily: 'SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace',
90 },
91 },
92 actionContainer: {
93 '& button': {
94 margin: [0, 10],
95 },
96 },
97 devRecipeList: {
98 marginTop: 20,
99 height: 'auto',
100 },
101 proBadge: {
102 marginLeft: '10px !important',
103 },
104};
105
106export default @injectSheet(styles) @observer class RecipesDashboard extends Component {
51 static propTypes = { 107 static propTypes = {
52 recipes: MobxPropTypes.arrayOrObservableArray.isRequired, 108 recipes: MobxPropTypes.arrayOrObservableArray.isRequired,
53 isLoading: PropTypes.bool.isRequired, 109 isLoading: PropTypes.bool.isRequired,
@@ -56,12 +112,18 @@ export default @observer class RecipesDashboard extends Component {
56 searchRecipes: PropTypes.func.isRequired, 112 searchRecipes: PropTypes.func.isRequired,
57 resetSearch: PropTypes.func.isRequired, 113 resetSearch: PropTypes.func.isRequired,
58 serviceStatus: MobxPropTypes.arrayOrObservableArray.isRequired, 114 serviceStatus: MobxPropTypes.arrayOrObservableArray.isRequired,
59 devRecipesCount: PropTypes.number.isRequired,
60 searchNeedle: PropTypes.string, 115 searchNeedle: PropTypes.string,
116 recipeFilter: PropTypes.string,
117 recipeDirectory: PropTypes.string.isRequired,
118 openRecipeDirectory: PropTypes.func.isRequired,
119 openDevDocs: PropTypes.func.isRequired,
120 classes: PropTypes.object.isRequired,
121 isCommunityRecipesPremiumFeature: PropTypes.bool.isRequired,
61 }; 122 };
62 123
63 static defaultProps = { 124 static defaultProps = {
64 searchNeedle: '', 125 searchNeedle: '',
126 recipeFilter: 'all',
65 } 127 }
66 128
67 static contextTypes = { 129 static contextTypes = {
@@ -77,11 +139,20 @@ export default @observer class RecipesDashboard extends Component {
77 searchRecipes, 139 searchRecipes,
78 resetSearch, 140 resetSearch,
79 serviceStatus, 141 serviceStatus,
80 devRecipesCount,
81 searchNeedle, 142 searchNeedle,
143 recipeFilter,
144 recipeDirectory,
145 openRecipeDirectory,
146 openDevDocs,
147 classes,
148 isCommunityRecipesPremiumFeature,
82 } = this.props; 149 } = this.props;
83 const { intl } = this.context; 150 const { intl } = this.context;
84 151
152
153 const communityRecipes = recipes.filter(r => !r.isDevRecipe);
154 const devRecipes = recipes.filter(r => r.isDevRecipe);
155
85 return ( 156 return (
86 <div className="settings__main"> 157 <div className="settings__main">
87 <div className="settings__header"> 158 <div className="settings__header">
@@ -124,20 +195,14 @@ export default @observer class RecipesDashboard extends Component {
124 > 195 >
125 {intl.formatMessage(messages.allRecipes)} 196 {intl.formatMessage(messages.allRecipes)}
126 </Link> 197 </Link>
127 {devRecipesCount > 0 && ( 198 <Link
128 <Link 199 to="/settings/recipes/dev"
129 to="/settings/recipes/dev" 200 className="badge"
130 className="badge" 201 activeClassName={`${!searchNeedle ? 'badge--primary' : ''}`}
131 activeClassName={`${!searchNeedle ? 'badge--primary' : ''}`} 202 onClick={() => resetSearch()}
132 onClick={() => resetSearch()} 203 >
133 > 204 {intl.formatMessage(messages.customRecipes)}
134 {intl.formatMessage(messages.devRecipes)} 205 </Link>
135 {' '}
136(
137 {devRecipesCount}
138)
139 </Link>
140 )}
141 <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">
142 {intl.formatMessage(messages.missingService)} 207 {intl.formatMessage(messages.missingService)}
143 {' '} 208 {' '}
@@ -148,23 +213,78 @@ export default @observer class RecipesDashboard extends Component {
148 {isLoading ? ( 213 {isLoading ? (
149 <Loader /> 214 <Loader />
150 ) : ( 215 ) : (
151 <div className="recipes__list"> 216 <>
152 {hasLoadedRecipes && recipes.length === 0 && ( 217 {recipeFilter === 'dev' && (
153 <p className="align-middle settings__empty-state"> 218 <>
154 <span className="emoji"> 219 <H2>
155 <img src="./assets/images/emoji/dontknow.png" alt="" /> 220 {intl.formatMessage(messages.headlineCustomRecipes)}
156 </span> 221 {isCommunityRecipesPremiumFeature && (
157 {intl.formatMessage(messages.nothingFound)} 222 <ProBadge className={classes.proBadge} />
158 </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>
159 )} 286 )}
160 {recipes.map(recipe => ( 287 </>
161 <RecipeItem
162 key={recipe.id}
163 recipe={recipe}
164 onClick={() => showAddServiceInterface({ recipeId: recipe.id })}
165 />
166 ))}
167 </div>
168 )} 288 )}
169 </div> 289 </div>
170 </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 c2363923f..3d11a372d 100644
--- a/src/i18n/locales/defaultMessages.json
+++ b/src/i18n/locales/defaultMessages.json
@@ -1486,104 +1486,182 @@
1486 "defaultMessage": "!!!Available Services", 1486 "defaultMessage": "!!!Available Services",
1487 "end": { 1487 "end": {
1488 "column": 3, 1488 "column": 3,
1489 "line": 19 1489 "line": 23
1490 }, 1490 },
1491 "file": "src/components/settings/recipes/RecipesDashboard.js", 1491 "file": "src/components/settings/recipes/RecipesDashboard.js",
1492 "id": "settings.recipes.headline", 1492 "id": "settings.recipes.headline",
1493 "start": { 1493 "start": {
1494 "column": 12, 1494 "column": 12,
1495 "line": 16 1495 "line": 20
1496 } 1496 }
1497 }, 1497 },
1498 { 1498 {
1499 "defaultMessage": "!!!Search service", 1499 "defaultMessage": "!!!Search service",
1500 "end": { 1500 "end": {
1501 "column": 3, 1501 "column": 3,
1502 "line": 23 1502 "line": 27
1503 }, 1503 },
1504 "file": "src/components/settings/recipes/RecipesDashboard.js", 1504 "file": "src/components/settings/recipes/RecipesDashboard.js",
1505 "id": "settings.searchService", 1505 "id": "settings.searchService",
1506 "start": { 1506 "start": {
1507 "column": 17, 1507 "column": 17,
1508 "line": 20 1508 "line": 24
1509 } 1509 }
1510 }, 1510 },
1511 { 1511 {
1512 "defaultMessage": "!!!Most popular", 1512 "defaultMessage": "!!!Most popular",
1513 "end": { 1513 "end": {
1514 "column": 3, 1514 "column": 3,
1515 "line": 27 1515 "line": 31
1516 }, 1516 },
1517 "file": "src/components/settings/recipes/RecipesDashboard.js", 1517 "file": "src/components/settings/recipes/RecipesDashboard.js",
1518 "id": "settings.recipes.mostPopular", 1518 "id": "settings.recipes.mostPopular",
1519 "start": { 1519 "start": {
1520 "column": 22, 1520 "column": 22,
1521 "line": 24 1521 "line": 28
1522 } 1522 }
1523 }, 1523 },
1524 { 1524 {
1525 "defaultMessage": "!!!All services", 1525 "defaultMessage": "!!!All services",
1526 "end": { 1526 "end": {
1527 "column": 3, 1527 "column": 3,
1528 "line": 31 1528 "line": 35
1529 }, 1529 },
1530 "file": "src/components/settings/recipes/RecipesDashboard.js", 1530 "file": "src/components/settings/recipes/RecipesDashboard.js",
1531 "id": "settings.recipes.all", 1531 "id": "settings.recipes.all",
1532 "start": { 1532 "start": {
1533 "column": 14, 1533 "column": 14,
1534 "line": 28 1534 "line": 32
1535 } 1535 }
1536 }, 1536 },
1537 { 1537 {
1538 "defaultMessage": "!!!Development", 1538 "defaultMessage": "!!!Custom Services",
1539 "end": { 1539 "end": {
1540 "column": 3, 1540 "column": 3,
1541 "line": 35 1541 "line": 39
1542 }, 1542 },
1543 "file": "src/components/settings/recipes/RecipesDashboard.js", 1543 "file": "src/components/settings/recipes/RecipesDashboard.js",
1544 "id": "settings.recipes.dev", 1544 "id": "settings.recipes.custom",
1545 "start": { 1545 "start": {
1546 "column": 14, 1546 "column": 17,
1547 "line": 32 1547 "line": 36
1548 } 1548 }
1549 }, 1549 },
1550 { 1550 {
1551 "defaultMessage": "!!!Sorry, but no service matched your search term.", 1551 "defaultMessage": "!!!Sorry, but no service matched your search term.",
1552 "end": { 1552 "end": {
1553 "column": 3, 1553 "column": 3,
1554 "line": 39 1554 "line": 43
1555 }, 1555 },
1556 "file": "src/components/settings/recipes/RecipesDashboard.js", 1556 "file": "src/components/settings/recipes/RecipesDashboard.js",
1557 "id": "settings.recipes.nothingFound", 1557 "id": "settings.recipes.nothingFound",
1558 "start": { 1558 "start": {
1559 "column": 16, 1559 "column": 16,
1560 "line": 36 1560 "line": 40
1561 } 1561 }
1562 }, 1562 },
1563 { 1563 {
1564 "defaultMessage": "!!!Service successfully added", 1564 "defaultMessage": "!!!Service successfully added",
1565 "end": { 1565 "end": {
1566 "column": 3, 1566 "column": 3,
1567 "line": 43 1567 "line": 47
1568 }, 1568 },
1569 "file": "src/components/settings/recipes/RecipesDashboard.js", 1569 "file": "src/components/settings/recipes/RecipesDashboard.js",
1570 "id": "settings.recipes.servicesSuccessfulAddedInfo", 1570 "id": "settings.recipes.servicesSuccessfulAddedInfo",
1571 "start": { 1571 "start": {
1572 "column": 31, 1572 "column": 31,
1573 "line": 40 1573 "line": 44
1574 } 1574 }
1575 }, 1575 },
1576 { 1576 {
1577 "defaultMessage": "!!!Missing a service?", 1577 "defaultMessage": "!!!Missing a service?",
1578 "end": { 1578 "end": {
1579 "column": 3, 1579 "column": 3,
1580 "line": 47 1580 "line": 51
1581 }, 1581 },
1582 "file": "src/components/settings/recipes/RecipesDashboard.js", 1582 "file": "src/components/settings/recipes/RecipesDashboard.js",
1583 "id": "settings.recipes.missingService", 1583 "id": "settings.recipes.missingService",
1584 "start": { 1584 "start": {
1585 "column": 18, 1585 "column": 18,
1586 "line": 44 1586 "line": 48
1587 }
1588 },
1589 {
1590 "defaultMessage": "!!!To add a custom service, copy the recipe folder into:",
1591 "end": {
1592 "column": 3,
1593 "line": 55
1594 },
1595 "file": "src/components/settings/recipes/RecipesDashboard.js",
1596 "id": "settings.recipes.customService.intro",
1597 "start": {
1598 "column": 21,
1599 "line": 52
1600 }
1601 },
1602 {
1603 "defaultMessage": "!!!Open directory",
1604 "end": {
1605 "column": 3,
1606 "line": 59
1607 },
1608 "file": "src/components/settings/recipes/RecipesDashboard.js",
1609 "id": "settings.recipes.customService.openFolder",
1610 "start": {
1611 "column": 14,
1612 "line": 56
1613 }
1614 },
1615 {
1616 "defaultMessage": "!!!Developer Documentation",
1617 "end": {
1618 "column": 3,
1619 "line": 63
1620 },
1621 "file": "src/components/settings/recipes/RecipesDashboard.js",
1622 "id": "settings.recipes.customService.openDevDocs",
1623 "start": {
1624 "column": 15,
1625 "line": 60
1626 }
1627 },
1628 {
1629 "defaultMessage": "!!!Custom Service Recipes",
1630 "end": {
1631 "column": 3,
1632 "line": 67
1633 },
1634 "file": "src/components/settings/recipes/RecipesDashboard.js",
1635 "id": "settings.recipes.customService.headline.customRecipes",
1636 "start": {
1637 "column": 25,
1638 "line": 64
1639 }
1640 },
1641 {
1642 "defaultMessage": "!!!Community Services",
1643 "end": {
1644 "column": 3,
1645 "line": 71
1646 },
1647 "file": "src/components/settings/recipes/RecipesDashboard.js",
1648 "id": "settings.recipes.customService.headline.communityRecipes",
1649 "start": {
1650 "column": 28,
1651 "line": 68
1652 }
1653 },
1654 {
1655 "defaultMessage": "!!!Your Development Service Recipes",
1656 "end": {
1657 "column": 3,
1658 "line": 75
1659 },
1660 "file": "src/components/settings/recipes/RecipesDashboard.js",
1661 "id": "settings.recipes.customService.headline.devRecipes",
1662 "start": {
1663 "column": 22,
1664 "line": 72
1587 } 1665 }
1588 } 1666 }
1589 ], 1667 ],
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index e921d8958..0a8e04b21 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -188,7 +188,13 @@
188 "settings.navigation.yourServices": "Your services", 188 "settings.navigation.yourServices": "Your services",
189 "settings.navigation.yourWorkspaces": "Your workspaces", 189 "settings.navigation.yourWorkspaces": "Your workspaces",
190 "settings.recipes.all": "All services", 190 "settings.recipes.all": "All services",
191 "settings.recipes.dev": "Development", 191 "settings.recipes.custom": "Custom Services",
192 "settings.recipes.customService.headline.communityRecipes": "Community Services",
193 "settings.recipes.customService.headline.customRecipes": "Custom Service Recipes",
194 "settings.recipes.customService.headline.devRecipes": "Your Development Service Recipes",
195 "settings.recipes.customService.intro": "To add a custom service, copy the service recipe to:",
196 "settings.recipes.customService.openDevDocs": "Developer Documentation",
197 "settings.recipes.customService.openFolder": "Open folder",
192 "settings.recipes.headline": "Available services", 198 "settings.recipes.headline": "Available services",
193 "settings.recipes.missingService": "Missing a service?", 199 "settings.recipes.missingService": "Missing a service?",
194 "settings.recipes.mostPopular": "Most popular", 200 "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 07eada1dc..8afaaed50 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": 16, 7 "line": 20,
8 "column": 12 8 "column": 12
9 }, 9 },
10 "end": { 10 "end": {
11 "line": 19, 11 "line": 23,
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": 20, 20 "line": 24,
21 "column": 17 21 "column": 17
22 }, 22 },
23 "end": { 23 "end": {
24 "line": 23, 24 "line": 27,
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": 24, 33 "line": 28,
34 "column": 22 34 "column": 22
35 }, 35 },
36 "end": { 36 "end": {
37 "line": 27, 37 "line": 31,
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": 28, 46 "line": 32,
47 "column": 14 47 "column": 14
48 }, 48 },
49 "end": { 49 "end": {
50 "line": 31, 50 "line": 35,
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": 32, 59 "line": 36,
60 "column": 14 60 "column": 17
61 }, 61 },
62 "end": { 62 "end": {
63 "line": 35, 63 "line": 39,
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": 36, 72 "line": 40,
73 "column": 16 73 "column": 16
74 }, 74 },
75 "end": { 75 "end": {
76 "line": 39, 76 "line": 43,
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": 40, 85 "line": 44,
86 "column": 31 86 "column": 31
87 }, 87 },
88 "end": { 88 "end": {
89 "line": 43, 89 "line": 47,
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": 44, 98 "line": 48,
99 "column": 18 99 "column": 18
100 }, 100 },
101 "end": { 101 "end": {
102 "line": 47, 102 "line": 51,
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": 52,
112 "column": 21
113 },
114 "end": {
115 "line": 55,
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": 56,
125 "column": 14
126 },
127 "end": {
128 "line": 59,
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": 60,
138 "column": 15
139 },
140 "end": {
141 "line": 63,
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": 64,
151 "column": 25
152 },
153 "end": {
154 "line": 67,
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": 68,
164 "column": 28
165 },
166 "end": {
167 "line": 71,
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": 72,
177 "column": 22
178 },
179 "end": {
180 "line": 75,
103 "column": 3 181 "column": 3
104 } 182 }
105 } 183 }
diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js
index 1ac05d3b9..27334c9a2 100644
--- a/src/stores/FeaturesStore.js
+++ b/src/stores/FeaturesStore.js
@@ -17,6 +17,7 @@ import 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 serviceLimit from '../features/serviceLimit'; 19import serviceLimit from '../features/serviceLimit';
20import communityRecipes from '../features/communityRecipes';
20 21
21import { DEFAULT_FEATURES_CONFIG } from '../config'; 22import { DEFAULT_FEATURES_CONFIG } from '../config';
22 23
@@ -77,5 +78,6 @@ export default class FeaturesStore extends Store {
77 announcements(this.stores, this.actions); 78 announcements(this.stores, this.actions);
78 settingsWS(this.stores, this.actions); 79 settingsWS(this.stores, this.actions);
79 serviceLimit(this.stores, this.actions); 80 serviceLimit(this.stores, this.actions);
81 communityRecipes(this.stores, this.actions);
80 } 82 }
81} 83}
diff --git a/src/stores/index.js b/src/stores/index.js
index ff01a3dd3..c9ec77612 100644
--- a/src/stores/index.js
+++ b/src/stores/index.js
@@ -13,6 +13,7 @@ import GlobalErrorStore from './GlobalErrorStore';
13import { workspaceStore } from '../features/workspaces'; 13import { workspaceStore } from '../features/workspaces';
14import { announcementsStore } from '../features/announcements'; 14import { announcementsStore } from '../features/announcements';
15import { serviceLimitStore } from '../features/serviceLimit'; 15import { serviceLimitStore } from '../features/serviceLimit';
16import { communityRecipesStore } from '../features/communityRecipes';
16 17
17export default (api, actions, router) => { 18export default (api, actions, router) => {
18 const stores = {}; 19 const stores = {};
@@ -33,6 +34,7 @@ export default (api, actions, router) => {
33 workspaces: workspaceStore, 34 workspaces: workspaceStore,
34 announcements: announcementsStore, 35 announcements: announcementsStore,
35 serviceLimit: serviceLimitStore, 36 serviceLimit: serviceLimitStore,
37 communityRecipes: communityRecipesStore,
36 }); 38 });
37 // Initialize all stores 39 // Initialize all stores
38 Object.keys(stores).forEach((name) => { 40 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%);