├── .gitignore ├── assets └── screenshots │ └── search-config.png ├── src ├── search-configuration │ ├── shims.d.ts │ ├── index.ts │ └── options.vue └── intercept-search │ └── index.ts ├── tsconfig.json ├── README.md ├── LICENSE.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .turbo 3 | node_modules 4 | dist -------------------------------------------------------------------------------- /assets/screenshots/search-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmiteam/directus-extension-custom-search/HEAD/assets/screenshots/search-config.png -------------------------------------------------------------------------------- /src/search-configuration/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue'; 3 | const component: DefineComponent<{}, {}, any>; 4 | export default component; 5 | } 6 | -------------------------------------------------------------------------------- /src/search-configuration/index.ts: -------------------------------------------------------------------------------- 1 | import { defineInterface } from '@directus/extensions-sdk' 2 | import OptionsComponent from './options.vue' 3 | import { ComponentOptions } from 'vue' 4 | 5 | export default defineInterface({ 6 | id: 'search-configuration', 7 | name: 'Configure Search', 8 | icon: 'search', 9 | description: 10 | 'Override the Directus internal search system with a custom search filter - supports relationships.', 11 | component: () => null, 12 | options: OptionsComponent as ComponentOptions, 13 | hideLabel: true, 14 | hideLoader: true, 15 | types: ['alias'], 16 | localTypes: ['presentation'], 17 | group: 'presentation', 18 | }) 19 | -------------------------------------------------------------------------------- /src/search-configuration/options.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["ES2019", "DOM"], 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "noFallthroughCasesInSwitch": true, 8 | "esModuleInterop": true, 9 | "noImplicitAny": true, 10 | "noImplicitThis": true, 11 | "noImplicitReturns": true, 12 | "noUnusedLocals": true, 13 | "noUncheckedIndexedAccess": true, 14 | "noUnusedParameters": true, 15 | "alwaysStrict": true, 16 | "strictNullChecks": true, 17 | "strictFunctionTypes": true, 18 | "strictBindCallApply": true, 19 | "strictPropertyInitialization": true, 20 | "resolveJsonModule": false, 21 | "skipLibCheck": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "allowSyntheticDefaultImports": true, 24 | "isolatedModules": true, 25 | "rootDir": "./src" 26 | }, 27 | "include": ["./src/**/*.ts"] 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Search Configuration for Directus 2 | 3 | Provides a way to configure the Directus search filters for a collection. This allows you to supercharge your Directus `search` field with AND/OR groups, strict equality, case-insensitive searches, and nested relational searches, fully under your control. 4 | 5 | # Installation 6 | 7 | This plugin has not yet been published on NPM, but you can try installing it directly from GitHub. 8 | 9 | # Usage 10 | 11 | 1. Add a new field named `_search_config` in the collection you with to add search configuration for. 12 | 13 | > **The field must have the key `_search_config` at the moment. In the future we might add a way to configure this.** 14 | 15 | 2. Configure your filter in the `Search Config` interface options. Use `$SEARCH` as a placeholder for the user's search query. 16 | 17 | Search the collection. Both app and API searches will now use the filter pattern you've specified. You can use nested relational fields for the search. 18 | 19 | ![](./assets/screenshots/search-config.png) 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2024 Creation Ministries International (US) Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "directus-extension-custom-search", 3 | "description": "Override the Directus internal search system with a custom search filter - supports relationships", 4 | "icon": "extension", 5 | "version": "0.1.0", 6 | "license": "MIT", 7 | "contributors": [ 8 | { 9 | "name": "Creation Ministries International - IT Team", 10 | "url": "https://creation.com" 11 | } 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "github:cmiteam/directus-extension-custom-search" 16 | }, 17 | "keywords": [ 18 | "directus", 19 | "directus-extension", 20 | "directus-extension-bundle" 21 | ], 22 | "type": "module", 23 | "files": [ 24 | "dist" 25 | ], 26 | "directus:extension": { 27 | "type": "bundle", 28 | "sandbox": { 29 | "enabled": true, 30 | "requestedScopes": {} 31 | }, 32 | "path": { 33 | "app": "dist/app.js", 34 | "api": "dist/api.js" 35 | }, 36 | "entries": [ 37 | { 38 | "type": "hook", 39 | "name": "intercept-search", 40 | "source": "src/intercept-search/index.ts" 41 | }, 42 | { 43 | "type": "interface", 44 | "name": "search-configuration", 45 | "source": "src/search-configuration/index.ts" 46 | } 47 | ], 48 | "host": "^10.10.0" 49 | }, 50 | "scripts": { 51 | "build": "directus-extension build", 52 | "dev": "directus-extension build -w --no-minify", 53 | "link": "directus-extension link", 54 | "add": "directus-extension add" 55 | }, 56 | "devDependencies": { 57 | "@directus/extensions-sdk": "^16", 58 | "@types/node": "^20.12.13", 59 | "typescript": "^5.4.5", 60 | "vue": "^3.4.27" 61 | } 62 | } -------------------------------------------------------------------------------- /src/intercept-search/index.ts: -------------------------------------------------------------------------------- 1 | import type { SandboxHookRegisterContext } from 'directus:api' 2 | 3 | // Recursively run a replace operation on any string object values. 4 | function recursivelyReplaceString( 5 | value: any, 6 | // User-defined string replacement function. 7 | replaceFunc: (string: string) => string, 8 | ) { 9 | if (value == null) return value 10 | 11 | if (Array.isArray(value)) { 12 | // Iterate through arrays and recursively replace any strings in them. 13 | for (let i = 0; i < value.length; i++) { 14 | value[i] = recursivelyReplaceString(value[i], replaceFunc) 15 | } 16 | } else if (typeof value === 'object') { 17 | // Iterate through object and recursively replace any strings in them. 18 | for (let key in value) { 19 | value[key] = recursivelyReplaceString(value[key], replaceFunc) 20 | } 21 | } else if (typeof value === 'string' && value === '$SEARCH') { 22 | // Replace strings with user defined function. 23 | value = replaceFunc(value) 24 | } else if (typeof value === 'string' && value == '-1') { 25 | const result = +replaceFunc('$SEARCH') 26 | if (!isNaN(result)) value = result 27 | } 28 | 29 | return value 30 | } 31 | 32 | // Overrides the search functionality with additional configuration from a _search_config field from a collection. 33 | export default ({ filter }: SandboxHookRegisterContext, { services }) => { 34 | filter( 35 | 'items.query', 36 | //@ts-ignore 37 | async ( 38 | query: { search?: string; filter: any }, 39 | { collection }: { collection: string }, 40 | context: { schema: any }, 41 | ) => { 42 | if (!query.search) return query 43 | // Load _search_config field metadata from Directus. 44 | // Unfortunately, we can't filter by interface, so the field name is hardcoded for all collections. 45 | const fieldsService = new services.FieldsService({ 46 | schema: context?.schema, 47 | accountability: { admin: true, roles: [] }, 48 | }) 49 | 50 | // Bail out early if we don't find any search configuration information. 51 | let searchConfig = null 52 | try { 53 | searchConfig = ( 54 | await fieldsService.readOne(collection, '_search_config') 55 | )?.meta?.options?.search_config 56 | if (!searchConfig) return query 57 | } catch (e) { 58 | return query 59 | } 60 | 61 | const searchFilter = recursivelyReplaceString(searchConfig, (entry) => 62 | entry.replace('$SEARCH', query.search || ''), 63 | ) 64 | 65 | // Take search out of the query. 66 | const modifiedQuery = { ...query, search: undefined } 67 | if (!modifiedQuery.filter) modifiedQuery.filter = searchFilter 68 | else modifiedQuery.filter = { _and: [modifiedQuery.filter, searchFilter] } 69 | return modifiedQuery 70 | }, 71 | ) 72 | } 73 | --------------------------------------------------------------------------------