search: wildcards as an option #658
Massimo Melina committed
Jul 2, 2024 at 20:40 UTC
95a16d1df9c140027a7852154c1f5a2573f1922e
6 files changed
+63
-12
frontend/src/dialog.ts
+18
@@ -92,6 +92,24 @@ export async function promptDialog(msg: string, { value, type, helperText, trim=
92
}
93
}
94
95
+export async function formDialog({ ...rest }: DialogOptions): Promise<any> {
96
+ return new Promise<any>(resolve => {
97
+ const { close } = newDialog({
98
+ className: 'dialog-form',
99
+ ...rest,
100
+ onClose: resolve,
101
+ Content() {
102
+ return h('form', {
103
+ onSubmit(ev: any) {
104
+ ev.preventDefault()
105
+ close(Object.fromEntries(new FormData(ev.target).entries()))
106
+ },
107
+ }, h(rest.Content))
108
+ }
109
+ })
110
+ })
111
+}
112
+
113
export type AlertType = 'error' | 'warning' | 'info'
114
115
export function alertDialog(msg: ReactElement | string | Error, type:AlertType='info') {
frontend/src/index.scss
+2
@@ -497,6 +497,8 @@ button .icon + .label {
497
}
498
}
499
500
+form label+input { margin-top: .2em; }
501
+
502
.miss-perm { margin: 0 0.3em }
503
504
.popup-menu-button {
frontend/src/menu.ts
+35
-9
@@ -2,9 +2,11 @@
2
3
import { state, useSnapState } from './state'
4
import { createElement as h, Fragment, useEffect, useMemo, useState } from 'react'
5
-import { alertDialog, confirmDialog, ConfirmOptions, promptDialog, toast } from './dialog'
6
-import { defaultPerms, err2msg, ErrorMsg, onlyTruthy, prefix, throw_, useStateMounted, VfsPerms, working,
7
- buildUrlQueryString} from './misc'
5
+import { alertDialog, confirmDialog, ConfirmOptions, formDialog, toast } from './dialog'
6
+import {
7
+ defaultPerms, err2msg, ErrorMsg, onlyTruthy, prefix, useStateMounted, VfsPerms, working,
8
+ buildUrlQueryString, hIcon, WIKI_URL
9
+} from './misc'
10
import { loginDialog } from './login'
11
import { showOptions } from './options'
12
import showUserPanel from './UserPanel'
@@ -16,10 +18,10 @@ import { apiCall } from '@hfs/shared/api'
18
import { reloadList } from './useFetchList'
19
import { t, useI18N } from './i18n'
20
import { cut } from './clip'
19
-import { Btn, BtnProps, CustomCode } from './components'
21
+import { Btn, BtnProps, CustomCode, iconBtn } from './components'
22
23
export function MenuPanel() {
22
- const { showFilter, remoteSearch, stopSearch, searchManuallyInterrupted, selected, props } = useSnapState()
24
+ const { showFilter, remoteSearch, stopSearch, searchManuallyInterrupted, selected, props, searchOptions } = useSnapState()
25
const { can_upload, can_delete, can_archive } = props ? { ...defaultPerms, ...props } : {} as VfsPerms
26
const { uploading, qs } = useSnapshot(uploadState)
27
useEffect(() => {
@@ -143,11 +145,35 @@ export function MenuPanel() {
145
icon: 'search',
146
label: t`Search`,
147
onClickAnimation: false,
146
- async onClick() {
147
- state.remoteSearch = await promptDialog(t('search_msg', "Search this folder and sub-folders"),
148
- { title: t`Search`, onSubmit: x => x.includes('/') ? throw_(t`Invalid value`) : x }) || ''
148
+ onClick: () => formDialog({
149
+ title: t`Search`,
150
+ Content: () => h('div', {},
151
+ h('label', { htmlFor: 'text' }, t('search_msg', "Search this folder and sub-folders")),
152
+ h('input', {
153
+ name: 'text',
154
+ style: { width: 0, minWidth: '100%', maxWidth: '100%', boxSizing: 'border-box' },
155
+ autoFocus: true,
156
+ }),
157
+ h('div', { style: { margin: '1em 0' } },
158
+ h('input', {
159
+ type: 'checkbox',
160
+ name: 'wild',
161
+ defaultChecked: searchOptions.wild,
162
+ style: { marginRight: '1em' },
163
+ }),
164
+ "Wildcards",
165
+ h('a', { href: `${WIKI_URL}Wildcards`, target: 'doc' }, hIcon('info')),
166
+ ),
167
+ h('div', { style: { textAlign: 'right', marginTop: '.8em' } },
168
+ h('button', {}, t`Continue`)),
169
+ )
170
+ }).then(res => {
171
+ if (!res) return
172
+ const { text='', wild, ...rest } = res
173
+ state.searchOptions = { ...rest, wild: Boolean(wild) }
174
+ state.remoteSearch = text
175
state.stopSearch?.()
150
- }
176
+ })
177
}
178
}
179
}
frontend/src/state.ts
+2
@@ -36,7 +36,9 @@ export const state = proxy<typeof FRONTEND_OPTIONS & {
36
canChangePassword: boolean
37
uri: string
38
uploadOnExisting: 'skip' | 'overwrite' | 'rename'
39
+ searchOptions: any
40
}>({
41
+ searchOptions: { wild: true },
42
uploadOnExisting: getHFS().dontOverwriteUploading ? 'rename' : 'skip',
43
uri: '',
44
canChangePassword: false,
frontend/src/useFetchList.ts
+2
-1
@@ -45,7 +45,8 @@ export default function useFetchList() {
45
return
46
}
47
48
- const params = { uri, search }
48
+ const params = { uri, search, ...snap.searchOptions }
49
+ params.wild = params.wild ? undefined : 'no'
50
if (snap.listReloader === lastReloader.current && _.isEqual(params, lastParams.current)) return
51
lastParams.current = params
52
lastReloader.current = snap.listReloader
src/api.get_file_list.ts
+4
-2
@@ -19,7 +19,7 @@ import { SendListReadable } from './SendList'
19
20
export interface DirEntry { n:string, s?:number, m?:Date, c?:Date, p?: string, comment?: string, web?: boolean, url?: string, target?: string }
21
22
-export const get_file_list: ApiHandler = async ({ uri='/', offset, limit, search, c, onlyFolders, admin }, ctx) => {
22
+export const get_file_list: ApiHandler = async ({ uri='/', offset, limit, search, wild, c, onlyFolders, admin }, ctx) => {
23
const node = await urlToNode(uri, ctx)
24
const list = ctx.get('accept') === 'text/event-stream' ? new SendListReadable() : undefined
25
if (!node)
@@ -34,7 +34,9 @@ export const get_file_list: ApiHandler = async ({ uri='/', offset, limit, search
34
return fail()
35
offset = Number(offset)
36
limit = Number(limit)
37
- const filter = pattern2filter(String(search || ''))
37
+ search = String(search || '').toLocaleLowerCase()
38
+ const filter = wild === 'no' ? (s: string) => s.includes(search)
39
+ : pattern2filter(search)
40
const walker = walkNode(node, { ctx: admin ? undefined : ctx, onlyFolders, depth: search ? Infinity : 0 })
41
const onDirEntryHandlers = mapPlugins(plug => plug.onDirEntry)
42
const can_upload = admin || hasPermission(node, 'can_upload', ctx)