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
import url from 'url'
import React from 'react'
import domtoimage from 'dom-to-image'
import dynamic from 'next/dynamic'
@ -15,7 +14,6 @@ import Carbon from './Carbon'
import ExportMenu from './ExportMenu'
import Themes from './Themes'
import TweetButton from './TweetButton'
import GistContainer from './GistContainer'
import {
LANGUAGES,
LANGUAGE_MIME_HASH,
@ -29,7 +27,7 @@ import {
DEFAULT_LANGUAGE,
DEFAULT_PRESET_ID
} from '../lib/constants'
import { serializeState, getQueryStringState } from '../lib/routing'
import { serializeState, getRouteState } from '../lib/routing'
import { getSettings, unescapeHtml, formatCode, omit } from '../lib/util'
import LanguageIcon from './svg/Language'
@ -41,6 +39,7 @@ const BackgroundSelect = dynamic(() => import('./BackgroundSelect'), {
class Editor extends React.Component {
static contextType = ApiContext
constructor(props) {
super(props)
this.state = {
@ -61,15 +60,24 @@ class Editor extends React.Component {
}
async componentDidMount() {
const { asPath = '' } = this.props.router
const { query } = url.parse(asPath, true)
const initialState = getQueryStringState(query)
const { queryState, parameter } = getRouteState(this.props.router)
// 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 = {
// Load from localStorage
...getSettings(localStorage),
// Load options from gist or localStorage
...(gistState ? gistState : getSettings(localStorage)),
// and then URL params
...initialState,
...queryState,
loading: false
}
@ -88,9 +96,11 @@ class Editor extends React.Component {
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 })
updateAspectRatio = aspectRatio => this.updateState({ aspectRatio })
async getCarbonImage(
@ -335,8 +345,6 @@ class Editor extends React.Component {
</Overlay>
)}
</Dropzone>
<GistContainer onChange={stateFromGist => this.setState(stateFromGist)} />
<style jsx>
{`
.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 */
import { editorVisible } from '../support'
describe('Gist', () => {
const test = Cypress.env('CI') ? it.skip : it
test('Should pull text from the first Gist file', () => {
cy.visit('/3208813b324d82a9ebd197e4b1c3bae8')
editorVisible()
@ -9,4 +11,20 @@ describe('Gist', () => {
cy.contains('Y-Combinator implemented in 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,11 +42,12 @@ function image(state) {
// ~ makes the file come later alphabetically, which is how gists are sorted
const CARBON_STORAGE_KEY = '~carbon.json'
function getGist(uid) {
return gistClient
async function getGist(uid) {
try {
return await gistClient
.get(`/gists/${uid}`)
.then(res => res.data)
.then(({ owner, files }) => {
.then(({ id, owner, files }) => {
let config
if (files[CARBON_STORAGE_KEY]) {
try {
@ -61,12 +62,20 @@ function getGist(uid) {
const snippet = files[otherFiles[0]]
return {
code: snippet.content,
language: snippet.language,
id,
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

@ -1,4 +1,7 @@
import Morph from 'morphmorph'
import url from 'url'
import { escapeHtml } from './util'
const mapper = new Morph({
types: {
@ -31,7 +34,8 @@ const mappings = [
{ field: 'es:exportSize' },
{ field: 'wm:watermark', type: 'bool' },
{ field: 'copy', type: 'bool' },
{ field: 'readonly', type: 'bool' }
{ field: 'readonly', type: 'bool' },
{ field: 'id' }
]
const reverseMappings = mappings.map(mapping =>
@ -62,27 +66,21 @@ export const deserializeState = serializedState => {
return JSON.parse(decodeURIComponent(stateString))
}
const getQueryStringObject = query => {
if (query.state) {
return deserializeState(query.state)
export const getRouteState = router => {
const { asPath = '' } = router
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 => {
const queryParams = getQueryStringObject(query)
return Object.keys(queryParams).length ? queryParams : {}
}
export const updateQueryString = (router, state) => {
export const updateRouteState = (router, state) => {
const mappedState = mapper.map(reverseMappings, state)
serializeCode(mappedState)
// calls `encodeURIComponent` on each key internally
@ -101,6 +99,26 @@ export const updateQueryString = (router, state) => {
}
// 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) {
try {
if (state.code) state.code = encodeURIComponent(state.code)

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

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

Loading…
Cancel
Save