plugins: new frontend events: loginUsernameField, loginPasswordField
Massimo Melina committed
Apr 2, 2025 at 23:59 UTC
8933731f115d6e3cb8d1e3bf79c5cf83b1ddd403
7 files changed
+29
-21
dev-plugins.md
+16
-8
@@ -428,9 +428,11 @@ Some frontend events can return HTML, which can be expressed in several ways:
428
429
So when referring to type `Html`, below, we are actually meaning `Promisable<string | Element | ReactNode | ReactNode[]>`.
430
431
-These events will receive a `def` property (in addition event's specific properties),
432
-with the default content that will be displayed if no callback return a valid output.
433
-You can decide to embed such default content inside your content.
431
+These events will receive, in addition event's specific properties, a `def` property
432
+with the *default* content that will be displayed if no callback return a valid output.
433
+It is useful if you want to embed such default content inside your content.
434
+Most events have this `def` undefined as they have no default content and are designed for custom insertions,
435
+but when this is not the case, you can replace the default content with nothing by returning `null`.
436
You can produce output for such events also by adding sections (with same name as the event) to file `custom.html`.
437
438
This is a list of available frontend-events, with respective object parameter and output.
@@ -456,7 +458,6 @@ This is a list of available frontend-events, with respective object parameter an
458
- you receive each entry of the list, and optionally produce HTML code that will completely replace the entry row/slot.
459
- parameter `{ entry: DirEntry }` (refer above for DirEntry object)
460
- output `Html`
459
- - return null if you want to hide this entry, or undefined to leave it unchanged
461
- `afterEntryName`
462
- you receive each entry of the list, and optionally produce HTML code that will be added after the name of the entry.
463
- parameter `{ entry: DirEntry }` (refer above for DirEntry object)
@@ -476,6 +477,12 @@ This is a list of available frontend-events, with respective object parameter an
477
- no parameter
478
- output `Html`
479
- you can generate inputs with a name, and they will be sent to the login API
480
+- `loginUsernameField`
481
+ - no parameter
482
+ - output `Html`
483
+- `loginPasswordField`
484
+ - no parameter
485
+ - output `Html`
486
- `fileMenu`
487
- add or manipulate entries of the menu. If you return something, that will be added to the menu.
488
You can also delete or replace the content of the `menu` array.
@@ -777,8 +784,9 @@ HFS will scan through them in inverted alphabetical order searching for a compat
784
785
## React developers
786
787
+Using React is a good option to create a frontend parts for your plugin.
788
Most React developers are used to JSX, which is not (currently) supported here.
781
-If you want, you can try solutions to JSX support, like transpiling.
789
+If you want, you can try solutions to support JSX, like transpiling.
790
Anyway, React is not JSX, and can be easily used without.
791
792
Any time in JSX you do
@@ -824,13 +832,13 @@ HFS._.set(HFS.lang, 'en.translate.Options', 'Settings')
832
This works because all translations are stored inside `HFS.lang`.
833
Using `HFS._.set` is not necessary, but in this case is convenient, because the language-code key may not exist.
834
827
-If you want to override a text regardless of the language, use the special language-code `all`.
835
+If you want to override a text regardless of the language, use the special language-code `all`.
836
837
## API version history
838
831
-- 12.1 (v0.57.0)
839
+- 12.2 (v0.57.0)
840
- backend event: finalizingLogin, httpsServerOptions, clearTextLogin
833
- - frontend events: beforeLoginSubmit
841
+ - frontend events: beforeLoginSubmit, loginUsernameField, loginPasswordField
842
- exports.changelog
843
- automatic unload of api.events listeners
844
- removed DirEntry.t
frontend/src/BrowseFiles.ts
+1
-1
@@ -309,7 +309,7 @@ const Entry = ({ entry, midnight, separator }: EntryProps) => {
309
'aria-labelledby': ariaId,
310
value: selected[uri] || false,
311
onChange(v) {
312
- if (hfsEvent('entryToggleSelection', { entry }).isDefaultPrevent()) return
312
+ if (hfsEvent('entryToggleSelection', { entry }).isDefaultPrevented()) return
313
if (v)
314
return state.selected[uri] = true
315
delete state.selected[uri]
frontend/src/clip.ts
+1
-1
@@ -45,7 +45,7 @@ export function ClipBar() {
45
}
46
47
function paste() {
48
- if (hfsEvent('paste', { from: state.clip, to: here }).isDefaultPrevent()) return
48
+ if (hfsEvent('paste', { from: state.clip, to: here }).isDefaultPrevented()) return
49
return apiCall('move_files', {
50
uri_from: clip.map(x => x.uri),
51
uri_to: here,
frontend/src/components.ts
+1
-1
@@ -87,7 +87,7 @@ export function CustomCode({ name, children, render, ...props }: {
87
[name, children, ...props ? Object.values(props) : []])
88
const [out, setOut] = useStateMounted<null | ReactNode[]>([])
89
useEffect(() => {
90
- if (raw.some(x => x === null)) // null means skip this
90
+ if (raw.isDefaultPrevented() || raw.some(x => x === null)) // null means skip this
91
return setOut(null)
92
const worked: ReactNode[] = raw.map(toElement)
93
setOut(onlyTruthy(worked))
frontend/src/login.ts
+8
-8
@@ -11,13 +11,14 @@ import { createElement as h, Fragment, useEffect, useRef } from 'react'
11
import { reloadList } from './useFetchList'
12
import { Checkbox, CustomCode } from './components'
13
import { changePassword } from './UserPanel'
14
+import _ from 'lodash'
15
import i18n from './i18n'
16
const { t, useI18N } = i18n
17
18
async function login(username:string, password:string, extra?: object) {
19
const stopWorking = working()
20
return srpClientSequence(username, password, apiCall, extra).catch(err => {
20
- if (err.code == HTTP_METHOD_NOT_ALLOWED)
21
+ if (err.code == HTTP_METHOD_NOT_ALLOWED || !password) // allow alternative authentications without a password
22
return apiCall('login', { username, password, ...extra })
23
throw err
24
}).then(res => {
@@ -80,7 +81,7 @@ export async function loginDialog(closable=true, reloadAfter=true) {
81
}
82
},
83
h(CustomCode, { name: 'beforeLogin' }),
83
- h('div', { className: 'field' },
84
+ h(CustomCode, { name: 'loginUsernameField' }, h('div', { className: 'field' },
85
h('label', { htmlFor: 'login_username' }, t`Username`),
86
h('input', {
87
ref: usrRef,
@@ -90,8 +91,8 @@ export async function loginDialog(closable=true, reloadAfter=true) {
91
required: true,
92
onKeyDown
93
}),
93
- ),
94
- h('div', { className: 'field' },
94
+ )),
95
+ h(CustomCode, { name: 'loginPasswordField' }, h('div', { className: 'field' },
96
h('label', { htmlFor: 'login_password' }, t`Password`),
97
h('input', {
98
ref: pwdRef,
@@ -102,7 +103,7 @@ export async function loginDialog(closable=true, reloadAfter=true) {
103
required: true,
104
onKeyDown
105
}),
105
- ),
106
+ )),
107
h(CustomCode, { name: 'beforeLoginSubmit' }),
108
h('div', { className: 'submit' },
109
h('button', { type: 'submit' }, t`Continue`)),
@@ -124,10 +125,9 @@ export async function loginDialog(closable=true, reloadAfter=true) {
125
const form = ev.target instanceof HTMLElement && ev.target.closest('form')
126
if (!form) return
127
ev.stopPropagation()
127
- const { username, password, ...rest } = Object.fromEntries(Array.from(form.querySelectorAll('[name]'))
128
- .map(el => el instanceof HTMLInputElement ? [el.name, el.value] : []))
128
+ const { username, password, ...rest } = _.pickBy(Object.fromEntries(new FormData(form).entries()), _.isString) // skip files
129
const u = username.trim()
130
- if (going || !u || !password) return
130
+ if (going || !u) return
131
going = true
132
try {
133
const res = await login(u, password, {
frontend/src/misc.ts
+1
-1
@@ -60,7 +60,7 @@ export function hfsEvent(name: string, params?:Dict) {
60
document.dispatchEvent(ev)
61
const sortedOutput = order.length && _.sortBy(output.map((x, i) => [order[i] || 0, x]), '0').map(x => x[1])
62
return Object.assign(sortedOutput || output, {
63
- isDefaultPrevent: () => ev.defaultPrevented,
63
+ isDefaultPrevented: () => ev.defaultPrevented,
64
})
65
}
66
src/const.ts
+1
-1
@@ -9,7 +9,7 @@ import { formatTimestamp } from './cross'
9
import { argv } from './argv'
10
export * from './cross-const'
11
12
-export const API_VERSION = 12.1
12
+export const API_VERSION = 12.2
13
export const COMPATIBLE_API_VERSION = 1 // while changes in the api are not breaking, this number stays the same, otherwise it is made equal to API_VERSION
14
15
// you can add arguments with this file, currently used for the update process on mac/linux