All notable changes to Masu are documented in this file.
The format follows Keep a Changelog, and Masu follows Semantic Versioning.
[Unreleased]
Added
Masu.Components.data_grid/1: a virtualized data grid for LiveView, with a:colslot for each column and cells rendered by HEEx.- Sources: a list of rows (
Masu.Source.List) and an Ecto query or schema (Masu.Source.Ecto), which sorts, filters, searches and counts in the database.Masu.Sourcefor your own. - Sorting by several columns, filters (
Masu.Filter) and a search box. - Columns that resize, and fit their content on a double click.
- Keyboard navigation of the cells, a selected range, and copy of the range as tab-separated text that spreadsheets paste.
- Cells formatted for the locale: numbers, decimals, dates and times.
Masu.Test, to read and query a grid inPhoenix.LiveViewTest.- A filter menu on each column (the funnel of its header, or Alt+↓):
the filters of the column's type, an enum's values with their counts
(
Masu.Source.values/5: a list in the LiveView, oneGROUP BYfor Ecto), and the filters as chips above the grid.Masu.Test.values/4. - Editing:
changeset(orupdate) on the grid,editable={false}on a column. A cell edits on a key typed, Enter, F2 or a double click, with an editor for its type; the edits of a row are saved together, through the changeset and the repo (or applied to a row of a list), and the changeset's errors go on their cells. Paste of tab-separated text, fill down, Delete, undo and redo. A row is found again through the grid's own query (Masu.Source.fetch/3).Masu.Test.edit/3. - Read-only rows and cells by their state:
row_editableon the grid, andeditableas a function of the row on a column. The server refuses an edit of a read-only cell (with the row it finds again) and saves the row's other edits; the browser gets each row's read-only cells, which open no editor and havearia-readonly. - Edits of a cell that changed meanwhile are refused: each edit carries
the value the person saw (when the editor opened), compared on the
server by the column's type, a compare-and-swap of each cell; the cell
then shows its value now. An Ecto source compares and saves each row in
a transaction that locks it (
SELECT ... FOR UPDATEon PostgreSQL and MySQL), and takesoptimistic_lock/3'sEcto.StaleEntryErroras the same refusal.Masu.Test.edit/3takes{id, key, value, before}. on_save: a function called with the saved rows, in the LiveView's process, for an audit, a flash or the LiveView's own assigns.- Collaboration (
Masu.Presence):presenceanduseron a grid withlive. The others who have the grid open show as avatars (a:personslot draws them), each one's cell as a line in their color with their name (by row id and column key, whatever each person's order and filters), a cell someone types in as "Ada is typing" and locked for the others, and the rows they save flash in their color.configure/1of the bundle draws the labels; CSS for the rest. - Following a person: a click on their avatar shows their view (order,
filters, search) and keeps their place in it on the screen; Stop brings
back one's own view and place, and taking over (scrolling, clicking,
typing) keeps theirs, with "Back to your view".
share_view={false}keeps a person's view to themselves; whoever follows them stays in their own order, and the grid finds the person's row in it (Masu.Source.locate/3, arow_number()window for Ecto;Masu.Test.place/3). Each person's selected range shows to the others in their color. A refused edit names who changed the cell: "Changed by Ada meanwhile." - Export (
Masu.Export):exportadds an Export menu, CSV and XLSX (written with no library, zipped as it is written) of every row of the query, downloaded by the browser from the routemasu_exportsmounts (Masu.Router) as the rows are read, with a link good once, for five minutes; nothing of the file goes over the LiveView's socket.Masu.Live.export/1gives an app the export to write or stream anywhere. Selection:selectableboxes, Shift ranges, Ctrl and Space, and every row of the query at once, whichMasu.Live.selected/1gives as anEcto.Query;on_select. An Ecto source reads the next block by key, not byOFFSET.Masu.Test.export/4,export_url/3,select/4,select_all/3andselection/2. - Theming to match an app's own look: tokens for the grid's box
(
--masu-grid-border,-radius,-shadow), the header (its height, type, padding, border or shadow, hover), the rows (background, every second row's, the line under each) and a cell's hover;.masu-row-evenon every second row; a row's hover over its own background;density(:compact,:normal,:comfortable); a dark palette under adarkclass ordata-theme="dark"; Tailwind in the Theming guide, and Lotus Web's results table as its worked example, compared pixel for pixel in e2e. - Slow sources:
load={:async}reads a source in a task (a new query's first rows and count, a block, a page, a count): the LiveView answers meanwhile, the grid says "Running…" with Stop and keeps its rows,timeoutstops a long read, and a source's error (a raise, or{:error, reason}fromrows/3,Masu.Source.Error) shows in the grid with Run again.load={:inline}, the default, reads as before. - Controlled grids: with
on_query, the LiveView runs its own queries; the grid shows therowsit is given (withtotalandloading), and reports the person's sort, filter, search, page and view. One sort at a time withmulti_sort={false}.Masu.Testwaits for tasks. - Columns not known in advance, as a query tool's results: rows as
lists of values (
field={2}reads a place of a list),type={:auto}(the type of the values of the first block of rows),columnson a grid with no:colslots, andMasu.Source.columns/1. Rows of other columns keep the person's view of the columns there still and name the rest (Masu.Query's sorts, filters and groups too); an order the person did not change follows the new columns.Masu.Column.infer/1. - The filters a source runs: the optional
Masu.Source.filters/2gives the operators a source filters a column by. The filter menu, the chips, the menu of a cell, a view's params andMasu.Test.filter/3offer only those, and the server refuses the others (a link's or a saved view's are dropped and named). Masu runs "between" as "at least" and "at most" for a source without it; an enum without "is one of" filters by one of its values.Masu.Filter.ops/1. The Sources guide maps each operator to SQL. - Menus of your own (
Masu.Action)::header_actionitems in each column's header menu, and a menu on each cell, opened by a right click, the context-menu key or Shift+F10, as an ARIA menu.cell_menugives it copy, copy with headers, filter by the value, exclude it, empty and not empty, and sort;:cell_actionadds the app's items. An item runs a function (on_action), sends the LiveView{Masu.Action, name, context}(event), runs a JS command in the browser (js, after amasu:actionevent), or opens a panel of its content under the header or the cell; for some columns (columns,types,show). The context has the column, and the cell's row and value read from the source, and the range it is in. The server runs only the items the grid offers.Masu.Test.actions/4,header_action/4,cell_action/6andclose_panel/2. - Row ids:
row_id={:position}knows rows by their place in the query, for a read-only grid of rows with no id; a grid that needs no ids does so for a row without one. A grid whose features need ids that stay (editing, new and deleted rows, selection, live rows, presence, details, trees) refuses:position, and a row without an id, saying which. - Reads:
block_size(100) is the rows of one read as the person scrolls, andmax_block(500) the most of one read; a page (paging={:pages}) is one read, up tomax_block, and the next page is not read ahead. The first render reads one block, which the browser keeps. - Counts and pages:
count={:deferred}counts after the first rows, in a task;count={:none}counts when the person goes to the end. A source'scount/1may give{:at_least, n}or:unknown, androws/3{rows, count}: the grid scrolls over the rows it read and grows,aria-rowcount="-1", "1,000+ rows". An Ecto source counts only when asked.paging={:pages}andpage_size: a page at a time, with a pager, the page in the view and the URL.Masu.Test.count/2,ask_count/2,page/3. - New and deleted rows:
create(orchangesetwithstruct) adds a "+ New row" row, Ctrl+Enter and Insert; a new row is a draft until the person leaves it, with its errors on its cells; a paste past the last row offers to add the rest.deletable(Repo.delete, or out of a list; a function of the row says which) ordeletedeletes rows with Ctrl+Delete or from the selection bar, after a question; Ctrl+Z brings them back (restorefor a grid withdelete). New and deleted rows go to the grid's topic;on_delete.Masu.Test.add_rows/3,delete_rows/3,restore_rows/3. - Editors: an enum edits with a list of its values (
values, or a schema'sEcto.Enumfield's), which narrows as the person types;decimalson a number column; a value the server would refuse (text in a number, a day that is not one) is caught in the browser. - Groups and totals (
Masu.Group):aggregateon a column (:sum,:avg,:min,:max,:count,:count_distinct, or a function of a list's rows) and a row of totals at the bottom;group_byand "Group by" in the header menu, with a chip, as part of the view. Groups open on groups and rows as a tree does, with their counts and totals, and sort by value or by a total. An Ecto source groups and totals in the database, aGROUP BYfor each level that shows, and reads an open group's rows a block at a time.Masu.Test.group/3,groups/3andtotals/2. A filter on a hidden column keeps its chip. - Trees and details (
Masu.Tree):tree={:parent_id}shows rows in a tree (atreegrid), opened and closed with their arrow, →, ← and*, as one list of the rows that are open: a tree of twenty thousand rows scrolls as a list does. Siblings sort among themselves, and a filter keeps its matches and their ancestors. Both sources take it; the Ecto source reads the children of a row when it opens. A:detailslot opens a row on anything rendered by the server under it (anotherdata_gridtoo), with Alt+→/←, Alt+↓ into it and Escape out.Masu.Test.nodes/3,expand/3,collapse/3,expand_siblings/3anddetail/4. Layout and views:
pinned={:start | :end}andhiddenon a:col; columns that move by dragging their header (or Alt+Shift+←/→), and a columns menu (columns_menu) to show, hide, pin and move them. A person's view (Masu.View: order, filters, search andMasu.Layout) goes into the URL withurl, and into a store withviews(Masu.Views:Masu.Views.Ectowithmix masu.gen.views, andMasu.Views.Memory): their last view comes back, and a views menu saves named views, shared or not. A view's parts that no longer apply are dropped, and the grid says so.Masu.Test.view/2,apply_view/3andlayout/3.- Live rows:
live={{pubsub, topic}}, theMasu.Livehook, andMasu.Live.broadcast_upsert/3andbroadcast_delete/3. A grid takes the changes of its topic into the person's query, a batch at a time; the browser keeps the scroll and the rows on the screen until the new ones come, and flashes the changed rows.