Skip to content

Commit 00739ef

Browse files
author
antoine
committed
wip POC autocomplete endpoint / closure
1 parent 4a086f8 commit 00739ef

29 files changed

+363
-176
lines changed

demo/app/Sharp/Posts/PostForm.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Models\Category;
66
use App\Models\Post;
7+
use App\Models\User;
78
use App\Sharp\Utils\Embeds\AuthorEmbed;
89
use App\Sharp\Utils\Embeds\CodeEmbed;
910
use App\Sharp\Utils\Embeds\RelatedPostEmbed;
@@ -138,9 +139,19 @@ public function buildFormFields(FieldsContainer $formFields): void
138139
->setReadOnly(! auth()->user()->isAdmin())
139140
->setLabel('Author')
140141
->setRemoteEndpoint('/api/admin/users')
141-
// ->setTemplate('<div>{{$name}}</div><div><small>{{$email}}</small></div>')
142-
->setListItemInlineTemplate('<div>{{name}}</div><div><small>{{email}}</small></div>')
143-
->setResultItemInlineTemplate('<div>{{name}}</div><div><small>{{email}}</small></div>')
142+
->queryResultsUsing(function ($search) {
143+
$users = User::orderBy('name');
144+
145+
foreach (explode(' ', trim($search)) as $word) {
146+
$users->where(function ($query) use ($word) {
147+
$query->orWhere('name', 'like', "%$word%")
148+
->orWhere('email', 'like', "%$word%");
149+
});
150+
}
151+
152+
return $users->limit(10)->get();
153+
})
154+
->setListItemTemplate('<div>{{ $name }}</div><div><small>{{ $email }}</small></div>')
144155
->setHelpMessage('This field is only editable by admins.'),
145156
));
146157
}

demo/app/Sharp/TestForm/TestForm.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ public function buildFormFields(FieldsContainer $formFields): void
4040
->setLocalized()
4141
->setLabel('Autocomplete local')
4242
->setLocalSearchKeys(['label'])
43-
->setListItemInlineTemplate('{{label}}')
44-
->setResultItemInlineTemplate('{{label}} ({{id}})')
43+
->setListItemTemplate('{{ $label }}')
44+
->setResultItemTemplate('{{ $label }} ({{ $id }})')
4545
->setLocalValues($this->options(true)),
4646
)
4747
->addField(
@@ -61,8 +61,8 @@ public function buildFormFields(FieldsContainer $formFields): void
6161
SharpFormAutocompleteRemoteField::make('item')
6262
->setLabel('Passenger')
6363
->setPlaceholder('test')
64-
->setListItemInlineTemplate('{{ name }}')
65-
->setResultItemInlineTemplate('{{name}} ({{num}})')
64+
->setListItemTemplate('{{ $name }}')
65+
->setResultItemTemplate('{{ $name }} ({{ $num }})')
6666
->setRemoteEndpoint(url('/passengers')),
6767
),
6868
)

demo/app/Sharp/Utils/Embeds/AuthorEmbed.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ public function buildFormFields(FieldsContainer $formFields): void
3636
SharpFormAutocompleteRemoteField::make('author')
3737
->setLabel('Author')
3838
->setRemoteEndpoint('/api/admin/users')
39-
->setListItemInlineTemplate('<div>{{name}}</div><div><small>{{email}}</small></div>')
40-
->setResultItemInlineTemplate('<div>{{name}}</div><div><small>{{email}}</small></div>'),
39+
->setListItemTemplate('<div>{{name}}</div><div><small>{{email}}</small></div>')
40+
// ->setListItemInlineTemplate('<div>{{name}}</div><div><small>{{email}}</small></div>')
41+
// ->setResultItemInlineTemplate('<div>{{name}}</div><div><small>{{email}}</small></div>'),
4142
)
4243
->addField(
4344
SharpFormUploadField::make('picture')

demo/routes/api.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use Illuminate\Http\Request;
66
use Illuminate\Support\Facades\Route;
77

8-
Route::middleware('auth:sanctum')->get('/admin/users', function (Request $request) {
8+
Route::get('/admin/users', function (Request $request) {
99
$users = User::orderBy('name');
1010

1111
foreach (explode(' ', trim($request->query('query'))) as $word) {

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@types/google.maps": "^3.54.3",
2424
"@types/jest": "^26.0.19",
2525
"@types/leaflet": "^1.9.6",
26+
"@types/lodash": "^4.17.13",
2627
"@types/nprogress": "^0.2.3",
2728
"@types/qs": "^6.9.7",
2829
"@types/sortablejs": "^1.15.8",

resources/js/Layouts/Layout.vue

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import GlobalFilters from "@/filters/components/GlobalFilters.vue";
4141
import SharpLogoMini from '../../svg/logo-mini.svg';
4242
import ColorModeDropdownItems from "@/components/ColorModeDropdownItems.vue";
4343
import Icon from "@/components/ui/Icon.vue";
44+
import Iframe from "@/form/components/fields/editor/extensions/iframe/Iframe";
45+
import { Dialog, DialogContent } from "@/components/ui/dialog";
4446
4547
const dialogs = useDialogs();
4648
const menu = useMenu();
@@ -329,33 +331,45 @@ provide('menuBoundary', menuBoundary);
329331
<Notifications />
330332

331333
<template v-for="dialog in dialogs" :key="dialog.id">
332-
<AlertDialog
333-
v-model:open="dialog.open"
334-
@update:open="(open) => !open && window.setTimeout(() => dialog.onHidden(), 200)"
335-
>
336-
<AlertDialogContent :highlight-element="dialog.highlightElement">
337-
<AlertDialogHeader>
338-
<template v-if="dialog.title">
339-
<AlertDialogTitle>
340-
{{ dialog.title }}
341-
</AlertDialogTitle>
342-
</template>
343-
<AlertDialogDescription>
344-
{{ dialog.text }}
345-
</AlertDialogDescription>
346-
</AlertDialogHeader>
347-
<AlertDialogFooter>
348-
<template v-if="!dialog.okOnly">
349-
<AlertDialogCancel>
350-
{{ __('sharp::modals.cancel_button') }}
351-
</AlertDialogCancel>
352-
</template>
353-
<AlertDialogAction :class="buttonVariants({ variant: dialog.okVariant })" @click="dialog.onOk()">
354-
{{ dialog.okTitle ?? __('sharp::modals.ok_button') }}
355-
</AlertDialogAction>
356-
</AlertDialogFooter>
357-
</AlertDialogContent>
358-
</AlertDialog>
334+
<template v-if="dialog.isHtmlContent">
335+
<Dialog
336+
v-model:open="dialog.open"
337+
@update:open="(open) => !open && window.setTimeout(() => dialog.onHidden(), 200)"
338+
>
339+
<DialogContent class="max-w-5xl h-[90dvh]">
340+
<iframe class="size-full" :srcdoc="`<style>body,pre{margin:0}</style>${dialog.text}`"></iframe>
341+
</DialogContent>
342+
</Dialog>
343+
</template>
344+
<template v-else>
345+
<AlertDialog
346+
v-model:open="dialog.open"
347+
@update:open="(open) => !open && window.setTimeout(() => dialog.onHidden(), 200)"
348+
>
349+
<AlertDialogContent :highlight-element="dialog.highlightElement">
350+
<AlertDialogHeader>
351+
<template v-if="dialog.title">
352+
<AlertDialogTitle>
353+
{{ dialog.title }}
354+
</AlertDialogTitle>
355+
</template>
356+
<AlertDialogDescription class="break-all">
357+
{{ dialog.text }}
358+
</AlertDialogDescription>
359+
</AlertDialogHeader>
360+
<AlertDialogFooter>
361+
<template v-if="!dialog.okOnly">
362+
<AlertDialogCancel>
363+
{{ __('sharp::modals.cancel_button') }}
364+
</AlertDialogCancel>
365+
</template>
366+
<AlertDialogAction :class="buttonVariants({ variant: dialog.okVariant })" @click="dialog.onOk()">
367+
{{ dialog.okTitle ?? __('sharp::modals.ok_button') }}
368+
</AlertDialogAction>
369+
</AlertDialogFooter>
370+
</AlertDialogContent>
371+
</AlertDialog>
372+
</template>
359373
</template>
360374
</ConfigProvider>
361375
</template>

resources/js/api/errors.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@ export function getErrorMessage({ data, status }) {
88
}
99

1010
export async function handleErrorAlert({ data, method, status }) {
11-
const text = getErrorMessage({ data, status });
12-
const title = __(`sharp::modals.api.${status}.title`) === `sharp::modals.api.${status}.title`
13-
? __(`sharp::modals.error.title`)
14-
: __(`sharp::modals.api.${status}.title`);
15-
1611
if(status === 404 && method === 'get' || status === 422) {
1712
return;
1813
}
@@ -21,6 +16,15 @@ export async function handleErrorAlert({ data, method, status }) {
2116
if(status === 401 || status === 419) {
2217
location.reload();
2318
} else {
24-
showAlert(text, { title, isError: true });
19+
const text = getErrorMessage({ data, status });
20+
const title = __(`sharp::modals.api.${status}.title`) === `sharp::modals.api.${status}.title`
21+
? __(`sharp::modals.error.title`)
22+
: __(`sharp::modals.api.${status}.title`);
23+
24+
if(typeof data === 'string') {
25+
showAlert(data, { title, isError: true, isHtmlContent: true });
26+
} else {
27+
showAlert(text, { title, isError: true });
28+
}
2529
}
2630
}

resources/js/components/ui/alert-dialog/AlertDialogHeader.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const props = defineProps<{
99

1010
<template>
1111
<div
12-
:class="cn('flex flex-col gap-y-2 text-center sm:text-left', props.class)"
12+
:class="cn('flex flex-col gap-y-2 text-center min-w-0 sm:text-left', props.class)"
1313
>
1414
<slot />
1515
</div>

resources/js/form/components/fields/Autocomplete.vue

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@
44
import {
55
FormAutocompleteLocalFieldData,
66
FormAutocompleteRemoteFieldData,
7-
FormTextFieldData,
8-
SelectFilterData
97
} from "@/types";
108
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
119
import { ref } from "vue";
1210
import { Button } from "@/components/ui/button";
1311
import { __ } from "@/utils/i18n";
14-
import { Check, ChevronsUpDown } from 'lucide-vue-next';
12+
import { ChevronsUpDown, X } from 'lucide-vue-next';
1513
import { cn } from "@/utils/cn";
1614
import {
1715
Command,
@@ -21,12 +19,11 @@
2119
CommandList,
2220
CommandSeparator
2321
} from "@/components/ui/command";
24-
import { Checkbox } from "@/components/ui/checkbox";
25-
import { useDebounceFn } from "@vueuse/core";
2622
import { route } from "@/utils/url";
2723
import { api } from "@/api/api";
2824
import { useParentForm } from "@/form/useParentForm";
2925
import debounce from "lodash/debounce";
26+
import { fuzzySearch } from "@/utils/search";
3027
3128
const props = defineProps<FormFieldProps<FormAutocompleteLocalFieldData | FormAutocompleteRemoteFieldData>>();
3229
const emit = defineEmits<FormFieldEmits<FormAutocompleteLocalFieldData | FormAutocompleteRemoteFieldData>>();
@@ -36,39 +33,57 @@
3633
const searchTerm = ref('');
3734
const results = ref([]);
3835
39-
const remoteSearch = debounce(async (term: string) => {
36+
const abort = new AbortController();
37+
const remoteSearch = debounce(async (query: string) => {
4038
results.value = await api.get(route('code16.sharp.api.form.autocomplete.index', {
4139
entityKey: form.entityKey,
4240
autocompleteFieldKey: props.field.key,
4341
}), {
4442
params: {
4543
endpoint: props.field.mode === 'remote' && props.field.remoteEndpoint,
46-
search: term,
44+
search: query,
4745
},
46+
signal: abort.signal,
4847
})
4948
.then(response => response.data.data);
5049
}, 300);
5150
52-
const search = useDebounceFn(async (term) => {
51+
function search(query: string) {
5352
if(props.field.mode === 'remote') {
54-
remoteSearch(term);
53+
if(query.length >= props.field.searchMinChars) {
54+
remoteSearch(query);
55+
}
5556
} else {
57+
if(query.length > 0) {
58+
results.value = fuzzySearch(props.field.localValues, query, { searchKeys: props.field.searchKeys });
59+
}
60+
}
61+
}
5662
63+
if(props.field.mode === 'local' && props.value) {
64+
const localValue = props.field.localValues
65+
.find(v => props.value[props.field.itemIdAttribute] == v[props.field.itemIdAttribute]);
66+
console.log(localValue);
67+
if(localValue) {
68+
emit('input', localValue, { force: true });
5769
}
58-
}, 300);
70+
}
5971
</script>
6072

6173
<template>
6274
<FormFieldLayout :field="props.field">
6375
<Popover v-model:open="open">
6476
<template v-if="props.value">
65-
<div class="border border-input rounded-md p-2">
66-
<div v-html="props.value.toString()"></div>
77+
<div class="relative border border-input flex items-center rounded-md min-h-10 text-sm px-3 py-2">
78+
<div class="flex-1" v-html="props.value._htmlResult ?? props.value._html ?? props.value[props.field.itemIdAttribute]"></div>
79+
<Button class="absolute right-0 top-1/2 -translate-y-1/2" variant="ghost" size="icon" @click="$emit('input', null)">
80+
<X class="size-4" />
81+
</Button>
6782
</div>
6883
</template>
6984
<template v-else>
7085
<PopoverTrigger as-child>
71-
<Button variant="outline">
86+
<Button class="w-full justify-between" variant="outline">
7287
{{ props.field.placeholder ?? __('sharp::form.multiselect.placeholder') }}
7388
<ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
7489
</Button>

0 commit comments

Comments
 (0)