+
+ Name:
+ {username}/{projectName}
+
+
+ Version:
+ {version}
+
+
+ License:
+ {license}
+
+
+ Description:
+ {description}
+
+ {isPublishing ? (
+
+ Publishing…
+
+ ) : (
+
+
+
+
+
+ )}
+
+ );
+};
+
+PopupPublishProject.propTypes = {
+ isVisible: React.PropTypes.bool,
+ isPublishing: React.PropTypes.bool,
+ user: React.PropTypes.object,
+ project: sanctuaryPropType(XP.Project),
+ onPublish: React.PropTypes.func,
+ onRequestToEditPreferences: React.PropTypes.func,
+ onClose: React.PropTypes.func,
+};
+
+PopupPublishProject.defaultProps = {
+ isVisible: false,
+};
+
+export default PopupPublishProject;
diff --git a/packages/xod-client/src/project/messages.js b/packages/xod-client/src/project/messages.js
new file mode 100644
index 00000000..916b9f11
--- /dev/null
+++ b/packages/xod-client/src/project/messages.js
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import/prefer-default-export
+export const SUCCESSFULLY_PUBLISHED = 'Successfully published';
diff --git a/packages/xod-client/src/user/actions.js b/packages/xod-client/src/user/actions.js
index d9a0db94..89c34c76 100644
--- a/packages/xod-client/src/user/actions.js
+++ b/packages/xod-client/src/user/actions.js
@@ -41,6 +41,8 @@ export const fetchGrant = () => dispatch =>
.then((grant) => {
dispatch(setGrant(grant));
dispatch(updateCompileLimit(false));
+
+ return grant;
});
export const login = (username, password) => (dispatch) => {
diff --git a/packages/xod-client/src/user/messages.js b/packages/xod-client/src/user/messages.js
index 973f331e..1503dc5b 100644
--- a/packages/xod-client/src/user/messages.js
+++ b/packages/xod-client/src/user/messages.js
@@ -1,2 +1,3 @@
export const INCORRECT_CREDENTIALS = 'Incorrect username or password';
export const SERVICE_UNAVAILABLE = 'Service unavailable';
+export const LOG_IN_TO_CONTINUE = 'Please log in to continue';
diff --git a/packages/xod-client/src/user/selectors.js b/packages/xod-client/src/user/selectors.js
index 40a8baff..9c0b1fbb 100644
--- a/packages/xod-client/src/user/selectors.js
+++ b/packages/xod-client/src/user/selectors.js
@@ -24,7 +24,12 @@ export const isAuthorising = createSelector(
R.prop('isAuthorising')
);
-export const getUser = createSelector(
+export const getGrant = createSelector(
getUserState,
- R.pipe(R.path(['grant', 'user']), Maybe)
+ R.pipe(R.prop('grant'), Maybe)
+);
+
+export const getUser = createSelector(
+ getGrant,
+ R.map(R.prop('user'))
);
diff --git a/packages/xod-client/src/utils/menu.js b/packages/xod-client/src/utils/menu.js
index 08a042f4..150f2ff3 100644
--- a/packages/xod-client/src/utils/menu.js
+++ b/packages/xod-client/src/utils/menu.js
@@ -51,6 +51,10 @@ const rawItems = {
key: 'addLibrary',
label: 'Add Library',
},
+ publish: {
+ key: 'publish',
+ label: 'Publish Library',
+ },
edit: {
key: 'edit',
diff --git a/packages/xod-client/src/utils/urls.js b/packages/xod-client/src/utils/urls.js
index a355fe8b..75f58551 100644
--- a/packages/xod-client/src/utils/urls.js
+++ b/packages/xod-client/src/utils/urls.js
@@ -30,7 +30,7 @@ export const getUtmSiteUrl = getUtmUrl(process.env.XOD_SITE_DOMAIN);
// :: String -> String
export const getUtmForumUrl = getUtmUrl(process.env.XOD_FORUM_DOMAIN, '', 'forum');
-const HOSTNAME = process.env.XOD_HOSTNAME || 'xod.io';
+export const HOSTNAME = process.env.XOD_HOSTNAME || 'xod.io';
// :: () -> String
export const getCompileLimitUrl = () =>
diff --git a/packages/xod-pm/src/fetch.js b/packages/xod-pm/src/fetch.js
index 1ae5fc23..88f4ca51 100644
--- a/packages/xod-pm/src/fetch.js
+++ b/packages/xod-pm/src/fetch.js
@@ -1,5 +1,4 @@
import R from 'ramda';
-import swaggerClient from 'swagger-client';
import {
retryOrFail,
rejectFetchResult,
@@ -11,7 +10,7 @@ import { fromXodballData, listMissingLibraryNames } from 'xod-project';
import * as ERR_CODES from './errorCodes';
import * as MSG from './messages';
-import { parseLibQuery, unfoldMaybeLibQuery, rejectUnexistingVersion, getLibName } from './utils';
+import { parseLibQuery, unfoldMaybeLibQuery, rejectUnexistingVersion, getLibName, getSwaggerClient } from './utils';
// =============================================================================
//
@@ -25,22 +24,6 @@ const retryExcept404 = retryOrFail(
res => (res.status && res.status === 404),
);
-// Create memoized function to prevent fetching swagger URL
-// on each fetch request
-const getSwaggerClient = (() => {
- const memo = {};
-
- return (url) => {
- const index = JSON.stringify(url);
-
- if (index in memo) {
- return memo[index];
- }
-
- return (memo[index] = swaggerClient(url));
- };
-})();
-
// =============================================================================
//
// Fetching data
diff --git a/packages/xod-pm/src/index.js b/packages/xod-pm/src/index.js
index 68adc17d..027cc68e 100644
--- a/packages/xod-pm/src/index.js
+++ b/packages/xod-pm/src/index.js
@@ -1,4 +1,5 @@
export * from './fetch';
+export { default as publish } from './publish';
export * from './errorCodes';
export {
parseLibQuery,
diff --git a/packages/xod-pm/src/lib-uri.js b/packages/xod-pm/src/lib-uri.js
new file mode 100644
index 00000000..62b2d0cc
--- /dev/null
+++ b/packages/xod-pm/src/lib-uri.js
@@ -0,0 +1,14 @@
+// TODO: duplicates xod-cli/lib-uri.js
+
+import { Maybe } from 'ramda-fantasy';
+
+export const createLibUri = (orgname, libname, tag) => ({ orgname, libname, tag });
+
+export const parseLibUri = string => Maybe
+ .toMaybe(string.match(/^([^@/]+?)\/([^@/]+?)(?:@([^@/]+?))?$/))
+ .map(([, orgname, libname, tag]) =>
+ createLibUri(orgname, libname, tag || 'latest'));
+
+export const toStringWithoutTag = libUri => `${libUri.orgname}/${libUri.libname}`;
+
+export const toString = libUri => `${toStringWithoutTag(libUri)}@${libUri.tag}`;
diff --git a/packages/xod-pm/src/publish.js b/packages/xod-pm/src/publish.js
new file mode 100644
index 00000000..0e5e80bc
--- /dev/null
+++ b/packages/xod-pm/src/publish.js
@@ -0,0 +1,61 @@
+import * as XP from 'xod-project';
+
+import { getSwaggerClient, swaggerError } from './utils';
+import { createLibUri, toString, toStringWithoutTag } from './lib-uri';
+
+const packLibVersion = project => ({
+ libname: XP.getProjectName(project),
+ version: {
+ description: XP.getProjectDescription(project),
+ folder: { 'xodball.json': XP.toXodball(project) },
+ semver: `v${XP.getProjectVersion(project)}`,
+ },
+});
+
+export default function publish(swaggerUrl, grant, project) {
+ return getSwaggerClient(swaggerUrl)
+ .then((swagger) => {
+ const { Library, Organization, User, Version } = swagger.apis;
+
+ // eslint-disable-next-line no-param-reassign
+ swagger.authorizations = {
+ bearer_token: `Bearer ${grant.accessToken}`,
+ };
+ const { username } = grant.user;
+ const orgname = username;
+
+ const { libname, version } = packLibVersion(project);
+ const libUri = createLibUri(orgname, libname, version.semver);
+
+ return Organization.getOrg({ orgname })
+ .catch((err) => {
+ if (err.status !== 404) throw swaggerError(err);
+ return User.putUserOrg({ org: {}, orgname, username }).catch((err2) => {
+ if (err2.status === 403) {
+ throw new Error(`user "${username}" is not registered.`);
+ }
+ if (err2.status === 409) {
+ throw new Error(`orgname "${orgname}" is already taken.`);
+ }
+ throw swaggerError(err2);
+ });
+ })
+ .then(() => Library.getOrgLib({ libname, orgname }).catch((err) => {
+ if (err.status !== 404) throw swaggerError(err);
+ return Library.putOrgLib({ lib: {}, libname, orgname }).catch((err2) => {
+ if (err2.status === 403) {
+ throw new Error(`user "${username}" can't access ${
+ toStringWithoutTag(libUri)}.`);
+ }
+ throw swaggerError(err2);
+ });
+ }))
+ .then(() => Version.postLibVersion({ libname, orgname, version }).catch((err) => {
+ if (err.status === 409) {
+ throw new Error(`version "${toString(libUri)}" already exists.`);
+ }
+ throw swaggerError(err);
+ }));
+ });
+}
+
diff --git a/packages/xod-pm/src/utils.js b/packages/xod-pm/src/utils.js
index c8a96e80..7382f9fa 100644
--- a/packages/xod-pm/src/utils.js
+++ b/packages/xod-pm/src/utils.js
@@ -1,4 +1,5 @@
import R from 'ramda';
+import swaggerClient from 'swagger-client';
import { Maybe } from 'ramda-fantasy';
import {
notEmpty,
@@ -120,3 +121,38 @@ export const getLibVersion = R.curry(
)(versions);
}
);
+
+// Create memoized function to prevent fetching swagger URL
+// on each fetch request
+export const getSwaggerClient = (() => {
+ const memo = {};
+
+ return (url) => {
+ const index = JSON.stringify(url);
+
+ if (index in memo) {
+ return memo[index];
+ }
+
+ return (memo[index] = swaggerClient(url));
+ };
+})();
+
+// TODO: duplicate in xod-cli
+export const swaggerError = (err) => {
+ const { response, status } = err;
+ let res;
+ if (response.body && response.body.originalResponse) {
+ res = JSON.parse(response.body.originalResponse);
+ } else if (response.body) {
+ res = response.body;
+ } else {
+ res = response;
+ }
+
+ if (status === 400) {
+ return new Error(R.pathOr('Bad request', ['errors', 0, 'errors', 0, 'message'], res));
+ }
+
+ return new Error(`${status} ${JSON.stringify(res, null, 2)}`);
+};