update gist handling (#715)

* update gist handling

* address comments

* clean up a bit

* getRouteState(router)

* move try/catch into getGist

* updateQueryString -> updateRouteState

* clean up more

* add cypress tests

* delete comments

* remove wait
main
Sean 6 years ago committed by Michael Fix
parent eefc49b737
commit d4d0ef9949

@ -1,5 +1,4 @@
// Theirs // Theirs
import url from 'url'
import React from 'react' import React from 'react'
import domtoimage from 'dom-to-image' import domtoimage from 'dom-to-image'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
@ -15,7 +14,6 @@ import Carbon from './Carbon'
import ExportMenu from './ExportMenu' import ExportMenu from './ExportMenu'
import Themes from './Themes' import Themes from './Themes'
import TweetButton from './TweetButton' import TweetButton from './TweetButton'
import GistContainer from './GistContainer'
import { import {
LANGUAGES, LANGUAGES,
LANGUAGE_MIME_HASH, LANGUAGE_MIME_HASH,
@ -29,7 +27,7 @@ import {
DEFAULT_LANGUAGE, DEFAULT_LANGUAGE,
DEFAULT_PRESET_ID DEFAULT_PRESET_ID
} from '../lib/constants' } from '../lib/constants'
import { serializeState, getQueryStringState } from '../lib/routing' import { serializeState, getRouteState } from '../lib/routing'
import { getSettings, unescapeHtml, formatCode, omit } from '../lib/util' import { getSettings, unescapeHtml, formatCode, omit } from '../lib/util'
import LanguageIcon from './svg/Language' import LanguageIcon from './svg/Language'
@ -41,6 +39,7 @@ const BackgroundSelect = dynamic(() => import('./BackgroundSelect'), {
class Editor extends React.Component { class Editor extends React.Component {
static contextType = ApiContext static contextType = ApiContext
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
@ -61,15 +60,24 @@ class Editor extends React.Component {
} }
async componentDidMount() { async componentDidMount() {
const { asPath = '' } = this.props.router const { queryState, parameter } = getRouteState(this.props.router)
const { query } = url.parse(asPath, true)
const initialState = getQueryStringState(query) // TODO we could create an interface for loading this config, so that it looks identical
// whether config is loaded from localStorage, gist, or even something like IndexDB
let gistState
if (this.context.gist && parameter) {
const { config, ...gist } = (await this.context.gist.get(parameter)) || {}
if (typeof config === 'object') {
this.gist = gist
gistState = config
}
}
const newState = { const newState = {
// Load from localStorage // Load options from gist or localStorage
...getSettings(localStorage), ...(gistState ? gistState : getSettings(localStorage)),
// and then URL params // and then URL params
...initialState, ...queryState,
loading: false loading: false
} }
@ -88,9 +96,11 @@ class Editor extends React.Component {
carbonNode = React.createRef() carbonNode = React.createRef()
updateState = updates => this.setState(updates, () => this.props.onUpdate(this.state)) updateState = updates =>
this.setState(updates, () => !this.gist && this.props.onUpdate(this.state))
updateCode = code => this.updateState({ code }) updateCode = code => this.updateState({ code })
updateAspectRatio = aspectRatio => this.updateState({ aspectRatio }) updateAspectRatio = aspectRatio => this.updateState({ aspectRatio })
async getCarbonImage( async getCarbonImage(
@ -335,8 +345,6 @@ class Editor extends React.Component {
</Overlay> </Overlay>
)} )}
</Dropzone> </Dropzone>
<GistContainer onChange={stateFromGist => this.setState(stateFromGist)} />
<style jsx> <style jsx>
{` {`
.editor { .editor {

@ -1,40 +0,0 @@
import React from 'react'
import { withRouter } from 'next/router'
import url from 'url'
import { escapeHtml } from '../lib/util'
import ApiContext from './ApiContext'
class GistContainer extends React.Component {
static contextType = ApiContext
async componentDidMount() {
const { asPath = '' } = this.props.router
const { pathname } = url.parse(asPath, true)
const path = escapeHtml(pathname.split('/').pop())
let newState = {}
if (this.context.gist && path.length >= 19 && path.indexOf('.') === -1) {
try {
const { code, language, config } = await this.context.gist.get(path)
if (typeof config === 'object') {
newState = config
}
if (language) {
newState.language = language.toLowerCase()
}
newState.code = code
} catch (e) {
// eslint-disable-next-line
console.log(e)
}
}
this.props.onChange(newState)
}
render() {
return null
}
}
export default withRouter(GistContainer)

@ -1,7 +1,9 @@
/* global cy Cypress */ /* global cy Cypress */
import { editorVisible } from '../support' import { editorVisible } from '../support'
describe('Gist', () => { describe('Gist', () => {
const test = Cypress.env('CI') ? it.skip : it const test = Cypress.env('CI') ? it.skip : it
test('Should pull text from the first Gist file', () => { test('Should pull text from the first Gist file', () => {
cy.visit('/3208813b324d82a9ebd197e4b1c3bae8') cy.visit('/3208813b324d82a9ebd197e4b1c3bae8')
editorVisible() editorVisible()
@ -9,4 +11,20 @@ describe('Gist', () => {
cy.contains('Y-Combinator implemented in JavaScript') cy.contains('Y-Combinator implemented in JavaScript')
cy.get('#downshift-input-JavaScript').should('have.value', 'JavaScript') cy.get('#downshift-input-JavaScript').should('have.value', 'JavaScript')
}) })
const pages = ['/', '/embed/']
pages.forEach(page => {
test(`${page} should not contain a query string in the url`, () => {
cy.visit('/82d742f4efad9757cc826d20f2a5e5af')
cy.url().should('not.contain', '?')
})
test(`${page} should have a green editor background`, () => {
cy.visit(`${page}82d742f4efad9757cc826d20f2a5e5af`)
cy.get('.container-bg .bg').should('have.css', 'background-color', 'rgb(0, 128, 0)')
})
})
}) })

@ -42,31 +42,40 @@ function image(state) {
// ~ makes the file come later alphabetically, which is how gists are sorted // ~ makes the file come later alphabetically, which is how gists are sorted
const CARBON_STORAGE_KEY = '~carbon.json' const CARBON_STORAGE_KEY = '~carbon.json'
function getGist(uid) { async function getGist(uid) {
return gistClient try {
.get(`/gists/${uid}`) return await gistClient
.then(res => res.data) .get(`/gists/${uid}`)
.then(({ owner, files }) => { .then(res => res.data)
let config .then(({ id, owner, files }) => {
if (files[CARBON_STORAGE_KEY]) { let config
try { if (files[CARBON_STORAGE_KEY]) {
config = JSON.parse(files[CARBON_STORAGE_KEY].content) try {
} catch (error) { config = JSON.parse(files[CARBON_STORAGE_KEY].content)
// pass } catch (error) {
// pass
}
} }
}
const otherFiles = Object.keys(files).filter(key => key !== CARBON_STORAGE_KEY) const otherFiles = Object.keys(files).filter(key => key !== CARBON_STORAGE_KEY)
const snippet = files[otherFiles[0]] const snippet = files[otherFiles[0]]
return { return {
code: snippet.content, id,
language: snippet.language, owner,
owner, config: {
config ...config,
} code: snippet.content,
}) language: snippet.language && snippet.language.toLowerCase()
}
}
})
} catch (error) {
// eslint-disable-next-line
console.log(e)
return null
}
} }
// private // private

@ -1,4 +1,7 @@
import Morph from 'morphmorph' import Morph from 'morphmorph'
import url from 'url'
import { escapeHtml } from './util'
const mapper = new Morph({ const mapper = new Morph({
types: { types: {
@ -31,7 +34,8 @@ const mappings = [
{ field: 'es:exportSize' }, { field: 'es:exportSize' },
{ field: 'wm:watermark', type: 'bool' }, { field: 'wm:watermark', type: 'bool' },
{ field: 'copy', type: 'bool' }, { field: 'copy', type: 'bool' },
{ field: 'readonly', type: 'bool' } { field: 'readonly', type: 'bool' },
{ field: 'id' }
] ]
const reverseMappings = mappings.map(mapping => const reverseMappings = mappings.map(mapping =>
@ -62,27 +66,21 @@ export const deserializeState = serializedState => {
return JSON.parse(decodeURIComponent(stateString)) return JSON.parse(decodeURIComponent(stateString))
} }
const getQueryStringObject = query => { export const getRouteState = router => {
if (query.state) { const { asPath = '' } = router
return deserializeState(query.state) const { query, pathname } = url.parse(asPath, true)
const queryState = getQueryStringState(query)
const path = escapeHtml(pathname.split('/').pop())
// TODO fix this hack
const parameter = path.length >= 19 && path.indexOf('.') === -1 && path
return {
parameter,
queryState
} }
const state = mapper.map(mappings, query)
deserializeCode(state)
Object.keys(state).forEach(key => {
if (state[key] === '') state[key] = undefined
})
return state
} }
export const getQueryStringState = query => { export const updateRouteState = (router, state) => {
const queryParams = getQueryStringObject(query)
return Object.keys(queryParams).length ? queryParams : {}
}
export const updateQueryString = (router, state) => {
const mappedState = mapper.map(reverseMappings, state) const mappedState = mapper.map(reverseMappings, state)
serializeCode(mappedState) serializeCode(mappedState)
// calls `encodeURIComponent` on each key internally // calls `encodeURIComponent` on each key internally
@ -101,6 +99,26 @@ export const updateQueryString = (router, state) => {
} }
// private // private
const getQueryStringObject = query => {
if (query.state) {
return deserializeState(query.state)
}
const state = mapper.map(mappings, query)
deserializeCode(state)
Object.keys(state).forEach(key => {
if (state[key] === '') state[key] = undefined
})
return state
}
function getQueryStringState(query) {
const queryParams = getQueryStringObject(query)
return Object.keys(queryParams).length ? queryParams : {}
}
function serializeCode(state) { function serializeCode(state) {
try { try {
if (state.code) state.code = encodeURIComponent(state.code) if (state.code) state.code = encodeURIComponent(state.code)

@ -2,15 +2,14 @@
import React from 'react' import React from 'react'
import Head from 'next/head' import Head from 'next/head'
import { withRouter } from 'next/router' import { withRouter } from 'next/router'
import url from 'url'
import morph from 'morphmorph' import morph from 'morphmorph'
// Ours // Ours
import ApiContext from '../components/ApiContext'
import { StylesheetLink, CodeMirrorLink, MetaTags } from '../components/Meta' import { StylesheetLink, CodeMirrorLink, MetaTags } from '../components/Meta'
import Carbon from '../components/Carbon' import Carbon from '../components/Carbon'
import GistContainer from '../components/GistContainer'
import { DEFAULT_CODE, DEFAULT_SETTINGS } from '../lib/constants' import { DEFAULT_CODE, DEFAULT_SETTINGS } from '../lib/constants'
import { getQueryStringState } from '../lib/routing' import { getRouteState } from '../lib/routing'
const isInIFrame = morph.get('parent.window.parent') const isInIFrame = morph.get('parent.window.parent')
const getParent = win => { const getParent = win => {
@ -46,6 +45,8 @@ const Page = props => (
) )
class Embed extends React.Component { class Embed extends React.Component {
static contextType = ApiContext
state = { state = {
...DEFAULT_SETTINGS, ...DEFAULT_SETTINGS,
code: DEFAULT_CODE, code: DEFAULT_CODE,
@ -53,18 +54,21 @@ class Embed extends React.Component {
readOnly: true readOnly: true
} }
handleUpdate = updates => { async componentDidMount() {
const { asPath = '' } = this.props.router const { queryState, parameter } = getRouteState(this.props.router)
const { query } = url.parse(asPath, true)
const initialState = getQueryStringState(query) let gistState
if (this.context.gist && parameter) {
const gist = await this.context.gist.get(parameter)
gistState = gist && gist.config
}
this.setState( this.setState(
{ {
...initialState, ...gistState,
...updates, ...queryState,
id: query.id, copyable: queryState.copy !== false,
copyable: initialState.copy !== false, readOnly: queryState.readonly !== false,
readOnly: initialState.readonly !== false,
mounted: true mounted: true
}, },
this.postMessage this.postMessage
@ -100,7 +104,6 @@ class Embed extends React.Component {
render() { render() {
return ( return (
<Page theme={this.state.theme}> <Page theme={this.state.theme}>
<GistContainer onChange={this.handleUpdate} />
{this.state.mounted && ( {this.state.mounted && (
<Carbon <Carbon
ref={this.ref} ref={this.ref}

@ -8,7 +8,7 @@ import debounce from 'lodash.debounce'
import Editor from '../components/Editor' import Editor from '../components/Editor'
import Page from '../components/Page' import Page from '../components/Page'
import { MetaLinks } from '../components/Meta' import { MetaLinks } from '../components/Meta'
import { updateQueryString } from '../lib/routing' import { updateRouteState } from '../lib/routing'
import { saveSettings, clearSettings, omit } from '../lib/util' import { saveSettings, clearSettings, omit } from '../lib/util'
class Index extends React.Component { class Index extends React.Component {
@ -23,7 +23,7 @@ class Index extends React.Component {
onEditorUpdate = debounce( onEditorUpdate = debounce(
state => { state => {
updateQueryString(this.props.router, state) updateRouteState(this.props.router, state)
saveSettings( saveSettings(
localStorage, localStorage,
omit(state, ['code', 'backgroundImage', 'backgroundImageSelection', 'filename']) omit(state, ['code', 'backgroundImage', 'backgroundImageSelection', 'filename'])

Loading…
Cancel
Save