Filters Metadata Definition

From 3B Knowledge
Jump to navigation Jump to search

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, check event.title (it's 'shift' or 'request') inside matchesItems and return true for 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.record is the Salesforce record (with the fields your loaders / context provider fetch), event.start / event.end are Dates, event.title is 'shift' or 'request'.
  • Resources: resource.extendedProps.record is the Contact record, resource.id is the row id. The Unassigned swimlane has no record — return true when record?.Id is 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 input name.

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:

  • required inputs 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 day input in the shipped Resource_Availability filter, which labels Today / Tomorrow / day-after with real dates.
  • When active, the header pill summarizes the entered values as Label: value pairs.

One-time setup for address inputs

Address search uses Google Places and needs an API key:

  1. Setup → Custom SettingsScheduler Settings (b3s__Scheduler__c) → Manage.
  2. 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, OR and 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 / MailingLongitude in fieldToLoad (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__s and ...__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 filterRecordsBy clauses or an Apex Filter plugin (see Advanced 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 undefined on 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 matchesItems cheap — 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, matchesItems runs per record).