Merge branch 'main' into delete-account-improvements
This commit is contained in:
@@ -5,10 +5,20 @@ class Boards::PublicationsController < ApplicationController
|
||||
|
||||
def create
|
||||
@board.publish
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { render partial: "boards/board", locals: { board: @board }, status: :created }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@board.unpublish
|
||||
@board.reload
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,9 +15,18 @@ class My::AccessTokensController < ApplicationController
|
||||
|
||||
def create
|
||||
access_token = my_access_tokens.create!(access_token_params)
|
||||
expiring_id = verifier.generate access_token.id, expires_in: 10.seconds
|
||||
|
||||
redirect_to my_access_token_path(expiring_id)
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
expiring_id = verifier.generate access_token.id, expires_in: 10.seconds
|
||||
redirect_to my_access_token_path(expiring_id)
|
||||
end
|
||||
|
||||
format.json do
|
||||
render status: :created, json: \
|
||||
{ token: access_token.token, description: access_token.description, permission: access_token.permission }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Public::Boards::Columns::ClosedsController < Public::BaseController
|
||||
def show
|
||||
set_page_and_extract_portion_from @board.cards.closed.recently_closed_first
|
||||
set_page_and_extract_portion_from @board.cards.closed.published.recently_closed_first
|
||||
end
|
||||
end
|
||||
|
||||
@@ -54,6 +54,12 @@ export default class extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
frameColumnOnMobile(event) {
|
||||
if (!this.#isDesktop) {
|
||||
event.currentTarget.scrollIntoView({ behavior: "smooth", inline: "center" })
|
||||
}
|
||||
}
|
||||
|
||||
async #restoreColumnsDisablingTransitions() {
|
||||
this.#disableTransitions()
|
||||
this.#restoreColumns()
|
||||
|
||||
@@ -7,7 +7,8 @@ export default class extends Controller {
|
||||
static values = {
|
||||
modal: { type: Boolean, default: false },
|
||||
sizing: { type: Boolean, default: true },
|
||||
autoOpen: { type: Boolean, default: false }
|
||||
autoOpen: { type: Boolean, default: false },
|
||||
orient: { type: Boolean, default: true }
|
||||
}
|
||||
|
||||
connect() {
|
||||
@@ -26,7 +27,9 @@ export default class extends Controller {
|
||||
this.dialogTarget.showModal()
|
||||
} else {
|
||||
this.dialogTarget.show()
|
||||
orient({ target: this.dialogTarget, anchor: this.element })
|
||||
if (this.orientValue) {
|
||||
orient({ target: this.dialogTarget, anchor: this.element })
|
||||
}
|
||||
}
|
||||
|
||||
this.loadLazyFrames()
|
||||
|
||||
@@ -4,4 +4,6 @@ json.cache! board do
|
||||
json.url board_url(board)
|
||||
|
||||
json.creator board.creator, partial: "users/user", as: :user
|
||||
|
||||
json.public_url published_board_url(board) if board.published?
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
drag_and_strum_target: "container",
|
||||
collapsible_columns_target: "column",
|
||||
controller: "clicker",
|
||||
action: "turbo:before-stream-render@document->collapsible-columns#restoreState focus->collapsible-columns#focusOnColumn"
|
||||
action: "turbo:before-stream-render@document->collapsible-columns#restoreState focus->collapsible-columns#focusOnColumn dialog:show->collapsible-columns#frameColumnOnMobile"
|
||||
} do %>
|
||||
<header class="cards__header">
|
||||
<%= render "boards/show/menu/column", column: column %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="<%= dom_id(column, :menu) %>" class="cards__menu" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
|
||||
<div id="<%= dom_id(column, :menu) %>" class="cards__menu" data-controller="dialog" data-dialog-orient-value="false" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
|
||||
<button class="btn btn--circle txt-x-small borderless" data-action="click->dialog#open">
|
||||
<%= icon_tag "menu-dots-horizontal", class: "translucent" %>
|
||||
<span class="for-screen-reader">Column options</span>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<p class="subtitle">Please enter this 6-character verification code on the Fizzy sign-in page:</p>
|
||||
<% else %>
|
||||
<h1 class="title">Welcome to Fizzy!</h1>
|
||||
<p class="subtitle">Please enter this 6-character verification code on the Fizzy sign-up page to create your new account:</p>
|
||||
<p class="subtitle">Please enter this 6-character verification code to finish creating your account:</p>
|
||||
<% end %>
|
||||
|
||||
<strong class="txt-large"><%= @magic_link.code %></strong>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<% if @magic_link.for_sign_in? %>
|
||||
Please enter this 6-character verification code on the Fizzy sign-in page:
|
||||
<% else %>
|
||||
Please enter this 6-character verification code on the Fizzy sign-up page to create your new account:
|
||||
Please enter this 6-character verification code to finish creating your account:
|
||||
<% end %>
|
||||
|
||||
<%= @magic_link.code %>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<div class="cards__transition-container">
|
||||
<header class="cards__header">
|
||||
<%= render "boards/show/expander", title: "Done", count: board.cards.closed.count, column_id: "closed-cards" %>
|
||||
<%= render "boards/show/expander", title: "Done", count: board.cards.closed.published.count, column_id: "closed-cards" %>
|
||||
|
||||
<%= link_to public_board_columns_closed_url(board.publication.key), class: "cards__maximize-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %>
|
||||
<%= icon_tag "grid", class: "translucent" %>
|
||||
|
||||
+90
-1
@@ -134,6 +134,48 @@ __Response:__
|
||||
|
||||
Returns `204 No Content` on success.
|
||||
|
||||
#### Create an access token via the API
|
||||
|
||||
You can programmatically create a personal access token using either a session cookie or an existing Bearer token:
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-H "Cookie: session_token=eyJfcmFpbHMi..." \
|
||||
-d '{"access_token": {"description": "Fizzy CLI", "permission": "write"}}' \
|
||||
https://app.fizzy.do/1234567/my/access_tokens
|
||||
```
|
||||
|
||||
Or with a Bearer token (must have `write` permission):
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-H "Authorization: Bearer put-your-access-token-here" \
|
||||
-d '{"access_token": {"description": "Fizzy CLI", "permission": "write"}}' \
|
||||
https://app.fizzy.do/1234567/my/access_tokens
|
||||
```
|
||||
|
||||
The `permission` field accepts `read` or `write`.
|
||||
|
||||
__Response:__
|
||||
|
||||
```
|
||||
HTTP/1.1 201 Created
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "4f9Q6d2wXr8Kp1Ls0Vz3BnTa",
|
||||
"description": "Fizzy CLI",
|
||||
"permission": "write"
|
||||
}
|
||||
```
|
||||
|
||||
Store the `token` value securely — it won't be retrievable again. Use it as a Bearer token for subsequent API requests.
|
||||
|
||||
## Caching
|
||||
|
||||
Most endpoints return [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag) and [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control) headers. You can use these to avoid re-downloading unchanged data.
|
||||
@@ -427,10 +469,13 @@ __Response:__
|
||||
"email_address": "david@example.com",
|
||||
"created_at": "2025-12-05T19:36:35.401Z",
|
||||
"url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7"
|
||||
}
|
||||
},
|
||||
"public_url": "http://fizzy.localhost:3006/897362094/public/boards/aB3dEfGhIjKlMnOp"
|
||||
}
|
||||
```
|
||||
|
||||
The `public_url` field is only present when the board is published.
|
||||
|
||||
### `POST /:account_slug/boards`
|
||||
|
||||
Creates a new Board in the account.
|
||||
@@ -502,6 +547,50 @@ __Response:__
|
||||
|
||||
Returns `204 No Content` on success.
|
||||
|
||||
## Board Publications
|
||||
|
||||
Publishing a board makes it publicly accessible via a shareable link, without requiring authentication. Only board administrators can publish or unpublish a board.
|
||||
|
||||
### `POST /:account_slug/boards/:board_id/publication`
|
||||
|
||||
Publishes a board, generating a shareable public link.
|
||||
|
||||
__Response:__
|
||||
|
||||
```
|
||||
HTTP/1.1 201 Created
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "03f5v9zkft4hj9qq0lsn9ohcm",
|
||||
"name": "Fizzy",
|
||||
"all_access": true,
|
||||
"created_at": "2025-12-05T19:36:35.534Z",
|
||||
"url": "http://fizzy.localhost:3006/897362094/boards/03f5v9zkft4hj9qq0lsn9ohcm",
|
||||
"creator": {
|
||||
"id": "03f5v9zjw7pz8717a4no1h8a7",
|
||||
"name": "David Heinemeier Hansson",
|
||||
"role": "owner",
|
||||
"active": true,
|
||||
"email_address": "david@example.com",
|
||||
"created_at": "2025-12-05T19:36:35.401Z",
|
||||
"url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7"
|
||||
},
|
||||
"public_url": "http://fizzy.localhost:3006/897362094/public/boards/aB3dEfGhIjKlMnOp"
|
||||
}
|
||||
```
|
||||
|
||||
If the board is already published, the existing publication is returned.
|
||||
|
||||
### `DELETE /:account_slug/boards/:board_id/publication`
|
||||
|
||||
Unpublishes a board, removing public access.
|
||||
|
||||
__Response:__
|
||||
|
||||
Returns `204 No Content` on success.
|
||||
|
||||
## Cards
|
||||
|
||||
Cards are tasks or items of work on a board. They can be organized into columns, tagged, assigned to users, and have comments.
|
||||
|
||||
@@ -27,6 +27,31 @@ class Boards::PublicationsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_turbo_stream action: :replace, target: dom_id(@board, :publication)
|
||||
end
|
||||
|
||||
test "publish a board via JSON" do
|
||||
assert_not @board.published?
|
||||
|
||||
assert_changes -> { @board.reload.published? }, from: false, to: true do
|
||||
post board_publication_path(@board), as: :json
|
||||
end
|
||||
|
||||
assert_response :created
|
||||
body = @response.parsed_body
|
||||
@board.reload
|
||||
assert_equal @board.name, body["name"]
|
||||
assert_equal published_board_url(@board), body["public_url"]
|
||||
end
|
||||
|
||||
test "unpublish a board via JSON" do
|
||||
@board.publish
|
||||
assert @board.published?
|
||||
|
||||
assert_changes -> { @board.reload.published? }, from: true, to: false do
|
||||
delete board_publication_path(@board), as: :json
|
||||
end
|
||||
|
||||
assert_response :no_content
|
||||
end
|
||||
|
||||
test "publish requires board admin permission" do
|
||||
logout_and_sign_in_as :jz
|
||||
|
||||
|
||||
@@ -250,6 +250,24 @@ class BoardsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_equal boards(:writebook).name, @response.parsed_body["name"]
|
||||
end
|
||||
|
||||
test "show as JSON includes public_url when published" do
|
||||
board = boards(:writebook)
|
||||
board.publish
|
||||
|
||||
get board_path(board), as: :json
|
||||
assert_response :success
|
||||
assert_equal published_board_url(board), @response.parsed_body["public_url"]
|
||||
end
|
||||
|
||||
test "show as JSON excludes public_url when not published" do
|
||||
board = boards(:writebook)
|
||||
assert_not board.published?
|
||||
|
||||
get board_path(board), as: :json
|
||||
assert_response :success
|
||||
assert_nil @response.parsed_body["public_url"]
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
assert_difference -> { Board.count }, +1 do
|
||||
post boards_path, params: { board: { name: "My new board" } }, as: :json
|
||||
|
||||
@@ -19,6 +19,41 @@ class My::AccessTokensControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
end
|
||||
|
||||
test "create new token via JSON with session" do
|
||||
assert_difference -> { identities(:kevin).access_tokens.count }, +1 do
|
||||
post my_access_tokens_path, params: { access_token: { description: "Fizzy CLI", permission: "write" } }, as: :json
|
||||
end
|
||||
assert_response :created
|
||||
body = @response.parsed_body
|
||||
assert body["token"].present?
|
||||
assert_equal "Fizzy CLI", body["description"]
|
||||
assert_equal "write", body["permission"]
|
||||
end
|
||||
|
||||
test "create new token via JSON with bearer token" do
|
||||
sign_out
|
||||
bearer_token = { "HTTP_AUTHORIZATION" => "Bearer #{identity_access_tokens(:davids_api_token).token}" }
|
||||
|
||||
assert_difference -> { identities(:david).access_tokens.count }, +1 do
|
||||
post my_access_tokens_path, params: { access_token: { description: "Fizzy CLI", permission: "read" } }, env: bearer_token, as: :json
|
||||
end
|
||||
assert_response :created
|
||||
body = @response.parsed_body
|
||||
assert body["token"].present?
|
||||
assert_equal "Fizzy CLI", body["description"]
|
||||
assert_equal "read", body["permission"]
|
||||
end
|
||||
|
||||
test "cannot create new token via JSON with read-only bearer token" do
|
||||
sign_out
|
||||
bearer_token = { "HTTP_AUTHORIZATION" => "Bearer #{identity_access_tokens(:jasons_api_token).token}" }
|
||||
|
||||
assert_no_difference -> { identities(:jason).access_tokens.count } do
|
||||
post my_access_tokens_path, params: { access_token: { description: "Fizzy CLI", permission: "read" } }, env: bearer_token, as: :json
|
||||
end
|
||||
assert_response :unauthorized
|
||||
end
|
||||
|
||||
test "accessing new token after reveal window redirects to index" do
|
||||
assert_changes -> { identities(:kevin).access_tokens.count }, +1 do
|
||||
post my_access_tokens_path, params: { access_token: { description: "GitHub", permission: "read" } }
|
||||
|
||||
@@ -9,4 +9,14 @@ class Public::Boards::Columns::ClosedsControllerTest < ActionDispatch::Integrati
|
||||
get public_board_columns_closed_path(boards(:writebook).publication.key)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "show excludes draft cards" do
|
||||
draft_card = cards(:buy_domain)
|
||||
draft_card.update!(status: :drafted)
|
||||
Current.set(user: users(:david)) { draft_card.close }
|
||||
|
||||
get public_board_columns_closed_path(boards(:writebook).publication.key)
|
||||
assert_response :success
|
||||
assert_not_includes response.body, draft_card.title
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,6 +21,16 @@ class Public::BoardsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_response :not_found
|
||||
end
|
||||
|
||||
test "show excludes draft cards from closed count" do
|
||||
draft_card = cards(:buy_domain)
|
||||
draft_card.update!(status: :drafted)
|
||||
Current.set(user: users(:david)) { draft_card.close }
|
||||
|
||||
get published_board_path(boards(:writebook))
|
||||
assert_response :success
|
||||
assert_select ".cards--closed .cards__expander-count", "1"
|
||||
end
|
||||
|
||||
test "show works without authentication" do
|
||||
sign_out
|
||||
get published_board_path(boards(:writebook))
|
||||
|
||||
Reference in New Issue
Block a user