1(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1424],{68800:(e,t,n)=>{(window.__NEXT_P=window.__NEXT_P||[]).push(["/5.x/headless-cms/extending/customize-entry-list-filters",function(){return n(85879)}])},85879:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>C});var i=n(52411),r=n(16690),o=n(68946);n(14232);var l=n(17674),a=n(48230),s=n.n(a),p=n(57272),d=n(81843),m=n(75381),g=n(54416),c=n(75369),y=m.P,u=function(e){return function(t){return console.warn("Component "+e+" was not imported, exported, or provided by MDXProvider as global scope"),(0,l.yg)("div",t)}},f=u("Editor"),h=u("Image"),w={Layout:y,pageData:{title:"Customize Entry List Filters",description:"Learn how to add, replace, or remove filters in the Entry List.",type:"docs",showPageHeader:!0,fullWidth:!1,version:"5.x"},docsearch:{type:"docs",weight:100,versions:["5.x"]},navigation:g,versions:c,tableOfContents:[{title:"Overview",slug:"overview",children:[]},{title:"Add a Filter",slug:"add-a-filter",children:[{title:"Simple Filter",slug:"simple-filter",children:[]},{title:"Advanced Filter",slug:"advanced-filter",children:[]}]},{title:"Discovering Filters",slug:"discovering-filters",children:[]},{title:"Positioning a Filter",slug:"positioning-a-filter",children:[]},{title:"Removing a Filter",slug:"removing-a-filter",children:[]},{title:"Replacing a Filter",slug:"replacing-a-filter",children:[]}]};function C(e){var t=e.components,n=(0,o._)(e,["components"]);return(0,l.yg)("wrapper",(0,r._)((0,i._)({},w,n),{components:t,mdxType:"MDXLayout"}),(0,l.yg)(d.F,{type:"info",title:"Can I Use This?",mdxType:"Alert"},(0,l.yg)("p",null,"This feature is available since Webiny ",(0,l.yg)("strong",{parentName:"p"},"v5.x"),".")),(0,l.yg)(d.F,{type:"success",title:"What you'll learn",mdxType:"Alert"},(0,l.yg)("ul",null,(0,l.yg)("li",{parentName:"ul"},"how to add a custom filter to the Entry List"),(0,l.yg)("li",{parentName:"ul"},"how to discover existing filter names"),(0,l.yg)("li",{parentName:"ul"},"how to change the position of filters, remove, or replace an existing filter"))),(0,l.yg)(p.D,{level:2,id:"overview",nextElement:{type:"paragraph"}},"Overview"),(0,l.yg)("p",null,"By default, Headless CMS offers a single built-in filter to help you quickly filter entries based on their status (draft, published, unpublished)."),(0,l.yg)("p",null,"To work with filters, you need to use the ",(0,l.yg)("inlineCode",{parentName:"p"},"ContentEntryListConfig")," component. For this article, we will use the ",(0,l.yg)("inlineCode",{parentName:"p"},"Filter")," component, located in the ",(0,l.yg)("inlineCode",{parentName:"p"},"Browser")," namespace."),(0,l.yg)(f,{title:"apps/admin/src/App.tsx",lang:"diff-tsx",mdxType:"Editor"},'import React from "react";\nimport { Admin } from "@webiny/app-serverless-cms";\nimport { Cognito } from "@webiny/app-admin-users-cognito";\n+ import { ContentEntryListConfig } from "@webiny/app-headless-cms";\nimport "./App.scss";\n\n+ // You can destructure child components to make the code more readable and easier to work with.\n+ const { Browser } = ContentEntryListConfig;\n\nexport const App = () => {\n return (\n <Admin>\n <Cognito />\n+ <ContentEntryListConfig>\n+ <Browser.Filter name={"demo-filter"} element={<span>Demo Filter</span>} />\n+ </ContentEntryListConfig>\n </Admin>\n );\n};'),(0,l.yg)("p",null,"In the admin app, Browser is the main area of a Content Model Entry List, where users browse entries, apply filters, perform searching, organize entries into folders, create new entries, etc."),(0,l.yg)(h,{src:"/docs/_next/static/media/hcms-filters.ed4a89b15cba5cd0b5acfa50139d3a38.png",title:"Browser and the built-in filter by status",mdxType:"Image"}),(0,l.yg)(d.F,{type:"info",title:"Content entry setup",mdxType:"Alert"},(0,l.yg)("p",null,"The code examples below are based on a content model called ",(0,l.yg)("inlineCode",{parentName:"p"},"Property")," with the following fields:"),(0,l.yg)("ul",null,(0,l.yg)("li",{parentName:"ul"},(0,l.yg)("inlineCode",{parentName:"li"},"title")," text input"),(0,l.yg)("li",{parentName:"ul"},(0,l.yg)("inlineCode",{parentName:"li"},"price")," number input"))),(0,l.yg)(p.D,{level:2,id:"add-a-filter",nextElement:{type:"paragraph"}},"Add a Filter"),(0,l.yg)("p",null,"To add a new filter, you need a regular React element to pass to the ",(0,l.yg)("inlineCode",{parentName:"p"},"Browser.Filter")," component through the ",(0,l.yg)("inlineCode",{parentName:"p"},"element")," prop. You must also include the ",(0,l.yg)("inlineCode",{parentName:"p"},"name")," prop, which serves as a unique identifier, enabling further interactions, such as positioning, remov
1ing, and replacing."),(0,l.yg)(f,{title:"apps/admin/src/App.tsx",lang:"tsx",mdxType:"Editor"},'import { ContentEntryListConfig } from "@webiny/app-headless-cms";\n\nconst { Browser } = ContentEntryListConfig;\n\nconst DemoFilter = () => {\n return <span>Demo Filter</span>;\n};\n\n/* Register your custom filter. */\n<ContentEntryListConfig>\n <Browser.Filter name={"demo-filter"} element={<DemoFilter />} modelIds={["property"]} />\n</ContentEntryListConfig>;'),(0,l.yg)("p",null,"By declaring the ",(0,l.yg)("inlineCode",{parentName:"p"},"modelIds")," prop, you can define in which Content Model Entry List you want to show your custom filter. If you exclude this prop, the filter will be registered for all models in the system."),(0,l.yg)("p",null,"In the example above, the next time you open your ",(0,l.yg)("inlineCode",{parentName:"p"},"property")," content model browser and expand the filters bar, youâll see your new element there. This is the whole process of registering a new filter element. In the following sections, weâll focus on implementing actual filtering."),(0,l.yg)(p.D,{level:3,id:"simple-filter",nextElement:{type:"paragraph"}},"Simple Filter"),(0,l.yg)("p",null,"Hereâs an example of how to create a filter that displays only âDeluxeâ properties. We consider a property to be âDeluxeâ if its price is over 1 million."),(0,l.yg)(f,{title:"apps/admin/src/App.tsx",lang:"tsx",mdxType:"Editor"},'import { ContentEntryListConfig } from "@webiny/app-headless-cms";\nimport { useBind } from "@webiny/form";\nimport { Switch } from "@webiny/ui/Switch";\n\nconst { Browser } = ContentEntryListConfig;\n\nconst DeluxeFilter = () => {\n const bind = useBind({\n name: "price_gt"\n });\n\n const toggleFilter = () => {\n bind.onChange(bind.value ? undefined : 1000000);\n };\n\n return <Switch {...bind} label={"Show deluxe properties only"} onChange={toggleFilter} />;\n};\n\n/* Register your custom filter. */\n<ContentEntryListConfig>\n <Browser.Filter name={"deluxe"} element={<DeluxeFilter />} modelIds={["property"]} />\n</ContentEntryListConfig>;'),(0,l.yg)("p",null,"Under the hood, filters are handled via a regular form component: this means we can hook into the form to set new filter values. Weâre using the ",(0,l.yg)("inlineCode",{parentName:"p"},"useBind")," hook and give it a name. This ",(0,l.yg)("inlineCode",{parentName:"p"},"name")," will become a key within the form data object, which will be passed to the GraphQL query. To unset a filter, we set ",(0,l.yg)("inlineCode",{parentName:"p"},"undefined"),"."),(0,l.yg)("p",null,"You can write any filtering logic based on ",(0,l.yg)(s(),{href:"/5.x/headless-cms/basics/using-graphql-api-advanced-filtering"},"Headless CMSâs built-in GraphQL API filtering"),"."),(0,l.yg)(h,{src:"/docs/_next/static/media/hcms-toggle-filter.537bdfcac2aaeafc5f7a137f55ac4646.gif",title:"Toggle Filter in Action",mdxType:"Image"}),(0,l.yg)(p.D,{level:3,id:"advanced-filter",nextElement:{type:"paragraph"}},"Advanced Filter"),(0,l.yg)("p",null,"Sometimes, you need to implement filtering logic that is more complex than just setting a single value. The following example shows a filter in the form of a dropdown menu, where each option needs to use a different GraphQL query operator. ",(0,l.yg)("inlineCode",{parentName:"p"},"> 500k")," needs to use the ",(0,l.yg)("inlineCode",{parentName:"p"},"price_gt")," key, while ",(0,l.yg)("inlineCode",{parentName:"p"},"< 500k")," needs to use the ",(0,l.yg)("inlineCode",{parentName:"p"},"price_lt")," key. This cannot be achieved with simple form binding."),(0,l.yg)("p",null,"To correctly format our filters, weâll use the ",(0,l.yg)("inlineCode",{parentName:"p"},"Browser.FiltersToWhere")," component to register our custom converter, allowing you to convert form data to a valid GraphQL query input. With this approach, you have full control over values that will be passed to the API when the user interacts with the filters UI."),(0,l.yg)(f,{title:"apps/admin/src/App.tsx",lang:"tsx",mdxType:"Editor"},'import { ContentEntryListConfig } from "@webiny/app-headless-cms";\nimport { useBind } from "@webiny/form";\nimport { Select } from "@webiny/ui/Select";\n\nconst { Browser } = ContentEntryListConfig;\n\nexport const FilterByPrice = () => {\n const bind = useBind({\n name: "price",\n defaultValue: "all",\n beforeChange(value, cb) {\n cb(value === "all" ? undefined : value);\n }\n });\n\n const options = [\n { label: "All", value: "all" },\n { label: "> 500k", value: "gt:500000" },\n { label: "< 500k", value: "lt:500000" }\n ];\n\n return <Select {...bind} placeholder={"Filter by price"}
1 options={options} size="medium" />;\n};\n\nconst convertFilters = ({ price, ...rest }: any) => {\n if (!price) {\n return rest;\n }\n\n const [operator, value] = price.split(":");\n return { ...rest, [`price_${operator}`]: parseInt(value) };\n};\n\n/* Register your custom filter and the filters converter. */\n<ContentEntryListConfig>\n <Browser.Filter name={"price"} element={<FilterByPrice />} modelIds={["property"]} />\n <Browser.FiltersToWhere converter={convertFilters} modelIds={["property"]} />\n</ContentEntryListConfig>;'),(0,l.yg)(h,{src:"/docs/_next/static/media/hcms-dropdown-filter.42b17580e0b51ea364141ad6d756a0d7.gif",title:"Dropdown Filter in Action",mdxType:"Image"}),(0,l.yg)(d.F,{type:"info",title:"Multiple Converters",mdxType:"Alert"},"You can add as many converters as you need. The output of the previous converter will be passed as an input to the next converter."),(0,l.yg)(p.D,{level:2,id:"discovering-filters",nextElement:{type:"paragraph"}},"Discovering Filters"),(0,l.yg)("p",null,"This section demonstrates how you can discover the names of existing filters. This is important for further sections on positioning, removing, and replacing filters."),(0,l.yg)("p",null,"The easiest way to discover existing filters is to use your browserâs React Dev Tools plugins and look for the ",(0,l.yg)("inlineCode",{parentName:"p"},"Filters")," element. From there, you can either look for ",(0,l.yg)("inlineCode",{parentName:"p"},"filters")," props or look at the child elements and their keys:"),(0,l.yg)(h,{src:"/docs/_next/static/media/hcms-discover-filters.39f9b217a46742eac33b4e0acde16e48.png",title:"Discover Existing Filters",mdxType:"Image"}),(0,l.yg)(p.D,{level:2,id:"positioning-a-filter",nextElement:{type:"paragraph"}},"Positioning a Filter"),(0,l.yg)("p",null,"To position your custom filter before or after an existing filter, you can use the ",(0,l.yg)("inlineCode",{parentName:"p"},"before")," and ",(0,l.yg)("inlineCode",{parentName:"p"},"after")," props on the ",(0,l.yg)("inlineCode",{parentName:"p"},"<Browser.Filter>")," element:"),(0,l.yg)(f,{title:"",lang:"tsx",mdxType:"Editor"},'<ContentEntryListConfig>\n <Browser.Filter name={"price"} element={<FilterByPrice />} before={"type"} />\n</ContentEntryListConfig>'),(0,l.yg)(h,{src:"/docs/_next/static/media/hcms-position-before.8b0f7ee68a7b7dc67da04fe9e4c60644.png",title:"Position Your Filter Before Another Filter",mdxType:"Image"}),(0,l.yg)(p.D,{level:2,id:"removing-a-filter",nextElement:{type:"paragraph"}},"Removing a Filter"),(0,l.yg)("p",null,"Sometimes you might want to remove an existing filter. All you need to do is reference the filter by name and pass a ",(0,l.yg)("inlineCode",{parentName:"p"},"remove")," prop to the ",(0,l.yg)("inlineCode",{parentName:"p"},"<Browser.Filter>")," element:"),(0,l.yg)(f,{title:"",lang:"tsx",mdxType:"Editor"},'<ContentEntryListConfig>\n <Browser.Filter name={"type"} remove />\n</ContentEntryListConfig>'),(0,l.yg)(p.D,{level:2,id:"replacing-a-filter",nextElement:{type:"paragraph"}},"Replacing a Filter"),(0,l.yg)("p",null,"To replace an existing filter with a new filter element, you need to reference an existing filter by name and pass a new component via the ",(0,l.yg)("inlineCode",{parentName:"p"},"element")," prop:"),(0,l.yg)(f,{title:"",lang:"tsx",mdxType:"Editor"},'<ContentEntryListConfig>\n <Browser.Filter name={"type"} element={<span>A new filter!</span>} />\n</ContentEntryListConfig>'))}C.isMDXComponent=!0,C.layoutProps=w}},e=>{e.O(0,[3220,9730,9610,636,6593,8792],()=>e(e.s=68800)),_N_E=e.O()}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.