Skip to content

Commit

Permalink
Merge pull request #1470 from oodamien
Browse files Browse the repository at this point in the history
* pr/1470:
  Add history feature

Closes gh-1470
  • Loading branch information
mhalbritter committed May 28, 2024
2 parents d939a1c + 1c8492d commit 8d605fe
Show file tree
Hide file tree
Showing 16 changed files with 554 additions and 8 deletions.
1 change: 1 addition & 0 deletions start-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"js-search": "^2.0.0",
"jszip": "^3.6.0",
"lodash": "^4.17.21",
"luxon": "^3.4.4",
"prism-react-renderer": "^1.2.0",
"prismjs": "^1.24.0",
"prop-types": "^15.7.2",
Expand Down
20 changes: 19 additions & 1 deletion start-client/src/components/Application.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { getConfig, getInfo, getProject } from './utils/ApiUtils'

const Explore = lazy(() => import('./common/explore/Explore'))
const Share = lazy(() => import('./common/share/Share'))
const History = lazy(() => import('./common/history/History'))
const HotKeys = lazy(() => import('./common/builder/HotKeys'))

export default function Application() {
Expand All @@ -32,6 +33,7 @@ export default function Application() {
theme,
share: shareOpen,
explore: exploreOpen,
history: historyOpen,
list,
dependencies,
} = useContext(AppContext)
Expand Down Expand Up @@ -78,6 +80,7 @@ export default function Application() {
setGenerating(false)
if (project) {
FileSaver.saveAs(project, `${get(values, 'meta.artifact')}.zip`)
dispatch({ type: 'ADD_HISTORY', payload: share })
}
}

Expand All @@ -102,7 +105,13 @@ export default function Application() {
setBlob(null)
dispatch({
type: 'UPDATE',
payload: { list: false, share: false, explore: false, nav: false },
payload: {
list: false,
share: false,
explore: false,
nav: false,
history: false,
},
})
}

Expand Down Expand Up @@ -163,6 +172,15 @@ export default function Application() {
onClose={onEscape}
/>
</Suspense>
<Suspense fallback=''>
<History open={historyOpen || false} onClose={onEscape} />
<Explore
projectName={`${get(values, 'meta.artifact')}.zip`}
blob={blob}
open={exploreOpen || false}
onClose={onEscape}
/>
</Suspense>
</>
)
}
23 changes: 23 additions & 0 deletions start-client/src/components/common/history/History.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import '../../../styles/history.scss'

import PropTypes from 'prop-types'
import React from 'react'

import Modal from './Modal'
import { Overlay } from '../form'

function History({ open, onClose }) {
return (
<>
<Modal open={open || false} onClose={onClose} />
<Overlay open={open || false} />
</>
)
}

History.propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
}

export default History
213 changes: 213 additions & 0 deletions start-client/src/components/common/history/Modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import PropTypes from 'prop-types'
import get from 'lodash/get'
import React, { useEffect, useRef, useContext, useMemo } from 'react'
import { CSSTransition, TransitionGroup } from 'react-transition-group'
import { clearAllBodyScrollLocks, disableBodyScroll } from 'body-scroll-lock'
import { AppContext } from '../../reducer/App'
import { Transform } from './Utils'
import queryString from 'query-string'

function HistoryDate({ label, items, onClose }) {
return (
<>
<div className='date'>{label}</div>
<ul>
{items.map(item => (
<HistoryItem
key={item.value}
time={item.time}
value={item.value}
onClose={onClose}
/>
))}
</ul>
</>
)
}

HistoryDate.propTypes = {
label: PropTypes.string.isRequired,
items: PropTypes.arrayOf(
PropTypes.shape({
time: PropTypes.string,
value: PropTypes.string,
})
),
onClose: PropTypes.func.isRequired,
}

HistoryDate.defaultProps = {
items: [],
}

function getLabelFromList(list, key) {
return list.find(item => item.key === key)?.text || key
}

function getLabelFromDepsList(list, key) {
return list.find(item => item.id === key)?.name || key
}

function HistoryItem({ time, value, onClose }) {
const { config } = useContext(AppContext)
const params = queryString.parse(value)
const deps = get(params, 'dependencies', '')
.split(',')
.filter(dep => !!dep)
return (
<li>
<a
className='item'
href={`/#${value}`}
onClick={() => {
onClose()
}}
>
<span className='time'>{time}</span>
<span className='desc'>
<span className='main'>
Project{' '}
<strong>
{getLabelFromList(
get(config, 'lists.project'),
get(params, 'type')
)}
</strong>
{`, `}
Language{' '}
<strong>
{getLabelFromList(
get(config, 'lists.language'),
get(params, 'language')
)}
</strong>
{`, `}
Spring Boot{' '}
<strong>
{getLabelFromList(
get(config, 'lists.boot'),
get(params, 'platformVersion')
)}
</strong>
</span>
<span className='deps'>
{deps.length === 0 && 'No dependencies'}
{deps.length > 0 && (
<>
Dependencies:{' '}
<strong>
{deps
.map(dep =>
getLabelFromDepsList(
get(config, 'lists.dependencies'),
dep
)
)
.join(', ')}
</strong>
</>
)}
</span>
</span>
</a>
</li>
)
}

HistoryItem.propTypes = {
time: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
}

function Modal({ open, onClose }) {
const wrapper = useRef(null)
const { histories, dispatch } = useContext(AppContext)

const historiesTransform = useMemo(() => Transform(histories), [histories])

useEffect(() => {
const clickOutside = event => {
const children = get(wrapper, 'current')
if (children && !children.contains(event.target)) {
onClose()
}
}
document.addEventListener('mousedown', clickOutside)
return () => {
document.removeEventListener('mousedown', clickOutside)
}
}, [onClose])

useEffect(() => {
if (get(wrapper, 'current') && open) {
disableBodyScroll(get(wrapper, 'current'))
}
return () => {
clearAllBodyScrollLocks()
}
}, [wrapper, open])

return (
<TransitionGroup component={null}>
{open && (
<CSSTransition classNames='popup' timeout={300}>
<div className='modal-share'>
<div className='modal-history-container' ref={wrapper}>
<div className='modal-header'>
<h1>History</h1>
<a
href='/#'
onClick={e => {
e.preventDefault()
dispatch({ type: 'CLEAR_HISTORY' })
onClose()
}}
className='button'
>
<span className='button-content' tabIndex='-1'>
<span>Clear</span>
</span>
</a>
</div>
<div className='modal-content'>
<div className='list'>
{historiesTransform.map(history => (
<HistoryDate
key={history.label}
label={history.label}
items={history.histories}
onClose={onClose}
/>
))}
</div>
</div>
<div className='modal-action'>
<a
href='/#'
onClick={e => {
e.preventDefault()
onClose()
}}
className='button'
>
<span className='button-content' tabIndex='-1'>
<span>Close</span>
<span className='secondary desktop-only'>ESC</span>
</span>
</a>
</div>
</div>
</div>
</CSSTransition>
)}
</TransitionGroup>
)
}

Modal.propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
}

export default Modal
46 changes: 46 additions & 0 deletions start-client/src/components/common/history/Utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { DateTime } from 'luxon'

function isToday(date) {
return date.hasSame(DateTime.now(), 'day')
}

function isYesterday(date) {
return date.hasSame(DateTime.now().minus({ days: 1 }), 'day')
}

export function Transform(histories) {
if (histories.length === 0) {
return []
}
const parsed = histories.map(history => {
const dateLuxon = DateTime.fromISO(history.date)
let label = ''
if (isToday(dateLuxon)) {
label = 'Today, '
} else if (isYesterday(dateLuxon)) {
label = 'Yesterday, '
}
return {
date: dateLuxon,
time: `${dateLuxon.toFormat('HH:mm')}`,
label: `${label}${dateLuxon.toFormat('cccc, d LLLL yyyy')}`,
value: history.value,
}
})
return parsed.reduce((acc, history) => {
if (acc.length === 0) {
acc.push({
label: history.label,
histories: [history],
})
} else if (acc[acc.length - 1].label === history.label) {
acc[acc.length - 1].histories.push(history)
} else {
acc.push({
label: history.label,
histories: [history],
})
}
return acc
}, [])
}
1 change: 1 addition & 0 deletions start-client/src/components/common/history/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as History } from './History'
31 changes: 31 additions & 0 deletions start-client/src/components/common/icons/Icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,3 +368,34 @@ export function IconEnter() {
</svg>
)
}

export function IconHistory() {
return (
<svg
aria-hidden='true'
focusable='false'
version='1.1'
viewBox='0 0 20 21'
width='20px'
role='img'
className='icon-history'
xmlns='http://www.w3.org/2000/svg'
>
<g>
<g
fill='currentColor'
id='Core'
opacity='0.9'
transform='translate(-464.000000, -254.000000)'
>
<g transform='translate(464.000000, 254.500000)'>
<path
d='M10.5,0 C7,0 3.9,1.9 2.3,4.8 L0,2.5 L0,9 L6.5,9 L3.7,6.2 C5,3.7 7.5,2 10.5,2 C14.6,2 18,5.4 18,9.5 C18,13.6 14.6,17 10.5,17 C7.2,17 4.5,14.9 3.4,12 L1.3,12 C2.4,16 6.1,19 10.5,19 C15.8,19 20,14.7 20,9.5 C20,4.3 15.7,0 10.5,0 L10.5,0 Z M9,5 L9,10.1 L13.7,12.9 L14.5,11.6 L10.5,9.2 L10.5,5 L9,5 L9,5 Z'
id='Shape'
/>
</g>
</g>
</g>
</svg>
)
}
1 change: 1 addition & 0 deletions start-client/src/components/common/icons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ export { IconSun } from './Icons'
export { IconMoon } from './Icons'
export { IconRemove } from './Icons'
export { IconEnter } from './Icons'
export { IconHistory } from './Icons'

export { IconSpring } from './IconSpring'
Loading

0 comments on commit 8d605fe

Please sign in to comment.