Skip to content

About

A sortable, filterable table over any Dataverse view, with inline editing, pinned columns and a New button.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Data Table

A sortable, filterable table over any Dataverse view, with inline editing, pinned columns and a New button.

Reference example · built with AI. This control was written with AI (Claude) and tested on a live Dataverse form; its code has not been reviewed line by line. It is published as a worked example and is not maintained — read the source and SPEC.md (what was measured on the form) before you use it. Fixes are not guaranteed.

Build Release

Try it live on PCFHub

Documentation lives on PCFHub, built from the docs/ directory in this repository. Edit the Markdown here; the hub recompiles it.

What it does

Data Table binds a dataset — a subgrid, a view, a canvas collection — and renders it as a semantic HTML table with a pager, sortable headers and optional row selection.

The decision that shaped everything else: it declares no property-set roles. A property-set is a fixed-arity declaration, one named role per line of manifest, so a role-based table is capped at whatever N the manifest hard-codes and the maker with N+1 columns is stuck. It also cannot carry the metadata a table needs — order, visualSizeFactor, isPrimary and disableSorting exist on real view columns and nowhere else. So the control reads dataset.columns and renders what the view already says. There is nothing to configure per column, and the view designer is the configuration UI.

The second decision: paging and sorting go back through the dataset API, not through the array on screen. sortedRecordIds already is the current page on a real platform, so slicing it client-side hides records the platform paged for; and sorting the rows on screen reorders twenty-five out of two hundred and forty, which is a wrong answer that looks completely right. Both cost a round trip, and both are correct across the whole result set.

The third is a footgun rather than a design: updateView fires on every dataset change, including the ones this control caused. setPageSize() does nothing until the next fetch, so it has to be followed by refresh() — and refresh() fires updateView. The guard is on a control-instance field rather than on ds.paging.pageSize, because the platform's own value does not equal the requested one until the refresh lands. Without it, the control spins. See SPEC.md.

Two <feature-usage> entries, both required="false". Utility was declared in 0.4.0 after three releases of none, for getEntityMetadata: it buys the option list a choice column's editor and filter box are built from, which the platform hands a dataset control no other way. WebAPI followed in 0.5.0, for exactly one call — updateRecord, for lookup cells, and nothing else. Device is untouched, and openDatasetItem() and navigation.openForm() sit on surfaces no feature gates. So the prompt at import is two items long, and declining either costs only what that one buys: the choice cells, or the lookup editor.

Editing still asks nothing of the environment, which is the whole reason it is built the way it is. The write goes through record.setValue() and record.save() on the dataset record itself — measured working on a real subgrid, 2026-09-09, committed and survived a reload; measured again 2026-09-11 for a choice column, integer in and integer read back. Neither method is in @types/powerapps-component-framework@1.3.18, where EntityRecord declares four methods and none of them writes, so both are feature-detected at runtime before an editor is offered. The obvious alternative, webAPI.updateRecord, needs <uses-feature name="WebAPI" /> — an install-time prompt in every environment — and does nothing at all in a canvas app.

What that write path does not do is a lookup: setValue on a lookup column stages nothing, five shapes tried, every save() refused. So since 0.5.0 a lookup cell is the one exception — picked in the platform's own dialog (utils.lookupObjects) and written with webAPI.updateRecord through an @odata.bind key whose navigation property is read from EntityDefinitions rather than derived, because on the probe table it was the logical name and not the schema-cased one, and a Customer lookup has two. That costs the WebAPI feature and makes lookup editing model-driven only; SPEC.md 0.5.0 has the nine measurements it rests on.

The same undocumented surface answers isEditable(column) per column and per record, which is what lets the control render a cell the user cannot write as plain text instead of offering an editor and discovering the truth when the save is refused.

Related through (0.8.0) is the PCF Gallery request board's top open ask, a subgrid whose rows are related to the form's record by a path its one relationship cannot express. It is built on dataset.linking.addLinkedEntity, which nothing in the catalogue had called before. A FetchXML <link-entity> with {id} is parsed into links and conditions that name them by entityAliasName, ANDed into every filter the control sets, so the platform still pages, sorts and counts the rows. A probe build measured all of it on a subgrid first, and one answer contradicted the documentation. A dataset numbers the hierarchy operators the PCF reference's way (79 is eq-or-under), not the SDK's. SPEC.md 0.8.0 has the eleven answers.

Properties

Property Type Usage Default What it controls
records DataSet dataset — The view or collection to render. No property-set roles: the columns are the view's
↳ cds-data-set-options manifest attribute — all three on Keeps the model-driven subgrid's command bar, view selector and quick find. Design-time only; not a maker-facing property
pageSize Whole.None input (unset) Rows requested per page, clamped to 1–250. Unset adopts the host's own page size and never calls setPageSize
pageSizeOptions SingleLine.Text input (unset) A comma-separated list, e.g. 10,25,50, offered as a rows-per-page picker. Unset means no picker
enableFiltering TwoOptions input true A filter box under each text, numeric, date and — in a model-driven app — choice column heading, applied server-side
enableExport TwoOptions input false An Export CSV button covering the rows loaded so far
selectionMode Enum input single none, single or multiple
enableSorting TwoOptions input true Show sort controls on the columns the view allows sorting on
openOnRowClick TwoOptions input true Open the record on row click, as well as from the primary column
pinnedStart Whole.None input (unset) How many of the view's first columns stay put while the table scrolls sideways. Pins the select column with them
pinnedEnd Whole.None input (unset) The same at the other end. One column is always left unpinned, and pinning drops itself entirely where it would leave nothing to scroll
enableEditing TwoOptions input false Edit a cell in place and save it. Only cells the platform reports as editable are offered
editableColumns SingleLine.Text input (unset) A comma-separated allow-list of logical names. It narrows what the platform permits; it never widens it
enableCreate TwoOptions input false A New button that opens the table's quick create form. Model-driven only: canvas has no form to open
groupBy SingleLine.Text input (unset) Comma-separated logical names to group the rows by. Unset renders exactly as 0.5.0 did
aggregates SingleLine.Text input (unset) Per-group measures as aggregate:column, e.g. sum:revenue, avg:revenue. A record count is always shown
groupSort Enum input label Order group headers by their label or by their record count
parentLookup SingleLine.Text input (unset) The lookup pointing at the record a subgrid sits under. Normally unset; set it where the table has several lookups to the same table, or none where the subgrid is unrelated
relatedThrough SingleLine.Text input (unset) Rows related to the form's record by any path: a FetchXML <link-entity> with {id} for the record, inline or as the name of a Data (XML) web resource. Turn off Show related records on the subgrid. Model-driven only; unset renders exactly as 0.7.x
enableMultiSort TwoOptions input false Shift-click a heading to add that column to the sort order. Off by default, so a plain click behaves exactly as it did
exportScope Enum input loaded Whether Export CSV writes the rows already loaded or reads view — the whole view — first, at one request per page
lockColumnWidths TwoOptions input false Stop users resizing columns. Unlocked, a user drags a header's edge (or uses the arrow keys on it) and the width is remembered in their browser, per table and view
editedRecordId SingleLine.Text output — The row most recently saved by an inline edit
createdRecordId SingleLine.Text output — The row most recently created through the New button
selectedRecordIds Multiple output — Selected row IDs, one per line
openedRecordId SingleLine.Text output — The row most recently opened

Strings ship in five languages — English (1033), German (1031), French (1036), Japanese (1041) and Spanish (3082) — all carrying the same 134 keys. The four translations are machine-generated and have not been read by a native speaker; npm run check proves every key and every {0} placeholder is present in each, and proves nothing about the wording. Corrections are welcome as a patch.

React and Fluent UI come from the platform as <platform-library> entries rather than from the bundle — the build confirms it, emitting Reactv16 and FluentUIReactv940 as webpack externals.

On the hub

demo.fidelity is limited. That is now because of a few named features, not because of the table as a whole.

Since pcfhub/pcfhub#51, the hub's demo harness keeps a dataset's page, sort, filter and selection between renders. It applies them on the next fetch, as a form does, so paging, sorting and filtering work against the fixture. So does opening a group, which is a filter.

demo/records.json also carries a stand-in Dataverse: the account table, a contact table and the primarycontactid lookup between them. So:

  • an inline edit writes through and survives the next render;
  • Primary contact edits through the platform's own route: utils.lookupObjects, the relationship metadata, and webAPI.updateRecord with @odata.bind;
  • New asks navigation.openForm for a quick create form, and the harness answers with a stand-in form (pcfhub/pcfhub#64) whose save creates the row;
  • Category is a choice whose options the stand-in describes (pcfhub/pcfhub#52), read through utils.getEntityMetadata. So its cells edit through a dropdown, the filter row gives it one, and grouping by it groups on the choice. It stands where Status stood, because a form reports statecode read-only and the control keeps it that way;
  • making an account a Preferred Customer is refused, by a fault in the fixture (pcfhub/pcfhub#53), so save() rejects and the rollback runs: the cell returns to Standard and names the reason under it.

Nothing leaves the browser.

What keeps it limited is what the harness cannot answer truthfully:

  • Grouping always takes the browser route. The stand-in Dataverse refuses FetchXML.
  • openDatasetItem opens nothing. The event log names the record instead.

Each is named in demo.limitations.

There are five presets over the 24-row account fixture. Filtering and export shows 10 rows a page, so its pager has three pages. The others show 25, which is one page.

Install

Download the managed solution from the latest release, or from the component's page on the hub, and import it into your environment.

Develop

npm install
npm start          # the PCF test harness
npm run build
npm run lint
npm run check      # what CI runs first: placeholders, pcfhub.json, control shape
npm run smoke      # assertions against the built bundle, with an exit code
npm run preview    # serves dev/preview.html — the control in a real browser

Run npm run refreshTypes after every manifest edit — until you do, context.parameters is typed from the old manifest and tsc will accept code that cannot work.

To pack the solution locally you need msbuild — either Visual Studio or the Visual Studio Build Tools:

cd Solution
msbuild /t:build /restore /p:configuration=Release

Both zips land in Solution/bin/Release. This is the only local step that compiles in production mode, so a green npm run build is not evidence the shipping bundle compiles — and the pack is incremental, so delete obj/, out/, Solution/obj/ and Solution/bin/ first if you intend to quote a bundle size from it.

Release

  1. Bump the version in three places, in one commit — they are checked against each other in CI:
    • DataTable/ControlManifest.Input.xml → <control version="…">
    • Solution/src/Other/Solution.xml → <Version>
    • package.json → "version"
  2. Tag it: git tag v1.2.3 && git push --tags

The release workflow builds, packs both solution types, and attaches them to a GitHub Release. PCFHub picks the release up from its webhook within seconds, or from the hourly sweep otherwise. A sync imports a draft; a person publishes it.

Repository layout

Path What it is
DataTable/ The control: manifest, entry point, CSS, localised strings
Solution/ The Dataverse solution that packages it
demo/ The dataset fixture the hub's demo harness runs against
SPEC.md What building this corrected, and what is verified versus read
docs/ The pages PCFHub publishes — see the comments in each file
media/ Images and video referenced from the docs
pcfhub.json The hub's manifest: identity, links, docs path, demo
scripts/ Template setup and the CI guard that keeps it adopted

Licence

MIT

About

A sortable, filterable table over any Dataverse view, with inline editing, pinned columns and a New button.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages