mirror of
https://github.com/directus/directus.git
synced 2026-01-29 00:27:59 -05:00
Search input (#473)
* Add search-input component * Add searchquery support to tabular view * Add search bar on collections browse * Debounce input values * Handle searchQuery in collection preset composition * Handle searchquery in use-items composition * Support search query in cards layout * Add search-input to files/users
This commit is contained in:
@@ -82,5 +82,18 @@ export function useCollectionPreset(collection: Ref<string>) {
|
||||
},
|
||||
});
|
||||
|
||||
return { viewType, viewOptions, viewQuery, filters };
|
||||
const searchQuery = computed<Filter[]>({
|
||||
get() {
|
||||
return localPreset.value.search_query || null;
|
||||
},
|
||||
set(val) {
|
||||
localPreset.value = {
|
||||
...localPreset.value,
|
||||
search_query: val,
|
||||
};
|
||||
savePreset(localPreset.value);
|
||||
},
|
||||
});
|
||||
|
||||
return { viewType, viewOptions, viewQuery, filters, searchQuery };
|
||||
}
|
||||
|
||||
@@ -14,13 +14,14 @@ type Options = {
|
||||
sort: Ref<string>;
|
||||
page: Ref<number>;
|
||||
filters: Ref<readonly Filter[]>;
|
||||
searchQuery: Ref<string>;
|
||||
};
|
||||
|
||||
export function useItems(collection: Ref<string>, options: Options) {
|
||||
const projectsStore = useProjectsStore();
|
||||
const { primaryKeyField } = useCollection(collection);
|
||||
|
||||
const { limit, fields, sort, page, filters } = options;
|
||||
const { limit, fields, sort, page, filters, searchQuery } = options;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const items = ref<any>([]);
|
||||
@@ -79,7 +80,7 @@ export function useItems(collection: Ref<string>, options: Options) {
|
||||
}
|
||||
});
|
||||
|
||||
watch([filters, limit], async (after, before) => {
|
||||
watch([filters, limit, searchQuery], async (after, before) => {
|
||||
if (!before || isEqual(after, before)) {
|
||||
return;
|
||||
}
|
||||
@@ -145,6 +146,7 @@ export function useItems(collection: Ref<string>, options: Options) {
|
||||
fields: fieldsToFetch,
|
||||
sort: sort.value,
|
||||
page: page.value,
|
||||
q: searchQuery.value,
|
||||
...filtersToQuery(filters.value),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -176,6 +176,10 @@ export default defineComponent({
|
||||
type: Object as PropType<File>,
|
||||
default: null,
|
||||
},
|
||||
searchQuery: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const mainElement = inject('main-element', ref<Element>(null));
|
||||
@@ -186,7 +190,7 @@ export default defineComponent({
|
||||
const _viewQuery = useSync(props, 'viewQuery', emit);
|
||||
const _filters = useSync(props, 'filters', emit);
|
||||
|
||||
const { collection } = toRefs(props);
|
||||
const { collection, searchQuery } = toRefs(props);
|
||||
const { primaryKeyField, fields: fieldsInCollection } = useCollection(collection);
|
||||
|
||||
const availableFields = computed(() =>
|
||||
@@ -206,6 +210,7 @@ export default defineComponent({
|
||||
page,
|
||||
fields: fields,
|
||||
filters: _filters,
|
||||
searchQuery,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -162,6 +162,10 @@ export default defineComponent({
|
||||
type: Array as PropType<Filter[]>,
|
||||
default: () => [],
|
||||
},
|
||||
searchQuery: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
selectMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -182,7 +186,7 @@ export default defineComponent({
|
||||
const _viewQuery = useSync(props, 'viewQuery', emit);
|
||||
const _filters = useSync(props, 'filters', emit);
|
||||
|
||||
const { collection } = toRefs(props);
|
||||
const { collection, searchQuery } = toRefs(props);
|
||||
const { primaryKeyField, fields: fieldsInCollection } = useCollection(collection);
|
||||
|
||||
const availableFields = computed(() =>
|
||||
@@ -197,6 +201,7 @@ export default defineComponent({
|
||||
page,
|
||||
fields,
|
||||
filters: _filters,
|
||||
searchQuery,
|
||||
});
|
||||
|
||||
const {
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<search-input v-model="searchQuery" />
|
||||
|
||||
<v-dialog v-model="confirmDelete">
|
||||
<template #activator="{ on }">
|
||||
<v-button
|
||||
@@ -63,6 +65,7 @@
|
||||
:view-options.sync="viewOptions"
|
||||
:view-query.sync="viewQuery"
|
||||
:filters.sync="filters"
|
||||
:search-query.sync="searchQuery"
|
||||
/>
|
||||
</private-view>
|
||||
</template>
|
||||
@@ -81,6 +84,7 @@ import useCollection from '@/compositions/use-collection';
|
||||
import useCollectionPreset from '@/compositions/use-collection-preset';
|
||||
import FilterDrawerDetail from '@/views/private/components/filter-drawer-detail';
|
||||
import LayoutDrawerDetail from '@/views/private/components/layout-drawer-detail';
|
||||
import SearchInput from '@/views/private/components/search-input';
|
||||
|
||||
const redirectIfNeeded: NavigationGuard = async (to, from, next) => {
|
||||
const collectionsStore = useCollectionsStore();
|
||||
@@ -123,6 +127,7 @@ export default defineComponent({
|
||||
CollectionsNotFound,
|
||||
FilterDrawerDetail,
|
||||
LayoutDrawerDetail,
|
||||
SearchInput,
|
||||
},
|
||||
props: {
|
||||
collection: {
|
||||
@@ -140,7 +145,9 @@ export default defineComponent({
|
||||
const { selection } = useSelection();
|
||||
const { info: currentCollection, primaryKeyField } = useCollection(collection);
|
||||
const { addNewLink, batchLink, collectionsLink } = useLinks();
|
||||
const { viewType, viewOptions, viewQuery, filters } = useCollectionPreset(collection);
|
||||
const { viewType, viewOptions, viewQuery, filters, searchQuery } = useCollectionPreset(
|
||||
collection
|
||||
);
|
||||
const { confirmDelete, deleting, batchDelete } = useBatchDelete();
|
||||
|
||||
return {
|
||||
@@ -157,6 +164,7 @@ export default defineComponent({
|
||||
viewOptions,
|
||||
viewQuery,
|
||||
viewType,
|
||||
searchQuery,
|
||||
};
|
||||
|
||||
function useSelection() {
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<search-input v-model="searchQuery" />
|
||||
|
||||
<v-dialog v-model="confirmDelete">
|
||||
<template #activator="{ on }">
|
||||
<v-button
|
||||
@@ -64,6 +66,7 @@
|
||||
:view-options.sync="viewOptions"
|
||||
:view-query.sync="viewQuery"
|
||||
:filters="filtersWithFolderAndType"
|
||||
:search-query="searchQuery"
|
||||
@update:filters="filters = $event"
|
||||
:detail-route="'/{{project}}/files/{{primaryKey}}'"
|
||||
/>
|
||||
@@ -81,6 +84,7 @@ import useCollectionPreset from '@/compositions/use-collection-preset';
|
||||
import FilterDrawerDetail from '@/views/private/components/filter-drawer-detail';
|
||||
import LayoutDrawerDetail from '@/views/private/components/layout-drawer-detail';
|
||||
import AddFolder from '../../components/add-folder';
|
||||
import SearchInput from '@/views/private/components/search-input';
|
||||
|
||||
type Item = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -89,7 +93,7 @@ type Item = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'files-browse',
|
||||
components: { FilesNavigation, FilterDrawerDetail, LayoutDrawerDetail, AddFolder },
|
||||
components: { FilesNavigation, FilterDrawerDetail, LayoutDrawerDetail, AddFolder, SearchInput },
|
||||
props: {},
|
||||
setup() {
|
||||
const layout = ref<LayoutComponent>(null);
|
||||
@@ -97,7 +101,7 @@ export default defineComponent({
|
||||
|
||||
const selection = ref<Item[]>([]);
|
||||
|
||||
const { viewType, viewOptions, viewQuery, filters } = useCollectionPreset(
|
||||
const { viewType, viewOptions, viewQuery, filters, searchQuery } = useCollectionPreset(
|
||||
ref('directus_files')
|
||||
);
|
||||
const { addNewLink, batchLink } = useLinks();
|
||||
@@ -164,6 +168,7 @@ export default defineComponent({
|
||||
viewType,
|
||||
currentFolder,
|
||||
filtersWithFolderAndType,
|
||||
searchQuery,
|
||||
};
|
||||
|
||||
function useBatchDelete() {
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<search-input v-model="searchQuery" />
|
||||
|
||||
<v-dialog v-model="confirmDelete">
|
||||
<template #activator="{ on }">
|
||||
<v-button
|
||||
@@ -63,6 +65,7 @@
|
||||
:view-query.sync="viewQuery"
|
||||
:detail-route="'/{{project}}/users/{{item.role}}/{{primaryKey}}'"
|
||||
:filters="_filters"
|
||||
:search-query="searchQuery"
|
||||
@update:filters="filters = $event"
|
||||
/>
|
||||
</private-view>
|
||||
@@ -78,6 +81,7 @@ import { LayoutComponent } from '@/layouts/types';
|
||||
import useCollectionPreset from '@/compositions/use-collection-preset';
|
||||
import FilterDrawerDetail from '@/views/private/components/filter-drawer-detail';
|
||||
import LayoutDrawerDetail from '@/views/private/components/layout-drawer-detail';
|
||||
import SearchInput from '@/views/private/components/search-input';
|
||||
|
||||
type Item = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -86,7 +90,7 @@ type Item = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'users-browse',
|
||||
components: { UsersNavigation, FilterDrawerDetail, LayoutDrawerDetail },
|
||||
components: { UsersNavigation, FilterDrawerDetail, LayoutDrawerDetail, SearchInput },
|
||||
props: {
|
||||
role: {
|
||||
type: String,
|
||||
@@ -99,7 +103,7 @@ export default defineComponent({
|
||||
|
||||
const selection = ref<Item[]>([]);
|
||||
|
||||
const { viewType, viewOptions, viewQuery, filters } = useCollectionPreset(
|
||||
const { viewType, viewOptions, viewQuery, filters, searchQuery } = useCollectionPreset(
|
||||
ref('directus_users')
|
||||
);
|
||||
const { addNewLink, batchLink } = useLinks();
|
||||
@@ -161,6 +165,7 @@ export default defineComponent({
|
||||
viewOptions,
|
||||
viewQuery,
|
||||
viewType,
|
||||
searchQuery,
|
||||
};
|
||||
|
||||
function useBatchDelete() {
|
||||
|
||||
4
src/views/private/components/search-input/index.ts
Normal file
4
src/views/private/components/search-input/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import SearchInput from './search-input.vue';
|
||||
|
||||
export { SearchInput };
|
||||
export default SearchInput;
|
||||
26
src/views/private/components/search-input/readme.md
Normal file
26
src/views/private/components/search-input/readme.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Search Input
|
||||
|
||||
Renders as a standard header toggle button, expands into a full search bar. Should ideally be used
|
||||
in the last position in the header bar actions slot (left most action).
|
||||
|
||||
## Usage
|
||||
|
||||
```html
|
||||
<search-filter v-model="searchQuery" />
|
||||
```
|
||||
|
||||
## Props
|
||||
| Prop | Description | Default |
|
||||
|---------|------------------------------|---------|
|
||||
| `value` | Value for use with `v-model` | `null` |
|
||||
|
||||
## Events
|
||||
| Event | Description | Value |
|
||||
|---------|----------------------------------|----------|
|
||||
| `input` | Input event, used with `v-model` | `string` |
|
||||
|
||||
## Slots
|
||||
n/a
|
||||
|
||||
## CSS Variables
|
||||
n/a
|
||||
@@ -0,0 +1,29 @@
|
||||
import withPadding from '../../../../../.storybook/decorators/with-padding';
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import readme from './readme.md';
|
||||
import SearchInput from './search-input.vue';
|
||||
import RawValue from '../../../../../.storybook/raw-value.vue';
|
||||
|
||||
export default {
|
||||
title: 'Views / Private / Components / Search Input',
|
||||
decorators: [withPadding],
|
||||
parameters: {
|
||||
notes: readme,
|
||||
},
|
||||
};
|
||||
|
||||
export const basic = () =>
|
||||
defineComponent({
|
||||
components: { SearchInput, RawValue },
|
||||
props: {},
|
||||
setup() {
|
||||
const value = ref(null);
|
||||
return { value };
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<search-input v-model="value" />
|
||||
<raw-value>{{ value }}</raw-value>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
104
src/views/private/components/search-input/search-input.vue
Normal file
104
src/views/private/components/search-input/search-input.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div
|
||||
class="search-input"
|
||||
:class="{ active }"
|
||||
v-click-outside="closeIfNoContent"
|
||||
@click="active = true"
|
||||
>
|
||||
<v-icon name="search" />
|
||||
<input ref="input" :value="value" @input="emitValue" />
|
||||
<v-icon class="empty" name="close" @click="$emit('input', null)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch } from '@vue/composition-api';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const input = ref<HTMLInputElement>(null);
|
||||
|
||||
const active = ref(props.value !== null);
|
||||
|
||||
watch(active, (newActive: boolean) => {
|
||||
if (newActive === true && input.value !== null) {
|
||||
input.value.focus();
|
||||
}
|
||||
});
|
||||
|
||||
const emitValue = debounce(
|
||||
(event: InputEvent) => emit('input', (event.target as HTMLInputElement).value),
|
||||
850
|
||||
);
|
||||
|
||||
return { active, closeIfNoContent, input, emitValue };
|
||||
|
||||
function closeIfNoContent() {
|
||||
if (props.value === null || props.value.length === 0) active.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: 2px solid var(--border-normal);
|
||||
border-radius: calc(44px / 2);
|
||||
transition: width var(--slow) var(--transition);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--border-normal-alt);
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
flex-grow: 1;
|
||||
width: 0px;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.v-icon {
|
||||
margin: 0 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.empty {
|
||||
--v-icon-color: var(--foreground-subdued);
|
||||
|
||||
display: none;
|
||||
|
||||
&:hover {
|
||||
--v-icon-color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
.search-input.active {
|
||||
width: 300px;
|
||||
|
||||
input,
|
||||
.empty {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user