Filters Metadata Definition
Filters let scheduler users hide/show shifts, requests and workers directly in the calendar. As an admin you define filters as Custom Metadata records — no code deployment needed, and every filter is a small JavaScript object you control.
There are two flavors:
- Standard (checkbox) filters — show a list of checkboxes (e.g. every Shift Status found in the loaded data). Ticking a box applies immediately.
- Advanced filters — show input fields (text, dropdown, slider, date, time, address search…) and apply only when the user clicks Execute.
Both live in the filters panel (right-hand drawer), participate in the Filter Logic expression, and appear as numbered summary pills in the header when active.
1. Where filters are defined
Create records of b3s__Scheduling_Filter__mdt (Setup → Custom Metadata Types → Scheduling Filter → Manage Records):
| Field | Purpose |
|---|---|
Label / DeveloperName
|
Admin-facing name of the record (not shown to users). |
b3s__Javascript_Code__c
|
A single JavaScript object literal defining the filter (see below). |
b3s__Order__c
|
Sort order in the filters panel (ascending). The shipped filters use 1–10. |
All Scheduling Filter records in the org are loaded when the scheduler boots — there is no per-app or per-group assignment. To remove a filter, delete (or don't deploy) the record.
Ready-made examples ship in unpackaged/customMetadata/ as b3s__Scheduling_Filter.*.md-meta.xml — deploy the ones you want.
2. The filter object — common contract
b3s__Javascript_Code__c holds one JS object literal:
{
name: "shift-status", // unique across all filters
label: "Shift Status", // heading shown in the panel
type: 'shift', // 'shift' | 'request' | 'resource'
isDefaultOpen: true, // optional: panel section starts expanded
// EITHER items() ........... standard checkbox filter
// OR inputs: [...] ...... advanced filter
matchesItems: function (object, items) {
// return true -> keep the object visible
// return false -> hide it
}
}
type — what the filter hides
'resource'— evaluated against worker rows (resources). Hiding a worker also hides all of their events, and removes the row entirely.'shift'/'request'— evaluated against calendar events. Note that either value is evaluated against all events (both shifts and requests); the value mainly drives the shows/hides counters on the header pills. If your filter should only affect one kind, checkevent.title(it's'shift'or'request') insidematchesItemsand returntruefor the other kind.
A filter never applies across categories: while evaluating a worker row, any active shift filters count as "pass" (and vice versa), so they stay neutral in the filter-logic expression.
What matchesItems receives
The first argument is a FullCalendar object:
- Events:
event.extendedProps.recordis the Salesforce record (with the fields your loaders / context provider fetch),event.start/event.endareDates,event.titleis'shift'or'request'. - Resources:
resource.extendedProps.recordis the Contact record,resource.idis the row id. The Unassigned swimlane has no record — returntruewhenrecord?.Idis missing so it stays visible.
The second argument is the current selection:
- Standard filter → array of selected item
names. - Advanced filter → a single-element array;
items[0]is an object with the user's input values keyed by inputname.
Variables available to your code
The snippet is evaluated inside the scheduler with these in scope:
| Variable | Contents |
|---|---|
events
|
All loaded calendar events (unfiltered). |
screenEvents
|
Events currently visible (after filters). |
resources
|
All worker rows, including currently hidden ones. |
screenReources
|
Worker rows currently visible (note the spelling). |
viewName
|
Current calendar view type. |
Filters are re-evaluated (the whole snippet re-runs) whenever data reloads — so items() can derive its options from events and stay current, but keep the code fast: it runs against every event on every filter run, and large datasets are processed in chunks with a progress bar.
3. Standard (checkbox) filters
Declare items() returning the checkbox list:
{
name: "shift-status",
label: "Shift Status",
type: 'shift',
isDefaultOpen: true,
items: function () {
// Distinct statuses from the loaded events
const statuses = [...new Set(
events.map(e => e.extendedProps?.record?.b3s__Status__c).filter(Boolean)
)];
return statuses.map(s => ({ name: s, label: s }));
},
matchesItems: function (event, selected = []) {
return selected.includes(event.extendedProps?.record?.b3s__Status__c);
}
}
Item properties: name (the value passed back in selected), label, optional color (checkbox border color) or classes (extra CSS classes). Items render in the order returned. A search box appears automatically when a filter has more than 5 items.
Checking/unchecking applies immediately. matchesItems is only called while at least one item is selected.
4. Advanced filters
Declare inputs instead of items. The panel renders the inputs plus Execute and Clear buttons; nothing applies until Execute is clicked. On Execute all values are collected into one object and handed to matchesItems as items[0].
{
name: "resource_distance",
label: "Workers by Distance",
type: 'resource',
inputs: [
{ name: "address", type: "address", label: "Address",
placeholder: "Search address..", required: true },
{ name: "distance", type: "select", label: "Within", default: "5",
options: [
{ label: "1 km", value: "1" },
{ label: "5 km", value: "5" },
{ label: "10 km", value: "10" }
] }
],
matchesItems: function (resource, items) {
const values = items[0] ?? {}; // { address: {label,lat,lng}, distance: "5" }
const record = resource.extendedProps?.record;
if (!record?.Id) return true; // keep the Unassigned lane
// ... distance calculation, return true/false
}
}
Input types
type
|
Renders | Value received in matchesItems
|
|---|---|---|
text
|
text box | string |
select
|
dropdown (needs options)
|
the option value (always a string — Number(...) it if numeric)
|
multiselect
|
checkbox list (needs options)
|
array of option values |
range
|
slider | with options: the selected option's value (discrete stops); otherwise a number, configured via min / max / step / unit
|
date
|
native date picker | "YYYY-MM-DD" string
|
time
|
native time picker | "HH:MM" string
|
address
|
Google address search | { label, lat, lng } — coordinates only exist when the user picked a suggestion
|
Common input properties: name, type, label, required, placeholder, default, options ([{ label, value }]).
Behavior notes:
requiredinputs are validated on Execute; empty ones show an inline error. An address input must be picked from the suggestions — typed text alone carries no coordinates and counts as empty.- Typed values survive panel refreshes and data reloads (drafts are kept until Clear / Clear All).
- Options can be computed dynamically with an IIFE — see the
dayinput in the shippedResource_Availabilityfilter, which labels Today / Tomorrow / day-after with real dates. - When active, the header pill summarizes the entered values as
Label: valuepairs.
One-time setup for address inputs
Address search uses Google Places and needs an API key:
- Setup → Custom Settings → Scheduler Settings (
b3s__Scheduler__c) → Manage. - Set Google Maps API Key (
b3s__Google_Maps_API_Key__c) at org level (Places API enabled on the key).
Without the key, filters containing an address input show a warning and can't be executed. All other input types need no setup.
5. Filter Logic
Each active filter gets a number badge (1, 2, 3…) in activation order. By default all active filters are AND-ed. Users (or you, testing) can enter a custom expression in the Filter Logic box at the top of the panel:
1 AND (2 OR 3)
Rules — same style as Salesforce report filter logic:
- Only
AND,ORand parentheses (case-insensitive). - Every active filter's number must appear at least once; no unknown numbers.
- Invalid logic shows an error and the calendar falls back to the default AND-everything behavior until fixed.
- Reset returns to the default; Clear All clears every filter and the logic.
Filters of the "other" category (resource vs event) evaluate as true inside the expression for that target, so mixed logic like 1 AND (2 OR 3) works even when 1 is a worker filter and 2–3 are shift filters.
6. Shipped filters
Found in unpackaged/customMetadata/ — deploy what you need:
| Filter | Record | Kind | What it does |
|---|---|---|---|
| Shift Status | Shift_Status_Filter
|
checkbox | One checkbox per distinct b3s__Status__c in the loaded shifts.
|
| Job / Site / Job Group | Shift_Job_Filter, Shift_Site_Filter, Shift_Job_Group_Filter
|
checkbox | Filter shifts by their job / site / job group. |
| Workers | Resource_Filter
|
checkbox | Filter the worker rows by name. |
| Workers by Distance | Resource_Distance_Filter
|
advanced | Address + radius dropdown (1–15 km); keeps workers whose Contact MailingLatitude/MailingLongitude is in range.
|
| Shifts by Distance | Shift_Distance_Filter
|
advanced | Address + distance slider; keeps shifts whose work-site location is in range. |
| Available After / Before Time | Resource_Available_After, Resource_Available_Before
|
advanced | Time input; keeps workers with no shift and no "Unavailable" request overlapping the rest of the day after (resp. before) that time. |
| Worker Availability | Resource_Availability
|
advanced | Day (Today / Tomorrow / day-after) + period (Morning / Day / Night / Full day); keeps workers not booked in that window. |
Data prerequisites:
- Workers by Distance — the contacts loader must include
MailingLatitude/MailingLongitudeinfieldToLoad(shipped contact loaders already do). - Shifts by Distance — the shift definition in the Scheduling Context Provider must list the site coordinates in
additionalFields:b3s__Work_Site__r.b3s__Location__Latitude__sand...__Longitude__s(present in the shipped providers). - Availability filters — evaluate against events currently loaded in the scheduler's date range, in the browser's local timezone. Cancelled shifts never count as bookings; a request counts when
b3s__Type__c = "Unavailable".
7. Gotchas & tips
- Filtering is client-side. Filters hide already-loaded data; they don't change what the loaders fetch. To restrict what's loaded, use loader
filterRecordsByclauses or an Apex Filter plugin (seeAdvanced Filtering) instead. - Any field your filter reads must be fetched by a loader or listed in the definition's field paths — otherwise it's simply
undefinedon the record. - Special-date background highlights are never filtered.
- Hiding an event also removes it from any current selection.
- Hovering an active filter's header pill shows how many items it showed/hid in the last run.
- Keep
matchesItemscheap — it runs once per event/worker per filter run. Precompute lookups outside the returned object if needed (the snippet body runs once per data load,matchesItemsruns per record).