mirror of
https://github.com/xodio/xod.git
synced 2026-03-11 03:06:50 +01:00
feat(xod-client, xod-client-browser, xod-client-electron, xod-fs): add feature to install libraries from cloud in the IDE, integrate in both clients, add saving on FS for desktop client
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
"handlebars": "^4.0.6",
|
||||
"hm-def": "^0.2.0",
|
||||
"ramda": "^0.24.1",
|
||||
"ramda-fantasy": "^0.7.0",
|
||||
"ramda-fantasy": "^0.8.0",
|
||||
"sanctuary-def": "^0.9.0",
|
||||
"xod-func-tools": "^0.15.0",
|
||||
"xod-project": "^0.15.0"
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"xod-arduino": "^0.15.0",
|
||||
"xod-fs": "^0.15.0",
|
||||
"xod-func-tools": "^0.15.0",
|
||||
"xod-project": "^0.15.0"
|
||||
"xod-project": "^0.15.0",
|
||||
"xod-pm": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.0.0",
|
||||
|
||||
@@ -1,115 +1,26 @@
|
||||
import * as path from 'path';
|
||||
import * as xodFs from 'xod-fs';
|
||||
import { parseLibUri, toString, toStringWithoutTag } from './lib-uri';
|
||||
import { fetchLibData, fetchLibXodball, getLibVersion } from 'xod-pm';
|
||||
import * as messages from './messages';
|
||||
import * as swagger from './swagger';
|
||||
|
||||
/**
|
||||
* Checks whether library directory does not already exist.
|
||||
* @param {Path} libDir - Library installation directory.
|
||||
* @param {LibUri} libUri - Library URI in the package manager.
|
||||
* @returns {Promise.<void>}
|
||||
*/
|
||||
function libDirDoesNotExist(libDir, libUri) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!xodFs.doesDirectoryExist(libDir)) return resolve();
|
||||
return reject(new Error(`could not install "${toString(libUri)}".\n` +
|
||||
`"${libDir}" is not empty, remove it manually.`));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses string to the library URI in the package manager.
|
||||
* @param string
|
||||
* @return {Promise.<LibUri>}
|
||||
*/
|
||||
function getLibUri(string) {
|
||||
return parseLibUri(string)
|
||||
.map(libUri => Promise.resolve.bind(Promise, libUri))
|
||||
.getOrElse(Promise.reject.bind(Promise,
|
||||
new Error(`could not parse "${string}" as <orgname>/<libname>[@<tag>].`)))
|
||||
.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the semver from `libUri` or fetches it from the package manager.
|
||||
* @param swaggerClient - `swagger-js` client.
|
||||
* @param {LibUri} libUri - Library URI in the package manager.
|
||||
* @returns {Promise.<Semver>}
|
||||
*/
|
||||
function getSemver(swaggerClient, libUri) {
|
||||
const { libname, orgname, tag } = libUri;
|
||||
if (tag !== 'latest') return Promise.resolve(tag);
|
||||
return swaggerClient.apis.Library.getOrgLib({ libname, orgname })
|
||||
.catch((err) => {
|
||||
if (err.status !== 404) throw swagger.error(err);
|
||||
throw new Error('could not find library ' +
|
||||
`"${toStringWithoutTag(libUri)}".`);
|
||||
})
|
||||
.then(({ obj: lib }) => {
|
||||
const [version] = lib.versions;
|
||||
if (version) return version;
|
||||
throw new Error('could not find latest version of ' +
|
||||
`"${toStringWithoutTag(libUri)}".`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the library version xodball from package manager.
|
||||
* @param swaggerClient - `swagger-js` client.
|
||||
* @param {LibUri} libUri - Library URI in the package manager.
|
||||
* @param {Semver} semver
|
||||
* @returns {Promise.<Xodball>}
|
||||
*/
|
||||
function loadXodball(swaggerClient, libUri, semver) {
|
||||
const { libname, orgname } = libUri;
|
||||
return swaggerClient.apis.Version.getLibVersionXodball({
|
||||
libname, orgname, semver,
|
||||
}).then(({ obj: xodball }) => xodball)
|
||||
.catch((err) => {
|
||||
if (err.status !== 404) throw swagger.error(err);
|
||||
throw new Error(`could not find version "${toString(libUri)}".`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the library version `Project` from package manager.
|
||||
* @param swaggerClient - `swagger-js` client.
|
||||
* @param {LibUri} libUri - Library URI in the package manager.
|
||||
* @returns {Promise.<Project>}
|
||||
*/
|
||||
function getProject(swaggerClient, libUri) {
|
||||
return getSemver(swaggerClient, libUri)
|
||||
.then(semver => loadXodball(swaggerClient, libUri, semver))
|
||||
.catch(() => {
|
||||
throw new Error(`could not find "${toString(libUri)}".`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the library version from package manager.
|
||||
* @param swaggerUrl - Swagger.json URL for package manager.
|
||||
* @param {LibUri} libUri - Library URI in the package manager.
|
||||
* @param {Path} path2 - Installation destination.
|
||||
* @param {LibUri} libQuery - Library identifier in the package manager (`owner/name@version`).
|
||||
* @param {Path} distPath - Path to workspace, where should be library installed
|
||||
* @return {Promise.<void>}
|
||||
*/
|
||||
export default function install(swaggerUrl, libUri, path2) {
|
||||
return Promise
|
||||
.all([
|
||||
getLibUri(libUri),
|
||||
swagger.client(swaggerUrl),
|
||||
xodFs.findClosestWorkspaceDir(path2),
|
||||
])
|
||||
.then(([libUri2, swaggerClient, workspaceDir]) => {
|
||||
const orgDir = path.resolve(
|
||||
xodFs.resolveLibPath(workspaceDir),
|
||||
xodFs.fsSafeName(libUri2.orgname)
|
||||
);
|
||||
const libDir = path.resolve(orgDir, xodFs.fsSafeName(libUri2.libname));
|
||||
return libDirDoesNotExist(libDir, libUri2)
|
||||
.then(() => getProject(swaggerClient, libUri2))
|
||||
.then(xodFs.saveProject(orgDir))
|
||||
.then(() => `Installed "${toString(libUri2)}" at "${libDir}".`);
|
||||
export default function install(swaggerUrl, libQuery, distPath) {
|
||||
return Promise.all([
|
||||
fetchLibData(swaggerUrl, libQuery),
|
||||
xodFs.findClosestWorkspaceDir(distPath),
|
||||
])
|
||||
.then(([libData, wsPath]) => {
|
||||
const params = libData.requestParams;
|
||||
const libName = `${params.owner}/${params.name}`;
|
||||
const version = getLibVersion(libData);
|
||||
|
||||
return fetchLibXodball(swaggerUrl, `${libName}@${version}`)
|
||||
.then(xodball => xodFs.saveProjectAsLibrary(params.owner, xodball, wsPath))
|
||||
.then(() => `Installed new library "${libQuery}" into workspace "${wsPath}".`);
|
||||
})
|
||||
.then(messages.success)
|
||||
.catch((err) => {
|
||||
|
||||
@@ -178,6 +178,8 @@ class App extends client.App {
|
||||
onClick(items.exportProject, this.onExport),
|
||||
items.separator,
|
||||
onClick(items.newPatch, this.props.actions.createPatch),
|
||||
items.separator,
|
||||
onClick(items.addLibrary, this.props.actions.showLibSuggester),
|
||||
]
|
||||
),
|
||||
submenu(
|
||||
|
||||
@@ -155,6 +155,7 @@ const onReady = () => {
|
||||
});
|
||||
ipcMain.on(EVENTS.GET_SIDEBAR_PANE_HEIGHT, loadSidebarPaneHeight);
|
||||
ipcMain.on(EVENTS.CHANGE_SIDEBAR_PANE_HEIGHT, saveSidebarPaneHeight);
|
||||
ipcMain.on(EVENTS.INSTALL_LIBRARY, WA.saveLibrary);
|
||||
|
||||
createWindow();
|
||||
win.webContents.on('did-finish-load', () => {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
findProjectMetaByName,
|
||||
resolveDefaultProjectPath,
|
||||
ensureWorkspacePath,
|
||||
saveProjectAsLibrary,
|
||||
ERROR_CODES as FS_ERROR_CODES,
|
||||
} from 'xod-fs';
|
||||
import {
|
||||
@@ -171,6 +172,28 @@ export const loadProjectsOrSpawnDefault = R.curry(
|
||||
)(workspacePath)
|
||||
);
|
||||
|
||||
// :: SaveLibPayload :: { request: LibParams, xodball: Xodball }
|
||||
// :: IpcEvent -> SaveLibPayload -> Promise SaveLibPayload Error
|
||||
export const saveLibrary = R.curry(
|
||||
(event, payload) => R.composeP(
|
||||
() => {
|
||||
event.sender.send(
|
||||
EVENTS.INSTALL_LIBRARY_COMPLETE,
|
||||
payload.request
|
||||
);
|
||||
return payload;
|
||||
},
|
||||
saveProjectAsLibrary(payload.request.owner, payload.xodball),
|
||||
loadWorkspacePath
|
||||
)()
|
||||
.catch((err) => {
|
||||
event.sender.send(
|
||||
EVENTS.INSTALL_LIBRARY_FAILED,
|
||||
errorToPlainObject(err)
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
//
|
||||
// Handlers
|
||||
|
||||
@@ -8,16 +8,22 @@ import popupsReducer from './popups/reducer';
|
||||
import uploadReducer from './upload/reducer';
|
||||
|
||||
import debuggerMiddleware from './debugger/middleware';
|
||||
import installLibMiddleware from './view/installLibMiddleware';
|
||||
|
||||
const extraReducers = {
|
||||
popups: popupsReducer,
|
||||
upload: uploadReducer,
|
||||
};
|
||||
|
||||
const extraMiddlewares = [
|
||||
debuggerMiddleware,
|
||||
installLibMiddleware,
|
||||
];
|
||||
|
||||
ReactDOM.render(
|
||||
<Root
|
||||
extraReducers={extraReducers}
|
||||
extraMiddlewares={[debuggerMiddleware]}
|
||||
extraMiddlewares={extraMiddlewares}
|
||||
initialState={initialState} // TODO: Remove project and opened patch when possible
|
||||
>
|
||||
<App />
|
||||
|
||||
@@ -28,6 +28,7 @@ const ERROR_FORMATTERS = {
|
||||
[XFS_EC.CANT_COPY_DEFAULT_PROJECT]: err => `Could not copy default project at ${err.path}: ${err.message}`,
|
||||
[XFS_EC.CANT_ENUMERATE_PROJECTS]: err => `Could not enumerate projects at ${err.path}: ${err.message}`,
|
||||
[XFS_EC.CANT_SAVE_PROJECT]: err => `Could not save the project at ${err.path}: ${err.message}`,
|
||||
[XFS_EC.CANT_SAVE_LIBRARY]: err => `Could not save the library at ${err.path}: ${err.message}`,
|
||||
|
||||
[EC.CANT_CREATE_NEW_PROJECT]: err => `Could not create a new project: ${err.message}`,
|
||||
[EC.CANT_OPEN_SELECTED_PROJECT]: err => `Could not open selected project: ${err.message}`,
|
||||
|
||||
@@ -34,3 +34,7 @@ export const CONFIRM_CLOSE_WINDOW = 'CONFIRM_CLOSE_WINDOW';
|
||||
|
||||
export const GET_SIDEBAR_PANE_HEIGHT = 'GET_SIDEBAR_PANE_HEIGHT';
|
||||
export const CHANGE_SIDEBAR_PANE_HEIGHT = 'CHANGE_SIDEBAR_PANE_HEIGHT';
|
||||
|
||||
export const INSTALL_LIBRARY = 'INSTALL_LIBRARY';
|
||||
export const INSTALL_LIBRARY_COMPLETE = 'INSTALL_LIBRARY_COMPLETE';
|
||||
export const INSTALL_LIBRARY_FAILED = 'INSTALL_LIBRARY_FAILED';
|
||||
|
||||
@@ -165,6 +165,13 @@ class App extends client.App {
|
||||
EVENTS.GET_SIDEBAR_PANE_HEIGHT,
|
||||
(event, sidebarPaneHeight) => this.setState({ sidebarPaneHeight })
|
||||
);
|
||||
ipcRenderer.on(
|
||||
EVENTS.INSTALL_LIBRARY_FAILED,
|
||||
(event, error) => {
|
||||
console.error(error); // eslint-disable-line no-console
|
||||
this.props.actions.addError(formatError(error));
|
||||
}
|
||||
);
|
||||
|
||||
// Debugger
|
||||
debuggerIPC.subscribeOnDebuggerEvents(ipcRenderer, this);
|
||||
@@ -417,6 +424,8 @@ class App extends client.App {
|
||||
onClick(items.exportProject, this.onExport),
|
||||
items.separator,
|
||||
onClick(items.newPatch, this.props.actions.createPatch),
|
||||
items.separator,
|
||||
onClick(items.addLibrary, this.props.actions.showLibSuggester),
|
||||
]
|
||||
),
|
||||
submenu(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import client from 'xod-client';
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
import { INSTALL_LIBRARY } from '../shared/events';
|
||||
|
||||
export default () => next => (action) => {
|
||||
if (action.type === client.INSTALL_LIBRARY_COMPLETE) {
|
||||
ipcRenderer.send(
|
||||
INSTALL_LIBRARY,
|
||||
{
|
||||
request: action.payload.request,
|
||||
xodball: action.payload.xodball,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return next(action);
|
||||
};
|
||||
56
packages/xod-client-electron/test-func/addLibrary.spec.js
Normal file
56
packages/xod-client-electron/test-func/addLibrary.spec.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import fse from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import prepareSuite from './prepare';
|
||||
|
||||
import { TRIGGER_MAIN_MENU_ITEM } from '../src/testUtils/triggerMainMenu';
|
||||
|
||||
describe('Add library in the IDE', () => {
|
||||
const ide = prepareSuite();
|
||||
const wsPath = (...extraPath) => path.resolve(ide.tmpHomeDir, 'xod', ...extraPath);
|
||||
|
||||
it('opens an "Add Library" suggester', () =>
|
||||
ide.app.electron.ipcRenderer.emit(TRIGGER_MAIN_MENU_ITEM, ['File', 'Add Library'])
|
||||
.then(() => ide.page.assertLibSuggesterShown())
|
||||
);
|
||||
|
||||
it('searches for nonexistent library', () =>
|
||||
ide.page.findLibSuggester().setValue('input', 'xod/nonexistent-library')
|
||||
.then(ide.page.assertLibsNotFound)
|
||||
);
|
||||
|
||||
it('searches for existing library', () =>
|
||||
ide.page.findLibSuggester().setValue('input', 'xod/core@0.11.0')
|
||||
.then(ide.page.assertLibraryFound)
|
||||
);
|
||||
|
||||
it('double clicks on found library to install', () =>
|
||||
ide.page.installLibrary()
|
||||
.then(() => Promise.all([
|
||||
ide.page.assertLibSuggesterHidden(),
|
||||
ide.page.assertProjectBrowserHasInstallingLib('xod/core'),
|
||||
]))
|
||||
);
|
||||
|
||||
it('checks that installed xod/core have not `concat-4` node', () =>
|
||||
ide.page.waitUntilLibraryInstalled()
|
||||
.then(() => ide.page.findPatchGroup('xod/core').click())
|
||||
.then(() => ide.page.assertNodeUnavailableInProjectBrowser('concat-4'))
|
||||
);
|
||||
|
||||
it('checks that library installed on the FS', () =>
|
||||
assert.eventually.isTrue(
|
||||
fse.pathExists(wsPath('__lib__', 'xod/core'))
|
||||
)
|
||||
);
|
||||
|
||||
it('checks that library is on version 0.11.0', () =>
|
||||
assert.eventually.equal(
|
||||
fse.readJSON(wsPath('__lib__', 'xod/core', 'project.xod'))
|
||||
.then(proj => proj.version),
|
||||
'0.11.0'
|
||||
)
|
||||
);
|
||||
});
|
||||
140
packages/xod-client-electron/test-func/blink.spec.js
Normal file
140
packages/xod-client-electron/test-func/blink.spec.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import fsp from 'fsp';
|
||||
import path from 'path';
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import prepareSuite from './prepare';
|
||||
|
||||
import { TRIGGER_MAIN_MENU_ITEM } from '../src/testUtils/triggerMainMenu';
|
||||
|
||||
const workspacePath = subPath => path.resolve(__dirname, '../../../workspace/', subPath);
|
||||
|
||||
describe('IDE: Blink project', () => {
|
||||
const ide = prepareSuite();
|
||||
|
||||
it('opens a window', () =>
|
||||
assert.eventually.isTrue(ide.app.browserWindow.isVisible())
|
||||
);
|
||||
|
||||
it('has proper title', () =>
|
||||
assert.eventually.strictEqual(ide.app.client.getTitle(), 'XOD')
|
||||
);
|
||||
|
||||
describe('creating a new patch', () => {
|
||||
it('prompts for a name', () =>
|
||||
ide.page.clickAddPatch()
|
||||
.then(() => ide.page.assertPopupShown('Create new patch'))
|
||||
);
|
||||
|
||||
it('closes popup on confirm', () =>
|
||||
ide.page.findPopup().setValue('input', 'my-blink')
|
||||
.then(() => ide.page.confirmPopup())
|
||||
.then(() => ide.page.assertNoPopups())
|
||||
);
|
||||
|
||||
it('opens new tab for the new patch', () =>
|
||||
ide.page.assertActiveTabHasTitle('my-blink')
|
||||
);
|
||||
});
|
||||
|
||||
describe('xod/core library', () => {
|
||||
it('provides a patch group that is collapsed initially', () =>
|
||||
ide.page.assertPatchGroupCollapsed('xod/core')
|
||||
);
|
||||
|
||||
it('expands on click', () =>
|
||||
ide.page.findPatchGroup('xod/core').click()
|
||||
.then(() => ide.page.assertPatchGroupExpanded('xod/core'))
|
||||
);
|
||||
|
||||
it('provides clock node', () =>
|
||||
ide.page.assertNodeAvailableInProjectBrowser('clock')
|
||||
);
|
||||
});
|
||||
|
||||
describe('adding nodes to patch', () => {
|
||||
it('selects a node in project browser', () =>
|
||||
ide.page.scrollToPatchInProjectBrowser('clock')
|
||||
.then(() => ide.page.selectPatchInProjectBrowser('clock'))
|
||||
.then(() => ide.page.assertPatchSelected('clock'))
|
||||
);
|
||||
|
||||
it('adds a node to patch', () =>
|
||||
ide.page.clickAddNodeButton('clock')
|
||||
.then(() => ide.page.findNode('clock').isVisible())
|
||||
);
|
||||
|
||||
it('drags a node in place', () =>
|
||||
assert.eventually.deepEqual(
|
||||
ide.page.dragNode('clock', 150, 10).getLocation(),
|
||||
{ x: 387, y: 81 }
|
||||
)
|
||||
);
|
||||
|
||||
it('adds the rest of the nodes for the blink patch', () =>
|
||||
ide.page.addNode('digital-output', 150, 300)
|
||||
.then(() => ide.page.addNode('flip-flop', 150, 200))
|
||||
);
|
||||
});
|
||||
|
||||
describe('creating links between nodes', () => {
|
||||
it('activates linking mode when clicking on a pin', () =>
|
||||
ide.page.findPin('clock', 'TICK').click()
|
||||
.then(() => ide.page.assertPinIsSelected('clock', 'TICK'))
|
||||
);
|
||||
|
||||
it('marks pins that can be linked with selected pin', () =>
|
||||
Promise.all([
|
||||
ide.page.assertPinIsAcceptingLinks('flip-flop', 'SET'),
|
||||
ide.page.assertPinIsAcceptingLinks('flip-flop', 'TGL'),
|
||||
ide.page.assertPinIsAcceptingLinks('flip-flop', 'RST'),
|
||||
])
|
||||
);
|
||||
|
||||
it('connects two compatible pins with a link', () =>
|
||||
ide.page.findPin('flip-flop', 'TGL').click()
|
||||
.then(() => ide.page.findLink('pulse'))
|
||||
);
|
||||
|
||||
it('adds the rest of the links for the blink patch', () =>
|
||||
ide.page.findPin('flip-flop', 'MEM').click()
|
||||
.then(() => ide.page.findPin('digital-output', 'SIG').click())
|
||||
);
|
||||
});
|
||||
|
||||
describe('binding values to inputs', () => {
|
||||
it('sets clock interval to 0.25', () =>
|
||||
ide.page.bindValue('clock', 'IVAL', '0.25')
|
||||
);
|
||||
|
||||
it('sets digital-output port to 13', () =>
|
||||
ide.page.bindValue('digital-output', 'PORT', '13')
|
||||
);
|
||||
});
|
||||
|
||||
describe('showing code for arduino', () => {
|
||||
const expectedCpp = fsp.readFileSync(
|
||||
workspacePath('blink/__fixtures__/arduino.cpp'),
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
it('shows code', () =>
|
||||
ide.app.electron.ipcRenderer.emit(TRIGGER_MAIN_MENU_ITEM, ['Deploy', 'Show Code For Arduino'])
|
||||
.then(() => ide.page.getCodeboxValue())
|
||||
.then(code => assert.strictEqual(code, expectedCpp, 'Actual and expected C++ don’t match'))
|
||||
);
|
||||
it('closes show code popup', () =>
|
||||
ide.page.closePopup()
|
||||
);
|
||||
});
|
||||
|
||||
describe('deleting a patch', () => {
|
||||
it('deletes "my-blink" patch', () =>
|
||||
ide.page.findPatchGroup('welcome-to-xod').click()
|
||||
.then(() => ide.page.assertPatchGroupExpanded('welcome-to-xod'))
|
||||
.then(() => ide.page.deletePatch('my-blink'))
|
||||
.then(() => ide.page.assertNodeUnavailableInProjectBrowser('my-blink'))
|
||||
.then(() => ide.page.assertTabWithTitleDoesNotExist('my-blink'))
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -126,6 +126,50 @@ function clickAddNodeButton(client, name) {
|
||||
return client.element(`${getSelectorForPatchInProjectBrowser(name)} .add-node`).click();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// LibSuggester
|
||||
//-----------------------------------------------------------------------------
|
||||
function findLibSuggester(client) {
|
||||
return client.element('.Suggester-libs');
|
||||
}
|
||||
|
||||
function assertLibSuggesterShown(client) {
|
||||
return assert.eventually.isTrue(findLibSuggester(client).isVisible());
|
||||
}
|
||||
|
||||
function assertLibsNotFound(client) {
|
||||
return assert.eventually.isTrue(
|
||||
findLibSuggester(client).waitForExist('.error', 5000)
|
||||
);
|
||||
}
|
||||
|
||||
function assertLibraryFound(client) {
|
||||
return assert.eventually.isTrue(
|
||||
findLibSuggester(client).waitForExist('.Suggester-item--library', 5000)
|
||||
);
|
||||
}
|
||||
|
||||
function installLibrary(client) {
|
||||
return findLibSuggester(client).doubleClick('.Suggester-item--library');
|
||||
}
|
||||
|
||||
function assertLibSuggesterHidden(client) {
|
||||
return assert.eventually.isFalse(
|
||||
client.isExisting('.Suggester-libs')
|
||||
);
|
||||
}
|
||||
|
||||
function assertProjectBrowserHasInstallingLib(client, libName) {
|
||||
return assert.eventually.equal(
|
||||
client.element('.PatchGroup--installing').getText('.name'),
|
||||
libName
|
||||
);
|
||||
}
|
||||
|
||||
function waitUntilLibraryInstalled(client) {
|
||||
return findProjectBrowser(client).waitForExist('.PatchGroup--installing', 5000, true);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Patch
|
||||
//-----------------------------------------------------------------------------
|
||||
@@ -247,6 +291,15 @@ const API = {
|
||||
selectPatchInProjectBrowser,
|
||||
openPatchFromProjectBrowser,
|
||||
deletePatch,
|
||||
// LibSuggester
|
||||
findLibSuggester,
|
||||
assertLibSuggesterShown,
|
||||
assertLibsNotFound,
|
||||
assertLibraryFound,
|
||||
installLibrary,
|
||||
assertLibSuggesterHidden,
|
||||
assertProjectBrowserHasInstallingLib,
|
||||
waitUntilLibraryInstalled,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
73
packages/xod-client-electron/test-func/prepare.js
Normal file
73
packages/xod-client-electron/test-func/prepare.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import os from 'os';
|
||||
import fsp from 'fsp';
|
||||
import fse from 'fs-extra';
|
||||
import path from 'path';
|
||||
import chai from 'chai';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
|
||||
// Spectron is hoisted at the root of monorepo
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { Application } from 'spectron';
|
||||
|
||||
import Page from './pageObject';
|
||||
|
||||
const DEBUG = process.env.XOD_DEBUG_TESTS;
|
||||
|
||||
// =============================================================================
|
||||
//
|
||||
// Prepare new suite
|
||||
//
|
||||
// =============================================================================
|
||||
|
||||
export default function prepareSuite() {
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
const state = {
|
||||
passed: true,
|
||||
};
|
||||
|
||||
before(() => {
|
||||
const tmpDir = path.join(os.tmpdir(), 'xod-test-workspace-');
|
||||
|
||||
state.app = new Application({
|
||||
path: '../../node_modules/.bin/electron',
|
||||
args: ['.'],
|
||||
});
|
||||
|
||||
// With a dirty hack we extract out objects that we’d interact later
|
||||
// in tests and tear-down. Impure, but can’t figure out a better solution
|
||||
// with Mocha
|
||||
return fsp.mkdtempP(tmpDir)
|
||||
.then((home) => {
|
||||
process.env.USERDATA_DIR = process.env.HOME = state.tmpHomeDir = home;
|
||||
})
|
||||
.then(() => state.app.start())
|
||||
.then(() => {
|
||||
state.page = Page.createPageObject(state.app.client);
|
||||
chaiAsPromised.transferPromiseness = state.app.transferPromiseness;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function checkForFailure() {
|
||||
if (this.currentTest.state === 'failed') {
|
||||
state.passed = false;
|
||||
}
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (!state.passed || DEBUG) {
|
||||
state.app.client.getMainProcessLogs().then((logs) => {
|
||||
logs.forEach(console.log); // eslint-disable-line no-console
|
||||
});
|
||||
}
|
||||
|
||||
const shouldClose = state.app && state.app.isRunning() && (state.passed || !DEBUG);
|
||||
return shouldClose
|
||||
// TODO: simulating click on 'confirm' is probably cleaner
|
||||
? state.app.electron.ipcRenderer.send('CONFIRM_CLOSE_WINDOW')
|
||||
.then(() => fse.remove(state.tmpHomeDir))
|
||||
: Promise.resolve();
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
import os from 'os';
|
||||
import fsp from 'fsp';
|
||||
import fse from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import chai, { assert } from 'chai';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
|
||||
// Spectron is hoisted at the root of monorepo
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { Application } from 'spectron';
|
||||
|
||||
import Page from './pageObject';
|
||||
|
||||
import { TRIGGER_MAIN_MENU_ITEM } from '../src/testUtils/triggerMainMenu';
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
const DEBUG = process.env.XOD_DEBUG_TESTS;
|
||||
|
||||
const workspacePath = subPath => path.resolve(__dirname, '../../../workspace/', subPath);
|
||||
|
||||
describe('IDE', () => {
|
||||
let page;
|
||||
let app;
|
||||
let tmpHomeDir;
|
||||
let passed = true;
|
||||
|
||||
before(() => {
|
||||
const tmpDir = path.join(os.tmpdir(), 'xod-test-workspace-');
|
||||
|
||||
app = new Application({
|
||||
path: '../../node_modules/.bin/electron',
|
||||
args: ['.'],
|
||||
});
|
||||
|
||||
// With a dirty hack we extract out objects that we’d interact later
|
||||
// in tests and tear-down. Impure, but can’t figure out a better solution
|
||||
// with Mocha
|
||||
return fsp.mkdtempP(tmpDir)
|
||||
.then((home) => { process.env.USERDATA_DIR = process.env.HOME = tmpHomeDir = home; })
|
||||
.then(() => app.start())
|
||||
.then(() => {
|
||||
page = Page.createPageObject(app.client);
|
||||
chaiAsPromised.transferPromiseness = app.transferPromiseness;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function checkForFailure() {
|
||||
if (this.currentTest.state === 'failed') {
|
||||
passed = false;
|
||||
}
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (!passed || DEBUG) {
|
||||
app.client.getMainProcessLogs().then((logs) => {
|
||||
logs.forEach(console.log); // eslint-disable-line no-console
|
||||
});
|
||||
}
|
||||
|
||||
const shouldClose = app && app.isRunning() && (passed || !DEBUG);
|
||||
return shouldClose
|
||||
// TODO: simulating click on 'confirm' is probably cleaner
|
||||
? app.electron.ipcRenderer.send('CONFIRM_CLOSE_WINDOW')
|
||||
.then(() => fse.removeSync(tmpHomeDir))
|
||||
: Promise.resolve();
|
||||
});
|
||||
|
||||
it('opens a window', () =>
|
||||
assert.eventually.isTrue(app.browserWindow.isVisible())
|
||||
);
|
||||
|
||||
it('has proper title', () =>
|
||||
assert.eventually.strictEqual(app.client.getTitle(), 'XOD')
|
||||
);
|
||||
|
||||
describe('creating a new patch', () => {
|
||||
it('prompts for a name', () =>
|
||||
page.clickAddPatch()
|
||||
.then(() => page.assertPopupShown('Create new patch'))
|
||||
);
|
||||
|
||||
it('closes popup on confirm', () =>
|
||||
page.findPopup().setValue('input', 'my-blink')
|
||||
.then(() => page.confirmPopup())
|
||||
.then(() => page.assertNoPopups())
|
||||
);
|
||||
|
||||
it('opens new tab for the new patch', () =>
|
||||
page.assertActiveTabHasTitle('my-blink')
|
||||
);
|
||||
});
|
||||
|
||||
describe('xod/core library', () => {
|
||||
it('provides a patch group that is collapsed initially', () =>
|
||||
page.assertPatchGroupCollapsed('xod/core')
|
||||
);
|
||||
|
||||
it('expands on click', () =>
|
||||
page.findPatchGroup('xod/core').click()
|
||||
.then(() => page.assertPatchGroupExpanded('xod/core'))
|
||||
);
|
||||
|
||||
it('provides clock node', () =>
|
||||
page.assertNodeAvailableInProjectBrowser('clock')
|
||||
);
|
||||
});
|
||||
|
||||
describe('adding nodes to patch', () => {
|
||||
it('selects a node in project browser', () =>
|
||||
page.scrollToPatchInProjectBrowser('clock')
|
||||
.then(() => page.selectPatchInProjectBrowser('clock'))
|
||||
.then(() => page.assertPatchSelected('clock'))
|
||||
);
|
||||
|
||||
it('adds a node to patch', () =>
|
||||
page.clickAddNodeButton('clock')
|
||||
.then(() => page.findNode('clock').isVisible())
|
||||
);
|
||||
|
||||
it('drags a node in place', () =>
|
||||
assert.eventually.deepEqual(
|
||||
page.dragNode('clock', 150, 10).getLocation(),
|
||||
{ x: 387, y: 81 }
|
||||
)
|
||||
);
|
||||
|
||||
it('adds the rest of the nodes for the blink patch', () =>
|
||||
page.addNode('digital-output', 150, 300)
|
||||
.then(() => page.addNode('flip-flop', 150, 200))
|
||||
);
|
||||
});
|
||||
|
||||
describe('creating links between nodes', () => {
|
||||
it('activates linking mode when clicking on a pin', () =>
|
||||
page.findPin('clock', 'TICK').click()
|
||||
.then(() => page.assertPinIsSelected('clock', 'TICK'))
|
||||
);
|
||||
|
||||
it('marks pins that can be linked with selected pin', () =>
|
||||
Promise.all([
|
||||
page.assertPinIsAcceptingLinks('flip-flop', 'SET'),
|
||||
page.assertPinIsAcceptingLinks('flip-flop', 'TGL'),
|
||||
page.assertPinIsAcceptingLinks('flip-flop', 'RST'),
|
||||
])
|
||||
);
|
||||
|
||||
it('connects two compatible pins with a link', () =>
|
||||
page.findPin('flip-flop', 'TGL').click()
|
||||
.then(() => page.findLink('pulse'))
|
||||
);
|
||||
|
||||
it('adds the rest of the links for the blink patch', () =>
|
||||
page.findPin('flip-flop', 'MEM').click()
|
||||
.then(() => page.findPin('digital-output', 'SIG').click())
|
||||
);
|
||||
});
|
||||
|
||||
describe('binding values to inputs', () => {
|
||||
it('sets clock interval to 0.25', () =>
|
||||
page.bindValue('clock', 'IVAL', '0.25')
|
||||
);
|
||||
|
||||
it('sets digital-output port to 13', () =>
|
||||
page.bindValue('digital-output', 'PORT', '13')
|
||||
);
|
||||
});
|
||||
|
||||
describe('showing code for arduino', () => {
|
||||
const expectedCpp = fsp.readFileSync(
|
||||
workspacePath('blink/__fixtures__/arduino.cpp'),
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
it('shows code', () =>
|
||||
app.electron.ipcRenderer.emit(TRIGGER_MAIN_MENU_ITEM, ['Deploy', 'Show Code For Arduino'])
|
||||
.then(() => page.getCodeboxValue())
|
||||
.then(code => assert.strictEqual(code, expectedCpp, 'Actual and expected C++ don’t match'))
|
||||
);
|
||||
it('closes show code popup', () =>
|
||||
page.closePopup()
|
||||
);
|
||||
});
|
||||
|
||||
describe('deleting a patch', () => {
|
||||
it('deletes "my-blink" patch', () =>
|
||||
page.findPatchGroup('welcome-to-xod').click()
|
||||
.then(() => page.assertPatchGroupExpanded('welcome-to-xod'))
|
||||
.then(() => page.deletePatch('my-blink'))
|
||||
.then(() => page.assertNodeUnavailableInProjectBrowser('my-blink'))
|
||||
.then(() => page.assertTabWithTitleDoesNotExist('my-blink'))
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,7 @@
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.5",
|
||||
"commander": "^2.9.0",
|
||||
"debounce": "^1.1.0",
|
||||
"font-awesome": "^4.6.3",
|
||||
"formsy-react": "^0.18.1",
|
||||
"formsy-react-components": "^0.8.1",
|
||||
@@ -27,7 +28,7 @@
|
||||
"line-intersection": "^1.0.8",
|
||||
"prop-types": "^15.5.10",
|
||||
"ramda": "^0.24.1",
|
||||
"ramda-fantasy": "^0.7.0",
|
||||
"ramda-fantasy": "^0.8.0",
|
||||
"rc-menu": "^5.0.10",
|
||||
"rc-progress": "^2.1.2",
|
||||
"rc-switch": "^1.4.2",
|
||||
@@ -59,6 +60,7 @@
|
||||
"xod-arduino": "^0.15.0",
|
||||
"xod-func-tools": "^0.15.0",
|
||||
"xod-patch-search": "^0.15.0",
|
||||
"xod-pm": "^0.15.0",
|
||||
"xod-project": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -150,6 +150,7 @@ App.propTypes = {
|
||||
toggleDebugger: PropTypes.func.isRequired,
|
||||
logDebugger: PropTypes.func.isRequired,
|
||||
clearDebugger: PropTypes.func.isRequired,
|
||||
showLibSuggester: PropTypes.func.isRequired,
|
||||
/* eslint-enable react/no-unused-prop-types */
|
||||
}),
|
||||
};
|
||||
@@ -185,4 +186,5 @@ App.actions = {
|
||||
cutEntities: actions.cutEntities,
|
||||
copyEntities: actions.copyEntities,
|
||||
pasteEntities: actions.pasteEntities,
|
||||
showLibSuggester: actions.showLibSuggester,
|
||||
};
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
.PatchGroup {
|
||||
background-color: $sidebar-color-bg;
|
||||
|
||||
&__contentInner {
|
||||
border-top: 1px solid $sidebar-color-border;
|
||||
}
|
||||
|
||||
&__trigger {
|
||||
@mixin collapsibleBlock() {
|
||||
display: block;
|
||||
font-family: $font-family-normal;
|
||||
text-decoration: none;
|
||||
@@ -15,17 +11,6 @@
|
||||
padding: 10px 10px 10px 30px;
|
||||
color: $sidebar-color-text;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:after {
|
||||
font-family: 'FontAwesome';
|
||||
content: '\f0da';
|
||||
position: absolute;
|
||||
right: 13px;
|
||||
top: 12px;
|
||||
display: block;
|
||||
transition: transform 100ms;
|
||||
}
|
||||
|
||||
&.my:before,
|
||||
&.library:before {
|
||||
@@ -41,6 +26,43 @@
|
||||
&.library:before {
|
||||
content: '\f02d'; // TODO: bg image
|
||||
}
|
||||
}
|
||||
|
||||
&__contentInner {
|
||||
border-top: 1px solid $sidebar-color-border;
|
||||
}
|
||||
|
||||
&--installing {
|
||||
@include collapsibleBlock();
|
||||
cursor: wait;
|
||||
color: darken($sidebar-color-text, 20%);
|
||||
background: darken($sidebar-color-bg, 3%);
|
||||
|
||||
.fa-spin {
|
||||
position: absolute;
|
||||
right: 13px;
|
||||
top: 12px;
|
||||
display: block;
|
||||
}
|
||||
.version {
|
||||
margin-left: 0.25em;
|
||||
color: darken($sidebar-color-text, 40%);
|
||||
}
|
||||
}
|
||||
|
||||
&__trigger {
|
||||
@include collapsibleBlock();
|
||||
cursor: pointer;
|
||||
|
||||
&:after {
|
||||
font-family: 'FontAwesome';
|
||||
content: '\f0da';
|
||||
position: absolute;
|
||||
right: 13px;
|
||||
top: 12px;
|
||||
display: block;
|
||||
transition: transform 100ms;
|
||||
}
|
||||
|
||||
&.is-open:after {
|
||||
transform: rotateZ(90deg);
|
||||
|
||||
@@ -23,6 +23,22 @@
|
||||
margin-left: -490px;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 6px;
|
||||
width: 18px;
|
||||
height: 30px;
|
||||
background: $input-color-bg;
|
||||
font-size: $font-size-l;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
|
||||
.fa {
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
font-size: $font-size-l;
|
||||
@@ -58,6 +74,12 @@
|
||||
bottom: 0;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.error, .hint {
|
||||
display: block;
|
||||
margin: 1em 0 .5em 0;
|
||||
font-size: $font-size-m;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
@@ -73,6 +95,21 @@
|
||||
|
||||
border-radius: 2px;
|
||||
|
||||
&--library {
|
||||
height: 52px;
|
||||
|
||||
.add {
|
||||
display: block;
|
||||
font-size: $font-size-s;
|
||||
padding-top: 2px;
|
||||
|
||||
.author, .license {
|
||||
display: inline-block;
|
||||
padding-right: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-highlighted {
|
||||
background: $sidebar-color-bg-selected;
|
||||
}
|
||||
@@ -87,6 +124,11 @@
|
||||
margin-bottom: 2px;
|
||||
margin-top: -2px;
|
||||
line-height: 1.2;
|
||||
|
||||
.version {
|
||||
font-size: $font-size-m;
|
||||
margin-left: 0.2em;
|
||||
}
|
||||
}
|
||||
.description {
|
||||
display: block;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import R from 'ramda';
|
||||
import {
|
||||
listPatches,
|
||||
listLocalPatches,
|
||||
assocPatchUnsafe,
|
||||
getPatchPath,
|
||||
getPatchByPathUnsafe,
|
||||
@@ -123,11 +123,11 @@ export default (reducer, afterHistoryNavigation = R.identity) => (state, action)
|
||||
default: {
|
||||
const nextState = reducer(state, action);
|
||||
|
||||
const previousPatchesList = R.compose(listPatches, getProject)(state);
|
||||
const previousPatchesList = R.compose(listLocalPatches, getProject)(state);
|
||||
|
||||
const currentPatches = R.compose(
|
||||
R.indexBy(getPatchPath),
|
||||
listPatches,
|
||||
listLocalPatches,
|
||||
getProject
|
||||
)(nextState);
|
||||
|
||||
|
||||
@@ -23,4 +23,11 @@ export const SHOW_SUGGESTER = 'SHOW_SUGGESTER';
|
||||
export const HIDE_SUGGESTER = 'HIDE_SUGGESTER';
|
||||
export const HIGHLIGHT_SUGGESTER_ITEM = 'HIGHLIGHT_SUGGESTER_ITEM';
|
||||
|
||||
export const SHOW_LIB_SUGGESTER = 'SHOW_LIB_SUGGESTER';
|
||||
export const HIDE_LIB_SUGGESTER = 'HIDE_LIB_SUGGESTER';
|
||||
|
||||
export const START_DRAGGING_PATCH = 'START_DRAGGING_PATCH';
|
||||
|
||||
export const INSTALL_LIBRARY_BEGIN = 'INSTALL_LIBRARY_BEGIN';
|
||||
export const INSTALL_LIBRARY_COMPLETE = 'INSTALL_LIBRARY_COMPLETE';
|
||||
export const INSTALL_LIBRARY_FAILED = 'INSTALL_LIBRARY_FAILED';
|
||||
|
||||
@@ -2,6 +2,7 @@ import R from 'ramda';
|
||||
import { Maybe } from 'ramda-fantasy';
|
||||
|
||||
import * as XP from 'xod-project';
|
||||
import { fetchLibXodball, stringifyLibQuery } from 'xod-pm';
|
||||
|
||||
import {
|
||||
SELECTION_ENTITY_TYPE,
|
||||
@@ -10,6 +11,8 @@ import {
|
||||
} from './constants';
|
||||
import { LINK_ERRORS, CLIPBOARD_ERRORS } from '../messages/constants';
|
||||
|
||||
import { libInstalled } from './messages';
|
||||
|
||||
import * as ActionType from './actionTypes';
|
||||
|
||||
import {
|
||||
@@ -20,6 +23,7 @@ import {
|
||||
} from '../project/actions';
|
||||
import {
|
||||
addError,
|
||||
addConfirmation,
|
||||
} from '../messages/actions';
|
||||
|
||||
import * as Selectors from './selectors';
|
||||
@@ -43,6 +47,7 @@ import {
|
||||
resetClipboardEntitiesPosition,
|
||||
} from './utils';
|
||||
import { isInput, isEdge } from '../utils/browser';
|
||||
import { getPmSwaggerUrl } from '../utils/urls';
|
||||
import { addPoints } from '../project/nodeLayout';
|
||||
|
||||
import { ClipboardEntities } from '../types';
|
||||
@@ -271,6 +276,16 @@ export const highlightSugessterItem = patchPath => ({
|
||||
},
|
||||
});
|
||||
|
||||
// Suggester for libraries.
|
||||
// Maybe it will be merged with common suggester one day.
|
||||
export const showLibSuggester = () => ({
|
||||
type: ActionType.SHOW_LIB_SUGGESTER,
|
||||
payload: {},
|
||||
});
|
||||
export const hideLibSuggester = () => ({
|
||||
type: ActionType.HIDE_LIB_SUGGESTER,
|
||||
});
|
||||
|
||||
// Microsoft Edge only supports Text and URL data types
|
||||
const getClipboardDataType = () => (isEdge() ? 'Text' : CLIPBOARD_DATA_TYPE);
|
||||
|
||||
@@ -401,3 +416,43 @@ export const cutEntities = event => (dispatch) => {
|
||||
dispatch(copyEntities(event));
|
||||
dispatch(deleteSelection());
|
||||
};
|
||||
|
||||
export const installLibrary = reqParams => (dispatch) => {
|
||||
dispatch({
|
||||
type: ActionType.INSTALL_LIBRARY_BEGIN,
|
||||
payload: reqParams,
|
||||
});
|
||||
|
||||
const libName = `${reqParams.owner}/${reqParams.name}`;
|
||||
|
||||
fetchLibXodball(getPmSwaggerUrl(), stringifyLibQuery(reqParams))
|
||||
.then(xodball => R.compose(
|
||||
(patches) => {
|
||||
dispatch({
|
||||
type: ActionType.INSTALL_LIBRARY_COMPLETE,
|
||||
payload: {
|
||||
libName,
|
||||
request: reqParams,
|
||||
patches,
|
||||
xodball,
|
||||
},
|
||||
});
|
||||
dispatch(
|
||||
addConfirmation(libInstalled(libName, xodball.version))
|
||||
);
|
||||
},
|
||||
XP.prepareLibPatchesToInsertIntoProject,
|
||||
)(libName, xodball))
|
||||
.catch((err) => {
|
||||
dispatch({
|
||||
type: ActionType.INSTALL_LIBRARY_FAILED,
|
||||
payload: {
|
||||
libName,
|
||||
request: reqParams,
|
||||
error: err.message,
|
||||
errorCode: err.errorCode,
|
||||
},
|
||||
});
|
||||
dispatch(addError(err.message));
|
||||
});
|
||||
};
|
||||
|
||||
245
packages/xod-client/src/editor/components/LibSuggester.jsx
Normal file
245
packages/xod-client/src/editor/components/LibSuggester.jsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import R from 'ramda';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import Autosuggest from 'react-autosuggest';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import { Icon } from 'react-fa';
|
||||
import debounce from 'debounce';
|
||||
|
||||
import { fetchLibData, getLibVersion, isLibQueryValid } from 'xod-pm';
|
||||
import { isAmong } from 'xod-func-tools';
|
||||
|
||||
import { getPmSwaggerUrl } from '../../utils/urls';
|
||||
import { KEYCODE } from '../../utils/constants';
|
||||
import SuggesterContainer from './SuggesterContainer';
|
||||
import * as MSG from '../messages';
|
||||
|
||||
const getSuggestionValue = R.prop('requestParams');
|
||||
|
||||
const renderNothingFound = () => (
|
||||
<div className="error">
|
||||
{MSG.LIB_SUGGESTER_NOTHING_FOUND}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderTypeToBegin = () => (
|
||||
<div className="hint">
|
||||
{MSG.LIB_SUGGESTER_TYPE_TO_BEGIN}
|
||||
</div>
|
||||
);
|
||||
|
||||
class LibSuggester extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
notFound: false,
|
||||
value: '',
|
||||
suggestions: [],
|
||||
loading: false,
|
||||
};
|
||||
|
||||
this.input = null;
|
||||
|
||||
this.renderItem = this.renderItem.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this);
|
||||
this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this);
|
||||
this.onSuggestionSelected = this.onSuggestionSelected.bind(this);
|
||||
this.storeInputReference = this.storeInputReference.bind(this);
|
||||
this.renderContent = this.renderContent.bind(this);
|
||||
|
||||
// call it once to prepare debounced function
|
||||
this.fetchLibData = debounce(this.fetchLibData.bind(this), 500);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.input) {
|
||||
// A hack to avoid typing any character into input when pressing Hotkey
|
||||
setTimeout(() => this.input.focus(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
onChange(e, { newValue, method }) {
|
||||
if (isAmong(['up', 'down', 'click'], method)) return;
|
||||
|
||||
this.setState({
|
||||
value: newValue,
|
||||
notFound: (newValue === '') ? false : this.state.notFound,
|
||||
});
|
||||
}
|
||||
|
||||
onSelect(value) {
|
||||
this.props.onInstallLibrary(value);
|
||||
}
|
||||
|
||||
onSuggestionsFetchRequested({ reason }) {
|
||||
if (reason !== 'input-changed') return;
|
||||
if (isLibQueryValid(this.state.value)) {
|
||||
this.fetchLibData();
|
||||
}
|
||||
}
|
||||
|
||||
onSuggestionsClearRequested() {
|
||||
this.setState({
|
||||
suggestions: [],
|
||||
});
|
||||
}
|
||||
|
||||
onSuggestionSelected(event, { suggestionValue }) {
|
||||
this.onSelect(suggestionValue);
|
||||
}
|
||||
|
||||
fetchLibData() {
|
||||
const request = this.state.value;
|
||||
this.setState({
|
||||
loading: true,
|
||||
suggestions: [],
|
||||
notFound: false,
|
||||
});
|
||||
fetchLibData(getPmSwaggerUrl(), request)
|
||||
.then(R.of) // TODO: Once it will become an array
|
||||
.catch(R.always([]))
|
||||
.then(
|
||||
data => this.setState({
|
||||
suggestions: data,
|
||||
loading: false,
|
||||
notFound: (data.length === 0),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
storeInputReference(autosuggest) {
|
||||
if (autosuggest !== null) {
|
||||
this.input = autosuggest.input;
|
||||
}
|
||||
}
|
||||
|
||||
isNothingFound() {
|
||||
return (
|
||||
this.state.notFound &&
|
||||
this.state.value !== '' &&
|
||||
this.state.suggestions.length === 0 &&
|
||||
!this.state.loading
|
||||
);
|
||||
}
|
||||
isNothingSearched() {
|
||||
return (
|
||||
!this.state.notFound &&
|
||||
this.state.suggestions.length === 0 &&
|
||||
!this.state.loading
|
||||
);
|
||||
}
|
||||
|
||||
renderItem(item, { isHighlighted }) {
|
||||
const cls = classNames('Suggester-item Suggester-item--library', {
|
||||
'is-highlighted': isHighlighted,
|
||||
});
|
||||
|
||||
const license = (item.license)
|
||||
? <span className="license">{item.license}</span>
|
||||
: null;
|
||||
|
||||
const libName = `${item.owner}/${item.libname}`;
|
||||
|
||||
return (
|
||||
<div className={cls} title={libName}>
|
||||
<span className="path">
|
||||
<Highlighter
|
||||
searchWords={[this.state.value]}
|
||||
textToHighlight={libName}
|
||||
/>
|
||||
<span className="version">{getLibVersion(item)}</span>
|
||||
</span>
|
||||
<div className="add">
|
||||
<span className="author">by {item.owner}</span>
|
||||
{license}
|
||||
</div>
|
||||
<Highlighter
|
||||
className="description"
|
||||
searchWords={[this.state.value]}
|
||||
textToHighlight={item.description}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent(children) {
|
||||
if (this.isNothingFound()) {
|
||||
return renderNothingFound();
|
||||
}
|
||||
if (this.isNothingSearched()) {
|
||||
return renderTypeToBegin();
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { value, suggestions } = this.state;
|
||||
|
||||
const inputProps = {
|
||||
placeholder: 'Search libraries',
|
||||
value,
|
||||
onChange: this.onChange,
|
||||
onKeyDown: (event) => {
|
||||
const code = event.keyCode || event.which;
|
||||
if (code === KEYCODE.ESCAPE && event.target.value === '') {
|
||||
this.props.onBlur();
|
||||
}
|
||||
},
|
||||
onBlur: () => this.props.onBlur(),
|
||||
type: 'search',
|
||||
};
|
||||
|
||||
const loading = (this.state.loading) ? (
|
||||
<div className="loading-icon">
|
||||
<Icon
|
||||
name="circle-o-notch"
|
||||
spin
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const cls = `Suggester Suggester-libs ${this.props.addClassName}`;
|
||||
|
||||
return (
|
||||
<div className={cls}>
|
||||
{loading}
|
||||
<Autosuggest
|
||||
suggestions={suggestions}
|
||||
value={value}
|
||||
inputProps={inputProps}
|
||||
alwaysRenderSuggestions
|
||||
highlightFirstSuggestion
|
||||
getSuggestionValue={getSuggestionValue}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
renderSuggestion={this.renderItem}
|
||||
renderSuggestionsContainer={({ containerProps, children }) => (
|
||||
<SuggesterContainer containerProps={containerProps}>
|
||||
{this.renderContent(children)}
|
||||
</SuggesterContainer>
|
||||
)}
|
||||
ref={this.storeInputReference}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LibSuggester.defaultProps = {
|
||||
addClassName: '',
|
||||
onBlur: () => {},
|
||||
};
|
||||
|
||||
LibSuggester.propTypes = {
|
||||
addClassName: PropTypes.string,
|
||||
onInstallLibrary: PropTypes.func.isRequired,
|
||||
onBlur: PropTypes.func,
|
||||
};
|
||||
|
||||
export default LibSuggester;
|
||||
@@ -24,6 +24,7 @@ import { FOCUS_AREAS, DEBUGGER_TAB_ID } from '../constants';
|
||||
import Patch from './Patch';
|
||||
import NoPatch from '../components/NoPatch';
|
||||
import Suggester from '../components/Suggester';
|
||||
import LibSuggester from '../components/LibSuggester';
|
||||
import Inspector from '../components/Inspector';
|
||||
import Debugger from '../../debugger/containers/Debugger';
|
||||
import Breadcrumbs from '../../debugger/containers/Breadcrumbs';
|
||||
@@ -119,6 +120,14 @@ class Editor extends React.Component {
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const libSuggester = (this.props.isLibSuggesterVisible) ? (
|
||||
<LibSuggester
|
||||
addClassName={(this.props.isHelpbarVisible) ? 'with-helpbar' : ''}
|
||||
onInstallLibrary={this.props.actions.installLibrary}
|
||||
onBlur={this.props.actions.hideLibSuggester}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const DebuggerContainer = (this.props.isDebuggerVisible) ? <Debugger /> : null;
|
||||
const BreadcrumbsContainer = (
|
||||
this.props.isDebuggerVisible &&
|
||||
@@ -170,6 +179,7 @@ class Editor extends React.Component {
|
||||
{DebugSessionStopButton}
|
||||
{openedPatch}
|
||||
{suggester}
|
||||
{libSuggester}
|
||||
{DebuggerContainer}
|
||||
{BreadcrumbsContainer}
|
||||
</Workarea>
|
||||
@@ -195,6 +205,7 @@ Editor.propTypes = {
|
||||
isDebugSessionRunning: PropTypes.bool,
|
||||
suggesterIsVisible: PropTypes.bool,
|
||||
suggesterPlacePosition: PropTypes.object,
|
||||
isLibSuggesterVisible: PropTypes.bool,
|
||||
defaultNodePosition: PropTypes.object.isRequired,
|
||||
stopDebuggerSession: PropTypes.func,
|
||||
actions: PropTypes.shape({
|
||||
@@ -208,6 +219,8 @@ Editor.propTypes = {
|
||||
showSuggester: PropTypes.func.isRequired,
|
||||
hideSuggester: PropTypes.func.isRequired,
|
||||
highlightSugessterItem: PropTypes.func.isRequired,
|
||||
hideLibSuggester: PropTypes.func.isRequired,
|
||||
installLibrary: PropTypes.func.isRequired,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -219,6 +232,7 @@ const mapStateToProps = R.applySpec({
|
||||
patchesIndex: ProjectSelectors.getPatchSearchIndex,
|
||||
suggesterIsVisible: EditorSelectors.isSuggesterVisible,
|
||||
suggesterPlacePosition: EditorSelectors.getSuggesterPlacePosition,
|
||||
isLibSuggesterVisible: EditorSelectors.isLibSuggesterVisible,
|
||||
isHelpbarVisible: EditorSelectors.isHelpbarVisible,
|
||||
isDebuggerVisible: DebuggerSelectors.isDebuggerVisible,
|
||||
isDebugSessionRunning: DebuggerSelectors.isDebugSession,
|
||||
@@ -237,6 +251,8 @@ const mapDispatchToProps = dispatch => ({
|
||||
showSuggester: Actions.showSuggester,
|
||||
hideSuggester: Actions.hideSuggester,
|
||||
highlightSugessterItem: Actions.highlightSugessterItem,
|
||||
hideLibSuggester: Actions.hideLibSuggester,
|
||||
installLibrary: Actions.installLibrary,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
export const PATCH_FOR_NODE_IS_MISSING = 'Patch for this node is missing.';
|
||||
|
||||
export default {};
|
||||
export const libInstalled = (libName, version) => `${libName} @ ${version} installed successfully`;
|
||||
|
||||
export const LIB_SUGGESTER_TYPE_TO_BEGIN = 'Type owner/libname to find a library';
|
||||
export const LIB_SUGGESTER_NOTHING_FOUND = 'No library found';
|
||||
|
||||
@@ -534,6 +534,11 @@ const editorReducer = (state = {}, action) => {
|
||||
)(state);
|
||||
case EAT.HIGHLIGHT_SUGGESTER_ITEM:
|
||||
return R.assocPath(['suggester', 'highlightedPatchPath'], action.payload.patchPath, state);
|
||||
case EAT.SHOW_LIB_SUGGESTER:
|
||||
return R.assoc('libSuggesterVisible', true, state);
|
||||
case EAT.INSTALL_LIBRARY_BEGIN:
|
||||
case EAT.HIDE_LIB_SUGGESTER:
|
||||
return R.assoc('libSuggesterVisible', false, state);
|
||||
|
||||
//
|
||||
// debugger
|
||||
|
||||
@@ -135,6 +135,11 @@ export const getSuggesterHighlightedPatchPath = R.pipe(
|
||||
R.prop('highlightedPatchPath')
|
||||
);
|
||||
|
||||
export const isLibSuggesterVisible = R.pipe(
|
||||
getEditor,
|
||||
R.prop('libSuggesterVisible')
|
||||
);
|
||||
|
||||
//
|
||||
// debugger breadcrumbs
|
||||
//
|
||||
|
||||
@@ -22,4 +22,5 @@ export default {
|
||||
offset: DEFAULT_PANNING_OFFSET,
|
||||
},
|
||||
},
|
||||
libSuggesterVisible: false,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ import * as ProjectBrowserActions from './projectBrowser/actions';
|
||||
import * as PopupActions from './popups/actions';
|
||||
import * as DebuggerActions from './debugger/actions';
|
||||
|
||||
import { TAB_CLOSE } from './editor/actionTypes';
|
||||
import { TAB_CLOSE, INSTALL_LIBRARY_COMPLETE } from './editor/actionTypes';
|
||||
import { SAVE_PROJECT } from './project/actionTypes';
|
||||
|
||||
import * as EditorConstants from './editor/constants';
|
||||
@@ -54,7 +54,7 @@ export * from './projectBrowser/actions';
|
||||
export * from './popups/actions';
|
||||
export * from './debugger/actions';
|
||||
|
||||
export { TAB_CLOSE } from './editor/actionTypes';
|
||||
export { TAB_CLOSE, INSTALL_LIBRARY_COMPLETE } from './editor/actionTypes';
|
||||
export { SAVE_PROJECT } from './project/actionTypes';
|
||||
|
||||
export * from './editor/selectors';
|
||||
@@ -113,6 +113,7 @@ export default Object.assign({
|
||||
hasUnsavedChanges,
|
||||
TAB_CLOSE,
|
||||
SAVE_PROJECT,
|
||||
INSTALL_LIBRARY_COMPLETE,
|
||||
},
|
||||
UserSelectors,
|
||||
EditorSelectors,
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as XP from 'xod-project';
|
||||
import { explodeEither } from 'xod-func-tools';
|
||||
|
||||
import * as AT from './actionTypes';
|
||||
import { PASTE_ENTITIES } from '../editor/actionTypes';
|
||||
import { PASTE_ENTITIES, INSTALL_LIBRARY_COMPLETE } from '../editor/actionTypes';
|
||||
|
||||
import {
|
||||
addPoints,
|
||||
@@ -57,6 +57,19 @@ const moveEntities = (positionLens, deltaPosition) => R.map(
|
||||
)
|
||||
);
|
||||
|
||||
// :: LibName -> Project -> Project
|
||||
const omitLibPatches = R.curry(
|
||||
(libName, project) => R.compose(
|
||||
XP.omitPatches(R.__, project),
|
||||
R.filter(R.compose(
|
||||
R.equals(libName),
|
||||
XP.getLibraryName
|
||||
)),
|
||||
R.map(XP.getPatchPath),
|
||||
XP.listLibraryPatches
|
||||
)(project)
|
||||
);
|
||||
|
||||
export default (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
//
|
||||
@@ -148,6 +161,12 @@ export default (state = {}, action) => {
|
||||
);
|
||||
}
|
||||
|
||||
case INSTALL_LIBRARY_COMPLETE:
|
||||
return R.compose(
|
||||
XP.assocPatchListUnsafe(action.payload.patches),
|
||||
omitLibPatches(action.payload.libName)
|
||||
)(state);
|
||||
|
||||
|
||||
//
|
||||
// Bulk actions on multiple entities
|
||||
|
||||
@@ -145,7 +145,7 @@ const addDeadFlag = R.curry(
|
||||
// :: State -> StrMap RenderableNode
|
||||
export const getRenderableNodes = createMemoizedSelector(
|
||||
[getProject, getCurrentPatch, getCurrentPatchNodes, getConnectedPins],
|
||||
[R.T, R.equals, R.equals, R.equals],
|
||||
[R.equals, R.equals, R.equals, R.equals],
|
||||
(project, maybeCurrentPatch, currentPatchNodes, connectedPins) =>
|
||||
Maybe.maybe(
|
||||
{},
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
isPathLocal,
|
||||
getBaseName,
|
||||
} from 'xod-project';
|
||||
import { isAmong } from 'xod-func-tools';
|
||||
|
||||
import * as ProjectActions from '../../project/actions';
|
||||
import * as ProjectBrowserActions from '../actions';
|
||||
@@ -222,31 +223,59 @@ class ProjectBrowser extends React.Component {
|
||||
}
|
||||
|
||||
renderLibraryPatches() {
|
||||
const { libs, selectedPatchPath } = this.props;
|
||||
const { libs, installingLibs, selectedPatchPath } = this.props;
|
||||
const { setSelection, switchPatch, startDraggingPatch } = this.props.actions;
|
||||
const installingLibsComponents = R.map(
|
||||
({ owner, name, version }) => ({
|
||||
name: `${owner}/${name}`,
|
||||
component: (<div
|
||||
className="PatchGroup PatchGroup--installing library"
|
||||
>
|
||||
<span className="name">{owner}/{name}</span>
|
||||
<span className="version">{version}</span>
|
||||
<Icon name="circle-o-notch" spin />
|
||||
</div>),
|
||||
}),
|
||||
installingLibs
|
||||
);
|
||||
const installingLibNames = R.pluck('name', installingLibsComponents);
|
||||
|
||||
return R.toPairs(libs).map(([libName, libPatches]) => (
|
||||
<PatchGroup
|
||||
key={libName}
|
||||
type="library"
|
||||
name={libName}
|
||||
onClose={this.deselectIfInLibrary(libName)}
|
||||
>
|
||||
{libPatches.map(({ path, dead }) =>
|
||||
<PatchGroupItem
|
||||
key={path}
|
||||
patchPath={path}
|
||||
dead={dead}
|
||||
label={getBaseName(path)}
|
||||
isSelected={path === selectedPatchPath}
|
||||
onClick={() => setSelection(path)}
|
||||
onDoubleClick={() => switchPatch(path)}
|
||||
onBeginDrag={startDraggingPatch}
|
||||
hoverButtons={this.libraryPatchesHoveredButtons(path)}
|
||||
/>
|
||||
)}
|
||||
</PatchGroup>
|
||||
));
|
||||
const libComponents = R.compose(
|
||||
R.reject(R.compose(
|
||||
isAmong(installingLibNames),
|
||||
R.prop('name')
|
||||
)),
|
||||
R.map(([libName, libPatches]) => ({
|
||||
name: libName,
|
||||
component: (<PatchGroup
|
||||
key={libName}
|
||||
type="library"
|
||||
name={libName}
|
||||
onClose={this.deselectIfInLibrary(libName)}
|
||||
>
|
||||
{libPatches.map(({ path, dead }) =>
|
||||
<PatchGroupItem
|
||||
key={path}
|
||||
patchPath={path}
|
||||
dead={dead}
|
||||
label={getBaseName(path)}
|
||||
isSelected={path === selectedPatchPath}
|
||||
onClick={() => setSelection(path)}
|
||||
onDoubleClick={() => switchPatch(path)}
|
||||
onBeginDrag={startDraggingPatch}
|
||||
hoverButtons={this.libraryPatchesHoveredButtons(path)}
|
||||
/>
|
||||
)}
|
||||
</PatchGroup>),
|
||||
})),
|
||||
R.toPairs
|
||||
)(libs);
|
||||
|
||||
return R.compose(
|
||||
R.map(R.prop('component')),
|
||||
R.sortBy(R.prop('name')),
|
||||
R.concat
|
||||
)(libComponents, installingLibsComponents);
|
||||
}
|
||||
|
||||
renderPatches(patchType) {
|
||||
@@ -309,6 +338,7 @@ ProjectBrowser.propTypes = {
|
||||
localPatches: sanctuaryPropType($.Array(Patch)),
|
||||
popups: PropTypes.object.isRequired,
|
||||
libs: sanctuaryPropType($.StrMap($.Array(Patch))),
|
||||
installingLibs: PropTypes.array,
|
||||
defaultNodePosition: PropTypes.object.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
addNode: PropTypes.func.isRequired,
|
||||
@@ -335,6 +365,7 @@ const mapStateToProps = R.applySpec({
|
||||
localPatches: ProjectBrowserSelectors.getLocalPatches,
|
||||
popups: PopupSelectors.getProjectBrowserPopups,
|
||||
libs: ProjectBrowserSelectors.getLibs,
|
||||
installingLibs: ProjectBrowserSelectors.getInstallingLibraries,
|
||||
defaultNodePosition: EditorSelectors.getDefaultNodePlacePosition,
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ import {
|
||||
PATCH_RENAME,
|
||||
} from '../project/actionTypes';
|
||||
|
||||
import {
|
||||
INSTALL_LIBRARY_BEGIN,
|
||||
INSTALL_LIBRARY_COMPLETE,
|
||||
INSTALL_LIBRARY_FAILED,
|
||||
} from '../editor/actionTypes';
|
||||
|
||||
const selectionReducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case SET_SELECTION:
|
||||
@@ -36,10 +42,23 @@ const selectionReducer = (state, action) => {
|
||||
}
|
||||
};
|
||||
|
||||
const installingLibrariesReducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case INSTALL_LIBRARY_BEGIN:
|
||||
return R.append(action.payload, state);
|
||||
case INSTALL_LIBRARY_FAILED:
|
||||
case INSTALL_LIBRARY_COMPLETE:
|
||||
return R.reject(R.equals(action.payload.request), state);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default (state = initialState, action) =>
|
||||
R.merge(
|
||||
state,
|
||||
{
|
||||
selectedPatchPath: selectionReducer(state.selectedPatchPath, action),
|
||||
installingLibraries: installingLibrariesReducer(state.installingLibraries, action),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -69,3 +69,8 @@ export const getLibs = createMemoizedSelector(
|
||||
R.map(markDeadPatches(project))
|
||||
)(patches)
|
||||
);
|
||||
|
||||
export const getInstallingLibraries = R.compose(
|
||||
R.prop('installingLibraries'),
|
||||
getProjectBrowser
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export default {
|
||||
selectedPatchPath: null,
|
||||
installingLibraries: [],
|
||||
};
|
||||
|
||||
@@ -47,6 +47,10 @@ const rawItems = {
|
||||
label: 'New Patch',
|
||||
command: COMMAND.ADD_PATCH,
|
||||
},
|
||||
addLibrary: {
|
||||
key: 'addLibrary',
|
||||
label: 'Add Library',
|
||||
},
|
||||
|
||||
edit: {
|
||||
key: 'edit',
|
||||
|
||||
@@ -35,3 +35,6 @@ const HOSTNAME = process.env.XOD_HOSTNAME || 'xod.io';
|
||||
// :: String -> String
|
||||
export const getCompileLimitUrl = () =>
|
||||
`https://compile.${HOSTNAME}/limits`;
|
||||
|
||||
export const getPmSwaggerUrl = () =>
|
||||
`https://pm.${HOSTNAME}/swagger`;
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"author": "XOD Team <dev@xod.io>",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"file-save": "^0.2.0",
|
||||
"fs-extra": "^3.0.1",
|
||||
"hm-def": "^0.2.0",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
|
||||
@@ -6,4 +6,5 @@ export const CANT_COPY_STDLIB = 'CANT_COPY_STDLIB';
|
||||
export const CANT_COPY_DEFAULT_PROJECT = 'CANT_COPY_DEFAULT_PROJECT';
|
||||
export const CANT_ENUMERATE_PROJECTS = 'CANT_ENUMERATE_PROJECTS';
|
||||
export const CANT_SAVE_PROJECT = 'CANT_SAVE_PROJECT';
|
||||
export const CANT_SAVE_LIBRARY = 'CANT_SAVE_LIBRARY';
|
||||
export const INVALID_FILE_CONTENTS = 'INVALID_FILE_CONTENTS';
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as ERROR_CODES from './errorCodes';
|
||||
|
||||
export { default as pack } from './pack';
|
||||
export { arrangeByFiles, fsSafeName } from './unpack';
|
||||
export { saveArrangedFiles, saveProject } from './save';
|
||||
export { saveArrangedFiles, saveProject, saveProjectAsLibrary } from './save';
|
||||
export { writeJSON, writeFile } from './write';
|
||||
export { spawnWorkspaceFile, spawnStdLib, spawnDefaultProject } from './spawn';
|
||||
export { readDir, readFile, readJSON } from './read';
|
||||
|
||||
@@ -2,10 +2,10 @@ import path from 'path';
|
||||
import R from 'ramda';
|
||||
import { rejectWithCode } from 'xod-func-tools';
|
||||
|
||||
import { resolvePath, expandHomeDir, isPatchFile, isProjectFile } from './utils';
|
||||
import { resolvePath, expandHomeDir, isPatchFile, isProjectFile, resolveLibPath } from './utils';
|
||||
import { writeFile, writeJSON } from './write';
|
||||
import { Backup } from './backup';
|
||||
import { arrangeByFiles } from './unpack';
|
||||
import { arrangeByFiles, fsSafeName } from './unpack';
|
||||
import {
|
||||
omitDefaultOptionsFromPatchFileContents,
|
||||
omitDefaultOptionsFromProjectFileContents,
|
||||
@@ -49,10 +49,9 @@ export const saveArrangedFiles = R.curry((pathToWorkspace, data) => {
|
||||
|
||||
return Promise.all(savingFiles)
|
||||
.then(backup.clear)
|
||||
.catch((err) => {
|
||||
backup.restore()
|
||||
.then(() => { throw err; });
|
||||
});
|
||||
.catch(err => backup.restore()
|
||||
.then(() => Promise.reject(err))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,4 +75,17 @@ export const saveProject = R.curry(
|
||||
.catch(rejectWithCode(ERROR_CODES.CANT_SAVE_PROJECT))
|
||||
);
|
||||
|
||||
// :: String -> Project -> Path -> Promise Project Error
|
||||
export const saveProjectAsLibrary = R.curry(
|
||||
(owner, project, workspacePath) => {
|
||||
const distPath = R.compose(
|
||||
libPath => path.resolve(libPath, fsSafeName(owner)),
|
||||
resolveLibPath
|
||||
)(workspacePath);
|
||||
|
||||
return saveProject(distPath, project)
|
||||
.catch(rejectWithCode(ERROR_CODES.CANT_SAVE_LIBRARY));
|
||||
}
|
||||
);
|
||||
|
||||
export default saveProject;
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
import { curry, unless, test, concat, __ } from 'ramda';
|
||||
import fileSave from 'file-save';
|
||||
import { outputFile } from 'fs-extra';
|
||||
// import fileSave from 'file-save';
|
||||
import stringify from 'json-stable-stringify';
|
||||
import { resolvePath } from './utils';
|
||||
|
||||
// :: outputPath -> data -> Promise
|
||||
export const writeFile = curry((outputPath, data, encoding) => new Promise(
|
||||
(resolve, reject) => {
|
||||
const resolvedPath = resolvePath(outputPath);
|
||||
const fstream = fileSave(resolvedPath);
|
||||
export const writeFile = curry((outputPath, data, encoding) => {
|
||||
const resolvedPath = resolvePath(outputPath);
|
||||
|
||||
const dataWithEol = unless(
|
||||
test(/\n$/g),
|
||||
concat(__, '\n')
|
||||
)(data);
|
||||
const dataWithEol = unless(
|
||||
test(/\n$/g),
|
||||
concat(__, '\n')
|
||||
)(data);
|
||||
|
||||
fstream.write(dataWithEol, encoding);
|
||||
fstream.end();
|
||||
fstream.finish(() => resolve({ path: resolvedPath, data }));
|
||||
fstream.error((err) => {
|
||||
reject(Object.assign(err, { path: resolvedPath, data }));
|
||||
});
|
||||
}
|
||||
));
|
||||
return outputFile(resolvedPath, dataWithEol, encoding)
|
||||
.then(() => ({ path: resolvedPath, data }))
|
||||
.catch(
|
||||
err => Promise.reject(Object.assign(err, { path: resolvedPath, data }))
|
||||
);
|
||||
});
|
||||
|
||||
// :: outputPath -> data -> Promise
|
||||
export const writeJSON = curry((outputPath, data) =>
|
||||
writeFile(outputPath, stringify(data, { space: 2 }), 'utf8')
|
||||
.catch((err) => { throw Object.assign(err, { path: outputPath, data }); })
|
||||
);
|
||||
|
||||
export default {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"dependencies": {
|
||||
"hm-def": "^0.2.0",
|
||||
"ramda": "^0.22.1",
|
||||
"ramda-fantasy": "^0.7.0",
|
||||
"ramda-fantasy": "^0.8.0",
|
||||
"sanctuary-def": "^0.9.0",
|
||||
"sanctuary-type-identifiers": "^1.0.0"
|
||||
},
|
||||
|
||||
@@ -66,7 +66,9 @@ export const unfoldMaybeLibQuery = R.curry(
|
||||
() => rejectWithCode(
|
||||
ERR_CODES.CANT_PARSE_LIBRARY_REQUEST,
|
||||
new Error(MSG.CANT_PARSE_LIBRARY_REQUEST)
|
||||
),
|
||||
)(),
|
||||
// ^ this function call prevents from constructing Error without the need,
|
||||
// so it prevents falsy promise rejection
|
||||
justFn,
|
||||
maybe
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"dependencies": {
|
||||
"hm-def": "^0.2.0",
|
||||
"ramda": "^0.24.1",
|
||||
"ramda-fantasy": "^0.7.0",
|
||||
"ramda-fantasy": "^0.8.0",
|
||||
"sanctuary-def": "^0.9.0",
|
||||
"shortid": "^2.2.6",
|
||||
"xod-func-tools": "^0.15.0"
|
||||
|
||||
@@ -7,11 +7,15 @@ import {
|
||||
omitTypeHints,
|
||||
} from 'xod-func-tools';
|
||||
|
||||
import { getPatchPath } from './patch';
|
||||
import {
|
||||
getPatchPath,
|
||||
resolveNodeTypesInPatch,
|
||||
} from './patch';
|
||||
import {
|
||||
listLibraryPatches,
|
||||
omitPatches,
|
||||
injectProjectTypeHints,
|
||||
listPatchesWithoutBuiltIns,
|
||||
} from './project';
|
||||
import {
|
||||
addMissingOptionalProjectFields,
|
||||
@@ -68,3 +72,19 @@ export const toXodball = def(
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
export const prepareLibPatchesToInsertIntoProject = def(
|
||||
'prepareLibPatchesToInsertIntoProject :: String -> Project -> [Patch]',
|
||||
(libName, xodball) => R.compose(
|
||||
explodeEither,
|
||||
R.map(R.compose(
|
||||
R.map(R.compose(
|
||||
resolveNodeTypesInPatch,
|
||||
R.over(R.lensProp('path'), R.replace('@', libName)),
|
||||
)),
|
||||
listPatchesWithoutBuiltIns,
|
||||
)),
|
||||
fromXodballData
|
||||
)(xodball)
|
||||
);
|
||||
|
||||
@@ -3038,6 +3038,10 @@ dateformat@^1.0.11, dateformat@^1.0.12:
|
||||
get-stdin "^4.0.1"
|
||||
meow "^3.3.0"
|
||||
|
||||
debounce@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.1.0.tgz#6a1a4ee2a9dc4b7c24bb012558dbcdb05b37f408"
|
||||
|
||||
debug@2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
|
||||
|
||||
Reference in New Issue
Block a user