diff --git a/package.json b/package.json
index c9a2d55..01c5551 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@shoutem/cli",
- "version": "0.13.9",
+ "version": "0.13.10",
"description": "Command-line tools for Shoutem applications",
"repository": {
"type": "git",
@@ -32,7 +32,7 @@
"colors": "1.1.2",
"command-exists": "~1.2.4",
"decamelize": "^1.2.0",
- "decompress": "4.0.0",
+ "decompress": "4.2.1",
"diff": "^3.3.1",
"download-cached": "1.0.8",
"download-file": "latest",
diff --git a/src/cli/page/add.js b/src/cli/page/add.js
index 2dc81b4..09a00be 100644
--- a/src/cli/page/add.js
+++ b/src/cli/page/add.js
@@ -1,8 +1,9 @@
+import { ensureUserIsLoggedIn } from '../../commands/login';
import { executeAndHandleError } from '../../services/error-handler';
-import {ensureInExtensionDir, loadExtensionJson} from '../../services/extension';
-import {askPageCreationQuestions} from "../../services/page";
-import {instantiateExtensionTemplate} from "../../services/extension-template";
-import {offerChanges} from "../../services/diff";
+import { ensureInExtensionDir, loadExtensionJson } from '../../services/extension';
+import { askPageCreationQuestions } from '../../services/page';
+import { instantiateExtensionTemplate } from '../../services/extension-template';
+import { offerChanges } from '../../services/diff';
export const description = 'Adds a settings page to the current extension.';
export const command = 'add [name]';
@@ -14,7 +15,9 @@ export const handler = args => executeAndHandleError(async () => {
});
export async function createPage(opts, extensionPath) {
- const changes = await instantiateExtensionTemplate('settings-page', { ...opts, extensionPath });
+ const developer = await ensureUserIsLoggedIn();
+ const changes = await instantiateExtensionTemplate('settings-page', { ...opts, extensionPath, developer });
await offerChanges(changes);
console.log('Success'.green.bold);
+ console.log('Remember to create \'server/translations/en.json\' and add your translation strings to it.\nYou can use \'server/translations/example.json\' to check the format.');
}
diff --git a/src/commands/init.js b/src/commands/init.js
index a82df1b..4dabf97 100644
--- a/src/commands/init.js
+++ b/src/commands/init.js
@@ -1,17 +1,16 @@
import _ from 'lodash';
import inquirer from 'inquirer';
import decamelize from 'decamelize';
-import { pathExists } from 'fs-extra';
+import fs from 'fs-extra';
import path from 'path';
import semver from 'semver';
-import { ensureUserIsLoggedIn } from '../commands/login';
import msg from '../user_messages';
import { getPlatforms } from '../clients/extension-manager';
import * as utils from '../services/extension';
-import {instantiateExtensionTemplate} from "../services/extension-template";
-import {offerChanges} from "../services/diff";
-import {stringify} from "../services/data";
-
+import { instantiateExtensionTemplate } from '../services/extension-template';
+import { offerChanges } from '../services/diff';
+import { stringify } from '../services/data';
+import { ensureUserIsLoggedIn } from './login';
function generateNoPatchSemver(version) {
const [a, b] = version.split('.');
@@ -54,7 +53,7 @@ export async function initExtension(extName, extensionPath = process.cwd()) {
utils.getExtensionCanonicalName(developer.name, extJson.name, extJson.version);
const dirname = `${developer.name}.${extJson.name}`;
- if (await pathExists(path.join(process.cwd(), dirname))) {
+ if (fs.existsSync(path.join(process.cwd(), dirname))) {
throw new Error(`Folder ${dirname} already exists.`);
}
diff --git a/src/services/extension-template.js b/src/services/extension-template.js
index 357ec00..b094eb5 100644
--- a/src/services/extension-template.js
+++ b/src/services/extension-template.js
@@ -1,5 +1,5 @@
-import {loadExtensionJson} from "./extension";
-import * as template from "./template";
+import { loadExtensionJson } from './extension';
+import * as template from './template';
export async function instantiateExtensionTemplate(localTemplatePath, context, opts) {
if (!context.extJson && context.extensionPath) {
@@ -11,5 +11,6 @@ export async function instantiateExtensionTemplate(localTemplatePath, context, o
}
await template.instantiateTemplatePath(localTemplatePath, context.extensionPath, context, opts);
+
return await template.instantiateTemplatePath('extension-js', context.extensionPath, context, opts);
}
diff --git a/src/services/page.js b/src/services/page.js
index cfb2878..8326882 100644
--- a/src/services/page.js
+++ b/src/services/page.js
@@ -1,8 +1,8 @@
import _ from 'lodash';
-import decamelize from "decamelize";
-import { prompt } from "inquirer";
-import {isVariableName} from "./cli-parsing";
-import { askScreenCreationQuestions } from "./screen";
+import decamelize from 'decamelize';
+import { prompt } from 'inquirer';
+import { isVariableName } from './cli-parsing';
+import { askScreenCreationQuestions } from './screen';
function validatePageName(name, existingPages) {
if (!isVariableName(name)) {
diff --git a/src/services/template.js b/src/services/template.js
index 2c46bbb..0619858 100644
--- a/src/services/template.js
+++ b/src/services/template.js
@@ -1,15 +1,15 @@
-import getOrSet from 'lodash-get-or-set';
import Promise from 'bluebird';
import fs from 'fs-extra';
-import path from 'path';
+import getOrSet from 'lodash-get-or-set';
import Mustache from 'mustache';
-import { pathExists } from 'fs-extra';
+import path from 'path';
const templatesDirectory = path.join(__dirname, '../..', 'src/templates');
export function load(pathWithSlashes, templateContext) {
const p = path.join(templatesDirectory, ...pathWithSlashes.split('/'));
const template = fs.readFileSync(p, 'utf8');
+
return Mustache.render(template, templateContext);
}
@@ -28,6 +28,7 @@ async function instantiateTemplatePathRec(localTemplatePath, destinationPath, co
await Promise.map(files, file => {
const src = path.join(localTemplatePath, file);
const dest = path.join(destinationPath, file);
+
return instantiateTemplatePathRec(src, dest, context, opts);
});
} else if (templatePathState.isFile()) {
diff --git a/src/templates/extension-js/template-init.js b/src/templates/extension-js/template-init.js
index ec8c043..f86ef38 100644
--- a/src/templates/extension-js/template-init.js
+++ b/src/templates/extension-js/template-init.js
@@ -1,6 +1,6 @@
import _ from 'lodash';
import decamelize from 'decamelize';
-import {isReactPage} from "../settings-page-react/template-init";
+import {isReactPage} from '../settings-page-react/template-init';
function importStatements(names, path, directoriesNames = names) {
return names.map((name, i) => `import ${name} from '${path}/${directoriesNames[i]}';`).join('\n');
diff --git a/src/templates/settings-page-html-extension/server/pages/{{pageDirectoryName}}/index.js b/src/templates/settings-page-html-extension/server/pages/{{pageDirectoryName}}/index.js
index 1e4c75f..27ad3aa 100644
--- a/src/templates/settings-page-html-extension/server/pages/{{pageDirectoryName}}/index.js
+++ b/src/templates/settings-page-html-extension/server/pages/{{pageDirectoryName}}/index.js
@@ -11,7 +11,6 @@ function onShoutemReady(event) {
$(document).ready(function() {
shoutem.api.init(config.context);
onPageReady(config);
-
});
};
diff --git a/src/templates/settings-page-html/template-init.js b/src/templates/settings-page-html/template-init.js
index 71aa371..4f12794 100644
--- a/src/templates/settings-page-html/template-init.js
+++ b/src/templates/settings-page-html/template-init.js
@@ -1,7 +1,7 @@
+import decamelize from 'decamelize';
import _ from 'lodash';
import getOrSet from 'lodash-get-or-set';
-import {instantiateExtensionTemplate} from "../../services/extension-template";
-import decamelize from 'decamelize';
+import { instantiateExtensionTemplate } from '../../services/extension-template';
function isHtmlPage({ type, path }) {
return type === 'html' && !_.includes(path, 'server/build');
@@ -12,7 +12,7 @@ export async function before(context) {
const pages = getOrSet(extJson, 'pages', []);
if (!_.every(pages, isHtmlPage)) {
- throw new Error("Html pages can't be mixed with non-html settings pages in the same extension");
+ throw new Error('Html pages can\'t be mixed with non-html settings pages in the same extension');
}
if (_.find(pages, { name })) {
diff --git a/src/templates/settings-page-react-bin/server/.babelrc b/src/templates/settings-page-react-bin/server/.babelrc
deleted file mode 100644
index b4e0857..0000000
--- a/src/templates/settings-page-react-bin/server/.babelrc
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "presets": [
- ["es2015", { "modules": false }],
- "react",
- "stage-0"
- ],
- "plugins": ["transform-runtime"]
-}
diff --git a/src/templates/settings-page-react-bin/server/bin/localization/LocalizationProvider.js b/src/templates/settings-page-react-bin/server/bin/localization/LocalizationProvider.js
new file mode 100644
index 0000000..c96dae2
--- /dev/null
+++ b/src/templates/settings-page-react-bin/server/bin/localization/LocalizationProvider.js
@@ -0,0 +1,108 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import i18next from 'i18next';
+import { initReactI18next } from 'react-i18next';
+import _ from 'lodash';
+import { connect } from 'react-redux';
+import { LoaderContainer } from '@shoutem/react-web-ui';
+import translation from '../../translations/en.json';
+
+export class LocalizationProvider extends PureComponent {
+ constructor(props) {
+ super(props);
+
+ this.handleInjection = this.handleInjection.bind(this);
+
+ this.state = {
+ inProgress: true,
+ };
+ }
+
+ componentDidMount() {
+ const { ownExtensionName, locale, translationUrl } = this.props;
+ const dictionary = _.get(translation, ownExtensionName);
+
+ i18next.use(initReactI18next).init({
+ lng: 'en',
+ fallbackLng: 'en',
+ ns: [ownExtensionName],
+ defaultNS: ownExtensionName,
+ nsSeparator: false,
+ keySeparator: false,
+ resources: {
+ en: {
+ [ownExtensionName]: dictionary,
+ },
+ },
+ });
+
+ this.handleInjection(locale, translationUrl);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const {
+ locale: nextLocale,
+ translationUrl: nextTranslationUrl,
+ } = nextProps;
+ const { locale, translationUrl } = this.props;
+
+ if (translationUrl !== nextTranslationUrl || nextLocale !== locale) {
+ this.handleInjection(nextLocale, nextTranslationUrl);
+ }
+ }
+
+ async handleInjection(locale, translationUrl) {
+ const { ownExtensionName } = this.props;
+
+ if (translationUrl) {
+ try {
+ const response = await fetch(translationUrl);
+ const translation = await response.json();
+ const dictionary = _.get(translation, ownExtensionName);
+
+ if (dictionary) {
+ await i18next.addResourceBundle(locale, ownExtensionName, dictionary);
+ await i18next.changeLanguage(locale);
+ }
+ } catch (error) {
+ // do nothing
+ }
+ }
+
+ this.setState({ inProgress: false });
+ }
+
+ render() {
+ const { children } = this.props;
+ const { inProgress } = this.state;
+
+ if (inProgress) {
+ return ;
+ }
+
+ return children;
+ }
+}
+
+LocalizationProvider.propTypes = {
+ children: PropTypes.node,
+ ownExtensionName: PropTypes.string,
+ locale: PropTypes.string,
+ translationUrl: PropTypes.string,
+};
+
+function mapStateToProps(state, ownProps) {
+ const { context } = ownProps;
+
+ const ownExtensionName = _.get(context, 'ownExtensionName');
+ const locale = _.get(context, 'i18n.locale');
+ const translationUrl = _.get(context, 'i18n.translationUrl');
+
+ return {
+ ownExtensionName,
+ locale,
+ translationUrl,
+ };
+}
+
+export default connect(mapStateToProps)(LocalizationProvider);
diff --git a/src/templates/settings-page-react-bin/server/bin/localization/index.js b/src/templates/settings-page-react-bin/server/bin/localization/index.js
new file mode 100644
index 0000000..bfb3ecd
--- /dev/null
+++ b/src/templates/settings-page-react-bin/server/bin/localization/index.js
@@ -0,0 +1 @@
+export { default as LocalizationProvider } from './LocalizationProvider';
diff --git a/src/templates/settings-page-react-bin/server/bin/main.js b/src/templates/settings-page-react-bin/server/bin/main.js
index f811017..7010265 100644
--- a/src/templates/settings-page-react-bin/server/bin/main.js
+++ b/src/templates/settings-page-react-bin/server/bin/main.js
@@ -1,8 +1,7 @@
-require('es6-promise').polyfill();
import 'fetch-everywhere';
-
import '@shoutem/react-web-ui/lib/styles/index.scss';
import '@shoutem/extension-sandbox';
+
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
@@ -14,8 +13,10 @@ import { RioStateSerializer } from '@shoutem/redux-io';
import { SyncStateEngine } from '@shoutem/redux-sync-state-engine';
import * as extension from '../src/index';
import { PageProvider, connectPage, Page } from './page';
+import { LocalizationProvider } from './localization';
import { SyncStateEngineProvider } from './syncStateEngine';
import configureStore from './configureStore';
+require('es6-promise').polyfill();
const uri = new URI(window.location.href);
const pageName = _.get(uri.search(true), 'page', '');
@@ -24,13 +25,12 @@ const rioStateSerializer = new RioStateSerializer();
function renderPage() {
if (!PageComponent) {
- return (
-
Page not found: {pageName}
- );
+ return Page not found: {pageName}
;
}
const ConnectedPageComponent = connectPage()(PageComponent);
- return ();
+
+ return ;
}
// handler for Shoutem initialization finished
@@ -60,12 +60,12 @@ function onShoutemReady(event) {
ReactDOM.render(
-
- {renderPage()}
-
+
+ {renderPage()}
+
,
- document.getElementById('root')
+ document.getElementById('root'),
);
}
@@ -75,7 +75,5 @@ document.addEventListener('shoutemready', onShoutemReady, false);
// Render it to DOM
ReactDOM.render(
,
- document.getElementById('root')
+ document.getElementById('root'),
);
-
-
diff --git a/src/templates/settings-page-react-bin/server/bin/page/Page.js b/src/templates/settings-page-react-bin/server/bin/page/Page.js
index 3522922..8ce7fbe 100644
--- a/src/templates/settings-page-react-bin/server/bin/page/Page.js
+++ b/src/templates/settings-page-react-bin/server/bin/page/Page.js
@@ -2,7 +2,6 @@ import { ext } from '../../src/const';
export default class Page {
constructor(context, parameters) {
-
this.pageContext = {
ownExtensionName: ext(),
...context,
diff --git a/src/templates/settings-page-react-bin/server/bin/page/PageProvider.js b/src/templates/settings-page-react-bin/server/bin/page/PageProvider.js
index cb2df05..ddfc36e 100644
--- a/src/templates/settings-page-react-bin/server/bin/page/PageProvider.js
+++ b/src/templates/settings-page-react-bin/server/bin/page/PageProvider.js
@@ -1,8 +1,10 @@
-import React, { PropTypes, Component, Children } from 'react';
+import { PureComponent, Children } from 'react';
+import PropTypes from 'prop-types';
-export default class PageProvider extends Component {
+export default class PageProvider extends PureComponent {
getChildContext() {
const { page } = this.props;
+
return { page };
}
@@ -14,10 +16,10 @@ export default class PageProvider extends Component {
}
PageProvider.propTypes = {
- page: React.PropTypes.object,
+ page: PropTypes.object,
children: PropTypes.node,
};
PageProvider.childContextTypes = {
- page: React.PropTypes.object,
+ page: PropTypes.object,
};
diff --git a/src/templates/settings-page-react-bin/server/bin/page/connectPage.js b/src/templates/settings-page-react-bin/server/bin/page/connectPage.js
index fd8da86..68dd716 100644
--- a/src/templates/settings-page-react-bin/server/bin/page/connectPage.js
+++ b/src/templates/settings-page-react-bin/server/bin/page/connectPage.js
@@ -1,4 +1,5 @@
import React from 'react';
+import PropTypes from 'prop-types';
import _ from 'lodash';
import { connect } from 'react-redux';
import { getShortcut, getExtension } from '@shoutem/redux-api-sdk';
@@ -8,6 +9,7 @@ export function connectPageContext(WrappedComponent) {
const { page } = context;
const pageProps = _.pick(page.getPageContext(), [
'appId',
+ 'appOwnerId',
'extensionName',
'ownExtensionName',
'shortcutId',
@@ -16,11 +18,11 @@ export function connectPageContext(WrappedComponent) {
const parameters = page.getParameters();
- return ();
+ return ;
}
PageProvider.contextTypes = {
- page: React.PropTypes.object,
+ page: PropTypes.object,
};
return PageProvider;
@@ -37,5 +39,6 @@ function mapStateToProps(state, ownProps) {
}
export default function connectPage() {
- return wrappedComponent => connectPageContext(connect(mapStateToProps)(wrappedComponent));
+ return wrappedComponent =>
+ connectPageContext(connect(mapStateToProps)(wrappedComponent));
}
diff --git a/src/templates/settings-page-react-bin/server/bin/page/index.js b/src/templates/settings-page-react-bin/server/bin/page/index.js
index d498e83..f87f955 100644
--- a/src/templates/settings-page-react-bin/server/bin/page/index.js
+++ b/src/templates/settings-page-react-bin/server/bin/page/index.js
@@ -1,8 +1,3 @@
-import connectPage from './connectPage';
-export { connectPage };
-
-import Page from './Page';
-export { Page };
-
-import PageProvider from './PageProvider';
-export { PageProvider };
+export { default as connectPage } from './connectPage';
+export { default as Page } from './Page';
+export { default as PageProvider } from './PageProvider';
diff --git a/src/templates/settings-page-react-bin/server/bin/reducers.js b/src/templates/settings-page-react-bin/server/bin/reducers.js
index 6cbf294..10649ae 100644
--- a/src/templates/settings-page-react-bin/server/bin/reducers.js
+++ b/src/templates/settings-page-react-bin/server/bin/reducers.js
@@ -1,7 +1,5 @@
import { combineReducers } from 'redux';
-import {
- reducer as coreReducer,
-} from '@shoutem/redux-api-sdk';
+import { reducer as coreReducer } from '@shoutem/redux-api-sdk';
export function createRootReducer(extensionName, reducer) {
return combineReducers({
diff --git a/src/templates/settings-page-react-bin/server/bin/syncStateEngine/SyncStateEngineProvider.jsx b/src/templates/settings-page-react-bin/server/bin/syncStateEngine/SyncStateEngineProvider.jsx
index 0092d08..c1fc640 100644
--- a/src/templates/settings-page-react-bin/server/bin/syncStateEngine/SyncStateEngineProvider.jsx
+++ b/src/templates/settings-page-react-bin/server/bin/syncStateEngine/SyncStateEngineProvider.jsx
@@ -1,10 +1,11 @@
-import React, { PropTypes, Component, Children } from 'react';
+import { Children, PureComponent } from 'react';
+import PropTypes from 'prop-types';
import _ from 'lodash';
import { connect } from 'react-redux';
import sandbox from '@shoutem/extension-sandbox';
import { ext } from '../../src/const';
-export class SyncStateEngineProvider extends Component {
+export class SyncStateEngineProvider extends PureComponent {
constructor(props) {
super(props);
@@ -72,16 +73,14 @@ export class SyncStateEngineProvider extends Component {
}
SyncStateEngineProvider.propTypes = {
- state: React.PropTypes.object,
- syncStateEngine: React.PropTypes.object,
- syncAction: React.PropTypes.func,
+ state: PropTypes.object,
+ syncStateEngine: PropTypes.object,
+ syncAction: PropTypes.func,
children: PropTypes.node,
};
function mapStateToProps(state) {
- return {
- state,
- };
+ return { state };
}
function mapDispatchToProps(dispatch) {
diff --git a/src/templates/settings-page-react-bin/server/bin/webpack/devServer.js b/src/templates/settings-page-react-bin/server/bin/webpack/devServer.js
index a7361c0..4438031 100644
--- a/src/templates/settings-page-react-bin/server/bin/webpack/devServer.js
+++ b/src/templates/settings-page-react-bin/server/bin/webpack/devServer.js
@@ -12,6 +12,7 @@ function resolveDevServer() {
port: 4790,
compress: isProduction,
inline: !isProduction,
+ disableHostCheck: true,
hot: !isProduction,
host: '0.0.0.0',
https: true,
diff --git a/src/templates/settings-page-react-bin/server/bin/webpack/moduleRules.js b/src/templates/settings-page-react-bin/server/bin/webpack/moduleRules.js
index e9c4780..74bfbbb 100644
--- a/src/templates/settings-page-react-bin/server/bin/webpack/moduleRules.js
+++ b/src/templates/settings-page-react-bin/server/bin/webpack/moduleRules.js
@@ -1,48 +1,60 @@
-const ExtractTextPlugin = require('extract-text-webpack-plugin');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const isProduction = require('./env');
function resolveModuleRules() {
const jsRule = {
test: /\.(js|jsx)$/,
- exclude: /node_modules/,
- use: [
- 'babel-loader',
- ],
+ use: { loader: 'babel-loader' },
};
- const styleProductionRule = {
- test: /\.scss$/,
- use: ExtractTextPlugin.extract({
- use: [{
- loader: 'css-loader',
- }, {
- loader: 'postcss-loader',
- }, {
- loader: 'sass-loader',
- }],
- fallback: 'style-loader',
- }),
- };
-
- const styleDevelopmentRule = {
- test: /\.scss$/,
- use: [
- 'style-loader',
- // Using source maps breaks urls in the CSS loader
- // https://github.com/webpack/css-loader/issues/232
- // This comment solves it, but breaks testing from a local network
- // https://github.com/webpack/css-loader/issues/232#issuecomment-240449998
- // 'css-loader?sourceMap',
- 'css-loader',
- 'postcss-loader',
- 'sass-loader?sourceMap',
- ],
- };
+ const styleRules = [
+ {
+ test: /\.css$/,
+ use: [
+ !isProduction ? 'style-loader' : MiniCssExtractPlugin.loader,
+ { loader: 'css-loader' },
+ ],
+ },
+ {
+ test: /\.scss$/,
+ use: [
+ !isProduction ? 'style-loader' : MiniCssExtractPlugin.loader,
+ {
+ loader: 'css-loader',
+ options: {
+ sourceMap: !isProduction,
+ },
+ },
+ {
+ loader: 'postcss-loader',
+ options: {
+ plugins: [require('cssnano')()],
+ sourceMap: !isProduction,
+ },
+ },
+ {
+ loader: 'sass-loader',
+ options: {
+ sourceMap: !isProduction,
+ },
+ },
+ ],
+ },
+ ];
- const imgRule = {
- test: /\.(png|gif|jpg|svg)$/,
- use: 'url-loader?limit=8192',
- };
+ const imgRules = [
+ {
+ test: /\.(png|gif|jpg|svg)$/,
+ use: [
+ {
+ loader: 'url-loader',
+ options: {
+ limit: 10000,
+ },
+ },
+ ],
+ },
+ ];
const fontRules = [
{
@@ -50,7 +62,11 @@ function resolveModuleRules() {
use: [
{
loader: 'url-loader',
- query: 'prefix=fonts/&name=fonts/[name].[ext]&limit=10000&mimetype=application/font-woff',
+ options: {
+ name: '[path][name].[ext]',
+ mimetype: 'application/font-woff',
+ limit: 10000,
+ },
},
],
},
@@ -59,8 +75,11 @@ function resolveModuleRules() {
use: [
{
loader: 'url-loader',
- query:
- 'prefix=fonts/&name=fonts/[name].[ext]&limit=10000&mimetype=application/font-woff2',
+ options: {
+ name: '[path][name].[ext]',
+ mimetype: 'application/font-woff2',
+ limit: 10000,
+ },
},
],
},
@@ -69,7 +88,11 @@ function resolveModuleRules() {
use: [
{
loader: 'file-loader',
- query: 'prefix=fonts/&name=fonts/[name].[ext]&limit=10000&mimetype=font/opentype',
+ options: {
+ name: '[path][name].[ext]',
+ mimetype: 'font/opentype',
+ limit: 10000,
+ },
},
],
},
@@ -78,7 +101,11 @@ function resolveModuleRules() {
use: [
{
loader: 'url-loader',
- query: 'prefix=fonts/&name=fonts/[name].[ext]&limit=10000&mimetype=application/octet-stream',
+ options: {
+ name: '[path][name].[ext]',
+ mimetype: 'application/octet-stream',
+ limit: 10000,
+ },
},
],
},
@@ -87,27 +114,15 @@ function resolveModuleRules() {
use: [
{
loader: 'file-loader',
- query: 'prefix=fonts/&name=fonts/[name].[ext]',
+ options: {
+ name: '[path][name].[ext]',
+ },
},
],
},
];
- if (isProduction) {
- return [
- jsRule,
- styleProductionRule,
- ...fontRules,
- imgRule,
- ];
- };
-
- return [
- jsRule,
- styleDevelopmentRule,
- ...fontRules,
- imgRule,
- ];
+ return [jsRule, ...styleRules, ...fontRules, ...imgRules];
}
-exports = module.exports = resolveModuleRules;
+module.exports = resolveModuleRules;
diff --git a/src/templates/settings-page-react-bin/server/bin/webpack/optimizations.js b/src/templates/settings-page-react-bin/server/bin/webpack/optimizations.js
new file mode 100644
index 0000000..e913eec
--- /dev/null
+++ b/src/templates/settings-page-react-bin/server/bin/webpack/optimizations.js
@@ -0,0 +1,35 @@
+const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
+const TerserPlugin = require('terser-webpack-plugin');
+
+function resolveOptimizations() {
+ return {
+ splitChunks: {
+ cacheGroups: {
+ styles: {
+ name: 'app',
+ test: /\.css$/,
+ chunks: 'all',
+ enforce: true,
+ },
+ vendors: {
+ name: 'vendor',
+ test: /[\\/]node_modules[\\/]/,
+ chunks: 'all',
+ enforce: true,
+ },
+ },
+ },
+ minimizer: [
+ new OptimizeCSSAssetsPlugin({}),
+ new TerserPlugin({
+ terserOptions: {
+ output: {
+ comments: false,
+ },
+ },
+ }),
+ ],
+ };
+}
+
+module.exports = resolveOptimizations;
diff --git a/src/templates/settings-page-react-bin/server/bin/webpack/plugins.js b/src/templates/settings-page-react-bin/server/bin/webpack/plugins.js
index d073bbd..62cf800 100644
--- a/src/templates/settings-page-react-bin/server/bin/webpack/plugins.js
+++ b/src/templates/settings-page-react-bin/server/bin/webpack/plugins.js
@@ -1,18 +1,12 @@
const webpack = require('webpack');
-const cssnano = require('cssnano');
-const ExtractTextPlugin = require('extract-text-webpack-plugin');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
-const DashboardPlugin = require('webpack-dashboard/plugin');
const isProduction = require('./env');
function resolvePlugins() {
- const commonsChunkPlugin = new webpack.optimize.CommonsChunkPlugin({
- name: 'vendor',
- minChunks: (module) => (
- // this assumes your vendor imports exist in the node_modules directory
- module.context && module.context.indexOf('node_modules') !== -1
- ),
- filename: 'vendor.[hash].js',
+ const miniCssExtractPlugin = new MiniCssExtractPlugin({
+ filename: '[name].css?q=[contenthash]',
+ allChunks: true,
});
const htmlWebpackPlugin = new HtmlWebpackPlugin({
@@ -21,55 +15,7 @@ function resolvePlugins() {
filename: 'index.html',
});
- const loaderOptionsPlugin = new webpack.LoaderOptionsPlugin({
- options: {
- postcss: [
- cssnano({
- autoprefixer: {
- add: true,
- remove: true,
- browsers: ['last 2 versions'],
- },
- discardComments: {
- removeAll: true,
- },
- safe: true,
- sourcemap: true,
- }),
- ],
- context: '/',
- },
- });
-
- const minimizeLoaderOptionsPlugin = new webpack.LoaderOptionsPlugin({
- minimize: true,
- debug: false,
- });
-
- const extractTextPlugin = new ExtractTextPlugin({
- filename: '[name].[hash].css',
- allChunks: true,
- });
-
- const uglifyJsPlugin = new webpack.optimize.UglifyJsPlugin({
- compress: {
- warnings: false,
- screw_ie8: true,
- conditionals: true,
- unused: true,
- comparisons: true,
- sequences: true,
- dead_code: true,
- evaluate: true,
- if_return: true,
- join_vars: true,
- },
- output: {
- comments: false,
- },
- });
-
- const dashboardPlugin = new DashboardPlugin();
+ const occurrenceOrderPlugin = new webpack.optimize.OccurrenceOrderPlugin();
const hotModuleReplacementPlugin = new webpack.HotModuleReplacementPlugin();
@@ -82,22 +28,13 @@ function resolvePlugins() {
if (isProduction) {
return [
nodeEnv,
- commonsChunkPlugin,
htmlWebpackPlugin,
- loaderOptionsPlugin,
- minimizeLoaderOptionsPlugin,
- uglifyJsPlugin,
- extractTextPlugin,
+ occurrenceOrderPlugin,
+ miniCssExtractPlugin,
];
}
- return [
- commonsChunkPlugin,
- htmlWebpackPlugin,
- loaderOptionsPlugin,
- hotModuleReplacementPlugin,
- dashboardPlugin,
- ];
+ return [htmlWebpackPlugin, hotModuleReplacementPlugin, occurrenceOrderPlugin];
}
-exports = module.exports = resolvePlugins;
+module.exports = resolvePlugins;
diff --git a/src/templates/settings-page-react-bin/server/bin/webpack/webpack.config.js b/src/templates/settings-page-react-bin/server/bin/webpack/webpack.config.js
index 9fa5986..4528cee 100644
--- a/src/templates/settings-page-react-bin/server/bin/webpack/webpack.config.js
+++ b/src/templates/settings-page-react-bin/server/bin/webpack/webpack.config.js
@@ -2,9 +2,11 @@ const path = require('path');
const resolvePlugins = require('./plugins');
const resolveModuleRules = require('./moduleRules');
const resolveDevServer = require('./devServer');
+const resolveOptimizations = require('./optimizations');
const isProduction = require('./env');
module.exports = {
+ mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'false' : '#source-maps',
context: path.join(__dirname, '../../'),
entry: {
@@ -19,12 +21,10 @@ module.exports = {
rules: resolveModuleRules(),
},
plugins: resolvePlugins(),
+ optimization: resolveOptimizations(),
resolve: {
- modules: [
- path.join(__dirname, '../..'),
- 'node_modules',
- ],
- extensions: ['.js', '.jsx', '.json', '.css', '.sass', '.scss', '.html']
+ modules: [path.join(__dirname, '../..'), 'node_modules'],
+ extensions: ['.js', '.jsx', '.json', '.css', '.sass', '.scss', '.html'],
},
devServer: resolveDevServer(),
};
diff --git a/src/templates/settings-page-react-bin/template-init.js b/src/templates/settings-page-react-bin/template-init.js
index bd6c817..c07326e 100644
--- a/src/templates/settings-page-react-bin/template-init.js
+++ b/src/templates/settings-page-react-bin/template-init.js
@@ -10,59 +10,85 @@ import {
const pkgJsonTemplate = {
"scripts": {
+ "lint": "eslint --no-eslintrc -c .eslintrc src/**/*.{js,jsx}",
"clean": "rimraf ./build/*",
"build": "npm run clean && cross-env NODE_ENV=production webpack --config ./bin/webpack/webpack.config.js",
- "dev-dashboard": "webpack-dashboard -c -- webpack-dev-server --config ./bin/webpack/webpack.config.js",
"dev": "webpack-dev-server --config ./bin/webpack/webpack.config.js"
},
"devDependencies": {
- "babel-cli": "^6.24.0",
- "babel-core": "^6.24.0",
- "babel-loader": "^6.4.1",
- "babel-plugin-transform-react-jsx": "^6.23.0",
- "babel-preset-es2015": "^6.24.0",
- "babel-preset-react": "^6.23.0",
- "babel-preset-stage-0": "^6.22.0",
+ "@babel/core": "^7.8.4",
+ "@babel/plugin-proposal-class-properties": "^7.8.3",
+ "@babel/plugin-transform-modules-commonjs": "^7.8.3",
+ "@babel/plugin-transform-runtime": "^7.8.3",
+ "@babel/preset-env": "^7.8.4",
+ "@babel/preset-react": "^7.8.3",
+ "@shoutem/eslint-config-react": "^1.0.1",
+ "babel-loader": "^8.0.6",
+ "babel-eslint": "^10.1.0",
"cross-env": "^4.0.0",
- "css-loader": "^0.27.3",
- "cssnano": "^3.10.0",
- "extract-text-webpack-plugin": "^2.1.0",
- "file-loader": "^0.10.1",
- "html-webpack-plugin": "^2.28.0",
- "node-sass": "^4.5.0",
- "postcss-loader": "^1.3.3",
- "rimraf": "^2.6.1",
+ "css-loader": "^3.4.2",
+ "cssnano": "^4.1.10",
+ "eslint": "^6.8.0",
+ "eslint-loader": "^3.0.3",
+ "eslint-plugin-babel": "^5.3.0",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-jsx-a11y": "^1.3.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "eslint-plugin-react": "^5.1.1",
+ "file-loader": "^6.0.0",
+ "html-webpack-plugin": "^4.0.3",
+ "mini-css-extract-plugin": "0.9.0",
+ "optimize-css-assets-webpack-plugin": "5.0.3",
+ "terser-webpack-plugin": "2.3.5",
+ "node-sass": "^4.13.1",
+ "path": "^0.12.7",
+ "postcss-loader": "^3.0.0",
+ "prettier": "^1.19.1",
+ "rimraf": "^3.0.2",
"sass-loader": "^6.0.3",
- "style-loader": "^0.14.1",
- "url-loader": "^0.5.8",
- "webpack": "^2.2.1",
- "webpack-dashboard": "^0.3.0",
- "webpack-dev-server": "^2.4.2"
+ "style-loader": "^1.1.3",
+ "url-loader": "^3.0.0",
+ "webpack": "^4.41.6",
+ "webpack-cli": "^3.3.11",
+ "webpack-dev-server": "^3.10.3"
},
"dependencies": {
"@shoutem/extension-sandbox": "^0.1.4",
- "@shoutem/react-web-ui": "^0.5.1",
- "@shoutem/redux-api-sdk": "^1.1.0",
- "@shoutem/redux-composers": "^0.1.5",
- "@shoutem/redux-io": "^2.3.0",
+ "@shoutem/react-web-ui": "0.12.4",
+ "@shoutem/redux-api-sdk": "^2.0.0",
+ "@shoutem/redux-composers": "^0.1.6",
+ "@shoutem/redux-io": "^3.2.1",
"@shoutem/redux-sync-state-engine": "^0.0.2",
+ "auto-bind": "^4.0.0",
"es6-promise": "^4.1.1",
"fetch-everywhere": "^1.0.5",
"lodash": "^4.17.4",
- "react": "^15.4.2",
- "react-dom": "^15.4.2",
+ "moment": "^2.16.0",
+ "normalize-url": "^1.6.0",
+ "prop-types": "^15.7.2",
+ "react": "^16.12.0",
+ "react-dom": "^16.12.0",
+ "i18next": "^19.7.0",
+ "react-i18next": "^11.7.3",
"react-redux": "^5.0.3",
+ "react-select": "^1.0.0-rc.5",
"redux": "^3.6.0",
+ "redux-form": "^5.2.5",
"redux-thunk": "^2.2.0",
- "urijs": "^1.18.9"
+ "reselect": "^2.5.4",
+ "urijs": "^1.18.9",
+ "validator": "^6.2.1"
},
"babel": {
"presets": [
- ["es2015", { "modules": false }],
- "react",
- "stage-0"
+ "@babel/preset-env",
+ "@babel/preset-react"
],
- "plugins": ["transform-runtime"]
+ "plugins": [
+ "@babel/plugin-transform-runtime",
+ "@babel/plugin-proposal-class-properties",
+ "@babel/plugin-transform-modules-commonjs"
+ ]
}
};
diff --git a/src/templates/settings-page-react-extension/server/src/pages/{{pageDirectoryName}}/localization.js b/src/templates/settings-page-react-extension/server/src/pages/{{pageDirectoryName}}/localization.js
new file mode 100644
index 0000000..2d0a3ab
--- /dev/null
+++ b/src/templates/settings-page-react-extension/server/src/pages/{{pageDirectoryName}}/localization.js
@@ -0,0 +1,11 @@
+const key = 'extension-page';
+
+const SAVE_BUTTON = `${key}.save-button`;
+const ENTER_COMPANY_NAME = `${key}.enter-company-name`;
+const COMPANY = `${key}.company`;
+
+export default {
+ SAVE_BUTTON,
+ ENTER_COMPANY_NAME,
+ COMPANY,
+};
diff --git a/src/templates/settings-page-react-extension/server/src/pages/{{pageDirectoryName}}/style.scss b/src/templates/settings-page-react-extension/server/src/pages/{{pageDirectoryName}}/style.scss
index 9e1c0ed..c640fa6 100644
--- a/src/templates/settings-page-react-extension/server/src/pages/{{pageDirectoryName}}/style.scss
+++ b/src/templates/settings-page-react-extension/server/src/pages/{{pageDirectoryName}}/style.scss
@@ -1,12 +1,14 @@
-:global {
- body.extension-sandbox-container {
- .settings-page {
- max-width: 552px;
- margin: auto;
+body.extension-sandbox-container {
+ .settings-page {
+ max-width: 552px;
+ margin: auto;
- &.is-wide {
- max-width: 808px;
- }
+ &.is-wide {
+ max-width: 808px;
}
}
}
+
+.save-button {
+ padding-top: 20px;
+}
diff --git a/src/templates/settings-page-react-extension/server/src/pages/{{pageDirectoryName}}/{{pageClassName}}.jsx b/src/templates/settings-page-react-extension/server/src/pages/{{pageDirectoryName}}/{{pageClassName}}.jsx
index 7e22828..c6e548d 100644
--- a/src/templates/settings-page-react-extension/server/src/pages/{{pageDirectoryName}}/{{pageClassName}}.jsx
+++ b/src/templates/settings-page-react-extension/server/src/pages/{{pageDirectoryName}}/{{pageClassName}}.jsx
@@ -1,5 +1,8 @@
-import React, { Component, PropTypes } from 'react';
+import React, { PureComponent } from 'react';
+import autoBindReact from 'auto-bind/react';
+import i18next from 'i18next';
import _ from 'lodash';
+import PropTypes from 'prop-types';
import {
Button,
ButtonToolbar,
@@ -8,17 +11,18 @@ import {
FormGroup,
HelpBlock,
} from 'react-bootstrap';
+import { connect } from 'react-redux';
import { LoaderContainer } from '@shoutem/react-web-ui';
import {
fetchExtension,
- updateExtensionSettings,
getExtension,
+ updateExtensionSettings,
} from '@shoutem/redux-api-sdk';
import { shouldRefresh } from '@shoutem/redux-io';
-import { connect } from 'react-redux';
+import LOCALIZATION from './localization';
import './style.scss';
-class {{pageClassName}} extends Component {
+class {{pageClassName}} extends PureComponent {
static propTypes = {
extension: PropTypes.object,
fetchExtension: PropTypes.func,
@@ -28,9 +32,7 @@ class {{pageClassName}} extends Component {
constructor(props) {
super(props);
- this.handleTextChange = this.handleTextChange.bind(this);
- this.handleSave = this.handleSave.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
+ autoBindReact(this);
props.fetchExtension();
@@ -87,11 +89,11 @@ class {{pageClassName}} extends Component {
const { error, hasChanges, inProgress, company } = this.state;
return (
-
+
-
+
diff --git a/src/templates/settings-page-react-extension/server/translations/example.json b/src/templates/settings-page-react-extension/server/translations/example.json
new file mode 100644
index 0000000..6bbf437
--- /dev/null
+++ b/src/templates/settings-page-react-extension/server/translations/example.json
@@ -0,0 +1,12 @@
+{
+ "{{developer.name}}": {
+ "{{extJson.name}}": {
+ "extension-page.save-button": "Save",
+ "extension-page.enter-company-name": "Enter company name",
+ "extension-page.company": "Company:",
+ "shortcut-page.save-button": "Save",
+ "shortcut-page.choose-your-greeting": "Choose your greeting",
+ "shortcut-page.name": "Name:"
+ }
+ }
+}
diff --git a/src/templates/settings-page-react-shortcut/server/src/pages/{{pageDirectoryName}}/localization.js b/src/templates/settings-page-react-shortcut/server/src/pages/{{pageDirectoryName}}/localization.js
new file mode 100644
index 0000000..9845e40
--- /dev/null
+++ b/src/templates/settings-page-react-shortcut/server/src/pages/{{pageDirectoryName}}/localization.js
@@ -0,0 +1,11 @@
+const key = 'shortcut-page';
+
+const SAVE_BUTTON = `${key}.save-button`;
+const CHOOSE_YOUR_GREETING = `${key}.choose-your-greeting`;
+const NAME = `${key}.name`;
+
+export default {
+ SAVE_BUTTON,
+ CHOOSE_YOUR_GREETING,
+ NAME,
+};
diff --git a/src/templates/settings-page-react-shortcut/server/src/pages/{{pageDirectoryName}}/style.scss b/src/templates/settings-page-react-shortcut/server/src/pages/{{pageDirectoryName}}/style.scss
index 9e1c0ed..c640fa6 100644
--- a/src/templates/settings-page-react-shortcut/server/src/pages/{{pageDirectoryName}}/style.scss
+++ b/src/templates/settings-page-react-shortcut/server/src/pages/{{pageDirectoryName}}/style.scss
@@ -1,12 +1,14 @@
-:global {
- body.extension-sandbox-container {
- .settings-page {
- max-width: 552px;
- margin: auto;
+body.extension-sandbox-container {
+ .settings-page {
+ max-width: 552px;
+ margin: auto;
- &.is-wide {
- max-width: 808px;
- }
+ &.is-wide {
+ max-width: 808px;
}
}
}
+
+.save-button {
+ padding-top: 20px;
+}
diff --git a/src/templates/settings-page-react-shortcut/server/src/pages/{{pageDirectoryName}}/{{pageClassName}}.jsx b/src/templates/settings-page-react-shortcut/server/src/pages/{{pageDirectoryName}}/{{pageClassName}}.jsx
index 5732da8..a1d27f0 100644
--- a/src/templates/settings-page-react-shortcut/server/src/pages/{{pageDirectoryName}}/{{pageClassName}}.jsx
+++ b/src/templates/settings-page-react-shortcut/server/src/pages/{{pageDirectoryName}}/{{pageClassName}}.jsx
@@ -1,5 +1,8 @@
-import React, { Component, PropTypes } from 'react';
+import React, { PureComponent } from 'react';
+import autoBindReact from 'auto-bind/react';
+import i18next from 'i18next';
import _ from 'lodash';
+import PropTypes from 'prop-types';
import {
Button,
ButtonToolbar,
@@ -8,12 +11,13 @@ import {
FormGroup,
HelpBlock,
} from 'react-bootstrap';
+import { connect } from 'react-redux';
import { LoaderContainer } from '@shoutem/react-web-ui';
import { updateShortcutSettings } from '@shoutem/redux-api-sdk';
-import { connect } from 'react-redux';
+import LOCALIZATION from './localization';
import './style.scss';
-class {{pageClassName}} extends Component {
+class {{pageClassName}} extends PureComponent {
static propTypes = {
shortcut: PropTypes.object,
updateShortcutSettings: PropTypes.func,
@@ -22,9 +26,7 @@ class {{pageClassName}} extends Component {
constructor(props) {
super(props);
- this.handleTextChange = this.handleTextChange.bind(this);
- this.handleSave = this.handleSave.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
+ autoBindReact(this);
this.state = {
error: null,
@@ -65,20 +67,21 @@ class {{pageClassName}} extends Component {
this.props.updateShortcutSettings(shortcut, { greeting })
.then(() => (
this.setState({ hasChanges: false, inProgress: false })
- )).catch((err) => {
- this.setState({ error: err, inProgress: false });
- });
+ ))
+ .catch((err) => {
+ this.setState({ error: err, inProgress: false });
+ });
}
render() {
const { error, hasChanges, inProgress, greeting } = this.state;
return (
-
+
-
+
diff --git a/src/templates/settings-page-react-shortcut/server/translations/example.json b/src/templates/settings-page-react-shortcut/server/translations/example.json
new file mode 100644
index 0000000..6bbf437
--- /dev/null
+++ b/src/templates/settings-page-react-shortcut/server/translations/example.json
@@ -0,0 +1,12 @@
+{
+ "{{developer.name}}": {
+ "{{extJson.name}}": {
+ "extension-page.save-button": "Save",
+ "extension-page.enter-company-name": "Enter company name",
+ "extension-page.company": "Company:",
+ "shortcut-page.save-button": "Save",
+ "shortcut-page.choose-your-greeting": "Choose your greeting",
+ "shortcut-page.name": "Name:"
+ }
+ }
+}
diff --git a/src/templates/settings-page-react/template-init.js b/src/templates/settings-page-react/template-init.js
index 3c788d2..707e108 100644
--- a/src/templates/settings-page-react/template-init.js
+++ b/src/templates/settings-page-react/template-init.js
@@ -1,8 +1,8 @@
+import decamelize from 'decamelize';
import _ from 'lodash';
import getOrSet from 'lodash-get-or-set';
-import decamelize from "decamelize";
import pascalize from 'uppercamelcase';
-import {instantiateExtensionTemplate} from "../../services/extension-template";
+import { instantiateExtensionTemplate } from '../../services/extension-template';
export function isReactPage({ type, path }) {
return type === 'react-page' || _.includes(path, 'server/build');
@@ -18,7 +18,7 @@ export async function before(context) {
}
if (!_.every(pages, isReactPage)) {
- throw new Error("React pages can't be mixed with non-react settings pages in the same extension");
+ throw new Error('React pages can\'t be mixed with non-react settings pages in the same extension');
}
pages.push({ name, type: 'react-page' });
diff --git a/src/templates/settings-page/template-init.js b/src/templates/settings-page/template-init.js
index 5786288..267f6bd 100644
--- a/src/templates/settings-page/template-init.js
+++ b/src/templates/settings-page/template-init.js
@@ -1,9 +1,10 @@
import getOrSet from 'lodash-get-or-set';
-import {instantiateExtensionTemplate} from "../../services/extension-template";
-import {linkSettingsPageWithExistingScreen} from "../../services/shortcut";
+import { instantiateExtensionTemplate } from '../../services/extension-template';
+import { linkSettingsPageWithExistingScreen } from '../../services/shortcut';
export async function after(context) {
const { type, extensionScope, extJson, existingScreenName, newScreen, name, title } = context;
+
if (type === 'react') {
await instantiateExtensionTemplate('settings-page-react', context)
} else if (type === 'html') {
@@ -27,6 +28,7 @@ export async function after(context) {
if (newScreen) {
await instantiateExtensionTemplate('screen', { ...context, ...newScreen });
+
if (newScreen.newShortcut) {
linkSettingsPageWithExistingScreen(extJson, context, newScreen.name);
}
diff --git a/src/templates/shortcut/template-init.js b/src/templates/shortcut/template-init.js
index ce3825a..b39d1f1 100644
--- a/src/templates/shortcut/template-init.js
+++ b/src/templates/shortcut/template-init.js
@@ -1,4 +1,4 @@
-import {addShortcut} from "../../services/shortcut";
+import {addShortcut} from '../../services/shortcut';
export async function before(context) {
addShortcut(context.extJson, context);