From d654feca82ea933275bfbc0dfdce7c8b6d691133 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 22 Aug 2017 15:30:57 -0700 Subject: [PATCH] Updates to cookie editor --- app/common/render.js | 16 +- app/models/cookie-jar.js | 29 +++- app/models/index.js | 38 +++-- .../__tests__/request-extension.test.js | 2 +- .../extensions/request-extension.js | 2 +- app/ui/components/cookie-list.js | 40 ++--- .../components/modals/cookie-modify-modal.js | 144 +++++++++++------- app/ui/components/modals/cookies-modal.js | 91 ++++++----- app/ui/components/modals/index.js | 4 - app/ui/components/rendered-text.js | 42 +++++ app/ui/components/wrapper.js | 16 +- app/ui/containers/app.js | 4 +- app/ui/css/components/cookie-list.less | 4 - .../css/components/cookie-modify-modal.less | 18 +-- app/ui/css/layout/base.less | 11 +- app/ui/redux/selectors.js | 9 ++ 16 files changed, 279 insertions(+), 191 deletions(-) create mode 100644 app/ui/components/rendered-text.js diff --git a/app/common/render.js b/app/common/render.js index ebba283ff1..5adcc3718e 100644 --- a/app/common/render.js +++ b/app/common/render.js @@ -7,23 +7,14 @@ import * as models from '../models'; import {setDefaultProtocol} from './misc'; import * as db from './database'; import * as templating from '../templating'; +import type {CookieJar} from '../models/cookie-jar'; export const KEEP_ON_ERROR = 'keep'; export const THROW_ON_ERROR = 'throw'; -type Cookie = { - domain: string, - path: string, - key: string, - value: string, - expires: number -} - export type RenderedRequest = Request & { cookies: Array<{name: string, value: string, disabled?: boolean}>, - cookieJar: { - cookies: Array - } + cookieJar: CookieJar }; export async function buildRenderContext ( @@ -227,7 +218,8 @@ export async function getRenderedRequest ( models.workspace.type ]); const workspace = ancestors.find(doc => doc.type === models.workspace.type); - const cookieJar = await models.cookieJar.getOrCreateForWorkspace(workspace); + const parentId = workspace ? workspace._id : 'n/a'; + const cookieJar = await models.cookieJar.getOrCreateForParentId(parentId); const renderContext = await getRenderContext(request, environmentId, ancestors); diff --git a/app/models/cookie-jar.js b/app/models/cookie-jar.js index 4509b25c3a..55bfb0d7e4 100644 --- a/app/models/cookie-jar.js +++ b/app/models/cookie-jar.js @@ -1,9 +1,27 @@ +// @flow import * as db from '../common/database'; +import type {BaseModel} from './index'; export const name = 'Cookie Jar'; export const type = 'CookieJar'; export const prefix = 'jar'; export const canDuplicate = true; + +export type Cookie = { + domain: string, + path: string, + key: string, + value: string, + expires: number +} + +type BaseCookieJar = { + name: string, + cookies: Array +}; + +export type CookieJar = BaseModel & BaseCookieJar; + export function init () { return { name: 'Default Jar', @@ -11,16 +29,15 @@ export function init () { }; } -export function migrate (doc) { +export function migrate (doc: CookieJar): CookieJar { return doc; } -export function create (patch = {}) { +export function create (patch: Object = {}) { return db.docCreate(type, patch); } -export async function getOrCreateForWorkspace (workspace) { - const parentId = workspace._id; +export async function getOrCreateForParentId (parentId: string) { const cookieJars = await db.find(type, {parentId}); if (cookieJars.length === 0) { return await create({parentId}); @@ -33,10 +50,10 @@ export function all () { return db.all(type); } -export function getById (id) { +export function getById (id: string) { return db.get(type, id); } -export function update (cookieJar, patch) { +export function update (cookieJar: CookieJar, patch: Object = {}) { return db.docUpdate(cookieJar, patch); } diff --git a/app/models/index.js b/app/models/index.js index bab5a54a73..1dce32c354 100644 --- a/app/models/index.js +++ b/app/models/index.js @@ -37,24 +37,22 @@ export const requestMeta = _requestMeta; export const response = _response; export const oAuth2Token = _oAuth2Token; -const _models = { - [stats.type]: stats, - [settings.type]: settings, - [workspace.type]: workspace, - [workspaceMeta.type]: workspaceMeta, - [environment.type]: environment, - [cookieJar.type]: cookieJar, - [requestGroup.type]: requestGroup, - [requestGroupMeta.type]: requestGroupMeta, - [request.type]: request, - [requestVersion.type]: requestVersion, - [requestMeta.type]: requestMeta, - [response.type]: response, - [oAuth2Token.type]: oAuth2Token -}; - export function all () { - return Object.keys(_models).map(type => _models[type]); + return [ + stats, + settings, + workspace, + workspaceMeta, + environment, + cookieJar, + requestGroup, + requestGroupMeta, + request, + requestVersion, + requestMeta, + response, + oAuth2Token + ]; } export function types () { @@ -62,7 +60,7 @@ export function types () { } export function getModel (type: string) { - return _models[type] || null; + return all().find(m => m.type === type) || null; } export function canDuplicate (type: string) { @@ -88,12 +86,12 @@ export function initModel (type: string, ...sources: Array) { const model = getModel(type); if (!model) { - const choices = Object.keys(_models).join(', '); + const choices = all().map(m => m.type).join(', '); throw new Error(`Tried to init invalid model "${type}". Choices are ${choices}`); } // Define global default fields - const objectDefaults = Object.assign({ + const objectDefaults = Object.assign({}, { _id: null, type: type, parentId: null, diff --git a/app/templating/extensions/__tests__/request-extension.test.js b/app/templating/extensions/__tests__/request-extension.test.js index db78d6a38a..52aed25816 100644 --- a/app/templating/extensions/__tests__/request-extension.test.js +++ b/app/templating/extensions/__tests__/request-extension.test.js @@ -13,7 +13,7 @@ describe('RequestExtension cookie', async () => { parentId: workspace._id, url: 'https://insomnia.rest/foo/bar' }); - const cookieJar = await models.cookieJar.getOrCreateForWorkspace(workspace); + const cookieJar = await models.cookieJar.getOrCreateForParentId(workspace._id); const jar = jarFromCookies(cookieJar.cookies); jar.setCookieSync([ 'foo=bar', diff --git a/app/templating/extensions/request-extension.js b/app/templating/extensions/request-extension.js index 0f6fe8118e..9bd5658070 100644 --- a/app/templating/extensions/request-extension.js +++ b/app/templating/extensions/request-extension.js @@ -54,7 +54,7 @@ export default { case 'url': return getRequestUrl(context, request); case 'cookie': - const cookieJar = await context.util.models.cookieJar.getOrCreateForWorkspace(workspace); + const cookieJar = await context.util.models.cookieJar.getOrCreateForParentId(workspace._id); const url = await getRequestUrl(context, request); const value = await getCookieValue(cookieJar, url, name); return value; diff --git a/app/ui/components/cookie-list.js b/app/ui/components/cookie-list.js index e4cdcfb4a7..7af22cc975 100644 --- a/app/ui/components/cookie-list.js +++ b/app/ui/components/cookie-list.js @@ -5,13 +5,10 @@ import {Cookie} from 'tough-cookie'; import {cookieToString} from '../../common/cookies'; import PromptButton from './base/prompt-button'; +import RenderedText from './rendered-text'; @autobind class CookieList extends PureComponent { - shouldComponentUpdate (nextProps, nextState) { - return nextProps.cookies !== this.props.cookies; - } - _handleCookieAdd () { const newCookie = new Cookie({ key: 'foo', @@ -32,7 +29,8 @@ class CookieList extends PureComponent { render () { const { cookies, - handleShowModifyCookieModal + handleShowModifyCookieModal, + handleRender } = this.props; return ( @@ -43,10 +41,10 @@ class CookieList extends PureComponent { Domain Cookie - @@ -57,18 +55,20 @@ class CookieList extends PureComponent { return ( - handleShowModifyCookieModal(cookie)}> + {cookie.domain} - - handleShowModifyCookieModal(cookie)}> + + {cookieString} - - - + + + {' '} + this._handleDeleteCookie(cookie)} @@ -87,8 +87,7 @@ class CookieList extends PureComponent { I couldn't find any cookies for you.

-

@@ -104,7 +103,8 @@ CookieList.propTypes = { onCookieDelete: PropTypes.func.isRequired, cookies: PropTypes.array.isRequired, newCookieDomainName: PropTypes.string.isRequired, - handleShowModifyCookieModal: PropTypes.func.isRequired + handleShowModifyCookieModal: PropTypes.func.isRequired, + handleRender: PropTypes.func.isRequired }; export default CookieList; diff --git a/app/ui/components/modals/cookie-modify-modal.js b/app/ui/components/modals/cookie-modify-modal.js index 1164178872..56c2451419 100644 --- a/app/ui/components/modals/cookie-modify-modal.js +++ b/app/ui/components/modals/cookie-modify-modal.js @@ -1,6 +1,8 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; +import {Tabs, TabList, Tab, TabPanel} from 'react-tabs'; import autobind from 'autobind-decorator'; +import {Cookie} from 'tough-cookie'; import * as models from '../../../models'; import {DEBOUNCE_MILLIS} from '../../../common/constants'; import {trackEvent} from '../../../analytics/index'; @@ -9,6 +11,7 @@ import ModalBody from '../base/modal-body'; import ModalHeader from '../base/modal-header'; import ModalFooter from '../base/modal-footer'; import OneLineEditor from '../codemirror/one-line-editor'; +import {cookieToString} from '../../../common/cookies'; @autobind class CookieModifyModal extends PureComponent { @@ -17,16 +20,8 @@ class CookieModifyModal extends PureComponent { this.state = { cookieJar: null, - defaultCookie: { - key: 'foo', - value: 'bar', - domain: 'domain.tld', - path: '/', - expires: Infinity, - secure: false, - httpOnly: false - }, cookie: null, + rawValue: '', isValid: { key: true, value: true, @@ -43,7 +38,7 @@ class CookieModifyModal extends PureComponent { async _load () { const {workspace} = this.props; - const cookieJar = await models.cookieJar.getOrCreateForWorkspace(workspace); + const cookieJar = await models.cookieJar.getOrCreateForParentId(workspace._id); this.setState({cookieJar}); } @@ -66,11 +61,20 @@ class CookieModifyModal extends PureComponent { async _saveChanges () { const {cookieJar} = this.state; await models.cookieJar.update(cookieJar); - this.props.reloadCookiesModal(); - this._load(); + await this._load(); } - _handleCookieUpdate (oldCookie, cookie) { + _handleChangeRawValue (e) { + const value = e.target.value; + + clearTimeout(this._rawTimeout); + this._rawTimeout = setTimeout(async () => { + const cookie = Cookie.parse(value); + await this._handleCookieUpdate(this.state.cookie, cookie); + }, DEBOUNCE_MILLIS); + } + + async _handleCookieUpdate (oldCookie, cookie) { const {cookieJar} = this.state; const {cookies} = cookieJar; const index = cookies.findIndex(c => c.domain === oldCookie.domain && c.key === oldCookie.key); @@ -81,7 +85,9 @@ class CookieModifyModal extends PureComponent { ...cookies.slice(index + 1) ]; - this._saveChanges(cookieJar); + this.setState({cookie}); + + await this._saveChanges(cookieJar); trackEvent('Cookie', 'Update'); } @@ -120,61 +126,92 @@ class CookieModifyModal extends PureComponent { handleRender, handleGetRenderContext } = this.props; + const { isValid, cookie, - defaultCookie + cookieJar } = this.state; + if (!cookie || !cookieJar) { + return null; + } + const textFields = ['key', 'value', 'domain', 'path', 'expires']; const checkFields = ['secure', 'httpOnly']; + let rawCookieString = ''; + try { + rawCookieString = cookieToString(Cookie.fromJSON(JSON.stringify(cookie))); + } catch (err) { + console.warn('Failed to parse cookie', err); + } + return ( - Modify Cookie + Edit Cookie -
- {textFields.map((field, i) => { - const val = Object.assign({}, defaultCookie, cookie)[field].toString(); + + + + + + + + + + +
+ {textFields.map((field, i) => { + const val = (cookie[field] || '').toString(); - return ( -
- -
- ); - })} -
-
- {checkFields.map((field, i) => { - const checked = Object.assign({}, defaultCookie, cookie)[field]; + return ( +
+ +
+ ); + })} +
+
+ {checkFields.map((field, i) => { + const checked = !!cookie[field]; - return ( -
+
+ +
+ - ); - })} -
+
+ +
-
- * cookies are automatically sent with relevant requests -
@@ -185,7 +222,6 @@ class CookieModifyModal extends PureComponent { } CookieModifyModal.propTypes = { - reloadCookiesModal: PropTypes.func.isRequired, handleRender: PropTypes.func.isRequired, handleGetRenderContext: PropTypes.func.isRequired, workspace: PropTypes.object.isRequired diff --git a/app/ui/components/modals/cookies-modal.js b/app/ui/components/modals/cookies-modal.js index 45907cc33d..24bf554ab4 100644 --- a/app/ui/components/modals/cookies-modal.js +++ b/app/ui/components/modals/cookies-modal.js @@ -1,5 +1,5 @@ +// @flow import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; import autobind from 'autobind-decorator'; import Modal from '../base/modal'; import ModalBody from '../base/modal-body'; @@ -8,63 +8,74 @@ import ModalFooter from '../base/modal-footer'; import CookieList from '../cookie-list'; import * as models from '../../../models'; import {trackEvent} from '../../../analytics/index'; +import type {Cookie, CookieJar} from '../../../models/cookie-jar'; +import type {Workspace} from '../../../models/workspace'; @autobind class CookiesModal extends PureComponent { - constructor (props) { + props: { + handleShowModifyCookieModal: Function, + handleRender: Function, + cookieJar: CookieJar, + workspace: Workspace + }; + + state: { + filter: string + }; + + modal: Modal | null; + filterInput: HTMLInputElement | null; + + constructor (props: any) { super(props); this.state = { - cookieJar: null, filter: '' }; } - _setModalRef (n) { + _setModalRef (n: React.Element<*> | null) { this.modal = n; } - _setFilterInputRef (n) { + _setFilterInputRef (n: HTMLInputElement | null) { this.filterInput = n; } async _saveChanges () { - const {cookieJar} = this.state; + const {cookieJar} = this.props; await models.cookieJar.update(cookieJar); - this._load(); } - _handleCookieAdd (cookie) { - const {cookieJar} = this.state; + async _handleCookieAdd (cookie: Cookie) { + const {cookieJar} = this.props; const {cookies} = cookieJar; + cookieJar.cookies = [cookie, ...cookies]; - this._saveChanges(cookieJar); + await this._saveChanges(); trackEvent('Cookie', 'Create'); } - _handleCookieDelete (cookie) { - const {cookieJar} = this.state; + async _handleCookieDelete (cookie: Cookie) { + const {cookieJar} = this.props; const {cookies} = cookieJar; // NOTE: This is sketchy because it relies on the same reference cookieJar.cookies = cookies.filter(c => c !== cookie); - this._saveChanges(cookieJar); + await this._saveChanges(); trackEvent('Cookie', 'Delete'); } - _handleFilterChange (e) { + _handleFilterChange (e: Event & {target: HTMLInputElement}) { const filter = e.target.value; this.setState({filter}); trackEvent('Cookie Editor', 'Filter Change'); } _getFilteredSortedCookies () { - const {cookieJar, filter} = this.state; - - if (!cookieJar) { - // Nothing to do yet. - return []; - } + const {cookieJar} = this.props; + const {filter} = this.state; const {cookies} = cookieJar; return cookies.filter(c => { @@ -73,39 +84,29 @@ class CookiesModal extends PureComponent { }); } - async _load () { - const {workspace} = this.props; - const cookieJar = await models.cookieJar.getOrCreateForWorkspace(workspace); - this.setState({cookieJar}); - } - async show () { - await this._load(); + this.modal && this.modal.show(); - this.modal.show(); - - setTimeout(() => this.filterInput.focus(), 100); + setTimeout(() => { + this.filterInput && this.filterInput.focus(); + }, 100); trackEvent('Cookie Manager', 'Show'); } hide () { - this.modal.hide(); - } - - toggle () { - if (this.modal.isOpen()) { - this.hide(); - } else { - this.show(); - } + this.modal && this.modal.hide(); } render () { const filteredCookies = this._getFilteredSortedCookies(); const { - handleShowModifyCookieModal + handleShowModifyCookieModal, + handleRender } = this.props; - const {filter} = this.state; + + const { + filter + } = this.state; return ( @@ -126,6 +127,7 @@ class CookiesModal extends PureComponent {
- * click a cookie to modify it + * cookies are automatically sent with relevant requests