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:
Rijk van Zanten
2020-04-24 16:27:17 -04:00
committed by GitHub
parent 3ca7e66a3f
commit 9138e7ab5b
11 changed files with 216 additions and 10 deletions

View File

@@ -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 };
}

View File

@@ -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),
},
});

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -0,0 +1,4 @@
import SearchInput from './search-input.vue';
export { SearchInput };
export default SearchInput;

View 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

View File

@@ -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>
`,
});

View 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>