Saltar al contenido
SuperScheduler

InteracciónSe aplica aSuperScheduler Pro

Árboles de recursos, columnas y selección

Activa treeEnabled y anida recursos en children; un padre sigue plegado salvo que tenga expanded: true. Añade columnas a la cabecera de filas con rowHeaderColumns y rellena sus celdas a partir de campos del recurso mediante display. Filtra con control.rows.filter() y control.events.filter() junto con onRowFilter y onEventFilter, y selecciona con eventClickHandling: 'Select', la selección rectangular, rowClickHandling: 'Select' y las APIs multiselect y multirange.

Verificado con v0.1.0 · revisado el 7 de octubre de 2026.md

Las planificaciones reales rara vez tienen una lista plana de recursos. Las habitaciones pertenecen a plantas, los técnicos a regiones y las máquinas a líneas. Esta guía trata las funciones de Pro que organizan las filas y permiten trabajar con varios elementos a la vez: árboles de recursos, columnas en la cabecera de filas, filtros y los tres tipos de selección (eventos, filas y rangos de tiempo).

Todo lo que aparece aquí necesita SuperScheduler Pro (super-scheduler). La edición Lite pinta una lista plana y rechaza los children y las columns de los recursos.

Agrupar recursos en un árbol

Un árbol son datos sin más: un recurso con un array children se convierte en una fila padre. Dos cosas lo activan:

  • treeEnabled: true en el scheduler. Sin esta opción, los children se ignoran y la cabecera de filas no tiene botones para plegar y desplegar.
  • expanded: true en cada padre que deba empezar abierto. Los padres están plegados salvo que expanded sea exactamente true.
src/TeamPlanner.tsxtsx
import { useMemo } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import 'super-scheduler/styles.css'

// Regions are parent rows and technicians their children. A parent stays collapsed
// unless its resource says `expanded: true`.
const RESOURCES: SuperScheduler.ResourceData[] = [
  {
    id: 'north',
    name: 'North region',
    expanded: true,
    children: [
      { id: 'ana', name: 'Ana Ruiz' },
      { id: 'ben', name: 'Ben Ortiz' },
    ],
  },
  {
    id: 'south',
    name: 'South region',
    children: [
      { id: 'cai', name: 'Cai Moreno' },
      { id: 'dee', name: 'Dee Patel' },
    ],
  },
]

const JOBS: SuperScheduler.EventData[] = [
  {
    id: 'job-1',
    resource: 'ana',
    start: '2026-10-05T00:00:00',
    end: '2026-10-07T00:00:00',
    text: 'Panel upgrade',
  },
  {
    id: 'job-2',
    resource: 'cai',
    start: '2026-10-06T00:00:00',
    end: '2026-10-09T00:00:00',
    text: 'Boiler service',
  },
]

export function TeamPlanner() {
  const { controlRef, control } = useSchedulerControl()
  // The control adopts the events array and mutates it: hand it a copy.
  const events = useMemo(() => JOBS.slice(), [])

  return (
    <section aria-labelledby="team-plan-title">
      <h2 id="team-plan-title">Field teams</h2>
      <div role="toolbar" aria-label="Rows">
        <button type="button" disabled={control === null} onClick={() => control?.rows.expandAll()}>
          Expand all
        </button>
        <button
          type="button"
          disabled={control === null}
          onClick={() => control?.rows.collapseAll()}
        >
          Collapse all
        </button>
      </div>
      <SuperSchedulerComponent
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        cellWidth={56}
        treeEnabled
        treeIndent={16}
        // Region rows group technicians: no jobs and no time selection on them.
        treePreventParentUsage
        resources={RESOURCES}
        events={events}
      />
    </section>
  )
}

Deberías ver «North region» abierta con sus dos técnicos, «South region» cerrada y los dos botones funcionando en cuanto el scheduler se haya montado (hasta entonces, control es null).

Opciones relacionadas:

OpciónPor defectoEfecto
treeEnabledfalseConstruye las filas a partir de children.
treeIndent20Sangría por nivel, en píxeles.
treePreventParentUsagefalseLas filas padre no aceptan eventos ni selección de tiempo.
treeAutoExpandtrueUn padre plegado se abre cuando se arrastra un evento sobre él durante medio segundo.
rowFilterParentsAlwaysVisibletrueMantiene los ancestros de una fila que cumple un filtro.

Los padres también pueden contener eventos (una reserva para «todo el equipo») salvo que actives treePreventParentUsage. Un recurso concreto rechaza los eventos que se sueltan sobre él con preventUsage: true, y los recursos de primer nivel se pueden fijar por encima o por debajo de las filas con scroll con frozen: 'top' o frozen: 'bottom', algo útil para filas de resumen.

Desplegar y plegar desde código

El control expone el árbol mediante control.rows:

  • control.rows.expandAll() y control.rows.collapseAll();
  • control.rows.expand(level) abre los padres por encima de un nivel (1 por defecto, -1 para todos los niveles);
  • control.rows.find(id) devuelve una fila; row.expand(), row.collapse() y row.toggle() la cambian.

onResourceExpand y onResourceCollapse comunican un cambio después de que ocurra, con la fila como args.resource. Úsalos para recordar qué ha abierto el usuario o para mantener sincronizadas otras partes de tu interfaz.

Añadir columnas a la cabecera de filas

Sin rowHeaderColumns, la cabecera de filas muestra el name de cada recurso. Con esta opción, la cabecera se convierte en una pequeña tabla:

  • text es el título de la columna (también se aceptan name y title);
  • display nombra un campo del recurso cuyo valor rellena la celda (antes se busca un campo con ese nombre dentro de tags);
  • la primera columna muestra el name del recurso cuando no tiene display;
  • width fija el ancho en píxeles (por defecto, rowHeaderColumnDefaultWidth, 80);
  • sort hace que el título se pueda pulsar: ordena las filas por ese campo, primero de forma ascendente y después descendente, y pone aria-sort en el título.

Un recurso también puede llevar sus propias columns: [{ text }, { html }]. Esas entradas se asignan por posición, una por columna.

src/TechnicianColumns.tsxtsx
import { useState } from 'react'
import { type SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import 'super-scheduler/styles.css'

type Status = 'available' | 'off'

const TECHNICIANS: SuperScheduler.ResourceData[] = [
  { id: 'ana', name: 'Ana Ruiz', skill: 'Electrical', shift: 'Early', status: 'available' },
  { id: 'ben', name: 'Ben Ortiz', skill: 'HVAC', shift: 'Late', status: 'off' },
  { id: 'cai', name: 'Cai Moreno', skill: 'Plumbing', shift: 'Early', status: 'available' },
]

// `display` reads a resource field, so a column keeps its values when it is reordered.
// `sort` makes the title clickable: it sorts the rows by that field, ascending then descending.
const INITIAL_COLUMNS: SuperScheduler.RowHeaderColumnData[] = [
  { text: 'Technician', width: 150, sort: 'name' },
  { text: 'Skill', display: 'skill', width: 110, sort: 'skill' },
  { text: 'Shift', display: 'shift', width: 70, nonresizable: true },
  { text: 'Status', display: 'status', width: 100 },
]

const STATUS_LABEL: Record<Status, string> = { available: 'Available', off: 'Day off' }

// Runs with `this` = the control, whose rowHeaderColumns are always in the current order.
function decorateStatus(
  this: SuperScheduler.SchedulerApi,
  args: SuperScheduler.SchedulerBeforeRowHeaderRenderArgs,
): void {
  const index = this.rowHeaderColumns?.findIndex((column) => column.display === 'status') ?? -1
  const cell = args.row.columns[index]
  const status = args.row.data.status
  if (cell === undefined || (status !== 'available' && status !== 'off')) return
  // With rowHeaderColumns, row.html is ignored: write the column's own html (trusted markup).
  cell.html = `<span class="tech-status tech-status--${status}">${STATUS_LABEL[status]}</span>`
}

export function TechnicianColumns({ events }: { events: SuperScheduler.EventData[] }) {
  // Kept in state so a parent re-render never hands the control the original order again.
  const [columns, setColumns] = useState(INITIAL_COLUMNS)

  return (
    <SuperSchedulerComponent
      startDate="2026-10-05"
      days={14}
      scale="Day"
      cellWidth={56}
      resources={TECHNICIANS}
      events={events}
      rowHeaderColumns={columns}
      rowHeaderColumnsReorderable
      onRowHeaderColumnsChange={(args) => setColumns([...args.columns])}
      onBeforeRowHeaderRender={decorateStatus}
    />
  )
}

Para decorar una celda, usa onBeforeRowHeaderRender. Con columnas, el html propio de la fila se ignora: escribe en args.row.columns[i].html. Ese marcado es HTML de confianza, así que escapa el texto de los usuarios con SuperScheduler.Util.escapeHtml. Para contenido React en la cabecera de filas, consulta Slots de renderizado React.

Redimensionar y reordenar columnas

Las columnas se pueden redimensionar por defecto (rowHeaderColumnsResizable: true); nonresizable: true lo desactiva para una columna. Tras redimensionar, el control escribe el nuevo ancho en ese objeto de columna de tu array rowHeaderColumns y llama a onRowHeaderColumnResized({ column }).

La reordenación está desactivada por defecto. Con rowHeaderColumnsReorderable, los usuarios arrastran los títulos de las columnas; los bordes de redimensionado tienen prioridad sobre el arrastre. En keyboardMode: 'Full', Alt+Mayús+flecha izquierda y Alt+Mayús+flecha derecha mueven el título con foco y anuncian el cambio. Desde código, control.moveRowHeaderColumn(from, to) mueve una columna según su índice en el orden actual.

Cada redimensionado y cada reordenación llaman a onRowHeaderColumnsChange con:

  • columns: las columnas en su nuevo orden, con sus anchos;
  • order: order[i] es el índice original (en la prop que pasaste) de la columna que ahora está en la posición i;
  • widths y reason ('resize' o 'reorder').

Guarda las columnas en el estado de React, como hace el snippet, y adopta args.columns. De lo contrario, un padre que se vuelva a renderizar con un array literal nuevo devolvería el orden original al control.

Filtrar filas y eventos

Un filtro es un parámetro más un callback. control.rows.filter(param) guarda el parámetro y llama a onRowFilter para cada fila; pon args.visible = false para ocultarla. control.events.filter(param) y onEventFilter funcionan igual con los eventos. Un parámetro falsy ('', null, 0) borra el filtro.

src/FilteredJobs.tsxtsx
import { useEffect, useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'

type JobStatus = 'scheduled' | 'done' | 'cancelled'
type Job = { status: JobStatus }

// Module-level handlers keep their identity: a new onRowFilter or onEventFilter
// function makes the control evaluate the active filter again.
const FILTERS: Pick<SchedulerProps, 'onRowFilter' | 'onEventFilter'> = {
  onRowFilter: (args) => {
    const query = String(args.filterParam).toLowerCase()
    args.visible = args.row.name.toLowerCase().includes(query)
  },
  onEventFilter: (args) => {
    const job = args.e.data as SuperScheduler.EventData<Job>
    args.visible = job.status === args.filterParam
  },
}

export function FilteredJobs(props: {
  resources: SuperScheduler.ResourceData[]
  jobs: SuperScheduler.EventData<Job>[]
}) {
  const { controlRef, control } = useSchedulerControl()
  const [query, setQuery] = useState('')
  const [status, setStatus] = useState<JobStatus | ''>('')
  const events = useMemo(() => props.jobs.slice(), [props.jobs])

  // A falsy parameter ('' or null) clears the filter.
  useEffect(() => control?.rows.filter(query.trim() || null), [control, query])
  useEffect(() => control?.events.filter(status || null), [control, status])

  return (
    <>
      <label>
        Technician <input type="search" value={query} onChange={(e) => setQuery(e.target.value)} />
      </label>
      <label>
        Status
        <select value={status} onChange={(e) => setStatus(e.target.value as JobStatus | '')}>
          <option value="">All</option>
          <option value="scheduled">Scheduled</option>
          <option value="done">Done</option>
          <option value="cancelled">Cancelled</option>
        </select>
      </label>
      <SuperSchedulerComponent
        {...FILTERS}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        treeEnabled
        // Keeps the region row of every matching technician (default true).
        rowFilterParentsAlwaysVisible
        resources={props.resources}
        events={events}
      />
    </>
  )
}

Al escribir «ben» quedan «North region» y «Ben Ortiz»: rowFilterParentsAlwaysVisible mantiene los ancestros de cada fila que coincide. Elegir un estado oculta los demás eventos sin tocar las filas.

Los filtros se ejecutan en el navegador sobre los datos que tiene el control. Una búsqueda en datos que no están cargados (otro mes, otra sede) es una consulta a tu backend, seguida de nuevos resources o events.

Seleccionar eventos

Por defecto, un clic en un evento solo ejecuta onEventClick. La selección necesita eventClickHandling: 'Select':

  • un clic alterna la selección del evento y deselecciona los demás;
  • Ctrl+clic (Cmd+clic en macOS) alterna su selección y mantiene los demás, mientras allowMultiSelect sea true (el valor por defecto);
  • con eventMultiSelectRange, Mayús+clic selecciona los eventos entre el último evento pulsado y este, a lo largo de las filas visibles; Ctrl/Cmd+Mayús los añade.

onEventSelect se ejecuta antes del cambio y puede cancelarlo con args.preventDefault(); onEventSelected se ejecuta después.

La API control.multiselect lee y cambia la selección: get(), add(e), remove(e), clear(), isSelected(e) y selectAll({ scope, filter }). selectAll selecciona por defecto los eventos visibles; scope: 'all' recorre todos los eventos cargados, con un coste proporcional al tamaño del almacén.

src/SelectableJobs.tsxtsx
import { useEffect, useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
import { subscribeScheduler } from 'super-scheduler/hooks'

type Job = { status: 'scheduled' | 'done' }

export function SelectableJobs(props: {
  resources: SuperScheduler.ResourceData[]
  jobs: SuperScheduler.EventData<Job>[]
  onReschedule: (ids: SuperScheduler.EventId[]) => void
}) {
  const { controlRef, control } = useSchedulerControl()
  const [count, setCount] = useState(0)
  const events = useMemo(() => props.jobs.slice(), [props.jobs])

  // Rectangle selections and multiselect calls (selectAll, clear) publish the
  // 'selection' topic; clicks are reported by onEventSelected below.
  useEffect(() => {
    if (control === null) return
    return subscribeScheduler(control, 'selection', (selected) => setCount(selected.length))
  }, [control])

  const config = useMemo<SchedulerProps>(
    () => ({
      // A click toggles the event; Ctrl+click (Cmd+click on macOS) keeps the others.
      eventClickHandling: 'Select',
      // Shift+click selects every event between the last clicked one and this one.
      eventMultiSelectRange: true,
      // Shift+drag over the grid selects the events inside the rectangle.
      rectangleSelectHandling: 'EventSelect',
      // With the rectangle: Shift adds to the selection, Alt removes from it.
      rectangleSelectModifiers: true,
      onEventSelected() {
        setCount(this.multiselect.get().length)
      },
      // Marks selected events for CSS. The hook runs again for every event whose
      // selection changes.
      onBeforeEventRender: (args) => {
        const e = args.control.events.find(args.data.id)
        if (e !== null && args.control.multiselect.isSelected(e)) {
          args.data.cssClass = [args.data.cssClass, 'is-selected'].filter(Boolean).join(' ')
        }
      },
    }),
    [],
  )

  const selectScheduled = () =>
    control?.multiselect.selectAll({
      scope: 'view',
      filter: (e) => (e.data as SuperScheduler.EventData<Job>).status === 'scheduled',
    })

  const reschedule = () => {
    const ids = control?.multiselect.get().map((e) => e.id()) ?? []
    if (ids.length > 0) props.onReschedule(ids)
  }

  return (
    <>
      <div role="toolbar" aria-label="Selection">
        <button type="button" onClick={selectScheduled}>
          Select scheduled in view
        </button>
        <button type="button" onClick={() => control?.multiselect.clear()}>
          Clear
        </button>
        <button type="button" disabled={count === 0} onClick={reschedule}>
          Reschedule {count} {count === 1 ? 'job' : 'jobs'}
        </button>
      </div>
      <SuperSchedulerComponent
        {...config}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        resources={props.resources}
        events={events}
      />
    </>
  )
}

Deberías ver cómo el contador del botón sigue los clics, los rectángulos y las acciones de la barra de herramientas, y los trabajos seleccionados resaltados con este CSS:

csscss
.super-scheduler__event.is-selected {
  z-index: 2;
  box-shadow: 0 0 0 2px var(--super-scheduler-accent), var(--super-scheduler-shadow-2);
}

Con allowMultiMove, arrastrar un evento seleccionado mueve toda la selección; cada evento pasa por las mismas reglas de movimiento (consulta arrastrar, redimensionar y reglas de negocio). En el modo de teclado completo, Ctrl/Cmd+A selecciona todos los eventos visibles.

Selección rectangular

rectangleSelectHandling decide qué hace un rectángulo trazado con el puntero:

  • 'EventSelect' selecciona los eventos que contiene (y ejecuta los callbacks);
  • 'Enabled' solo ejecuta los callbacks, para aplicaciones que actúan por su cuenta sobre el rectángulo;
  • 'Disabled' (por defecto) lo desactiva.

Los usuarios empiezan un rectángulo con Mayús + arrastrar sobre la cuadrícula. Para empezarlo sin Mayús, pon args.action = 'RectangleSelect' en onGridMouseDown, o llama a control.multiselect.startRectangle() antes de la siguiente pulsación. rectangleSelectMode: 'Row' mantiene el rectángulo en la fila donde empezó la pulsación. Con rectangleSelectModifiers, Mayús añade a la selección actual y Alt quita de ella; sin ninguna de las dos teclas, el rectángulo sustituye la selección.

Los callbacks son onRectangleSelecting (en cada fotograma; args.visible = false oculta el rectángulo), onRectangleSelect (cancelable, con events, start, end y resources) y onRectangleSelected.

Seleccionar filas

rowClickHandling: 'Select' hace que un clic en la cabecera de fila seleccione la fila. Ctrl/Cmd alterna la selección de una fila y Mayús selecciona un rango desde la última fila pulsada. Las filas seleccionadas llevan aria-selected="true".

src/RosterRows.tsxtsx
import { useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'

export function RosterRows(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
  onNotify: (resourceIds: SuperScheduler.ResourceId[]) => void
}) {
  const { controlRef, control } = useSchedulerControl()
  const [selected, setSelected] = useState<readonly SuperScheduler.ResourceId[]>([])
  const events = useMemo(() => props.events.slice(), [props.events])

  const config = useMemo<SchedulerProps>(
    () => ({
      // A click on a row header selects the row; Ctrl/Cmd toggles, Shift selects a range.
      rowClickHandling: 'Select',
      onRowSelected() {
        setSelected(this.rows.selection.get().map((row) => row.id))
      },
    }),
    [],
  )

  return (
    <>
      <button
        type="button"
        disabled={selected.length === 0}
        onClick={() => props.onNotify([...selected])}
      >
        Notify {selected.length} technicians
      </button>
      <button
        type="button"
        onClick={() => {
          control?.rows.selection.clear()
          setSelected([])
        }}
      >
        Clear rows
      </button>
      <SuperSchedulerComponent
        {...config}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        resources={props.resources}
        events={events}
      />
    </>
  )
}

control.rows.selection ofrece get(), add(row), remove(row), isSelected(row) y clear(); estas llamadas resaltan las filas al instante y no disparan ningún callback, y por eso el snippet restablece su propio estado después de clear(). onRowSelect (cancelable) y onRowSelected se ejecutan alrededor de cada selección hecha con un clic.

La prop selectedRows contiene los ids de los recursos seleccionados. Pásala para empezar con una selección; el control actualiza ese mismo array a medida que cambia la selección.

Seleccionar rangos de tiempo

Arrastrar sobre celdas vacías selecciona un rango de tiempo y llama a onTimeRangeSelected una vez, al soltar, con start, end (exclusivo), resource y origin: 'drag'. Otros orígenes:

  • 'click': un clic simple en una celda vacía selecciona esa celda;
  • 'keyboard': con el teclado activado, Intro sobre una celda con foco, o Mayús+flecha izquierda/derecha para ampliar un rango;
  • 'api': control.selectTimeRange(start, end, resource).

Comprueba args.origin cuando solo un arrastre deba crear algo. La selección sigue dibujada después de soltar, hasta la siguiente selección o hasta control.clearSelection(). Un clic en otro sitio también la borra, salvo con timeRangeSelectedHandling: 'HoldForever'; 'Disabled' desactiva la selección de rangos.

onTimeRangeSelecting se ejecuta en cada cambio durante el arrastre. Pon args.allowed = false para rechazar un rango (la sombra se marca y al soltar no se selecciona nada), o reescribe args.start y args.end para acotarlo. Las celdas deshabilitadas y los padres bajo treePreventParentUsage rechazan las selecciones sin necesidad de código.

Varios rangos a la vez

Con allowMultiRange, un nuevo arrastre puede añadir un rango en lugar de sustituir el actual: mientras se mantiene pulsado Ctrl/Cmd (multiRangeMode: 'CtrlOrMeta', el valor por defecto) o siempre ('Always'). args.multirange enumera todos los rangos seleccionados, y control.multirange los lee o los cambia: get(), clear() y add(new SuperScheduler.Selection(start, end, resource)).

src/RangeBlocker.tsxtsx
import { useMemo, useState } from 'react'
import { SuperScheduler, SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps } from 'super-scheduler'

export interface BlockRequest {
  readonly resource: SuperScheduler.ResourceId
  /** ISO wall-clock values; `end` is exclusive. */
  readonly start: string
  readonly end: string
}

export function RangeBlocker(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
  onBlock: (requests: BlockRequest[]) => void
}) {
  const { controlRef, control } = useSchedulerControl()
  const [ranges, setRanges] = useState<readonly SuperScheduler.SchedulerRange[]>([])
  const events = useMemo(() => props.events.slice(), [props.events])

  const config = useMemo<SchedulerProps>(
    () => ({
      // Ctrl/Cmd + drag adds a range instead of replacing the current one.
      allowMultiRange: true,
      multiRangeMode: 'CtrlOrMeta',
      onTimeRangeSelecting: (args) => {
        // Past days cannot be blocked: the shadow turns red and the drop is refused.
        args.allowed = args.start.getTime() >= SuperScheduler.Date.today().getTime()
      },
      onTimeRangeSelected: (args) => {
        // A plain click on an empty cell also selects it (origin 'click').
        setRanges([...args.multirange])
      },
    }),
    [],
  )

  const block = () => {
    props.onBlock(
      ranges.map((range) => ({
        resource: range.resource,
        start: range.start.value,
        end: range.end.value,
      })),
    )
    control?.clearSelection()
    setRanges([])
  }

  return (
    <>
      <button type="button" disabled={ranges.length === 0} onClick={block}>
        Block {ranges.length} selected {ranges.length === 1 ? 'range' : 'ranges'}
      </button>
      <SuperSchedulerComponent
        {...config}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        resources={props.resources}
        events={events}
      />
    </>
  )
}

Arrastra sobre días libres de una habitación y después de otra manteniendo pulsado Ctrl (Cmd en macOS): el botón muestra «Block 2 selected ranges». Los rangos llegan a onBlock como valores ISO de reloj con finales exclusivos, listos para enviarlos a tu backend.

De qué es dueña tu aplicación

La librería mantiene los árboles, los filtros y las selecciones en el navegador, durante la sesión actual. Tu aplicación decide:

  • de dónde sale el árbol: las filas son los resources que pasas, y el control nunca pide hijos por su cuenta (onLoadNode está reservado);
  • qué significa un filtro cuando los datos no están cargados, y cómo llega la búsqueda al backend;
  • qué hace una selección: las acciones en bloque, los permisos para ejecutarlas y su persistencia;
  • si las filas desplegadas y la disposición de las columnas sobreviven a una recarga (consulta las vistas guardadas).

Las selecciones no se guardan en ningún sitio. El estado de despliegue vive en los objetos de recurso que pasaste hasta que los sustituyas.

Planning de habitaciones de hotelGotea la ducha de la 104. Recoloca al siguiente cliente, bloquea la habitación para el fontanero y localiza las noches que ya están llenas. Planificación de aulas de formaciónLa matrícula superó el aula. Selecciona ambas sesiones, mira qué está libre para las dos, muévelas juntas y conserva la vista. Despacho de servicio técnicoEntra un trabajo urgente en la cola. Encuentra la cuadrilla que puede atenderlo a tiempo.