admin/fs: new "order" field
Massimo Melina committed
Jan 23, 2025 at 20:59 UTC
b989b898ca5ace8793cb8df7f8490c8c7bb20c64
7 files changed
+16
-7
admin/src/FileForm.ts
+6
-3
@@ -3,7 +3,8 @@
3
import { state, useSnapState } from './state'
4
import { createElement as h, forwardRef, ReactElement, ReactNode, useEffect, useMemo, useState } from 'react'
5
import { Alert, Box, Collapse, FormHelperText, Link, MenuItem, MenuList, useTheme } from '@mui/material'
6
-import { BoolField, DisplayField, Field, FieldProps, Form, MultiSelectField, SelectField, StringField
6
+import {
7
+ BoolField, DisplayField, Field, FieldProps, Form, MultiSelectField, NumberField, SelectField, StringField
8
} from '@hfs/mui-grid-form'
9
import { apiCall, UseApi } from './api'
10
import {
@@ -158,15 +159,17 @@ export default function FileForm({ file, addToBar, statusApi, accounts, saved }:
159
: isDir ? "Content from this path on disk will be listed, but you can also add more" : undefined,
160
},
161
{ k: 'id', comp: LinkField, statusApi, xs: 12 },
162
+ { k: 'order', comp: NumberField, min: -1E5, max: 1E5, placeholder: 'default', sm: 4, helperText: wikiLink('Virtual-file-system#order', "To force position") },
163
{
164
k: 'iconType',
165
comp: SelectField,
166
options: ['default', 'file', 'embedded'],
167
value: !values.icon ? 'default' : embeddedIcon ? 'embedded' : 'file',
168
fromField: v => setValues({ ...values, icon: v === 'default' ? '' : v === 'file' ? 'select.a.file' : Object.keys(SYS_ICONS)[0] }),
167
- xs: defaultIcon ? 12 : true,
169
+ xs: true,
170
+ sm: defaultIcon ? 8 : true,
171
},
169
- !defaultIcon && { k: 'icon', xs: 7, md: 8, xl: 10,
172
+ !defaultIcon && { k: 'icon', xs: 8, sm: 4,
173
...embeddedIcon ? {
174
comp: SelectField, // uniqBy to avoid same icon (with different names), but it works only on array, so first step is to convert the object
175
options: _.map(_.uniqBy(_.map(SYS_ICONS, (v,k) => [k, v[0], v[1] ?? k] as const), x => x[2]), ([k, emoji]) =>
config.md
+2
@@ -140,6 +140,8 @@ Valid keys in a node are:
140
Value is a dictionary, where the key is the original name. No UI.
141
- `mime`: specify what mime to use for this resource. Use "auto" for automatic detection.
142
- `url`: when this value is present, the element is a link to the URL you specify.
143
+- `order`: a number that you can set if you want to force the position of this element to the top or the bottom of the list.
144
+ A positive number for the top, negative for the bottom. If you set "1" for an entry and "2" for another, the "2" will be the topmost.
145
- `target`: optional, for links only, used to [open the link in a new browser](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#target). E.g. `_blank`
146
- `accept`: valid only on upload folders, used to restrict the type of files you can upload. E.g. `.zip,.rar`
147
- `default`: to be used with a folder where you want to serve a default html. E.g.: "index.html". Using this will make `mime` default to "auto".
frontend/src/state.ts
+1
@@ -102,6 +102,7 @@ export class DirEntry implements ServerDirEntry {
102
public readonly web?: true
103
public readonly url?: string
104
public readonly target?: string
105
+ public readonly order?: number
106
public comment?: string
107
// we memoize these value for speed
108
public readonly name: string
frontend/src/useFetchList.ts
+3
-2
@@ -179,7 +179,8 @@ function sort(list: DirList) {
179
const byCreation = sort_by === 'creation'
180
const invert = state.invert_order ? -1 : 1
181
return list.sort((a,b) =>
182
- hfsEvent('sortCompare', { a, b }).find(Boolean)
182
+ -compare(a.order||0, b.order||0)
183
+ || hfsEvent('sortCompare', { a, b }).find(Boolean)
184
|| folders_first && -compare(a.isFolder, b.isFolder)
185
|| invert * (bySize ? compare(a.s||0, b.s||0)
186
: byExt ? localCompare(a.ext, b.ext)
@@ -206,7 +207,7 @@ function sort(list: DirList) {
207
208
// generic comparison
209
function compare(a:any, b:any) {
209
- return a < b ? -1 : a > b ? 1 : 0
210
+ return a - b
211
}
212
213
// update list on sorting criteria
src/api.get_file_list.ts
+2
-1
@@ -17,7 +17,7 @@ import { ctxAdminAccess } from './adminApis'
17
import { dontOverwriteUploading } from './upload'
18
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, icon?: string | true }
20
+export interface DirEntry { n:string, s?:number, m?:Date, c?:Date, p?: string, comment?: string, web?: boolean, url?: string, target?: string, icon?: string | true, order?: number }
21
22
export function paramsToFilter({ search, wild, searchComment }: any) {
23
search = String(search || '').toLocaleLowerCase()
@@ -133,6 +133,7 @@ export const get_file_list: ApiHandler = async ({ uri='/', offset, limit, c, onl
133
m: !st || Math.abs(st.mtimeMs - st.birthtimeMs) < 1000 ? undefined : st.mtime,
134
s: isFolder ? undefined : st?.size,
135
p: (pr + pl + pd + pa) || undefined,
136
+ order: node.order,
137
comment: node.comment ?? await getCommentFor(source),
138
icon: getNodeIcon(node),
139
web: await hasDefaultFile(node, ctx) ? true : undefined,
src/api.vfs.ts
+1
-1
@@ -25,7 +25,7 @@ async function urlToNodeOriginal(uri: string) {
25
return n?.isTemp ? n.original : n
26
}
27
28
-const ALLOWED_KEYS = ['name','source','masks','default','accept','rename','mime','url','target','comment','icon', ...PERM_KEYS]
28
+const ALLOWED_KEYS = ['name','source','masks','default','accept','rename','mime','url','target','comment','icon','order', ...PERM_KEYS]
29
30
export interface LsEntry { n:string, s?:number, m?:string, c?:string, k?:'d' }
31
src/vfs.ts
+1
@@ -36,6 +36,7 @@ export interface VfsNodeStored extends VfsPerms {
36
accept?: string
37
comment?: string
38
icon?: string
39
+ order?: number
40
}
41
export interface VfsNode extends VfsNodeStored { // include fields that are only filled at run-time
42
isTemp?: true // this node doesn't belong to the tree and was created by necessity