Skip to content

Navigating the app

Every screen in a Victor app is described by metadata, not hand-built pages. Three pieces work together:

  • Menus form the navigation — the entries a user clicks to move around the app.
  • Actions tie a model to the set of view kinds a user can open for it.
  • Views (list, form, kanban, card, search) declare exactly what the user sees on screen.

This page walks through the app the way a user experiences it — from the top-level navigation down to a single record — and shows the view XML that produces each screen. If you are coming from Odoo, this will feel familiar: you define a model and a few views, and the framework renders the tables, forms, and boards for you. You never write bespoke screens.

The running example is the built-in contact app. Its complete view file declares a list, a form, a kanban board, a search bar, an action, and a menu — everything below is drawn from it.

Where views come from

You declare views in your module's XML. See Views for the full view DSL and Models & fields for the models the views render.

Menus are the entry points. Each <menu> is one navigation entry, ordered by sequence.

  • A top-level menu (one with an icon) is an app entry point — the things a user picks from the app navigation.
  • A menu that has child menus renders as a tabbed page: each child becomes a tab, and the active tab embeds that child's record collection.
  • A leaf menu (no children) opens its action's record collection directly.
<action id="action_contacts" name="Contacts" model="contact" views="list,kanban,form" path="contacts"/>
<menu id="menu" parent="base.menu_root" label="Contacts" action="contact.action_contacts" icon="Users" sequence="10"/>

The action defines the screen; the menu makes it reachable. Here the Contacts menu is a top-level app (it has an icon and sits under the root menu), and clicking it opens the contact collection.

Attribute Purpose
id Identifier for this menu within the module.
parent Dotted reference of the parent menu, e.g. base.menu_root.
label Text shown in the navigation.
action Dotted reference of the action to open (leaf menus).
icon Icon name, e.g. Users. A menu with an icon reads as a top-level app entry.
sequence Sort order (integer); lower numbers appear first.

Grouped tabs

To group several screens under one tabbed page (for example a Settings area), give a parent menu no action and hang child menus off it. Each child renders as a tab. See Adding a Settings tab.

Actions: binding a model to its views

An <action> connects a model to the view kinds a user can open for it, and gives the collection a URL slug. The views attribute is a comma-separated list — it declares which kinds the user can switch between, and, through the position of its first non-form kind, which view the collection opens on.

  • The toolbar switcher offers exactly the kinds you list. Its buttons appear in a fixed built-in order, not the order you write them in — e.g. views="kanban,list,form" still shows the List button before the Kanban button.
  • The first non-form kind is the default landing view. In views="list,kanban,form" the user lands on the list; the form opens when they click into a record.

<action> attributes

Attribute Purpose
id Identifier for this action within the module.
name Title shown in the collection header.
model The model whose records this action shows.
views Comma-separated view kinds the user can switch between; the first non-form kind is the landing view. E.g. list,kanban,form.
path URL slug for the collection, e.g. contacts.

The list view

The default landing for most collections is the list view — a paginated table. Each <field> child is one column, in order.

<view id="view_list" model="contact" type="list">
  <field name="display_name"/>
  <field name="email"/>
  <field name="is_company"/>
  <field name="stage"/>
  <field name="tag_ids" widget="tags"/>
</view>

The list toolbar gives the user everything they need to work through the table:

  • Sorting by the table's columns.
  • A pager to page through large collections.
  • A search box and the filter bar declared by the model's search view (below).
  • Multi-select with delete — tick rows (or select all) and remove them in one action.

You can also add action buttons as columns — see Record action buttons.

Searching, filtering, and grouping

The filter chips and group-by options that appear above a collection come from a search view. It is attached to the model by kind and is not listed in the action's views attribute — if a model has no search view, the collection simply shows no filter bar.

<view id="view_search" model="contact" type="search">
  <search>
    <filter name="companies" string="Companies" domain='[["is_company", "=", true]]' group="kind"/>
    <filter name="people" string="People" domain='[["is_company", "=", false]]' group="kind"/>
    <filter name="by_company" string="Company" context="{'group_by': 'is_company'}"/>
  </search>
</view>

Each <filter> is a toggle chip:

  • A filter with a domain narrows the list when the chip is active. Separate (ungrouped) domain filters are AND-ed together — the list must match all of them.
  • A filter with context="{'group_by': 'field'}" groups the list by that field.
  • group="…" tags chips as part of one facet. When two or more active chips in a group each test the same field with = — as Companies and People both test is_company — they collapse into a single "is one of" (OR) match, so activating both shows companies and people together instead of matching nothing. Chips that test different fields, or use an operator other than =, still combine with AND, just like ungrouped filters.

<filter> attributes

Attribute Purpose
name Stable key for the chip.
string The chip's label.
domain JSON array of [field, operator, value] triples, applied when the chip is active.
group Optional; tags chips as one facet. Two or more active same-field = chips collapse into a single "is one of" (OR) match; otherwise grouped chips combine with AND, like ungrouped ones.
context Optional; {'group_by': 'field'} to group records by a field.

Domain operators

A domain is a list of [field, operator, value] triples. The available operators:

Operator Meaning
= Equal to.
!= Not equal to.
< Less than.
<= Less than or equal to.
> Greater than.
>= Greater than or equal to.
ilike Case-insensitive substring match.
in Value is one of an array of values.

Opening a record: the form view

Clicking a row in a list, or a card in a kanban or card view, opens that record in its form view — the single-record screen for viewing and editing.

Fields are laid out inside titled <group> blocks (a two-column label + value layout). An optional <notebook> adds tabbed pages below or alongside the groups; the first page is active by default.

<view id="view_form" model="contact" type="form">
  <group string="Details">
    <field name="display_name"/>
    <field name="email" widget="email"/>
    <field name="is_company"/>
    <field name="stage"/>
    <field name="parent_id"/>
    <field name="tag_ids" widget="tags"/>
  </group>
  <notebook>
    <page string="Notes">
      <field name="email"/>
    </page>
  </notebook>
</view>

The form toolbar lets the user save, discard, and delete the record, and hosts any record action buttons for the model.

<group> and <notebook> / <page>

Tag Purpose
<group string="…"> A titled two-column label + value block wrapping related fields.
<notebook> Tab container placed on the form.
<page string="…"> One tab inside the notebook; the first page is active by default.

The kanban board

A kanban view shows records as cards arranged into columns. The <kanban> element's attributes drive the board, and its <field> children are the fields shown on each card.

<view id="view_kanban" model="contact" type="kanban">
  <kanban default_group_by="is_company" records_draggable="1">
    <field name="display_name"/>
    <field name="email"/>
  </kanban>
</view>
  • default_group_by — the field used to form the initial columns.
  • records_draggable="1" — lets the user drag a card between columns to move it.

Clicking a card opens the record's form, just like a list row.

The card view

A card view renders records as a responsive grid of cards. The first <field> is the card heading; the remaining fields become label: value rows. Any <button> becomes a card action.

<view id="view_card" model="contact" type="card">
  <field name="display_name"/>
  <field name="email"/>
  <button action="archive" string="Archive" confirm="Archive?" variant="ghost"/>
</view>

Card, list, and kanban are switchable from the toolbar when the action offers more than one — for example views="card,list" lets the user flip between the card grid and the table.

Creating, editing, and deleting records

The framework wires up the full record lifecycle from your views — you do not build any of it:

  • Create — the New button opens a blank form for the model.
  • Edit — open a record, change its fields, and Save from the form toolbar. Discard abandons unsaved changes.
  • Delete — remove a single record from its form toolbar, or select multiple rows in the list (including select all) and delete them together.

Record action buttons

Beyond create/edit/delete, a model can expose custom actions as buttons. A <button> runs a named server action on the record and can appear in a list row, the form toolbar, or on a card.

A destructive list-row button with a confirmation prompt:

<button action="install" string="Install" confirm="Uninstall? This drops data." variant="destructive"/>

Form-toolbar buttons that appear only in the right record state:

<button action="confirm" string="Confirm" variant="default" invisible="state == 'done'"/>
<button action="archive" string="Archive" confirm="Archive this record?" variant="ghost"/>

On a form, invisible is evaluated against the saved record, so a button can show only when the record is in a matching state (for example, hide Confirm once state == 'done').

<button> attributes

Attribute Purpose
action The registered server action to run on the record.
string The button label.
confirm Optional confirmation text shown before the action fires.
preview Optional read-only action that describes the consequences before the real action runs.
variant Button style: default, soft, ghost, or destructive.
invisible Optional expression (e.g. state == 'done'); hides the button when true. Evaluated on the saved record.
show_on_new 1 to also render on an unsaved record — clicking it creates the record, then runs the action.
run_on_save 1 to run the action automatically after every save, with no click.

Two automatic modes

Use show_on_new="1" when an action should be available on a brand-new record (it creates, then acts). Use run_on_save="1" for a check that must always run when the record is saved — it fires on both create and update, and the result surfaces as a toast. See View actions & buttons for how to register the server action a button calls.

Field widgets

A <field> renders as one column (in list, kanban, and card views) or one input/value (in a form). Its display picks a widget automatically: an explicit widget= wins, otherwise the field's type default is used, falling back to a plain string.

<field name="email" widget="email"/>
<field name="tag_ids" widget="tags"/>

<field> attributes

Attribute Purpose
name The field to show.
widget Optional named widget, e.g. email or tags.
readonly 1 to show the read-only widget even in edit mode.
invisible 1 to not render the field.
required required="0" suppresses the required asterisk on a field that is required in the model. The asterisk itself comes from the model's required=True, not from this attribute.

Field types and their default widgets

The field type chosen on the model determines the default display and edit widget when you don't set widget=:

Type Notes
String Single-line text.
Text Multi-line text.
Integer Whole number.
Float Decimal number.
Boolean Checkbox / toggle.
Date Date picker.
Datetime Date and time picker.
Selection(options=[(value, label), …]) Choice from a fixed set of options.
Reference(target=…) Link to a single record of another model.
Many2many(target=…) Set of related records; pairs well with widget="tags".

The fields the views above reference come from the model itself:

from victor import fields, models


class Contact(models.Model):
    name = "contact"
    label = "Contact"
    display_name = fields.String(label="Name", required=True)
    email = fields.String(label="Email")
    is_company = fields.Boolean(label="Is company", default=False)
    stage = fields.Selection(
        options=[("lead", "Lead"), ("customer", "Customer"), ("vendor", "Vendor")],
        label="Stage", default="lead",
    )
    parent_id = fields.Reference(target="contact", label="Parent company")
    tag_ids = fields.Many2many(target="tag", label="Tags")

Putting it together

Reading a screen back to its definition is straightforward once you know the pieces:

  1. A menu put the app in the navigation.
  2. Its action named the model and the view kinds — the first non-form kind is where the user landed.
  3. A view (list, kanban, or card) laid out the collection; the search view supplied its filter bar.
  4. Clicking a record opened the form view, with its groups, notebook tabs, and toolbar buttons.

Every one of those is metadata you declare. To go deeper, see Models & fields, Views, and View actions & buttons.