proctolog 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/README.md +178 -0
- data/app/controllers/proctolog/acceptances_controller.rb +29 -0
- data/app/controllers/proctolog/application_controller.rb +32 -0
- data/app/controllers/proctolog/bursts_controller.rb +34 -0
- data/app/controllers/proctolog/choices_controller.rb +30 -0
- data/app/controllers/proctolog/code_reviews_controller.rb +26 -0
- data/app/controllers/proctolog/decisions_controller.rb +33 -0
- data/app/controllers/proctolog/entries_controller.rb +14 -0
- data/app/controllers/proctolog/evidence_controller.rb +23 -0
- data/app/controllers/proctolog/freshness_controller.rb +27 -0
- data/app/controllers/proctolog/good_to_go_controller.rb +29 -0
- data/app/controllers/proctolog/memberships_controller.rb +20 -0
- data/app/controllers/proctolog/mentions_controller.rb +16 -0
- data/app/controllers/proctolog/moments_controller.rb +26 -0
- data/app/controllers/proctolog/note_params.rb +24 -0
- data/app/controllers/proctolog/notes_controller.rb +56 -0
- data/app/controllers/proctolog/open_pull_requests_controller.rb +59 -0
- data/app/controllers/proctolog/pictures_controller.rb +27 -0
- data/app/controllers/proctolog/prompts_controller.rb +28 -0
- data/app/controllers/proctolog/pull_request_scoped.rb +21 -0
- data/app/controllers/proctolog/pull_requests_controller.rb +34 -0
- data/app/controllers/proctolog/put_away_controller.rb +19 -0
- data/app/controllers/proctolog/recordings_controller.rb +28 -0
- data/app/controllers/proctolog/rendered_pieces_controller.rb +11 -0
- data/app/controllers/proctolog/review_data_controller.rb +12 -0
- data/app/controllers/proctolog/rules_controller.rb +20 -0
- data/app/controllers/proctolog/scenario_notes_controller.rb +26 -0
- data/app/controllers/proctolog/scenario_scoped.rb +21 -0
- data/app/controllers/proctolog/scenarios_controller.rb +67 -0
- data/app/controllers/proctolog/sitting_servers_controller.rb +30 -0
- data/app/controllers/proctolog/waiting_controller.rb +14 -0
- data/app/controllers/proctolog/with_decisions.rb +28 -0
- data/app/helpers/proctolog/bursts_helper.rb +52 -0
- data/app/helpers/proctolog/decisions_helper.rb +102 -0
- data/app/helpers/proctolog/desk_helper.rb +61 -0
- data/app/helpers/proctolog/mentions_helper.rb +27 -0
- data/app/helpers/proctolog/open_pull_requests_helper.rb +31 -0
- data/app/helpers/proctolog/prompts_helper.rb +21 -0
- data/app/helpers/proctolog/pull_requests_helper.rb +67 -0
- data/app/helpers/proctolog/recordings_helper.rb +30 -0
- data/app/helpers/proctolog/rounds_helper.rb +27 -0
- data/app/helpers/proctolog/scenarios_helper.rb +123 -0
- data/app/helpers/proctolog/standings_helper.rb +80 -0
- data/app/jobs/proctolog/application_job.rb +8 -0
- data/app/jobs/proctolog/ask_for_release_job.rb +9 -0
- data/app/jobs/proctolog/fetch_pictures_ahead_job.rb +36 -0
- data/app/jobs/proctolog/prune_fresh_profiles_job.rb +9 -0
- data/app/jobs/proctolog/read_mentions_ahead_job.rb +12 -0
- data/app/models/proctolog/acceptance.rb +47 -0
- data/app/models/proctolog/app.rb +92 -0
- data/app/models/proctolog/browser.rb +42 -0
- data/app/models/proctolog/burst.rb +268 -0
- data/app/models/proctolog/burst_notices.rb +143 -0
- data/app/models/proctolog/chapters.rb +42 -0
- data/app/models/proctolog/check.rb +122 -0
- data/app/models/proctolog/choices.rb +112 -0
- data/app/models/proctolog/code_review.rb +251 -0
- data/app/models/proctolog/decision_record.rb +12 -0
- data/app/models/proctolog/decisions.rb +199 -0
- data/app/models/proctolog/description.rb +165 -0
- data/app/models/proctolog/difference.rb +138 -0
- data/app/models/proctolog/doctor.rb +127 -0
- data/app/models/proctolog/evidence.rb +29 -0
- data/app/models/proctolog/failure.rb +7 -0
- data/app/models/proctolog/focus.rb +48 -0
- data/app/models/proctolog/fresh_sitting.rb +35 -0
- data/app/models/proctolog/github.rb +353 -0
- data/app/models/proctolog/handover/brief.rb +138 -0
- data/app/models/proctolog/handover/layers.rb +128 -0
- data/app/models/proctolog/handover/places.rb +98 -0
- data/app/models/proctolog/handover/taken.rb +41 -0
- data/app/models/proctolog/handover/worklist.rb +35 -0
- data/app/models/proctolog/handover.rb +123 -0
- data/app/models/proctolog/housekeeping.rb +23 -0
- data/app/models/proctolog/kit.rb +36 -0
- data/app/models/proctolog/marks.rb +130 -0
- data/app/models/proctolog/membership.rb +91 -0
- data/app/models/proctolog/mentions.rb +117 -0
- data/app/models/proctolog/note.rb +94 -0
- data/app/models/proctolog/outside.rb +45 -0
- data/app/models/proctolog/overlay/brief.rb +52 -0
- data/app/models/proctolog/overlay/written.rb +33 -0
- data/app/models/proctolog/overlay.rb +87 -0
- data/app/models/proctolog/owner.rb +16 -0
- data/app/models/proctolog/pick.rb +47 -0
- data/app/models/proctolog/picture.rb +138 -0
- data/app/models/proctolog/pieces_rendering.rb +40 -0
- data/app/models/proctolog/project.rb +118 -0
- data/app/models/proctolog/pull_request.rb +89 -0
- data/app/models/proctolog/recording.rb +194 -0
- data/app/models/proctolog/release.rb +59 -0
- data/app/models/proctolog/rendering.rb +303 -0
- data/app/models/proctolog/reports.rb +66 -0
- data/app/models/proctolog/response_files.rb +55 -0
- data/app/models/proctolog/rounds.rb +305 -0
- data/app/models/proctolog/route.rb +151 -0
- data/app/models/proctolog/rules.rb +121 -0
- data/app/models/proctolog/scenario.rb +126 -0
- data/app/models/proctolog/server.rb +273 -0
- data/app/models/proctolog/since.rb +108 -0
- data/app/models/proctolog/size.rb +30 -0
- data/app/models/proctolog/standing.rb +132 -0
- data/app/models/proctolog/theme.rb +40 -0
- data/app/models/proctolog/unopened/numbers.rb +48 -0
- data/app/models/proctolog/unopened/opening.rb +88 -0
- data/app/models/proctolog/unopened.rb +203 -0
- data/app/models/proctolog/visit.rb +26 -0
- data/app/views/layouts/proctolog/application.html.erb +58 -0
- data/app/views/proctolog/acceptances/new.html.erb +34 -0
- data/app/views/proctolog/bursts/_line.html.erb +28 -0
- data/app/views/proctolog/bursts/_merged.html.erb +23 -0
- data/app/views/proctolog/bursts/_moved.html.erb +16 -0
- data/app/views/proctolog/bursts/_notices.html.erb +39 -0
- data/app/views/proctolog/bursts/_put_away.html.erb +11 -0
- data/app/views/proctolog/bursts/_rules.html.erb +27 -0
- data/app/views/proctolog/bursts/_side.html.erb +49 -0
- data/app/views/proctolog/bursts/_side_stop.html.erb +10 -0
- data/app/views/proctolog/bursts/show.html.erb +54 -0
- data/app/views/proctolog/code_reviews/show.html.erb +33 -0
- data/app/views/proctolog/decisions/_alternative.html.erb +5 -0
- data/app/views/proctolog/decisions/_beneath.html.erb +21 -0
- data/app/views/proctolog/decisions/_box.html.erb +27 -0
- data/app/views/proctolog/decisions/_choices.html.erb +9 -0
- data/app/views/proctolog/decisions/_draft_tools.html.erb +47 -0
- data/app/views/proctolog/decisions/_part.html.erb +61 -0
- data/app/views/proctolog/decisions/_round_data.html.erb +1 -0
- data/app/views/proctolog/decisions/_theme.html.erb +13 -0
- data/app/views/proctolog/decisions/index.html.erb +75 -0
- data/app/views/proctolog/notes/_card.html.erb +19 -0
- data/app/views/proctolog/notes/_template.html.erb +41 -0
- data/app/views/proctolog/open_pull_requests/_side.html.erb +30 -0
- data/app/views/proctolog/open_pull_requests/index.html.erb +106 -0
- data/app/views/proctolog/prompts/show.html.erb +41 -0
- data/app/views/proctolog/pull_requests/_connections.html.erb +65 -0
- data/app/views/proctolog/pull_requests/_finish.html.erb +42 -0
- data/app/views/proctolog/pull_requests/_margin.html.erb +33 -0
- data/app/views/proctolog/pull_requests/_note.html.erb +4 -0
- data/app/views/proctolog/pull_requests/_outline.html.erb +26 -0
- data/app/views/proctolog/pull_requests/_paragraphs.html.erb +9 -0
- data/app/views/proctolog/pull_requests/_scenarios.html.erb +16 -0
- data/app/views/proctolog/pull_requests/_since.html.erb +42 -0
- data/app/views/proctolog/pull_requests/_standing.html.erb +51 -0
- data/app/views/proctolog/pull_requests/_status.html.erb +5 -0
- data/app/views/proctolog/pull_requests/_try.html.erb +122 -0
- data/app/views/proctolog/pull_requests/show.html.erb +145 -0
- data/app/views/proctolog/recordings/_dialog.html.erb +26 -0
- data/app/views/proctolog/recordings/_list.html.erb +8 -0
- data/app/views/proctolog/recordings/_note.html.erb +22 -0
- data/app/views/proctolog/recordings/index.html.erb +33 -0
- data/app/views/proctolog/recordings/show.html.erb +118 -0
- data/app/views/proctolog/scenarios/_dialog.html.erb +22 -0
- data/app/views/proctolog/scenarios/_note.html.erb +18 -0
- data/app/views/proctolog/scenarios/_open.html.erb +9 -0
- data/app/views/proctolog/scenarios/_scenario.html.erb +48 -0
- data/app/views/proctolog/scenarios/_server.html.erb +20 -0
- data/app/views/proctolog/scenarios/index.html.erb +62 -0
- data/bin/proctolog +330 -0
- data/completions/_proctolog +143 -0
- data/config/application.rb +94 -0
- data/config/database.yml +16 -0
- data/config/environment.rb +6 -0
- data/config/initializers/grouse.rb +113 -0
- data/config/initializers/grouse_notes.rb +9 -0
- data/config/keys.yml +6 -0
- data/config/routes.rb +71 -0
- data/config/storage.yml +4 -0
- data/db/migrate/20261019213542_create_grouse_tables.grouse.rb +128 -0
- data/db/migrate/20261019213550_create_active_storage_tables.active_storage.rb +58 -0
- data/docs/manual/app.md +157 -0
- data/docs/manual/bursts.md +129 -0
- data/docs/manual/deciding.md +89 -0
- data/docs/manual/desk.png +0 -0
- data/docs/manual/films.md +20 -0
- data/docs/manual/handover.md +93 -0
- data/docs/manual/pr-format.md +75 -0
- data/docs/manual/reading.md +233 -0
- data/docs/manual/unopened.md +80 -0
- data/docs/manual/updates.md +55 -0
- data/lib/proctolog/installation.rb +125 -0
- data/lib/proctolog/version.rb +7 -0
- data/public/asking.js +50 -0
- data/public/code-review.js +40 -0
- data/public/cursor.js +292 -0
- data/public/decision-screenshots.js +97 -0
- data/public/decisions.css +949 -0
- data/public/decisions.js +1289 -0
- data/public/desk.css +582 -0
- data/public/desk.js +126 -0
- data/public/favicon.png +0 -0
- data/public/keys.js +228 -0
- data/public/mentions.js +389 -0
- data/public/messages.js +103 -0
- data/public/notes.js +731 -0
- data/public/pick.css +99 -0
- data/public/plaque.png +0 -0
- data/public/pr-number.js +11 -0
- data/public/prompt.css +46 -0
- data/public/prompt.js +16 -0
- data/public/pull_request.css +805 -0
- data/public/recording.css +379 -0
- data/public/recording.js +394 -0
- data/public/release.js +25 -0
- data/public/respond.css +260 -0
- data/public/respond.js +95 -0
- data/public/review-drafts.js +949 -0
- data/public/review-rounds.js +1447 -0
- data/public/scenarios.css +253 -0
- data/public/scenarios.js +36 -0
- data/public/schemas/proctolog-response-v1.json +101 -0
- data/public/side.js +133 -0
- data/public/standing.css +478 -0
- data/public/terminal-apl.svg +6 -0
- data/public/terminal-cuneiform.svg +3 -0
- data/public/terminal-grain-dots.svg +6 -0
- data/public/terminal-grain-weave.svg +6 -0
- data/public/terminal-grain-wedges.svg +6 -0
- data/public/terminal-shades.svg +3 -0
- data/public/terminal.css +4426 -0
- data/public/toast.js +26 -0
- data/public/try.js +124 -0
- data/public/viewer.js +808 -0
- data/public/words.js +81 -0
- data/skills/proctolog-handover/SKILL.md +77 -0
- data/skills/proctolog-pr-writing/SKILL.md +228 -0
- data/skills/proctolog-pr-writing/references/example-description.md +33 -0
- metadata +366 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# The repository's open pull requests, to pick what to review: where the
|
|
5
|
+
# desk opens while it has no burst. A press makes the burst for the pick
|
|
6
|
+
# among the reviewer's own, so nobody writes one to review a pull request.
|
|
7
|
+
class OpenPullRequestsController < ApplicationController
|
|
8
|
+
before_action do
|
|
9
|
+
@repository = Project.current.github_repository or raise Failure, "#{Project.current.root} has no GitHub repository as its origin."
|
|
10
|
+
@github = Github.new(@repository)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# `fresh` reads GitHub again, then comes back to an address a reload can repeat.
|
|
14
|
+
def index
|
|
15
|
+
@pick = Pick.new(@github.open_pull_requests(fresh: params.key?(:fresh)), viewer: Github.viewer, unopened: unopened)
|
|
16
|
+
return redirect_to(open_pull_requests_path) if params.key?(:fresh)
|
|
17
|
+
|
|
18
|
+
@bursts, @put_away = Burst.newest_first.partition { !it.put_away? }
|
|
19
|
+
@put_away_shown = params.key?(:put_away)
|
|
20
|
+
@read_at = @github.read_at
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# One pull request, reviewed alone: one open on GitHub, or one nobody
|
|
24
|
+
# has opened yet, which its burst names by its branch.
|
|
25
|
+
def alone
|
|
26
|
+
pull_request = held(params.expect(:id))
|
|
27
|
+
slug = pull_request.opened? ? "pr-#{pull_request.number}" : "branch-#{pull_request.head.parameterize}"
|
|
28
|
+
burst = Burst.find_or_start(slug, name: "##{pull_request.number} #{pull_request.title}", pull_requests: [pull_request.listed_as])
|
|
29
|
+
redirect_to burst_pull_request_path(burst.slug, pull_request.number)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A pull request nobody has opened yet, with the chain under it,
|
|
33
|
+
# followed down by base branches.
|
|
34
|
+
def chain
|
|
35
|
+
top = held(params.expect(:id))
|
|
36
|
+
burst = Burst.find_or_start("chain-#{top.head.parameterize}", name: "Up to #{top.head}: #{top.title}", tops: [top.listed_as])
|
|
37
|
+
redirect_to burst_path(burst.slug)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# One of GitHub's stacks, whole.
|
|
41
|
+
def stack
|
|
42
|
+
number = params.expect(:id).to_i
|
|
43
|
+
layers = @github.open_pull_requests.select { it.stack == number }.sort_by(&:position)
|
|
44
|
+
layers.any? or raise Failure, "No open pull request is in stack #{number}."
|
|
45
|
+
burst = Burst.find_or_start("stack-#{number}", name: "Stack #{number}: #{layers.last.title}", stacks: [number])
|
|
46
|
+
redirect_to burst_path(burst.slug)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def held(id)
|
|
52
|
+
number = PullRequest.number(id)
|
|
53
|
+
[*@github.open_pull_requests, *unopened].find { it.number == number } || raise(Failure, "##{id} is neither open nor waiting unopened.")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Those nobody has opened yet: on a branch with no open pull request.
|
|
57
|
+
def unopened = Unopened.waiting(open: @github.open_pull_requests).map(&:pull_request)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
class PicturesController < ApplicationController
|
|
5
|
+
rescue_from(Picture::Unavailable) { render plain: it.message, status: :bad_gateway }
|
|
6
|
+
|
|
7
|
+
# A picture never changes under its asset id, so the browser keeps it.
|
|
8
|
+
def show
|
|
9
|
+
extension = params[:format]
|
|
10
|
+
return head(:not_found) unless Picture::EXTENSIONS.include?(extension)
|
|
11
|
+
|
|
12
|
+
file = Picture.new(params.expect(:asset), extension).file
|
|
13
|
+
expires_in 1.year, public: true
|
|
14
|
+
send_file file, type: Mime::Type.lookup_by_extension(extension).to_s, disposition: :inline
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Where the halves of a pair differ, for the viewer to outline and go
|
|
18
|
+
# through, and whether they could be compared at all.
|
|
19
|
+
def compare
|
|
20
|
+
pictures = params.expect(:before, :after).map { Picture.new(*it.split(".", 2)) }
|
|
21
|
+
return head(:not_found) unless pictures.all? { Picture::EXTENSIONS.include?(it.extension) }
|
|
22
|
+
|
|
23
|
+
result = Difference.between(*pictures.map(&:file), cache: Project.current.cache)
|
|
24
|
+
render json: result.to_h.slice(:compared, :same, :sizes, :boxes)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# The prompt for a fresh session: what would be handed over now; and the
|
|
5
|
+
# handover `e` asks for, written as a folder where the desk keeps
|
|
6
|
+
# handovers, with the address of a new session that reads it.
|
|
7
|
+
class PromptsController < ApplicationController
|
|
8
|
+
before_action { @burst = Burst.find(params.expect(:burst_id)) }
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
@taken_in = @burst.reports.take_in
|
|
12
|
+
@handover = @burst.handover(@burst.route, focus:)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# The pass is the desk's own, from the choices the page saved before it
|
|
16
|
+
# asked. `again` takes back what an agent took and never answered.
|
|
17
|
+
def create
|
|
18
|
+
route = @burst.route
|
|
19
|
+
worklist = Rounds.new(@burst, route:, focus:).worklist(batch_id: SecureRandom.uuid, again: params[:again].present?)
|
|
20
|
+
file = @burst.handover(route, focus:, worklist:).write
|
|
21
|
+
render json: {file: file.to_s, session: helpers.new_session_address(file), prompt: helpers.session_prompt(file),
|
|
22
|
+
requests: worklist.items.size, withAgent: worklist.with_agent.size}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# What `create` would write, file by file, and writes nothing.
|
|
26
|
+
def preview = render(plain: @burst.handover(@burst.route, focus:).to_s)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# For a controller nested under a pull request of a burst: finds the
|
|
5
|
+
# burst and the pull request's place in its route, and answers by going
|
|
6
|
+
# back to the pull request's page.
|
|
7
|
+
module PullRequestScoped
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
before_action { @burst = Burst.find(params.expect(:burst_id)) }
|
|
12
|
+
before_action { @stop = @burst.route.at(PullRequest.number(params.expect(:pull_request_number))) || render(plain: "##{params[:pull_request_number]} is not part of #{@burst.name}.", status: :not_found) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def marks = @marks ||= @burst.marks(@stop.number)
|
|
18
|
+
|
|
19
|
+
def back_to_the_page(anchor:) = redirect_to(burst_pull_request_path(@burst, @stop.number, anchor:))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
class PullRequestsController < ApplicationController
|
|
5
|
+
# One pull request of a burst: where it stands and what moved since the
|
|
6
|
+
# owner's mark, then its description as GitHub renders it, with the
|
|
7
|
+
# owner's notes drawn on it and listed beside it. `fresh` reads GitHub
|
|
8
|
+
# again, then comes back to an address that a reload can repeat.
|
|
9
|
+
def show
|
|
10
|
+
@burst = Burst.find(params.expect(:burst_id))
|
|
11
|
+
@route = @burst.route(fresh: params.key?(:fresh))
|
|
12
|
+
return redirect_to(burst_pull_request_path(@burst, params.expect(:number))) if params.key?(:fresh)
|
|
13
|
+
|
|
14
|
+
@burst.reports.take_in
|
|
15
|
+
@stop = @route.at(PullRequest.number(params.expect(:number))) or return render(plain: "##{params[:number]} is not part of #{@burst.name}.", status: :not_found)
|
|
16
|
+
standings = @burst.standings(@route)
|
|
17
|
+
@standing = standings.fetch(@stop)
|
|
18
|
+
# Next to review is the next with blocking decisions left: the non-blocking
|
|
19
|
+
# wait for one gesture, and do not hold up deciding.
|
|
20
|
+
@next_to_review = focus.round_from(@stop).find { standings.fetch(it).progress.blocking_left.positive? }
|
|
21
|
+
@since = @burst.since(@stop) if @standing.changed?
|
|
22
|
+
@scenarios = @burst.scenarios.select { it.pr == @stop.number }
|
|
23
|
+
@rendering = Rendering.new(@burst.rendering(@stop.number), notes: @standing.marks.notes, pages: pages, linked: @stop.description.linked)
|
|
24
|
+
@decisions = @burst.decisions(pages:)
|
|
25
|
+
@boxes = @decisions.boxes_of(@stop)
|
|
26
|
+
@code = CodeReview.new(@burst, @stop.number).tally
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Where each pull request of the burst is, as GitHub says and as the desk says.
|
|
32
|
+
def pages = @route.to_h { [it.url, burst_pull_request_path(@burst, it.number)] }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# The owner putting a burst away while some of it is still open, as a
|
|
5
|
+
# finished one is put away, or taking it out again.
|
|
6
|
+
class PutAwayController < ApplicationController
|
|
7
|
+
before_action { @burst = Burst.find(params.expect(:burst_id)) }
|
|
8
|
+
|
|
9
|
+
def create
|
|
10
|
+
@burst.put_away!
|
|
11
|
+
redirect_to burst_path(@burst), notice: "Put away."
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def destroy
|
|
15
|
+
@burst.take_out!
|
|
16
|
+
redirect_to burst_path(@burst), notice: "Out again."
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
class RecordingsController < ApplicationController
|
|
5
|
+
before_action { @burst = Burst.find(params.expect(:burst_id)) }
|
|
6
|
+
|
|
7
|
+
# Every recording of the burst, a folder at a time: the owner's sitting
|
|
8
|
+
# first, then the pull requests in number order.
|
|
9
|
+
def index
|
|
10
|
+
@folders = Recording.of(@burst).group_by(&:folder).sort_by { |folder, _| [(folder == "sitting") ? 0 : 1, folder.to_i, folder] }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# One recording to play, with the owner's notes pinned to its moments.
|
|
14
|
+
def show
|
|
15
|
+
@recording = Recording.find(@burst, params.expect(:path)) or return render(plain: "There is no recording at #{params[:path]}.", status: :not_found)
|
|
16
|
+
@notes = @recording.noted_in(@burst.marks(@recording.folder))
|
|
17
|
+
@changes = @recording.changes(Recording::Rounds.new(@burst).before(@recording)) if @recording.chapters.any?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The picture a step of a film opens on, for the strip beside it; none
|
|
21
|
+
# when it cannot be had, and the step's name stands alone.
|
|
22
|
+
def still
|
|
23
|
+
recording = Recording.find(@burst, params.expect(:path)) or return head(:not_found)
|
|
24
|
+
file = recording.still(params.expect(:index).to_i) or return head(:not_found)
|
|
25
|
+
send_file file, type: "image/png", disposition: :inline
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# Draws pieces of a description, which the page has only as markdown.
|
|
5
|
+
class RenderedPiecesController < ApplicationController
|
|
6
|
+
def create
|
|
7
|
+
burst = Burst.find(params.expect(:burst_id))
|
|
8
|
+
render json: {pieces: burst.draw(params.expect(markdown: []))}
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# What the pages' script needs of the burst's review rounds, built from the
|
|
5
|
+
# whole burst; with `pr`, that pull request's descriptions too.
|
|
6
|
+
class ReviewDataController < ApplicationController
|
|
7
|
+
def show
|
|
8
|
+
@burst = Burst.find(params.expect(:burst_id))
|
|
9
|
+
render json: helpers.round_data(@burst, pr: params[:pr]&.then { PullRequest.number(it) })
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# A rule the owner made of an answer, edited or withdrawn from the burst
|
|
5
|
+
# page. The pages' script makes rules; these change the kept choices the
|
|
6
|
+
# same way, and an open page catches up with them.
|
|
7
|
+
class RulesController < ApplicationController
|
|
8
|
+
before_action { @burst = Burst.find(params.expect(:burst_id)) }
|
|
9
|
+
|
|
10
|
+
def update
|
|
11
|
+
@burst.rules.edit(params.expect(:id), params.expect(:text))
|
|
12
|
+
redirect_to burst_path(@burst, anchor: "rules"), notice: "Rule saved."
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def destroy
|
|
16
|
+
@burst.rules.withdraw(params.expect(:id))
|
|
17
|
+
redirect_to burst_path(@burst, anchor: "rules"), notice: "Rule withdrawn."
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# A note on a scenario, and moving it along. It is kept with the marks of
|
|
5
|
+
# the scenario's pull request, or of the sitting for a step of it.
|
|
6
|
+
class ScenarioNotesController < ApplicationController
|
|
7
|
+
include ScenarioScoped
|
|
8
|
+
include NoteParams
|
|
9
|
+
|
|
10
|
+
before_action { @scenario = @burst.scenario(params.expect(:scenario_id)) }
|
|
11
|
+
|
|
12
|
+
def create
|
|
13
|
+
marks.pin(kind: one_of(Note::KINDS, params.expect(:kind)), text: params.expect(:text).strip, place: Note::Scenario.new(name: @scenario.name))
|
|
14
|
+
back_to_the_page(notice: "Note saved.")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def update
|
|
18
|
+
note = marks.move(params.expect(:id), to: one_of(Note::STATES, params.expect(:state))) or return head(:not_found)
|
|
19
|
+
back_to_the_page(notice: "The note is #{note.state}.")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def marks = @burst.marks(@scenario.marked_on)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# For a controller that works on a scenario of a burst and answers by going
|
|
5
|
+
# back to the page it was asked from, at the scenario: the scenarios page or
|
|
6
|
+
# a pull request's.
|
|
7
|
+
module ScenarioScoped
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
before_action { @burst = Burst.find(params.expect(:burst_id)) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def back_to_the_page(**flash)
|
|
17
|
+
page = url_from(request.referer&.sub(/#.*/, "")) || burst_scenarios_path(@burst)
|
|
18
|
+
redirect_to "#{page}##{@scenario&.to_param}", **flash
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
class ScenariosController < ApplicationController
|
|
5
|
+
include ScenarioScoped
|
|
6
|
+
|
|
7
|
+
before_action(only: %i[open saw pull]) { @scenario = @burst.scenario(params[:id]) }
|
|
8
|
+
rescue_from(Server::Error) { back_to_the_page(alert: it.message.truncate(1200)) }
|
|
9
|
+
|
|
10
|
+
def index
|
|
11
|
+
@server = @burst.server
|
|
12
|
+
@state = @server.state
|
|
13
|
+
@route = @burst.route
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Opens the app in the scenario's state. Every browser comes by an address
|
|
17
|
+
# of the desk that is good once and sets the cookies, so the session never
|
|
18
|
+
# shows in an address: your own browser follows it at once, and the other
|
|
19
|
+
# kinds are started at it.
|
|
20
|
+
#
|
|
21
|
+
# Where the app names an `overlay`, the press opens Grouse's overlay there
|
|
22
|
+
# too, labelled with the scenario, and keeps its session beside it.
|
|
23
|
+
def open
|
|
24
|
+
visit = @scenario.visit(@burst.server, overlay: (brief if @burst.server.app.overlay?))
|
|
25
|
+
keep(visit.overlay)
|
|
26
|
+
address = entry_url(visit.keep)
|
|
27
|
+
flash[:alert] = visit.overlay&.missing if visit.overlay&.missing
|
|
28
|
+
return redirect_to(address, status: :see_other) if @scenario.browser.mine?
|
|
29
|
+
|
|
30
|
+
@scenario.browser.launch(address, size: @scenario.window)
|
|
31
|
+
back_to_the_page(notice: "Opened in a #{@scenario.browser.name} Chrome.")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Brings back what the owner wrote in the scenario's overlays, each note
|
|
35
|
+
# pinned on the scenario once.
|
|
36
|
+
def pull
|
|
37
|
+
back_to_the_page(notice: Overlay.pull(@burst.server, @burst.marks(@scenario.marked_on), @scenario.name).sentence)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# What the owner saw, kept with the commit it was seen on.
|
|
41
|
+
def saw
|
|
42
|
+
result = params.expect(:result)
|
|
43
|
+
return head(:unprocessable_content) unless Scenario::RESULTS.value?(result)
|
|
44
|
+
|
|
45
|
+
@burst.marks(@scenario.marked_on).saw(@scenario.name, head:, result:)
|
|
46
|
+
back_to_the_page
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# The commit the scenario is seen on: its pull request's head, or the sitting's.
|
|
52
|
+
def head = @scenario.pr ? @burst.route.at(@scenario.pr)&.head_oid : @burst.server.head
|
|
53
|
+
|
|
54
|
+
# The scenario as the overlay's strip shows it, with the way back to the
|
|
55
|
+
# page the press was made on.
|
|
56
|
+
def brief
|
|
57
|
+
stop = @scenario.pr && @burst.route.at(@scenario.pr)
|
|
58
|
+
page = url_from(request.referer&.sub(/#.*/, "")) || burst_scenarios_url(@burst)
|
|
59
|
+
Overlay::Brief.for(@scenario, burst: @burst, steps: stop ? @scenario.steps_in(stop.description).map(&:text) : [], head:,
|
|
60
|
+
desk_url: "#{page}##{@scenario.to_param}")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def keep(overlay)
|
|
64
|
+
@burst.marks(@scenario.marked_on).keep_overlay(@scenario.name, Overlay.opening(overlay.session, head:)) if overlay&.session
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# The server a burst's scenarios open the app on: asked after, started,
|
|
5
|
+
# stopped. A press goes back to the page it was made on, the Scenarios page
|
|
6
|
+
# or a pull request's; asked for JSON, it answers where the server stands,
|
|
7
|
+
# so that page can stay where it is.
|
|
8
|
+
class SittingServersController < ApplicationController
|
|
9
|
+
before_action { @burst = Burst.find(params.expect(:burst_id)) }
|
|
10
|
+
rescue_from(Server::Error) { answer(alert: it.message.truncate(1200)) }
|
|
11
|
+
|
|
12
|
+
def show = render(json: {state: @burst.server.state})
|
|
13
|
+
|
|
14
|
+
def create = answer(notice: @burst.server.start)
|
|
15
|
+
|
|
16
|
+
def destroy
|
|
17
|
+
@burst.server.stop
|
|
18
|
+
answer(notice: "The server was stopped.")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def answer(**flash)
|
|
24
|
+
respond_to do |format|
|
|
25
|
+
format.html { redirect_back_or_to burst_scenarios_path(@burst), **flash }
|
|
26
|
+
format.json { render json: {state: @burst.server.state, **flash}, status: flash.key?(:alert) ? :unprocessable_content : :ok }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# What a pull request waits on, outside the burst, in a sentence. An empty
|
|
5
|
+
# sentence says it waits on nothing.
|
|
6
|
+
class WaitingController < ApplicationController
|
|
7
|
+
include PullRequestScoped
|
|
8
|
+
|
|
9
|
+
def update
|
|
10
|
+
marks.wait_on(params[:what].to_s.strip)
|
|
11
|
+
back_to_the_page(anchor: "standing")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
# What every page that works on the boxes of "Decisions for the owner"
|
|
5
|
+
# starts from: the burst, and its decisions as GitHub holds them now. A
|
|
6
|
+
# page that names a box GitHub no longer holds as it was shown goes back to
|
|
7
|
+
# the list with a sentence.
|
|
8
|
+
module WithDecisions
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
before_action { @burst = Burst.find(params.expect(:burst_id)) }
|
|
13
|
+
rescue_from(Decisions::Changed) { redirect_to(decisions_path_for(params[:pull_request_number]), alert: it.message) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def decisions = @decisions ||= @burst.decisions(pages:)
|
|
19
|
+
|
|
20
|
+
# Where each pull request of the burst is, as GitHub says and as the desk says.
|
|
21
|
+
def pages = @burst.route.to_h { [it.url, burst_pull_request_path(@burst, it.number)] }
|
|
22
|
+
|
|
23
|
+
def number = PullRequest.number(params.expect(:pull_request_number))
|
|
24
|
+
|
|
25
|
+
# The list as the page that sent the request had it, at a pull request.
|
|
26
|
+
def decisions_path_for(number) = burst_decisions_path(@burst, show: params[:show], anchor: ("pr-#{number}" if number.present?))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
module BurstsHelper
|
|
5
|
+
# Which of the two a mark will later be compared against moved last.
|
|
6
|
+
def last_change(pull_request)
|
|
7
|
+
what, at = {"description" => pull_request.described_at, "code" => pull_request.committed_at}.max_by(&:last)
|
|
8
|
+
tag.time("#{what}, #{time_ago_in_words(at)} ago", datetime: at.iso8601, title: at.localtime.strftime("%-d %b %H:%M"))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def open_threads(pull_request)
|
|
12
|
+
" · #{pluralize(pull_request.open_threads, "review thread")} open" if pull_request.open_threads.positive?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# "Stack #1055, 21 from the bottom up", "A chain of 2 on #1062, from the
|
|
16
|
+
# bottom up", "Standing alone". A stack placed from the membership kept
|
|
17
|
+
# beside the marks, when GitHub could not say, is said to be as last read.
|
|
18
|
+
def part_heading(part, route)
|
|
19
|
+
stops = route.stops_of(part)
|
|
20
|
+
if part.alone? then "Standing alone"
|
|
21
|
+
elsif part.stack? then "Stack ##{part.id}#{" as last read" unless route.stacks_known?}, #{stops.size} from the bottom up"
|
|
22
|
+
else "A chain of #{stops.size}#{" on ##{stops.first.on}" if stops.first.on}, from the bottom up"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The heading over each kind of disagreement between a burst's file and GitHub.
|
|
27
|
+
DISAGREEMENTS = {off_the_burst: "On a member's branch, not in this burst", empty_stack: "Listed stacks with nothing open",
|
|
28
|
+
filed_twice: "Filed twice", held_elsewhere: "Also in another burst"}.freeze
|
|
29
|
+
|
|
30
|
+
def disagreement_heading(cause) = DISAGREEMENTS.fetch(cause)
|
|
31
|
+
|
|
32
|
+
# What the owner chose on a decision of a pull request that left.
|
|
33
|
+
CHOSEN = {"leave" => "Left as is", "question" => "Question", "change" => "Change", "later" => "Left for later"}.freeze
|
|
34
|
+
|
|
35
|
+
def entry_line(entry)
|
|
36
|
+
said = entry.draft ? "Draft" : CHOSEN.fetch(entry.kind, entry.kind.to_s)
|
|
37
|
+
safe_join([tag.strong("#{said}:"), entry.lead, (tag.span("— #{entry.text}", class: "text") if entry.text.present?)].compact, " ")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# What a rule's state means for the next handover.
|
|
41
|
+
RULE_STATES = {written: "goes in the next handover", sent: "handed over; the agent's answer is awaited",
|
|
42
|
+
answered: "answered: yours to confirm under Returned work", confirmed: "recorded, and confirmed", withdrawn: "withdrawn"}.freeze
|
|
43
|
+
|
|
44
|
+
# A rule's line once the cursor is on it: what it was made from, what its
|
|
45
|
+
# state means, and the line it replaces until the replacement is handed over.
|
|
46
|
+
def rule_facts(rule)
|
|
47
|
+
[("made from #{rule.from.map { "##{it.pr} “#{it.lead}”" }.to_sentence}" if rule.from.any?),
|
|
48
|
+
RULE_STATES.fetch(rule.state),
|
|
49
|
+
("replaces “#{rule.replaces}”" if rule.replaces && rule.state == :written)].compact.join(" · ")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proctolog
|
|
4
|
+
module DecisionsHelper
|
|
5
|
+
# Every decision of the burst as the pages' script takes it, in JSON. It
|
|
6
|
+
# holds what GitHub says and nothing of the owner's marks, so it is built
|
|
7
|
+
# once for a reading: building it reads every description again. It is of
|
|
8
|
+
# the whole burst whatever a page is focused on, so a choice outside the
|
|
9
|
+
# focus is never taken for one whose decision is gone.
|
|
10
|
+
# What the inventory holds changed with the desk: a page built from an
|
|
11
|
+
# older one would read its decisions wrong.
|
|
12
|
+
INVENTORY = 3
|
|
13
|
+
|
|
14
|
+
# The pictures' files and pixel signatures are looked up at each page,
|
|
15
|
+
# not kept with the rest: a picture fetched since the reading is known.
|
|
16
|
+
def decision_inventory(burst)
|
|
17
|
+
Rails.cache.fetch([:inventory, INVENTORY, burst.slug, burst.read_at.to_f]) { burst.decisions.boxes.map { draft_decision(it) } }
|
|
18
|
+
.map { |decision| decision.merge(pictures: decision[:pictures].map { picture_now(it) }) }.to_json
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def draft_decision(box)
|
|
22
|
+
{id: box.to_param, pr: box.stop.number, title: box.stop.title, url: box.stop.url, branch: box.stop.pull_request.head,
|
|
23
|
+
head: box.stop.head_oid, description: box.stop.description.digest, position: box.position, lead: box.lead,
|
|
24
|
+
text: box.decision.text, key: box.decision.key, ticked: box.decision.ticked, protected: @burst.refusal(box.stop.number).present?,
|
|
25
|
+
grade: box.decision.grade, theme: box.decision.theme_name, pictures: box.decision.pictures.map(&:to_h)}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# A picture a decision rests on, with its file on this Mac and its pixels'
|
|
29
|
+
# signature; both nil while it is not here, its signature while
|
|
30
|
+
# ImageMagick cannot read it.
|
|
31
|
+
def picture_now(picture) = picture.merge(file: Picture.on_disk(picture[:asset])&.basename&.to_s, signature: Picture.signature(picture[:asset]))
|
|
32
|
+
|
|
33
|
+
# "3 blocking · 12 non-blocking". More blocking decisions than one owner
|
|
34
|
+
# should be asked at once says so in words: "8 blocking · over five".
|
|
35
|
+
def grades_of(decisions)
|
|
36
|
+
blocking = decisions.count(&:blocking?)
|
|
37
|
+
[("#{blocking} blocking" if blocking.positive?), ("over five" if blocking > Check::MOST_BLOCKING),
|
|
38
|
+
("#{decisions.size - blocking} non-blocking" if decisions.size > blocking)].compact.join(" · ")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A grade as the end of a pull request's decisions heads it, and what it
|
|
42
|
+
# asks of the owner, in a sentence under its name.
|
|
43
|
+
Grade = Data.define(:name, :meaning)
|
|
44
|
+
GRADES = {
|
|
45
|
+
blocking: Grade.new("Blocking", "Each needs your answer before the work goes on."),
|
|
46
|
+
non_blocking: Grade.new("Non-blocking", "Each has a sensible default: left as is, what the agent built stands.")
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
def grade_named(grade) = GRADES.fetch(grade)
|
|
50
|
+
|
|
51
|
+
# Why leaving every non-blocking decision at once passes over this one,
|
|
52
|
+
# as the desk knows it: the page adds a draft of its own.
|
|
53
|
+
def held_by(box)
|
|
54
|
+
[("starred" if box.starred), ("named in the burst file" if box.named.any?), ("answered" if box.answers.any?), ("asked" if box.asked.any?)].compact
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def decision_html(box, burst)
|
|
58
|
+
Nokogiri::HTML5.fragment(box.html).tap do |html|
|
|
59
|
+
html.css('a[href^="#screenshot-"]').each do |link|
|
|
60
|
+
link["href"] = burst_pull_request_path(burst, box.stop.number) + link["href"]
|
|
61
|
+
end
|
|
62
|
+
end.to_html
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The ways to look at the boxes, each with how many it lists.
|
|
66
|
+
def filter_links(burst, decisions, current)
|
|
67
|
+
safe_join(Decisions::FILTERS.map do |filter, name|
|
|
68
|
+
link_to(safe_join([name, tag.span(number_with_delimiter(decisions.count(filter)), class: "count")], " "),
|
|
69
|
+
burst_decisions_path(burst, show: filter), data: {filter:}, aria: {current: ("page" if filter == current)})
|
|
70
|
+
end)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def filter_name(filter) = Decisions::FILTERS.fetch(filter.to_s)
|
|
74
|
+
|
|
75
|
+
# "22 open, 2 ticked elsewhere": what a pull request's boxes are in, all of them.
|
|
76
|
+
def tally_of(decisions, stop)
|
|
77
|
+
decisions.tally(stop).map { |state, count| "#{number_with_delimiter(count)} #{filter_name(state).downcase}" }.to_sentence
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# What a box says of itself beyond being open: where it stands, and what
|
|
81
|
+
# else says it needs the owner. Nothing, for a box that is simply open.
|
|
82
|
+
def status_of(box)
|
|
83
|
+
parts = [("Starred by you" if box.starred), ("Named by #{named_by(box.named)}" if box.named.any?)]
|
|
84
|
+
parts.unshift(tag.span(filter_name(box.state), class: "state")) unless box.state == :open
|
|
85
|
+
safe_join(parts.compact, " · ")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def named_by(entries) = entries.filter_map(&:by).uniq.to_sentence.presence || "the burst file"
|
|
89
|
+
|
|
90
|
+
# The box's place on its pull request's page, which numbers its boxes.
|
|
91
|
+
def box_on_its_page(burst, box) = burst_pull_request_path(burst, box.stop.number, anchor: "decision-#{box.position}")
|
|
92
|
+
|
|
93
|
+
# The star, which comes back to the page it was pressed on, at `back`.
|
|
94
|
+
def star_button(burst, box, back)
|
|
95
|
+
button_to(box.starred ? "★ Starred" : "☆ Star", burst_pull_request_decision_path(burst, box.stop.number, box.position),
|
|
96
|
+
method: :patch, params: {lead: box.lead, starred: box.starred ? 0 : 1, return: back}, class: "quiet", form_class: "star", aria: {pressed: box.starred.to_s})
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# What a button under a box asks: the kind of note it writes, as a word of the owner's.
|
|
100
|
+
def respond_button(label, kind) = tag.button(label, type: "button", class: "quiet", data: {respond: kind})
|
|
101
|
+
end
|
|
102
|
+
end
|