mirror of
https://github.com/Kong/insomnia
synced 2024-11-07 22:30:15 +00:00
Better Nunjucks Tag Editor (#234)
* Helper to tokenize Nunjucks tag * More granular types * Add tag definitions * Improve editor to be more WYSIWYG * Fixed tests * Added raw response tag
This commit is contained in:
parent
461a36dec6
commit
79b3f3fe7c
@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
build
|
||||
dist
|
||||
coverage
|
@ -37,8 +37,117 @@ describe('getKeys()', () => {
|
||||
};
|
||||
|
||||
const keys = utils.getKeys(obj);
|
||||
expect(keys).toEqual([
|
||||
{name: 'foo', value: 'bar'}
|
||||
]);
|
||||
expect(keys).toEqual([{name: 'foo', value: 'bar'}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tokenizeTag()', () => {
|
||||
it('tokenizes complex tag', () => {
|
||||
const actual = utils.tokenizeTag(
|
||||
`{% name bar, "baz \\"qux\\"" , 1 + 5 | default("foo") %}`
|
||||
);
|
||||
|
||||
const expected = {
|
||||
name: 'name',
|
||||
args: [
|
||||
{type: 'variable', value: 'bar'},
|
||||
{type: 'string', value: 'baz "qux"', quotedBy: '"'},
|
||||
{type: 'expression', value: '1 + 5 | default("foo")'}
|
||||
]
|
||||
};
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('handles whitespace', () => {
|
||||
const minimal = utils.tokenizeTag(`{%name'foo',bar%}`);
|
||||
const generous = utils.tokenizeTag(`{%name \t'foo' , bar\t\n%}`);
|
||||
|
||||
const expected = {
|
||||
name: 'name',
|
||||
args: [
|
||||
{type: 'string', value: 'foo', quotedBy: "'"},
|
||||
{type: 'variable', value: 'bar'}
|
||||
]
|
||||
};
|
||||
|
||||
expect(minimal).toEqual(expected);
|
||||
expect(generous).toEqual(expected);
|
||||
});
|
||||
|
||||
it('handles type string', () => {
|
||||
const actual = utils.tokenizeTag(`{% name 'foo' %}`);
|
||||
|
||||
const expected = {
|
||||
name: 'name',
|
||||
args: [{type: 'string', value: 'foo', quotedBy: "'"}]
|
||||
};
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('handles type number', () => {
|
||||
const actual = utils.tokenizeTag(`{% name 1.222, 123 %}`);
|
||||
|
||||
const expected = {
|
||||
name: 'name',
|
||||
args: [
|
||||
{type: 'number', value: '1.222'},
|
||||
{type: 'number', value: '123'}
|
||||
]
|
||||
};
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('handles type boolean', () => {
|
||||
const actual = utils.tokenizeTag(`{% name true, false %}`);
|
||||
|
||||
const expected = {
|
||||
name: 'name',
|
||||
args: [
|
||||
{type: 'boolean', value: 'true'},
|
||||
{type: 'boolean', value: 'false'}
|
||||
]
|
||||
};
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('handles type expression', () => {
|
||||
const actual = utils.tokenizeTag(`{% name 5 * 10 + 'hello' | default(2 - 3) %}`);
|
||||
|
||||
const expected = {
|
||||
name: 'name',
|
||||
args: [{type: 'expression', value: `5 * 10 + 'hello' | default(2 - 3)`}]
|
||||
};
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
/**
|
||||
* NOTE: This is actually invalid Nunjucks but we handle it anyway
|
||||
* because it's better (and easier to implement) than failing.
|
||||
*/
|
||||
it('handles no commas', () => {
|
||||
const actual = utils.tokenizeTag(`{% name foo bar baz %}`);
|
||||
|
||||
const expected = {
|
||||
name: 'name',
|
||||
args: [{type: 'expression', value: 'foo bar baz'}]
|
||||
};
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unTokenizeTag()', () => {
|
||||
it('untokenizes a tag', () => {
|
||||
const tagStr = `{% name bar, "baz \\"qux\\"" , 1 + 5, 'hi' %}`;
|
||||
|
||||
const tagData = utils.tokenizeTag(tagStr);
|
||||
const result = utils.unTokenizeTag(tagData);
|
||||
|
||||
expect(result).toEqual(`{% name bar, "baz \\"qux\\"", 1 + 5, 'hi' %}`);
|
||||
});
|
||||
});
|
||||
|
@ -25,6 +25,17 @@ describe('ResponseExtension General', async () => {
|
||||
expect(err.message).toContain('Could not find request req_test');
|
||||
}
|
||||
});
|
||||
|
||||
it('fails on empty filter', async () => {
|
||||
await models.response.create({parentId: 'req_test', body: '{"foo": "bar"}'});
|
||||
|
||||
try {
|
||||
await templating.render(`{% response "body", "req_test", "" %}`);
|
||||
fail('Should have failed');
|
||||
} catch (err) {
|
||||
expect(err.message).toContain('No body filter specified');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ResponseExtension JSONPath', async () => {
|
||||
@ -32,7 +43,11 @@ describe('ResponseExtension JSONPath', async () => {
|
||||
|
||||
it('renders basic response "body", query', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({parentId: request._id, body: '{"foo": "bar"}'});
|
||||
await models.response.create({
|
||||
parentId: request._id,
|
||||
statusCode: 200,
|
||||
body: '{"foo": "bar"}'
|
||||
});
|
||||
|
||||
const result = await templating.render(`{% response "body", "${request._id}", "$.foo" %}`);
|
||||
|
||||
@ -41,7 +56,11 @@ describe('ResponseExtension JSONPath', async () => {
|
||||
|
||||
it('fails on invalid JSON', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({parentId: request._id, body: '{"foo": "bar"'});
|
||||
await models.response.create({
|
||||
parentId: request._id,
|
||||
body: '{"foo": "bar"',
|
||||
statusCode: 200
|
||||
});
|
||||
|
||||
try {
|
||||
await templating.render(`{% response "body", "${request._id}", "$.foo" %}`);
|
||||
@ -53,7 +72,11 @@ describe('ResponseExtension JSONPath', async () => {
|
||||
|
||||
it('fails on invalid query', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({parentId: request._id, body: '{"foo": "bar"}'});
|
||||
await models.response.create({
|
||||
parentId: request._id,
|
||||
statusCode: 200,
|
||||
body: '{"foo": "bar"}'
|
||||
});
|
||||
|
||||
try {
|
||||
await templating.render(`{% response "body", "${request._id}", "$$" %}`);
|
||||
@ -65,7 +88,11 @@ describe('ResponseExtension JSONPath', async () => {
|
||||
|
||||
it('fails on no results', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({parentId: request._id, body: '{"foo": "bar"}'});
|
||||
await models.response.create({
|
||||
parentId: request._id,
|
||||
statusCode: 200,
|
||||
body: '{"foo": "bar"}'
|
||||
});
|
||||
|
||||
try {
|
||||
await templating.render(`{% response "body", "${request._id}", "$.missing" %}`);
|
||||
@ -77,7 +104,11 @@ describe('ResponseExtension JSONPath', async () => {
|
||||
|
||||
it('fails on more than 1 result', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({parentId: request._id, body: '{"array": [1,2,3]}'});
|
||||
await models.response.create({
|
||||
parentId: request._id,
|
||||
statusCode: 200,
|
||||
body: '{"array": [1,2,3]}'
|
||||
});
|
||||
|
||||
try {
|
||||
await templating.render(`{% response "body", "${request._id}", "$.array.*" %}`);
|
||||
@ -95,6 +126,7 @@ describe('ResponseExtension XPath', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({
|
||||
parentId: request._id,
|
||||
statusCode: 200,
|
||||
body: '<foo><bar>Hello World!</bar></foo>'
|
||||
});
|
||||
|
||||
@ -105,7 +137,11 @@ describe('ResponseExtension XPath', async () => {
|
||||
|
||||
it('no results on invalid XML', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({parentId: request._id, body: '<hi></hi></sstr>'});
|
||||
await models.response.create({
|
||||
parentId: request._id,
|
||||
statusCode: 200,
|
||||
body: '<hi></hi></sstr>'
|
||||
});
|
||||
|
||||
try {
|
||||
await templating.render(`{% response "body", "${request._id}", "/foo" %}`);
|
||||
@ -119,6 +155,7 @@ describe('ResponseExtension XPath', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({
|
||||
parentId: request._id,
|
||||
statusCode: 200,
|
||||
body: '<foo><bar>Hello World!</bar></foo>'
|
||||
});
|
||||
|
||||
@ -134,6 +171,7 @@ describe('ResponseExtension XPath', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({
|
||||
parentId: request._id,
|
||||
statusCode: 200,
|
||||
body: '<foo><bar>Hello World!</bar></foo>'
|
||||
});
|
||||
|
||||
@ -149,6 +187,7 @@ describe('ResponseExtension XPath', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({
|
||||
parentId: request._id,
|
||||
statusCode: 200,
|
||||
body: '<foo><bar>Hello World!</bar><bar>And again!</bar></foo>'
|
||||
});
|
||||
|
||||
@ -160,3 +199,65 @@ describe('ResponseExtension XPath', async () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ResponseExtension Header', async () => {
|
||||
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
|
||||
|
||||
it('renders basic response "header"', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({
|
||||
parentId: request._id,
|
||||
statusCode: 200,
|
||||
headers: [
|
||||
{name: 'Content-Type', value: 'application/json'},
|
||||
{name: 'Content-Length', value: '20'}
|
||||
]
|
||||
});
|
||||
|
||||
const id = request._id;
|
||||
|
||||
expect(await templating.render(`{% response "header", "${id}", "content-type" %}`))
|
||||
.toBe('application/json');
|
||||
expect(await templating.render(`{% response "header", "${id}", "Content-Type" %}`))
|
||||
.toBe('application/json');
|
||||
expect(await templating.render(`{% response "header", "${id}", "CONTENT-type" %}`))
|
||||
.toBe('application/json');
|
||||
expect(await templating.render(`{% response "header", "${id}", " CONTENT-type " %}`))
|
||||
.toBe('application/json');
|
||||
});
|
||||
|
||||
it('no results on missing header', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({
|
||||
parentId: request._id,
|
||||
statusCode: 200,
|
||||
headers: [
|
||||
{name: 'Content-Type', value: 'application/json'},
|
||||
{name: 'Content-Length', value: '20'}
|
||||
]
|
||||
});
|
||||
|
||||
try {
|
||||
await templating.render(`{% response "header", "${request._id}", "dne" %}`);
|
||||
fail('should have failed');
|
||||
} catch (err) {
|
||||
expect(err.message).toContain('No match for header: dne');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ResponseExtension Raw', async () => {
|
||||
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
|
||||
|
||||
it('renders basic response "header"', async () => {
|
||||
const request = await models.request.create({parentId: 'foo'});
|
||||
await models.response.create({
|
||||
parentId: request._id,
|
||||
statusCode: 200,
|
||||
body: 'Hello World!'
|
||||
});
|
||||
|
||||
const result = await templating.render(`{% response "raw", "${request._id}", "" %}`);
|
||||
expect(result).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
|
@ -1,9 +1,40 @@
|
||||
import BaseExtension from './base/base-extension';
|
||||
|
||||
export default class Base64Extension extends BaseExtension {
|
||||
constructor () {
|
||||
super();
|
||||
this.tags = ['base64'];
|
||||
getName () {
|
||||
return 'Base64';
|
||||
}
|
||||
|
||||
getTag () {
|
||||
return 'base64';
|
||||
}
|
||||
|
||||
getDefaultFill () {
|
||||
return "base64 'encode', ''";
|
||||
}
|
||||
|
||||
getDescription () {
|
||||
return 'encode or decode values';
|
||||
}
|
||||
|
||||
getArguments () {
|
||||
return [
|
||||
{
|
||||
key: 'action',
|
||||
label: 'Action',
|
||||
type: 'enum',
|
||||
options: [
|
||||
{name: 'Encode', value: 'encode'},
|
||||
{name: 'Decode', value: 'decode'}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
label: 'Value',
|
||||
type: 'string',
|
||||
placeholder: 'My text'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
run (context, op, text) {
|
||||
|
@ -2,8 +2,27 @@ const EMPTY_ARG = '__EMPTY_NUNJUCKS_ARG__';
|
||||
|
||||
export default class BaseExtension {
|
||||
constructor () {
|
||||
// TODO: Subclass should set this
|
||||
this.tags = [];
|
||||
this.tags = [this.getTag()];
|
||||
}
|
||||
|
||||
getName () {
|
||||
throw new Error(`${this.constructor.name} did not implement getName()`);
|
||||
}
|
||||
|
||||
getTag () {
|
||||
throw new Error(`${this.constructor.name} did not implement getTag()`);
|
||||
}
|
||||
|
||||
getDefaultFill () {
|
||||
throw new Error(`${this.constructor.name} did not implement getDefaultFill()`);
|
||||
}
|
||||
|
||||
getDescription () {
|
||||
throw new Error(`${this.constructor.name} did not implement getDescription()`);
|
||||
}
|
||||
|
||||
getArguments () {
|
||||
throw new Error(`${this.constructor.name} did not implement getArguments()`);
|
||||
}
|
||||
|
||||
parse (parser, nodes, lexer) {
|
||||
|
@ -1,9 +1,33 @@
|
||||
import BaseExtension from './base/base-extension';
|
||||
|
||||
export default class NowExtension extends BaseExtension {
|
||||
constructor () {
|
||||
super();
|
||||
this.tags = ['now'];
|
||||
getName () {
|
||||
return 'Now';
|
||||
}
|
||||
|
||||
getTag () {
|
||||
return 'now';
|
||||
}
|
||||
|
||||
getDefaultFill () {
|
||||
return "now 'iso-8601'";
|
||||
}
|
||||
|
||||
getDescription () {
|
||||
return 'get the current time';
|
||||
}
|
||||
|
||||
getArguments () {
|
||||
return [{
|
||||
key: 'format',
|
||||
label: 'Timestamp Format',
|
||||
type: 'enum',
|
||||
options: [
|
||||
{name: 'Milliseconds', value: 'millis'},
|
||||
{name: 'Unix Timestamp', value: 'unix'},
|
||||
{name: 'ISO-8601 Format', value: 'iso-8601'}
|
||||
]
|
||||
}];
|
||||
}
|
||||
|
||||
run (context, dateType = 'iso-8601') {
|
||||
|
@ -6,16 +6,67 @@ import * as models from '../../models';
|
||||
import BaseExtension from './base/base-extension';
|
||||
|
||||
export default class ResponseExtension extends BaseExtension {
|
||||
constructor () {
|
||||
super();
|
||||
this.tags = ['response'];
|
||||
getName () {
|
||||
return 'Response Value';
|
||||
}
|
||||
|
||||
async run (context, field, id, query) {
|
||||
if (field !== 'body') {
|
||||
getTag () {
|
||||
return 'response';
|
||||
}
|
||||
|
||||
getDescription () {
|
||||
return 'reference values from other requests';
|
||||
}
|
||||
|
||||
getDefaultFill () {
|
||||
return "response 'body', '', ''";
|
||||
}
|
||||
|
||||
getArguments () {
|
||||
return [
|
||||
{
|
||||
key: 'field',
|
||||
label: 'Attribute',
|
||||
type: 'enum',
|
||||
options: [
|
||||
{name: 'Body – attribute of response body', value: 'body'},
|
||||
{name: 'Raw Body – entire response body', value: 'raw'},
|
||||
{name: 'Header – value of response header', value: 'header'}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'request',
|
||||
label: 'Request',
|
||||
type: 'model',
|
||||
model: 'Request'
|
||||
},
|
||||
{
|
||||
key: 'filter',
|
||||
type: 'string',
|
||||
hide: args => args[0].value === 'raw',
|
||||
label: args => {
|
||||
switch (args[0].value) {
|
||||
case 'body':
|
||||
return 'Filter (JSONPath or XPath)';
|
||||
case 'header':
|
||||
return 'Header Name';
|
||||
default :
|
||||
return 'Filter';
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async run (context, field, id, filter) {
|
||||
if (!['body', 'header', 'raw'].includes(field)) {
|
||||
throw new Error(`Invalid response field ${field}`);
|
||||
}
|
||||
|
||||
if (field !== 'raw' && !filter) {
|
||||
throw new Error(`No ${field} filter specified`);
|
||||
}
|
||||
|
||||
const request = await models.request.getById(id);
|
||||
if (!request) {
|
||||
throw new Error(`Could not find request ${id}`);
|
||||
@ -24,18 +75,33 @@ export default class ResponseExtension extends BaseExtension {
|
||||
const response = await models.response.getLatestForRequest(id);
|
||||
|
||||
if (!response) {
|
||||
throw new Error(`No responses for request ${id}`);
|
||||
throw new Error('No responses for request');
|
||||
}
|
||||
|
||||
const bodyBuffer = new Buffer(response.body, response.encoding);
|
||||
const bodyStr = bodyBuffer.toString();
|
||||
if (!response.statusCode || response.statusCode < 100) {
|
||||
throw new Error('No responses for request');
|
||||
}
|
||||
|
||||
if (query.indexOf('$') === 0) {
|
||||
return this.matchJSONPath(bodyStr, query);
|
||||
} else if (query.indexOf('/') === 0) {
|
||||
return this.matchXPath(bodyStr, query);
|
||||
const sanitizedFilter = filter.trim();
|
||||
|
||||
if (field === 'header') {
|
||||
return this.matchHeader(response.headers, sanitizedFilter);
|
||||
} else if (field === 'raw') {
|
||||
const bodyBuffer = new Buffer(response.body, response.encoding);
|
||||
return bodyBuffer.toString();
|
||||
} else if (field === 'body') {
|
||||
const bodyBuffer = new Buffer(response.body, response.encoding);
|
||||
const bodyStr = bodyBuffer.toString();
|
||||
|
||||
if (sanitizedFilter.indexOf('$') === 0) {
|
||||
return this.matchJSONPath(bodyStr, sanitizedFilter);
|
||||
} else if (sanitizedFilter.indexOf('/') === 0) {
|
||||
return this.matchXPath(bodyStr, sanitizedFilter);
|
||||
} else {
|
||||
throw new Error(`Invalid format for response query: ${sanitizedFilter}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Invalid format for response query: ${query}`);
|
||||
throw new Error(`Unknown field ${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,4 +150,16 @@ export default class ResponseExtension extends BaseExtension {
|
||||
|
||||
return results[0].childNodes.toString();
|
||||
}
|
||||
|
||||
matchHeader (headers, name) {
|
||||
const header = headers.find(
|
||||
h => h.name.toLowerCase() === name.toLowerCase()
|
||||
);
|
||||
|
||||
if (!header) {
|
||||
throw new Error(`No match for header: ${name}`);
|
||||
}
|
||||
|
||||
return header.value;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,27 @@ import BaseExtension from './base/base-extension';
|
||||
export default class TimestampExtension extends BaseExtension {
|
||||
constructor () {
|
||||
super();
|
||||
this.tags = ['timestamp'];
|
||||
this.deprecated = true;
|
||||
}
|
||||
|
||||
getName () {
|
||||
return 'Timestamp';
|
||||
}
|
||||
|
||||
getDefaultFill () {
|
||||
return 'timestamp';
|
||||
}
|
||||
|
||||
getTag () {
|
||||
return 'timestamp';
|
||||
}
|
||||
|
||||
getDescription () {
|
||||
return 'generate timestamp in milliseconds';
|
||||
}
|
||||
|
||||
getArguments () {
|
||||
return [];
|
||||
}
|
||||
|
||||
run (context) {
|
||||
|
@ -2,9 +2,32 @@ import uuid from 'uuid';
|
||||
import BaseExtension from './base/base-extension';
|
||||
|
||||
export default class UuidExtension extends BaseExtension {
|
||||
constructor () {
|
||||
super();
|
||||
this.tags = ['uuid'];
|
||||
getName () {
|
||||
return 'UUID';
|
||||
}
|
||||
|
||||
getTag () {
|
||||
return 'uuid';
|
||||
}
|
||||
|
||||
getDefaultFill () {
|
||||
return "uuid 'v4'";
|
||||
}
|
||||
|
||||
getDescription () {
|
||||
return 'generate v1 or v4 UUIDs';
|
||||
}
|
||||
|
||||
getArguments () {
|
||||
return [{
|
||||
key: 'version',
|
||||
label: 'Version',
|
||||
type: 'enum',
|
||||
options: [
|
||||
{value: 'v4', name: 'Version 4'},
|
||||
{value: 'v1', name: 'Version 1'}
|
||||
]
|
||||
}];
|
||||
}
|
||||
|
||||
run (context, uuidType = 'v4') {
|
||||
|
@ -50,6 +50,22 @@ export function render (text, config = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getTagDefinitions () {
|
||||
const env = getNunjucks();
|
||||
|
||||
return Object.keys(env.extensions)
|
||||
.map(k => env.extensions[k])
|
||||
.filter(ext => !ext.deprecated)
|
||||
.map(ext => ({
|
||||
name: ext.getTag(),
|
||||
displayName: ext.getName(),
|
||||
defaultFill: ext.getDefaultFill(),
|
||||
description: ext.getDescription(),
|
||||
args: ext.getArguments()
|
||||
}))
|
||||
.sort((a, b) => a.name > b.name ? 1 : -1);
|
||||
}
|
||||
|
||||
function getNunjucks (variablesOnly) {
|
||||
if (variablesOnly && nunjucksVariablesOnly) {
|
||||
return nunjucksVariablesOnly;
|
||||
|
@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Get list of paths to all primitive types in nested object
|
||||
* @param {object} obj - object to analyse
|
||||
* @param {String} [prefix] - base path to prefix to all paths
|
||||
* @returns {Array} - list of paths
|
||||
*/
|
||||
export function getKeys (obj, prefix = '') {
|
||||
let allKeys = [];
|
||||
|
||||
@ -18,3 +24,122 @@ export function getKeys (obj, prefix = '') {
|
||||
|
||||
return allKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a Nunjucks tag string into a usable abject
|
||||
* @param {string} tagStr - the template string for the tag
|
||||
* @return {object} parsed tag data
|
||||
*/
|
||||
export function tokenizeTag (tagStr) {
|
||||
// ~~~~~~~~ //
|
||||
// Sanitize //
|
||||
// ~~~~~~~~ //
|
||||
|
||||
const withoutEnds = tagStr
|
||||
.trim()
|
||||
.replace(/^{%/, '')
|
||||
.replace(/%}$/, '')
|
||||
.trim();
|
||||
|
||||
const nameMatch = withoutEnds.match(/^[a-zA-Z_$][0-9a-zA-Z_$]*/);
|
||||
const name = nameMatch ? nameMatch[0] : withoutEnds;
|
||||
const argsStr = withoutEnds.slice(name.length);
|
||||
|
||||
// ~~~~~~~~~~~~~ //
|
||||
// Tokenize Args //
|
||||
// ~~~~~~~~~~~~~ //
|
||||
|
||||
const args = [];
|
||||
let quotedBy = null;
|
||||
let currentArg = null;
|
||||
for (let i = 0; i < argsStr.length; i++) {
|
||||
const c = argsStr.charAt(i);
|
||||
|
||||
// Do nothing if we're still on a space o comma
|
||||
if (currentArg === null && c.match(/[\s,]/)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start a new single-quoted string
|
||||
if (currentArg === null && c === "'") {
|
||||
currentArg = '';
|
||||
quotedBy = "'";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start a new double-quoted string
|
||||
if (currentArg === null && c === '"') {
|
||||
currentArg = '';
|
||||
quotedBy = '"';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start a new unquoted string
|
||||
if (currentArg === null) {
|
||||
currentArg = c;
|
||||
quotedBy = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
const endQuoted = quotedBy && c === quotedBy;
|
||||
const endUnquoted = !quotedBy && c === ',';
|
||||
const finalChar = i === argsStr.length - 1;
|
||||
const argCompleted = endQuoted || endUnquoted;
|
||||
|
||||
// Append current char to argument
|
||||
if (!argCompleted && currentArg !== null) {
|
||||
if (c === '\\') {
|
||||
// Handle backslashes
|
||||
i += 1;
|
||||
currentArg += argsStr.charAt(i);
|
||||
} else {
|
||||
currentArg += c;
|
||||
}
|
||||
}
|
||||
|
||||
// End current argument
|
||||
if (currentArg !== null && (argCompleted || finalChar)) {
|
||||
let arg;
|
||||
if (quotedBy) {
|
||||
arg = {type: 'string', value: currentArg, quotedBy};
|
||||
} else if (['true', 'false'].includes(currentArg)) {
|
||||
arg = {type: 'boolean', value: currentArg};
|
||||
} else if (currentArg.match(/^\d*\.?\d*$/)) {
|
||||
arg = {type: 'number', value: currentArg};
|
||||
} else if (currentArg.match(/^[a-zA-Z_$][0-9a-zA-Z_$]*$/)) {
|
||||
arg = {type: 'variable', value: currentArg};
|
||||
} else {
|
||||
arg = {type: 'expression', value: currentArg};
|
||||
}
|
||||
|
||||
args.push(arg);
|
||||
|
||||
currentArg = null;
|
||||
quotedBy = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {name, args};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a tokenized tag back into a Nunjucks string
|
||||
* @param {object} tagData - tag data to serialize
|
||||
* @return {string} tag as a Nunjucks string
|
||||
*/
|
||||
export function unTokenizeTag (tagData) {
|
||||
const args = [];
|
||||
for (const arg of tagData.args) {
|
||||
if (arg.type === 'string') {
|
||||
const q = arg.quotedBy || "'";
|
||||
const re = new RegExp(`([^\\\\])${q}`, 'g');
|
||||
const str = arg.value.replace(re, `$1\\${q}`);
|
||||
args.push(`${q}${str}${q}`);
|
||||
} else {
|
||||
args.push(arg.value);
|
||||
}
|
||||
}
|
||||
|
||||
const argsStr = args.join(', ');
|
||||
return `{% ${tagData.name} ${argsStr} %}`;
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ import 'codemirror/keymap/emacs';
|
||||
import 'codemirror/keymap/sublime';
|
||||
import './modes/nunjucks';
|
||||
import './modes/curl';
|
||||
import './extensions/environments-autocomplete';
|
||||
import './extensions/autocomplete';
|
||||
import './extensions/clickable';
|
||||
import './extensions/nunjucks-tags';
|
||||
|
||||
|
@ -13,6 +13,7 @@ import {trackEvent} from '../../../analytics/index';
|
||||
import {prettifyJson} from '../../../common/prettify';
|
||||
import {DEBOUNCE_MILLIS} from '../../../common/constants';
|
||||
import './base-imports';
|
||||
import {getTagDefinitions} from '../../../templating/index';
|
||||
|
||||
const TAB_KEY = 9;
|
||||
const TAB_SIZE = 4;
|
||||
@ -400,15 +401,7 @@ class CodeEditor extends PureComponent {
|
||||
};
|
||||
|
||||
// Only allow tags if we have variables too
|
||||
getTags = () => ([
|
||||
`uuid 'v4'`,
|
||||
`uuid 'v1'`,
|
||||
`now 'ISO-8601'`,
|
||||
`now 'unix'`,
|
||||
`now 'millis'`,
|
||||
`base64 'encode', 'my string'`,
|
||||
`base64 'decode', 'bXkgc3RyaW5n'`
|
||||
]);
|
||||
getTags = getTagDefinitions;
|
||||
}
|
||||
options.environmentAutocomplete = {
|
||||
getVariables,
|
||||
|
@ -294,6 +294,8 @@ function matchSegments (listOfThings, segment, type, limit = -1) {
|
||||
for (const t of listOfThings) {
|
||||
const name = typeof t === 'string' ? t : t.name;
|
||||
const value = typeof t === 'string' ? '' : t.value;
|
||||
const displayName = t.displayName || name;
|
||||
const defaultFill = t.defaultFill || name;
|
||||
|
||||
const matchSegment = segment.toLowerCase();
|
||||
const matchName = name.toLowerCase();
|
||||
@ -312,8 +314,8 @@ function matchSegments (listOfThings, segment, type, limit = -1) {
|
||||
score: name.length, // In case we want to sort by this
|
||||
|
||||
// CodeMirror
|
||||
text: name,
|
||||
displayText: name,
|
||||
text: defaultFill,
|
||||
displayText: displayName,
|
||||
render: renderHintMatch,
|
||||
hint: replaceHintMatch
|
||||
});
|
@ -2,6 +2,8 @@ import CodeMirror from 'codemirror';
|
||||
import * as misc from '../../../../common/misc';
|
||||
import NunjucksVariableModal from '../../modals/nunjucks-modal';
|
||||
import {showModal} from '../../modals/index';
|
||||
import {tokenizeTag} from '../../../../templating/utils';
|
||||
import {getTagDefinitions} from '../../../../templating/index';
|
||||
|
||||
CodeMirror.defineExtension('enableNunjucksTags', function (handleRender) {
|
||||
if (!handleRender) {
|
||||
@ -222,18 +224,23 @@ async function _updateElementText (render, mark, text) {
|
||||
.trim();
|
||||
|
||||
if (tagMatch) {
|
||||
const tag = tagMatch[1];
|
||||
const tagData = tokenizeTag(str);
|
||||
const tagDefinition = getTagDefinitions().find(d => d.name === tagData.name);
|
||||
|
||||
// Don't render other tags because they may be two-parters
|
||||
// eg. {% for %}...{% endfor %}
|
||||
const cleaned = cleanedStr.replace(tag, '').trim();
|
||||
const short = cleaned.length > 30 ? `${cleaned.slice(0, 30)}…` : cleaned;
|
||||
el.innerHTML = `<label>${tag}</label> ${short}`.trim();
|
||||
|
||||
if (['response', 'res', 'uuid', 'timestamp', 'now', 'base64'].includes(tag)) {
|
||||
if (tagDefinition) {
|
||||
// Try rendering these so we can show errors if needed
|
||||
const firstArg = tagDefinition.args[0];
|
||||
if (firstArg && firstArg.type === 'enum') {
|
||||
const argData = tagData.args[0];
|
||||
const option = firstArg.options.find(d => d.value === argData.value);
|
||||
el.innerHTML = `<label></label>${tagDefinition.displayName} ⇒ ${option.name}`;
|
||||
} else {
|
||||
el.innerHTML = `<label></label>${tagData.name}`;
|
||||
}
|
||||
el.title = await render(str);
|
||||
} else {
|
||||
el.innerHTML = `<label></label>${cleanedStr}`;
|
||||
el.title = 'Unrecognized tag';
|
||||
el.setAttribute('data-ignore', 'on');
|
||||
}
|
||||
} else {
|
||||
|
@ -56,7 +56,7 @@ class NunjucksModal extends PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {handleRender, handleGetRenderContext, uniqueKey} = this.props;
|
||||
const {handleRender, handleGetRenderContext, uniqueKey, workspace} = this.props;
|
||||
const {defaultTemplate} = this.state;
|
||||
|
||||
let editor = null;
|
||||
@ -78,21 +78,22 @@ class NunjucksModal extends PureComponent {
|
||||
onChange={this._handleTemplateChange}
|
||||
defaultValue={defaultTemplate}
|
||||
handleRender={handleRender}
|
||||
workspace={workspace}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal ref={this._setModalRef} onHide={this._handleModalHide} key={uniqueKey}>
|
||||
<form onSubmit={this._handleSubmit}>
|
||||
<ModalHeader>Edit {title}</ModalHeader>
|
||||
<ModalBody className="pad" key={defaultTemplate}>
|
||||
<ModalHeader>Edit {title}</ModalHeader>
|
||||
<ModalBody className="pad" key={defaultTemplate}>
|
||||
<form onSubmit={this._handleSubmit}>
|
||||
{editor}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<button type="submit" className="btn">Done</button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<button className="btn" onClick={this.hide}>Done</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -101,7 +102,8 @@ class NunjucksModal extends PureComponent {
|
||||
NunjucksModal.propTypes = {
|
||||
uniqueKey: PropTypes.string.isRequired,
|
||||
handleRender: PropTypes.func.isRequired,
|
||||
handleGetRenderContext: PropTypes.func.isRequired
|
||||
handleGetRenderContext: PropTypes.func.isRequired,
|
||||
workspace: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default NunjucksModal;
|
||||
|
@ -1,45 +1,91 @@
|
||||
import React, {PropTypes, PureComponent} from 'react';
|
||||
import autobind from 'autobind-decorator';
|
||||
import Input from '../codemirror/one-line-editor';
|
||||
|
||||
const TAGS = [
|
||||
{name: `uuid 'v4'`},
|
||||
{name: `uuid 'v1'`},
|
||||
{name: `now 'ISO-8601'`},
|
||||
{name: `now 'unix'`},
|
||||
{name: `now 'millis'`},
|
||||
{name: `base64 'encode'`, suffix: `, 'my string'`},
|
||||
{name: `base64 'decode'`, suffix: `, 'bXkgc3RyaW5n'`}
|
||||
// 'response'
|
||||
];
|
||||
|
||||
const CUSTOM_TAG_VALUE = `{% custom 'tag' %}`;
|
||||
import clone from 'clone';
|
||||
import * as templating from '../../../templating';
|
||||
import * as templateUtils from '../../../templating/utils';
|
||||
import * as db from '../../../common/database';
|
||||
import {types as allModelTypes} from '../../../models';
|
||||
import HelpTooltip from '../help-tooltip';
|
||||
|
||||
@autobind
|
||||
class TagEditor extends PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
const inner = props.defaultValue
|
||||
.replace(/\s*%}$/, '')
|
||||
.replace(/^{%\s*/, '');
|
||||
const activeTagData = templateUtils.tokenizeTag(props.defaultValue);
|
||||
|
||||
const tagDefinitions = templating.getTagDefinitions();
|
||||
const activeTagDefinition = tagDefinitions.find(d => d.name === activeTagData.name);
|
||||
|
||||
// Edit tags raw that we don't know about
|
||||
if (!activeTagDefinition) {
|
||||
activeTagData.rawValue = props.defaultValue;
|
||||
}
|
||||
|
||||
const value = `{% ${inner} %}`;
|
||||
this.state = {
|
||||
tags: TAGS,
|
||||
value: value,
|
||||
selectValue: value,
|
||||
activeTagData,
|
||||
activeTagDefinition,
|
||||
loadingModels: true,
|
||||
models: {},
|
||||
preview: '',
|
||||
error: ''
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this._update(this.state.value, true);
|
||||
async componentWillMount () {
|
||||
await this._refreshModels(this.props.workspace);
|
||||
await this._update(this.state.activeTagDefinition, this.state.activeTagData, true);
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const {workspace} = nextProps;
|
||||
|
||||
if (this.props.workspace._id !== workspace._id) {
|
||||
this._refreshModels(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
async _refreshModels (workspace) {
|
||||
const models = {};
|
||||
for (const type of allModelTypes()) {
|
||||
models[type] = [];
|
||||
}
|
||||
|
||||
for (const doc of await db.withDescendants(workspace)) {
|
||||
models[doc.type].push(doc);
|
||||
}
|
||||
|
||||
this.setState({models, loadingModels: false});
|
||||
}
|
||||
|
||||
_updateArg (argValue, argIndex) {
|
||||
const {activeTagData, activeTagDefinition} = this.state;
|
||||
|
||||
const tagData = clone(activeTagData);
|
||||
tagData.args[argIndex].value = argValue;
|
||||
|
||||
this._update(activeTagDefinition, tagData, false);
|
||||
}
|
||||
|
||||
_handleChange (e) {
|
||||
this._update(e.target.value, false, e.target.value);
|
||||
const parent = e.target.parentNode;
|
||||
const argIndex = parent.getAttribute('data-arg-index');
|
||||
return this._updateArg(e.target.value, argIndex);
|
||||
}
|
||||
|
||||
_handleChangeCustomArg (e) {
|
||||
const {activeTagData, activeTagDefinition} = this.state;
|
||||
|
||||
const tagData = clone(activeTagData);
|
||||
tagData.rawValue = e.target.value;
|
||||
|
||||
this._update(activeTagDefinition, tagData, false);
|
||||
}
|
||||
|
||||
_handleChangeTag (e) {
|
||||
const name = e.target.value;
|
||||
const tagDefinition = templating.getTagDefinitions().find(d => d.name === name);
|
||||
this._update(tagDefinition, false);
|
||||
}
|
||||
|
||||
_setSelectRef (n) {
|
||||
@ -51,14 +97,35 @@ class TagEditor extends PureComponent {
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async _update (value, noCallback = false, selectValue = null) {
|
||||
async _update (tagDefinition, tagData, noCallback = false) {
|
||||
const {handleRender} = this.props;
|
||||
|
||||
let preview = '';
|
||||
let error = '';
|
||||
|
||||
let activeTagData = tagData;
|
||||
if (!activeTagData && tagDefinition) {
|
||||
activeTagData = {
|
||||
name: tagDefinition.name,
|
||||
rawValue: null,
|
||||
args: tagDefinition.args.map(arg => {
|
||||
if (arg.type === 'enum') {
|
||||
return {type: 'string', value: arg.options[0].value};
|
||||
} else {
|
||||
return {type: 'string', value: ''};
|
||||
}
|
||||
})
|
||||
};
|
||||
} else if (!activeTagData && !tagDefinition) {
|
||||
activeTagData = {name: 'custom', rawValue: "{% tag 'arg1', 'arg2' %}"};
|
||||
}
|
||||
|
||||
let template;
|
||||
try {
|
||||
preview = await handleRender(value, true);
|
||||
template = typeof activeTagData.rawValue === 'string'
|
||||
? activeTagData.rawValue
|
||||
: templateUtils.unTokenizeTag(activeTagData);
|
||||
preview = await handleRender(template, true);
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
}
|
||||
@ -66,53 +133,129 @@ class TagEditor extends PureComponent {
|
||||
const isMounted = !!this._select;
|
||||
if (isMounted) {
|
||||
this.setState({
|
||||
activeTagData,
|
||||
preview,
|
||||
error,
|
||||
value,
|
||||
selectValue: selectValue || this.state.selectValue
|
||||
activeTagDefinition: tagDefinition
|
||||
});
|
||||
}
|
||||
|
||||
// Call the callback if we need to
|
||||
if (!noCallback) {
|
||||
this.props.onChange(value);
|
||||
this.props.onChange(template);
|
||||
}
|
||||
}
|
||||
|
||||
renderArgString (value, placeholder) {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={value || ''}
|
||||
placeholder={placeholder}
|
||||
onChange={this._handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderArgEnum (value, options) {
|
||||
return (
|
||||
<select value={value} onChange={this._handleChange}>
|
||||
{options.map(option => {
|
||||
return (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
renderArgModel (value, modelType) {
|
||||
const {models, loadingModels} = this.state;
|
||||
const docs = models[modelType] || [];
|
||||
const id = value || 'n/a';
|
||||
|
||||
return (
|
||||
<select value={id} disabled={loadingModels} onChange={this._handleChange}>
|
||||
<option value="n/a">-- Select Item --</option>
|
||||
{docs.map(m => (
|
||||
<option key={m._id} value={m._id}>{m.name}</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
renderArg (argDefinition, args, argIndex) {
|
||||
// Decide whether or not to show it
|
||||
if (argDefinition.hide && argDefinition.hide(args)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const argData = args[argIndex];
|
||||
const value = argData.value;
|
||||
|
||||
let argInput;
|
||||
if (argDefinition.type === 'string') {
|
||||
const {placeholder} = argDefinition;
|
||||
argInput = this.renderArgString(value, placeholder);
|
||||
} else if (argDefinition.type === 'enum') {
|
||||
const {options} = argDefinition;
|
||||
argInput = this.renderArgEnum(value, options);
|
||||
} else if (argDefinition.type === 'model') {
|
||||
const {model} = argDefinition;
|
||||
argInput = this.renderArgModel(value, model);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = argDefinition.label;
|
||||
const labelStr = typeof label === 'function' ? label(args) : label;
|
||||
|
||||
return (
|
||||
<div key={argDefinition.key} className="form-control form-control--outlined">
|
||||
<label>
|
||||
{labelStr || argDefinition.key}
|
||||
{argDefinition.help && (
|
||||
<HelpTooltip>{argDefinition.help}</HelpTooltip>
|
||||
)}
|
||||
<div data-arg-index={argIndex}>
|
||||
{argInput}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {error, value, preview, tags, selectValue} = this.state;
|
||||
const isFound = !!tags.find(v => value === `{% ${v.name} %}`);
|
||||
const isFlexible = value.indexOf('{% base64') === 0;
|
||||
const isCustom = !isFound && !isFlexible;
|
||||
const {error, preview, activeTagDefinition, activeTagData} = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>Template Function
|
||||
<label>Function to Perform
|
||||
<select ref={this._setSelectRef}
|
||||
onChange={this._handleChange}
|
||||
value={isCustom ? CUSTOM_TAG_VALUE : selectValue}>
|
||||
{tags.map((t, i) => (
|
||||
<option key={`${i}::${t.name}`} value={`{% ${t.name}${t.suffix || ''} %}`}>
|
||||
{t.name}
|
||||
onChange={this._handleChangeTag}
|
||||
value={activeTagDefinition ? activeTagDefinition.name : ''}>
|
||||
{templating.getTagDefinitions().map((tagDefinition, i) => (
|
||||
<option key={`${i}::${tagDefinition.name}`} value={tagDefinition.name}>
|
||||
{tagDefinition.displayName} – {tagDefinition.description}
|
||||
</option>
|
||||
))}
|
||||
<option value={`{% custom 'tag' %}`}>
|
||||
-- Custom --
|
||||
</option>
|
||||
<option value="">-- Custom --</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
{(!isFound || isFlexible) && (
|
||||
{activeTagDefinition && activeTagDefinition.args.map((argDefinition, index) => (
|
||||
this.renderArg(argDefinition, activeTagData.args, index)
|
||||
))}
|
||||
{!activeTagDefinition && (
|
||||
<div className="form-control form-control--outlined">
|
||||
<Input
|
||||
key={selectValue}
|
||||
forceEditor
|
||||
mode="nunjucks"
|
||||
type="text"
|
||||
defaultValue={value}
|
||||
onChange={this._update}
|
||||
/>
|
||||
<label>Custom
|
||||
<input type="text"
|
||||
defaultValue={activeTagData.rawValue}
|
||||
onChange={this._handleChangeCustomArg}/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
<div className="form-control form-control--outlined">
|
||||
@ -131,7 +274,8 @@ class TagEditor extends PureComponent {
|
||||
TagEditor.propTypes = {
|
||||
handleRender: PropTypes.func.isRequired,
|
||||
defaultValue: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
onChange: PropTypes.func.isRequired,
|
||||
workspace: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default TagEditor;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, {PropTypes, PureComponent} from 'react';
|
||||
import autobind from 'autobind-decorator';
|
||||
import Input from '../codemirror/one-line-editor';
|
||||
|
||||
@autobind
|
||||
class VariableEditor extends PureComponent {
|
||||
@ -84,13 +83,7 @@ class VariableEditor extends PureComponent {
|
||||
</div>
|
||||
{isOther && (
|
||||
<div className="form-control form-control--outlined">
|
||||
<Input
|
||||
forceEditor
|
||||
mode="nunjucks"
|
||||
type="text"
|
||||
defaultValue={value}
|
||||
onChange={this._update}
|
||||
/>
|
||||
<input type="text" defaultValue={value} onChange={this._handleChange} />
|
||||
</div>
|
||||
)}
|
||||
<div className="form-control form-control--outlined">
|
||||
|
@ -386,6 +386,7 @@ class Wrapper extends PureComponent {
|
||||
ref={registerModal}
|
||||
handleRender={handleRender}
|
||||
handleGetRenderContext={handleGetRenderContext}
|
||||
workspace={activeWorkspace}
|
||||
/>
|
||||
<WorkspaceSettingsModal
|
||||
ref={registerModal}
|
||||
|
@ -562,10 +562,12 @@ class App extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
async _handleToggleMenuBar (hide) {
|
||||
_handleToggleMenuBar (hide) {
|
||||
let win = remote.BrowserWindow.getFocusedWindow();
|
||||
win.setAutoHideMenuBar(hide);
|
||||
win.setMenuBarVisibility(!hide);
|
||||
if (win.isMenuBarAutoHide() === hide) {
|
||||
win.setAutoHideMenuBar(hide);
|
||||
win.setMenuBarVisibility(!hide);
|
||||
}
|
||||
}
|
||||
|
||||
async _handleToggleSidebar () {
|
||||
@ -629,8 +631,6 @@ class App extends PureComponent {
|
||||
trackEvent('General', 'Launched', getAppVersion(), {nonInteraction: true});
|
||||
}
|
||||
|
||||
this._handleToggleMenuBar(this.props.settings.autoHideMenuBar);
|
||||
|
||||
db.onChange(async changes => {
|
||||
for (const change of changes) {
|
||||
const [
|
||||
@ -688,6 +688,9 @@ class App extends PureComponent {
|
||||
ipcRenderer.on('toggle-sidebar', this._handleToggleSidebar);
|
||||
|
||||
process.nextTick(() => ipcRenderer.send('app-ready'));
|
||||
|
||||
// handle this
|
||||
this._handleToggleMenuBar(this.props.settings.autoHideMenuBar);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
@ -751,6 +754,7 @@ App.propTypes = {
|
||||
paneWidth: PropTypes.number.isRequired,
|
||||
paneHeight: PropTypes.number.isRequired,
|
||||
handleCommand: PropTypes.func.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
activeWorkspace: PropTypes.shape({
|
||||
_id: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
|
@ -34,15 +34,19 @@
|
||||
.modal__content__wrapper {
|
||||
top: 1rem;
|
||||
width: @modal-width;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
transform: scale(0.95);
|
||||
transition: transform 150ms ease-out, top 200ms ease-out;
|
||||
|
||||
// We want pointer events to pass through to the backdrop so we can close it
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.modal--fixed-height .modal__content__wrapper {
|
||||
&.modal--fixed-height .modal__content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@ -65,11 +69,13 @@
|
||||
box-shadow: 0 0 2rem 0 rgba(0, 0, 0, 0.2);
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-font);
|
||||
border: 1px solid @hl-sm;
|
||||
|
||||
// Since we disable pointer-events on the parent, re-enable them here
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.modal__header {
|
||||
|
@ -19,7 +19,7 @@
|
||||
"start-hot": "npm run build-main && cross-env HOT=1 INSOMNIA_ENV=development electron app",
|
||||
"build-main": "cross-env NODE_ENV=development webpack --config ./webpack/webpack.config.electron.babel.js",
|
||||
"hot-server": "webpack-dev-server --config ./webpack/webpack.config.development.babel.js",
|
||||
"dev": "concurrently --kill-others 'npm run hot-server' 'npm run start-hot'",
|
||||
"dev": "concurrently --kill-others \"npm run hot-server\" \"npm run start-hot\"",
|
||||
"build:clean": "rm -rf ./build/* && rm -rf ./dist/* && mkdirp ./build",
|
||||
"build:renderer": "cross-env NODE_ENV=production webpack --config ./webpack/webpack.config.production.babel.js",
|
||||
"build:main": "cross-env NODE_ENV=production webpack --config ./webpack/webpack.config.electron.babel.js",
|
||||
|
Loading…
Reference in New Issue
Block a user