@@ -6,6 +6,14 @@
|
||||
# Ignore bundler config.
|
||||
/.bundle
|
||||
|
||||
# Ignore documentation
|
||||
/docs/
|
||||
/README.md
|
||||
/CLAUDE.md
|
||||
/AGENTS.md
|
||||
/STYLE.md
|
||||
/CONTRIBUTING.md
|
||||
|
||||
# Ignore all environment files (except templates).
|
||||
/.env*
|
||||
!/.env*.erb
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
.access_tokens_table {
|
||||
border-collapse: collapse;
|
||||
inline-size: 100%;
|
||||
|
||||
td, th {
|
||||
border-block-end: 1px solid var(--color-ink-light);
|
||||
padding-inline: var(--inline-space);
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: var(--text-x-small);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
tr:nth-of-type(even) {
|
||||
background-color: var(--color-ink-lightest);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,11 @@ class Boards::ColumnsController < ApplicationController
|
||||
|
||||
before_action :set_column, only: %i[ show update destroy ]
|
||||
|
||||
def index
|
||||
@columns = @board.columns.sorted
|
||||
fresh_when etag: @columns
|
||||
end
|
||||
|
||||
def show
|
||||
set_page_and_extract_portion_from @column.cards.active.latest.with_golden_first.preloaded
|
||||
fresh_when etag: @page.records
|
||||
@@ -10,14 +15,29 @@ class Boards::ColumnsController < ApplicationController
|
||||
|
||||
def create
|
||||
@column = @board.columns.create!(column_params)
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :created, location: board_column_path(@board, @column, format: :json) }
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@column.update!(column_params)
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@column.destroy
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
class BoardsController < ApplicationController
|
||||
include FilterScoped
|
||||
|
||||
before_action :set_board, except: %i[ new create ]
|
||||
before_action :set_board, except: %i[ index new create ]
|
||||
before_action :ensure_permission_to_admin_board, only: %i[ update destroy ]
|
||||
|
||||
def index
|
||||
set_page_and_extract_portion_from Current.user.boards
|
||||
end
|
||||
|
||||
def show
|
||||
if @filter.used?(ignore_boards: true)
|
||||
show_filtered_cards
|
||||
@@ -18,7 +22,11 @@ class BoardsController < ApplicationController
|
||||
|
||||
def create
|
||||
@board = Board.create! board_params.with_defaults(all_access: true)
|
||||
redirect_to board_path(@board)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to board_path(@board) }
|
||||
format.json { head :created, location: board_path(@board, format: :json) }
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@@ -31,16 +39,25 @@ class BoardsController < ApplicationController
|
||||
@board.update! board_params
|
||||
@board.accesses.revise granted: grantees, revoked: revokees if grantees_changed?
|
||||
|
||||
if @board.accessible_to?(Current.user)
|
||||
redirect_to edit_board_path(@board), notice: "Saved"
|
||||
else
|
||||
redirect_to root_path, notice: "Saved (you were removed from the board)"
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
if @board.accessible_to?(Current.user)
|
||||
redirect_to edit_board_path(@board), notice: "Saved"
|
||||
else
|
||||
redirect_to root_path, notice: "Saved (you were removed from the board)"
|
||||
end
|
||||
end
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@board.destroy
|
||||
redirect_to root_path
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to root_path }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -9,5 +9,10 @@ class Cards::AssignmentsController < ApplicationController
|
||||
|
||||
def create
|
||||
@card.toggle_assignment @board.users.active.find(params[:assignee_id])
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,7 +11,11 @@ class Cards::BoardsController < ApplicationController
|
||||
|
||||
def update
|
||||
@card.move_to(@board)
|
||||
redirect_to @card
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to @card }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -3,11 +3,19 @@ class Cards::ClosuresController < ApplicationController
|
||||
|
||||
def create
|
||||
@card.close
|
||||
render_card_replacement
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream { render_card_replacement }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@card.reopen
|
||||
render_card_replacement
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream { render_card_replacement }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,10 +13,20 @@ class Cards::Comments::ReactionsController < ApplicationController
|
||||
|
||||
def create
|
||||
@reaction = @comment.reactions.create!(params.expect(reaction: :content))
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :created }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@reaction.destroy
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -4,8 +4,17 @@ class Cards::CommentsController < ApplicationController
|
||||
before_action :set_comment, only: %i[ show edit update destroy ]
|
||||
before_action :ensure_creatorship, only: %i[ edit update destroy ]
|
||||
|
||||
def index
|
||||
set_page_and_extract_portion_from @card.comments.chronologically
|
||||
end
|
||||
|
||||
def create
|
||||
@comment = @card.comments.create!(comment_params)
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :created, location: card_comment_path(@card, @comment, format: :json) }
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@@ -16,10 +25,20 @@ class Cards::CommentsController < ApplicationController
|
||||
|
||||
def update
|
||||
@comment.update! comment_params
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@comment.destroy
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -3,11 +3,19 @@ class Cards::GoldnessesController < ApplicationController
|
||||
|
||||
def create
|
||||
@card.gild
|
||||
render_card_replacement
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream { render_card_replacement }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@card.ungild
|
||||
render_card_replacement
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream { render_card_replacement }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,10 @@ class Cards::ImagesController < ApplicationController
|
||||
|
||||
def destroy
|
||||
@card.image.purge_later
|
||||
redirect_to @card
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to @card }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,10 @@ class Cards::NotNowsController < ApplicationController
|
||||
|
||||
def create
|
||||
@card.postpone
|
||||
render_card_replacement
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream { render_card_replacement }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,11 @@ class Cards::StepsController < ApplicationController
|
||||
|
||||
def create
|
||||
@step = @card.steps.create!(step_params)
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :created, location: card_step_path(@card, @step, format: :json) }
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@@ -15,10 +20,20 @@ class Cards::StepsController < ApplicationController
|
||||
|
||||
def update
|
||||
@step.update!(step_params)
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { render :show }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@step.destroy!
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -9,6 +9,11 @@ class Cards::TaggingsController < ApplicationController
|
||||
|
||||
def create
|
||||
@card.toggle_tag_with sanitized_tag_title_param
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -5,11 +5,18 @@ class Cards::TriagesController < ApplicationController
|
||||
column = @card.board.columns.find(params[:column_id])
|
||||
@card.triage_into(column)
|
||||
|
||||
redirect_to @card
|
||||
respond_to do |format|
|
||||
format.html { redirect_to @card }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@card.send_back_to_triage
|
||||
redirect_to @card
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to @card }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,9 +7,19 @@ class Cards::WatchesController < ApplicationController
|
||||
|
||||
def create
|
||||
@card.watch_by Current.user
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@card.unwatch_by Current.user
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,8 +10,18 @@ class CardsController < ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
card = @board.cards.find_or_create_by!(creator: Current.user, status: "drafted")
|
||||
redirect_to card
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
card = @board.cards.find_or_create_by!(creator: Current.user, status: "drafted")
|
||||
redirect_to card
|
||||
end
|
||||
|
||||
format.json do
|
||||
card = @board.cards.create! card_params.merge(creator: Current.user)
|
||||
card.publish
|
||||
head :created, location: card_path(card, format: :json)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@@ -22,11 +32,20 @@ class CardsController < ApplicationController
|
||||
|
||||
def update
|
||||
@card.update! card_params
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { render :show }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@card.destroy!
|
||||
redirect_to @card.board, notice: "Card deleted"
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to @card.board, notice: "Card deleted" }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -7,7 +7,7 @@ module Authentication
|
||||
after_action :ensure_development_magic_link_not_leaked
|
||||
helper_method :authenticated?
|
||||
|
||||
etag { Current.session.id if authenticated? }
|
||||
etag { Current.identity.id if authenticated? }
|
||||
|
||||
include LoginHelper
|
||||
end
|
||||
@@ -32,7 +32,7 @@ module Authentication
|
||||
|
||||
private
|
||||
def authenticated?
|
||||
Current.session.present?
|
||||
Current.identity.present?
|
||||
end
|
||||
|
||||
def require_account
|
||||
@@ -42,7 +42,7 @@ module Authentication
|
||||
end
|
||||
|
||||
def require_authentication
|
||||
resume_session || request_authentication
|
||||
resume_session || authenticate_by_bearer_token || request_authentication
|
||||
end
|
||||
|
||||
def resume_session
|
||||
@@ -55,6 +55,16 @@ module Authentication
|
||||
Session.find_signed(cookies.signed[:session_token])
|
||||
end
|
||||
|
||||
def authenticate_by_bearer_token
|
||||
if request.authorization.to_s.include?("Bearer")
|
||||
authenticate_or_request_with_http_token do |token|
|
||||
if identity = Identity.find_by_permissable_access_token(token, method: request.method)
|
||||
Current.identity = identity
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def request_authentication
|
||||
if Current.account.present?
|
||||
session[:return_to_after_authenticating] = request.url
|
||||
|
||||
@@ -12,7 +12,7 @@ module RequestForgeryProtection
|
||||
end
|
||||
|
||||
def verified_request?
|
||||
super || safe_fetch_site?
|
||||
super || safe_fetch_site? || request.format.json?
|
||||
end
|
||||
|
||||
SAFE_FETCH_SITES = %w[ same-origin same-site ]
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
class My::AccessTokensController < ApplicationController
|
||||
def index
|
||||
@access_tokens = my_access_tokens.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def show
|
||||
@access_token = my_access_tokens.find(verifier.verify(params[:id]))
|
||||
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
||||
redirect_to my_access_tokens_path, alert: "Token is no longer visible"
|
||||
end
|
||||
|
||||
def new
|
||||
@access_token = my_access_tokens.new
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
def destroy
|
||||
my_access_tokens.find(params[:id]).destroy!
|
||||
redirect_to my_access_tokens_path
|
||||
end
|
||||
|
||||
private
|
||||
def my_access_tokens
|
||||
Current.identity.access_tokens
|
||||
end
|
||||
|
||||
def access_token_params
|
||||
params.expect(access_token: %i[ description permission ])
|
||||
end
|
||||
|
||||
def verifier
|
||||
Rails.application.message_verifier(:access_tokens)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
class My::IdentitiesController < ApplicationController
|
||||
disallow_account_scope
|
||||
|
||||
def show
|
||||
@identity = Current.identity
|
||||
end
|
||||
end
|
||||
@@ -2,10 +2,15 @@ class Notifications::BulkReadingsController < ApplicationController
|
||||
def create
|
||||
Current.user.notifications.unread.read_all
|
||||
|
||||
if from_tray?
|
||||
head :ok
|
||||
else
|
||||
redirect_to notifications_path
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
if from_tray?
|
||||
head :ok
|
||||
else
|
||||
redirect_to notifications_path
|
||||
end
|
||||
end
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -2,10 +2,20 @@ class Notifications::ReadingsController < ApplicationController
|
||||
def create
|
||||
@notification = Current.user.notifications.find(params[:notification_id])
|
||||
@notification.read
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@notification = Current.user.notifications.find(params[:notification_id])
|
||||
@notification.unread
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
class NotificationsController < ApplicationController
|
||||
MAX_UNREAD_NOTIFICATIONS = 500
|
||||
MAX_UNREAD_NOTIFICATIONS_VIA_API = 100
|
||||
|
||||
def index
|
||||
@unread = Current.user.notifications.unread.ordered.preloaded.limit(MAX_UNREAD_NOTIFICATIONS) unless current_page_param
|
||||
@unread = Current.user.notifications.unread.ordered.preloaded.limit(max_unread_notifications) unless current_page_param
|
||||
set_page_and_extract_portion_from Current.user.notifications.read.ordered.preloaded
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream if current_page_param # Allows read-all action to side step pagination
|
||||
format.html
|
||||
format.json
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def max_unread_notifications
|
||||
request.format.json? ? MAX_UNREAD_NOTIFICATIONS_VIA_API : MAX_UNREAD_NOTIFICATIONS
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class TagsController < ApplicationController
|
||||
def index
|
||||
set_page_and_extract_portion_from Current.account.tags.alphabetically
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,11 @@
|
||||
class UsersController < ApplicationController
|
||||
before_action :set_user
|
||||
before_action :set_user, except: %i[ index ]
|
||||
before_action :ensure_permission_to_change_user, only: %i[ update destroy ]
|
||||
|
||||
def index
|
||||
set_page_and_extract_portion_from Current.account.users.active.alphabetically
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
@@ -10,15 +14,25 @@ class UsersController < ApplicationController
|
||||
|
||||
def update
|
||||
if @user.update(user_params)
|
||||
redirect_to @user
|
||||
respond_to do |format|
|
||||
format.html { redirect_to @user }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
respond_to do |format|
|
||||
format.html { render :edit, status: :unprocessable_entity }
|
||||
format.json { render json: @user.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@user.deactivate
|
||||
redirect_to users_path
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to users_path }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
+10
-4
@@ -1,13 +1,19 @@
|
||||
class Current < ActiveSupport::CurrentAttributes
|
||||
attribute :session, :user, :account
|
||||
attribute :session, :user, :identity, :account
|
||||
attribute :http_method, :request_id, :user_agent, :ip_address, :referrer
|
||||
|
||||
delegate :identity, to: :session, allow_nil: true
|
||||
|
||||
def session=(value)
|
||||
super(value)
|
||||
|
||||
if value.present? && account.present?
|
||||
if value.present?
|
||||
self.identity = session.identity
|
||||
end
|
||||
end
|
||||
|
||||
def identity=(identity)
|
||||
super(identity)
|
||||
|
||||
if identity.present?
|
||||
self.user = identity.users.find_by(account: account)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class Identity < ApplicationRecord
|
||||
include Joinable, Transferable
|
||||
|
||||
has_many :access_tokens, dependent: :destroy
|
||||
has_many :magic_links, dependent: :destroy
|
||||
has_many :sessions, dependent: :destroy
|
||||
has_many :users, dependent: :nullify
|
||||
@@ -13,6 +14,12 @@ class Identity < ApplicationRecord
|
||||
validates :email_address, format: { with: URI::MailTo::EMAIL_REGEXP }
|
||||
normalizes :email_address, with: ->(value) { value.strip.downcase.presence }
|
||||
|
||||
def self.find_by_permissable_access_token(token, method:)
|
||||
if (access_token = AccessToken.find_by(token: token)) && access_token.allows?(method)
|
||||
access_token.identity
|
||||
end
|
||||
end
|
||||
|
||||
def send_magic_link(**attributes)
|
||||
attributes[:purpose] = attributes.delete(:for) if attributes.key?(:for)
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
class Identity::AccessToken < ApplicationRecord
|
||||
belongs_to :identity
|
||||
|
||||
has_secure_token
|
||||
enum :permission, %w[ read write ].index_by(&:itself), default: :read
|
||||
|
||||
def allows?(method)
|
||||
method.in?(%w[ GET HEAD ]) || write?
|
||||
end
|
||||
end
|
||||
@@ -1,8 +1,7 @@
|
||||
json.cache! board do
|
||||
json.(board, :id, :name, :all_access)
|
||||
json.created_at board.created_at.utc
|
||||
json.url board_url(board)
|
||||
|
||||
json.creator do
|
||||
json.partial! "users/user", user: board.creator
|
||||
end
|
||||
json.creator board.creator, partial: "users/user", as: :user
|
||||
end
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @columns, partial: "columns/column", as: :column
|
||||
@@ -0,0 +1 @@
|
||||
json.partial! "columns/column", column: @column
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @page.records, partial: "boards/board", as: :board
|
||||
@@ -0,0 +1 @@
|
||||
json.partial! "boards/board", board: @board
|
||||
@@ -1,26 +1,20 @@
|
||||
json.cache! [ card, card.column&.color ] do
|
||||
json.(card, :id, :title, :status)
|
||||
json.cache! card do
|
||||
json.(card, :id, :number, :title, :status)
|
||||
json.description card.description.to_plain_text
|
||||
json.description_html card.description.to_s
|
||||
json.image_url card.image.presence && url_for(card.image)
|
||||
|
||||
json.tags card.tags.pluck(:title).sort
|
||||
|
||||
json.golden card.golden?
|
||||
json.last_active_at card.last_active_at.utc
|
||||
json.created_at card.created_at.utc
|
||||
|
||||
json.url card_url(card)
|
||||
|
||||
json.board do
|
||||
json.partial! "boards/board", locals: { board: card.board }
|
||||
end
|
||||
json.board card.board, partial: "boards/board", as: :board
|
||||
json.column card.column, partial: "columns/column", as: :column if card.column
|
||||
json.creator card.creator, partial: "users/user", as: :user
|
||||
|
||||
json.column do
|
||||
if card.column
|
||||
json.partial! "columns/column", column: card.column
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
json.creator do
|
||||
json.partial! "users/user", user: card.creator
|
||||
end
|
||||
json.comments_url card_comments_url(card)
|
||||
end
|
||||
|
||||
@@ -9,9 +9,7 @@ json.cache! comment do
|
||||
json.html comment.body.to_s
|
||||
end
|
||||
|
||||
json.creator do
|
||||
json.partial! "users/user", user: comment.creator
|
||||
end
|
||||
json.creator comment.creator, partial: "users/user", as: :user
|
||||
|
||||
json.reactions_url card_comment_reactions_url(comment.card_id, comment.id)
|
||||
json.url card_comment_url(comment.card_id, comment.id)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @page.records, partial: "cards/comments/comment", as: :comment
|
||||
@@ -0,0 +1,5 @@
|
||||
json.cache! reaction do
|
||||
json.(reaction, :id, :content)
|
||||
json.reacter reaction.reacter, partial: "users/user", as: :user
|
||||
json.url card_comment_reaction_url(reaction.comment.card, reaction.comment, reaction)
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @comment.reactions.ordered, partial: "cards/comments/reactions/reaction", as: :reaction
|
||||
@@ -0,0 +1 @@
|
||||
json.partial! "cards/comments/comment", comment: @comment
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @page.records, partial: "cards/card", as: :card
|
||||
@@ -0,0 +1,2 @@
|
||||
json.partial! "cards/card", card: @card
|
||||
json.steps @card.steps, partial: "steps/step", as: :step
|
||||
@@ -0,0 +1,3 @@
|
||||
json.cache! step do
|
||||
json.(step, :id, :content, :completed)
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
json.partial! "cards/steps/step", step: @step
|
||||
@@ -1,2 +1,4 @@
|
||||
json.(column, :id, :name, :color)
|
||||
json.created_at column.created_at.utc
|
||||
json.cache! column do
|
||||
json.(column, :id, :name, :color)
|
||||
json.created_at column.created_at.utc
|
||||
end
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<tr style="view-transition-name: <%= dom_id(access_token) %>">
|
||||
<td><strong><%= access_token.description %></strong></td>
|
||||
<td><%= access_token.permission.humanize %></td>
|
||||
<td><%= local_datetime_tag access_token.created_at, style: :datetime %></td>
|
||||
<td>
|
||||
<%= button_to my_access_token_path(access_token), method: :delete,
|
||||
class: "btn txt-negative btn--circle txt-x-small borderless fill-transparent",
|
||||
data: { turbo_confirm: "Are you sure you want to permanently revoke this access token?" } do %>
|
||||
<%= icon_tag "trash" %>
|
||||
<span class="for-screen-reader">Edit this token</span>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -0,0 +1,35 @@
|
||||
<% @page_title = "Personal access tokens" %>
|
||||
|
||||
<% content_for :header do %>
|
||||
<div class="header__actions header__actions--start">
|
||||
<%= back_link_to "My profile", user_path(Current.user), "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
|
||||
</div>
|
||||
|
||||
<h1 class="header__title"><%= @page_title %></h1>
|
||||
<% end %>
|
||||
|
||||
<section class="panel panel--wide shadow center webhooks">
|
||||
<% if @access_tokens.any? %>
|
||||
<p class="margin-none-block-start">Tokens you have generated that can be used to access the Fizzy API.</p>
|
||||
<table class="access_tokens_table margin-block-end-double max-width txt-small">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th>Permission</th>
|
||||
<th>Created</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= render partial: "my/access_tokens/access_token", collection: @access_tokens %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p class="margin-none-block-start">Personal access tokens can be used like a password to access the Fizzy developer API. You can have as many tokens as you need and revoke access to each one at any time.</p>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_my_access_token_path, class: "btn btn--link" do %>
|
||||
<%= icon_tag "add" %>
|
||||
<span>Generate a new access token</span>
|
||||
<% end %>
|
||||
</section>
|
||||
@@ -0,0 +1,29 @@
|
||||
<% @page_title = "Generate a personal access token" %>
|
||||
|
||||
<% content_for :header do %>
|
||||
<div class="header__actions header__actions--start">
|
||||
<%= back_link_to "tokens", my_access_tokens_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
|
||||
</div>
|
||||
|
||||
<h1 class="header__title"><%= @page_title %></h1>
|
||||
<% end %>
|
||||
|
||||
<article class="panel panel--wide shadow center txt-align-start" style="view-transition-name: <%= dom_id(@access_token) %>">
|
||||
<%= form_with model: @access_token, url: my_access_tokens_path, scope: :access_token, data: { controller: "form" }, html: { class: "flex flex-column gap" } do |form| %>
|
||||
<div class="flex flex-column gap-half">
|
||||
<strong><%= form.label :description, "Access token description" %></strong>
|
||||
<%= form.text_field :description, required: true, autofocus: true, class: "input", placeholder: "e.g. Github", data: { action: "keydown.esc@document->form#cancel" } %>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column gap-half">
|
||||
<strong><%= form.label :permission %></strong>
|
||||
<%= form.select :permission, options_for_select({ "Read" => "read", "Read + Write" => "write"}), {}, class: "input input--select" %>
|
||||
</div>
|
||||
|
||||
<%= form.button type: :submit, class: "btn btn--link center txt-medium" do %>
|
||||
<span>Generate access token</span>
|
||||
<% end %>
|
||||
|
||||
<%= link_to "Cancel and go back", my_access_tokens_path, data: { form_target: "cancel" }, hidden: true %>
|
||||
<% end %>
|
||||
</article>
|
||||
@@ -0,0 +1,26 @@
|
||||
<% @page_title = "New personal access token" %>
|
||||
|
||||
<% content_for :header do %>
|
||||
<div class="header__actions header__actions--start">
|
||||
<%= back_link_to "Tokens", my_access_tokens_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
|
||||
</div>
|
||||
|
||||
<h1 class="header__title"><%= @page_title %></h1>
|
||||
<% end %>
|
||||
|
||||
<article class="panel panel--wide shadow center txt-align-start" style="view-transition-name: <%= dom_id(@access_token) %>">
|
||||
<div class="flex flex-column gap">
|
||||
<label class="flex flex-column gap-half txt-align-start">
|
||||
<strong><%= @access_token.description %> (<%= @access_token.permission == "write" ? "Read + Write" : "Read" %>)</strong>
|
||||
<input type="text" value="<%= @access_token.token %>" class="input" readonly>
|
||||
</label>
|
||||
<p class="margin-none txt-small">Be sure to save this access token now because you won’t be able to see it again.</p>
|
||||
|
||||
<%= tag.button class: "btn btn--link center", data: {
|
||||
controller: "copy-to-clipboard", action: "copy-to-clipboard#copy",
|
||||
copy_to_clipboard_success_class: "btn--success", copy_to_clipboard_content_value: @access_token.token } do %>
|
||||
<%= icon_tag "copy-paste" %>
|
||||
<span>Copy access token</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,4 @@
|
||||
json.cache! account do
|
||||
json.(account, :id, :name, :slug)
|
||||
json.created_at account.created_at.utc
|
||||
end
|
||||
@@ -0,0 +1,4 @@
|
||||
json.accounts @identity.users do |user|
|
||||
json.partial! "my/identities/account", account: user.account
|
||||
json.user user, partial: "users/user", as: :user
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
json.cache! notification do
|
||||
json.(notification, :id)
|
||||
json.read notification.read?
|
||||
json.read_at notification.read_at&.utc
|
||||
json.created_at notification.created_at.utc
|
||||
|
||||
json.partial! "notifications/notification/#{notification.source_type.underscore}/body", notification: notification
|
||||
|
||||
json.creator notification.creator, partial: "users/user", as: :user
|
||||
|
||||
json.card do
|
||||
json.(notification.card, :id, :title, :status)
|
||||
json.url card_url(notification.card)
|
||||
end
|
||||
|
||||
json.url notification_url(notification)
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
json.array! (@unread || []) + @page.records, partial: "notifications/notification", as: :notification
|
||||
@@ -0,0 +1,2 @@
|
||||
json.title event_notification_title(notification.source)
|
||||
json.body event_notification_body(notification.source)
|
||||
@@ -0,0 +1,4 @@
|
||||
mention = notification.source
|
||||
|
||||
json.title "#{mention.mentioner.first_name} @mentioned you"
|
||||
json.body mention.source.mentionable_content.truncate(200)
|
||||
@@ -0,0 +1,5 @@
|
||||
json.cache! tag do
|
||||
json.(tag, :id, :title)
|
||||
json.created_at tag.created_at.utc
|
||||
json.url cards_url(tag_ids: [ tag ])
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @page.records, partial: "tags/tag", as: :tag
|
||||
@@ -1,13 +1,13 @@
|
||||
<div class="flex flex-column align-center gap txt-medium--responsive txt-medium">
|
||||
<div class="flex flex-column align-center gap">
|
||||
<% url = session_transfer_url(user.identity.transfer_id, script_name: nil) %>
|
||||
|
||||
<header class="full-width">
|
||||
<h2 class="divider txt-large">Link a device</h2>
|
||||
<p class="margin-none-block" id="session_transfer_label">Use this link to sign-in on another device</p>
|
||||
</header>
|
||||
|
||||
<label class="flex flex-column gap full-width">
|
||||
<div class="flex align-center gap justify-center">
|
||||
<strong id="session_transfer_label" class="txt-medium">Link to automatically log in on another device</strong>
|
||||
</div>
|
||||
<span class="flex align-center gap margin-inline">
|
||||
<input type="text" class="input fill-white" id="session_transfer_url" value="<%= url %>" aria-labelledby="session_transfer_label" readonly>
|
||||
</span>
|
||||
<input type="text" class="input fill-white" id="session_transfer_url" value="<%= url %>" aria-labelledby="session_transfer_label" readonly>
|
||||
</label>
|
||||
|
||||
<div class="flex align-center gap">
|
||||
@@ -34,4 +34,4 @@
|
||||
<span class="for-screen-reader">Copy auto-login link</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
json.cache! user do
|
||||
json.(user, :id, :name, :role, :active)
|
||||
|
||||
json.email_address user.identity.email_address
|
||||
json.email_address user.identity&.email_address
|
||||
json.created_at user.created_at.utc
|
||||
|
||||
json.url user_url(user)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @page.records, partial: "users/user", as: :user
|
||||
@@ -32,9 +32,9 @@
|
||||
<% if @user.verified? %>
|
||||
<div class="flex-inline center justify-center flex-wrap gap">
|
||||
<%= link_to "Which cards are assigned to #{me_or_you}?",
|
||||
cards_path(assignee_ids: [ @user.id ], sorted_by: "newest"), class: "btn", data: { turbo_frame: "_top" } %>
|
||||
cards_path(assignee_ids: [ @user.id ], sorted_by: "newest"), class: "btn btn--link", data: { turbo_frame: "_top" } %>
|
||||
<%= link_to "Which cards were added by #{me_or_you}?",
|
||||
cards_path(creator_ids: [ @user.id ], sorted_by: "newest"), class: "btn", data: { turbo_frame: "_top" } %>
|
||||
cards_path(creator_ids: [ @user.id ], sorted_by: "newest"), class: "btn btn--link", data: { turbo_frame: "_top" } %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -44,9 +44,19 @@
|
||||
<section class="panel shadow" style="--panel-size: 45ch;">
|
||||
<%= render "users/transfer", user: @user %>
|
||||
|
||||
<div class="flex flex-column align-center gap margin-block-start-double">
|
||||
<header class="full-width">
|
||||
<h2 class="divider txt-large margin-none-block">API</h2>
|
||||
</header>
|
||||
|
||||
<div class="flex align-center gap txt-normal">
|
||||
<%= link_to "Personal access tokens", my_access_tokens_path, class: "btn" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="center margin-block-start-double">
|
||||
<%= button_to session_url(script_name: nil), method: :delete, class: "btn btn--plain txt-link txt-small", data: { turbo: false } do %>
|
||||
<span>Sign out</span>
|
||||
<span>Sign out of Fizzy</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
json.partial! "users/user", user: @user
|
||||
@@ -9,11 +9,6 @@ json.cache! @event do
|
||||
end
|
||||
end
|
||||
|
||||
json.board do
|
||||
json.partial! "boards/board", locals: { board: @event.board }
|
||||
end
|
||||
|
||||
json.creator do
|
||||
json.partial! "users/user", user: @event.creator
|
||||
end
|
||||
json.board @event.board, partial: "boards/board", as: :board
|
||||
json.creator @event.creator, partial: "users/user", as: :user
|
||||
end
|
||||
|
||||
@@ -94,6 +94,8 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
resources :tags, only: :index
|
||||
|
||||
namespace :notifications do
|
||||
resource :settings
|
||||
resource :unsubscribe
|
||||
@@ -162,6 +164,8 @@ Rails.application.routes.draw do
|
||||
resource :landing
|
||||
|
||||
namespace :my do
|
||||
resource :identity, only: :show
|
||||
resources :access_tokens
|
||||
resources :pins
|
||||
resource :timezone
|
||||
resource :menu
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
class CreateIdentityAccessTokens < ActiveRecord::Migration[8.2]
|
||||
def change
|
||||
create_table :identity_access_tokens, id: :uuid do |t|
|
||||
t.uuid :identity_id, null: false
|
||||
t.string :token
|
||||
t.string :permission
|
||||
t.text :description
|
||||
|
||||
t.timestamps
|
||||
|
||||
t.index ["identity_id"], name: "index_access_token_on_identity_id"
|
||||
end
|
||||
end
|
||||
end
|
||||
Generated
+10
@@ -321,6 +321,16 @@ ActiveRecord::Schema[8.2].define(version: 2025_12_05_010536) do
|
||||
t.index ["email_address"], name: "index_identities_on_email_address", unique: true
|
||||
end
|
||||
|
||||
create_table "identity_access_tokens", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.text "description"
|
||||
t.uuid "identity_id", null: false
|
||||
t.string "permission"
|
||||
t.string "token"
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["identity_id"], name: "index_access_token_on_identity_id"
|
||||
end
|
||||
|
||||
create_table "magic_links", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "code", null: false
|
||||
t.datetime "created_at", null: false
|
||||
|
||||
@@ -321,6 +321,16 @@ ActiveRecord::Schema[8.2].define(version: 2025_12_05_010536) do
|
||||
t.index ["email_address"], name: "index_identities_on_email_address", unique: true
|
||||
end
|
||||
|
||||
create_table "identity_access_tokens", id: :uuid, force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.text "description", limit: 65535
|
||||
t.uuid "identity_id", null: false
|
||||
t.string "permission", limit: 255
|
||||
t.string "token", limit: 255
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["identity_id"], name: "index_access_token_on_identity_id"
|
||||
end
|
||||
|
||||
create_table "magic_links", id: :uuid, force: :cascade do |t|
|
||||
t.string "code", limit: 255, null: false
|
||||
t.datetime "created_at", null: false
|
||||
|
||||
+1146
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
require "test_helper"
|
||||
|
||||
class ApiTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@davids_bearer_token = bearer_token_env(identity_access_tokens(:davids_api_token).token)
|
||||
@jasons_bearer_token = bearer_token_env(identity_access_tokens(:jasons_api_token).token)
|
||||
end
|
||||
|
||||
test "authenticate with valid access token" do
|
||||
get boards_path(format: :json), env: @davids_bearer_token
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "fail to authenticate with invalid access token" do
|
||||
get boards_path(format: :json), env: bearer_token_env("nonsense")
|
||||
assert_response :unauthorized
|
||||
end
|
||||
|
||||
test "changing data requires a write-endowed access token" do
|
||||
post boards_path(format: :json), params: { board: { name: "My new board" } }, env: @jasons_bearer_token
|
||||
assert_response :unauthorized
|
||||
|
||||
post boards_path(format: :json), params: { board: { name: "My new board" } }, env: @davids_bearer_token
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
private
|
||||
def bearer_token_env(token)
|
||||
{ "HTTP_AUTHORIZATION" => "Bearer #{token}" }
|
||||
end
|
||||
end
|
||||
@@ -36,4 +36,52 @@ class Boards::ColumnsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_response :success
|
||||
end
|
||||
end
|
||||
|
||||
test "index as JSON" do
|
||||
board = boards(:writebook)
|
||||
|
||||
get board_columns_path(board), as: :json
|
||||
|
||||
assert_response :success
|
||||
assert_equal board.columns.count, @response.parsed_body.count
|
||||
end
|
||||
|
||||
test "show as JSON" do
|
||||
column = columns(:writebook_in_progress)
|
||||
|
||||
get board_column_path(column.board, column), as: :json
|
||||
|
||||
assert_response :success
|
||||
assert_equal column.id, @response.parsed_body["id"]
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
board = boards(:writebook)
|
||||
|
||||
assert_difference -> { board.columns.count }, +1 do
|
||||
post board_columns_path(board), params: { column: { name: "New Column" } }, as: :json
|
||||
end
|
||||
|
||||
assert_response :created
|
||||
assert_equal board_column_path(board, Column.last, format: :json), @response.headers["Location"]
|
||||
end
|
||||
|
||||
test "update as JSON" do
|
||||
column = columns(:writebook_in_progress)
|
||||
|
||||
put board_column_path(column.board, column), params: { column: { name: "Updated Name" } }, as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_equal "Updated Name", column.reload.name
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
column = columns(:writebook_on_hold)
|
||||
|
||||
assert_difference -> { column.board.columns.count }, -1 do
|
||||
delete board_column_path(column.board, column), as: :json
|
||||
end
|
||||
|
||||
assert_response :no_content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -190,4 +190,44 @@ class BoardsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_select "input.switch__input[name='user_ids[]'][value='#{david.id}'][disabled]"
|
||||
end
|
||||
end
|
||||
|
||||
test "index as JSON" do
|
||||
get boards_path, as: :json
|
||||
assert_response :success
|
||||
assert_equal users(:kevin).boards.count, @response.parsed_body.count
|
||||
end
|
||||
|
||||
test "show as JSON" do
|
||||
get board_path(boards(:writebook)), as: :json
|
||||
assert_response :success
|
||||
assert_equal boards(:writebook).name, @response.parsed_body["name"]
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
assert_difference -> { Board.count }, +1 do
|
||||
post boards_path, params: { board: { name: "My new board" } }, as: :json
|
||||
end
|
||||
|
||||
assert_response :created
|
||||
assert_equal board_path(Board.last, format: :json), @response.headers["Location"]
|
||||
end
|
||||
|
||||
test "update as JSON" do
|
||||
board = boards(:writebook)
|
||||
|
||||
put board_path(board), params: { board: { name: "Updated Name" } }, as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_equal "Updated Name", board.reload.name
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
board = boards(:writebook)
|
||||
|
||||
assert_difference -> { Board.count }, -1 do
|
||||
delete board_path(board), as: :json
|
||||
end
|
||||
|
||||
assert_response :no_content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,6 +22,20 @@ class Cards::AssignmentsControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
card = cards(:logo)
|
||||
|
||||
assert_not card.assigned_to?(users(:david))
|
||||
|
||||
post card_assignments_path(card), params: { assignee_id: users(:david).id }, as: :json
|
||||
assert_response :no_content
|
||||
assert card.reload.assigned_to?(users(:david))
|
||||
|
||||
post card_assignments_path(card), params: { assignee_id: users(:david).id }, as: :json
|
||||
assert_response :no_content
|
||||
assert_not card.reload.assigned_to?(users(:david))
|
||||
end
|
||||
|
||||
private
|
||||
def assert_meta_replaced(card)
|
||||
assert_turbo_stream action: :replace, target: dom_id(card, :meta)
|
||||
|
||||
@@ -17,4 +17,16 @@ class Cards::BoardsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
assert_redirected_to card
|
||||
end
|
||||
|
||||
test "update as JSON" do
|
||||
card = cards(:logo)
|
||||
new_board = boards(:private)
|
||||
|
||||
assert_not_equal new_board, card.board
|
||||
|
||||
put card_board_path(card), params: { board_id: new_board.id }, as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_equal new_board, card.reload.board
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,7 +9,7 @@ class Cards::ClosuresControllerTest < ActionDispatch::IntegrationTest
|
||||
card = cards(:logo)
|
||||
|
||||
assert_changes -> { card.reload.closed? }, from: false, to: true do
|
||||
post card_closure_path(card)
|
||||
post card_closure_path(card), as: :turbo_stream
|
||||
assert_card_container_rerendered(card)
|
||||
end
|
||||
end
|
||||
@@ -18,8 +18,30 @@ class Cards::ClosuresControllerTest < ActionDispatch::IntegrationTest
|
||||
card = cards(:shipping)
|
||||
|
||||
assert_changes -> { card.reload.closed? }, from: true, to: false do
|
||||
delete card_closure_path(card)
|
||||
delete card_closure_path(card), as: :turbo_stream
|
||||
assert_card_container_rerendered(card)
|
||||
end
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
card = cards(:logo)
|
||||
|
||||
assert_not card.closed?
|
||||
|
||||
post card_closure_path(card), as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert card.reload.closed?
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
card = cards(:shipping)
|
||||
|
||||
assert card.closed?
|
||||
|
||||
delete card_closure_path(card), as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_not card.reload.closed?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,6 +7,11 @@ class Cards::Comments::ReactionsControllerTest < ActionDispatch::IntegrationTest
|
||||
@card = @comment.card
|
||||
end
|
||||
|
||||
test "index" do
|
||||
get card_comment_reactions_path(@card, @comment)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "create" do
|
||||
assert_difference -> { @comment.reactions.count }, 1 do
|
||||
post card_comment_reactions_path(@comment.card, @comment, format: :turbo_stream), params: { reaction: { content: "Great work!" } }
|
||||
@@ -30,4 +35,29 @@ class Cards::Comments::ReactionsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_response :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
test "index as JSON" do
|
||||
get card_comment_reactions_path(@card, @comment), as: :json
|
||||
|
||||
assert_response :success
|
||||
assert_equal @comment.reactions.count, @response.parsed_body.count
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
assert_difference -> { @comment.reactions.count }, 1 do
|
||||
post card_comment_reactions_path(@card, @comment), params: { reaction: { content: "👍" } }, as: :json
|
||||
end
|
||||
|
||||
assert_response :created
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
reaction = reactions(:david)
|
||||
|
||||
assert_difference -> { @comment.reactions.count }, -1 do
|
||||
delete card_comment_reaction_path(@card, @comment, reaction), as: :json
|
||||
end
|
||||
|
||||
assert_response :no_content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,4 +27,51 @@ class Cards::CommentsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
assert_response :forbidden
|
||||
end
|
||||
|
||||
test "index as JSON" do
|
||||
card = cards(:logo)
|
||||
|
||||
get card_comments_path(card), as: :json
|
||||
|
||||
assert_response :success
|
||||
assert_equal card.comments.count, @response.parsed_body.count
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
card = cards(:logo)
|
||||
|
||||
assert_difference -> { card.comments.count }, +1 do
|
||||
post card_comments_path(card), params: { comment: { body: "New comment" } }, as: :json
|
||||
end
|
||||
|
||||
assert_response :created
|
||||
assert_equal card_comment_path(card, Comment.last, format: :json), @response.headers["Location"]
|
||||
end
|
||||
|
||||
test "show as JSON" do
|
||||
comment = comments(:logo_agreement_kevin)
|
||||
|
||||
get card_comment_path(cards(:logo), comment), as: :json
|
||||
|
||||
assert_response :success
|
||||
assert_equal comment.id, @response.parsed_body["id"]
|
||||
end
|
||||
|
||||
test "update as JSON" do
|
||||
comment = comments(:logo_agreement_kevin)
|
||||
|
||||
put card_comment_path(cards(:logo), comment), params: { comment: { body: "Updated comment" } }, as: :json
|
||||
|
||||
assert_response :success
|
||||
assert_equal "Updated comment", comment.reload.body.to_plain_text
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
comment = comments(:logo_agreement_kevin)
|
||||
|
||||
delete card_comment_path(cards(:logo), comment), as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_not Comment.exists?(comment.id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,4 +18,26 @@ class Cards::GoldnessesControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_card_container_rerendered(cards(:logo))
|
||||
end
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
card = cards(:text)
|
||||
|
||||
assert_not card.golden?
|
||||
|
||||
post card_goldness_path(card), as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert card.reload.golden?
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
card = cards(:logo)
|
||||
|
||||
assert card.golden?
|
||||
|
||||
delete card_goldness_path(card), as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_not card.reload.golden?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
require "test_helper"
|
||||
|
||||
class Cards::ImagesControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in_as :kevin
|
||||
end
|
||||
|
||||
test "destroy" do
|
||||
card = cards(:logo)
|
||||
card.image.attach(io: file_fixture("moon.jpg").open, filename: "moon.jpg")
|
||||
|
||||
assert card.image.attached?
|
||||
|
||||
delete card_image_path(card)
|
||||
|
||||
assert_redirected_to card
|
||||
assert_not card.reload.image.attached?
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
card = cards(:logo)
|
||||
card.image.attach(io: file_fixture("moon.jpg").open, filename: "moon.jpg")
|
||||
|
||||
assert card.image.attached?
|
||||
|
||||
delete card_image_path(card), as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_not card.reload.image.attached?
|
||||
end
|
||||
end
|
||||
@@ -9,8 +9,19 @@ class Cards::NotNowsControllerTest < ActionDispatch::IntegrationTest
|
||||
card = cards(:logo)
|
||||
|
||||
assert_changes -> { card.reload.postponed? }, from: false, to: true do
|
||||
post card_not_now_path(card)
|
||||
post card_not_now_path(card), as: :turbo_stream
|
||||
assert_card_container_rerendered(card)
|
||||
end
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
card = cards(:logo)
|
||||
|
||||
assert_not card.postponed?
|
||||
|
||||
post card_not_now_path(card), as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert card.reload.postponed?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -52,4 +52,47 @@ class Cards::StepsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_turbo_stream action: :replace, target: dom_id(step)
|
||||
end
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
card = cards(:logo)
|
||||
|
||||
assert_difference -> { card.steps.count }, +1 do
|
||||
post card_steps_path(card), params: { step: { content: "New step" } }, as: :json
|
||||
end
|
||||
|
||||
assert_response :created
|
||||
assert_equal card_step_path(card, Step.last, format: :json), @response.headers["Location"]
|
||||
end
|
||||
|
||||
test "show as JSON" do
|
||||
card = cards(:logo)
|
||||
step = card.steps.create!(content: "Test step")
|
||||
|
||||
get card_step_path(card, step), as: :json
|
||||
|
||||
assert_response :success
|
||||
assert_equal step.id, @response.parsed_body["id"]
|
||||
assert_equal "Test step", @response.parsed_body["content"]
|
||||
end
|
||||
|
||||
test "update as JSON" do
|
||||
card = cards(:logo)
|
||||
step = card.steps.create!(content: "Original")
|
||||
|
||||
put card_step_path(card, step), params: { step: { content: "Updated" } }, as: :json
|
||||
|
||||
assert_response :success
|
||||
assert_equal "Updated", step.reload.content
|
||||
assert_equal "Updated", @response.parsed_body["content"]
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
card = cards(:logo)
|
||||
step = card.steps.create!(content: "To delete")
|
||||
|
||||
delete card_step_path(card, step), as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_not Step.exists?(step.id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -23,4 +23,26 @@ class Cards::TaggingsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_turbo_stream action: :replace, target: dom_id(cards(:logo), :tags)
|
||||
end
|
||||
end
|
||||
|
||||
test "toggle tag on as JSON" do
|
||||
card = cards(:logo)
|
||||
|
||||
assert_not card.tagged_with?(tags(:mobile))
|
||||
|
||||
post card_taggings_path(card), params: { tag_title: tags(:mobile).title }, as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert card.reload.tagged_with?(tags(:mobile))
|
||||
end
|
||||
|
||||
test "toggle tag off as JSON" do
|
||||
card = cards(:logo)
|
||||
|
||||
assert card.tagged_with?(tags(:web))
|
||||
|
||||
post card_taggings_path(card), params: { tag_title: tags(:web).title }, as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_not card.reload.tagged_with?(tags(:web))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,4 +24,25 @@ class Cards::TriagesControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_redirected_to card
|
||||
end
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
card = cards(:logo)
|
||||
column = columns(:writebook_in_progress)
|
||||
|
||||
post card_triage_path(card, column_id: column.id), as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_equal column, card.reload.column
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
card = cards(:shipping)
|
||||
|
||||
assert card.column.present?
|
||||
|
||||
delete card_triage_path(card), as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_nil card.reload.column
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,7 +9,7 @@ class Cards::WatchesControllerTest < ActionDispatch::IntegrationTest
|
||||
cards(:logo).unwatch_by users(:kevin)
|
||||
|
||||
assert_changes -> { cards(:logo).watched_by?(users(:kevin)) }, from: false, to: true do
|
||||
post card_watch_path(cards(:logo))
|
||||
post card_watch_path(cards(:logo)), as: :turbo_stream
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,7 +17,31 @@ class Cards::WatchesControllerTest < ActionDispatch::IntegrationTest
|
||||
cards(:logo).watch_by users(:kevin)
|
||||
|
||||
assert_changes -> { cards(:logo).watched_by?(users(:kevin)) }, from: true, to: false do
|
||||
delete card_watch_path(cards(:logo))
|
||||
delete card_watch_path(cards(:logo)), as: :turbo_stream
|
||||
end
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
card = cards(:logo)
|
||||
card.unwatch_by users(:kevin)
|
||||
|
||||
assert_not card.watched_by?(users(:kevin))
|
||||
|
||||
post card_watch_path(card), as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert card.reload.watched_by?(users(:kevin))
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
card = cards(:logo)
|
||||
card.watch_by users(:kevin)
|
||||
|
||||
assert card.watched_by?(users(:kevin))
|
||||
|
||||
delete card_watch_path(card), as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_not card.reload.watched_by?(users(:kevin))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -132,4 +132,42 @@ class CardsControllerTest < ActionDispatch::IntegrationTest
|
||||
get card_path(card)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "show as JSON" do
|
||||
get card_path(cards(:logo)), as: :json
|
||||
assert_response :success
|
||||
assert_equal cards(:logo).title, @response.parsed_body["title"]
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
assert_difference -> { Card.count }, +1 do
|
||||
post board_cards_path(boards(:writebook)),
|
||||
params: { card: { title: "My new card", description: "Big if true", tag_ids: [ tags(:web).id, tags(:mobile).id ] } },
|
||||
as: :json
|
||||
end
|
||||
|
||||
assert_response :created
|
||||
assert_equal card_path(Card.last, format: :json), @response.headers["Location"]
|
||||
|
||||
card = Card.last
|
||||
assert_equal "My new card", card.title
|
||||
assert_equal "Big if true", card.description.to_plain_text
|
||||
assert_equal [ tags(:mobile), tags(:web) ].sort, card.tags.sort
|
||||
end
|
||||
|
||||
test "update as JSON" do
|
||||
card = cards(:logo)
|
||||
put card_path(card, format: :json), params: { card: { title: "Update test" } }
|
||||
|
||||
assert_response :success
|
||||
assert_equal "Update test", card.reload.title
|
||||
end
|
||||
|
||||
test "delete as JSON" do
|
||||
card = cards(:logo)
|
||||
delete card_path(card, format: :json)
|
||||
|
||||
assert_response :no_content
|
||||
assert_not Card.exists?(card.id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
require "test_helper"
|
||||
|
||||
class My::AccessTokensControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in_as :kevin
|
||||
end
|
||||
|
||||
test "create new token" do
|
||||
get my_access_tokens_path
|
||||
assert_response :success
|
||||
|
||||
get new_my_access_token_path
|
||||
assert_response :success
|
||||
|
||||
assert_changes -> { identities(:kevin).access_tokens.count }, +1 do
|
||||
post my_access_tokens_path, params: { access_token: { description: "GitHub", permission: "read" } }
|
||||
follow_redirect!
|
||||
assert_in_body identities(:kevin).access_tokens.last.token
|
||||
end
|
||||
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" } }
|
||||
travel_to 15.seconds.from_now
|
||||
follow_redirect!
|
||||
assert_equal "Token is no longer visible", flash[:alert]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
require "test_helper"
|
||||
|
||||
class My::IdentitiesControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in_as :kevin
|
||||
end
|
||||
|
||||
test "show as JSON" do
|
||||
identity = identities(:kevin)
|
||||
|
||||
untenanted do
|
||||
get my_identity_path, as: :json
|
||||
assert_response :success
|
||||
assert_equal identity.accounts.count, @response.parsed_body["accounts"].count
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -22,4 +22,14 @@ class Notifications::BulkReadingsControllerTest < ActionDispatch::IntegrationTes
|
||||
post bulk_reading_path, params: { from_tray: true }
|
||||
assert_response :ok
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
assert_changes -> { notifications(:logo_published_kevin).reload.read? }, from: false, to: true do
|
||||
assert_changes -> { notifications(:layout_commented_kevin).reload.read? }, from: false, to: true do
|
||||
post bulk_reading_path, as: :json
|
||||
end
|
||||
end
|
||||
|
||||
assert_response :no_content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,4 +21,21 @@ class Notifications::ReadingsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_response :success
|
||||
end
|
||||
end
|
||||
|
||||
test "create as JSON" do
|
||||
assert_changes -> { notifications(:logo_published_kevin).reload.read? }, from: false, to: true do
|
||||
post notification_reading_path(notifications(:logo_published_kevin)), as: :json
|
||||
assert_response :no_content
|
||||
end
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
notification = notifications(:logo_published_kevin)
|
||||
notification.read
|
||||
|
||||
assert_changes -> { notification.reload.read? }, from: true, to: false do
|
||||
delete notification_reading_path(notification), as: :json
|
||||
assert_response :no_content
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,27 @@
|
||||
require "test_helper"
|
||||
|
||||
class NotificationsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in_as :kevin
|
||||
end
|
||||
|
||||
test "index as JSON" do
|
||||
get notifications_path, as: :json
|
||||
|
||||
assert_response :success
|
||||
assert_kind_of Array, @response.parsed_body
|
||||
assert @response.parsed_body.any? { |n| n["id"] == notifications(:logo_published_kevin).id }
|
||||
end
|
||||
|
||||
test "index as JSON includes notification attributes" do
|
||||
get notifications_path, as: :json
|
||||
|
||||
notification = @response.parsed_body.find { |n| n["id"] == notifications(:logo_published_kevin).id }
|
||||
|
||||
assert_not_nil notification["title"]
|
||||
assert_not_nil notification["body"]
|
||||
assert_not_nil notification["created_at"]
|
||||
assert_not_nil notification["card"]
|
||||
assert_not_nil notification["creator"]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
require "test_helper"
|
||||
|
||||
class TagsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in_as :kevin
|
||||
end
|
||||
|
||||
test "index as JSON" do
|
||||
tags = users(:kevin).account.tags.alphabetically
|
||||
|
||||
get tags_path, as: :json
|
||||
assert_response :success
|
||||
assert_equal tags.count, @response.parsed_body.count
|
||||
assert_equal tags.pluck(:title), @response.parsed_body.pluck("title")
|
||||
end
|
||||
end
|
||||
@@ -86,4 +86,50 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
|
||||
assert users(:kevin).reload.avatar.attached?
|
||||
assert_equal "image/png", users(:kevin).avatar.content_type
|
||||
end
|
||||
|
||||
test "index as JSON" do
|
||||
sign_in_as :kevin
|
||||
|
||||
get users_path, as: :json
|
||||
assert_response :success
|
||||
assert_equal users(:kevin).account.users.active.count, @response.parsed_body.count
|
||||
end
|
||||
|
||||
test "show as JSON" do
|
||||
sign_in_as :kevin
|
||||
|
||||
get user_path(users(:david)), as: :json
|
||||
assert_response :success
|
||||
assert_equal users(:david).name, @response.parsed_body["name"]
|
||||
end
|
||||
|
||||
test "update as JSON" do
|
||||
sign_in_as :kevin
|
||||
|
||||
put user_path(users(:david)), params: { user: { name: "New David" } }, as: :json
|
||||
|
||||
assert_response :no_content
|
||||
assert_equal "New David", users(:david).reload.name
|
||||
end
|
||||
|
||||
test "update as JSON with invalid avatar returns errors" do
|
||||
sign_in_as :kevin
|
||||
|
||||
svg_file = fixture_file_upload("avatar.svg", "image/svg+xml")
|
||||
|
||||
put user_path(users(:kevin), format: :json), params: { user: { avatar: svg_file } }
|
||||
|
||||
assert_response :unprocessable_entity
|
||||
assert @response.parsed_body["avatar"].present?
|
||||
end
|
||||
|
||||
test "destroy as JSON" do
|
||||
sign_in_as :kevin
|
||||
|
||||
assert_difference -> { User.active.count }, -1 do
|
||||
delete user_path(users(:david)), as: :json
|
||||
end
|
||||
|
||||
assert_response :no_content
|
||||
end
|
||||
end
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
jasons_api_token:
|
||||
identity: jason
|
||||
token: 018cf1425682700098f24f0799e3fe20
|
||||
description: My Superscript
|
||||
permission: read
|
||||
|
||||
davids_api_token:
|
||||
identity: david
|
||||
token: x18cf1425682700098f24f0799e3fe20
|
||||
description: My Superscript
|
||||
permission: write
|
||||
@@ -0,0 +1,4 @@
|
||||
require "test_helper"
|
||||
|
||||
class Identity::AccessTokenTest < ActiveSupport::TestCase
|
||||
end
|
||||
Reference in New Issue
Block a user