Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117756023
D5712.1775204802.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
10 KB
Referenced Files
None
Subscribers
None
D5712.1775204802.diff
View Options
diff --git a/src/app/Utils.php b/src/app/Utils.php
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -450,6 +450,7 @@
'app.companion_download_link',
'app.shared_folder_types',
'app.with_signup',
+ 'app.with_user_search',
'mail.from.address',
];
diff --git a/src/package-lock.json b/src/package-lock.json
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -4,7 +4,6 @@
"requires": true,
"packages": {
"": {
- "name": "src",
"devDependencies": {
"@babel/eslint-parser": "^7.17.0",
"@fortawesome/fontawesome-svg-core": "^1.2.35",
@@ -3900,9 +3899,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001722",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001722.tgz",
- "integrity": "sha512-DCQHBBZtiK6JVkAGw7drvAMK0Q0POD/xZvEmDp6baiMMP6QXXk9HpD6mNYBZWhOPG6LvIDb82ITqtWjhDckHCA==",
+ "version": "1.0.30001753",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz",
+ "integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==",
"dev": true,
"funding": [
{
diff --git a/src/resources/js/utils.js b/src/resources/js/utils.js
--- a/src/resources/js/utils.js
+++ b/src/resources/js/utils.js
@@ -179,11 +179,77 @@
return true
}
+/**
+ * Initializes a user email autocompletion on a form input element.
+ *
+ * @param DOMElement $input Input element
+ * @param array $params Search parameters (alias, limit)
+ */
+const userAutocomplete = (input, params = {}) => {
+ if (!window.config['app.with_user_search']) {
+ return
+ }
+
+ const listId = input.id + '-datalist'
+
+ // Note: <datalist> element is a simple solution, it does not allow
+ // styling nor structured content though. It also displays differently
+ // in different browsers.
+
+ let controller = null
+ let timeout = null
+ let datalist = $('<datalist>').attr('id', listId)
+
+ const search_request = search => {
+ controller = new AbortController()
+
+ params = Object.assign({ alias: 0, limit: 10 }, params, { search })
+
+ axios.get('api/v4/search/user', { params, signal: controller.signal })
+ .then(response => {
+ datalist.empty()
+ response.data.list.forEach(user => {
+ let label = user.name ? `${user.name} <${user.email}>` : user.email
+ datalist.append($('<option>').val(user.email).text(label))
+ })
+ })
+ .catch(() => {
+ // ignore error
+ })
+ .finally(() => {
+ controller = null
+ })
+ }
+
+ $(input).attr({ autocomplete: 'off', list: listId })
+ .after(datalist)
+ .on('input', event => {
+ const search = input.value
+
+ window.clearTimeout(timeout)
+
+ if (controller) {
+ controller.abort()
+ }
+
+ // Reset the list when user selected an autocomplete entry or the input is not long enough
+ // Note: inputType is undefined in Chrome, and 'insertReplacementText' in Firefox
+ if (!search.length || !event.inputType || event.inputType == 'insertReplacementText') {
+ datalist.empty()
+ return
+ }
+
+ // Execute the search (with a delay, to limit number of requests when the user is typing fast)
+ timeout = window.setTimeout(() => search_request(search), 250)
+ })
+}
+
export {
clearFormValidation,
downloadFile,
paymentCheckout,
pick,
startLoading,
- stopLoading
+ stopLoading,
+ userAutocomplete
}
diff --git a/src/resources/vue/Distlist/Info.vue b/src/resources/vue/Distlist/Info.vue
--- a/src/resources/vue/Distlist/Info.vue
+++ b/src/resources/vue/Distlist/Info.vue
@@ -70,6 +70,7 @@
import ListInput from '../Widgets/ListInput'
import StatusComponent from '../Widgets/Status'
import SubscriptionSelect from '../Widgets/SubscriptionSelect'
+ import { userAutocomplete } from '../../js/utils'
export default {
components: {
@@ -92,12 +93,15 @@
.then(response => {
this.list = response.data
this.status = response.data.statusInfo
+
+ this.$nextTick().then(() => { userAutocomplete($('#sender-policy-input').get(0)) })
})
.catch(this.$root.errorHandler)
}
},
mounted() {
$('#name').focus()
+ userAutocomplete($('#members-input').get(0))
},
methods: {
deleteList() {
diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -309,6 +309,7 @@
import PasswordInput from '../Widgets/PasswordInput'
import StatusComponent from '../Widgets/Status'
import SubscriptionSelect from '../Widgets/SubscriptionSelect'
+ import { userAutocomplete } from '../../js/utils'
import { library } from '@fortawesome/fontawesome-svg-core'
@@ -468,6 +469,8 @@
}
})
+ userAutocomplete($('#delegation-email').get(0))
+
this.$refs.roleSelectDialog.events({
show: (event) => {
$('select', this.$refs.roleSelectDialog.$el).val(this.user.isController ? 'controller' : 'user')
diff --git a/src/resources/vue/Widgets/AclInput.vue b/src/resources/vue/Widgets/AclInput.vue
--- a/src/resources/vue/Widgets/AclInput.vue
+++ b/src/resources/vue/Widgets/AclInput.vue
@@ -26,6 +26,8 @@
</template>
<script>
+ import { userAutocomplete } from '../../js/utils'
+
const DEFAULT_TYPES = [ 'read-only', 'read-write', 'full' ]
export default {
@@ -54,6 +56,8 @@
this.updateList()
this.addItem(false)
})
+
+ userAutocomplete(this.input)
},
methods: {
aclIdent(item) {
diff --git a/src/tests/Browser/Components/UserAutocomplete.php b/src/tests/Browser/Components/UserAutocomplete.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Components/UserAutocomplete.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Tests\Browser\Components;
+
+use Laravel\Dusk\Component as BaseComponent;
+use Tests\Browser;
+
+class UserAutocomplete extends BaseComponent
+{
+ protected $selector;
+
+ public function __construct($selector)
+ {
+ $this->selector = $selector;
+ }
+
+ /**
+ * Get the root selector for the component.
+ *
+ * @return string
+ */
+ public function selector()
+ {
+ return $this->selector;
+ }
+
+ /**
+ * Assert that the browser page contains the component.
+ *
+ * @param Browser $browser
+ */
+ public function assert($browser)
+ {
+ $browser->assertAttributeRegExp($this->selector, 'autocomplete', '/^off$/')
+ ->assertAttributeRegExp($this->selector, 'list', '/.*-datalist$/');
+ }
+
+ /**
+ * Assert list
+ *
+ * @param Browser $browser
+ * @param array<string, string> $users List of user labels indexed by email address
+ */
+ public function assertAutocompleteList($browser, $users)
+ {
+ if (empty($users)) {
+ $browser->waitUntilMissing("{$this->selector}-datalist > option");
+ } else {
+ $browser->waitFor("{$this->selector}-datalist > option")
+ ->assertElementsCount("{$this->selector}-datalist > option", count($users));
+
+ $idx = 0;
+ foreach ($users as $email => $user) {
+ $idx++;
+ $selector = "{$this->selector}-datalist > option:nth-child({$idx})";
+ $browser->assertSeeIn($selector, $user)
+ ->assertAttributeRegExp($selector, 'value', '/^' . preg_quote($email, '/') . '$/');
+ }
+ }
+ }
+
+ /**
+ * Enter text into the input
+ *
+ * @param Browser $browser
+ * @param string $text
+ */
+ public function autocomplete($browser, $text)
+ {
+ $browser->keys($this->selector, str_split($text));
+ }
+
+ /**
+ * Select a user
+ *
+ * @param Browser $browser
+ * @param string $email Email address to select
+ */
+ public function selectAutocompleteUser($browser, $email)
+ {
+ $browser->click("{$this->selector}-datalist > option[value=\"{$email}\"]")
+ ->waitUntilMissing("{$this->selector}-datalist > option")
+ ->assertValue($this->selector, $email);
+ }
+}
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -16,6 +16,7 @@
use Tests\Browser\Components\ListInput;
use Tests\Browser\Components\QuotaInput;
use Tests\Browser\Components\Toast;
+use Tests\Browser\Components\UserAutocomplete;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\Home;
use Tests\Browser\Pages\UserInfo;
@@ -1005,6 +1006,19 @@
->assertSelectHasOptions('#delegation-contact', ['', 'read-only', 'read-write'])
->assertVisible('.row.form-text')
->type('#delegation-email', 'john@kolab.org')
+ /*
+ FIXME: For some reason assertAutocompleteList() below does not work
+ ->with(new UserAutocomplete('#delegation-email'), static function ($browser) {
+ $list = [
+ 'joe@kolab.org' => 'joe@kolab.org',
+ 'jack@kolab.org' => 'Jack Daniels <jack@kolab.org>',
+ 'john@kolab.org' => 'John Doe <john@kolab.org>',
+ ];
+ $browser->autocomplete('j')
+ ->assertAutocompleteList($list)
+ ->selectAutocompleteUser('john@kolab.org');
+ })
+ */
->select('#delegation-mail', 'read-only')
->select('#delegation-contact', 'read-write')
->assertSeeIn('@button-cancel', 'Cancel')
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 3, 8:26 AM (18 h, 34 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18802017
Default Alt Text
D5712.1775204802.diff (10 KB)
Attached To
Mode
D5712: User email autocompletion
Attached
Detach File
Event Timeline