Skip to content

Commit fb8b5e5

Browse files
authored
🐛 Fix Typeahead in settings (#3595)
1 parent 4587b91 commit fb8b5e5

File tree

6 files changed

+269
-35
lines changed

6 files changed

+269
-35
lines changed

app/Http/Controllers/API/v1/UserController.php

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -434,11 +434,11 @@ public function destroyMute(int $userId): JsonResponse {
434434

435435
/**
436436
* @OA\Get(
437-
* path="/user/search/{?query}",
437+
* path="/user/search/{query}",
438438
* operationId="searchUsers",
439439
* tags={"User"},
440-
* summary="Get paginated statuses for single user",
441-
* description="Returns paginated statuses of a single user specified by the username",
440+
* summary="Get paginated search results for combined search on username and (display)name",
441+
* description="Returns paginated search results for a user based on the given query.",
442442
* @OA\Parameter (
443443
* name="query",
444444
* in="path",
@@ -452,14 +452,58 @@ public function destroyMute(int $userId): JsonResponse {
452452
* in="query",
453453
* @OA\Schema(type="integer")
454454
* ),
455+
* @OA\Response(
456+
* response=200,
457+
* description="successful operation",
458+
* @OA\JsonContent(
459+
* @OA\Property(property="data", type="array",
460+
* @OA\Items(
461+
* ref="#/components/schemas/UserResource"
462+
* )
463+
* ),
464+
* @OA\Property(property="links", ref="#/components/schemas/Links"),
465+
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta"),
466+
* )
467+
* ),
468+
* @OA\Response(response=400, description="Bad request"),
469+
* security={
470+
* {"passport": {"read-search"}}, {"token": {}}
471+
* }
472+
* )
473+
*
474+
*/
475+
public function search(string $query): AnonymousResourceCollection|JsonResponse {
476+
try {
477+
return UserResource::collection(BackendUserBackend::searchUser($query));
478+
} catch (InvalidArgumentException) {
479+
return $this->sendError(['message' => __('messages.exception.general')], 400);
480+
}
481+
}
482+
483+
/**
484+
* @OA\Get(
485+
* path="/user/search",
486+
* operationId="searchUsersByParameters",
487+
* tags={"User"},
488+
* summary="Get paginated search results for users by either username or (display)name",
489+
* description="Returns paginated search results for users based on the given parameters.",
490+
* @OA\Parameter (
491+
* name="page",
492+
* description="Page of pagination",
493+
* required=false,
494+
* in="query",
495+
* @OA\Schema(type="integer")
496+
* ),
455497
* @OA\Parameter (
456498
* name="username",
457499
* in="query",
500+
* required=false,
458501
* description="Search for parts username",
459502
* ),
460503
* @OA\Parameter (
461504
* name="name",
462505
* in="query",
506+
* required=false,
463507
* description="Search for parts of users (display)name",
464508
* ),
465509
* @OA\Response(
@@ -482,17 +526,12 @@ public function destroyMute(int $userId): JsonResponse {
482526
* )
483527
*
484528
*/
485-
public function search(Request $request, ?string $query = null): AnonymousResourceCollection|JsonResponse {
529+
public function searchByParameters(Request $request): AnonymousResourceCollection|JsonResponse {
486530
try {
487531
$validated = $request->validate([
488532
'username' => ['nullable', 'string', 'max:255'],
489533
'name' => ['nullable', 'string', 'max:255'],
490534
]);
491-
if (empty($validated) && isset($query)) {
492-
// if no specific search criteria is given, search for the query in username and display_name
493-
return UserResource::collection(BackendUserBackend::searchUser($query));
494-
}
495-
496535
if (empty($validated)) {
497536
return response()->json(null, 400);
498537
}

resources/types/Api.gen.ts

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2429,25 +2429,58 @@ export class Api<
24292429
}),
24302430

24312431
/**
2432-
* @description Returns paginated statuses of a single user specified by the username
2432+
* @description Returns paginated search results for a user based on the given query.
24332433
*
24342434
* @tags User
24352435
* @name SearchUsers
2436-
* @summary Get paginated statuses for single user
2437-
* @request GET:/user/search/{?query}
2436+
* @summary Get paginated search results for combined search on username and (display)name
2437+
* @request GET:/user/search/{query}
24382438
* @secure
24392439
*/
24402440
searchUsers: (
2441-
queryParams: {
2441+
query?: any,
2442+
queryParams?: {
2443+
/** Page of pagination */
2444+
page?: number;
2445+
},
2446+
params: RequestParams = {},
2447+
) =>
2448+
this.request<
2449+
{
2450+
data?: UserResource[];
2451+
/** pagination links */
2452+
links?: Links;
2453+
/** Pagination meta data */
2454+
meta?: PaginationMeta;
2455+
},
2456+
void
2457+
>({
2458+
path: `/user/search/${query}`,
2459+
method: "GET",
2460+
query: queryParams,
2461+
secure: true,
2462+
format: "json",
2463+
...params,
2464+
}),
2465+
2466+
/**
2467+
* @description Returns paginated search results for users based on the given parameters.
2468+
*
2469+
* @tags User
2470+
* @name SearchUsersByParameters
2471+
* @summary Get paginated search results for users by either username or (display)name
2472+
* @request GET:/user/search
2473+
* @secure
2474+
*/
2475+
searchUsersByParameters: (
2476+
query?: {
24422477
/** Page of pagination */
24432478
page?: number;
24442479
/** Search for parts username */
24452480
username?: any;
24462481
/** Search for parts of users (display)name */
24472482
name?: any;
2448-
query: string;
24492483
},
2450-
query?: any,
24512484
params: RequestParams = {},
24522485
) =>
24532486
this.request<
@@ -2460,9 +2493,9 @@ export class Api<
24602493
},
24612494
void
24622495
>({
2463-
path: `/user/search/`,
2496+
path: `/user/search`,
24642497
method: "GET",
2465-
query: queryParams,
2498+
query: query,
24662499
secure: true,
24672500
format: "json",
24682501
...params,
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<script lang="ts" setup>
2+
import {trans} from "laravel-vue-i18n";
3+
import {ref, watch} from 'vue'
4+
5+
defineProps({
6+
name: {
7+
type: String,
8+
default: ''
9+
},
10+
title: {
11+
type: String,
12+
default: ''
13+
},
14+
errors: {
15+
type: Array,
16+
default: []
17+
},
18+
});
19+
20+
const model = defineModel<string>();
21+
const timezones = ref<{ value: string, label: string }[]>([]);
22+
const filteredTimezones = ref<{ value: string, label: string }[]>([]);
23+
const search = ref('');
24+
25+
timezones.value = Intl.supportedValuesOf('timeZone').map((timezone) => {
26+
return {value: timezone, label: timezone};
27+
});
28+
29+
function filterTimezones() {
30+
if (search.value.length === 0) {
31+
filteredTimezones.value = [];
32+
return;
33+
}
34+
const searchLower = search.value.toLowerCase();
35+
filteredTimezones.value = timezones.value.filter((timezone) => {
36+
return timezone.label.toLowerCase().includes(searchLower);
37+
});
38+
// maximum 10 results
39+
filteredTimezones.value = filteredTimezones.value.slice(0, 10);
40+
}
41+
42+
function selectTimezone(timezone: string) {
43+
if (!timezone) {
44+
return;
45+
}
46+
model.value = timezone;
47+
filterTimezones();
48+
}
49+
50+
watch(model, (newValue) => {
51+
search.value = newValue || '';
52+
filterTimezones();
53+
});
54+
</script>
55+
56+
<template>
57+
<div class="form-group row">
58+
<label :for="name" class="col-md-4 col-form-label text-md-right">
59+
{{ trans('user.timezone') }}
60+
</label>
61+
62+
<div class="col-md-6">
63+
<div class="col btn-group me-1">
64+
<button class="btn btn-sm btn-outline-primary dropdown-toggle"
65+
type="button"
66+
id="timezoneDropdown"
67+
data-bs-dropdown-animation="off"
68+
data-bs-toggle="dropdown"
69+
aria-expanded="false"
70+
>
71+
{{ $props.modelValue }}
72+
</button>
73+
<div
74+
aria-labelledby="timezoneDropdown"
75+
class="dropdown-menu pt-0 mx-0 rounded-3 shadow overflow-hidden"
76+
>
77+
<form class="p-2 mb-2 border-bottom">
78+
<input
79+
v-model="search"
80+
@input="filterTimezones"
81+
type="search"
82+
class="form-control mobile-input-fs-16"
83+
autocomplete="off"
84+
:placeholder="trans('user.timezone')"
85+
@keydown.enter="selectTimezone(filteredTimezones[0]?.value ?? '')"
86+
>
87+
</form>
88+
<ul class="list-unstyled mb-0" v-if="filteredTimezones.length > 0">
89+
<li v-for="timezone in filteredTimezones" :key="timezone?.value">
90+
<a href="#"
91+
class="dropdown-item d-flex align-items-center gap-2 py-2"
92+
@click="selectTimezone(timezone?.value)"
93+
>
94+
<div class="flex-grow-1">
95+
<div class="fw-bold">{{ timezone?.label }}</div>
96+
</div>
97+
</a>
98+
</li>
99+
</ul>
100+
<div v-else class="p-2 mb-0 text-center text-muted">
101+
</div>
102+
</div>
103+
</div>
104+
<span v-for="error in errors" class="invalid-feedback" role="alert">
105+
<strong>{{ error }}</strong>
106+
</span>
107+
</div>
108+
</div>
109+
</template>

resources/vue/components/Settings/ProfileSettings.vue

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import {
1313
import Input from "./Partials/Input.vue";
1414
import Select from "./Partials/Select.vue";
1515
import {SelectOption} from "./Partials/SelectOption";
16-
import DataLilst from "./Partials/DataLilst.vue";
1716
import {Notyf} from "notyf";
1817
import Textfield from "./Partials/Textfield.vue";
1918
import {showApiValidationErrors} from "../../helpers/NotyfHelper";
19+
import TimezoneDropdown from "./Partials/TimezoneDropdown.vue";
2020
2121
const notyf = new Notyf({position: {x: "right", y: "bottom"}});
2222
const api = new Api({baseUrl: window.location.origin + '/api/v1'});
@@ -136,18 +136,8 @@ getDefaultUserData();
136136
autocomplete="email"
137137
required="true"
138138
/>
139-
<Select
140-
:title="trans('user.mapprovider')"
141-
:name="'mapprovider'"
142-
v-model="userData.mapProvider"
143-
:options="providers"
144-
:errors="errors.mapProvider"
145-
/>
146-
<DataLilst
139+
<TimezoneDropdown
147140
v-model="userData.timezone"
148-
:errors="errors.timezone"
149-
:options="timezones"
150-
:title="trans('user.timezone')"
151141
/>
152142

153143
<Select

routes/api.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@
120120
Route::post('/{userId}/mute', [UserController::class, 'createMute']);
121121
Route::delete('/{userId}/mute', [UserController::class, 'destroyMute']);
122122
});
123-
Route::get('search/{query?}', [UserController::class, 'search'])->middleware(['scope:read-search']);
123+
Route::get('search/{query}', [UserController::class, 'search'])->middleware(['scope:read-search']);
124+
Route::get('search', [UserController::class, 'searchByParameters'])->middleware(['scope:read-search']);
124125
Route::get('statuses/active', [StatusController::class, 'getActiveStatus'])
125126
->middleware(['scope:read-statuses']);
126127
});

0 commit comments

Comments
 (0)