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 :teams, only: %i[show update]
|
||||||
resources :team_action_items, only: %i[update]
|
resources :team_action_items, only: %i[update]
|
||||||
resources :tournaments do
|
resources :tournaments do
|
||||||
|
patch 'team_action_lists/:key/teams/:team_id', to: 'team_action_lists#update_item'
|
||||||
resources :statistics, only: %i[index]
|
resources :statistics, only: %i[index]
|
||||||
resources :matches, only: %i[index]
|
resources :matches, only: %i[index]
|
||||||
resources :beamers, only: %i[index show create update destroy] do
|
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