A ActionType is a class which defines how to do
, undo
and redo
a particular action
in Baserow. It can freely use Handlers to do the logic, but it almost certainly
shouldn't call any other ActionType's unless it is some sort of meta
ActionAction if
we ever have one. ActionTypes will be retrieved from a registry given a type and
triggered by API
methods (
e.g. action_type_registry.get_by_type(DeleteGroupAction).do(user, group_to_delete)
).
- In
backend/src/baserow/core/actions/registries.py
there is aaction_type_registry
which can be used to registerActionType
's - An
ActionType
must implementdo
/undo
/redo
methods.do
Performs the action when a user requests it to happen, it must also save aAction
model usingcls.register_action
undo
Must undo the action done bydo
. It must not save anyAction
models.redo
Must redo the action after it has been undone byundo
. It must not save anyAction
models.
- An
ActionType
must implement aParams
dataclass which it will store any parameters it needs toundo
orredo
the action in. An instance of this dataclass must be provided tocls.register_action
in thedo
method, and it will be serialized to JSON and stored in theAction
table. Whenredo
orundo
is called thisdataclass
will be created again from the json in theAction
row and provided to the function.
See baserow.core.action.models.Action for more details.
id (serial) | user_id (fk to user table, nullable) | session (text nullable) | category (text) | created_on (auto_now_add DateTimeField) | type (text) | params (JSONB) | undone_at (nullable DateTimeField) | error (text nullable) |
---|---|---|---|---|---|---|---|---|
1 | 2 | 'some-uuid-from-client' | 'root' | datetime | 'group_created' | '{created_group_id:10}' | null | null |
The ActionHandler
has undo
and redo
methods which can be used to trigger an
undo/redo for a user. There are two corresponding endpoints in /api/user/undo
and /api/user/redo
which call the ActionHandler
. To trigger an undo
/ redo
we
need three pieces of information:
- The user triggering the undo/redo, so we can check if they still have permissions to undo/redo the action. For example a user might be redoing a deletion of a group, but if they have been banned from the group in the meantime they should be prevented from redoing.
- A
client session id
. Every time a user does an action in Baserow we check theClientSessionId
header. If set we associate the action with thatClientSessionId
. When a user then goes to undo or redo they also provide this header and we only let them undo/redo actions with a matchingClientSessionId
. This lets us have different undo/redo histories per tab the user has open as each tab will generate a uniqueClientSessionId
. - A
category
. Every time an action is performed in Baserow we associate it with a particular category. This is literally just a text column on theAction
model with values likeroot
ortable10
orgroup20
. An actions category describes in which logical part of Baserow the action was performed. TheActionType
implementation decides what to set its category to when callingcls.register_action
. When an undo/redo occurs the web-frontend sends the categories the user is currently looking at. For example if I have table 20 open, with group 6 in the side bar and I press undo/redo the category sent will be:
{
root: true,
table: 20,
group: 6
}
By sending this category to the undo/redo endpoint we are telling it to undo any actions which were done in:
- The root category
- The table 20 category
- The group 6 category
For example, if I renamed table 20, then the table_update action would be in group 6 category. If I was then looking at table 20 in the UI and pressed undo, the UI would send the group 6 category as one of the active categories as table 20 is in group 6. Meaning I could then undo this rename. If i was to first switch to group 5 and press undo, the UI would send group 5 as the category and I wouldn't be able to undo the rename of table 20 until I switched back into a part of the UI where the group 6 category is active.
- User A opens Table 10, which is in Application 2 in Group 1.
- On page load a ClientSessionId
example_client_session_id
is generated and stored in theauth
store. (its a uuid normally). - The current category for this page is set in the
undoRedo
store to be:{root: true, table_id:10, application_id:2, group_id:1}
- On page load a ClientSessionId
- User A changes the Tables name.
- A request is sent to the table update endpoint.
- The
ClientSessionId
header is set on the request toexample_client_session_id
- The
- The table update API endpoint will
call
action_type_registry.get(UpdateTableAction).do(user, ...)
- The change is made and a new Action is stored.
- UpdateTableAction sets the
category
of the action to begroup1
- The
ClientSessionId
is found from the request and the session of the action is set toexample_client_session_id
- The
user
of the action is set toUser A
- The old tables name is stored in the
action.params
JSONField to facilitate undos and redos.
- UpdateTableAction sets the
- A request is sent to the table update endpoint.
- User A presses
Undo
- A request is sent to the
undo
endpoint with thecategory
request data value set to the current category of the page the user has open obtained from theundoRedo
store (see above).- The
ClientSessionId
header is set on the request toexample_client_session_id
- The
ActionHandler.undo
is called.- It finds the latest action for
User A
in sessionexample_client_session_id
and in any of the following categories["root", "group1", "application2", "table10"]
. These were calculated from the category parameter provided to the endpoint. - The table rename action is found as it's session matches, it is in
category
group
, it was done byUser A
and it has not yet been undone ( theundone_at
column is null). - It deserializes the parameters for the latest action from the table into the
action's
Params
dataclass - It calls `action_type_registry.get(UpdateTableAction).undo(user, params, action_to_undo)
- UpdateTableAction using the params undoes the action
- Action.undone_at is set to
timezone.now()
indicating it has now been undone
- It finds the latest action for
- A request is sent to the
Imagine a situation when two users are working on a table at the same time, in order they: 1 User A changes a cell in a field called 'date'
- User A changes a cell in a field called 'Name'
- User B deletes the 'name' field
- User A presses 'undo' - in our current implementation they get an error saying the undo failed and was skipped
- User A presses 'undo' - in our current implementation Users A's first change now gets undone
We cannot undo User A's latest action as it was to a cell in the now deleted field ' name'. What will happen when is:
- We will attempt to undo User A's action by calling ActionHandler.undo
- It will crash and raise an exception
- In the ActionHandler.undo method we catch this exception and:
- We store it on the action's error field
- We mark the action as
undone
by setting it'sundone_at
datetime field totimezone.now()
- We send a specific error back to the user saying the undo failed, and we skipped over it.
Interestingly, if the user then presses redo twice we will:
- Redo user A's first action
- Now we are trying to redo the action that failed. It has an error set. We see this
error and send and error back to the user saying
can't redo due to error, skipping.
- However we also remove the error and mark the action as "redone".
- Now the user can press "undo" again and the action will be attempted to be undone a second time just like the first. If User B has by this point restored the delete field it could now work!