Files
it-know-how/vue/04-FlexibilityTable Filter Flow.md
2026-03-27 12:58:01 +01:00

7.5 KiB
Executable File

FlexibilityTable Filter Flow

This note explains how your selectedPeriodStartFrom example interacts with the rest of the code.

Goal of the feature

The table lets the user:

  • choose a start period
  • choose an end period
  • click Refresh
  • load matching backend data
  • view and export the filtered result

The important variables

From FlexibilityTable:

type PeriodFilterValue = string | null;

const periodStartOptions = getDownloadPeriodStartOptions();
const defaultPeriodStartFrom = periodStartOptions[0]?.value ?? null;
const defaultPeriodStartUntil = periodStartOptions[periodStartOptions.length - 1]?.value ?? null;

const selectedPeriodStartFrom = ref<PeriodFilterValue>(defaultPeriodStartFrom);
const selectedPeriodStartUntil = ref<PeriodFilterValue>(defaultPeriodStartUntil);
const appliedPeriodStartFrom = ref<PeriodFilterValue>(defaultPeriodStartFrom);
const appliedPeriodStartUntil = ref<PeriodFilterValue>(defaultPeriodStartUntil);

There are two groups of state here.

selected values

These track what the user is currently editing in the UI.

selectedPeriodStartFrom
selectedPeriodStartUntil

applied values

These track what was last confirmed with the Refresh button.

appliedPeriodStartFrom
appliedPeriodStartUntil

This separation is very important.

Why both selected and applied exist

Without this split, every dropdown change could immediately trigger a backend request.

That would mean:

  • too many requests
  • surprising UI updates while the user is still choosing dates
  • exports might not match the table state clearly

So the component uses a draft pattern.

Python analogy:

form_input = {'start': '2026-03-01', 'end': '2026-03-31'}
applied_filters = {'start': '2026-02-01', 'end': '2026-02-28'}

The form input is what the user is editing. The applied filters are what the system is currently using.

Step 1: build options for the dropdown

const periodStartOptions = getDownloadPeriodStartOptions();

This comes from jsonDownload.ts. That composable calculates valid backend week starts.

Each option looks roughly like this:

{
  title: '2026-03-23',
  value: '2026-03-22T23:00:00.000Z'
}
  • title is for humans
  • value is for the program

Step 2: connect the dropdown to state

<v-select
  v-model="selectedPeriodStartFrom"
  :items="periodStartOptions"
  item-title="title"
  item-value="value"
  label="Period start from"
/>

When the user chooses an option:

  • Vuetify reads the chosen item
  • Vue writes its value field into selectedPeriodStartFrom
  • any code depending on selectedPeriodStartFrom can react

Step 3: validate the selected range

const hasValidPeriodRange = computed(() => {
  if (!selectedPeriodStartFrom.value || !selectedPeriodStartUntil.value) {
    return true;
  }

  return new Date(selectedPeriodStartFrom.value).getTime() <= new Date(selectedPeriodStartUntil.value).getTime();
});

This computed reads the two selected values and checks if the range makes sense.

The Refresh button uses it:

<v-btn
  :disabled="loading || !hasValidPeriodRange"
  @click="applyDateFilters"
>
  Refresh
</v-btn>

So if the user chooses an invalid range, the button becomes disabled.

Step 4: user clicks Refresh

async function applyDateFilters() {
  if (!hasValidPeriodRange.value) {
    showMessage('Select a valid period range before refreshing.', 'error', true);
    return;
  }

  appliedPeriodStartFrom.value = selectedPeriodStartFrom.value;
  appliedPeriodStartUntil.value = selectedPeriodStartUntil.value;

  await refreshTableDataFromBackend(
    'Could not load data from backend.',
    buildSelectedDownloadPayload(),
  );
}

This does three things:

  1. validate
  2. copy selected values into applied values
  3. fetch new backend data

Step 5: build the request payload

function buildSelectedDownloadPayload(): DownloadPayload {
  return buildDownloadPayload(
    buildDateRange(selectedPeriodStartFrom.value, selectedPeriodStartUntil.value),
  );
}

and:

function buildDateRange(periodStartFrom: PeriodFilterValue, periodStartUntil: PeriodFilterValue): DownloadDateRange {
  return {
    periodStartFrom: periodStartFrom ?? undefined,
    periodStartUntil: periodStartUntil ?? undefined,
  };
}

This turns UI values into backend filter input.

Important detail:

  • null from the UI becomes undefined for the backend builder
  • that means "no boundary selected"

Step 6: call the composable

tableData.value = await downloadFlexData(payload);

This happens inside refreshTableDataFromBackend().

The component does not itself know how to talk to the backend in detail. That logic lives in useJsonDownload().

Step 7: transform backend data into rows

Inside jsonDownload.ts, the composable:

  • picks the correct endpoint
  • sends the request
  • receives backend offer data
  • converts it into table rows and display rows

So the data flow is:

Dropdown -> selectedPeriodStartFrom -> payload builder -> downloadFlexData -> tableData -> rendered table

Step 8: render the table

The table rows come from:

const rows = computed<FlexDisplayRow[]>(() => tableData.value.displayRows ?? []);

Then more filtering happens:

const filteredRows = computed<FlexDisplayRow[]>(() =>
  rows.value.filter((row) => {
    const aggregator = row.senderId ?? row.aggregator;
    const controlArea = row.receiverId ?? row.controlArea;

    if (selectedAggregators.value.length && (!aggregator || !selectedAggregators.value.includes(aggregator))) {
      return false;
    }

    if (selectedControlArea.value && controlArea !== selectedControlArea.value) {
      return false;
    }

    return true;
  }),
);

Notice something important:

  • date filters affect what is fetched from the backend
  • aggregator and control area filters affect what is shown locally in the frontend

That is a useful architectural distinction.

Step 9: exports use applied filters

For JSON export:

const rawData = await downloadRawFlexData(buildAppliedDownloadPayload());

That uses applied values, not selected values.

Why? Because if the user changed the dropdown but did not click Refresh yet, the table still reflects the old filters. Export should match what the user is actually seeing as the active state.

Full mental model

Use this simple chain:

1. Options are created
2. User picks a value
3. selectedPeriodStartFrom changes
4. Validation recomputes
5. User clicks Refresh
6. selected values become applied values
7. payload is built
8. backend data is fetched
9. tableData updates
10. table re-renders

Tiny simplified example

<template>
  <select v-model="selectedColor">
    <option value="red">Red</option>
    <option value="blue">Blue</option>
  </select>

  <button @click="apply">Apply</button>

  <p>Selected: {{ selectedColor }}</p>
  <p>Applied: {{ appliedColor }}</p>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const selectedColor = ref('red');
const appliedColor = ref('red');

function apply() {
  appliedColor.value = selectedColor.value;
}
</script>

This is the same pattern as your date filters, just simpler.

Key beginner lesson

When reading Vue, do not only ask "what is this variable". Ask:

  • who writes it?
  • who reads it?
  • is it draft state or applied state?
  • does it affect frontend-only filtering or backend requests?

That is how the whole interaction becomes understandable.