feature(user-menu) add (#221)

This commit is contained in:
coderaiser 2019-05-05 17:40:05 +03:00
parent f60af287d3
commit eb4f7c0d7c
34 changed files with 562 additions and 41 deletions

25
.cloudcmd.menu.js Normal file
View file

@ -0,0 +1,25 @@
'use strict';
module.exports = {
'F2 - Rename file': async ({DOM}) => {
await DOM.renameCurrent();
},
'D - Build Dev': async ({CloudCmd}) => {
await CloudCmd.TerminalRun.show({
command: 'npm run build:client:dev',
autoClose: false,
closeMessage: 'Press any button to close Terminal',
});
CloudCmd.refresh();
},
'P - Build Prod': async ({CloudCmd}) => {
await CloudCmd.TerminalRun.show({
command: 'npm run build:client',
autoClose: true,
});
CloudCmd.refresh();
},
};

View file

@ -21,6 +21,7 @@ const cssNames = [
'view',
'config',
'terminal',
'user-menu',
...getCSSList('columns'),
];
@ -35,7 +36,7 @@ const plugins = clean([
const rules = [{
test: /\.css$/,
exclude: /css\/(nojs|view|config|terminal|columns.*)\.css/,
exclude: /css\/(nojs|view|config|terminal|user-menu|columns.*)\.css/,
use: extractMain.extract([
'css-loader',
]),

View file

@ -32,7 +32,6 @@ const babelDev = {
babelrc: false,
plugins: [
'module:babel-plugin-macros',
'@babel/plugin-proposal-object-rest-spread',
],
};
@ -43,7 +42,7 @@ const rules = clean([
loader: 'babel-loader',
},
isDev && {
test: /sw\.js$/,
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: babelDev,
@ -90,7 +89,9 @@ module.exports = {
[modules + '/operation']: `${dirModules}/operation/index.js`,
[modules + '/konsole']: `${dirModules}/konsole.js`,
[modules + '/terminal']: `${dirModules}/terminal.js`,
[modules + '/terminal-run']: `${dirModules}/terminal-run.js`,
[modules + '/cloud']: `${dirModules}/cloud.js`,
[modules + '/user-menu']: `${dirModules}/user-menu/index.js`,
[modules + '/polyfill']: `${dirModules}/polyfill.js`,
},
output: {

View file

@ -10,6 +10,7 @@
".md"
],
"dictionary":[
"CloudCmd",
"Dev",
"Dropbox",
"Deepword",
@ -27,12 +28,14 @@
"Zalitok",
"WebSocket",
"auth",
"binded",
"cd",
"cloudcmd",
"coderaiser",
"com",
"dev",
"deepword",
"dev",
"destructuring",
"dropbox",
"dword",
"edward",

53
HELP.md
View file

@ -109,6 +109,7 @@ Cloud Commander supports the following command-line parameters:
| `--dropbox` | enable dropbox integration
| `--dropbox-token` | set dropbox token
| `--log` | enable logging
| `--user-menu` | enable user menu
| `--no-show-config` | do not show config values
| `--no-server` | do not start server
| `--no-auth` | disable authorization
@ -137,6 +138,7 @@ Cloud Commander supports the following command-line parameters:
| `--no-dropbox` | disable dropbox integration
| `--no-dropbox-token` | unset dropbox token
| `--no-log` | disable logging
| `--no-user-menu` | disable user menu
For options not specified by command-line parameters, Cloud Commander then reads configuration data from `~/.cloudcmd.json`. It uses port `8000` by default.
@ -162,7 +164,7 @@ Hot keys
|Key |Operation
|:----------------------|:--------------------------------------------
| `F1` | help
| `F2` | rename
| `F2` | rename or show `user menu`
| `F3` | view, change directory
| `Shift + F3` | view as markdown
| `F4` | edit
@ -421,7 +423,8 @@ Here's a description of all options:
"importListen" : false, // listen on config updates
"dropbox" : false, // disable dropbox integration
"dropboxToken" : "", // unset dropbox token
"log" : true // logging
"log" : true, // logging
"userMenu" : false // do not show user menu
}
```
@ -458,6 +461,52 @@ Some config options can be overridden with environment variables, such as:
- `CLOUDCMD_IMPORT_TOKEN` - authorization token used to connect to export server
- `CLOUDCMD_IMPORT_URL` - url of an import server
- `CLOUDCMD_IMPORT_LISTEN`- enable listen on config updates from import server
- `CLOUDCMD_USER_MENU`- enable `user menu`
### User Menu
You can enable `user menu` with help of a flag `--user-menu` (consider that file rename using `F2` will be disabled).
When you press `F2` Cloud Commander will a file `.cloudcmd.menu.js` by walking up parent directories, if can't read it will try to read `~/.cloudcmd.menu.js`.
Let's consider example `user menu` works file:
```js
module.exports = {
'F2 - Rename file': async ({DOM}) => {
await DOM.renameCurrent();
},
'D - Build Dev': async ({CloudCmd}) => {
await CloudCmd.TerminalRun.show({
command: 'npm run build:client:dev',
autoClose: false,
closeMessage: 'Press any button to close Terminal',
});
CloudCmd.refresh();
},
'P - Build Prod': async ({CloudCmd}) => {
await CloudCmd.TerminalRun.show({
command: 'npm run build:client',
autoClose: true,
});
CloudCmd.refresh();
},
};
```
You will have ability to run one of this 3 commands with help of double click, enter, or binded key (`F2`, `D` or `P` in this example). Also you can run commands in terminal, or execute any built-in function of `Cloud Commander` extended it's interface.
#### User Menu API
Here you can find `API` that can be used in **User Menu**. **DOM** and **CloudCmd** to main objects you receive in arguments list using destructuring.
**DOM** contains all base functions of `Cloud Commander` (rename, remove, download etc);
- `renameCurrent` - shows renames current file dialog, and does renaming.
**CloudCmd** contains all modules (`Terminal`, `View`, `Edit`, `Config`, `Console` etc);
- `TerminalRun` - module that shows `Terminal` with a `command` from options and closes terminal when everything is done.
### Distribute

View file

@ -73,6 +73,7 @@ const args = require('minimist')(argv.slice(2), {
'import-listen',
'log',
'dropbox',
'user-menu',
],
default: {
server : true,
@ -111,6 +112,7 @@ const args = require('minimist')(argv.slice(2), {
'one-file-panel': choose(env.bool('one_file_panel'), config('oneFilePanel')),
'confirm-copy': choose(env.bool('confirm_copy'), config('confirmCopy')),
'confirm-move': choose(env.bool('confirm_move'), config('confirmMove')),
'user-menu': choose(env.bool('user_menu'), config('userMenu')),
'keys-panel': env.bool('keys_panel') || config('keysPanel'),
'import-token': env('import_token') || config('importToken'),
'export-token': env('export_token') || config('exportToken'),
@ -180,6 +182,7 @@ function main() {
config('importToken', args['import-token']);
config('importListen', args['import-listen']);
config('importUrl', args['import-url']);
config('userMenu', args['user-menu']);
config('dropbox', args['dropbox']);
config('dropboxToken', args['dropbox-token'] || '');

View file

@ -5,6 +5,8 @@
const itype = require('itype/legacy');
const exec = require('execon');
const jonny = require('jonny/legacy');
const tryToCatch = require('try-to-catch/legacy');
const Util = require('../../common/util');
const Images = require('./images');
@ -746,7 +748,7 @@ function CmdProto() {
*
* @currentFile
*/
this.renameCurrent = (current) => {
this.renameCurrent = async (current) => {
const {Dialog} = DOM;
if (!DOM.isCurrentFile(current))
@ -757,30 +759,30 @@ function CmdProto() {
if (from === '..')
return Dialog.alert.noFiles();
const cancel = false;
const [e, to] = await tryToCatch(Dialog.prompt, 'Rename', from);
Dialog.prompt('Rename', from, {cancel}).then((to) => {
const isExist = !!DOM.getCurrentByName(to);
const dirPath = DOM.getCurrentDirPath();
if (from === to)
if (e)
return;
const isExist = !!DOM.getCurrentByName(to);
const dirPath = DOM.getCurrentDirPath();
if (from === to)
return;
const files = {
from : dirPath + from,
to : dirPath + to,
};
RESTful.mv(files, (error) => {
if (error)
return;
const files = {
from : dirPath + from,
to : dirPath + to,
};
DOM.setCurrentName(to, current);
Storage.remove(dirPath);
RESTful.mv(files, (error) => {
if (error)
return;
DOM.setCurrentName(to, current);
Storage.remove(dirPath);
if (isExist)
CloudCmd.refresh();
});
if (isExist)
CloudCmd.refresh();
});
};
@ -940,7 +942,7 @@ function CmdProto() {
const info = DOM.CurrentInfo;
const current = currentFile || DOM.getCurrentFile();
const files = current.parentElement;
const panel = files.parentElement;
const panel = files.parentElement || DOM.getPanel();
const panelPassive = DOM.getPanel({
active: false,

View file

@ -186,6 +186,9 @@ function KeyProto() {
break;
case Key.F2:
if (CloudCmd.config('userMenu'))
return CloudCmd.UserMenu.show();
DOM.renameCurrent(current);
break;

View file

@ -123,7 +123,7 @@ module.exports.initKeysPanel = () => {
const clickFuncs = {
'f1' : CloudCmd.Help.show,
'f2' : DOM.renameCurrent,
'f2' : initF2,
'f3' : CloudCmd.View.show,
'f4' : CloudCmd.EditFile.show,
'f5' : operation('copy'),
@ -141,6 +141,13 @@ module.exports.initKeysPanel = () => {
});
};
function initF2() {
if (CloudCmd.config('userMenu'))
return CloudCmd.UserMenu.show();
return DOM.renameCurrent();
}
const getPanel = (side) => {
if (!itype.string(side))
return side;

View file

@ -1,7 +1,6 @@
'use strict';
/* global DOM */
/* global CloudCmd */
const {
Dialog,

View file

@ -0,0 +1,146 @@
'use strict';
/* global CloudCmd, gritty */
const {promisify} = require('es6-promisify');
const tryToCatch = require('try-to-catch/legacy');
require('../../css/terminal.css');
const exec = require('execon');
const load = require('load.js');
const DOM = require('../dom');
const Images = require('../dom/images');
const loadParallel = promisify(load.parallel);
const {Dialog} = DOM;
const {
Key,
config,
} = CloudCmd;
CloudCmd.TerminalRun = exports;
let Loaded;
let Terminal;
let Socket;
const loadAll = async () => {
const {prefix} = CloudCmd;
const prefixGritty = getPrefix();
const js = `${prefixGritty}/gritty.js`;
const css = `${prefix}/dist/terminal.css`;
const [e] = await tryToCatch(loadParallel, [js, css]);
if (e) {
const src = e.target.src.replace(window.location.href, '');
return Dialog.alert(`file ${src} could not be loaded`);
}
Loaded = true;
};
module.exports.init = async () => {
if (!config('terminal'))
return;
Images.show.load('top');
await CloudCmd.View();
await loadAll();
};
module.exports.show = show;
module.exports.hide = hide;
function hide () {
CloudCmd.View.hide();
}
function getPrefix() {
return CloudCmd.prefix + '/gritty';
}
function getPrefixSocket() {
return CloudCmd.prefixSocket + '/gritty';
}
function getEnv() {
return {
ACTIVE_DIR: DOM.getCurrentDirPath,
PASSIVE_DIR: DOM.getNotCurrentDirPath,
CURRENT_NAME: DOM.getCurrentName,
CURRENT_PATH: DOM.getCurrentPath,
};
}
function create(createOptions) {
const {
command,
autoClose,
closeMessage = 'Press any key to close Terminal...',
} = createOptions;
const options = {
env: getEnv(),
prefix: getPrefixSocket(),
socketPath: CloudCmd.prefix,
fontFamily: 'Droid Sans Mono',
command,
autoRestart: false,
};
let commandExit = false;
const {socket, terminal} = gritty(document.body, options);
Socket = socket;
Terminal = terminal;
Terminal.on('key', (char, {keyCode, shiftKey}) => {
if (commandExit)
hide();
if (shiftKey && keyCode === Key.ESC) {
hide();
}
});
Socket.on('exit', () => {
if (autoClose)
return hide();
terminal.write(`\n${closeMessage}`);
commandExit = true;
});
Socket.on('connect', exec.with(authCheck, socket));
}
function authCheck(spawn) {
spawn.emit('auth', config('username'), config('password'));
spawn.on('reject', () => {
Dialog.alert('Wrong credentials!');
});
}
async function show(options = {}) {
if (!Loaded)
return;
if (!config('terminal'))
return;
await create(options);
CloudCmd.View.show(Terminal.element, {
afterShow: () => {
Terminal.focus();
},
});
}

View file

@ -24,6 +24,7 @@ CloudCmd.Terminal = exports;
let Loaded;
let Terminal;
let Socket;
const loadAll = async () => {
const {prefix} = CloudCmd;
@ -84,17 +85,19 @@ function create() {
socketPath: CloudCmd.prefix,
fontFamily: 'Droid Sans Mono',
};
const {socket, terminal} = gritty(document.body, options);
Socket = socket;
Terminal = terminal;
terminal.on('key', (char, {keyCode, shiftKey}) => {
Terminal.on('key', (char, {keyCode, shiftKey}) => {
if (shiftKey && keyCode === Key.ESC) {
hide();
}
});
socket.on('connect', exec.with(authCheck, socket));
Socket.on('connect', exec.with(authCheck, socket));
}
function authCheck(spawn) {
@ -105,7 +108,7 @@ function authCheck(spawn) {
});
}
function show(callback) {
function show() {
if (!Loaded)
return;
@ -114,10 +117,7 @@ function show(callback) {
CloudCmd.View.show(Terminal.element, {
afterShow: () => {
if (Terminal)
Terminal.focus();
exec(callback);
Terminal.focus();
},
});
}

View file

@ -0,0 +1,20 @@
'use strict';
const defaultUserMenu = {
'F2 - Rename file': async ({DOM}) => {
DOM.renameCurrent();
},
};
module.exports = (menuFn) => {
if (!menuFn)
return defaultUserMenu;
const module = {};
const fn = Function('module', menuFn);
fn(module);
return module.exports;
};

View file

@ -0,0 +1,29 @@
'use strict';
const test = require('supertape');
const getUserMenu = require('./get-user-menu');
test('user-menu: getUserMenu', (t) => {
const menu = `module.exports = {
'F2 - Rename file': ({DOM}) => {
const {element} = DOM.CurrentInfo;
DOM.renameCurrent(element);
}
}`;
const result = getUserMenu(menu);
const [key] = Object.keys(result);
t.equal(key, 'F2 - Rename file', 'should equal');
t.end();
});
test('user-menu: getUserMenu: no args', (t) => {
const result = getUserMenu();
const [key] = Object.keys(result);
t.equal(key, 'F2 - Rename file', 'should equal');
t.end();
});

View file

@ -0,0 +1,112 @@
'use strict';
/* global CloudCmd, DOM */
require('../../../css/user-menu.css');
const currify = require('currify/legacy');
const {promisify} = require('es6-promisify');
const load = require('load.js');
const createElement = require('@cloudcmd/create-element');
const Images = require('../../dom/images');
const getUserMenu = require('./get-user-menu');
const loadCSS = promisify(load.css);
const Name = 'UserMenu';
CloudCmd[Name] = module.exports;
const {Key} = CloudCmd;
module.exports.init = async () => {
await Promise.all([
loadCSS(`${CloudCmd.prefix}/dist/user-menu.css`),
CloudCmd.View(),
]);
};
module.exports.show = show;
module.exports.hide = hide;
const getKey = (a) => a.split(' - ')[0];
const beginWith = (a) => (b) => !b.indexOf(a);
const {CurrentInfo} = DOM;
async function show() {
Images.show.load('top');
const {dirPath} = CurrentInfo;
const res = await fetch(`/api/v1/user-menu?dir=${dirPath}`);
const userMenu = getUserMenu(await res.text());
const options = Object.keys(userMenu);
const el = createElement('select', {
className: 'cloudcmd-user-menu',
innerHTML: fillTemplate(options),
size: 10,
});
const keys = options.map(getKey);
el.addEventListener('keydown', onKeyDown(keys, options, userMenu));
el.addEventListener('dblclick', onDblClick(options, userMenu));
const afterShow = () => el.focus();
const autoSize = true;
Images.hide();
CloudCmd.View.show(el, {
autoSize,
afterShow,
});
}
function fillTemplate(options) {
const result = [];
for (const option of options) {
result.push(`<option>${option}</option>`);
}
return result.join('');
}
function hide() {
CloudCmd.View.hide();
}
const onDblClick = currify(async (options, userMenu, e) => {
const {value} = e.target;
await runUserMenu(value, options, userMenu);
});
const onKeyDown = currify(async (keys, options, userMenu, e) => {
const {keyCode} = e;
const key = e.key.toUpperCase();
let value;
if (keyCode === Key.ENTER)
({value} = e.target);
else if (keys.includes(key))
value = options.find(beginWith(key));
else
return;
e.preventDefault();
e.stopPropagation();
await runUserMenu(value, options, userMenu);
});
const runUserMenu = async (value, options, userMenu) => {
hide();
await userMenu[value]({
DOM,
CloudCmd,
});
};

View file

@ -132,3 +132,8 @@
font-family : 'Fontello';
content : '\e81b ';
}
.icon-user-menu::before {
font-family : 'Fontello';
content : '\e81c ';
}

14
css/user-menu.css Normal file
View file

@ -0,0 +1,14 @@
.cloudcmd-user-menu {
font-size: 16px;
font-family: 'Droid Sans Mono', 'Ubuntu Mono', 'Consolas', monospace;
width: 400px;
}
.cloudcmd-user-menu:focus {
outline: 0;
}
.cloudcmd-user-menu > option:checked {
box-shadow: 20px -20px 0 2px rgba(49, 123, 249) inset;
}

Binary file not shown.

View file

@ -174,6 +174,12 @@
"code": 59413,
"src": "entypo"
},
{
"uid": "f805bb95d40c7ef2bc51b3d50d4f2e5c",
"css": "th-list",
"code": 59420,
"src": "fontawesome"
},
{
"uid": "60617c8adc1e7eb3c444a5491dd13f57",
"css": "attention-circled-1",

View file

@ -1,7 +1,7 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2018 by original authors @ fontello.com</metadata>
<metadata>Copyright (C) 2019 by original authors @ fontello.com</metadata>
<defs>
<font id="fontello" horiz-adv-x="1000" >
<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
@ -61,6 +61,8 @@
<glyph glyph-name="logout" unicode="&#xe81a;" d="M502 0l0 100 98 0 0-100q0-40-29-70t-71-30l-400 0q-40 0-70 30t-30 70l0 700q0 42 30 71t70 29l400 0q42 0 71-29t29-71l0-150-98 0 0 150-402 0 0-700 402 0z m398 326l-198-196 0 120-450 0 0 150 450 0 0 120z" horiz-adv-x="900" />
<glyph glyph-name="terminal-1" unicode="&#xe81b;" d="M1360 849v-1000h-1360v1000h1360z m-838-600h318v77h-318v-77z m-362 77l317 135v96l-317 134v-99l209-84-209-83v-99z" horiz-adv-x="1360" />
<glyph glyph-name="th-list" unicode="&#xe81c;" d="M286 154v-108q0-22-16-37t-38-16h-178q-23 0-38 16t-16 37v108q0 22 16 38t38 15h178q23 0 38-15t16-38z m0 285v-107q0-22-16-38t-38-15h-178q-23 0-38 15t-16 38v107q0 23 16 38t38 16h178q23 0 38-16t16-38z m714-285v-108q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v108q0 22 16 38t38 15h535q23 0 38-15t16-38z m-714 571v-107q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v107q0 22 16 38t38 16h178q23 0 38-16t16-38z m714-286v-107q0-22-16-38t-38-15h-535q-23 0-38 15t-16 38v107q0 23 16 38t38 16h535q23 0 38-16t16-38z m0 286v-107q0-22-16-38t-38-16h-535q-23 0-38 16t-16 38v107q0 22 16 38t38 16h535q23 0 38-16t16-38z" horiz-adv-x="1000" />
</font>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -21,6 +21,7 @@
<div id="js-keyspanel" class="keyspanel">
<button id=f1 class="cmd-button reduce-text icon-help" title="Help" >F1</button>
<button id=f2 class="cmd-button reduce-text icon-rename" title="Rename" >F2</button>
<button id=f2 class="cmd-button reduce-text icon-user-menu" title="User Menu" >F2</button>
<button id=f3 class="cmd-button reduce-text icon-view" title="View" >F3</button>
<button id=f4 class="cmd-button reduce-text icon-edit " title="Edit" >F4</button>
<button id=f5 class="cmd-button reduce-text icon-copy" title="Copy" >F5</button>

View file

@ -43,6 +43,7 @@
"importListen": false,
"log": true,
"dropbox": false,
"dropboxToken": ""
"dropboxToken": "",
"userMenu": false
}

View file

@ -42,6 +42,7 @@
"--dropbox ": "enable dropbox integration",
"--dropbox-token ": "set dropbox token",
"--log ": "enable logging",
"--user-menu ": "enable user menu",
"--no-show-config ": "do not show config values",
"--no-server ": "do not start server",
"--no-auth ": "disable authorization",
@ -70,5 +71,6 @@
"--no-show-file-name ": "do not show file name in view and edit",
"--no-dropbox ": "disable dropbox integration",
"--no-dropbox-token ": "unset dropbox token",
"--no-log ": "disable logging"
"--no-log ": "disable logging",
"--no-user-menu ": "disable user menu"
}

View file

@ -15,7 +15,9 @@
"operation",
"konsole",
"terminal",
"cloud"
"terminal-run",
"cloud",
"user-menu"
],
"remote": [{
"name": "socket",

View file

@ -65,6 +65,7 @@ programs in browser from any computer, mobile or tablet device.
--dropbox enable dropbox integration
--dropbox-token set dropbox token
--log enable logging
--user-menu enable user menu
--no-show-config do not show config values
--no-server do not start server
--no-auth disable authorization
@ -94,6 +95,7 @@ programs in browser from any computer, mobile or tablet device.
--no-dropbox disable dropbox integration
--no-dropbox-token unset dropbox token
--no-log disable logging
--no-user-menu disable user menu
.SH RESOURCES AND DOCUMENTATION

View file

@ -102,6 +102,7 @@
"execon": "^1.2.0",
"express": "^4.13.0",
"files-io": "^3.0.0",
"find-up": "^4.0.0",
"flop": "^6.0.0",
"for-each-key": "^1.0.1",
"format-io": "^1.0.0",

View file

@ -11,6 +11,7 @@ const cloudfunc = require(DIR_COMMON + 'cloudfunc');
const authentication = require(DIR + 'auth');
const config = require(DIR + 'config');
const modulas = require(DIR + 'modulas');
const userMenu = require(DIR + 'user-menu');
const rest = require(DIR + 'rest');
const route = require(DIR + 'route');
const validate = require(DIR + 'validate');
@ -223,6 +224,7 @@ function cloudcmd(prefix, plugins, modules) {
root,
}),
userMenu,
rest,
route({
html: defaultHtml,

View file

@ -111,6 +111,7 @@ function indexProcessing(options) {
const noConfig = !config('configDialog');
const noConsole = !config('console');
const noTerminal = !config('terminal');
const noUserMenu = !config('userMenu');
const {panel} = options;
let {data} = options;
@ -139,6 +140,13 @@ function indexProcessing(options) {
data = data
.replace('icon-terminal', 'icon-terminal none');
if (noUserMenu)
data = data
.replace('icon-user-menu', 'icon-user-menu none');
else
data = data
.replace('icon-rename', 'icon-rename none');
const left = rendy(Template.panel, {
side : 'left',
content : panel,

View file

@ -264,6 +264,7 @@ test('cloudcmd: route: realpath: error', async (t) => {
options,
});
/*eslint require-atomic-updates:0*/
fs.realpath = realpath;
t.ok(/^ENOENT/.test(body), 'should return error');

53
server/user-menu.js Normal file
View file

@ -0,0 +1,53 @@
'use strict';
const {homedir} = require('os');
const fs = require('fs');
const {join} = require('path');
const {promisify} = require('util');
const tryToCatch = require('try-to-catch');
const findUp = require('find-up');
const readFile = promisify(fs.readFile);
const menuName = '.cloudcmd.menu.js';
const homeMenuPath = join(homedir(), menuName);
module.exports = async (req, res, next) => {
if (req.url.indexOf('/api/v1/user-menu'))
return next();
const {method} = req;
if (method === 'GET')
return onGET(req.query, res);
next();
};
async function onGET({dir}, res) {
const [errorFind, currentMenuPath] = await tryToCatch(findUp, [
menuName,
], {cwd: dir});
if (errorFind && errorFind.code !== 'ENOENT')
return res
.status(404)
.send(e.message);
if (errorFind && errorFind.code === 'ENOENT')
return res.send('');
const menuPath = currentMenuPath || homeMenuPath;
const [e, data] = await tryToCatch(readFile, menuPath, 'utf8');
if (!e)
return res.send(data);
if (e.code !== 'ENOENT')
return res
.status(404)
.send(e.message);
return res.send('');
}

21
server/user-menu.spec.js Normal file
View file

@ -0,0 +1,21 @@
'use strict';
const fs = require('fs');
const {join} = require('path');
const test = require('supertape');
const serveOnce = require('serve-once');
const userMenu = require('./user-menu');
const {request} = serveOnce(() => userMenu);
const userMenuPath = join(__dirname, '..', '.cloudcmd.menu.js');
const userMenuFile = fs.readFileSync(userMenuPath, 'utf8');
test('cloudcmd: user menu', async (t) => {
const {body} = await request.get(`/api/v1/user-menu?dir=${__dirname}`);
t.equal(userMenuFile, body, 'should equal');
t.end();
});