Skip to content

Commit

Permalink
Merge pull request #35 from codecentric/fix-tree-not-updating-after-a…
Browse files Browse the repository at this point in the history
…ction

Update tree after action but keep toggled nodes stable
  • Loading branch information
ruettenm authored Jan 15, 2020
2 parents effa7f2 + fbfa960 commit bef4396
Show file tree
Hide file tree
Showing 17 changed files with 412 additions and 207 deletions.
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
"prettier:write": "prettier --write {src,test,mocks}/**/*.{ts,tsx,js,jsx}",
"lint": "tslint '{src,test,mocks}/**/*.{ts,tsx,js,jsx}' --project ./tsconfig.json",
"lint:fix": "tslint '{src,test,mocks}/**/*.{ts,tsx}' --project ./tsconfig.json --fix",
"test": "jest '(\\/test\\/).*'",
"test:integration": "jest '(\\/test\\/integration/).*'",
"test": "npm run test:unit",
"test:unit": "jest --testRegex '\\.test\\.tsx?$'",
"test:unit:watch": "jest --testRegex '\\.test\\.tsx?$' --watch",
"test:integration": "jest --testRegex '\\.itest\\.ts$'",
"release:check": "npm run lint && npm test",
"release": "npm run release:check && npm run build && electron-builder --publish onTag",
"postinstall": "electron-builder install-app-deps"
Expand All @@ -27,7 +29,7 @@
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "(/test/.+\\.spec)\\.tsx?$",
"testRegex": "\\.?test\\.tsx?$",
"moduleFileExtensions": [
"ts",
"tsx",
Expand Down
22 changes: 14 additions & 8 deletions src/renderer/components/tree/TreeComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@ export interface Tree {
toggled?: boolean
loading?: boolean
children?: Tree[]
entryId?: any
path: string
}

export interface TreeComponentProps {
tree: Tree
onLeafClick: (leafId: string) => void
}

export default class TreeComponent extends React.Component<TreeComponentProps, any> {
public state: any = {}
interface TreeComponentState { selectedNode?: any }
export default class TreeComponent extends React.Component<TreeComponentProps, TreeComponentState> {
public state: TreeComponentState = {}

public render() {
return <t.Treebeard
Expand All @@ -29,19 +30,24 @@ export default class TreeComponent extends React.Component<TreeComponentProps, a
}

private onToggle = (node: any, toggled: boolean) => {
if ((!node.children || node.children.length === 0) && !!node.entryId) {
this.props.onLeafClick(node.entryId)
// if no children (thus being a leaf and thereby an entry), trigger the handler
if ((!node.children || node.children.length === 0)) {
this.props.onLeafClick(node.path)
}

if (this.state.cursor) {
this.state.cursor.active = false
// previously selected node is no more active
if (this.state.selectedNode) {
this.state.selectedNode.active = false
}

// newly selected node shall be active
node.active = true

// ...and toggled if having children
if (node.children) {
node.toggled = toggled
}
this.setState({ cursor: node })
this.setState({ selectedNode: node })

if (node.children && node.children.length === 1) {
this.onToggle(node.children[ 0 ], true)
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/tree/TreeHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { deriveIconFromSecretName } from '../../secrets/deriveIconFromSecretName
export const TreeHeader = ({ style, node }: any) => {
let iconType = node.toggled ? 'chevron_right' : 'folder'

if (!node.children && node.entryId) {
if (!node.children && node.path) {
iconType = deriveIconFromSecretName(node.name)
}

Expand Down
61 changes: 56 additions & 5 deletions src/renderer/explorer-app/ExplorerApplication.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,61 @@ import SecretExplorer from './side-navigation/SecretExplorer'
import MainContent from './MainContent'

import './ExplorerApplication.css'
import { Tree } from '../components/tree/TreeComponent'
import Gopass from '../secrets/Gopass'
import SecretsFilterService from './side-navigation/SecretsFilterService'
import SecretsDirectoryService from './side-navigation/SecretsDirectoryService'

const ExplorerApplication = () => <>
<SecretExplorer/>
<MainContent/>
</>
interface ExplorerApplicationState {
tree: Tree
secretNames: string[]
searchValue: string
}

export default ExplorerApplication
export default class ExplorerApplication extends React.Component<{}, ExplorerApplicationState> {
constructor(props: any) {
super(props)
this.state = {
tree: {
name: 'Stores',
toggled: true,
children: [],
path: ''
},
searchValue: '',
secretNames: []
}
}

public async componentDidMount() {
const secretNames = await Gopass.getAllSecretNames()
this.setState({ secretNames })
await this.calculateAndSetTreeState(this.state.searchValue)
}

public render() {
const { tree, searchValue } = this.state
return (
<>
<SecretExplorer
tree={tree}
searchValue={searchValue}
onSearchValueChange={async (newValue: string) => {
if (newValue !== searchValue) {
await this.calculateAndSetTreeState(newValue)
}
this.setState({ searchValue: newValue })
}}/>
<MainContent onTreeUpdate={() => this.componentDidMount()}/>
</>
)
}

private async calculateAndSetTreeState(searchValue: string) {
const { secretNames, tree: previousTree } = this.state

const filteredSecretNames = SecretsFilterService.filterBySearch(secretNames, searchValue)
const tree: Tree = SecretsDirectoryService.secretPathsToTree(filteredSecretNames, previousTree, filteredSecretNames.length <= 15)
this.setState({ ...this.state, tree })
}
}
9 changes: 5 additions & 4 deletions src/renderer/explorer-app/MainContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import PasswordHealthOverview from './pages/PasswordHealthPage'
import AddSecretPage from './pages/AddSecretPage'
import SecretDetailsPage from './pages/details/SecretDetailsPage'

function MainContentRoutes() {
function MainContentRoutes({ onTreeUpdate }: { onTreeUpdate: () => void }) {
return <>
<Route
path='/'
Expand All @@ -34,6 +34,7 @@ function MainContentRoutes() {
<>
<MainNavigation/>
<SecretDetailsPage
onSecretDelete={onTreeUpdate}
secretName={secretName}
isAdded={isAdded}
/>
Expand Down Expand Up @@ -67,20 +68,20 @@ function MainContentRoutes() {
render={() => (
<>
<GoBackNavigation/>
<AddSecretPage/>
<AddSecretPage onSecretSave={onTreeUpdate} />
</>
)}
/>
</>
}

function MainContent() {
function MainContent({ onTreeUpdate }: { onTreeUpdate: () => void }) {
return <div className='main-content'>
<NotificationProvider>
<Notification/>
<m.Row>
<m.Col s={12}>
<MainContentRoutes/>
<MainContentRoutes onTreeUpdate={onTreeUpdate}/>
</m.Col>
</m.Row>
</NotificationProvider>
Expand Down
Empty file.
5 changes: 3 additions & 2 deletions src/renderer/explorer-app/pages/AddSecretPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface AddSecretPageState {
value?: string
}

class AddSecretPage extends React.Component<RouteComponentProps, AddSecretPageState> {
class AddSecretPage extends React.Component<RouteComponentProps & { onSecretSave: () => void }, AddSecretPageState> {
constructor(props: any) {
super(props)
this.state = {
Expand Down Expand Up @@ -79,10 +79,11 @@ class AddSecretPage extends React.Component<RouteComponentProps, AddSecretPageSt
const { name, value } = this.state

if (name && value) {
const { history } = this.props
const { history, onSecretSave } = this.props

try {
await Gopass.addSecret(name, value)
onSecretSave()
history.replace(`/secret/${btoa(name)}?added`)
} catch (e) {
console.info('Error during adding a secret', e)
Expand Down
9 changes: 7 additions & 2 deletions src/renderer/explorer-app/pages/details/SecretDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import PasswordRatingComponent from '../../password-health/PasswordRatingCompone
interface SecretDetailsPageProps extends RouteComponentProps {
secretName: string
isAdded?: boolean
onSecretDelete: () => void
}

function SecretDetailsPage({ secretName, isAdded, history }: SecretDetailsPageProps) {
function SecretDetailsPage({ secretName, isAdded, history, onSecretDelete }: SecretDetailsPageProps) {
const [ secretValue, setSecretValue ] = React.useState('')
const [ historyEntries, setHistoryEntries ] = React.useState<HistoryEntry[]>([])
const [ loading, setLoading ] = React.useState(true)
Expand Down Expand Up @@ -44,7 +45,11 @@ function SecretDetailsPage({ secretName, isAdded, history }: SecretDetailsPagePr
const confirmSecretDeletion = () => Gopass.deleteSecret(secretName).then(() => history.replace('/'))
const deletionModeButtons = queryDeletion ? <>
<a className='link' onClick={denySecretDeletion}>NO, keep it!</a>
<a className='link' onClick={confirmSecretDeletion}>Sure!</a>
<a className='link'
onClick={async () => {
await confirmSecretDeletion()
onSecretDelete()
}}>Sure!</a>
</> : <a className='link' onClick={querySecretDeletion}>Delete</a>

const querySecretEditing = () => setEditedSecretValue(secretValue)
Expand Down
32 changes: 17 additions & 15 deletions src/renderer/explorer-app/side-navigation/SecretExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,34 @@ import * as KeyboardEventHandler from 'react-keyboard-event-handler'

import { RouteComponentProps, withRouter } from 'react-router'
import SecretTree from './SecretTree'
import { Tree } from '../../components/tree/TreeComponent'

class SecretExplorer extends React.Component<RouteComponentProps, { searchValue: string }> {
constructor(props: RouteComponentProps) {
super(props)
this.state = { searchValue: '' }
}
interface SecretExplorerProps extends RouteComponentProps {
tree: Tree
onSearchValueChange: (value: string) => void
searchValue: string
}

class SecretExplorer extends React.Component<SecretExplorerProps, {}> {
public render() {
const { searchValue } = this.state
const { tree, searchValue, onSearchValueChange } = this.props

return (
<div className='secret-explorer'>
<KeyboardEventHandler handleKeys={[ 'esc' ]} handleFocusableElements onKeyEvent={this.onCancelSearch}/>
<m.Input className='search-bar' value={searchValue} placeholder='Search...' onChange={this.onSearchChange}/>
<SecretTree searchValue={searchValue} onSecretClick={this.onSecretClick}/>
<KeyboardEventHandler handleKeys={[ 'esc' ]} handleFocusableElements onKeyEvent={this.clearSearch}/>
<m.Input className='search-bar' value={searchValue} placeholder='Search...' onChange={(_: any, updatedSearchValue: string) => {
onSearchValueChange(updatedSearchValue)
}}/>
<SecretTree tree={tree} onSecretClick={secretName => {
this.navigateToSecretDetailView(secretName)
}}/>
</div>
)
}

private onSecretClick = (secretName: string) => {
this.props.history.replace(`/secret/${btoa(secretName)}`)
}

private onSearchChange = (_: any, searchValue: string) => this.setState({ searchValue })
private navigateToSecretDetailView = (secretName: string) => this.props.history.replace(`/secret/${btoa(secretName)}`)

private onCancelSearch = () => this.setState({ searchValue: '' })
private clearSearch = () => this.setState({ searchValue: '' })
}

export default withRouter(SecretExplorer)
41 changes: 3 additions & 38 deletions src/renderer/explorer-app/side-navigation/SecretTree.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,18 @@
import * as React from 'react'

import Gopass from '../../secrets/Gopass'
import TreeComponent, { Tree } from '../../components/tree/TreeComponent'
import SecretsFilterService from './SecretsFilterService'
import SecretsDirectoryService from './SecretsDirectoryService'

export interface SecretTreeViewerProps {
onSecretClick: (name: string) => void
searchValue: string
tree: Tree
}

export default class SecretTreeViewer extends React.Component<SecretTreeViewerProps, { tree: Tree, secretNames: string[] }> {
constructor(props: any) {
super(props)
this.state = {
tree: {
name: 'Stores',
toggled: true,
children: []
},
secretNames: []
}
}

public async componentDidMount() {
const secretNames = await Gopass.getAllSecretNames()
this.setState({ secretNames })
await this.calculateAndSetTreeState(this.props.searchValue)
}

public async componentWillReceiveProps(newProps: SecretTreeViewerProps) {

if (newProps.searchValue !== this.props.searchValue) {
await this.calculateAndSetTreeState(newProps.searchValue)
}
}

export default class SecretTreeViewer extends React.Component<SecretTreeViewerProps, {}> {
public render() {
return (
<TreeComponent
tree={this.state.tree}
tree={this.props.tree}
onLeafClick={this.props.onSecretClick}
/>
)
}

private async calculateAndSetTreeState(searchValue: string) {
const filteredSecretNames = SecretsFilterService.filterBySearch(this.state.secretNames, searchValue)
const tree: Tree = SecretsDirectoryService.secretPathsToTree(filteredSecretNames)
this.setState({ ...this.state, tree })
}
}
Loading

0 comments on commit bef4396

Please sign in to comment.