diff --git a/Gemfile b/Gemfile index 533a87e9b..efd0166b4 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ gem "solid_queue", "~> 1.2" gem "sqlite3", ">= 2.0" gem "thruster", require: false source "https://e95ae463b12de3f204526a44650f6ae0@gems.stanko.io/private" do - gem "beamer-rails" + gem "beamer-rails", "~> 0.1.0.beta4" end # Features @@ -55,9 +55,6 @@ gem "prometheus-client-mmap", "~> 1.1" gem "autotuner" gem "benchmark" # indirect dependency, being removed from Ruby 3.5 stdlib so here to quash warnings -# AI -gem "sqlite-vec", "0.1.7.alpha.2" - group :development, :test do gem "debug" gem "bundler-audit", require: false diff --git a/Gemfile.lock b/Gemfile.lock index eb6ae5aa8..cfbb22c7b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -36,7 +36,7 @@ GIT GIT remote: https://github.com/rails/rails.git - revision: def13087d55a6dbbfa80d114c847555927ce2ed7 + revision: d3d17c297a70d3e255a34693a1fb0074712e9930 branch: main specs: actioncable (8.2.0.alpha) @@ -146,17 +146,15 @@ PATH GEM remote: https://gems.stanko.io/private/ specs: - beamer-rails (0.0.6-arm64-darwin) + beamer-rails (0.1.0.beta4-arm64-darwin) sqlite3 (>= 2.0) - beamer-rails (0.0.6-arm64-linux) + beamer-rails (0.1.0.beta4-arm64-linux-gnu) sqlite3 (>= 2.0) - beamer-rails (0.0.6-x86_64-darwin) + beamer-rails (0.1.0.beta4-x86_64-darwin) sqlite3 (>= 2.0) - beamer-rails (0.0.6-x86_64-linux) + beamer-rails (0.1.0.beta4-x86_64-linux-gnu) sqlite3 (>= 2.0) - beamer-rails (0.0.6-x86_64-linux-gnu) - sqlite3 (>= 2.0) - beamer-rails (0.0.6-x86_64-linux-musl) + beamer-rails (0.1.0.beta4-x86_64-linux-musl) sqlite3 (>= 2.0) GEM @@ -267,7 +265,7 @@ GEM activesupport (>= 6.0.0) railties (>= 6.0.0) io-console (0.8.1) - irb (1.15.2) + irb (1.15.3) pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) @@ -296,7 +294,7 @@ GEM logger (~> 1.6) letter_opener (1.10.0) launchy (>= 2.2, < 4) - lexxy (0.1.13.beta) + lexxy (0.1.18.beta) rails (>= 8.0.2) lint_roller (1.1.0) logger (1.7.0) @@ -345,7 +343,7 @@ GEM net-smtp (0.5.1) net-protocol net-ssh (7.3.0) - nio4r (2.7.4) + nio4r (2.7.5) nokogiri (1.18.10) mini_portile2 (~> 2.8.2) racc (~> 1.4) @@ -407,7 +405,7 @@ GEM nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (3.2.3) + rack (3.2.4) rack-session (2.1.1) base64 (>= 0.1.0) rack (>= 3.0.0) @@ -427,7 +425,7 @@ GEM rake-compiler-dock (1.9.1) rb_sys (0.9.117) rake-compiler-dock (= 1.9.1) - rdoc (6.15.0) + rdoc (6.15.1) erb psych (>= 4.0.0) tsort @@ -508,10 +506,6 @@ GEM fugit (~> 1.11) railties (>= 7.1) thor (>= 1.3.1) - sqlite-vec (0.1.7.alpha.2-arm64-darwin) - sqlite-vec (0.1.7.alpha.2-arm64-linux) - sqlite-vec (0.1.7.alpha.2-x86_64-darwin) - sqlite-vec (0.1.7.alpha.2-x86_64-linux) sqlite3 (2.7.4) mini_portile2 (~> 2.8.0) sqlite3 (2.7.4-arm64-darwin) @@ -543,7 +537,7 @@ GEM unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.1.0) - uri (1.0.4) + uri (1.1.0) useragent (0.16.11) vcr (6.3.1) base64 @@ -610,7 +604,7 @@ DEPENDENCIES autotuner aws-sdk-s3 bcrypt (~> 3.1.7) - beamer-rails! + beamer-rails (~> 0.1.0.beta4)! benchmark bootsnap brakeman @@ -646,7 +640,6 @@ DEPENDENCIES solid_cable (>= 3.0) solid_cache (~> 1.0) solid_queue (~> 1.2) - sqlite-vec (= 0.1.7.alpha.2) sqlite3 (>= 2.0) stimulus-rails thruster diff --git a/README.md b/README.md index cb7c40f78..d05487602 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,6 @@ This environment uses a FlashBlade bucket for blob storage, and shares nothing w ### Production -- https://app.box-car.com/ +- https://app.fizzy.do/ This environment uses a FlashBlade bucket for blob storage. diff --git a/app/assets/images/collection-add.svg b/app/assets/images/board-add.svg similarity index 100% rename from app/assets/images/collection-add.svg rename to app/assets/images/board-add.svg diff --git a/app/assets/images/collection.svg b/app/assets/images/board.svg similarity index 100% rename from app/assets/images/collection.svg rename to app/assets/images/board.svg diff --git a/app/assets/stylesheets/bubble.css b/app/assets/stylesheets/bubble.css index 808fefe31..33dd732c2 100644 --- a/app/assets/stylesheets/bubble.css +++ b/app/assets/stylesheets/bubble.css @@ -45,7 +45,7 @@ .bubble__number { display: grid; - font-size: clamp(10px, 50cqi, var(--bubble-number-max)); /* FF bug: https://app.box-car.com/5986089/collections/2/cards/1373 */ + font-size: clamp(10px, 50cqi, var(--bubble-number-max)); /* FF bug: https://app.fizzy.do/5986089/boards/2/cards/1373 */ font-weight: 900; inset: 0; place-content: center; diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index 2e38b9447..a74e8e3f8 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -441,14 +441,6 @@ } } - .card--drafted { - --card-color: var(--color-ink-medium) !important; - - border-color: var(--card-color) !important; - border-style: dashed !important; - box-shadow: none !important; - } - .card:has(.card__background img:not([src=""])) { .card__content, .card__meta, @@ -500,7 +492,7 @@ } } - .card__collection { + .card__board { background-color: transparent; color: var(--card-content-color); } @@ -527,10 +519,10 @@ } } - /* Collection tools + /* Board tools /* -------------------------------------------------------------------------- */ - .collection-tools.card { + .board-tools.card { --border-color: var(--color-selected-dark); --border-size: 1px; --card-padding-block: var(--block-space); @@ -568,7 +560,7 @@ } } - .collection-tools__watching { + .board-tools__watching { --btn-size: 32px; --gap: 0.5ch; @@ -580,7 +572,7 @@ position: relative; } - .collection-tools__watching-dialog { + .board-tools__watching-dialog { --panel-padding: 2ch; --panel-size: 100%; diff --git a/app/assets/stylesheets/card-perma.css b/app/assets/stylesheets/card-perma.css index d12e3eddc..0160fdaaf 100644 --- a/app/assets/stylesheets/card-perma.css +++ b/app/assets/stylesheets/card-perma.css @@ -85,7 +85,7 @@ } } - .collection-picker__button { + .board-picker__button { --btn-border-size: 0; font-size: 0.5em; diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index c32dc74c5..2b61ac4ce 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -55,7 +55,7 @@ } } - .card__collection { + .card__board { background-color: var(--card-color); border-radius: var(--border-radius) 0 var(--border-radius) 0; color: var(--color-ink-inverted); @@ -66,7 +66,7 @@ padding-block: 0.25lh; padding-inline: var(--card-padding-inline) 1ch; - &:has(.collection-picker__button) { + &:has(.board-picker__button) { cursor: pointer; transition: background-color 100ms ease-out; @@ -92,7 +92,7 @@ } } - .card__collection-name { + .card__board-name { border-inline-start: 1px solid color-mix(in hsl, transparent 75%, currentColor); color: currentColor; display: flex; @@ -404,7 +404,7 @@ padding-block-end: 0; } - .card__collection { + .card__board { font-size: var(--text-xx-small); padding-block: 0.5ch; padding-inline-start: var(--inline-space-double); @@ -487,7 +487,7 @@ } } - .card__collection-public-description { + .card__board-public-description { max-inline-size: 66ch; > *:first-child { margin-block-start: 0; } diff --git a/app/assets/stylesheets/events.css b/app/assets/stylesheets/events.css index 660fda3bd..06eaaa15c 100644 --- a/app/assets/stylesheets/events.css +++ b/app/assets/stylesheets/events.css @@ -198,6 +198,12 @@ z-index: var(--z-events-column-header); } + .events__column-footer { + grid-row-start: 26; + margin: var(--events-gap); + padding: var(--block-space-half) var(--inline-space); + } + .events__time-block { align-content: end; display: grid; @@ -225,17 +231,6 @@ text-align: center; } - .events__welcome-video { - .events:has(.day-timeline-pagination-link) + & { - display: none !important; - } - - iframe { - aspect-ratio: 16 / 9; - inline-size: 100%; - } - } - /* Event /* ------------------------------------------------------------------------ */ @@ -275,7 +270,7 @@ z-index: 0; } - .card__collection { + .card__board { background-color: transparent; color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); font-size: 0.7em; @@ -283,7 +278,7 @@ translate: 0 -0.3em; } - .card__collection-name { + .card__board-name { border-inline-start: 1px solid color-mix(in srgb, var(--color-ink) 33%, var(--color-canvas)); color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); margin: 0 0 0 calc(var(--inline-space) * 0.75); diff --git a/app/assets/stylesheets/icons.css b/app/assets/stylesheets/icons.css index e1c4df1d7..8575b72f2 100644 --- a/app/assets/stylesheets/icons.css +++ b/app/assets/stylesheets/icons.css @@ -42,8 +42,8 @@ .icon--clipboard { --svg: url("clipboard.svg "); } .icon--close { --svg: url("close.svg "); } .icon--close-circle { --svg: url("close-circle.svg "); } - .icon--collection { --svg: url("collection.svg "); } - .icon--collection-add { --svg: url("collection-add.svg "); } + .icon--board { --svg: url("board.svg "); } + .icon--board-add { --svg: url("board-add.svg "); } .icon--column-left { --svg: url("column-left.svg "); } .icon--column-right { --svg: url("column-right.svg "); } .icon--comment { --svg: url("comment.svg "); } diff --git a/app/assets/stylesheets/lexxy.css b/app/assets/stylesheets/lexxy.css index 5d0f37dc3..e3f8f8cbd 100644 --- a/app/assets/stylesheets/lexxy.css +++ b/app/assets/stylesheets/lexxy.css @@ -163,6 +163,10 @@ z-index: 1; } + :where(.lexxy-editor__toolbar-spacer) { + flex: 1; + } + /* Based on .input, .input--select */ .lexxy-code-language-picker { -webkit-appearance: none; diff --git a/app/assets/stylesheets/print.css b/app/assets/stylesheets/print.css index d3f849805..df7f8d3a8 100644 --- a/app/assets/stylesheets/print.css +++ b/app/assets/stylesheets/print.css @@ -63,7 +63,7 @@ } .card { - .card__collection { + .card__board { background: var(--color-canvas); border-block-end: var(--border-light); border-inline-end: var(--border-light); @@ -118,7 +118,7 @@ } } - /* Collections + /* Boards /* ------------------------------------------------------------------------ */ .filters, @@ -181,7 +181,7 @@ .card__meta-avatars--assignees > div > div:last-child, .steps > .step:last-child, div:has(> .tag-picker__button), - div:has(> .collection-picker__button), + div:has(> .board-picker__button), .delete-card, .header--card .header__title { display: none; diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 9832dbc93..72cba909b 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -61,6 +61,21 @@ display: none; } + :where(hr) { + block-size: 0; + border-block-end: 1px solid currentColor; + inline-size: 20%; + margin: var(--block-margin) 0; + } + + .horizontal-divider { + margin-inline: 0; + } + + .lexxy-content__strikethrough { + text-decoration: line-through; + } + /* Code */ :where(code, pre) { background-color: var(--color-canvas); diff --git a/app/assets/stylesheets/trays.css b/app/assets/stylesheets/trays.css index 519baca57..2352cb2b4 100644 --- a/app/assets/stylesheets/trays.css +++ b/app/assets/stylesheets/trays.css @@ -314,7 +314,7 @@ padding-block-end: 0; } - .card__collection { + .card__board { padding-block: 0.25ch; } diff --git a/app/assets/stylesheets/utilities.css b/app/assets/stylesheets/utilities.css index 9b7b00bb1..99bac2252 100644 --- a/app/assets/stylesheets/utilities.css +++ b/app/assets/stylesheets/utilities.css @@ -233,4 +233,18 @@ .hide-focus-ring { --focus-ring-size: 0; } + + .hide-on-touch { + @media (any-hover: none) { + display: none; + } + } + + .show-on-touch { + display: none; + + @media (any-hover: none) { + display: unset; + } + } } diff --git a/app/controllers/account/entropies_controller.rb b/app/controllers/account/entropies_controller.rb new file mode 100644 index 000000000..f45f3ef5f --- /dev/null +++ b/app/controllers/account/entropies_controller.rb @@ -0,0 +1,11 @@ +class Account::EntropiesController < ApplicationController + def update + Account.sole.entropy.update!(entropy_params) + redirect_to account_settings_path, notice: "Account updated" + end + + private + def entropy_params + params.expect(entropy: [ :auto_postpone_period ]) + end +end diff --git a/app/controllers/account/entropy_configurations_controller.rb b/app/controllers/account/entropy_configurations_controller.rb deleted file mode 100644 index 53b1f54e8..000000000 --- a/app/controllers/account/entropy_configurations_controller.rb +++ /dev/null @@ -1,12 +0,0 @@ -class Account::EntropyConfigurationsController < ApplicationController - def update - Entropy::Configuration.default.update!(entropy_configuration_params) - - redirect_to account_settings_path, notice: "Account updated" - end - - private - def entropy_configuration_params - params.expect(entropy_configuration: [ :auto_postpone_period ]) - end -end diff --git a/app/controllers/account/join_codes_controller.rb b/app/controllers/account/join_codes_controller.rb index b67246b80..026c692f9 100644 --- a/app/controllers/account/join_codes_controller.rb +++ b/app/controllers/account/join_codes_controller.rb @@ -8,11 +8,8 @@ class Account::JoinCodesController < ApplicationController end def update - if @join_code.update(join_code_params) - redirect_to account_join_code_path - else - render :edit, status: :unprocessable_entity - end + @join_code.update!(join_code_params) + redirect_to account_join_code_path end def destroy diff --git a/app/controllers/account/settings_controller.rb b/app/controllers/account/settings_controller.rb index 9acebbae4..42a6a60f7 100644 --- a/app/controllers/account/settings_controller.rb +++ b/app/controllers/account/settings_controller.rb @@ -1,19 +1,21 @@ class Account::SettingsController < ApplicationController + before_action :ensure_admin, only: :update + before_action :set_account + def show - @account = Account.sole @users = User.active.alphabetically end def update - if Current.user.can_administer? - Account.sole.update!(account_params) - redirect_to account_settings_path - else - head :forbidden - end + @account.update!(account_params) + redirect_to account_settings_path end private + def set_account + @account = Account.sole + end + def account_params params.expect account: %i[ name ] end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6c16b1906..dc70236ef 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -7,4 +7,6 @@ class ApplicationController < ActionController::Base stale_when_importmap_changes allow_browser versions: :modern + + etag { "v1" } # 2025-11-05 @todo: To invalidate HTTP cache after big renaming. To remove after a few days. end diff --git a/app/controllers/boards/columns/closeds_controller.rb b/app/controllers/boards/columns/closeds_controller.rb new file mode 100644 index 000000000..b9aad40b3 --- /dev/null +++ b/app/controllers/boards/columns/closeds_controller.rb @@ -0,0 +1,8 @@ +class Boards::Columns::ClosedsController < ApplicationController + include BoardScoped + + def show + set_page_and_extract_portion_from @board.cards.closed.recently_closed_first + fresh_when etag: @page.records + end +end diff --git a/app/controllers/boards/columns/not_nows_controller.rb b/app/controllers/boards/columns/not_nows_controller.rb new file mode 100644 index 000000000..a65f538c4 --- /dev/null +++ b/app/controllers/boards/columns/not_nows_controller.rb @@ -0,0 +1,8 @@ +class Boards::Columns::NotNowsController < ApplicationController + include BoardScoped + + def show + set_page_and_extract_portion_from @board.cards.postponed.reverse_chronologically.with_golden_first + fresh_when etag: @page.records + end +end diff --git a/app/controllers/boards/columns/streams_controller.rb b/app/controllers/boards/columns/streams_controller.rb new file mode 100644 index 000000000..03b48d900 --- /dev/null +++ b/app/controllers/boards/columns/streams_controller.rb @@ -0,0 +1,8 @@ +class Boards::Columns::StreamsController < ApplicationController + include BoardScoped + + def show + set_page_and_extract_portion_from @board.cards.awaiting_triage.latest.with_golden_first + fresh_when etag: @page.records + end +end diff --git a/app/controllers/boards/columns_controller.rb b/app/controllers/boards/columns_controller.rb new file mode 100644 index 000000000..1648da401 --- /dev/null +++ b/app/controllers/boards/columns_controller.rb @@ -0,0 +1,31 @@ +class Boards::ColumnsController < ApplicationController + include BoardScoped + + before_action :set_column, only: %i[ show update destroy ] + + def show + set_page_and_extract_portion_from @column.cards.active.latest.with_golden_first + fresh_when etag: @page.records + end + + def create + @column = @board.columns.create!(column_params) + end + + def update + @column.update!(column_params) + end + + def destroy + @column.destroy + end + + private + def set_column + @column = @board.columns.find(params[:id]) + end + + def column_params + params.expect(column: [ :name, :color ]) + end +end diff --git a/app/controllers/boards/entropies_controller.rb b/app/controllers/boards/entropies_controller.rb new file mode 100644 index 000000000..153dafe5f --- /dev/null +++ b/app/controllers/boards/entropies_controller.rb @@ -0,0 +1,12 @@ +class Boards::EntropiesController < ApplicationController + include BoardScoped + + def update + @board.entropy.update!(entropy_params) + end + + private + def entropy_params + params.expect(board: [ :auto_postpone_period ]) + end +end diff --git a/app/controllers/boards/involvements_controller.rb b/app/controllers/boards/involvements_controller.rb new file mode 100644 index 000000000..a904f2989 --- /dev/null +++ b/app/controllers/boards/involvements_controller.rb @@ -0,0 +1,7 @@ +class Boards::InvolvementsController < ApplicationController + include BoardScoped + + def update + @board.access_for(Current.user).update!(involvement: params[:involvement]) + end +end diff --git a/app/controllers/boards/publications_controller.rb b/app/controllers/boards/publications_controller.rb new file mode 100644 index 000000000..b7cda07ee --- /dev/null +++ b/app/controllers/boards/publications_controller.rb @@ -0,0 +1,12 @@ +class Boards::PublicationsController < ApplicationController + include BoardScoped + + def create + @board.publish + end + + def destroy + @board.unpublish + @board.reload + end +end diff --git a/app/controllers/boards_controller.rb b/app/controllers/boards_controller.rb new file mode 100644 index 000000000..4bff8dde4 --- /dev/null +++ b/app/controllers/boards_controller.rb @@ -0,0 +1,94 @@ +class BoardsController < ApplicationController + include FilterScoped + + before_action :set_board, except: %i[ new create ] + before_action :ensure_permission_to_change_accesses, only: %i[ update ] + + def show + if @filter.used?(ignore_boards: true) + show_filtered_cards + else + show_columns + end + end + + def new + @board = Board.new + end + + def create + @board = Board.create! board_params.with_defaults(all_access: true) + redirect_to board_path(@board) + end + + def edit + selected_user_ids = @board.users.pluck :id + @selected_users, @unselected_users = \ + User.active.alphabetically.partition { |user| selected_user_ids.include? user.id } + end + + def update + @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)" + end + end + + def destroy + @board.destroy + redirect_to root_path + end + + private + def set_board + @board = Current.user.boards.find params[:id] + end + + def ensure_permission_to_change_accesses + if trying_to_change_accesses? && !Current.user.can_administer_board?(@board) + head :forbidden + end + end + + def trying_to_change_accesses? + all_access_changed? || grantees_changed? + end + + def all_access_changed? + params[:board]&.key?(:all_access) + end + + def grantees_changed? + params.key?(:user_ids) + end + + def show_filtered_cards + @filter.board_ids = [ @board.id ] + set_page_and_extract_portion_from @filter.cards + end + + def show_columns + set_page_and_extract_portion_from @board.cards.awaiting_triage.latest.with_golden_first + fresh_when etag: [ @board, @page.records, @user_filtering ] + end + + def board_params + params.expect(board: [ :name, :all_access, :auto_postpone_period, :public_description ]) + end + + def grantees + User.active.where id: grantee_ids + end + + def revokees + @board.users.where.not id: grantee_ids + end + + def grantee_ids + params.fetch :user_ids, [] + end +end diff --git a/app/controllers/cards/assignments_controller.rb b/app/controllers/cards/assignments_controller.rb index 7b9464a61..cac32efde 100644 --- a/app/controllers/cards/assignments_controller.rb +++ b/app/controllers/cards/assignments_controller.rb @@ -2,11 +2,11 @@ class Cards::AssignmentsController < ApplicationController include CardScoped def new + @users = @board.users.active.alphabetically + fresh_when @users end def create - @card.toggle_assignment @collection.users.active.find(params[:assignee_id]) - - render turbo_stream: turbo_stream.replace([ @card, :assignees ], partial: "cards/display/perma/assignees", locals: { card: @card.reload }) + @card.toggle_assignment @board.users.active.find(params[:assignee_id]) end end diff --git a/app/controllers/cards/boards_controller.rb b/app/controllers/cards/boards_controller.rb new file mode 100644 index 000000000..dfbc522a4 --- /dev/null +++ b/app/controllers/cards/boards_controller.rb @@ -0,0 +1,21 @@ +class Cards::BoardsController < ApplicationController + include BoardScoped + + skip_before_action :set_board, only: %i[ edit ] + before_action :set_card + + def edit + @boards = Current.user.boards.ordered_by_recently_accessed + fresh_when @boards + end + + def update + @card.move_to(@board) + redirect_to @card + end + + private + def set_card + @card = Current.user.accessible_cards.find(params[:card_id]) + end +end diff --git a/app/controllers/cards/collections_controller.rb b/app/controllers/cards/collections_controller.rb deleted file mode 100644 index 7d3ff8b9b..000000000 --- a/app/controllers/cards/collections_controller.rb +++ /dev/null @@ -1,16 +0,0 @@ -class Cards::CollectionsController < ApplicationController - include CollectionScoped - - skip_before_action :set_collection, only: %i[ edit ] - - def edit - @card = Current.user.accessible_cards.find(params[:card_id]) - @collections = Current.user.collections.ordered_by_recently_accessed - end - - def update - @card = Current.user.accessible_cards.find(params[:card_id]) - @card.move_to(@collection) - redirect_to @card - end -end diff --git a/app/controllers/cards/columns_controller.rb b/app/controllers/cards/columns_controller.rb index e41225050..f250f2dd6 100644 --- a/app/controllers/cards/columns_controller.rb +++ b/app/controllers/cards/columns_controller.rb @@ -1,7 +1,7 @@ class Cards::ColumnsController < ApplicationController def edit @card = Current.user.accessible_cards.find(params[:card_id]) - @columns = @card.collection.columns + @columns = @card.board.columns fresh_when etag: [ @card, @columns ] end diff --git a/app/controllers/cards/pins_controller.rb b/app/controllers/cards/pins_controller.rb index 6ac118543..f8da07da3 100644 --- a/app/controllers/cards/pins_controller.rb +++ b/app/controllers/cards/pins_controller.rb @@ -2,6 +2,7 @@ class Cards::PinsController < ApplicationController include CardScoped def show + fresh_when etag: @card.pin_for(Current.user) || "none" end def create diff --git a/app/controllers/cards/publishes_controller.rb b/app/controllers/cards/publishes_controller.rb index 9aeab76ce..641f728cd 100644 --- a/app/controllers/cards/publishes_controller.rb +++ b/app/controllers/cards/publishes_controller.rb @@ -5,9 +5,9 @@ class Cards::PublishesController < ApplicationController @card.publish if add_another_param? - redirect_to @collection.cards.create!, notice: "Card added" + redirect_to @board.cards.create!, notice: "Card added" else - redirect_to @card.collection + redirect_to @card.board end end diff --git a/app/controllers/cards/readings_controller.rb b/app/controllers/cards/readings_controller.rb index d2e1bb8d1..e6a728829 100644 --- a/app/controllers/cards/readings_controller.rb +++ b/app/controllers/cards/readings_controller.rb @@ -5,11 +5,11 @@ class Cards::ReadingsController < ApplicationController def create @notifications = @card.read_by(Current.user) - record_collection_access + record_board_access end private - def record_collection_access - @card.collection.accessed_by(Current.user) + def record_board_access + @card.board.accessed_by(Current.user) end end diff --git a/app/controllers/cards/recovers_controller.rb b/app/controllers/cards/recovers_controller.rb deleted file mode 100644 index b4614bc2a..000000000 --- a/app/controllers/cards/recovers_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Cards::RecoversController < ApplicationController - include CardScoped - - def create - redirect_to @card.recover_abandoned_creation - end -end diff --git a/app/controllers/cards/steps_controller.rb b/app/controllers/cards/steps_controller.rb index 4f3434632..305890508 100644 --- a/app/controllers/cards/steps_controller.rb +++ b/app/controllers/cards/steps_controller.rb @@ -5,7 +5,6 @@ class Cards::StepsController < ApplicationController def create @step = @card.steps.create!(step_params) - render_card_replacement end def show @@ -16,12 +15,10 @@ class Cards::StepsController < ApplicationController def update @step.update!(step_params) - render_card_replacement end def destroy @step.destroy! - render_card_replacement end private diff --git a/app/controllers/cards/taggings_controller.rb b/app/controllers/cards/taggings_controller.rb index a8fe7ecf9..50f58e34b 100644 --- a/app/controllers/cards/taggings_controller.rb +++ b/app/controllers/cards/taggings_controller.rb @@ -3,6 +3,7 @@ class Cards::TaggingsController < ApplicationController def new @tags = Tag.all.alphabetically + fresh_when @tags end def create diff --git a/app/controllers/cards/triages_controller.rb b/app/controllers/cards/triages_controller.rb index 12fe1a5fb..0c1a7bd1e 100644 --- a/app/controllers/cards/triages_controller.rb +++ b/app/controllers/cards/triages_controller.rb @@ -2,14 +2,14 @@ class Cards::TriagesController < ApplicationController include CardScoped def create - column = @card.collection.columns.find(params[:column_id]) + column = @card.board.columns.find(params[:column_id]) @card.triage_into(column) - render_card_replacement + redirect_to @card end def destroy @card.send_back_to_triage - render_card_replacement + redirect_to @card end end diff --git a/app/controllers/cards/watches_controller.rb b/app/controllers/cards/watches_controller.rb index 7308c2279..4d83567d0 100644 --- a/app/controllers/cards/watches_controller.rb +++ b/app/controllers/cards/watches_controller.rb @@ -1,6 +1,10 @@ class Cards::WatchesController < ApplicationController include CardScoped + def show + fresh_when etag: @card.watch_for(Current.user) || "none" + end + def create @card.watch_by Current.user end diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb index db1a0d7e0..3b9f0fcde 100644 --- a/app/controllers/cards_controller.rb +++ b/app/controllers/cards_controller.rb @@ -1,15 +1,16 @@ class CardsController < ApplicationController include FilterScoped - before_action :set_collection, only: %i[ create ] + before_action :set_board, only: %i[ create ] before_action :set_card, only: %i[ show edit update destroy ] + before_action :ensure_permission_to_administer_card, only: %i[ destroy ] def index set_page_and_extract_portion_from @filter.cards end def create - card = @collection.cards.create! + card = @board.cards.find_or_create_by!(creator: Current.user, status: "drafted") redirect_to card end @@ -30,23 +31,27 @@ class CardsController < ApplicationController def destroy @card.destroy! - redirect_to @card.collection, notice: ("Card deleted" unless @card.creating?) + redirect_to @card.board, notice: "Card deleted" end private - def set_collection - @collection = Current.user.collections.find params[:collection_id] + def set_board + @board = Current.user.boards.find params[:board_id] end def set_card @card = Current.user.accessible_cards.find params[:id] end + def ensure_permission_to_administer_card + head :forbidden unless Current.user.can_administer_card?(@card) + end + def suppressing_broadcasts_unless_published(card, &block) if card.published? yield else - Collection.suppressing_turbo_broadcasts(&block) + Board.suppressing_turbo_broadcasts(&block) end end diff --git a/app/controllers/collections/columns/closeds_controller.rb b/app/controllers/collections/columns/closeds_controller.rb deleted file mode 100644 index b5bf35953..000000000 --- a/app/controllers/collections/columns/closeds_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -class Collections::Columns::ClosedsController < ApplicationController - include CollectionScoped - - def show - set_page_and_extract_portion_from @collection.cards.closed.recently_closed_first - fresh_when etag: @page.records - end -end diff --git a/app/controllers/collections/columns/not_nows_controller.rb b/app/controllers/collections/columns/not_nows_controller.rb deleted file mode 100644 index 4c1a6796b..000000000 --- a/app/controllers/collections/columns/not_nows_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -class Collections::Columns::NotNowsController < ApplicationController - include CollectionScoped - - def show - set_page_and_extract_portion_from @collection.cards.postponed.reverse_chronologically.with_golden_first - fresh_when etag: @page.records - end -end diff --git a/app/controllers/collections/columns/streams_controller.rb b/app/controllers/collections/columns/streams_controller.rb deleted file mode 100644 index dccc9aa15..000000000 --- a/app/controllers/collections/columns/streams_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -class Collections::Columns::StreamsController < ApplicationController - include CollectionScoped - - def show - set_page_and_extract_portion_from @collection.cards.awaiting_triage.latest.with_golden_first - fresh_when etag: @page.records - end -end diff --git a/app/controllers/collections/columns_controller.rb b/app/controllers/collections/columns_controller.rb deleted file mode 100644 index b8f4d4de1..000000000 --- a/app/controllers/collections/columns_controller.rb +++ /dev/null @@ -1,31 +0,0 @@ -class Collections::ColumnsController < ApplicationController - include ActionView::RecordIdentifier, CollectionScoped - - before_action :set_column, only: [ :show, :update, :destroy ] - - def show - set_page_and_extract_portion_from @column.cards.active.latest.with_golden_first - fresh_when etag: @page.records - end - - def create - @column = @collection.columns.create!(column_params) - end - - def update - @column.update!(column_params) - end - - def destroy - @column.destroy - end - - private - def set_column - @column = @collection.columns.find(params[:id]) - end - - def column_params - params.require(:column).permit(:name, :color) - end -end diff --git a/app/controllers/collections/entropy_configurations_controller.rb b/app/controllers/collections/entropy_configurations_controller.rb deleted file mode 100644 index 76957627d..000000000 --- a/app/controllers/collections/entropy_configurations_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -class Collections::EntropyConfigurationsController < ApplicationController - include CollectionScoped - - def update - @collection.entropy_configuration.update!(entropy_configuration_params) - - render turbo_stream: [ - turbo_stream.replace([ @collection, :entropy_configuration ], partial: "collections/edit/auto_close", locals: { collection: @collection }), - turbo_stream_flash(notice: "Saved") - ] - end - - private - def entropy_configuration_params - params.expect(collection: [ :auto_postpone_period ]) - end -end diff --git a/app/controllers/collections/involvements_controller.rb b/app/controllers/collections/involvements_controller.rb deleted file mode 100644 index 69f7f2fbc..000000000 --- a/app/controllers/collections/involvements_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Collections::InvolvementsController < ApplicationController - include CollectionScoped - - def update - @collection.access_for(Current.user).update!(involvement: params[:involvement]) - end -end diff --git a/app/controllers/collections/publications_controller.rb b/app/controllers/collections/publications_controller.rb deleted file mode 100644 index 2043cf60a..000000000 --- a/app/controllers/collections/publications_controller.rb +++ /dev/null @@ -1,20 +0,0 @@ -class Collections::PublicationsController < ApplicationController - include CollectionScoped - - def create - @collection.publish - render turbo_stream: [ - turbo_stream.replace([ @collection, :publication ], partial: "collections/edit/publication", locals: { collection: @collection }), - turbo_stream_flash(notice: "Saved") - ] - end - - def destroy - @collection.unpublish - @collection.reload - render turbo_stream: [ - turbo_stream.replace([ @collection, :publication ], partial: "collections/edit/publication", locals: { collection: @collection }), - turbo_stream_flash(notice: "Saved") - ] - end -end diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb deleted file mode 100644 index 1fd336ce9..000000000 --- a/app/controllers/collections_controller.rb +++ /dev/null @@ -1,77 +0,0 @@ -class CollectionsController < ApplicationController - before_action :set_collection, except: %i[ new create ] - - include FilterScoped - - def new - @collection = Collection.new - end - - def show - if @filter.used?(ignore_collections: true) - show_filtered_cards - else - show_columns - end - end - - def create - @collection = Collection.create! collection_params.with_defaults(all_access: true) - redirect_to collection_path(@collection) - end - - def edit - selected_user_ids = @collection.users.pluck :id - @selected_users, @unselected_users = User.active.alphabetically.partition { |user| selected_user_ids.include? user.id } - end - - def update - @collection.update! collection_params - @collection.accesses.revise granted: grantees, revoked: revokees if grantees_changed? - if @collection.accessible_to?(Current.user) - redirect_to edit_collection_path(@collection), notice: "Saved" - else - redirect_to root_path, notice: "Saved (you were removed from the collection)" - end - end - - def destroy - @collection.destroy - redirect_to root_path - end - - private - def set_collection - @collection = Current.user.collections.find params[:id] - end - - def show_filtered_cards - @filter.collection_ids = [ @collection.id ] - set_page_and_extract_portion_from @filter.cards - end - - def show_columns - set_page_and_extract_portion_from @collection.cards.awaiting_triage.latest.with_golden_first - fresh_when etag: [ @collection, @page.records, @user_filtering ] - end - - def collection_params - params.expect(collection: [ :name, :all_access, :auto_postpone_period, :public_description ]) - end - - def grantees - User.active.where id: grantee_ids - end - - def revokees - @collection.users.where.not id: grantee_ids - end - - def grantee_ids - params.fetch :user_ids, [] - end - - def grantees_changed? - params.key?(:user_ids) - end -end diff --git a/app/controllers/columns/cards/drops/closures_controller.rb b/app/controllers/columns/cards/drops/closures_controller.rb index ba0dc67df..a58e86775 100644 --- a/app/controllers/columns/cards/drops/closures_controller.rb +++ b/app/controllers/columns/cards/drops/closures_controller.rb @@ -3,7 +3,5 @@ class Columns::Cards::Drops::ClosuresController < ApplicationController def create @card.close - - render turbo_stream: turbo_stream.replace("closed-cards", partial: "collections/show/closed", method: :morph, locals: { collection: @card.collection }) end end diff --git a/app/controllers/columns/cards/drops/columns_controller.rb b/app/controllers/columns/cards/drops/columns_controller.rb index cb2c3b4a8..1d2bd8ce5 100644 --- a/app/controllers/columns/cards/drops/columns_controller.rb +++ b/app/controllers/columns/cards/drops/columns_controller.rb @@ -1,10 +1,8 @@ class Columns::Cards::Drops::ColumnsController < ApplicationController - include ActionView::RecordIdentifier, CardScoped + include CardScoped def create - column = @card.collection.columns.find(params[:column_id]) - @card.triage_into(column) - - render turbo_stream: turbo_stream.replace(dom_id(column), partial: "collections/show/column", method: :morph, locals: { column: column }) + @column = @card.board.columns.find(params[:column_id]) + @card.triage_into(@column) end end diff --git a/app/controllers/columns/cards/drops/not_nows_controller.rb b/app/controllers/columns/cards/drops/not_nows_controller.rb index a8fe74298..c1b6a5fac 100644 --- a/app/controllers/columns/cards/drops/not_nows_controller.rb +++ b/app/controllers/columns/cards/drops/not_nows_controller.rb @@ -3,7 +3,5 @@ class Columns::Cards::Drops::NotNowsController < ApplicationController def create @card.postpone - - render turbo_stream: turbo_stream.replace("not-now", partial: "collections/show/not_now", method: :morph, locals: { collection: @card.collection }) end end diff --git a/app/controllers/columns/cards/drops/streams_controller.rb b/app/controllers/columns/cards/drops/streams_controller.rb index 09418133e..fefdbb23f 100644 --- a/app/controllers/columns/cards/drops/streams_controller.rb +++ b/app/controllers/columns/cards/drops/streams_controller.rb @@ -3,8 +3,6 @@ class Columns::Cards::Drops::StreamsController < ApplicationController def create @card.send_back_to_triage - set_page_and_extract_portion_from @collection.cards.awaiting_triage.latest.with_golden_first - - render turbo_stream: turbo_stream.replace("the-stream", partial: "collections/show/stream", method: :morph, locals: { collection: @card.collection, page: @page }) + set_page_and_extract_portion_from @board.cards.awaiting_triage.latest.with_golden_first end end diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index 485904051..5afbcccf8 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -23,6 +23,7 @@ module Authentication def allow_unauthenticated_access(**options) skip_before_action :require_authentication, **options before_action :resume_session, **options + allow_unauthorized_access **options end def require_untenanted_access(**options) @@ -37,7 +38,7 @@ module Authentication end def require_tenant - unless ApplicationRecord.current_tenant.present? + if ApplicationRecord.current_tenant.blank? redirect_to session_menu_url(script_name: nil) end end @@ -52,6 +53,7 @@ module Authentication end end + # FIXME: Remove before launch def clear_old_scoped_session_cookies if request.script_name.present? && cookies.signed[:session_token].present? && !find_session_by_cookie cookies.signed[:session_token] = { value: "invalid-session-token", path: request.script_name, expires: 1.hour.ago } @@ -71,7 +73,7 @@ module Authentication end def after_authentication_url - session.delete(:return_to_after_authenticating) || root_url + session.delete(:return_to_after_authenticating) || landing_url end def redirect_authenticated_user diff --git a/app/controllers/concerns/authorization.rb b/app/controllers/concerns/authorization.rb index df48f62f5..6d31f844e 100644 --- a/app/controllers/concerns/authorization.rb +++ b/app/controllers/concerns/authorization.rb @@ -2,7 +2,7 @@ module Authorization extend ActiveSupport::Concern included do - before_action :ensure_can_access_account, if: -> { ApplicationRecord.current_tenant && Current.session } + before_action :ensure_can_access_account, if: -> { ApplicationRecord.current_tenant && authenticated? } end class_methods do @@ -29,7 +29,7 @@ module Authorization if Current.membership.blank? redirect_to session_menu_url(script_name: nil) elsif Current.user.nil? && Current.membership.join_code.present? - redirect_to new_user_path + redirect_to new_users_join_path elsif !Current.user&.active? redirect_to unlink_membership_url(script_name: nil, membership_id: Current.membership.signed_id(purpose: :unlinking)) end diff --git a/app/controllers/concerns/board_scoped.rb b/app/controllers/concerns/board_scoped.rb new file mode 100644 index 000000000..2772c7d01 --- /dev/null +++ b/app/controllers/concerns/board_scoped.rb @@ -0,0 +1,12 @@ +module BoardScoped + extend ActiveSupport::Concern + + included do + before_action :set_board + end + + private + def set_board + @board = Current.user.boards.find(params[:board_id]) + end +end diff --git a/app/controllers/concerns/cached_publicly.rb b/app/controllers/concerns/cached_publicly.rb deleted file mode 100644 index 62ab23d05..000000000 --- a/app/controllers/concerns/cached_publicly.rb +++ /dev/null @@ -1,12 +0,0 @@ -module CachedPublicly - extend ActiveSupport::Concern - - included do - before_action :set_cache_headers - end - - private - def set_cache_headers - expires_in 30.seconds, public: true - end -end diff --git a/app/controllers/concerns/card_scoped.rb b/app/controllers/concerns/card_scoped.rb index 5438871c1..77496e160 100644 --- a/app/controllers/concerns/card_scoped.rb +++ b/app/controllers/concerns/card_scoped.rb @@ -2,7 +2,7 @@ module CardScoped extend ActiveSupport::Concern included do - before_action :set_card, :set_collection + before_action :set_card, :set_board end private @@ -10,8 +10,8 @@ module CardScoped @card = Current.user.accessible_cards.find(params[:card_id]) end - def set_collection - @collection = @card.collection + def set_board + @board = @card.board end def render_card_replacement diff --git a/app/controllers/concerns/collection_scoped.rb b/app/controllers/concerns/collection_scoped.rb deleted file mode 100644 index 2ddde9531..000000000 --- a/app/controllers/concerns/collection_scoped.rb +++ /dev/null @@ -1,12 +0,0 @@ -module CollectionScoped - extend ActiveSupport::Concern - - included do - before_action :set_collection - end - - private - def set_collection - @collection = Current.user.collections.find(params[:collection_id]) - end -end diff --git a/app/controllers/concerns/public_card_scoped.rb b/app/controllers/concerns/public_card_scoped.rb deleted file mode 100644 index 7416609ef..000000000 --- a/app/controllers/concerns/public_card_scoped.rb +++ /dev/null @@ -1,13 +0,0 @@ -module PublicCardScoped - extend ActiveSupport::Concern - - included do - before_action :set_collection_and_card - end - - private - def set_collection_and_card - @collection = Collection.find_by_published_key(params[:collection_id]) - @card = @collection.cards.find(params[:id]) - end -end diff --git a/app/controllers/concerns/public_collection_scoped.rb b/app/controllers/concerns/public_collection_scoped.rb deleted file mode 100644 index 11ecacfca..000000000 --- a/app/controllers/concerns/public_collection_scoped.rb +++ /dev/null @@ -1,12 +0,0 @@ -module PublicCollectionScoped - extend ActiveSupport::Concern - - included do - before_action :set_collection - end - - private - def set_collection - @collection = Collection.find_by_published_key(params[:collection_id] || params[:id]) - end -end diff --git a/app/controllers/concerns/search/query_terms_scoped.rb b/app/controllers/concerns/search/query_terms_scoped.rb deleted file mode 100644 index 11b88cdf2..000000000 --- a/app/controllers/concerns/search/query_terms_scoped.rb +++ /dev/null @@ -1,12 +0,0 @@ -module Search::QueryTermsScoped - extend ActiveSupport::Concern - - included do - before_action :set_query_terms - end - - private - def set_query_terms - @query_terms = params[:q] - end -end diff --git a/app/controllers/concerns/turbo_flash.rb b/app/controllers/concerns/turbo_flash.rb index 7e313a5b9..93dad24ed 100644 --- a/app/controllers/concerns/turbo_flash.rb +++ b/app/controllers/concerns/turbo_flash.rb @@ -1,4 +1,10 @@ module TurboFlash + extend ActiveSupport::Concern + + included do + helper_method :turbo_stream_flash + end + private def turbo_stream_flash(**flash_options) turbo_stream.replace(:flash, partial: "layouts/shared/flash", locals: { flash: flash_options }) diff --git a/app/controllers/join_codes_controller.rb b/app/controllers/join_codes_controller.rb index 972420439..f9980a8db 100644 --- a/app/controllers/join_codes_controller.rb +++ b/app/controllers/join_codes_controller.rb @@ -2,6 +2,7 @@ class JoinCodesController < ApplicationController require_untenanted_access allow_unauthenticated_access before_action :set_join_code + before_action :ensure_join_code_is_valid def new @account_name = ApplicationRecord.with_tenant(tenant) { Account.sole.name } @@ -9,15 +10,22 @@ class JoinCodesController < ApplicationController def create Identity.transaction do - identity = Identity.find_or_create_by(email_address: params.expect(:email_address)) - identity.memberships.create!(tenant: tenant, join_code: code) + identity = Identity.find_or_create_by!(email_address: params.expect(:email_address)) + identity.memberships.find_or_create_by!(tenant: tenant) do |membership| + membership.join_code = code + end identity.send_magic_link end + session[:return_to_after_authenticating] = landing_url(script_name: "/#{tenant}") redirect_to session_magic_link_path end private + def ensure_join_code_is_valid + head :not_found unless @join_code&.active? + end + def set_join_code @join_code ||= ApplicationRecord.with_tenant(tenant) { Account::JoinCode.active.find_by(code: code) } end diff --git a/app/controllers/landings_controller.rb b/app/controllers/landings_controller.rb new file mode 100644 index 000000000..b3b4c9162 --- /dev/null +++ b/app/controllers/landings_controller.rb @@ -0,0 +1,9 @@ +class LandingsController < ApplicationController + def show + if Current.user.boards.one? + redirect_to board_path(Current.user.boards.first) + else + redirect_to root_path + end + end +end diff --git a/app/controllers/memberships/email_addresses/confirmations_controller.rb b/app/controllers/memberships/email_addresses/confirmations_controller.rb index 8ad43971f..b53fdc135 100644 --- a/app/controllers/memberships/email_addresses/confirmations_controller.rb +++ b/app/controllers/memberships/email_addresses/confirmations_controller.rb @@ -11,7 +11,7 @@ class Memberships::EmailAddresses::ConfirmationsController < ApplicationControll def create membership = Membership.change_email_address_using_token(token) - terminate_session + terminate_session if Current.session start_new_session_for membership.reload.identity redirect_to edit_user_url(script_name: "/#{@membership.tenant}", id: @membership.user) diff --git a/app/controllers/memberships/email_addresses_controller.rb b/app/controllers/memberships/email_addresses_controller.rb index e918005e1..c75783ac1 100644 --- a/app/controllers/memberships/email_addresses_controller.rb +++ b/app/controllers/memberships/email_addresses_controller.rb @@ -8,7 +8,14 @@ class Memberships::EmailAddressesController < ApplicationController end def create - @membership.send_email_address_change_confirmation(new_email_address) + identity = Identity.find_by_email_address(new_email_address) + + if identity&.memberships&.exists?(tenant: @membership.tenant) + flash[:alert] = "You already have a user in this account with that email address" + redirect_to new_email_address_path + else + @membership.send_email_address_change_confirmation(new_email_address) + end end private diff --git a/app/controllers/my/pins_controller.rb b/app/controllers/my/pins_controller.rb index f361b6af7..27e14535b 100644 --- a/app/controllers/my/pins_controller.rb +++ b/app/controllers/my/pins_controller.rb @@ -1,5 +1,6 @@ class My::PinsController < ApplicationController def index @pins = Current.user.pins.includes(:card).ordered.limit(20) + fresh_when @pins end end diff --git a/app/controllers/notifications/bulk_readings_controller.rb b/app/controllers/notifications/bulk_readings_controller.rb index 71b387101..5f80938fe 100644 --- a/app/controllers/notifications/bulk_readings_controller.rb +++ b/app/controllers/notifications/bulk_readings_controller.rb @@ -1,5 +1,16 @@ class Notifications::BulkReadingsController < ApplicationController def create Current.user.notifications.unread.read_all + + if from_tray? + head :ok + else + redirect_to notifications_path + end end + + private + def from_tray? + params[:from_tray] + end end diff --git a/app/controllers/notifications/settings_controller.rb b/app/controllers/notifications/settings_controller.rb index 42608b558..c4aab7f69 100644 --- a/app/controllers/notifications/settings_controller.rb +++ b/app/controllers/notifications/settings_controller.rb @@ -2,7 +2,7 @@ class Notifications::SettingsController < ApplicationController before_action :set_settings def show - @collections = Current.user.collections.alphabetically + @boards = Current.user.boards.alphabetically end def update diff --git a/app/controllers/notifications/unsubscribes_controller.rb b/app/controllers/notifications/unsubscribes_controller.rb index ca15e78d4..f7486d91e 100644 --- a/app/controllers/notifications/unsubscribes_controller.rb +++ b/app/controllers/notifications/unsubscribes_controller.rb @@ -1,6 +1,5 @@ class Notifications::UnsubscribesController < ApplicationController allow_unauthenticated_access - allow_unauthorized_access skip_before_action :verify_authenticity_token before_action :set_user diff --git a/app/controllers/prompts/boards/users_controller.rb b/app/controllers/prompts/boards/users_controller.rb new file mode 100644 index 000000000..e5f8775a6 --- /dev/null +++ b/app/controllers/prompts/boards/users_controller.rb @@ -0,0 +1,11 @@ +class Prompts::Boards::UsersController < ApplicationController + include BoardScoped + + def index + @users = @board.users.alphabetically + + if stale? etag: @users + render layout: false + end + end +end diff --git a/app/controllers/prompts/collections/users_controller.rb b/app/controllers/prompts/collections/users_controller.rb deleted file mode 100644 index 19edbbd5d..000000000 --- a/app/controllers/prompts/collections/users_controller.rb +++ /dev/null @@ -1,11 +0,0 @@ -class Prompts::Collections::UsersController < ApplicationController - include CollectionScoped - - def index - @users = @collection.users.alphabetically - - if stale? etag: @users - render layout: false - end - end -end diff --git a/app/controllers/prompts/users_controller.rb b/app/controllers/prompts/users_controller.rb index 1069c3841..c926e2de4 100644 --- a/app/controllers/prompts/users_controller.rb +++ b/app/controllers/prompts/users_controller.rb @@ -1,6 +1,6 @@ class Prompts::UsersController < ApplicationController def index - @users = User.all.alphabetically + @users = User.active.alphabetically if stale? etag: @users render layout: false diff --git a/app/controllers/public/base_controller.rb b/app/controllers/public/base_controller.rb new file mode 100644 index 000000000..28a1bf463 --- /dev/null +++ b/app/controllers/public/base_controller.rb @@ -0,0 +1,20 @@ +class Public::BaseController < ApplicationController + allow_unauthenticated_access + + before_action :set_board, :set_card, :set_public_cache_expiration + + layout "public" + + private + def set_board + @board = Board.find_by_published_key(params[:board_id] || params[:id]) + end + + def set_card + @card = @board.cards.find(params[:id]) if params[:board_id] && params[:id] + end + + def set_public_cache_expiration + expires_in 30.seconds, public: true + end +end diff --git a/app/controllers/public/boards/columns/closeds_controller.rb b/app/controllers/public/boards/columns/closeds_controller.rb new file mode 100644 index 000000000..63ca9de7b --- /dev/null +++ b/app/controllers/public/boards/columns/closeds_controller.rb @@ -0,0 +1,5 @@ +class Public::Boards::Columns::ClosedsController < Public::BaseController + def show + set_page_and_extract_portion_from @board.cards.closed.recently_closed_first + end +end diff --git a/app/controllers/public/boards/columns/not_nows_controller.rb b/app/controllers/public/boards/columns/not_nows_controller.rb new file mode 100644 index 000000000..d518fb8e0 --- /dev/null +++ b/app/controllers/public/boards/columns/not_nows_controller.rb @@ -0,0 +1,5 @@ +class Public::Boards::Columns::NotNowsController < Public::BaseController + def show + set_page_and_extract_portion_from @board.cards.postponed.reverse_chronologically.with_golden_first + end +end diff --git a/app/controllers/public/boards/columns/streams_controller.rb b/app/controllers/public/boards/columns/streams_controller.rb new file mode 100644 index 000000000..778817b3e --- /dev/null +++ b/app/controllers/public/boards/columns/streams_controller.rb @@ -0,0 +1,5 @@ +class Public::Boards::Columns::StreamsController < Public::BaseController + def show + set_page_and_extract_portion_from @board.cards.awaiting_triage.latest.with_golden_first + end +end diff --git a/app/controllers/public/boards/columns_controller.rb b/app/controllers/public/boards/columns_controller.rb new file mode 100644 index 000000000..5a5c23d5e --- /dev/null +++ b/app/controllers/public/boards/columns_controller.rb @@ -0,0 +1,16 @@ +class Public::Boards::ColumnsController < Public::BaseController + before_action :set_column + + def show + set_page_and_extract_portion_from @column.cards.active.latest.with_golden_first + end + + private + # Unlike the other public controllers, this is using params[:id] to fetch the column instead of the card + def set_card + end + + def set_column + @column = @board.columns.find(params[:id]) + end +end diff --git a/app/controllers/public/boards_controller.rb b/app/controllers/public/boards_controller.rb new file mode 100644 index 000000000..d56a1f684 --- /dev/null +++ b/app/controllers/public/boards_controller.rb @@ -0,0 +1,5 @@ +class Public::BoardsController < Public::BaseController + def show + set_page_and_extract_portion_from @board.cards.awaiting_triage.latest.with_golden_first + end +end diff --git a/app/controllers/public/cards_controller.rb b/app/controllers/public/cards_controller.rb index 50d9b1ed0..353bbf4cf 100644 --- a/app/controllers/public/cards_controller.rb +++ b/app/controllers/public/cards_controller.rb @@ -1,11 +1,4 @@ -class Public::CardsController < ApplicationController - include CachedPublicly, PublicCardScoped - - allow_unauthenticated_access only: :show - allow_unauthorized_access only: :show - - layout "public" - +class Public::CardsController < Public::BaseController def show end end diff --git a/app/controllers/public/collections/columns/closeds_controller.rb b/app/controllers/public/collections/columns/closeds_controller.rb deleted file mode 100644 index c049785cb..000000000 --- a/app/controllers/public/collections/columns/closeds_controller.rb +++ /dev/null @@ -1,12 +0,0 @@ -class Public::Collections::Columns::ClosedsController < ApplicationController - include CachedPublicly, PublicCollectionScoped - - allow_unauthenticated_access only: :show - allow_unauthorized_access only: :show - - layout "public" - - def show - set_page_and_extract_portion_from @collection.cards.closed.recently_closed_first - end -end diff --git a/app/controllers/public/collections/columns/not_nows_controller.rb b/app/controllers/public/collections/columns/not_nows_controller.rb deleted file mode 100644 index d3cd2ed02..000000000 --- a/app/controllers/public/collections/columns/not_nows_controller.rb +++ /dev/null @@ -1,12 +0,0 @@ -class Public::Collections::Columns::NotNowsController < ApplicationController - include CachedPublicly, PublicCollectionScoped - - allow_unauthenticated_access only: :show - allow_unauthorized_access only: :show - - layout "public" - - def show - set_page_and_extract_portion_from @collection.cards.postponed.reverse_chronologically.with_golden_first - end -end diff --git a/app/controllers/public/collections/columns/streams_controller.rb b/app/controllers/public/collections/columns/streams_controller.rb deleted file mode 100644 index a8a2165e5..000000000 --- a/app/controllers/public/collections/columns/streams_controller.rb +++ /dev/null @@ -1,12 +0,0 @@ -class Public::Collections::Columns::StreamsController < ApplicationController - include CachedPublicly, PublicCollectionScoped - - allow_unauthenticated_access only: :show - allow_unauthorized_access only: :show - - layout "public" - - def show - set_page_and_extract_portion_from @collection.cards.awaiting_triage.latest.with_golden_first - end -end diff --git a/app/controllers/public/collections/columns_controller.rb b/app/controllers/public/collections/columns_controller.rb deleted file mode 100644 index dcc2b46a3..000000000 --- a/app/controllers/public/collections/columns_controller.rb +++ /dev/null @@ -1,19 +0,0 @@ -class Public::Collections::ColumnsController < ApplicationController - include ActionView::RecordIdentifier, CachedPublicly, PublicCollectionScoped - - allow_unauthenticated_access only: :show - allow_unauthorized_access only: :show - - layout "public" - - before_action :set_column, only: :show - - def show - set_page_and_extract_portion_from @column.cards.active.latest.with_golden_first - end - - private - def set_column - @column = @collection.columns.find(params[:id]) - end -end diff --git a/app/controllers/public/collections_controller.rb b/app/controllers/public/collections_controller.rb deleted file mode 100644 index 18ae24022..000000000 --- a/app/controllers/public/collections_controller.rb +++ /dev/null @@ -1,12 +0,0 @@ -class Public::CollectionsController < ApplicationController - include CachedPublicly, PublicCollectionScoped - - allow_unauthenticated_access only: :show - allow_unauthorized_access only: :show - - layout "public" - - def show - set_page_and_extract_portion_from @collection.cards.awaiting_triage.latest.with_golden_first - end -end diff --git a/app/controllers/searches/queries_controller.rb b/app/controllers/searches/queries_controller.rb index f6b474baa..5dae4b1c6 100644 --- a/app/controllers/searches/queries_controller.rb +++ b/app/controllers/searches/queries_controller.rb @@ -1,8 +1,6 @@ class Searches::QueriesController < ApplicationController - include Search::QueryTermsScoped - def create - Current.user.remember_search(@query_terms) + Current.user.remember_search(params[:q]) head :ok end end diff --git a/app/controllers/searches_controller.rb b/app/controllers/searches_controller.rb index ad9f7a690..b36d0b0d7 100644 --- a/app/controllers/searches_controller.rb +++ b/app/controllers/searches_controller.rb @@ -1,8 +1,8 @@ class SearchesController < ApplicationController - include Search::QueryTermsScoped, Turbo::DriveHelper + include Turbo::DriveHelper def show - if card = Current.user.accessible_cards.find_by_id(@query_terms) + if card = Current.user.accessible_cards.find_by_id(params[:q]) @card = card else perform_search @@ -11,7 +11,7 @@ class SearchesController < ApplicationController private def perform_search - set_page_and_extract_portion_from Current.user.search(@query_terms) + set_page_and_extract_portion_from Current.user.search(params[:q]) @recent_search_queries = Current.user.search_queries.order(updated_at: :desc).limit(10) end end diff --git a/app/controllers/sessions/menus_controller.rb b/app/controllers/sessions/menus_controller.rb index 91f3d742b..7ad3b6a09 100644 --- a/app/controllers/sessions/menus_controller.rb +++ b/app/controllers/sessions/menus_controller.rb @@ -1,17 +1,24 @@ class Sessions::MenusController < ApplicationController require_untenanted_access + before_action(if: :render_as_menu_section?) { request.variant = :menu_section } + layout "public" def show - if params[:menu_section] - request.variant = :menu_section - end - @memberships = Current.identity.memberships if params[:without] @memberships = @memberships.where.not(tenant: params[:without]) end + + if @memberships.one? && !render_as_menu_section? + redirect_to root_url(script_name: "/#{@memberships.first.tenant}") + end end + + private + def render_as_menu_section? + params[:menu_section] + end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 25cb98408..10a3c9773 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -10,7 +10,6 @@ class SessionsController < ApplicationController def create Identity.find_by_email_address(email_address)&.send_magic_link - redirect_to session_magic_link_path end diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb deleted file mode 100644 index 3a240af09..000000000 --- a/app/controllers/uploads_controller.rb +++ /dev/null @@ -1,29 +0,0 @@ -class UploadsController < ApplicationController - include ActiveStorage::SetCurrent - - before_action :set_file, only: :create - before_action :set_attachment, only: :show - - def create - # FIXME: Try to get upload attachments on root - @upload = Account.sole.uploads_attachments.create! blob: create_blob! - end - - def show - expires_in 5.minutes, public: true - redirect_to @attachment.url, allow_other_host: true - end - - private - def set_file - @file = params[:file] - end - - def set_attachment - @attachment = ActiveStorage::Attachment.find_by! slug: "#{params[:slug]}.#{params[:format]}" - end - - def create_blob! - ActiveStorage::Blob.create_and_upload! io: @file, filename: @file.original_filename, content_type: @file.content_type - end -end diff --git a/app/controllers/users/avatars_controller.rb b/app/controllers/users/avatars_controller.rb index 180a7c506..8be049cc3 100644 --- a/app/controllers/users/avatars_controller.rb +++ b/app/controllers/users/avatars_controller.rb @@ -4,31 +4,28 @@ class Users::AvatarsController < ApplicationController allow_unauthenticated_access only: :show before_action :set_user + before_action :ensure_permission_to_administer_user, only: :destroy def show - if stale? @user, cache_control: { max_age: cache_max_age, stale_while_revalidate: 1.week } + if stale? @user, cache_control: { max_age: (30.minutes unless Current.user == @user), stale_while_revalidate: 1.week }.compact render_avatar_or_initials end end def destroy @user.avatar.destroy - redirect_to user_path(@user) + redirect_to @user end private - def cache_max_age - if Current.user == @user - 0 - else - 30.minutes - end - end - def set_user @user = User.find(params[:user_id]) end + def ensure_permission_to_administer_user + head :forbidden unless Current.user.admin? || Current.user == @user + end + def render_avatar_or_initials if @user.avatar.attached? send_blob_stream @user.avatar diff --git a/app/controllers/users/events_controller.rb b/app/controllers/users/events_controller.rb new file mode 100644 index 000000000..2f1e3afb0 --- /dev/null +++ b/app/controllers/users/events_controller.rb @@ -0,0 +1,25 @@ +class Users::EventsController < ApplicationController + include FilterScoped + + before_action :set_user, :set_filter, :set_user_filtering + + def show + @filter = Current.user.filters.new(creator_ids: [ @user.id ]) + @day_timeline = Current.user.timeline_for(day_param, filter: @filter) + + fresh_when @day_timeline + end + + private + def set_user + @user = User.active.find(params[:user_id]) + end + + def day_param + if params[:day].present? + Time.zone.parse(params[:day]) + else + Time.current + end + end +end diff --git a/app/controllers/users/joins_controller.rb b/app/controllers/users/joins_controller.rb new file mode 100644 index 000000000..d811a6203 --- /dev/null +++ b/app/controllers/users/joins_controller.rb @@ -0,0 +1,31 @@ +class Users::JoinsController < ApplicationController + require_access_without_a_user + + before_action :set_join_code, :ensure_join_code_is_valid + + def new + end + + def create + @join_code.redeem do + User.create!(user_params.merge(membership: Current.membership)) + end + + redirect_to landing_path + end + + private + def set_join_code + @join_code = Account::JoinCode.active.find_by(code: Current.membership.join_code) + end + + def ensure_join_code_is_valid + unless @join_code&.active? + redirect_to unlink_membership_url(script_name: nil, membership_id: Current.membership.signed_id(purpose: :unlinking)) + end + end + + def user_params + params.expect(user: [ :name, :avatar ]) + end +end diff --git a/app/controllers/users/roles_controller.rb b/app/controllers/users/roles_controller.rb index 8cffa232b..f9eb7d232 100644 --- a/app/controllers/users/roles_controller.rb +++ b/app/controllers/users/roles_controller.rb @@ -3,7 +3,7 @@ class Users::RolesController < ApplicationController before_action :ensure_permission_to_administer_user def update - @user.update(role_params) + @user.update!(role_params) redirect_to users_path end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 811261c87..2ba535fef 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,32 +1,11 @@ class UsersController < ApplicationController - include FilterScoped - - require_access_without_a_user only: %i[ new create ] - - before_action :set_join_code, only: %i[ new create] - before_action :ensure_join_code_is_valid, only: %i[ new create ] before_action :set_user, only: %i[ show edit update destroy ] before_action :ensure_permission_to_change_user, only: %i[ update destroy ] - before_action :set_filter, only: %i[ edit show ] - before_action :set_user_filtering, only: %i[ edit show] - - def new - end - - def create - @join_code.redeem do - User.create!(user_params.merge(membership: Current.membership)) - end - - redirect_to root_path - end def edit end def show - @filter = Current.user.filters.new(creator_ids: [ @user.id ]) - @day_timeline = Current.user.timeline_for(day_param, filter: @filter) end def update @@ -40,16 +19,6 @@ class UsersController < ApplicationController end private - def set_join_code - @join_code = Account::JoinCode.active.find_by(code: Current.membership.join_code) - end - - def ensure_join_code_is_valid - unless @join_code&.active? - redirect_to unlink_membership_url(script_name: nil, membership_id: Current.membership.signed_id(purpose: :unlinking)) - end - end - def set_user @user = User.active.find(params[:id]) end @@ -58,14 +27,6 @@ class UsersController < ApplicationController head :forbidden unless Current.user.can_change?(@user) end - def day_param - if params[:day].present? - Time.zone.parse(params[:day]) - else - Time.current - end - end - def user_params params.expect(user: [ :name, :avatar ]) end diff --git a/app/controllers/webhooks/activations_controller.rb b/app/controllers/webhooks/activations_controller.rb index e99cd35b4..87b05cf91 100644 --- a/app/controllers/webhooks/activations_controller.rb +++ b/app/controllers/webhooks/activations_controller.rb @@ -1,14 +1,10 @@ class Webhooks::ActivationsController < ApplicationController before_action :ensure_admin - before_action :set_webhook def create - @webhook.activate unless @webhook.active? - redirect_to @webhook, status: :see_other - end + webhook = Webhook.find(params[:webhook_id]) + webhook.activate - private - def set_webhook - @webhook = Webhook.find(params[:webhook_id]) - end + redirect_to webhook + end end diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb index 41f9edffb..7d79c4587 100644 --- a/app/controllers/webhooks_controller.rb +++ b/app/controllers/webhooks_controller.rb @@ -1,22 +1,23 @@ class WebhooksController < ApplicationController + include BoardScoped + before_action :ensure_admin - before_action :set_collection before_action :set_webhook, except: %i[ index new create ] def index - set_page_and_extract_portion_from @collection.webhooks.ordered + set_page_and_extract_portion_from @board.webhooks.ordered end def show end def new - @webhook = @collection.webhooks.new + @webhook = @board.webhooks.new end def create - @webhook = @collection.webhooks.create!(webhook_params) - redirect_to @webhook + webhook = @board.webhooks.create!(webhook_params) + redirect_to webhook end def edit @@ -29,21 +30,17 @@ class WebhooksController < ApplicationController def destroy @webhook.destroy! - redirect_to collection_webhooks_path, status: :see_other + redirect_to board_webhooks_path end private - def set_collection - @collection = Collection.find(params[:collection_id]) - end - def set_webhook - @webhook = @collection.webhooks.find(params[:id]) + @webhook = @board.webhooks.find(params[:id]) end def webhook_params params .expect(webhook: [ :name, :url, subscribed_actions: [] ]) - .merge(collection_id: @collection.id) + .merge(board_id: @board.id) end end diff --git a/app/helpers/accesses_helper.rb b/app/helpers/accesses_helper.rb index ec98acc22..01aa57b7c 100644 --- a/app/helpers/accesses_helper.rb +++ b/app/helpers/accesses_helper.rb @@ -1,8 +1,8 @@ module AccessesHelper MAX_DISPLAYED_WATCHERS = 8 - def access_menu_tag(collection, **options, &) - tag.menu class: [ options[:class], { "toggler--toggled": collection.all_access? } ], data: { + def access_menu_tag(board, **options, &) + tag.menu class: [ options[:class], { "toggler--toggled": board.all_access? } ], data: { controller: "filter toggle-class navigable-list", action: "keydown->navigable-list#navigate filter:changed->navigable-list#reset", navigable_list_focus_on_selection_value: true, @@ -10,36 +10,36 @@ module AccessesHelper toggle_class_toggle_class: "toggler--toggled" }, & end - def access_toggles_for(users, selected:) - render partial: "collections/access_toggle", + def access_toggles_for(users, selected:, disabled: false) + render partial: "boards/access_toggle", collection: users, as: :user, - locals: { selected: selected }, + locals: { selected: selected, disabled: disabled }, cached: ->(user) { [ user, selected ] } end - def access_involvement_advance_button(collection, user, show_watchers: true, icon_only: false) - access = collection.access_for(user) + def access_involvement_advance_button(board, user, show_watchers: true, icon_only: false) + access = board.access_for(user) - turbo_frame_tag dom_id(collection, :involvement_button) do - concat collection_watchers_list(collection) if show_watchers - concat involvement_button(collection, access, show_watchers, icon_only) + turbo_frame_tag dom_id(board, :involvement_button) do + concat board_watchers_list(board) if show_watchers + concat involvement_button(board, access, show_watchers, icon_only) end end - def collection_watchers_list(collection) - watchers = collection.watchers + def board_watchers_list(board) + watchers = board.watchers displayed_watchers = watchers.limit(MAX_DISPLAYED_WATCHERS) overflow_count = watchers.count - MAX_DISPLAYED_WATCHERS safe_join([ tag.strong(displayed_watchers.any? ? "Watching for new cards" : "No one is watching for new cards", class: "txt-uppercase"), - tag.div(class: "collection-tools__watching") do + tag.div(class: "board-tools__watching") do safe_join([ safe_join(displayed_watchers.map { |watcher| avatar_tag(watcher) }), (tag.div(data: { controller: "dialog", action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" }) do concat tag.button("+#{overflow_count}", class: "overflow-count btn btn--circle borderless", data: { action: "dialog#open" }, aria: { label: "Show #{overflow_count} more watchers" }) - concat(tag.dialog(class: "collection-tools__watching-dialog dialog panel", data: { dialog_target: "dialog" }, aria: { hidden: "true" }) do + concat(tag.dialog(class: "board-tools__watching-dialog dialog panel", data: { dialog_target: "dialog" }, aria: { hidden: "true" }) do safe_join(watchers.map { |watcher| avatar_tag(watcher) }) end) end if overflow_count > 0) @@ -48,11 +48,11 @@ module AccessesHelper ]) end - def involvement_button(collection, access, show_watchers, icon_only) - involvement_label_id = dom_id(collection, :involvement_label) + def involvement_button(board, access, show_watchers, icon_only) + involvement_label_id = dom_id(board, :involvement_label) button_to( - collection_involvement_path(collection), + board_involvement_path(board), method: :put, aria: { labelledby: involvement_label_id }, class: class_names("btn", { "btn--reversed": access.watching? && icon_only }), diff --git a/app/helpers/ai_helper.rb b/app/helpers/ai_helper.rb deleted file mode 100644 index 171dcda01..000000000 --- a/app/helpers/ai_helper.rb +++ /dev/null @@ -1,20 +0,0 @@ -require "zlib" - -module AiHelper - LLM_MODELS = %w[ - gpt-5-chat-latest - gpt-5-mini - gpt-5-nano - gpt-5 - chatgpt-4o-latest - gpt-4.1 - gpt-3.5-turbo - gpt-4.1-mini - gpt-4.1-nano - gpt-4o-mini - ] - - def llm_model_options - LLM_MODELS.map { |model| [ model, model ] } - end -end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8447f024d..f6afd0ba4 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,6 +1,6 @@ module ApplicationHelper def page_title_tag - tag.title @page_title || "BOXCAR" + tag.title @page_title || "Fizzy" end def icon_tag(name, **options) diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb new file mode 100644 index 000000000..ae8739d05 --- /dev/null +++ b/app/helpers/boards_helper.rb @@ -0,0 +1,14 @@ +module BoardsHelper + def link_back_to_board(board) + link_to board, class: "btn borderless txt-medium", + data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click click->turbo-navigation#backIfSamePath" } do + tag.span ("←" + tag.strong(board.name, class: "overflow-ellipsis")).html_safe + end + end + + def link_to_edit_board(board) + link_to edit_board_path(board), class: "btn", data: { controller: "tooltip" } do + icon_tag("settings") + tag.span("Settings for #{board.name}", class: "for-screen-reader") + end + end +end diff --git a/app/helpers/cards_helper.rb b/app/helpers/cards_helper.rb index 5a4a35d62..56df2f54d 100644 --- a/app/helpers/cards_helper.rb +++ b/app/helpers/cards_helper.rb @@ -4,8 +4,7 @@ module CardsHelper options.delete(:class), ("golden-effect" if card.golden?), ("card--postponed" if card.postponed?), - ("card--active" if card.active?), - ("card--drafted" if card.drafted?) + ("card--active" if card.active?) ].compact.join(" ") tag.article \ @@ -28,14 +27,14 @@ module CardsHelper title = [ card.title, "added by #{card.creator.name}", - "in #{card.collection.name}" + "in #{card.board.name}" ] title << "assigned to #{card.assignees.map(&:name).to_sentence}" if card.assignees.any? title.join(" ") end def card_social_tags(card) - tag.meta(property: "og:title", content: "#{card.title} | #{card.collection.name}") + + tag.meta(property: "og:title", content: "#{card.title} | #{card.board.name}") + tag.meta(property: "og:description", content: format_excerpt(@card&.description, length: 200)) + tag.meta(property: "og:image", content: @card.image.attached? ? "#{request.base_url}#{url_for(@card.image)}" : "#{request.base_url}/app-icon.png") + tag.meta(property: "og:url", content: card_url(@card)) diff --git a/app/helpers/collections_helper.rb b/app/helpers/collections_helper.rb deleted file mode 100644 index d10b00a4c..000000000 --- a/app/helpers/collections_helper.rb +++ /dev/null @@ -1,14 +0,0 @@ -module CollectionsHelper - def link_back_to_collection(collection) - link_to collection, class: "btn borderless txt-medium", - data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click click->turbo-navigation#backIfSamePath" } do - tag.span ("←" + tag.strong(collection.name, class: "overflow-ellipsis")).html_safe - end - end - - def link_to_edit_collection(collection) - link_to edit_collection_path(collection), class: "btn", data: { controller: "tooltip" } do - icon_tag("settings") + tag.span("Settings for #{collection.name}", class: "for-screen-reader") - end - end -end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 6ee25f658..992463740 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -3,24 +3,30 @@ module EventsHelper case event_type when "added" events = day_timeline.events.where(action: [ "card_published", "card_reopened" ]) + full_events_count = events.count { - title: event_column_title("Added", events.count, day_timeline.day), + title: event_column_title("Added", full_events_count, day_timeline.day), index: 1, - events: events + events: events.limit(100).load, + full_events_count: full_events_count } when "closed" events = day_timeline.events.where(action: "card_closed") + full_events_count = events.count { - title: event_column_title("Closed", events.count, day_timeline.day), + title: event_column_title("Closed", full_events_count, day_timeline.day), index: 3, - events: events + events: events.limit(100).load, + full_events_count: full_events_count } else events = day_timeline.events.where.not(action: [ "card_published", "card_closed", "card_reopened" ]) + full_events_count = events.count { - title: event_column_title("Updated", events.count, day_timeline.day), + title: event_column_title("Updated", full_events_count, day_timeline.day), index: 2, - events: events + events: events.limit(100).load, + full_events_count: full_events_count } end end @@ -62,7 +68,12 @@ module EventsHelper end def event_creator_name(event) - event.creator == Current.user ? "You" : h(event.creator.name) + tag.span data: { creator_id: event.creator.id } do + safe_join([ + tag.span("You", data: { only_visible_to_you: true }), + tag.span(h(event.creator.name), data: { only_visible_to_others: true }) + ]) + end end def card_event_action_sentence(event) @@ -91,8 +102,8 @@ module EventsHelper "#{event_creator_name(event)} resumed #{event_card_title(card)}" when "card_title_changed" "#{event_creator_name(event)} renamed #{event_card_title(card)} (was: “#{h event.particulars.dig('particulars', 'old_title')}”)" - when "card_collection_changed" - "#{event_creator_name(event)} moved #{event_card_title(card)} to “#{h event.particulars.dig('particulars', 'new_collection')}”" + when "card_board_changed", "card_collection_changed" + "#{event_creator_name(event)} moved #{event_card_title(card)} to “#{h (event.particulars.dig('particulars', 'new_board') || event.particulars.dig('particulars', 'new_collection'))}”" when "card_triaged" "#{event_creator_name(event)} moved #{event_card_title(card)} to “#{h event.particulars.dig('particulars', 'column')}”" when "card_sent_back_to_triage" @@ -118,7 +129,7 @@ module EventsHelper "comment" when "card_title_changed" "rename" - when "card_collection_changed" + when "card_board_changed" "move" else "person" diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index cfb634223..49f175067 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -11,8 +11,8 @@ module FiltersHelper hidden_field_tag name, value, id: nil end - def filter_selected_collections_title(user_filtering) - user_filtering.selected_collection_titles.collect { tag.strong it }.to_sentence.html_safe + def filter_selected_boards_title(user_filtering) + user_filtering.selected_board_titles.collect { tag.strong it }.to_sentence.html_safe end def filter_place_menu_item(path, label, icon, new_window: false, current: false) diff --git a/app/helpers/my/menu_helper.rb b/app/helpers/my/menu_helper.rb new file mode 100644 index 000000000..2962a49ac --- /dev/null +++ b/app/helpers/my/menu_helper.rb @@ -0,0 +1,19 @@ +module My::MenuHelper + def jump_field_tag + text_field_tag :search, nil, + type: "search", + role: "combobox", + placeholder: "Type to jump to a board, person, place, or tag…", + class: "input input--transparent txt-small", + autofocus: true, + autocorrect: "off", + autocomplete: "off", + aria: { activedescendant: "" }, + data: { + "1p-ignore": "true", + filter_target: "input", + nav_section_expander_target: "input", + navigable_list_target: "input", + action: "input->filter#filter" } + end +end diff --git a/app/helpers/rich_text_helper.rb b/app/helpers/rich_text_helper.rb index 752af1075..c1bc2232a 100644 --- a/app/helpers/rich_text_helper.rb +++ b/app/helpers/rich_text_helper.rb @@ -1,6 +1,6 @@ module RichTextHelper - def mentions_prompt(collection) - content_tag "lexxy-prompt", "", trigger: "@", src: prompts_collection_users_path(collection), name: "mention" + def mentions_prompt(board) + content_tag "lexxy-prompt", "", trigger: "@", src: prompts_board_users_path(board), name: "mention" end def global_mentions_prompt @@ -19,7 +19,7 @@ module RichTextHelper content_tag "lexxy-code-language-picker" end - def general_prompts(collection) - safe_join([ mentions_prompt(collection), cards_prompt, code_language_picker ]) + def general_prompts(board) + safe_join([ mentions_prompt(board), cards_prompt, code_language_picker ]) end end diff --git a/app/helpers/webhooks_helper.rb b/app/helpers/webhooks_helper.rb index c809ac6ef..e0e0e3387 100644 --- a/app/helpers/webhooks_helper.rb +++ b/app/helpers/webhooks_helper.rb @@ -2,7 +2,7 @@ module WebhooksHelper ACTION_LABELS = { card_published: "Card added", card_title_changed: "Card title changed", - card_collection_changed: "Card collection changed", + card_board_changed: "Card board changed", comment_created: "Comment added", card_assigned: "Card assigned", card_unassigned: "Card unassigned", @@ -22,9 +22,9 @@ module WebhooksHelper ACTION_LABELS[action] || action.to_s.humanize end - def link_to_webhooks(collection, &) - link_to collection_webhooks_path(collection_id: collection), - class: [ "btn", { "btn--reversed": collection.webhooks.any? } ], + def link_to_webhooks(board, &) + link_to board_webhooks_path(board_id: board), + class: [ "btn", { "btn--reversed": board.webhooks.any? } ], data: { controller: "tooltip" } do icon_tag("world") + tag.span("Webhooks", class: "for-screen-reader") end diff --git a/app/javascript/controllers/bar_controller.js b/app/javascript/controllers/bar_controller.js index 0f995ae65..6b10a27b4 100644 --- a/app/javascript/controllers/bar_controller.js +++ b/app/javascript/controllers/bar_controller.js @@ -2,7 +2,7 @@ import { Controller } from "@hotwired/stimulus" import { post } from "@rails/request.js" export default class extends Controller { - static targets = [ "turboFrame", "searchInput", "buttonsContainer" ] + static targets = [ "turboFrame", "search", "searchInput", "form", "buttonsContainer" ] static outlets = [ "dialog" ] static values = { searchUrl: String, @@ -17,18 +17,13 @@ export default class extends Controller { this.dialogOutlet.close() this.#clearTurboFrame() - this.searchInputTarget.querySelector("input").value = "" this.#showItem(this.buttonsContainerTarget) - this.#hideItem(this.searchInputTarget) + this.#hideItem(this.searchTarget) } showModalAndSubmit(event) { - event.preventDefault() - - const form = event.target.closest("form") - this.showModal() - form.requestSubmit() + this.formTarget.requestSubmit() } showModal() { @@ -36,10 +31,18 @@ export default class extends Controller { } search(event) { - this.#openInTurboFrame(this.searchUrlValue) - - this.#showItem(this.searchInputTarget) + this.#showItem(this.searchTarget) this.#hideItem(this.buttonsContainerTarget) + + if (this.searchInputTarget.value.trim()) { + this.showModalAndSubmit() + } else { + this.#loadTurboFrame() + } + } + + #loadTurboFrame() { + this.turboFrameTarget.src = this.searchUrlValue } #clearTurboFrame() { @@ -47,10 +50,6 @@ export default class extends Controller { this.turboFrameTarget.innerHtml = "" } - #openInTurboFrame(url) { - this.turboFrameTarget.src = url - } - #showItem(element) { element.removeAttribute("hidden") diff --git a/app/javascript/controllers/collections_form_controller.js b/app/javascript/controllers/boards_form_controller.js similarity index 100% rename from app/javascript/controllers/collections_form_controller.js rename to app/javascript/controllers/boards_form_controller.js diff --git a/app/javascript/controllers/collapsible_columns_controller.js b/app/javascript/controllers/collapsible_columns_controller.js index 0a87e47d5..74dd1ff65 100644 --- a/app/javascript/controllers/collapsible_columns_controller.js +++ b/app/javascript/controllers/collapsible_columns_controller.js @@ -5,7 +5,7 @@ export default class extends Controller { static classes = [ "collapsed", "noTransitions" ] static targets = [ "column", "button" ] static values = { - collection: String + board: String } initialize() { @@ -104,6 +104,6 @@ export default class extends Controller { } #localStorageKeyFor(column) { - return `expand-${this.collectionValue}-${column.getAttribute("id")}` + return `expand-${this.boardValue}-${column.getAttribute("id")}` } } diff --git a/app/javascript/controllers/toggle_class_controller.js b/app/javascript/controllers/toggle_class_controller.js index 357525f67..04969aee2 100644 --- a/app/javascript/controllers/toggle_class_controller.js +++ b/app/javascript/controllers/toggle_class_controller.js @@ -24,7 +24,7 @@ export default class extends Controller { checkNone() { this.checkboxTargets.forEach(checkbox => { - if (checkbox.dataset.collectionsFormTarget === "meCheckbox") return + if (checkbox.dataset.boardsFormTarget === "meCheckbox") return checkbox.checked = false }) } diff --git a/app/jobs/board/clean_inaccessible_data_job.rb b/app/jobs/board/clean_inaccessible_data_job.rb new file mode 100644 index 000000000..b2cdff085 --- /dev/null +++ b/app/jobs/board/clean_inaccessible_data_job.rb @@ -0,0 +1,5 @@ +class Board::CleanInaccessibleDataJob < ApplicationJob + def perform(user, board) + board.clean_inaccessible_data_for(user) + end +end diff --git a/app/jobs/card/touch_all_job.rb b/app/jobs/card/touch_all_job.rb deleted file mode 100644 index 7b96fa5b7..000000000 --- a/app/jobs/card/touch_all_job.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Card::TouchAllJob < ApplicationJob - queue_as :backend - - def perform(container) - container.cards.in_batches(&:touch_all) - end -end diff --git a/app/jobs/collection/clean_inaccessible_data_job.rb b/app/jobs/collection/clean_inaccessible_data_job.rb deleted file mode 100644 index 16ff1b90f..000000000 --- a/app/jobs/collection/clean_inaccessible_data_job.rb +++ /dev/null @@ -1,5 +0,0 @@ -class Collection::CleanInaccessibleDataJob < ApplicationJob - def perform(user, collection) - collection.clean_inaccessible_data_for(user) - end -end diff --git a/app/jobs/remove_abandoned_creations_job.rb b/app/jobs/remove_abandoned_creations_job.rb deleted file mode 100644 index 6ca63169d..000000000 --- a/app/jobs/remove_abandoned_creations_job.rb +++ /dev/null @@ -1,7 +0,0 @@ -class RemoveAbandonedCreationsJob < ApplicationJob - def perform - ApplicationRecord.with_each_tenant do |tenant| - Card.remove_abandoned_creations - end - end -end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index d6db07d61..4e7878b62 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,5 +1,5 @@ class ApplicationMailer < ActionMailer::Base - default from: "The BOXCAR team " + default from: "The Fizzy team " layout "mailer" append_view_path Rails.root.join("app/views/mailers") diff --git a/app/mailers/magic_link_mailer.rb b/app/mailers/magic_link_mailer.rb index 52b9be031..391e64f6d 100644 --- a/app/mailers/magic_link_mailer.rb +++ b/app/mailers/magic_link_mailer.rb @@ -3,7 +3,7 @@ class MagicLinkMailer < ApplicationMailer @magic_link = magic_link @identity = @magic_link.identity - mail to: @identity.email_address, subject: "Sign in to BOXCAR" + mail to: @identity.email_address, subject: "Sign in to Fizzy" end private diff --git a/app/mailers/notification/bundle_mailer.rb b/app/mailers/notification/bundle_mailer.rb index 32f883efd..c821a1274 100644 --- a/app/mailers/notification/bundle_mailer.rb +++ b/app/mailers/notification/bundle_mailer.rb @@ -11,6 +11,6 @@ class Notification::BundleMailer < ApplicationMailer mail \ to: bundle.user.identity.email_address, - subject: "Latest Activity in BOXCAR" + subject: "Latest Activity in Fizzy" end end diff --git a/app/models/access.rb b/app/models/access.rb index fe4efe590..27026fb87 100644 --- a/app/models/access.rb +++ b/app/models/access.rb @@ -1,5 +1,5 @@ class Access < ApplicationRecord - belongs_to :collection, touch: true + belongs_to :board, touch: true belongs_to :user, touch: true enum :involvement, %i[ access_only watching ].index_by(&:itself), default: :access_only @@ -18,6 +18,6 @@ class Access < ApplicationRecord end def clean_inaccessible_data_later - Collection::CleanInaccessibleDataJob.perform_later(user, collection) + Board::CleanInaccessibleDataJob.perform_later(user, board) end end diff --git a/app/models/account.rb b/app/models/account.rb index 357d0cc29..0c730a937 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -1,5 +1,5 @@ class Account < ApplicationRecord - include Entropic + include Entropic, Seedeable has_many_attached :uploads @@ -16,7 +16,7 @@ class Account < ApplicationRecord end end - # To use the account as a generic card container. See +Entropy::Configuration+. + # To use the account as a generic card container. See +Entropy+. def cards Card.all end @@ -25,12 +25,6 @@ class Account < ApplicationRecord "/#{tenant}" end - def setup_basic_template - user = User.first - - Collection.create!(name: "Cards", creator: user, all_access: true) - end - private def create_join_code Account::JoinCode.create! diff --git a/app/models/account/entropic.rb b/app/models/account/entropic.rb index b82b1051b..e54af1432 100644 --- a/app/models/account/entropic.rb +++ b/app/models/account/entropic.rb @@ -1,17 +1,10 @@ module Account::Entropic extend ActiveSupport::Concern + DEFAULT_ENTROPY_PERIOD = 30.days + included do - has_one :default_entropy_configuration, class_name: "Entropy::Configuration", as: :container, dependent: :destroy - - before_save :set_default_entropy_configuration + has_one :entropy, as: :container, dependent: :destroy + after_create -> { create_entropy!(auto_postpone_period: DEFAULT_ENTROPY_PERIOD) } end - - private - DEFAULT_ENTROPY_PERIOD = 30.days - - def set_default_entropy_configuration - self.default_entropy_configuration ||= build_default_entropy_configuration \ - auto_postpone_period: DEFAULT_ENTROPY_PERIOD - end end diff --git a/app/models/account/join_code.rb b/app/models/account/join_code.rb index ddf4cf822..69d681197 100644 --- a/app/models/account/join_code.rb +++ b/app/models/account/join_code.rb @@ -3,11 +3,7 @@ class Account::JoinCode < ApplicationRecord scope :active, -> { where("usage_count < usage_limit") } - before_validation :generate_code, on: :create, if: -> { code.blank? } - - validates :code, presence: true, uniqueness: true - validates :usage_limit, numericality: { only_integer: true, greater_than_or_equal_to: 0 } - validates :usage_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + before_create :generate_code, if: -> { code.blank? } def redeem transaction do diff --git a/app/models/account/seedeable.rb b/app/models/account/seedeable.rb new file mode 100644 index 000000000..7e080d6ba --- /dev/null +++ b/app/models/account/seedeable.rb @@ -0,0 +1,7 @@ +module Account::Seedeable + extend ActiveSupport::Concern + + def setup_customer_template + Account::Seeder.new(self, User.active.first).seed + end +end diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb new file mode 100644 index 000000000..991877833 --- /dev/null +++ b/app/models/account/seeder.rb @@ -0,0 +1,111 @@ +class Account::Seeder + attr_reader :account, :creator + + def initialize(account, creator) + @account = account + @creator = creator + end + + def seed + Current.set session: session do + populate + end + end + + def seed! + raise "You can't run in production environments" unless Rails.env.local? + + delete_everything + seed + end + + private + def session + creator.identity.sessions.last + end + + def populate + # --------------- + # Playground Board + # --------------- + playground = Board.create! name: "Playground", creator: creator, all_access: true + + # Cards + playground.cards.create! creator: creator, title: "Watch this: Fizzy orientation video", status: "published", description: <<~HTML +

There’s a whole lot more you can do in Fizzy. In the video below 37signals founder and CEO, Jason Fried, will walk you through the basics in just 8 minutes.

+


+ + HTML + + playground.cards.create! creator: creator, title: "Grab the invite link to invite someone else", status: "published", description: <<~HTML +

Open Fizzy menu, select “+ Add people”, then copy the invite link. You can give this link to someone else so they can make an login for themselves in your account.

+


+ + HTML + + playground.cards.create! creator: creator, title: "Head back home to check out activity", status: "published", description: <<~HTML +

Hit “1” or pull down the Fizzy menu and select “Home”.

+


+ + HTML + + playground.cards.create! creator: creator, title: "Check out all cards assigned to you", status: "published", description: <<~HTML +

Pull down the Fizzy menu at the top of the screen, and select “Assigned to me” or just hit “3” on your keyboard any time.

+


+ + HTML + + playground.cards.create! creator: creator, title: "Open the Fizzy menu", status: "published", description: <<~HTML +

The Fizzy menu is how you get around the app. Click “Fizzy” at the top of the screen or hit the “J” key on your keyboard to pop it open.

+


+ + HTML + + playground.cards.create! creator: creator, title: "Assign this card to yourself", status: "published", description: <<~HTML +

Click the little head with the + next to it, pick yourself.

+


+ + HTML + + playground.cards.create! creator: creator, title: "Tag this card “Design” then move it to YES", status: "published", description: <<~HTML +

Click the little Tag icon, type “design”, then “Create tag”. Then, move the card to the new “YES” column you created in the previous step.

+


+ + HTML + + playground.cards.create! creator: creator, title: "Make two more columns", status: "published", description: <<~HTML +
    +
  1. Make one called "Yes"
  2. +
  3. Make another called "Working on"
  4. +
+


+

Go back to the Board view, click the little “+” to the right of the DONE column, name the column, pick a color, then do it again.

+


+

After that, drag this card to “DONE” or select “DONE” in the sidebar.

+


+ + HTML + + playground.cards.create! creator: creator, title: "Move this card to NOT NOW", status: "published", description: <<~HTML +

You can either select “NOT NOW” over in the sidebar, or you can go back out to the board view and drag this card into the “NOT NOW” column on the left side.

+


+ + HTML + + playground.cards.create! creator: creator, title: "Rename this card", status: "published", description: <<~HTML +
    +
  1. Click the title and you can rename the card, change the description, or add more information to the card.
  2. +
  3. Then, hit "Mark as Done" at the bottom of the card.
  4. +
  5. Finally, hit “←Playground” in the top left of the screen to go back to the board.
  6. +
+


+ + HTML + end + + def delete_everything + Current.set session: session do + Board.destroy_all + end + end +end diff --git a/app/models/board.rb b/app/models/board.rb new file mode 100644 index 000000000..9e259d652 --- /dev/null +++ b/app/models/board.rb @@ -0,0 +1,14 @@ +class Board < ApplicationRecord + include AutoPostponing, Accessible, Broadcastable, Cards, Entropic, Filterable, Publishable, Triageable + + belongs_to :creator, class_name: "User", default: -> { Current.user } + + has_rich_text :public_description + + has_many :tags, -> { distinct }, through: :cards + has_many :events + has_many :webhooks, dependent: :destroy + + scope :alphabetically, -> { order("lower(name)") } + scope :ordered_by_recently_accessed, -> { merge(Access.ordered_by_recently_accessed) } +end diff --git a/app/models/collection/accessible.rb b/app/models/board/accessible.rb similarity index 84% rename from app/models/collection/accessible.rb rename to app/models/board/accessible.rb index 42fb45692..6d62c0b21 100644 --- a/app/models/collection/accessible.rb +++ b/app/models/board/accessible.rb @@ -1,4 +1,4 @@ -module Collection::Accessible +module Board::Accessible extend ActiveSupport::Concern included do @@ -11,7 +11,7 @@ module Collection::Accessible end def grant_to(users) - Access.insert_all Array(users).collect { |user| { collection_id: proxy_association.owner.id, user_id: user.id } } + Access.insert_all Array(users).collect { |user| { board_id: proxy_association.owner.id, user_id: user.id } } end def revoke_from(users) @@ -57,7 +57,7 @@ module Collection::Accessible end def grant_access_to_everyone - accesses.grant_to(User.all) if all_access_previously_changed?(to: true) + accesses.grant_to(User.active) if all_access_previously_changed?(to: true) end def mentions_for_user(user) @@ -69,7 +69,7 @@ module Collection::Accessible .joins("LEFT JOIN cards ON mentions.source_id = cards.id AND mentions.source_type = 'Card'") .joins("LEFT JOIN comments ON mentions.source_id = comments.id AND mentions.source_type = 'Comment'") .joins("LEFT JOIN cards AS comment_cards ON comments.card_id = comment_cards.id") - .where("(mentions.source_type = 'Card' AND cards.collection_id = ?) OR (mentions.source_type = 'Comment' AND comment_cards.collection_id = ?)", id, id) + .where("(mentions.source_type = 'Card' AND cards.board_id = ?) OR (mentions.source_type = 'Comment' AND comment_cards.board_id = ?)", id, id) end def notifications_for_user(user) @@ -85,8 +85,8 @@ module Collection::Accessible .joins("LEFT JOIN cards AS event_cards ON events.eventable_id = event_cards.id AND events.eventable_type = 'Card'") .joins("LEFT JOIN comments AS event_comments ON events.eventable_id = event_comments.id AND events.eventable_type = 'Comment'") .joins("LEFT JOIN cards AS event_comment_cards ON event_comments.card_id = event_comment_cards.id") - .where("(notifications.source_type = 'Event' AND events.eventable_type = 'Card' AND event_cards.collection_id = ?) OR - (notifications.source_type = 'Event' AND events.eventable_type = 'Comment' AND event_comment_cards.collection_id = ?)", + .where("(notifications.source_type = 'Event' AND events.eventable_type = 'Card' AND event_cards.board_id = ?) OR + (notifications.source_type = 'Event' AND events.eventable_type = 'Comment' AND event_comment_cards.board_id = ?)", id, id) end end diff --git a/app/models/collection/auto_closing.rb b/app/models/board/auto_postponing.rb similarity index 61% rename from app/models/collection/auto_closing.rb rename to app/models/board/auto_postponing.rb index 98cf21385..4911c5e33 100644 --- a/app/models/collection/auto_closing.rb +++ b/app/models/board/auto_postponing.rb @@ -1,4 +1,4 @@ -module Collection::AutoClosing +module Board::AutoPostponing extend ActiveSupport::Concern included do @@ -6,9 +6,9 @@ module Collection::AutoClosing end private - DEFAULT_auto_postpone_period = 30.days + DEFAULT_AUTO_POSTPONE_PERIOD = 30.days def set_default_auto_postpone_period - self.auto_postpone_period ||= DEFAULT_auto_postpone_period unless attribute_present?(:auto_postpone_period) + self.auto_postpone_period ||= DEFAULT_AUTO_POSTPONE_PERIOD unless attribute_present?(:auto_postpone_period) end end diff --git a/app/models/board/broadcastable.rb b/app/models/board/broadcastable.rb new file mode 100644 index 000000000..177509800 --- /dev/null +++ b/app/models/board/broadcastable.rb @@ -0,0 +1,8 @@ +module Board::Broadcastable + extend ActiveSupport::Concern + + included do + broadcasts_refreshes + broadcasts_refreshes_to ->(_) { :all_boards } + end +end diff --git a/app/models/board/cards.rb b/app/models/board/cards.rb new file mode 100644 index 000000000..096400957 --- /dev/null +++ b/app/models/board/cards.rb @@ -0,0 +1,9 @@ +module Board::Cards + extend ActiveSupport::Concern + + included do + has_many :cards, dependent: :destroy + + after_update_commit -> { cards.touch_all }, if: :saved_change_to_name? + end +end diff --git a/app/models/board/entropic.rb b/app/models/board/entropic.rb new file mode 100644 index 000000000..b29d5ea42 --- /dev/null +++ b/app/models/board/entropic.rb @@ -0,0 +1,17 @@ +module Board::Entropic + extend ActiveSupport::Concern + + included do + delegate :auto_postpone_period, to: :entropy + has_one :entropy, as: :container, dependent: :destroy + end + + def entropy + super || Account.sole.entropy + end + + def auto_postpone_period=(new_value) + entropy ||= association(:entropy).reader || self.build_entropy + entropy.update auto_postpone_period: new_value + end +end diff --git a/app/models/board/publication.rb b/app/models/board/publication.rb new file mode 100644 index 000000000..a21fb00b6 --- /dev/null +++ b/app/models/board/publication.rb @@ -0,0 +1,5 @@ +class Board::Publication < ApplicationRecord + belongs_to :board + + has_secure_token :key +end diff --git a/app/models/collection/publishable.rb b/app/models/board/publishable.rb similarity index 65% rename from app/models/collection/publishable.rb rename to app/models/board/publishable.rb index 25b7f3fc7..6c1ce47d4 100644 --- a/app/models/collection/publishable.rb +++ b/app/models/board/publishable.rb @@ -1,14 +1,14 @@ -module Collection::Publishable +module Board::Publishable extend ActiveSupport::Concern included do - has_one :publication, class_name: "Collection::Publication", dependent: :destroy + has_one :publication, class_name: "Board::Publication", dependent: :destroy scope :published, -> { joins(:publication) } end class_methods do def find_by_published_key(key) - Collection::Publication.find_by!(key: key).collection + Board::Publication.find_by!(key: key).board end end diff --git a/app/models/collection/triageable.rb b/app/models/board/triageable.rb similarity index 76% rename from app/models/collection/triageable.rb rename to app/models/board/triageable.rb index a314868c8..3e922b4a2 100644 --- a/app/models/collection/triageable.rb +++ b/app/models/board/triageable.rb @@ -1,4 +1,4 @@ -module Collection::Triageable +module Board::Triageable extend ActiveSupport::Concern included do diff --git a/app/models/card.rb b/app/models/card.rb index 3b455a424..65c7cab18 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,9 +1,9 @@ class Card < ApplicationRecord - include Assignable, Attachments, Closeable, Colored, Entropic, Eventable, - Golden, Mentions, Multistep, Pinnable, Postponable, Promptable, Readable, - Searchable, Stallable, Statuses, Taggable, Triageable, Watchable + include Assignable, Attachments, Broadcastable, Closeable, Colored, Entropic, + Eventable, Golden, Mentions, Multistep, Pinnable, Postponable, Promptable, + Readable, Searchable, Stallable, Statuses, Taggable, Triageable, Watchable - belongs_to :collection, touch: true + belongs_to :board, touch: true belongs_to :creator, class_name: "User", default: -> { Current.user } has_many :comments, dependent: :destroy @@ -12,7 +12,7 @@ class Card < ApplicationRecord has_rich_text :description before_save :set_default_title, if: :published? - after_update :handle_collection_change, if: :saved_change_to_collection_id? + after_update :handle_board_change, if: :saved_change_to_board_id? scope :reverse_chronologically, -> { order created_at: :desc, id: :desc } scope :chronologically, -> { order created_at: :asc, id: :asc } @@ -39,41 +39,45 @@ class Card < ApplicationRecord end end - delegate :accessible_to?, to: :collection + delegate :accessible_to?, to: :board def card self end - def move_to(new_collection) + def move_to(new_board) transaction do - card.update!(collection: new_collection) - card.events.update_all(collection_id: new_collection.id) + card.update!(board: new_board) + card.events.update_all(board_id: new_board.id) end end + def filled? + title.present? || description.present? + end + private def set_default_title self.title = "Untitled" if title.blank? end - def handle_collection_change - old_collection = Collection.find_by(id: collection_id_before_last_save) + def handle_board_change + old_board = Board.find_by(id: board_id_before_last_save) transaction do update! column: nil - track_collection_change_event(old_collection.name) - grant_access_to_assignees unless collection.all_access? + track_board_change_event(old_board.name) + grant_access_to_assignees unless board.all_access? end remove_inaccessible_notifications_later end - def track_collection_change_event(old_collection_name) - track_event "collection_changed", particulars: { old_collection: old_collection_name, new_collection: collection.name } + def track_board_change_event(old_board_name) + track_event "board_changed", particulars: { old_board: old_board_name, new_board: board.name } end def grant_access_to_assignees - collection.accesses.grant_to(assignees) + board.accesses.grant_to(assignees) end end diff --git a/app/models/card/activity_spike/detector.rb b/app/models/card/activity_spike/detector.rb index 6721b946f..63f246ca0 100644 --- a/app/models/card/activity_spike/detector.rb +++ b/app/models/card/activity_spike/detector.rb @@ -20,10 +20,12 @@ class Card::ActivitySpike::Detector end def register_activity_spike - if card.activity_spike - card.activity_spike.touch - else - card.create_activity_spike! + Card.suppressing_turbo_broadcasts do + if card.activity_spike + card.activity_spike.touch + else + card.create_activity_spike! + end end end diff --git a/app/models/card/broadcastable.rb b/app/models/card/broadcastable.rb new file mode 100644 index 000000000..aa24b776c --- /dev/null +++ b/app/models/card/broadcastable.rb @@ -0,0 +1,7 @@ +module Card::Broadcastable + extend ActiveSupport::Concern + + included do + broadcasts_refreshes + end +end diff --git a/app/models/card/entropic.rb b/app/models/card/entropic.rb index 46acd8da8..478d4bc52 100644 --- a/app/models/card/entropic.rb +++ b/app/models/card/entropic.rb @@ -4,19 +4,19 @@ module Card::Entropic included do scope :due_to_be_postponed, -> do active - .left_outer_joins(collection: :entropy_configuration) - .where("last_active_at <= DATETIME('now', '-' || COALESCE(entropy_configurations.auto_postpone_period, ?) || ' seconds')", - Entropy::Configuration.default.auto_postpone_period) + .left_outer_joins(board: :entropy) + .where("last_active_at <= DATETIME('now', '-' || COALESCE(entropies.auto_postpone_period, ?) || ' seconds')", + Account.sole.entropy.auto_postpone_period) end scope :postponing_soon, -> do active - .left_outer_joins(collection: :entropy_configuration) - .where("last_active_at > DATETIME('now', '-' || COALESCE(entropy_configurations.auto_postpone_period, ?) || ' seconds')", Entropy::Configuration.default.auto_postpone_period) - .where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropy_configurations.auto_postpone_period, ?) * 0.75 AS INTEGER) || ' seconds')", Entropy::Configuration.default.auto_postpone_period) + .left_outer_joins(board: :entropy) + .where("last_active_at > DATETIME('now', '-' || COALESCE(entropies.auto_postpone_period, ?) || ' seconds')", Account.sole.entropy.auto_postpone_period) + .where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropies.auto_postpone_period, ?) * 0.75 AS INTEGER) || ' seconds')", Account.sole.entropy.auto_postpone_period) end - delegate :auto_postpone_period, to: :collection + delegate :auto_postpone_period, to: :board end class_methods do diff --git a/app/models/card/eventable.rb b/app/models/card/eventable.rb index 520fd95d4..c52fb7a29 100644 --- a/app/models/card/eventable.rb +++ b/app/models/card/eventable.rb @@ -19,6 +19,7 @@ module Card::Eventable def touch_last_active_at # Not using touch so that we can detect attribute change on callbacks update!(last_active_at: Time.current) + broadcast_activity end private @@ -35,4 +36,8 @@ module Card::Eventable def create_system_comment_for(event) SystemCommenter.new(self, event).comment end + + def broadcast_activity + broadcast_render_later_to self, :activity, partial: "card/display/refresh_activity", locals: { card: self } + end end diff --git a/app/models/card/eventable/system_commenter.rb b/app/models/card/eventable/system_commenter.rb index e74475c79..ace5701bf 100644 --- a/app/models/card/eventable/system_commenter.rb +++ b/app/models/card/eventable/system_commenter.rb @@ -28,8 +28,8 @@ class Card::Eventable::SystemCommenter "Closed as “Not Now” due to inactivity" when "card_title_changed" "#{event.creator.name} changed the title from “#{event.particulars.dig('particulars', 'old_title')}” to “#{event.particulars.dig('particulars', 'new_title')}”." - when "card_collection_changed" - "#{event.creator.name} moved this from “#{event.particulars.dig('particulars', 'old_collection')}” to “#{event.particulars.dig('particulars', 'new_collection')}”." + when "card_board_changed" + "#{event.creator.name} moved this from “#{event.particulars.dig('particulars', 'old_board')}” to “#{event.particulars.dig('particulars', 'new_board')}”." when "card_triaged" "#{event.creator.name} moved this to “#{event.particulars.dig('particulars', 'column')}”" when "card_sent_back_to_triage" diff --git a/app/models/card/pinnable.rb b/app/models/card/pinnable.rb index 0cdaf4000..ff3407ea0 100644 --- a/app/models/card/pinnable.rb +++ b/app/models/card/pinnable.rb @@ -9,6 +9,10 @@ module Card::Pinnable pins.exists?(user: user) end + def pin_for(user) + pins.find_by(user: user) + end + def pin_by(user) pins.find_or_create_by!(user: user) end diff --git a/app/models/card/promptable.rb b/app/models/card/promptable.rb index efbcd64db..e30816023 100644 --- a/app/models/card/promptable.rb +++ b/app/models/card/promptable.rb @@ -21,8 +21,8 @@ module Card::Promptable * Assigned to: #{assignees.map(&:name).join(", ")} * Column: #{column_prompt_label} * Created at: #{created_at}} - * Collection id: #{collection_id} - * Collection name: #{collection.name} + * Board id: #{board_id} + * Board name: #{board.name} * Number of comments: #{comments.count} * Path: #{card_path(self, script_name: Account.sole.slug)} diff --git a/app/models/card/statuses.rb b/app/models/card/statuses.rb index 73d3f122a..f26896b21 100644 --- a/app/models/card/statuses.rb +++ b/app/models/card/statuses.rb @@ -2,38 +2,17 @@ module Card::Statuses extend ActiveSupport::Concern included do - enum :status, %w[ creating drafted published ].index_by(&:itself) + enum :status, %w[ drafted published ].index_by(&:itself) after_create -> { track_event :published }, if: :published? scope :published_or_drafted_by, ->(user) { where(status: :published).or(where(status: :drafted, creator: user)) } end - class_methods do - def remove_abandoned_creations - Card.creating.where(updated_at: ..1.day.ago).destroy_all - end - end - - def can_recover_abandoned_creation? - abandoned_creations.where(updated_at: 1.day.ago..).any? - end - - def recover_abandoned_creation - abandoned_creations.last.tap do |card| - Card.creating.where(creator: creator).excluding(card).destroy_all - end - end - def publish transaction do published! track_event :published end end - - private - def abandoned_creations - Card.creating.where(creator: creator).where("created_at != updated_at").excluding(self) - end end diff --git a/app/models/card/triageable.rb b/app/models/card/triageable.rb index c324c709b..d15663902 100644 --- a/app/models/card/triageable.rb +++ b/app/models/card/triageable.rb @@ -17,7 +17,7 @@ module Card::Triageable end def triage_into(column) - raise "The column must belong to the card collection" unless collection == column.collection + raise "The column must belong to the card board" unless board == column.board transaction do resume diff --git a/app/models/card/watchable.rb b/app/models/card/watchable.rb index 2d23868bf..17235529e 100644 --- a/app/models/card/watchable.rb +++ b/app/models/card/watchable.rb @@ -9,7 +9,11 @@ module Card::Watchable end def watched_by?(user) - watchers.include?(user) + watch_for(user)&.watching? + end + + def watch_for(user) + watches.find_by(user: user) end def watch_by(user) diff --git a/app/models/collection.rb b/app/models/collection.rb deleted file mode 100644 index 504439e2e..000000000 --- a/app/models/collection.rb +++ /dev/null @@ -1,24 +0,0 @@ -class Collection < ApplicationRecord - include AutoClosing, Accessible, Broadcastable, Cards, Entropic, Filterable, Publishable, Triageable - - belongs_to :creator, class_name: "User", default: -> { Current.user } - - has_rich_text :public_description - - has_many :tags, -> { distinct }, through: :cards - has_many :events - has_many :webhooks, dependent: :destroy - has_one :entropy_configuration, class_name: "Entropy::Configuration", as: :container, dependent: :destroy - - scope :alphabetically, -> { order("lower(name)") } - scope :ordered_by_recently_accessed, -> { merge(Access.ordered_by_recently_accessed) } - - after_destroy_commit :ensure_default_collection - - private - def ensure_default_collection - if Collection.count.zero? - Collection.create!(name: "Cards") - end - end -end diff --git a/app/models/collection/broadcastable.rb b/app/models/collection/broadcastable.rb deleted file mode 100644 index e44adc0ac..000000000 --- a/app/models/collection/broadcastable.rb +++ /dev/null @@ -1,8 +0,0 @@ -module Collection::Broadcastable - extend ActiveSupport::Concern - - included do - broadcasts_refreshes - broadcasts_refreshes_to ->(_) { :all_collections } - end -end diff --git a/app/models/collection/cards.rb b/app/models/collection/cards.rb deleted file mode 100644 index 17ed592aa..000000000 --- a/app/models/collection/cards.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Collection::Cards - extend ActiveSupport::Concern - - included do - has_many :cards, dependent: :destroy - - after_update_commit :touch_all_cards_later, if: :saved_change_to_name? - end - - private - def touch_all_cards_later - Card::TouchAllJob.perform_later(self) - end -end diff --git a/app/models/collection/entropic.rb b/app/models/collection/entropic.rb deleted file mode 100644 index a3879955b..000000000 --- a/app/models/collection/entropic.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Collection::Entropic - extend ActiveSupport::Concern - - included do - delegate :auto_postpone_period, to: :entropy_configuration - end - - def entropy_configuration - super || Entropy::Configuration.default - end - - def auto_postpone_period=(new_value) - entropy_configuration ||= association(:entropy_configuration).reader || self.build_entropy_configuration - entropy_configuration.update auto_postpone_period: new_value - end -end diff --git a/app/models/collection/publication.rb b/app/models/collection/publication.rb deleted file mode 100644 index fbbc68741..000000000 --- a/app/models/collection/publication.rb +++ /dev/null @@ -1,5 +0,0 @@ -class Collection::Publication < ApplicationRecord - belongs_to :collection - - has_secure_token :key -end diff --git a/app/models/column.rb b/app/models/column.rb index 0285ab185..ec5fd6217 100644 --- a/app/models/column.rb +++ b/app/models/column.rb @@ -1,30 +1,10 @@ class Column < ApplicationRecord include Positioned - belongs_to :collection, touch: true + belongs_to :board, touch: true has_many :cards, dependent: :nullify - validates :name, presence: true - validates :color, presence: true - - before_validation :set_default_color - after_save_commit :touch_all_cards_later, if: :should_invalidate_cards? - after_destroy_commit :touch_all_collection_cards - - private - def set_default_color - self.color ||= Card::DEFAULT_COLOR - end - - def touch_all_cards_later - Card::TouchAllJob.perform_later(self) - end - - def should_invalidate_cards? - saved_change_to_name? || saved_change_to_color? - end - - def touch_all_collection_cards - Card::TouchAllJob.perform_later(collection) - end + before_validation -> { self.color ||= Card::DEFAULT_COLOR } + after_save_commit -> { cards.touch_all }, if: -> { saved_change_to_name? || saved_change_to_color? } + after_destroy_commit -> { board.cards.touch_all } end diff --git a/app/models/column/positioned.rb b/app/models/column/positioned.rb index 333882e36..8518172de 100644 --- a/app/models/column/positioned.rb +++ b/app/models/column/positioned.rb @@ -16,11 +16,11 @@ module Column::Positioned end def left_column - collection.columns.where("position < ?", position).sorted.last + board.columns.where("position < ?", position).sorted.last end def right_column - collection.columns.where("position > ?", position).sorted.first + board.columns.where("position > ?", position).sorted.first end def leftmost? @@ -33,7 +33,7 @@ module Column::Positioned private def set_position - max_position = collection.columns.maximum(:position) || 0 + max_position = board.columns.maximum(:position) || 0 self.position = max_position + 1 end diff --git a/app/models/comment.rb b/app/models/comment.rb index 703c3e6a9..4428c8e48 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -14,7 +14,7 @@ class Comment < ApplicationRecord after_create_commit :watch_card_by_creator - delegate :collection, :watch_by, to: :card + delegate :board, :watch_by, to: :card def to_partial_path "cards/#{super}" diff --git a/app/models/comment/eventable.rb b/app/models/comment/eventable.rb index 0b30c0504..7fe7abebb 100644 --- a/app/models/comment/eventable.rb +++ b/app/models/comment/eventable.rb @@ -17,6 +17,6 @@ module Comment::Eventable end def track_creation - track_event("created", collection: card.collection, creator: creator) + track_event("created", board: card.board, creator: creator) end end diff --git a/app/models/concerns/attachments.rb b/app/models/concerns/attachments.rb index f4328600e..1d1c2c4d6 100644 --- a/app/models/concerns/attachments.rb +++ b/app/models/concerns/attachments.rb @@ -28,6 +28,22 @@ module Attachments attachments.any? end + def remote_images + @remote_images ||= rich_text_record&.body&.attachables&.grep(ActionText::Attachables::RemoteImage) || [] + end + + def has_remote_images? + remote_images.any? + end + + def remote_videos + @remote_videos ||= rich_text_record&.body&.attachables&.grep(ActionText::Attachables::RemoteVideo) || [] + end + + def has_remote_videos? + remote_videos.any? + end + private def rich_text_record @rich_text_record ||= begin diff --git a/app/models/concerns/mentions.rb b/app/models/concerns/mentions.rb index fa50de543..055c114fc 100644 --- a/app/models/concerns/mentions.rb +++ b/app/models/concerns/mentions.rb @@ -41,7 +41,7 @@ module Mentions end def mentionable_users - collection.users + board.users end def rich_text_associations diff --git a/app/models/entropy.rb b/app/models/entropy.rb index 42e730a09..6be6675ee 100644 --- a/app/models/entropy.rb +++ b/app/models/entropy.rb @@ -1,5 +1,5 @@ -module Entropy - def self.table_name_prefix - "entropy_" - end +class Entropy < ApplicationRecord + belongs_to :container, polymorphic: true + + after_commit -> { container.cards.touch_all } end diff --git a/app/models/entropy/configuration.rb b/app/models/entropy/configuration.rb deleted file mode 100644 index 1884cb112..000000000 --- a/app/models/entropy/configuration.rb +++ /dev/null @@ -1,16 +0,0 @@ -class Entropy::Configuration < ApplicationRecord - belongs_to :container, polymorphic: true - - after_commit :touch_all_cards_later - - class << self - def default - Account.sole.default_entropy_configuration - end - end - - private - def touch_all_cards_later - Card::TouchAllJob.perform_later(container) - end -end diff --git a/app/models/event.rb b/app/models/event.rb index c45702833..ad77d3492 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,7 +1,7 @@ class Event < ApplicationRecord include Notifiable, Particulars, Promptable - belongs_to :collection + belongs_to :board belongs_to :creator, class_name: "User" belongs_to :eventable, polymorphic: true diff --git a/app/models/eventable.rb b/app/models/eventable.rb index e14803f09..c5e58cc08 100644 --- a/app/models/eventable.rb +++ b/app/models/eventable.rb @@ -5,9 +5,9 @@ module Eventable has_many :events, as: :eventable, dependent: :destroy end - def track_event(action, creator: Current.user, collection: self.collection, **particulars) + def track_event(action, creator: Current.user, board: self.board, **particulars) if should_track_event? - collection.events.create!(action: "#{eventable_prefix}_#{action}", creator:, collection:, eventable: self, particulars:) + board.events.create!(action: "#{eventable_prefix}_#{action}", creator:, board:, eventable: self, particulars:) end end diff --git a/app/models/filter.rb b/app/models/filter.rb index ca14629a9..459d18621 100644 --- a/app/models/filter.rb +++ b/app/models/filter.rb @@ -18,7 +18,7 @@ class Filter < ApplicationRecord def cards @cards ||= begin - result = creator.accessible_cards.published_or_drafted_by(creator) + result = creator.accessible_cards.published result = result.indexed_by(indexed_by) result = result.sorted_by(sorted_by) result = result.where(id: card_ids) if card_ids.present? @@ -27,7 +27,7 @@ class Filter < ApplicationRecord result = result.unassigned if assignment_status.unassigned? result = result.assigned_to(assignees.ids) if assignees.present? result = result.where(creator_id: creators.ids) if creators.present? - result = result.where(collection: collections.ids) if collections.present? + result = result.where(board: boards.ids) if boards.present? result = result.tagged_with(tags.ids) if tags.present? result = result.where("cards.created_at": creation_window) if creation_window result = result.closed_at_window(closure_window) if closure_window @@ -44,16 +44,16 @@ class Filter < ApplicationRecord self.class.normalize_params(as_params).blank? end - def single_collection - collections.first if collections.one? + def single_board + boards.first if boards.one? end def single_workflow - collections.first.workflow if collections.pluck(:workflow_id).uniq.one? + boards.first.workflow if boards.pluck(:workflow_id).uniq.one? end def cacheable? - collections.exists? + boards.exists? end def cache_key diff --git a/app/models/filter/fields.rb b/app/models/filter/fields.rb index c24ed74c8..c44dff996 100644 --- a/app/models/filter/fields.rb +++ b/app/models/filter/fields.rb @@ -1,7 +1,7 @@ module Filter::Fields extend ActiveSupport::Concern - INDEXES = %w[ all closed not_now stalled postponing_soon golden draft ] + INDEXES = %w[ all closed not_now stalled postponing_soon golden ] SORTED_BY = %w[ newest oldest latest ] delegate :default_value?, to: :class diff --git a/app/models/filter/params.rb b/app/models/filter/params.rb index 166436e4b..586a75885 100644 --- a/app/models/filter/params.rb +++ b/app/models/filter/params.rb @@ -11,7 +11,7 @@ module Filter::Params assignee_ids: [], creator_ids: [], closer_ids: [], - collection_ids: [], + board_ids: [], tag_ids: [], terms: [] ] @@ -40,9 +40,9 @@ module Filter::Params before_save { self.params_digest = self.class.digest_params(as_params) } end - def used?(ignore_collections: false) + def used?(ignore_boards: false) tags.any? || assignees.any? || creators.any? || closers.any? || - terms.any? || card_ids&.any? || (!ignore_collections && collections.present?) || + terms.any? || card_ids&.any? || (!ignore_boards && boards.present?) || assignment_status.unassigned? || !indexed_by.all? || !sorted_by.latest? end @@ -57,7 +57,7 @@ module Filter::Params params[:assignment_status] = assignment_status params[:terms] = terms params[:tag_ids] = tags.ids - params[:collection_ids] = collections.ids + params[:board_ids] = boards.ids params[:card_ids] = card_ids params[:assignee_ids] = assignees.ids params[:creator_ids] = creators.ids diff --git a/app/models/filter/resources.rb b/app/models/filter/resources.rb index 3dc777b2f..f7045f38e 100644 --- a/app/models/filter/resources.rb +++ b/app/models/filter/resources.rb @@ -3,7 +3,7 @@ module Filter::Resources included do has_and_belongs_to_many :tags - has_and_belongs_to_many :collections + has_and_belongs_to_many :boards has_and_belongs_to_many :assignees, class_name: "User", join_table: "assignees_filters", association_foreign_key: "assignee_id" has_and_belongs_to_many :creators, class_name: "User", join_table: "creators_filters", association_foreign_key: "creator_id" has_and_belongs_to_many :closers, class_name: "User", join_table: "closers_filters", association_foreign_key: "closer_id" @@ -17,19 +17,19 @@ module Filter::Resources destroy! end - def collections - creator.collections.where id: super.ids + def boards + creator.boards.where id: super.ids end - def collection_titles - if collections.none? - Collection.one? ? [ Collection.first.name ] : [ "all boards" ] + def board_titles + if boards.none? + Board.one? ? [ Board.first.name ] : [ "all boards" ] else - collections.map(&:name) + boards.map(&:name) end end - def collections_label - collection_titles.to_sentence + def boards_label + board_titles.to_sentence end end diff --git a/app/models/identity.rb b/app/models/identity.rb index 70bc089eb..f5daf3be1 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -6,7 +6,6 @@ class Identity < UntenantedRecord has_many :sessions, dependent: :destroy normalizes :email_address, with: ->(value) { value.strip.downcase } - validates :email_address, presence: true def send_magic_link magic_links.create!.tap do |magic_link| diff --git a/app/models/notifier/card_event_notifier.rb b/app/models/notifier/card_event_notifier.rb index f6801d7ec..570989566 100644 --- a/app/models/notifier/card_event_notifier.rb +++ b/app/models/notifier/card_event_notifier.rb @@ -1,6 +1,6 @@ class Notifier::CardEventNotifier < Notifier delegate :creator, to: :source - delegate :collection, to: :card + delegate :board, to: :card private def recipients @@ -8,11 +8,11 @@ class Notifier::CardEventNotifier < Notifier when "card_assigned" source.assignees.excluding(creator) when "card_published" - collection.watchers.without(creator, *card.mentionees) + board.watchers.without(creator, *card.mentionees) when "comment_created" card.watchers.without(creator, *source.eventable.mentionees) else - collection.watchers.without(creator) + board.watchers.without(creator) end end diff --git a/app/models/search.rb b/app/models/search.rb index a51e757f6..518462094 100644 --- a/app/models/search.rb +++ b/app/models/search.rb @@ -21,7 +21,7 @@ class Search "highlight(cards_search_index, 0, '#{HIGHLIGHT_OPENING_MARK}', '#{HIGHLIGHT_CLOSING_MARK}') AS card_title", "snippet(cards_search_index, 1, '#{HIGHLIGHT_OPENING_MARK}', '#{HIGHLIGHT_CLOSING_MARK}', '...', 20) AS card_description", "null as comment_body", - "collections.name as collection_name", + "boards.name as board_name", "cards.creator_id", "cards.created_at", "bm25(cards_search_index, 10.0, 2.0) AS score" @@ -34,7 +34,7 @@ class Search "cards.title AS card_title", "null AS card_description", "snippet(comments_search_index, 0, '#{HIGHLIGHT_OPENING_MARK}', '#{HIGHLIGHT_CLOSING_MARK}', '...', 20) AS comment_body", - "collections.name as collection_name", + "boards.name as board_name", "comments.creator_id", "comments.created_at", "bm25(comments_search_index, 1.0) AS score" diff --git a/app/models/step.rb b/app/models/step.rb index c4e35f33e..c2063dfaa 100644 --- a/app/models/step.rb +++ b/app/models/step.rb @@ -3,6 +3,8 @@ class Step < ApplicationRecord scope :completed, -> { where(completed: true) } + validates :content, presence: true + def completed? completed end diff --git a/app/models/user.rb b/app/models/user.rb index 2046b64d5..fabb0224a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,8 +3,6 @@ class User < ApplicationRecord Mentionable, Named, Notifiable, Role, Searcher, Watcher include Timelined # Depends on Accessor - self.ignored_columns = %i[ password_digest ] - has_one_attached :avatar belongs_to :membership, optional: true @@ -18,8 +16,6 @@ class User < ApplicationRecord has_many :pins, dependent: :destroy has_many :pinned_cards, through: :pins, source: :card - normalizes :email_address, with: ->(value) { value.strip.downcase } - delegate :staff?, to: :identity, allow_nil: true def deactivate diff --git a/app/models/user/accessor.rb b/app/models/user/accessor.rb index c538c87a4..a7826b0b4 100644 --- a/app/models/user/accessor.rb +++ b/app/models/user/accessor.rb @@ -3,15 +3,15 @@ module User::Accessor included do has_many :accesses, dependent: :destroy - has_many :collections, through: :accesses - has_many :accessible_cards, through: :collections, source: :cards + has_many :boards, through: :accesses + has_many :accessible_cards, through: :boards, source: :cards has_many :accessible_comments, through: :accessible_cards, source: :comments - after_create_commit :grant_access_to_collections, unless: :system? + after_create_commit :grant_access_to_boards, unless: :system? end private - def grant_access_to_collections - Access.insert_all Collection.all_access.pluck(:id).collect { |collection_id| { collection_id: collection_id, user_id: id } } + def grant_access_to_boards + Access.insert_all Board.all_access.pluck(:id).collect { |board_id| { board_id: board_id, user_id: id } } end end diff --git a/app/models/user/day_timeline.rb b/app/models/user/day_timeline.rb index ec3d53760..367eccdcb 100644 --- a/app/models/user/day_timeline.rb +++ b/app/models/user/day_timeline.rb @@ -41,6 +41,7 @@ class User::DayTimeline card_closed card_reopened card_collection_changed + card_board_changed card_postponed card_triaged card_sent_back_to_triage @@ -57,12 +58,12 @@ class User::DayTimeline def timelineable_events Event - .where(collection: collections) + .where(board: boards) .where(action: TIMELINEABLE_ACTIONS) end - def collections - filter.collections.presence || user.collections + def boards + filter.boards.presence || user.boards end def latest_event_before diff --git a/app/models/user/filtering.rb b/app/models/user/filtering.rb index c9f049ab3..4ac9cf86a 100644 --- a/app/models/user/filtering.rb +++ b/app/models/user/filtering.rb @@ -1,25 +1,23 @@ class User::Filtering - include Rails.application.routes.url_helpers - attr_reader :user, :filter, :expanded - delegate :as_params, :single_collection, to: :filter + delegate :as_params, :single_board, to: :filter delegate :only_closed?, to: :filter def initialize(user, filter, expanded: false) @user, @filter, @expanded = user, filter, expanded end - def collections - @collections ||= user.collections.ordered_by_recently_accessed + def boards + @boards ||= user.boards.ordered_by_recently_accessed end - def selected_collection_titles - filter.collection_titles + def selected_board_titles + filter.board_titles end - def selected_collections_label - filter.collections_label + def selected_boards_label + filter.boards_label end def tags @@ -39,7 +37,7 @@ class User::Filtering end def any? - filter.used?(ignore_collections: true) + filter.used?(ignore_boards: true) end def show_indexed_by? @@ -67,16 +65,16 @@ class User::Filtering filter.closers.any? end - def show_collections? - filter.collections.any? + def show_boards? + filter.boards.any? end - def single_collection_or_first + def single_board_or_first # Default to the first selected or, when no selection, to the first one - filter.collections.first || collections.first + filter.boards.first || boards.first end def cache_key - ActiveSupport::Cache.expand_cache_key([ user, filter, expanded?, collections, tags, users, filters ], "user-filtering") + ActiveSupport::Cache.expand_cache_key([ user, filter, expanded?, boards, tags, users, filters ], "user-filtering") end end diff --git a/app/models/user/role.rb b/app/models/user/role.rb index aaa4bd5ff..a327ffcf6 100644 --- a/app/models/user/role.rb +++ b/app/models/user/role.rb @@ -21,4 +21,12 @@ module User::Role def can_administer?(other = nil) admin? && other != self end + + def can_administer_board?(board) + admin? || board.creator == self + end + + def can_administer_card?(card) + admin? || card.creator == self + end end diff --git a/app/models/user/timelined.rb b/app/models/user/timelined.rb index 1f5f2c8fe..2cd808a44 100644 --- a/app/models/user/timelined.rb +++ b/app/models/user/timelined.rb @@ -2,7 +2,7 @@ module User::Timelined extend ActiveSupport::Concern included do - has_many :accessible_events, through: :collections, source: :events + has_many :accessible_events, through: :boards, source: :events end def timeline_for(day, filter:) diff --git a/app/models/webhook.rb b/app/models/webhook.rb index 9a103d9bc..f66cc6942 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -11,7 +11,7 @@ class Webhook < ApplicationRecord card_closed card_postponed card_auto_postponed - card_collection_changed + card_board_changed card_published card_reopened card_sent_back_to_triage @@ -25,7 +25,7 @@ class Webhook < ApplicationRecord has_many :deliveries, dependent: :delete_all has_one :delinquency_tracker, dependent: :delete - belongs_to :collection + belongs_to :board serialize :subscribed_actions, type: Array, coder: JSON @@ -40,11 +40,11 @@ class Webhook < ApplicationRecord validate :validate_url def activate - update_columns active: true + update! active: true unless active? end def deactivate - update_columns active: false + update! active: false end def renderer diff --git a/app/models/webhook/triggerable.rb b/app/models/webhook/triggerable.rb index a2f5ea186..443c801fd 100644 --- a/app/models/webhook/triggerable.rb +++ b/app/models/webhook/triggerable.rb @@ -2,7 +2,7 @@ module Webhook::Triggerable extend ActiveSupport::Concern included do - scope :triggered_by, ->(event) { where(collection: event.collection).triggered_by_action(event.action) } + scope :triggered_by, ->(event) { where(board: event.board).triggered_by_action(event.action) } scope :triggered_by_action, ->(action) { where("subscribed_actions LIKE ?", "%\"#{action}\"%") } end diff --git a/app/views/account/join_codes/edit.html.erb b/app/views/account/join_codes/edit.html.erb index c96a4ac82..de53bf8c8 100644 --- a/app/views/account/join_codes/edit.html.erb +++ b/app/views/account/join_codes/edit.html.erb @@ -1,7 +1,6 @@ <% @page_title = "Change usage limit" %> <% content_for :header do %> - <%= render "my/menus/menu" %>
<%= link_to account_join_code_path, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> Invite link @@ -14,13 +13,11 @@

<%= @page_title %>

How many times can this link be used to join the account?

+ <%= form_with model: @join_code, url: account_join_code_path, method: :patch, data: { controller: "form" }, html: { class: "flex flex-column gap" } do |form| %> <%= form.number_field :usage_limit, - required: true, - autofocus: true, - min: @join_code.usage_count, - class: "input center txt-large fit-content font-weight-black txt-align-center", - style: "max-inline-size: 8ch", + required: true, autofocus: true, min: @join_code.usage_count, + class: "input center txt-large fit-content font-weight-black txt-align-center", style: "max-inline-size: 8ch", data: { action: "keydown.esc@document->form#cancel focus->form#select" } %>

diff --git a/app/views/account/join_codes/show.html.erb b/app/views/account/join_codes/show.html.erb index f076518ff..f3142b091 100644 --- a/app/views/account/join_codes/show.html.erb +++ b/app/views/account/join_codes/show.html.erb @@ -1,7 +1,6 @@ <% @page_title = "Add people" %> <% content_for :header do %> - <%= render "my/menus/menu" %>

<%= link_to account_settings_path, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> Account Settings @@ -18,8 +17,8 @@ <% url = join_url(code: @join_code.code, tenant: @join_code.tenant, script_name: nil) %>
- <%= button_to account_join_code_path, method: :delete, class: "btn btn--circle txt-small", - data: { turbo_confirm: "Are you sure you want to generate a new link? The previous code will stop working." } do %> + <%= button_to account_join_code_path, method: :delete, class: "btn btn--circle txt-small", data: { + turbo_confirm: "Are you sure you want to generate a new link? The previous code will stop working." } do %> <%= icon_tag "refresh" %> Generate a new code <% end %> @@ -27,9 +26,8 @@
<%= tag.button class: "btn btn--link", data: { - controller: "copy-to-clipboard", action: "copy-to-clipboard#copy", - copy_to_clipboard_success_class: "btn--success", copy_to_clipboard_content_value: url } do %> - + controller: "copy-to-clipboard", action: "copy-to-clipboard#copy", + copy_to_clipboard_success_class: "btn--success", copy_to_clipboard_content_value: url } do %> <%= icon_tag "copy-paste" %> Copy invite link <% end %> @@ -60,6 +58,7 @@

"> This code has been used <%= @join_code.usage_count %>/<%= @join_code.usage_limit %> times (<%= @join_code.active? ? @join_code.usage_limit - @join_code.usage_count : "none" %> remaining) + <%= link_to edit_account_join_code_path, class: @join_code.active? ? "txt-link" : "txt-negative txt-underline" do %> Change limit <% end %> diff --git a/app/views/account/settings/_entropy.html.erb b/app/views/account/settings/_entropy.html.erb new file mode 100644 index 000000000..f6056ab37 --- /dev/null +++ b/app/views/account/settings/_entropy.html.erb @@ -0,0 +1,8 @@ +

+
+

Auto close

+

Fizzy doesn’t let stale cards stick around forever. Cards automatically close as “Not Now” without activity for specific period of time. This is the default, global setting — you can override it on each board.

+
+ + <%= render "entropy/auto_close", model: account.entropy, url: account_entropy_path %> +
diff --git a/app/views/account/settings/_entropy_configuration.html.erb b/app/views/account/settings/_entropy_configuration.html.erb deleted file mode 100644 index 9ecc64db6..000000000 --- a/app/views/account/settings/_entropy_configuration.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -
-
-

Auto close

-

BOXCAR doesn’t let stale cards stick around forever. Cards automatically close as “Not Now” without activity for specific period of time. This is the default, global setting — you can override it on each board.

-
- - <%= render "entropy/auto_close", model: account.default_entropy_configuration, url: account_entropy_configuration_path %> -
diff --git a/app/views/account/settings/_user.html.erb b/app/views/account/settings/_user.html.erb index 78395ff1c..45fc77c07 100644 --- a/app/views/account/settings/_user.html.erb +++ b/app/views/account/settings/_user.html.erb @@ -3,7 +3,7 @@ <%= avatar_preview_tag user, hidden_for_screen_reader: true %>
<%= user.name %> -
<%= user.email_address %>
+
<%= user.identity.email_address %>
<% end %> diff --git a/app/views/account/settings/show.html.erb b/app/views/account/settings/show.html.erb index 53533bba4..7e2dbbfbf 100644 --- a/app/views/account/settings/show.html.erb +++ b/app/views/account/settings/show.html.erb @@ -10,5 +10,5 @@ <%= render "account/settings/users", users: @users %>
- <%= render "account/settings/entropy_configuration", account: @account %> + <%= render "account/settings/entropy", account: @account %> diff --git a/app/views/action_text/attachables/_remote_image.html.erb b/app/views/action_text/attachables/_remote_image.html.erb new file mode 100644 index 000000000..174a95c5e --- /dev/null +++ b/app/views/action_text/attachables/_remote_image.html.erb @@ -0,0 +1,8 @@ +
+ <%= image_tag remote_image.url, width: remote_image.width, height: remote_image.height %> + <% if caption = remote_image.try(:caption) %> +
+ <%= caption %> +
+ <% end %> +
diff --git a/app/views/action_text/attachables/_remote_video.html.erb b/app/views/action_text/attachables/_remote_video.html.erb new file mode 100644 index 000000000..5233d062a --- /dev/null +++ b/app/views/action_text/attachables/_remote_video.html.erb @@ -0,0 +1,10 @@ +
+ <%= tag.video controls: true, width: remote_video.width, height: remote_video.height do %> + <%= tag.source src: remote_video.url, type: remote_video.content_type %> + <% end %> + <% if caption = remote_video.try(:caption) %> +
+ <%= caption %> +
+ <% end %> +
diff --git a/app/views/bar/_bar.html.erb b/app/views/bar/_bar.html.erb index 4858ce2a8..6d2014e12 100644 --- a/app/views/bar/_bar.html.erb +++ b/app/views/bar/_bar.html.erb @@ -3,11 +3,11 @@
<%= tag.button class: "btn btn--plain", data: { controller: "hotkey", action: "bar#search keydown.k@document->hotkey#click" } do %> - Search <% if platform.desktop? -%>K<% end %> + Search K <% end %>
-