os-backlog: plugin scaffold + board-ensure (refs #10)
Idempotent board-ensure over the installed planka-api gem: uniform lists (Backlog/Next/Doing/Waiting/Review/Done), uniform label set (P0-P3, hitl, semi, afk-ready) with per-label color-candidate fallback since Planka's color whitelist isn't documented, the archived--<name> rename convention (activate/archive primitives; ensure never archives automatically), and the bot-created-project visibility fix (shared-type project + explicit project-manager add + null ownerProjectManagerId). Unit tests stub the client layer entirely — no live API calls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XknQRvihHDpYE47RTmUR4N
This commit is contained in:
parent
3c5a17c288
commit
9a0e8a79fb
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "os-backlog",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Uniform Planka backlog boards across projects: idempotent board-ensure (lists, labels, archive convention, Planka 2.1.1 quirk handling) and a deterministic repo-to-board routing resolver."
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
#
|
||||||
|
# os-backlog CLI: board-ensure, activate, archive.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# os-backlog board-ensure NAME --project Dev|Clients
|
||||||
|
# os-backlog activate NAME
|
||||||
|
# os-backlog archive NAME --yes
|
||||||
|
#
|
||||||
|
# Talks to Planka over the network (the installed planka-api gem,
|
||||||
|
# credentials from PLANKA_BASE_URL/USERNAME/PASSWORD).
|
||||||
|
#
|
||||||
|
# Fails soft: if the gem or Planka credentials are unavailable, prints one
|
||||||
|
# clear error and exits nonzero. Never destructive.
|
||||||
|
|
||||||
|
require_relative "../lib/backlog"
|
||||||
|
|
||||||
|
def fail_soft(message)
|
||||||
|
warn "os-backlog: #{message}"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_client
|
||||||
|
require "planka_api"
|
||||||
|
client = Planka::Client.new
|
||||||
|
client.login
|
||||||
|
client
|
||||||
|
rescue LoadError
|
||||||
|
fail_soft("the planka-api gem is not installed/loadable; cannot reach Planka")
|
||||||
|
rescue StandardError => e
|
||||||
|
fail_soft("could not authenticate to Planka: #{e.message}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_project_flag(args)
|
||||||
|
idx = args.index("--project")
|
||||||
|
return "Dev" unless idx
|
||||||
|
|
||||||
|
args.delete_at(idx)
|
||||||
|
project = args.delete_at(idx)
|
||||||
|
project || fail_soft("--project requires a value (Dev or Clients)")
|
||||||
|
end
|
||||||
|
|
||||||
|
command, *rest = ARGV
|
||||||
|
|
||||||
|
case command
|
||||||
|
when "board-ensure"
|
||||||
|
name = rest.shift
|
||||||
|
fail_soft("board-ensure requires a board name") unless name
|
||||||
|
project = parse_project_flag(rest)
|
||||||
|
client = build_client
|
||||||
|
result = Backlog::BoardEnsurer.new(client: client).ensure(name, project_name: project)
|
||||||
|
puts "board #{result.board['name']} (#{result.created ? 'created' : 'existing'}) " \
|
||||||
|
"in project #{project}; lists created: #{result.lists_created.join(', ')}; " \
|
||||||
|
"labels created: #{result.labels_created.join(', ')}"
|
||||||
|
when "activate"
|
||||||
|
name = rest.shift
|
||||||
|
fail_soft("activate requires a board name") unless name
|
||||||
|
client = build_client
|
||||||
|
board = Backlog::BoardEnsurer.new(client: client).activate(name)
|
||||||
|
puts "activated: #{board['name']}"
|
||||||
|
when "archive"
|
||||||
|
name = rest.shift
|
||||||
|
fail_soft("archive requires a board name") unless name
|
||||||
|
fail_soft("archive requires explicit --yes (human go-ahead only)") unless rest.include?("--yes")
|
||||||
|
|
||||||
|
client = build_client
|
||||||
|
board = Backlog::BoardEnsurer.new(client: client).archive(name)
|
||||||
|
puts "archived: #{board['name']}"
|
||||||
|
when nil, "-h", "--help"
|
||||||
|
puts <<~USAGE
|
||||||
|
usage: os-backlog <command> [options]
|
||||||
|
|
||||||
|
commands:
|
||||||
|
board-ensure NAME --project Dev|Clients
|
||||||
|
activate NAME
|
||||||
|
archive NAME --yes
|
||||||
|
USAGE
|
||||||
|
exit(command.nil? ? 1 : 0)
|
||||||
|
else
|
||||||
|
fail_soft("unknown command #{command.inspect}")
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
require_relative "backlog/board_spec"
|
||||||
|
require_relative "backlog/board_ensurer"
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
require_relative "board_spec"
|
||||||
|
|
||||||
|
module Backlog
|
||||||
|
# Idempotently creates/repairs a uniform Planka board: the project it
|
||||||
|
# lives under, its lists, and its label set — plus the Planka 2.1.1
|
||||||
|
# quirks the planka-api gem does not handle for us:
|
||||||
|
#
|
||||||
|
# 1. undocumented `type` fields on create (project: "shared" so a
|
||||||
|
# manager can be added later; list: "active"; the gem already
|
||||||
|
# requires callers to pass these — it does not default them)
|
||||||
|
# 2. bot-created projects are invisible to the human: a `"private"`
|
||||||
|
# project auto-assigns its creator as sole ownerProjectManagerId and
|
||||||
|
# rejects additional managers (422). We create projects as
|
||||||
|
# `type: "shared"` instead, then explicitly add the human as a
|
||||||
|
# project manager and null out ownerProjectManagerId.
|
||||||
|
# 3. label colors must come from Planka's (undocumented) color-name
|
||||||
|
# whitelist — we try an ordered candidate list per label, same
|
||||||
|
# pattern the planka-api gem uses for its own priority labels.
|
||||||
|
#
|
||||||
|
# This class never archives a board — archiving only happens on explicit
|
||||||
|
# human request (see BoardSpec / the resolver). It does provide the raw
|
||||||
|
# rename primitives (+activate+ / +archive+) that a caller may invoke.
|
||||||
|
class BoardEnsurer
|
||||||
|
Result = Struct.new(:board, :created, :project, :lists_created, :labels_created, keyword_init: true)
|
||||||
|
|
||||||
|
# @param client [Planka::Client] an already-authenticated client
|
||||||
|
# @param human_user_id [String, Integer, nil] Planka user id of the
|
||||||
|
# human who must be able to see bot-created projects. Falls back to
|
||||||
|
# +ENV["PLANKA_HUMAN_USER_ID"]+. Only needed when a project doesn't
|
||||||
|
# already exist.
|
||||||
|
def initialize(client:, human_user_id: nil)
|
||||||
|
@client = client
|
||||||
|
@human_user_id = human_user_id || ENV["PLANKA_HUMAN_USER_ID"]
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param name [String] board name (never an archived---prefixed name;
|
||||||
|
# this method only ever targets the active board)
|
||||||
|
# @param project_name [String] "Dev" or "Clients" (or any project name)
|
||||||
|
# @return [Result]
|
||||||
|
def ensure(name, project_name:)
|
||||||
|
raise ArgumentError, "board name must not use the archived-- prefix" if BoardSpec.archived?(name)
|
||||||
|
|
||||||
|
project = find_or_create_project(project_name)
|
||||||
|
board, created = find_or_create_board(project, name)
|
||||||
|
|
||||||
|
lists_created = ensure_lists(board)
|
||||||
|
labels_created = ensure_labels(board)
|
||||||
|
|
||||||
|
Result.new(board: board, created: created, project: project,
|
||||||
|
lists_created: lists_created, labels_created: labels_created)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Rename an archived board back to its active name. Raises if no
|
||||||
|
# archived board by that name exists.
|
||||||
|
#
|
||||||
|
# @param name [String] the active (non-prefixed) name to activate
|
||||||
|
# @return [Hash] the updated board
|
||||||
|
def activate(name)
|
||||||
|
board = find_board_by_name(BoardSpec.archived_name(name))
|
||||||
|
raise "no archived board named #{BoardSpec.archived_name(name).inspect} found" unless board
|
||||||
|
|
||||||
|
@client.boards.update(board["id"], name: name)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Rename an active board to its archived form. Only ever called on
|
||||||
|
# explicit human request — never invoked by +ensure+.
|
||||||
|
#
|
||||||
|
# @param name [String] the active (non-prefixed) name to archive
|
||||||
|
# @return [Hash] the updated board
|
||||||
|
def archive(name)
|
||||||
|
board = find_board_by_name(name)
|
||||||
|
raise "no active board named #{name.inspect} found" unless board
|
||||||
|
|
||||||
|
@client.boards.update(board["id"], name: BoardSpec.archived_name(name))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_or_create_project(project_name)
|
||||||
|
existing = @client.projects.list.find { |p| p["name"] == project_name }
|
||||||
|
return existing if existing
|
||||||
|
|
||||||
|
project = @client.projects.create(name: project_name, type: "shared")
|
||||||
|
fix_visibility(project)
|
||||||
|
project
|
||||||
|
end
|
||||||
|
|
||||||
|
# Quirk 2: null the bot's sole ownerProjectManagerId and add the human
|
||||||
|
# as a project manager, so the project shows up for them.
|
||||||
|
def fix_visibility(project)
|
||||||
|
return unless @human_user_id
|
||||||
|
|
||||||
|
@client.projects.add_manager(project["id"], @human_user_id)
|
||||||
|
@client.projects.update(project["id"], ownerProjectManagerId: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_or_create_board(project, name)
|
||||||
|
board = find_board_by_name(name, project: project)
|
||||||
|
return [board, false] if board
|
||||||
|
|
||||||
|
created = @client.boards.create(project["id"], name: name, position: 65_536)
|
||||||
|
[created, true]
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_board_by_name(name, project: nil)
|
||||||
|
projects = project ? [project] : @client.projects.list
|
||||||
|
projects.each do |p|
|
||||||
|
board = @client.boards.list(p["id"]).find { |b| b["name"] == name }
|
||||||
|
return board if board
|
||||||
|
end
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_lists(board)
|
||||||
|
existing = @client.lists.list(board["id"]).filter_map { |l| l["name"] }
|
||||||
|
created = []
|
||||||
|
BoardSpec::LISTS.each_with_index do |list_name, index|
|
||||||
|
next if existing.include?(list_name)
|
||||||
|
|
||||||
|
@client.lists.create(board["id"], name: list_name, type: "active", position: (index + 1) * 65_536)
|
||||||
|
created << list_name
|
||||||
|
end
|
||||||
|
created
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_labels(board)
|
||||||
|
existing = @client.labels.list(board["id"]).filter_map { |l| l["name"] }
|
||||||
|
created = []
|
||||||
|
BoardSpec::LABEL_NAMES.each do |label_name|
|
||||||
|
next if existing.include?(label_name)
|
||||||
|
|
||||||
|
create_label_with_fallback_color(board, label_name)
|
||||||
|
created << label_name
|
||||||
|
end
|
||||||
|
created
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_label_with_fallback_color(board, label_name)
|
||||||
|
candidates = BoardSpec::LABEL_COLOR_CANDIDATES.fetch(label_name)
|
||||||
|
last_error = nil
|
||||||
|
candidates.each do |color|
|
||||||
|
return @client.labels.create(board["id"], name: label_name, color: color, position: 65_536)
|
||||||
|
rescue Planka::ValidationError => e
|
||||||
|
last_error = e
|
||||||
|
next
|
||||||
|
end
|
||||||
|
raise last_error || RuntimeError, "could not create label #{label_name.inspect}: no candidate color accepted"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
module Backlog
|
||||||
|
# The uniform board contract every os-backlog board must have: list order
|
||||||
|
# and the enforced label set. Values only — no client/network code.
|
||||||
|
module BoardSpec
|
||||||
|
# Lists in required left-to-right order.
|
||||||
|
LISTS = %w[Backlog Next Doing Waiting Review Done].freeze
|
||||||
|
|
||||||
|
# Label name => ordered candidate colors, tried in turn until the server
|
||||||
|
# accepts one (Planka's palette-name whitelist is not documented, so we
|
||||||
|
# can't assert it in advance — cf. planka-api gem's Priority::COLOR_CANDIDATES
|
||||||
|
# pattern). The first candidate in each list is the verified-good default.
|
||||||
|
LABEL_COLOR_CANDIDATES = {
|
||||||
|
"P0" => %w[berry-red red-burgundy salsa-red],
|
||||||
|
"P1" => %w[pumpkin-orange orange-peel sunset-orange],
|
||||||
|
"P2" => %w[egg-yellow sunny-grass bright-moss],
|
||||||
|
"P3" => %w[steel-grey morning-sky light-cocoa],
|
||||||
|
"hitl" => %w[tank-green bright-moss lagoon-blue],
|
||||||
|
"semi" => %w[lagoon-blue summer-sky navy-blue],
|
||||||
|
"afk-ready" => %w[antique-blue bright-moss slate-blue]
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
LABEL_NAMES = LABEL_COLOR_CANDIDATES.keys.freeze
|
||||||
|
|
||||||
|
ARCHIVE_PREFIX = "archived--"
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def archived_name(name) = "#{ARCHIVE_PREFIX}#{name}"
|
||||||
|
|
||||||
|
def archived?(name) = name.to_s.start_with?(ARCHIVE_PREFIX)
|
||||||
|
|
||||||
|
def active_name(name) = archived?(name) ? name.sub(ARCHIVE_PREFIX, "") : name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Run the whole suite: ruby tests/all.rb
|
||||||
|
Dir.glob(File.join(__dir__, "*_test.rb")).sort.each { |f| require f }
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
require_relative "test_helper"
|
||||||
|
|
||||||
|
class BoardEnsurerTest < Minitest::Test
|
||||||
|
include BacklogTestHelpers
|
||||||
|
|
||||||
|
def test_creates_missing_project_board_lists_and_labels
|
||||||
|
client = FakePlankaClient.new
|
||||||
|
ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1")
|
||||||
|
|
||||||
|
result = ensurer.ensure("llf-schema", project_name: "Dev")
|
||||||
|
|
||||||
|
assert result.created
|
||||||
|
assert_equal "llf-schema", result.board["name"]
|
||||||
|
assert_equal Backlog::BoardSpec::LISTS, result.lists_created
|
||||||
|
assert_equal Backlog::BoardSpec::LABEL_NAMES, result.labels_created
|
||||||
|
|
||||||
|
project = client.projects_store.first
|
||||||
|
assert_equal "Dev", project.name
|
||||||
|
assert_equal "shared", project.type
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_rerun_is_a_noop
|
||||||
|
client = FakePlankaClient.new
|
||||||
|
ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1")
|
||||||
|
ensurer.ensure("llf-schema", project_name: "Dev")
|
||||||
|
|
||||||
|
result = ensurer.ensure("llf-schema", project_name: "Dev")
|
||||||
|
|
||||||
|
refute result.created
|
||||||
|
assert_empty result.lists_created
|
||||||
|
assert_empty result.labels_created
|
||||||
|
assert_equal 1, client.boards_store.size
|
||||||
|
assert_equal Backlog::BoardSpec::LISTS.size, client.lists_store.size
|
||||||
|
assert_equal Backlog::BoardSpec::LABEL_NAMES.size, client.labels_store.size
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reuses_existing_project
|
||||||
|
client = FakePlankaClient.new
|
||||||
|
ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1")
|
||||||
|
ensurer.ensure("board-a", project_name: "Dev")
|
||||||
|
|
||||||
|
ensurer.ensure("board-b", project_name: "Dev")
|
||||||
|
|
||||||
|
assert_equal 1, client.projects_store.size
|
||||||
|
assert_equal 2, client.boards_store.size
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_applies_owner_manager_visibility_fix_on_new_project
|
||||||
|
client = FakePlankaClient.new
|
||||||
|
ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1")
|
||||||
|
|
||||||
|
ensurer.ensure("llf-schema", project_name: "Dev")
|
||||||
|
|
||||||
|
project = client.projects_store.first
|
||||||
|
assert_includes project.managers, "human-1"
|
||||||
|
assert_nil project.ownerProjectManagerId
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_semi_label_is_part_of_enforced_set
|
||||||
|
assert_includes Backlog::BoardSpec::LABEL_NAMES, "semi"
|
||||||
|
assert_includes Backlog::BoardSpec::LABEL_NAMES, "hitl"
|
||||||
|
assert_includes Backlog::BoardSpec::LABEL_NAMES, "afk-ready"
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_label_color_falls_back_to_next_candidate_on_rejection
|
||||||
|
first_choice = Backlog::BoardSpec::LABEL_COLOR_CANDIDATES.fetch("P0").first
|
||||||
|
client = FakePlankaClient.new(reject_colors: [first_choice])
|
||||||
|
ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1")
|
||||||
|
|
||||||
|
ensurer.ensure("llf-schema", project_name: "Dev")
|
||||||
|
|
||||||
|
p0 = client.labels_store.find { |l| l.name == "P0" }
|
||||||
|
refute_equal first_choice, p0.color
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_activate_renames_archived_board_back
|
||||||
|
client = FakePlankaClient.new
|
||||||
|
ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1")
|
||||||
|
ensurer.ensure("llf-schema", project_name: "Dev")
|
||||||
|
board = client.boards_store.first
|
||||||
|
board.name = "archived--llf-schema"
|
||||||
|
|
||||||
|
result = ensurer.activate("llf-schema")
|
||||||
|
|
||||||
|
assert_equal "llf-schema", result["name"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_archive_renames_active_board
|
||||||
|
client = FakePlankaClient.new
|
||||||
|
ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1")
|
||||||
|
ensurer.ensure("llf-schema", project_name: "Dev")
|
||||||
|
|
||||||
|
result = ensurer.archive("llf-schema")
|
||||||
|
|
||||||
|
assert_equal "archived--llf-schema", result["name"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_ensure_never_touches_archived_boards
|
||||||
|
client = FakePlankaClient.new
|
||||||
|
ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1")
|
||||||
|
ensurer.ensure("llf-schema", project_name: "Dev")
|
||||||
|
ensurer.archive("llf-schema")
|
||||||
|
|
||||||
|
result = ensurer.ensure("llf-schema", project_name: "Dev")
|
||||||
|
|
||||||
|
assert result.created
|
||||||
|
assert_equal 2, client.boards_store.size
|
||||||
|
archived = client.boards_store.find { |b| b.name == "archived--llf-schema" }
|
||||||
|
refute_nil archived
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_ensure_rejects_archived_prefixed_name
|
||||||
|
client = FakePlankaClient.new
|
||||||
|
ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1")
|
||||||
|
|
||||||
|
assert_raises(ArgumentError) { ensurer.ensure("archived--x", project_name: "Dev") }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
require "minitest/autorun"
|
||||||
|
require_relative "../lib/backlog"
|
||||||
|
|
||||||
|
# Minimal stand-ins for the planka-api gem's error hierarchy, so tests never
|
||||||
|
# require (or need) the real gem installed — only the shape our code
|
||||||
|
# rescues against.
|
||||||
|
module Planka
|
||||||
|
class Error < StandardError; end
|
||||||
|
class ValidationError < Error; end
|
||||||
|
end
|
||||||
|
|
||||||
|
module BacklogTestHelpers
|
||||||
|
# A hand-rolled fake of the subset of Planka::Client's resource surface
|
||||||
|
# BoardEnsurer touches: projects, boards, lists, labels. In-memory only,
|
||||||
|
# no network. Mirrors the gem's real return shapes (string-keyed hashes).
|
||||||
|
class FakePlankaClient
|
||||||
|
Project = Struct.new(:id, :name, :type, :ownerProjectManagerId, :managers, keyword_init: true)
|
||||||
|
Board = Struct.new(:id, :project_id, :name, keyword_init: true)
|
||||||
|
List = Struct.new(:id, :board_id, :name, :type, keyword_init: true)
|
||||||
|
Label = Struct.new(:id, :board_id, :name, :color, keyword_init: true)
|
||||||
|
|
||||||
|
attr_reader :projects, :boards, :lists, :labels
|
||||||
|
|
||||||
|
def initialize(reject_colors: [])
|
||||||
|
@next_id = 0
|
||||||
|
@projects_store = []
|
||||||
|
@boards_store = []
|
||||||
|
@lists_store = []
|
||||||
|
@labels_store = []
|
||||||
|
|
||||||
|
@projects = ProjectsResource.new(self)
|
||||||
|
@boards = BoardsResource.new(self)
|
||||||
|
@lists = ListsResource.new(self)
|
||||||
|
@labels = LabelsResource.new(self, reject_colors: reject_colors)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_id = (@next_id += 1).to_s
|
||||||
|
|
||||||
|
attr_reader :projects_store, :boards_store, :lists_store, :labels_store
|
||||||
|
|
||||||
|
class ProjectsResource
|
||||||
|
def initialize(client) = @client = client
|
||||||
|
|
||||||
|
def list = @client.projects_store.map { |p| stringify(p) }
|
||||||
|
|
||||||
|
def create(attrs)
|
||||||
|
project = FakePlankaClient::Project.new(
|
||||||
|
id: @client.next_id, name: attrs[:name], type: attrs[:type],
|
||||||
|
ownerProjectManagerId: @client.next_id, managers: []
|
||||||
|
)
|
||||||
|
@client.projects_store << project
|
||||||
|
stringify(project)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(id, attrs)
|
||||||
|
project = @client.projects_store.find { |p| p.id == id }
|
||||||
|
project.ownerProjectManagerId = attrs[:ownerProjectManagerId] if attrs.key?(:ownerProjectManagerId)
|
||||||
|
stringify(project)
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_manager(project_id, user_id)
|
||||||
|
project = @client.projects_store.find { |p| p.id == project_id }
|
||||||
|
project.managers << user_id
|
||||||
|
{ "id" => @client.next_id, "projectId" => project_id, "userId" => user_id }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def stringify(p) = { "id" => p.id, "name" => p.name, "type" => p.type }
|
||||||
|
end
|
||||||
|
|
||||||
|
class BoardsResource
|
||||||
|
def initialize(client) = @client = client
|
||||||
|
|
||||||
|
def list(project_id)
|
||||||
|
@client.boards_store.select { |b| b.project_id == project_id }.map { |b| stringify(b) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def create(project_id, attrs)
|
||||||
|
board = FakePlankaClient::Board.new(id: @client.next_id, project_id: project_id, name: attrs[:name])
|
||||||
|
@client.boards_store << board
|
||||||
|
stringify(board)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(id, attrs)
|
||||||
|
board = @client.boards_store.find { |b| b.id == id }
|
||||||
|
board.name = attrs[:name] if attrs.key?(:name)
|
||||||
|
stringify(board)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def stringify(b) = { "id" => b.id, "projectId" => b.project_id, "name" => b.name }
|
||||||
|
end
|
||||||
|
|
||||||
|
class ListsResource
|
||||||
|
def initialize(client) = @client = client
|
||||||
|
|
||||||
|
def list(board_id)
|
||||||
|
@client.lists_store.select { |l| l.board_id == board_id }.map { |l| stringify(l) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def create(board_id, attrs)
|
||||||
|
raise ArgumentError, "list create requires type" unless attrs[:type]
|
||||||
|
|
||||||
|
list = FakePlankaClient::List.new(id: @client.next_id, board_id: board_id, name: attrs[:name], type: attrs[:type])
|
||||||
|
@client.lists_store << list
|
||||||
|
stringify(list)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def stringify(l) = { "id" => l.id, "boardId" => l.board_id, "name" => l.name, "type" => l.type }
|
||||||
|
end
|
||||||
|
|
||||||
|
class LabelsResource
|
||||||
|
def initialize(client, reject_colors: [])
|
||||||
|
@client = client
|
||||||
|
@reject_colors = reject_colors
|
||||||
|
end
|
||||||
|
|
||||||
|
def list(board_id)
|
||||||
|
@client.labels_store.select { |l| l.board_id == board_id }.map { |l| stringify(l) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def create(board_id, attrs)
|
||||||
|
raise Planka::ValidationError, "color #{attrs[:color]} rejected" if @reject_colors.include?(attrs[:color])
|
||||||
|
|
||||||
|
label = FakePlankaClient::Label.new(id: @client.next_id, board_id: board_id, name: attrs[:name], color: attrs[:color])
|
||||||
|
@client.labels_store << label
|
||||||
|
stringify(label)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def stringify(l) = { "id" => l.id, "boardId" => l.board_id, "name" => l.name, "color" => l.color }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in New Issue