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)