YOUSEFUL / DELPHI / COMPONENTS

TAtlasKanbanModel

Non-visual Kanban data model: ordered columns with optional work-in-progress limits, cards with stable ids, vetoable moves and a validated JSON format.

Non-visual component Palette: Atlas UI Unit Atlas.Kanban · Inherits TComponent · Product Atlas UI

Description

The model holds the board; TAtlasKanban only draws it. Put the model on a form or data module from the Atlas UI palette page, or create it in code. It needs no window, so it also works in console programs, services and tests.

Columns have a unique id, a title and a limit (0 means no limit). They are added with AddColumn and read by position with ColumnCount, ColumnID, ColumnTitle and ColumnLimit. There is no method to remove, rename or reorder a column, and no method to change a card in place; build a new board document and pass it to LoadJSON for those.

Cards have an id that is unique across the whole board, a title and an optional detail text. They are added to the end of a column with AddCard, moved with MoveCard, removed with RemoveCard and read with CardCount and Card.

Validation. Every invalid call raises EAtlasKanban and leaves the board exactly as it was: a blank or duplicate id, a blank title, a negative limit, an unknown card or column, a destination index out of range, or a card that would exceed a column's limit. A move vetoed by BeforeMove is not an error: MoveCard returns False.

Notifications. Every change (a column or card added, a card removed or moved, a board loaded) repaints every board that shows the model and then fires OnChange, once per change. Several boards may show the same model. While BeforeMove runs, the model refuses all changes with EAtlasKanban (Cannot mutate the board during a move callback); reading it is allowed.

Lifetime. Freeing the model detaches every board that shows it; each board then shows Assign a Kanban model to begin.

The model is not thread-safe. Use it from one thread, normally the main thread when a board shows it.

Members

MemberKindWhat it does
CreateconstructorCreates an empty model with no columns.
DestroydestructorDetaches every TAtlasKanban that shows the model (their Model becomes nil), then frees the columns and cards.
AddColumnmethodAppends a column.
AddCardmethodAppends a card to the end of column ColumnID.
RemoveCardmethodRemoves the card with id ID from whichever column holds it.
MoveCardmethodMoves card ID to position Index of column ColumnID.
ColumnCountmethodThe number of columns.
ColumnIDmethodThe id of the column at position Index.
ColumnTitlemethodThe title of the column at position Index.
ColumnLimitmethodThe work-in-progress limit of the column at position Index; 0 means no limit.
CardCountmethodThe number of cards in the column at position Column (a position, not an id).
CardmethodA copy of card Index in the column at position Column, as a TAtlasKanbanCard record.
ToJSONmethodThe whole board as compact JSON, on one line: {"schema":1,"columns":[{"id":...,"title":...,"limit":...,"cards":[{"id":...,"title":...,"detail":...}]}]}.
LoadJSONmethodReplaces the whole board with the one in Text.
BeforeMoveeventCalled by MoveCard, and so by every mouse and keyboard move on a board, after the move has passed its checks and before the card moves.
OnChangeeventFires after every change to the board, once per AddColumn, AddCard, RemoveCard, successful MoveCard and LoadJSON, after the boards that show the model have been told to repaint.

Constructors

Create constructor

constructor Create(AOwner: TComponent); override;

Creates an empty model with no columns. Pass nil and free the model yourself, or pass a form or data module as owner.

pascal
Model := TAtlasKanbanModel.Create(Self);

Destructors

Destroy destructor

destructor Destroy; override;

Detaches every TAtlasKanban that shows the model (their Model becomes nil), then frees the columns and cards. Call Free, not Destroy.

Events

BeforeMove event

property BeforeMove: TAtlasBeforeCardMove read FBeforeMove write FBeforeMove;

Called by MoveCard, and so by every mouse and keyboard move on a board, after the move has passed its checks and before the card moves. Allow arrives as True; set it to False to decline. The handler receives the card id and the ids of the source and target columns (the same id for a reorder inside a column), but not the target position. Inside the handler the model may be read but not changed: AddColumn, AddCard, RemoveCard, MoveCard and LoadJSON raise EAtlasKanban. An exception raised by the handler propagates out of MoveCard with the board unchanged. See TAtlasBeforeCardMove.

pascal
Model.BeforeMove := ReviewRule.BeforeMove;

OnChange event

property OnChange: TNotifyEvent read FOnChange write FOnChange;

Fires after every change to the board, once per AddColumn, AddCard, RemoveCard, successful MoveCard and LoadJSON, after the boards that show the model have been told to repaint. It does not fire for a declined or refused move, or for a move to where the card already is. Sender is the model.

pascal
Model.OnChange := BoardChanged;

Methods

AddColumn method

procedure AddColumn(const ID, Title: string; Limit: Integer = 0);

Appends a column. ID identifies the column in AddCard, MoveCard, BeforeMove and the JSON; Title is the header text. Limit is the work-in-progress limit, the most cards the column may hold; 0, the default, means no limit. Raises EAtlasKanban (Invalid or duplicate column) when ID or Title is empty or only spaces, Limit is negative, or a column with that id exists. Fires OnChange.

pascal
Model.AddColumn('active', 'In progress', 3);

AddCard method

procedure AddCard(const ColumnID, ID, Title: string; const Detail: string = '');

Appends a card to the end of column ColumnID. The board shows Title in bold and Detail below it. Raises EAtlasKanban (Invalid or duplicate card) when the column does not exist, ID or Title is empty or only spaces, or any column already has a card with that id; raises EAtlasKanban (Column work-in-progress limit reached) when the column is full. Fires OnChange.

pascal
Model.AddCard('todo', 'order-42', 'Prepare customer order', 'Pick, pack and label');

RemoveCard method

procedure RemoveCard(const ID: string);

Removes the card with id ID from whichever column holds it. Raises EAtlasKanban (Unknown card) when there is no such card. A board that had the card selected clears its selection. Fires OnChange.

pascal
Model.RemoveCard('order-42');

MoveCard method

function MoveCard(const ID, ColumnID: string; Index: Integer = -1): Boolean;

Moves card ID to position Index of column ColumnID. -1, the default, appends it to the end. For a move inside the card's own column the positions are counted without the card itself.

The checks come first, and each raises EAtlasKanban without changing anything: an unknown card or column (Unknown card or column), an index outside 0..count (Invalid destination index), or a move into another column that is already at its limit (Column work-in-progress limit reached). A move inside one column never counts against the limit. When the card would end up where it already is, MoveCard returns True at once, without calling BeforeMove or firing OnChange.

Otherwise BeforeMove is called; if it clears Allow, the result is False and nothing changes. If not, the card moves, OnChange fires and the result is True.

pascal
if not Model.MoveCard('order-42', 'active', 0) then
  ShowMessage('The move was declined');

ColumnCount method

function ColumnCount: Integer;

The number of columns. Columns are numbered from 0 in the order they were added.

ColumnID method

function ColumnID(Index: Integer): string;

The id of the column at position Index. An index outside 0..ColumnCount - 1 raises the RTL's EArgumentOutOfRangeException.

ColumnTitle method

function ColumnTitle(Index: Integer): string;

The title of the column at position Index. An index out of range raises EArgumentOutOfRangeException.

ColumnLimit method

function ColumnLimit(Index: Integer): Integer;

The work-in-progress limit of the column at position Index; 0 means no limit. An index out of range raises EArgumentOutOfRangeException.

CardCount method

function CardCount(Column: Integer): Integer;

The number of cards in the column at position Column (a position, not an id). An index out of range raises EArgumentOutOfRangeException.

Card method

function Card(Column, Index: Integer): TAtlasKanbanCard;

A copy of card Index in the column at position Column, as a TAtlasKanbanCard record. Changing the copy does not change the board. Indexes out of range raise EArgumentOutOfRangeException.

pascal
Title := Model.Card(0, 0).Title;

ToJSON method

function ToJSON: string;

The whole board as compact JSON, on one line: {"schema":1,"columns":[{"id":...,"title":...,"limit":...,"cards":[{"id":...,"title":...,"detail":...}]}]}. Columns and cards keep their order. The result can be passed to LoadJSON of any model.

LoadJSON method

procedure LoadJSON(const Text: string);

Replaces the whole board with the one in Text. The document is first built in a separate model with AddColumn and AddCard, so it gets the same validation (unique ids, non-blank titles, limits); only when all of it is valid does it replace the current board, and then OnChange fires once. On any failure the current board is unchanged.

Every field is required: schema (must be 1), columns, and in each column id, title, limit and cards, and in each card id, title and detail (which may be empty). Text that is not a JSON object, or has another schema, raises EAtlasKanban (Unsupported board schema); text longer than 4 MiB (4,194,304 characters) raises EAtlasKanban (Board JSON exceeds 4 MiB); a missing field raises the RTL's EJSONException (for example Value 'detail' not found). {"schema":1,"columns":[]} empties the board. Boards that showed a card that no longer exists clear their selection.

pascal
Model.LoadJSON(TFile.ReadAllText(FileName, TEncoding.UTF8));

Examples

Build a board, move a card and print it as JSON

Creates a model in code with an unlimited and a limited column, adds two cards and moves one. Prints {"schema":1,"columns":[{"id":"todo","title":"To do","limit":0,"cards":[{"id":"invoice-7","title":"Send invoice 7","detail":""}]},{"id":"active","title":"Active","limit":3,"cards":[{"id":"order-42","title":"Prepare customer order","detail":"Pick, pack and label"}]}]}.

pascal
uses System.SysUtils, Atlas.Kanban;

procedure PrintBoardJson;
var
  Model: TAtlasKanbanModel;
begin
  Model := TAtlasKanbanModel.Create(nil);
  try
    Model.AddColumn('todo', 'To do');
    Model.AddColumn('active', 'Active', 3);
    Model.AddCard('todo', 'order-42', 'Prepare customer order', 'Pick, pack and label');
    Model.AddCard('todo', 'invoice-7', 'Send invoice 7');
    if Model.MoveCard('order-42', 'active') then
      Writeln(Model.ToJSON);
  finally
    Model.Free;
  end;
end;

Save a board to a file and load it back

Writes the board as UTF-8 JSON and reads it back. LoadBoardFile catches both exceptions that a damaged or hand-edited file can cause; in either case the model keeps the board it had. A missing file still raises the RTL's EFOpenError.

pascal
uses System.SysUtils, System.IOUtils, System.JSON, Atlas.Kanban;

procedure SaveBoardFile(Model: TAtlasKanbanModel; const FileName: string);
begin
  TFile.WriteAllText(FileName, Model.ToJSON, TEncoding.UTF8);
end;

// Returns '' on success, or the reason the file was refused. On failure the
// model keeps the board it had.
function LoadBoardFile(Model: TAtlasKanbanModel; const FileName: string): string;
begin
  Result := '';
  try
    Model.LoadJSON(TFile.ReadAllText(FileName, TEncoding.UTF8));
  except
    on E: EAtlasKanban do
      Result := E.Message;   // wrong schema, blank or duplicate id, full column, over 4 MiB
    on E: EJSONException do
      Result := E.Message;   // a required field is missing
  end;
end;

TAtlasKanban · TAtlasKanbanCard · TAtlasBeforeCardMove · EAtlasKanban

Also in Atlas UI

TAtlasKanban · TAtlasSyntaxMemo · TAtlasWebUpdater