Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions app/containers/gistEditorForm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ class GistEditorForm extends Component {
}

render () {
const { handleCancel, formStyle } = this.props
const { footerHelper, handleCancel, formStyle } = this.props
const { values, errors, touched, submitAttempted, submitting } = this.state

return (
Expand Down Expand Up @@ -197,19 +197,25 @@ class GistEditorForm extends Component {
}) }
<hr/>
<div className='control-button-group'>
{ footerHelper && (
<div className='gist-editor-footer-helper'>
<span aria-hidden='true'>ⓘ</span>
<span>{ footerHelper }</span>
</div>
) }
<Button
className='gist-editor-control-button'
type='submit'
onClick={ handleCancel }
bsStyle='default'
disabled={ submitting }>
{ t('editor.submit') }
{ t('editor.cancel') }
</Button>
<Button
className='gist-editor-control-button'
onClick={ handleCancel }
type='submit'
bsStyle='default'
disabled={ submitting }>
{ t('editor.cancel') }
{ t('editor.submit') }
</Button>
</div>
</form>
Expand Down
14 changes: 14 additions & 0 deletions app/containers/gistEditorForm/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,21 @@
}

.control-button-group {
align-items: center;
display: flex;
height: 45px;
justify-content: flex-end;
}

.gist-editor-footer-helper {
@extend .font-style-base;
align-items: center;
color: var(--text-secondary);
display: flex;
font-size: 12px;
gap: 5px;
margin-right: auto;
min-width: 0;
}

.gist-editor-customized-tag {
Expand Down
166 changes: 147 additions & 19 deletions app/containers/userPanel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Modal from '../compatModal'
import { notifySuccess, notifyFailure } from '../../utilities/notifier'
import React, { Component } from 'react'
import { subscribeIpc, unsubscribeIpc } from '../../utilities/ipcSubscriptions'
import { t } from '../../utilities/i18n'
import { getLocale, t } from '../../utilities/i18n'
import {
addLangPrefix as Prefixed,
descriptionParser,
Expand All @@ -28,6 +28,12 @@ import {
CREATE_SINGLE_GIST,
getGitHubApi,
} from '../../utilities/githubApi'
import {
clearNewGistDraft,
createNewGistDraft,
createNewGistWithDraft,
loadNewGistDraft,
} from '../../utilities/newGistDraft'

import './index.scss'

Expand All @@ -53,6 +59,30 @@ const kIsPrivate = conf.get('snippet:newSnippetPrivate')
const hideProfilePhoto = conf.get('userPanel:hideProfilePhoto')

class UserPanel extends Component {
constructor (props) {
super(props)
const newGistDraft = loadNewGistDraft(
electronBridge.localStorage,
this.getUserLogin(props)
)
this.newGistInitialData = this.createEmptyNewGistData()
this.state = {
newGistDraft,
newGistDraftLoaded: false
}
}

getUserLogin (props = this.props) {
const profile = props.userSession && props.userSession.profile
return profile && profile.login
}

createEmptyNewGistData () {
return createNewGistDraft({
private: kIsPrivate
})
}

componentDidMount () {
this.ipcSubscriptions = []
subscribeIpc(ipcRenderer, this.ipcSubscriptions, 'new-gist-renderer', () => {
Expand All @@ -74,24 +104,59 @@ class UserPanel extends Component {
const isPublic = data.private === undefined ? true : !data.private
const description = data.description.trim()
const processedFiles = {}
const userLogin = this.getUserLogin()

this.newGistInitialData = createNewGistDraft(data)

data.gistFiles.forEach((file) => {
processedFiles[file.filename.trim()] = {
content: file.content
}
})

return getGitHubApi(CREATE_SINGLE_GIST)(this.props.accessToken, description, processedFiles, isPublic)
.catch((err) => {
notifyFailure(t('notification.gistCreationFailed'))
logger.error(JSON.stringify(err))
})
.then((response) => {
this.updateGistsStoreWithNewGist(response)
})
.finally(() => {
this.closeGistEditorModal()
return createNewGistWithDraft({
storage: electronBridge.localStorage,
userLogin,
data,
createGist: () => getGitHubApi(CREATE_SINGLE_GIST)(
this.props.accessToken,
description,
processedFiles,
isPublic
)
}).then((result) => {
if (result.status === 'failed') {
notifyFailure(
t('notification.gistCreationFailed'),
result.draftWrite && result.draftWrite.status ? t('notification.gistDraftSaved') : ''
)
logger.error(result.error && result.error.message
? result.error.message
: String(result.error))
if (result.draftWrite && result.draftWrite.status) {
this.setState({
newGistDraft: result.draftWrite.data,
newGistDraftLoaded: true
})
}
return
}

this.updateGistsStoreWithNewGist(result.gistDetails)
this.newGistInitialData = this.createEmptyNewGistData()

const draftClear = clearNewGistDraft(electronBridge.localStorage, userLogin)
if (!draftClear || !draftClear.status) {
logger.error('Failed to clear the saved new snippet draft')
}

this.setState({
newGistDraft: null,
newGistDraftLoaded: false
})

this.closeGistEditorModal()
})
}

updateGistsStoreWithNewGist (gistDetails) {
Expand Down Expand Up @@ -159,22 +224,84 @@ class UserPanel extends Component {
}

renderGistEditorModalBody () {
const initialData = {
description: '',
private: kIsPrivate,
gists: [
{ filename: '', content: '' }
]
}
const showDraftReplacementWarning = Boolean(
this.state.newGistDraft && !this.state.newGistDraftLoaded
)

return (
<GistEditorForm
initialData={ initialData }
initialData={ this.newGistInitialData }
formStyle={ NEW_GIST }
footerHelper={ showDraftReplacementWarning
? t('editor.localDraftSubmitWarning')
: '' }
handleCancel = { this.closeGistEditorModal.bind(this) }
onSubmit={ this.handleCreateSingleGist.bind(this) }></GistEditorForm>
)
}

formatNewGistDraftCreatedAt () {
return new Intl.DateTimeFormat(getLocale(), {
dateStyle: 'medium',
timeStyle: 'short'
}).format(new Date(this.state.newGistDraft.createdAt))
}

handleLoadNewGistDraft () {
this.newGistInitialData = createNewGistDraft(this.state.newGistDraft)
this.setState({ newGistDraftLoaded: true })
}

handleDropNewGistDraft () {
const draftClear = clearNewGistDraft(
electronBridge.localStorage,
this.getUserLogin()
)
if (!draftClear || !draftClear.status) {
logger.error('Failed to drop the saved new snippet draft')
return
}

if (this.state.newGistDraftLoaded) {
this.newGistInitialData = this.createEmptyNewGistData()
}
this.setState({
newGistDraft: null,
newGistDraftLoaded: false
})
}

renderNewGistDraftCallout () {
if (!this.state.newGistDraft) return null

return (
<div className='new-gist-draft-callout' role='status'>
<span className='new-gist-draft-icon' aria-hidden='true'>◷</span>
<div className='new-gist-draft-summary'>
<strong>{ this.state.newGistDraftLoaded
? t('editor.localDraftLoaded')
: t('editor.localDraftAvailable') }</strong>
<span>{ t('editor.localDraftCreatedAt', {
timestamp: this.formatNewGistDraftCreatedAt()
}) }</span>
</div>
<div className='new-gist-draft-actions'>
{ !this.state.newGistDraftLoaded && (
<button type='button' onClick={ this.handleLoadNewGistDraft.bind(this) }>
{ t('editor.loadLocalDraft') }
</button>
) }
<button
className='drop-new-gist-draft'
type='button'
onClick={ this.handleDropNewGistDraft.bind(this) }>
{ t('editor.dropLocalDraft') }
</button>
</div>
</div>
)
}

renderGistEditorModal () {
return (
<Modal
Expand All @@ -189,6 +316,7 @@ class UserPanel extends Component {
<Modal.Title>{ t('userPanel.new') }</Modal.Title>
</Modal.Header>
<Modal.Body>
{ this.renderNewGistDraftCallout() }
{ this.renderGistEditorModalBody.bind(this)() }
</Modal.Body>
</Modal>
Expand Down
44 changes: 44 additions & 0 deletions app/containers/userPanel/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,50 @@

.new-modal {
width: 98vw;

.new-gist-draft-callout {
align-items: center;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 3px;
color: var(--text-primary);
display: flex;
margin: 0 5px 15px;
min-height: 64px;
padding: 10px 14px;
}

.new-gist-draft-icon {
font-size: 26px;
line-height: 1;
margin-right: 12px;
}

.new-gist-draft-summary {
display: flex;
flex: 1 1 auto;
flex-direction: column;
font-size: 13px;
line-height: 1.5;
}

.new-gist-draft-actions {
display: flex;
flex: 0 0 auto;
gap: 20px;

button {
background: transparent;
border: 0;
color: #4078C0;
cursor: pointer;
padding: 5px;
}

.drop-new-gist-draft {
color: #B94A48;
}
}
}

/* Global setting affecting all dialogs (NEW/EDIT) if not overwritten. */
Expand Down
3 changes: 3 additions & 0 deletions app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,9 @@ const renderFixture = getRenderFixture(getRenderFixtureName())
if (renderFixture) {
logger.info(`[render-fixture] Rendering ${renderFixture.name} with mock state`)
SearchIndex.resetFuseIndex(renderFixture.searchIndexRecords)
Object.entries(renderFixture.localStorage || {}).forEach(([key, value]) => {
electronBridge.localStorage.set(key, value)
})
}

const reduxStore = renderFixture
Expand Down
16 changes: 16 additions & 0 deletions app/renderFixtures.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { addLangPrefix as Prefixed } from './utilities/parser'
import leptonLogoImage from './containers/aboutPage/logo-light.webp?inline'
import { getNewGistDraftStorageKey } from './utilities/newGistDraft'
import SearchIndex from './utilities/search'

const FIXTURE_USER = {
Expand Down Expand Up @@ -295,6 +296,8 @@ function getFixtureOverrides (name) {
return { logoutModalStatus: 'ON' }
case 'new':
return { gistNewModalStatus: 'ON' }
case 'new-draft':
return { gistNewModalStatus: 'ON' }
case 'pinned-tags':
return { pinnedTagsModalStatus: 'ON' }
case 'php-html':
Expand Down Expand Up @@ -403,6 +406,19 @@ export function getRenderFixture (name) {

return {
initialSearchQuery,
localStorage: name === 'new-draft'
? {
[getNewGistDraftStorageKey(FIXTURE_USER.login)]: {
createdAt: '2026-09-06T21:35:00.000Z',
description: 'Recovered API helper',
private: true,
gists: [{
filename: 'recovered.js',
content: 'const recovered = true'
}]
}
}
: null,
name,
searchIndexRecords,
state: Object.assign({}, getBaseState(), stateOverrides)
Expand Down
Loading