docs: add team action list frontend guide
This commit is contained in:
parent
443b7ff869
commit
fa83923bf2
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class TeamActionListsController < ApplicationController
|
||||
before_action :set_tournament
|
||||
before_action :authenticate_user!, only: %i[update_item]
|
||||
before_action -> { require_owner! @tournament.owner }, only: %i[update_item]
|
||||
before_action -> { require_writable_tournament!(@tournament) }, only: %i[update_item]
|
||||
before_action :set_team_action_item, only: %i[update_item]
|
||||
|
||||
# PATCH /tournaments/:tournament_id/team_action_lists/:key/teams/:team_id
|
||||
def update_item
|
||||
if @team_action_item.update(team_action_item_params)
|
||||
push_sync_if_needed!(@tournament)
|
||||
render json: @team_action_item
|
||||
else
|
||||
render json: @team_action_item.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tournament
|
||||
@tournament = Tournament.find(params[:tournament_id])
|
||||
end
|
||||
|
||||
def set_team_action_item
|
||||
list = @tournament.team_action_lists.find_by!(key: params[:key])
|
||||
@team_action_item = list.team_action_items.find_by!(team_id: params[:team_id])
|
||||
end
|
||||
|
||||
def team_action_item_params
|
||||
params.slice(:completed).permit(:completed)
|
||||
end
|
||||
|
||||
def push_sync_if_needed!(tournament)
|
||||
TournamentSyncEnqueue.call(tournament)
|
||||
end
|
||||
end
|
||||
|
|
@ -19,6 +19,7 @@ Rails.application.routes.draw do
|
|||
resources :teams, only: %i[show update]
|
||||
resources :team_action_items, only: %i[update]
|
||||
resources :tournaments do
|
||||
patch 'team_action_lists/:key/teams/:team_id', to: 'team_action_lists#update_item'
|
||||
resources :statistics, only: %i[index]
|
||||
resources :matches, only: %i[index]
|
||||
resources :beamers, only: %i[index show create update destroy] do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
# Team Action Lists API
|
||||
|
||||
## Purpose
|
||||
|
||||
Team action lists let the backend expose checklist-style workflows for teams inside a tournament.
|
||||
|
||||
Current example:
|
||||
|
||||
- `group_stage_survivor_playoff_tokens`
|
||||
|
||||
That list contains all teams that advanced from the group stage and tracks whether each team already collected a playoff token.
|
||||
|
||||
## Read Flow
|
||||
|
||||
Frontend reads team action lists from the normal tournament payload:
|
||||
|
||||
- `GET /tournaments/:id`
|
||||
|
||||
Example response shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"name": "My Tournament",
|
||||
"team_action_lists": [
|
||||
{
|
||||
"id": 9,
|
||||
"name": "Playoff token collection",
|
||||
"key": "group_stage_survivor_playoff_tokens",
|
||||
"action_name": "Collect playoff token",
|
||||
"source": "group_stage_survivors",
|
||||
"team_action_items": [
|
||||
{
|
||||
"id": 41,
|
||||
"completed": false,
|
||||
"completed_at": null,
|
||||
"team": {
|
||||
"id": 55,
|
||||
"name": "Team A"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Write Flow
|
||||
|
||||
Only tournament owner may change item status.
|
||||
Everyone may read status.
|
||||
Follower tournaments in `read_only_mode` reject updates with `423`.
|
||||
|
||||
There are two supported write patterns.
|
||||
|
||||
### Option 1: update by item id
|
||||
|
||||
Use this when frontend already has the item id from tournament payload.
|
||||
|
||||
- `PATCH /team_action_items/:id`
|
||||
|
||||
Request body:
|
||||
|
||||
```json
|
||||
{
|
||||
"completed": true
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: update by list key + team id
|
||||
|
||||
Use this when frontend wants a business-key style request like:
|
||||
|
||||
- "tick off team X from list Y in tournament Z"
|
||||
|
||||
Endpoint:
|
||||
|
||||
- `PATCH /tournaments/:tournament_id/team_action_lists/:key/teams/:team_id`
|
||||
|
||||
Example:
|
||||
|
||||
- `PATCH /tournaments/123/team_action_lists/group_stage_survivor_playoff_tokens/teams/55`
|
||||
|
||||
Request body:
|
||||
|
||||
```json
|
||||
{
|
||||
"completed": true
|
||||
}
|
||||
```
|
||||
|
||||
Response body for both write endpoints is the updated `TeamActionItem`.
|
||||
|
||||
## Idempotency Rules
|
||||
|
||||
These endpoints are designed to be idempotent if frontend sends explicit target state.
|
||||
|
||||
Good:
|
||||
|
||||
- send `{"completed": true}` to mark complete
|
||||
- send `{"completed": false}` to mark incomplete
|
||||
|
||||
Avoid:
|
||||
|
||||
- toggle-style client behavior such as "invert current state"
|
||||
|
||||
Why this is idempotent:
|
||||
|
||||
- sending `completed=true` multiple times leaves item in same final state
|
||||
- sending `completed=false` multiple times leaves item in same final state
|
||||
- backend does not create duplicate items
|
||||
- backend stores one unique item per `(team_action_list_id, team_id)`
|
||||
|
||||
`completed_at` behavior:
|
||||
|
||||
- first successful transition to `completed=true` sets timestamp if none exists
|
||||
- sending `completed=true` again keeps same completed state
|
||||
- sending `completed=false` clears `completed_at`
|
||||
|
||||
## Frontend Implementation Guidance
|
||||
|
||||
Recommended frontend flow:
|
||||
|
||||
1. Load tournament via `GET /tournaments/:id`
|
||||
2. Find desired list by stable `key`
|
||||
3. Render `team_action_items`
|
||||
4. On user action, send explicit target state
|
||||
5. Update local UI from response or refetch tournament
|
||||
|
||||
Recommended frontend identifiers:
|
||||
|
||||
- use list `key` as stable frontend reference
|
||||
- use `team.id` as stable team reference
|
||||
- use `team_action_item.id` when available for direct item updates
|
||||
|
||||
Recommended optimistic UI:
|
||||
|
||||
- set checkbox immediately to requested target state
|
||||
- revert if API returns error
|
||||
- do not compute next state from stale cached data if multiple clients may edit
|
||||
|
||||
## Error Handling
|
||||
|
||||
Expected statuses:
|
||||
|
||||
- `200` update accepted
|
||||
- `403` authenticated but not tournament owner
|
||||
- `404` tournament, list, team, or item not found
|
||||
- `423` follower tournament is read-only
|
||||
- `401` unauthenticated
|
||||
|
||||
## Sync / Follower Behavior
|
||||
|
||||
Team action lists are included in tournament sync snapshots.
|
||||
|
||||
That means:
|
||||
|
||||
- source tournament owner updates item state
|
||||
- backend enqueues tournament sync
|
||||
- follower tournament receives updated list state
|
||||
- follower users can view but not mutate item state while read-only
|
||||
|
||||
## Future Extension Guidance
|
||||
|
||||
When new lists are added, frontend should not hardcode backend internals beyond:
|
||||
|
||||
- stable list `key`
|
||||
- user-facing `name`
|
||||
- user-facing `action_name`
|
||||
|
||||
New list types should automatically fit the same frontend rendering model:
|
||||
|
||||
- list metadata
|
||||
- list items
|
||||
- explicit `completed` state
|
||||
- idempotent set-state requests
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe TeamActionListsController, type: :controller do
|
||||
let(:team_action_item) { create(:team_action_item) }
|
||||
let(:tournament) { team_action_item.tournament }
|
||||
let(:list) { team_action_item.team_action_list }
|
||||
let(:team) { team_action_item.team }
|
||||
|
||||
describe 'PATCH #update_item' do
|
||||
context 'as owner' do
|
||||
before do
|
||||
apply_authentication_headers_for tournament.owner
|
||||
end
|
||||
|
||||
it 'updates item by list key and team id' do
|
||||
patch :update_item, params: {
|
||||
tournament_id: tournament.to_param,
|
||||
key: list.key,
|
||||
team_id: team.to_param,
|
||||
completed: true
|
||||
}
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(team_action_item.reload.completed).to eq(true)
|
||||
end
|
||||
|
||||
it 'is idempotent when same state sent repeatedly' do
|
||||
2.times do
|
||||
patch :update_item, params: {
|
||||
tournament_id: tournament.to_param,
|
||||
key: list.key,
|
||||
team_id: team.to_param,
|
||||
completed: true
|
||||
}
|
||||
expect(response).to be_successful
|
||||
end
|
||||
|
||||
expect(team_action_item.reload.completed).to eq(true)
|
||||
end
|
||||
|
||||
it 'enqueues tournament sync' do
|
||||
allow(TournamentSyncEnqueue).to receive(:call)
|
||||
|
||||
patch :update_item, params: {
|
||||
tournament_id: tournament.to_param,
|
||||
key: list.key,
|
||||
team_id: team.to_param,
|
||||
completed: true
|
||||
}
|
||||
|
||||
expect(TournamentSyncEnqueue).to have_received(:call).with(tournament)
|
||||
end
|
||||
end
|
||||
|
||||
context 'as another user' do
|
||||
before do
|
||||
apply_authentication_headers_for create(:user)
|
||||
end
|
||||
|
||||
it 'returns forbidden' do
|
||||
patch :update_item, params: {
|
||||
tournament_id: tournament.to_param,
|
||||
key: list.key,
|
||||
team_id: team.to_param,
|
||||
completed: true
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe TeamActionListsController, type: :routing do
|
||||
describe 'routing' do
|
||||
it 'routes to #update_item via PATCH' do
|
||||
expect(patch: '/tournaments/1/team_action_lists/group_stage_survivor_playoff_tokens/teams/2')
|
||||
.to route_to(
|
||||
'team_action_lists#update_item',
|
||||
tournament_id: '1',
|
||||
key: 'group_stage_survivor_playoff_tokens',
|
||||
team_id: '2'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue