reload by clicking on last breadcrumb

Massimo Melina committed Jan 1, 2022 at 20:56 UTC 307e06aec737e24602a56d7443270c26716a95dd
6 files changed +65 -13
frontend/src/BrowseFiles.ts
+3 -3
@@ -13,13 +13,13 @@ export function usePath() {
13
14 interface DirEntry { n:string, s?:number, m?:string, c?:string }
15 export type DirList = DirEntry[]
16 -interface ListRes { list:DirList, loading?:boolean, err?:Error }
16 +interface ListRes { list:DirList, loading?:boolean, err?:Error, reload?:()=>void }
17
18 export const ListContext = createContext<ListRes>({ list:[], loading: false })
19
20 export function BrowseFiles() {
21 - const { list, loading, error } = useFetchList()
22 - return h(ListContext.Provider, { value:{ list, loading } },
21 + const { list, loading, error, reload } = useFetchList()
22 + return h(ListContext.Provider, { value:{ list, loading, reload } },
23 h(Head),
24 hError(error && 'Failed to retrieve list') || h(list ? FilesList : Spinner))
25 }
frontend/src/Head.ts
+20 -7
@@ -6,7 +6,7 @@ import { formatBytes, hIcon, prefix } from './misc'
6 import { Spinner } from './components'
7 import { state, useSnapState } from './state'
8 import { useDebounce } from 'use-debounce'
9 -import { alertDialog, closeDialog, newDialog, promptDialog } from './dialog'
9 +import { alertDialog, closeDialog, confirmDialog, newDialog, promptDialog } from './dialog'
10 import { apiCall } from './api'
11
12 export function Head() {
@@ -168,20 +168,33 @@ function FolderStats() {
168 }
169
170 function Breadcrumbs() {
171 - const path = useLocation().pathname.slice(1,-1)
171 + const currentPath = useLocation().pathname.slice(1,-1)
172 let prev = ''
173 - const breadcrumbs = path ? path.split('/').map(x => [prev = prev + x + '/', decodeURIComponent(x)]) : []
173 + const breadcrumbs = currentPath ? currentPath.split('/').map(x => [prev = prev + x + '/', decodeURIComponent(x)]) : []
174 return h(Fragment, {},
175 h(Breadcrumb),
176 - breadcrumbs.map(([path,label]) => h(Breadcrumb, { key: path, path, label }))
176 + breadcrumbs.map(([path,label]) =>
177 + h(Breadcrumb, {
178 + key: path,
179 + path,
180 + label,
181 + current: path === currentPath+'/',
182 + }) )
183 )
184 }
185
180 -function Breadcrumb({ path, label }:{ path?: string, label?: string }) {
186 +function Breadcrumb({ path, label, current }:{ current: boolean, path?: string, label?: string }) {
187 const PAD = '\u00A0' // make small elements easier to tap. Don't use min-width 'cause it requires display-inline that breaks word-wrapping
188 if (label && label.length < 3)
189 label = PAD+label+PAD
184 - return h(Link, { className:'breadcrumb', to:path||'/' },
185 - label || hIcon('home') )
190 + const { reload } = useContext(ListContext)
191 + return h(Link, {
192 + className: 'breadcrumb',
193 + to: path || '/',
194 + async onClick() {
195 + if (current && await confirmDialog('Reload?'))
196 + reload?.()
197 + }
198 + }, label || hIcon('home') )
199 }
200
frontend/src/dialog.css
+4
@@ -42,6 +42,10 @@
42 line-height: 1.7em;
43 background-color: var(--color);
44 }
45 +.dialog-confirm button {
46 + margin-top: 1em;
47 +}
48 +.dialog-confirm,
49 .dialog-prompt {
50 --color: #228
51 }
frontend/src/dialog.ts
+21
@@ -121,3 +121,24 @@ export async function alertDialog(msg: string | Error, type:AlertType='info') {
121 }))
122 }
123
124 +export async function confirmDialog(msg: string) : Promise<boolean> {
125 + return new Promise(resolve => newDialog({
126 + className: 'dialog-confirm',
127 + icon: '?',
128 + onClose: resolve,
129 + content: Content
130 + }) )
131 +
132 + function Content() {
133 + return h('div', {},
134 + h('div', {}, msg),
135 + h('button', {
136 + autoFocus: true,
137 + onClick(){
138 + closeDialog(true)
139 + }
140 + }, 'Confirm')
141 + )
142 + }
143 +}
144 +
frontend/src/misc.ts
+6 -1
@@ -1,4 +1,4 @@
1 -import { createElement as h } from 'react'
1 +import { createElement as h, useCallback, useState } from 'react'
2 import { Spinner } from './components'
3 import { newDialog } from './dialog'
4 import { Icon } from './icons'
@@ -73,3 +73,8 @@ export function working() {
73 }
74 })
75 }
76 +
77 +export function useForceUpdate(): [()=>void, number] {
78 + const [n, setN] = useState(0)
79 + return [ useCallback(()=> setN(n => n+1), [setN]), n ]
80 +}
frontend/src/useFetchList.ts
+11 -2
@@ -2,6 +2,7 @@ import { state, useSnapState } from './state'
2 import { useEffect, useRef, useState } from 'react'
3 import { apiEvents } from './api'
4 import { DirList, usePath } from './BrowseFiles'
5 +import { useForceUpdate } from './misc'
6
7 export default function useFetchList() {
8 const snap = useSnapState()
@@ -11,6 +12,7 @@ export default function useFetchList() {
12 const [loading, setLoading] = useState(false)
13 const [error, setError] = useState<Error>()
14 const lastPath = useRef('')
15 + const [reload, forcer] = useForceUpdate()
16 useEffect(()=>{
17 const loc = window.location
18 if (!desiredPath.endsWith('/')) { // useful only in dev, while accessing the frontend directly without passing by the main server
@@ -66,7 +68,14 @@ export default function useFetchList() {
68 }
69
70 })()
69 - }, [desiredPath, search, snap.username])
70 - return { list, loading, error }
71 + }, [desiredPath, search, snap.username, forcer])
72 + return {
73 + list, loading, error,
74 + reload() {
75 + state.remoteSearch = ''
76 + state.stopSearch?.()
77 + reload()
78 + }
79 + }
80 }
81