Repository navigation
Releases: SalvadorCardona/react-resource-view
Release list
v1.0.0
Major Changes
-
60d41c1: 1.0: a declaration that reads once and makes sense without the documentation.
Breaking changes, each one rewritten bynpx react-resource-view migrateor
explained step by step inMIGRATION-1.0.md;migrations/1.0.jsonis the same
list as data, for a script or an AI to apply.- One place per notion.
viewis gone: what every action shares —form,
fullWidth,titleField,components,description— sits at the root of
the resource; what belongs to one action sits inviews.list | read | create | update | delete, each typed with only the options that have an effect there.
Root, action and layout are merged deeply, soviews.updatesays only what
differs, andinputs: { notes: false }removes an inherited field. Actions
are plain strings (views.update,rowActions: ["read"]);ActionListis
still accepted. - Permissions.
canRead,canList,canCreate,canUpdate,canDelete
andbehavior.canExportbecome onepermissionsobject —"all",
"readonly", a list of actions, or a boolean or a function per action.
Everything is allowed by default, so the shortest declaration has all its
buttons. A function is told the record (({ item }) => item.status !== "done")
and may answer asynchronously; answers are cached per record while a list is
on screen. A refused action also protects its route:/books/update/1draws
the scope'sforbiddenFallbackinstead of the form.canListis now read for
the list (it was ignored).usePermission(action, item)and
useAllowedActions(actions, item)are added;permissionResourcetakes
{ item, context }and may return a promise. behavioris gone, each option moved to where it has an effect:
openIn→views.<action>.open("page","dialog","drawer");
closeAfterUpdate,refreshDataAfterUpdate,redirectToAfterUpdate→ one
afterSave: "close" | "stay" | "reload" | { redirect: (item) => string };
rowActions→views.list.rowActions;eventSourced→realtimeat the
root;openRecordIn→views.read.open;selectable,bulkActions→
views.list.selectable,views.list.bulkActions, for every layout of the
list (bulkActionsalso takes"delete").- Layouts.
viewVariantsbecomesviews.list.layouts, and the seven
factories onelayoutnamespace:layout.table(),layout.cards({ columns }),
layout.board({ groupBy, columns }),layout.calendar({ dateField }),
layout.timeline({ startField, endField, groupBy }),layout.split({ readInList }),
layout.items()andlayout.custom()for one's own (read its options with
useLayout()). Field options are checked against the resource's item type —
a typo is a TypeScript error — and share one suffix:titleField,
dateField,colorField,statusField. A layout's URL id is its type, so a
new label breaks no shared link;fieldspicks a layout's columns. - Data. Without
path,storage: "local"must be declared: a forgotten
path no longer turns into localStorage silently.preGetCollection,
preGetItem,preCreate,preUpdatebecomehooks.beforeList,
beforeRead,beforeCreate,beforeUpdate, joined bybeforeDelete,
afterSaveandafterDelete; hooks may be asynchronous and always receive
the view context — creations and updates used to lose it on the way to the
repository. Hand-written repository methods move torepository: { … }.
limitbecomesquota: { get, fallback }(useLimit→useQuota). - Titles and components.
nameis declared once; titles are derived and
overridden withviews.<action>.title, a button's text withbuttonLabel.
viewComponentand the otherxxxComponentkeys, anddecoratorComponent,
become onecomponentsobject (view,wrapper,top,bottom,empty
— exnoResult—,pagination,navigation,title,actions). - Tabs.
subViewResourcebecomesviews.read | update.tabs: { orientation, aside, items }, a tab being{ id, label, resource, filter: ({ item }) => … }
instead of anonInitViewResourcereturning the whole context. - Scopes and defaults. A resource no longer names its
scope: the scope's
resourcesties them, andmenu: "auto"(or a list mixing resources, links
and groups) builds the navigation.setResourceConfig({ defaultResource })
becomesconfigureResourceView({ defaults }), merged deeply into every
resource, including those declared before it is called. - Types.
ResourceDefinitionis what one writes,ViewResourcewhat the
library builds;ViewResourceInterface,ResourceOptionInterface,
ViewInterfaceand the factory option interfaces are replaced. - Optimistic writes. Every write going through a resource's repository —
a form saved, a row deleted, a cell edited in place, a card moved, the
writes of a bulk action made through its newmutationargument — is drawn
before the API answers: the row changes at once (a created one appears,
marked as being saved), the API's answer replaces it as soon as it comes,
and a refusal puts it back with a toast — a form keeps the API's violations
on its fields. Several writes to one record show the last one; a list read
again while a write is in flight (a realtime push) keeps the row as
written. On by default — a change of perceived behaviour —, turned off with
optimistic: falseat the root or per action
(views.delete: { optimistic: false }).useResourceMutation(resource)
gives an application's own actions the same optimism and rollback
(create,update,remove,mutate,isPending), andRowInterface
gainspending. Built on the library's own state rather than React 19's
useOptimistic: no new requirement on the React version. - One routed opening.
views.<action>.openis resolved once, and anything
but"page"is written into the URL under the view it was opened from
(…/tasks/list/tasks/update/12, filter and layout kept): a row's button —
now a real link —, a board's card and a link typed by hand open the same
frame, Back closes it, a reload opens it again over the same list, and a
save closes it by going back to that URL. A list reads itself again once per
save, instead of once per button of its rows.layout.board({ openRecord, expandable })is gone: a card opens the record'sreadview where
views.read.opensays — on its page by default, where it used to open
nothing. - Frames of one's own.
open.custom({ type, placement, component }), on
the model oflayout.custom(): the component receives the title, the view,
closeand the link to the full page.placement: "inline"draws the view
under the row that opened it in a table, a plain list or a card grid — a
layout.custom()says it can withsupports: ["inline"]and draws
<InlineChildView row={row} />— and falls back to a dialog elsewhere, with
a warning in development.configureResourceView({ open: { accordion } })
registers a frame under a name, typed through theOpenFramesinterface. - "Open in full page". A
readorupdateview opened in a frame has a
link to the same view on its page beside the close button, a real
<a href>that Ctrl/Cmd+click opens in a new tab.expandable: falseon
the view takes it away;createanddeletedo not have it unless they say
expandable: true. - Checked in development.
createViewResourceandlayout.*throw on a key
they do not read — with the closest key, or the exact 1.0 rewrite of a 0.x
one — on a resource with no data source, a board withoutgroupBy, two
layouts sharing a URL id, and warn about a title identical to the derived
one. Nothing is checked in production. - Command line.
npx react-resource-view migrate [paths]rewrites 0.x
declarations (--dry-run,--check);create-view-variantwrites a
layout.custom()file;initstarts on the 1.0 syntax.
- One place per notion.
Minor Changes
-
29ce470: feat(views): a list sorts by its column headers, selects its records in every layout and says when it could not load, not only on the board and not with an empty list
views.list.sort: { fields, initial }: the table's headers of the fields named sort ascending, then descending, then not at all (aria-sort); the sort goes to the API through the dialect, into the URL, and survives "Clear the search".useSort()gives the same to a layout of one's own.- Breaking (1.0):
selectableandbulkActionsleavelayout.board()forviews.list, and reach every layout: checkboxes on the table's rows (and "Select all" in its header), the cards and the items.bulkActionsaccepts"delete", which deletes the selection after a confirmation, as far aspermissions.deleteallows.selectabledefaults totrueon a board and wherever bulk actions are declared.npx react-resource-view migraterewrites both (RV1-BEHAV-006, RV1-LAYOUT-014). - A view whose records could not be loaded draws what the API answered and "Try again" (
components.errorreplaces it), rather than "An error occurred" in a list, or an empty form that could be saved over the record.loadErroron the view context holds what the request threw.
-
e7e7654: Moving to another view shows that the click was heard, instead of nothing until
the next view appears at once: a thin bar runs along the top of the screen, in
the theme's primary colour, until the new view has its data, and the view fades
in, sliding up a few pixels. No new dependency.- The bar waits 150 ms, so an instant navigation flashes nothing, then stays
300 ms at least; it runs quickly, then slower, and fills up when the view is
there. The loading view is markedaria-busy, the b...
- The bar waits 150 ms, so an instant navigation flashes nothing, then stays
v0.15.0
Minor Changes
-
b1f8272: Open a card of a board, select several and move them together, instead of only
dragging one card at a time onto a column.behavior.openRecordIn("drawer","popup"or"window") makes the whole
card clickable — hover and focus states, Enter from the keyboard — and opens
the record'sreadview: in a panel on the right or a dialog, both written
into the URL as a child view of the list (…/tasks/list/tasks/read/12, which
parseLinknow reads), so the link is shareable and Back closes it; or on
the record's own page, throughgenerateLink. The panel has a way to edit the
record, traps the focus and gives it back to the card.- A button, a link, a menu, a checkbox or a drag handle drawn in a card keeps
its click, without anystopPropagation: the card only opens on a click that
lands on none of them (data-card-controlmarks anything else), and not when
text is being selected. - Cards can be selected — checkbox, Ctrl/Cmd+click, Shift+click for a range, a
long press on a phone, Escape to clear — and a bar offers "Move to…",
behavior.bulkActionsand "Deselect all". Dragging a selected card moves the
whole selection under an "N cards" preview. Each record is updated through
the repository and drawn in its new column at once; one that fails goes back,
and a message says how many could not be moved.behavior.selectable: false
turns selection off. - Each column scrolls on its own, and an empty column shows where to drop.
-
d970551: Add a Django REST Framework dialect:
djangoDialect()reads DRF's
{ count, next, previous, results }page (or a bare array when the view has no
paginator), pages withpage/page_size— orlimit/offsetwith
pagination: "limit-offset"— sorts withordering=-field, sends filters as
query parameters for django-filter, addresses a record at/articles/{id}/with
DRF's trailing slash, and pins a 400's{ field: ["message"] }on its input.
Options:primaryKey,pagination,pageSizeParam,defaultItemsPerPage,
orderParamandtrailingSlash.npx react-resource-view init -b djangostarts a back office on it, dumping the
schema drf-spectacular serves at/api/schema/?format=json.
Patch Changes
- 9582599:
initwrites thereact-data-form,react-mini-i18n,resource-registryand
@tanstack/react-routerranges from the package's ownpeerDependencies, so a
generated starter installs without--legacy-peer-depsinstead of failing with
ERESOLVEon a hard-codedreact-data-form@^0.2.1against a^0.1.0peer. - 35a40a9: A record saved from a button opening over a list (
openIn: "popup"or
"drawer") reads the list again with the filter it has now, instead of the one
the button mounted with — a list filtered on « A », then on « B », no longer
shows the rows of « A » under « B » after a save. The buttons of every row also
share a single request instead of each reading the list again.
v0.14.0
Minor Changes
-
642aac2: Add a FastAPI dialect:
fastapiDialect()lists, reads, creates, updates and
deletes against a standard FastAPI CRUD router, instead of showing empty lists
under the JSON-LD default.A page is
skip/limit, as in the FastAPI tutorial, orpage/sizewith
pagination: "page-size"for fastapi-pagination, whose{ items, total }
drives the page count; a bare array hides the pagination. A sort is
order_by=-created_at,title, an array filter repeats its parameter, and a 422
lands under the field itslocnames —["body", "address", "city"]on
address.city. Options:primaryKey,pagination,defaultItemsPerPage,
orderParamandtrailingSlash. -
5dfb920: Add
npx react-resource-view init: it starts a back office on your API — API
Platform, Spring Boot, another REST API, Supabase, Strapi or FastAPI — with the
dialect wired, the URL in.env, andapi-schema/api-schema:checkscripts
that dump the OpenAPI schema into TypeScript with api-dumper.Add a Spring Boot dialect:
springDialect()pages withpage(from 0) and
size, sorts withsort=field,desc, reads aPage, aPagedModelor Spring
Data REST's HAL, and pins Bean Validation errors on their field.
Patch Changes
- ef36d4e: Capitalise the first letter of each view name in the view switcher, so a
boardorvue d'ensembletab readsBoardandVue d'ensemble, custom and
translated names included — the rest of the label is left as written.
v0.13.0
Minor Changes
- 1a4b81b: Recognize a 401 or a 403 thrown by a scope's
authorizationfrom itsstatus,
not only fromUnauthorizedErrorandForbiddenError. Any error carrying
status: 401callsonUnauthorized, any error carryingstatus: 403renders
theforbiddenFallback: an auth library no longer has to import the classes,
and two copies ofreact-resource-viewin one bundle still agree. No other
status is recognized, and errors recognized today behave as before.
Patch Changes
-
6cd82c4: Save a card moved to another column of a board kept in the local repository,
instead of answering "Une erreur est survenue" and putting the card back.The drop sent only the short identifier of the record (
12), which the local
repository compared to the@idit keys records on (/tasks/12) and never
found. It now finds the dropped row again and sends the whole identity the
dialect reads it by —@idandidfor JSON-LD,documentIdfor Strapi, the
primary key for Supabase — so every repository addresses the record as before.
A card dropped back into its own column sends nothing.The error a failed update shows is now the translatable key
An error occurred,
also used when a list or a calendar fails to load, rather than a French sentence.
Applications translatingUne erreur est survenueshould add the new key. -
1d323a6: Keep a column of the columns view at its width when one of its cards holds a
long unbreakable word (a path, a URL, an identifier). As a flex item, the
column was never narrower than its widest content: once a parent sized the
columns, it grew around the word and slid under its neighbours. The column is
nowmin-w-0and the content of a record cardbreak-words, so the word wraps
inside the card instead. -
29e5142: Keep the horizontal sub-view bar inside the page and level with the side column when
subViewResourcehas one, instead of pushing the page sideways and starting 4rem below the column.The sub-view track was a bare
1fr, which is never narrower than what it holds —
and the bar is as wide as all its tabs, a table in a tab as wide as its columns.
The track is nowminmax(0, 1fr)and the tabs containermin-w-0, so the bar
scrolls inside the width left beside the column, as it already did without one.The bar was also meant to stay pinned under the top bar while a long sub-view
scrolls, but the scroll area's root setsposition: relativeinline, which beat
itsstickyclass: the bar never stuck, and itstopoffset moved it down
instead. The bar is now pinned from a wrapper of its own. -
47c7df9: Keep the horizontal sub-view tab bar within its column when
subViewResource.viewComponentdraws a left column beside it. The tabs' grid
track grew to the width of the whole row of tabs, so the bar never scrolled:
the last tabs were cut off and the page itself overflowed to the right. The bar
now scrolls sideways as it does without a left column; the vertical menu and
records without a left column render as before.
v0.12.0
Minor Changes
-
1b184f0: Let a scope's
authorizationwait for a real session —authClient.getSession(), a/mecall — instead of having to answer synchronously, and keep the scope hidden until it has.authorizationmay now returnboolean | Promise<boolean>; a synchronous
function works as before. Nothing of the scope renders while it is pending:
authorizationFallbackshows instead, the page loader by default. An
UnauthorizedError, thrown or rejected, still callsonUnauthorized. A
ForbiddenError, thrown or rejected, or afalseresult, now renders
forbiddenFallback— an "Access denied" message by default — where a
ForbiddenErrorused to reach the error boundary and afalsewas ignored.
Both fallbacks are accepted on the scope and on the provider configuration,
the scope's winning.The check runs again when the scope changes, and on demand through
recheckAuthorization()fromuseScopeContext(), so a sign-out is taken into
account without a reload. An answer that arrives after the scope changed is
ignored. -
dcd05f1: Pin a block at the bottom of
AdminLayout's sidebar — "Need help?", support, documentation — instead of leaving its footer empty with no way to fill it.AdminLayoutandcreateAdminLayoutnow takefooterMenu, menu entries drawn
like the scope'smenuunder an optionalfooterMenuTitle, and
sidebarFooter, any node rendered below them. An entry whosehrefis
absolute leaves the application: a web page opens in a new tab, amailto:
hands over to the mail client. On narrow screens both go behind a last entry of
the bottom bar, named afterfooterMenuTitle("More" without one), which opens
them in a drawer. Without either option the sidebar renders no footer, as
before.
Patch Changes
-
2f8da83: Leave out the foot of a record card — its separator and the padding under it — when the resource permits none of the row's actions, instead of ending the card on a stray line above an empty strip.
The card layout now reads which actions a row offers (
behavior.rowActions,
thencanUpdate,canDelete…) before drawing their bar, as the calendar and
timeline preview already did. A card with at least one permitted action is
unchanged.
v0.11.0
Minor Changes
-
509f297: Name the record on screen in
AdminLayout, instead of an empty top bar above a bare "Edit a user".The top bar now carries a breadcrumb — the scope's
label, the resource, the
record — where it used to hold nothing but the sidebar toggle. On a record, the
page header links back to the list, takes the record's name as its title (read
fromtitle,nameorlabel, or from the view's newtitleKey), says the
action and the id underneath, and offers the actions still worth having there:
edit from the read page, delete from either — deleting goes back to the list.A view overrides the title with
components.titleand the actions with
components.actions;components.navigationstill replaces the whole header. -
e9b26ae: Give a board, a calendar or a wide table the whole page in
AdminLayout, instead of the column every view was squeezed into.A view now takes
fullWidth. Set on a resource'sview, it covers every action
of the resource; on one ofviews, that action; on a list variant built by a
*ViewOptionFactory, that variant only. The most specific declaration wins, so
a resource drawn full width keeps its forms narrow with
views: { update: { fullWidth: false } }, and a board can be the one wide
layout of a list whose table stays in the column.AdminLayoutdrops its width
constraint for such a view and keeps its side margins; nothing changes for a
view that does not ask.isFullWidthViewresolves the option for the view on screen, for a
decoratorComponentof one's own that wants to honour it too.
Patch Changes
-
caa2c75: Light the sidebar entry of the page
AdminLayoutopens on, and unfold the group of the page on screen, instead of leaving the reader to find either.A scope opened bare —
/playground,?view=admin— shows the page its
defaultViewResourceContextParamsname, but its URL names none, so no entry
was lit until the reader clicked on one.useIsActiveItemMenunow takes that
default view, andAdminLayouthands it the scope's.A sub-entry on screen now unfolds its group, even one the reader had folded on
an earlier visit, and marks it with the accent colour, without the background
the page itself carries. The entry on screen also carriesaria-current="page". -
0b22ae2: Fold the admin sidebar away, and keep a menu entry lit on the page it points at.
Two things
AdminLayoutgot wrong on the screen it is mostly read on. The top
bar's button only ever opened the mobile drawer, so on a wide screen — the one
place it is shown — pressing it did nothing; it now folds the sidebar column
away and back.And a menu entry was matched against the address bar as a string, which held
only while the URL was exactly the one the entry was built from: opening a
record of a resource turned its entry off, and in query mode an application
whose views are mounted somewhere other than the configuredbasePathnever
lit any entry at all. The comparison is now made context by context — scope,
resource, record — so an entry pointing at a list stays lit for everything
inside that list, while two entries on the same resource (its list, its
creation form) stay distinguishable.
v0.10.0
Minor Changes
-
a52d64d: Give a scope a ready-made admin shell instead of writing one from scratch.
Every project reached for the same
decoratorComponent: a collapsible sidebar
built from the scope'smenu, a top bar, a page header with sub-navigation
tabs, and a bottom bar on narrow screens instead of the sidebar.AdminLayout
is that shell, exported so a scope only needs to point at it:const adminScope: ScopeInterface = { name: "admin", decoratorComponent: AdminLayout, menu: [createItemMenuWithResource({ resource: articlesResource })], }
createAdminLayout({ logo, topBarEnd })builds a configured variant when the
host wants a logo or actions in the top bar —decoratorComponentonly ever
receiveschildren, soAdminLayoutitself takes no props there.
v0.9.0
Minor Changes
-
fa4c785: Read the sub-views of a record without three rows of buttons in the way.
The tab bar wrapped onto as many lines as the tabs needed, and cut every label
to twelve characters belowmdto limit the damage — a record with eight
sub-views pushed its content off a phone screen before a word of it was read.
The bar now stays on one line and scrolls sideways, labels whole.subViewResource.orientation: "vertical"is the other shape: the tabs go down
a column beside the sub-view, which is what a record with a dozen of them
wants — the whole menu is read at once, and it follows the reader down the
page. Belowmdthe column would leave the sub-view a third of the screen, so
it falls back to the scrolling bar there; the same declaration reads both ways.
v0.8.0
Minor Changes
-
7120675: Open a small window on a calendar event or a timeline bar.
Clicking one used to print every key of the record —
@context,@idand the
dates already written on the event included — which is a data dump rather than a
preview. What opens now names the event, dates it in one line, summarises it in
at most five fields labelled the way the resource'sform.inputslabels them,
and hands over to the row's actions for the rest.The summary is the variant's
rowComponent, which both layouts now default to
PreviewRowComponentinstead ofDumpRowComponent: a resource with something
better to show still declares its own, exactly as in the card and list layouts.
Patch Changes
-
f974887: Keep a list header on one line when its description is long.
The header laid the name, the layout switcher and the create button side by
side, each free to wrap on its own. A resource introduced by a full sentence
took the whole width for it, and the create button was pushed alone onto the
line below, floating under the tabs.The text now yields instead of pushing — it is the flexible part of the line —
and the switcher, the export and the create button form a single bar, so the
width that does run out takes the whole bar down, never one button of it.
v0.7.0
Minor Changes
-
ec43323: Open an action in a drawer.
behavior: { openIn: "drawer" }joins"popup"and"window": the view slides
in as a panel rather than replacing the page or sitting in the middle of it.
From the right on a desktop, up from the bottom below themdbreakpoint —
where a thumb reaches — and swiped away towards the side it came from.The panel is as tall as the screen, which is what a long form wants: a record
with a dozen fields is filled in without the list behind it going anywhere. Like
a dialog, it closes itself on the resource'sonChangerather than navigating
after a creation. -
089e9df: Give a list a header of its own.
The create button, the export button and the layout switcher used to be stacked
above the rows as three loose controls, and the resource'sicon— already used
by the menu and by the sub-view tabs — appeared nowhere on the screen it belongs
to. They are now one line: the icon, the view'snameand itsdescriptionon
the left, the layout switcher beside them, the actions on the right.Nothing is declared for it: a resource that names no icon simply shows none, and
a list nested inside another view — a sub-view tab — leaves the naming to
whatever contains it. The filter bar is untouched. -
32cac69: Add a command that scaffolds a view variant.
npx react-resource-view create-view-variant Heatmap --dir src/viewswrites one
file — the list, row and item components, the factory that declares them and the
options interface to extend — where the project keeps its views. Run bare, it
asks for the name and the directory;--icon,--jsx,--force,--dry-run
and--yescover the rest.Nothing about declaring a variant changes: the generated factory goes into
viewVariantsbeside the built-in ones.