diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml index bbd1eb22a..d89381607 100644 --- a/.github/workflows/publish-image.yml +++ b/.github/workflows/publish-image.yml @@ -34,6 +34,9 @@ jobs: - runner: ubuntu-latest platform: linux/amd64 arch: amd64 + - runner: ubuntu-24.04-arm + platform: linux/arm64 + arch: arm64 env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} @@ -170,11 +173,13 @@ jobs: docker buildx imagetools create \ --tag "$tag" \ --annotation "index:org.opencontainers.image.description=${IMAGE_DESCRIPTION}" \ - "${src_tag}-amd64" + "${src_tag}-amd64" \ + "${src_tag}-arm64" else docker buildx imagetools create \ --tag "$tag" \ - "${src_tag}-amd64" + "${src_tag}-amd64" \ + "${src_tag}-arm64" fi done <<< "$tags" diff --git a/app/assets/images/person.svg b/app/assets/images/person.svg index 1e29ed27d..b126d685d 100644 --- a/app/assets/images/person.svg +++ b/app/assets/images/person.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index c438efa26..7cb14e15b 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -536,16 +536,33 @@ /* Set lower limit for font size */ font-size: clamp(0.6rem, 0.85cqi, 100px); - .card__comments { - --column-gap: 0.8ch; + .card__counts { + --gap: 0.5ch; + align-items: flex-end; display: flex; - margin-block-end: calc(var(--block-space) * -1.7); - margin-inline-end: calc(var(--card-padding-inline) * -0.5); + flex-shrink: 0; + gap: calc(2 * var(--gap)); + margin-inline: auto calc(var(--card-padding-inline) * -0.5); + padding-inline-start: var(--gap); + } - .icon { - --icon-size: 1.6em; + .card__boosts, + .card__comments { + --icon-size: 1.6em; + align-items: center; + display: flex; + flex-shrink: 0; + font-weight: 600; + gap: var(--gap); + + img { + block-size: var(--icon-size); + inline-size: var(--icon-size); + } + + .icon--comment { color: var(--card-color); } } @@ -584,11 +601,16 @@ .local-time-value { font-weight: inherit; } + + @media (max-width: 639px) { + inline-size: auto; + } } &:has(.card__background img:not([src=""])) { .card__content, .card__meta, + .card__boosts, .card__comments, .card__column-name:not(.card__column-name--current) { opacity: 0; @@ -599,6 +621,7 @@ &:hover { .card__content, .card__footer, + .card__boosts, .card__comments, .card__column-name:not(.card__column-name--current) { opacity: 1; diff --git a/app/assets/stylesheets/card-perma.css b/app/assets/stylesheets/card-perma.css index e68706765..b3d37d68a 100644 --- a/app/assets/stylesheets/card-perma.css +++ b/app/assets/stylesheets/card-perma.css @@ -34,6 +34,23 @@ } } + @media (max-width: 799px) { + --half-btn-height: 1.25rem; + --padding-inline: 1.5ch; + + column-gap: 0; + grid-template-areas: + "notch-top notch-top notch-top" + "card card card" + "actions-left notch-bottom actions-right" + "closure-message closure-message closure-message"; + grid-template-columns: 1fr auto 1fr; + inline-size: calc(100% + 2 * var(--padding-inline)); + margin-inline: calc(-1 * var(--padding-inline)); + max-inline-size: none; + position: relative; + } + .card { --card-aspect-ratio: 2 / 0.95; --lexxy-bg-color: var(--card-bg-color); @@ -47,6 +64,8 @@ } .card__body { + position: relative; + @media (max-width: 639px) { flex-direction: column; padding-block-end: calc(var(--card-padding-block) * 1.5); @@ -86,6 +105,11 @@ } } + .card__meta { + grid-area: meta; + margin-inline-end: auto; + } + .card__stages { max-inline-size: 32ch; @@ -119,6 +143,71 @@ } } + .card__footer { + --btn-size: 2.5rem; + + display: flex; + gap: 0.5ch; + inline-size: 100%; + text-align: start; + + /* Switch to grid layout so that the bg zoom button can stay next to the + * meta element, and the reactions can sit below */ + &:has(.reaction) { + display: grid; + grid-template-columns: 1fr auto; + grid-template-areas: + "meta bg-zoom" + "reactions reactions"; + } + + @media (max-width: 639px) { + font-size: var(--text-x-small); + } + } + + .reactions { + --reaction-size: var(--btn-size); + + align-self: flex-end; + display: flex; + gap: 0.5ch; + grid-area: reactions; + margin-inline-start: auto; + + &:has(.reaction) { + --padding: calc(var(--card-padding-block) / 2); + --reaction-size: 1.6875rem; + + margin-block: var(--padding) calc(-1 * var(--padding)); + padding-block-start: var(--padding); + position: relative; + + &:before { + border-block-start: 1px dashed color-mix(in srgb, transparent, var(--card-color) 33%); + content: ""; + inset: 0 calc(-1 * var(--card-padding-inline)) auto; + position: absolute; + } + } + + &:not(:has(.reaction)) { + position: static; + + .reactions__trigger { + --btn-border-color: var(--color-ink-light); + } + } + } + + .reaction__popup.popup { + inline-size: max-content; + } + + .card__zoom-bg-btn { + grid-area: bg-zoom; + } + .bubble { --bubble-number-max: 42px; --bubble-size: 6rem; @@ -132,23 +221,6 @@ inset: calc(var(--bubble-size) / 1.5) 0 auto auto; } } - - @media (max-width: 799px) { - --half-btn-height: 1.5rem; - --padding-inline: 1.5ch; - - column-gap: 0; - grid-template-areas: - "notch-top notch-top notch-top" - "card card card" - "actions-left notch-bottom actions-right" - "closure-message closure-message closure-message"; - grid-template-columns: 1fr auto 1fr; - inline-size: calc(100% + 2 * var(--padding-inline)); - margin-inline: calc(-1 * var(--padding-inline)); - max-inline-size: none; - position: relative; - } } /* Child items diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index 3aec6e3b0..cbaa66c91 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -62,6 +62,8 @@ } .card__board { + align-items: center; + align-self: start; background-color: var(--card-color); border-radius: var(--border-radius) 0 var(--border-radius) 0; color: var(--color-ink-inverted); @@ -252,9 +254,10 @@ padding-block: var(--block-space-half); } - /* Meta + /* Footer /* ------------------------------------------------------------------------ */ + /* Card metadata */ .card__meta { --meta-spacer-block: 0.5ch; --meta-spacer-inline: 0.75ch; @@ -376,11 +379,12 @@ display: flex; flex-direction: column; font-weight: bold; - inset: auto 1ch 1ch auto; + inset: auto 0 -1lh auto; justify-content: center; max-inline-size: 25ch; min-inline-size: 16ch; padding: 1ch; + pointer-events: none; position: absolute; rotate: 5deg; transform-origin: top right; diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index 006cbb985..a8605503b 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -80,6 +80,11 @@ display: none !important; } } + + .reactions { + margin-block-start: var(--block-space-half); + margin-inline: calc(var(--column-gap) / -1); + } } .comment__author { diff --git a/app/assets/stylesheets/events.css b/app/assets/stylesheets/events.css index 291d2ca47..8e28654bb 100644 --- a/app/assets/stylesheets/events.css +++ b/app/assets/stylesheets/events.css @@ -291,6 +291,7 @@ .event { --column-gap: 0.7ch; + --event-padding: 0.6em; background-color: color-mix(in srgb, var(--card-color) 10%, var(--color-canvas)); border-radius: 0.2em; @@ -300,7 +301,7 @@ max-inline-size: 100%; min-inline-size: 0; overflow: clip; - padding: 0.6em; + padding: var(--event-padding); position: relative; z-index: 0; @@ -325,12 +326,15 @@ z-index: 0; } + .card__header { + inline-size: 100%; + margin-block-start: calc(-1 * var(--event-padding)); + } + .card__board { background-color: transparent; color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); font-size: 0.7em; - padding: 0; - translate: 0 -0.3em; } .card__board-name { @@ -377,16 +381,11 @@ } .event__icon { + color: var(--card-color); display: grid; - margin: -0.3em -0.3em auto auto; + margin-inline-start: auto; place-content: center; - - .icon { - background-color: var(--card-color); - block-size: 1em; - inline-size: 1em; - grid-area: 1 / 1; - } + translate: calc(var(--event-padding) / 2); } .event__timestamp { diff --git a/app/assets/stylesheets/reactions.css b/app/assets/stylesheets/reactions.css index d1cd0a03f..64f2a5164 100644 --- a/app/assets/stylesheets/reactions.css +++ b/app/assets/stylesheets/reactions.css @@ -10,8 +10,6 @@ flex-wrap: wrap; gap: var(--inline-space-half); inline-size: 100%; - margin-block-start: var(--block-space-half); - margin-inline: calc(var(--column-gap) / -1); &:has([open]) { z-index: var(--z-popup); @@ -34,11 +32,8 @@ .reactions__trigger { --btn-border-color: var(--color-ink-lightest); - background-color: var(--color-ink-lightest); - &:not(:hover) { - opacity: 0.66; - } + background-color: var(--color-ink-lightest); } } } diff --git a/app/assets/stylesheets/trays.css b/app/assets/stylesheets/trays.css index 7793b6f2d..0e47857ab 100644 --- a/app/assets/stylesheets/trays.css +++ b/app/assets/stylesheets/trays.css @@ -440,6 +440,7 @@ .card__meta-text:not(.card__meta-text--updated), .card__stages, .card__steps, + .card__boosts, .card__comments, .card__closed { display: none; diff --git a/app/controllers/cards/comments/reactions_controller.rb b/app/controllers/cards/comments/reactions_controller.rb index 0f1277a3c..0c822360e 100644 --- a/app/controllers/cards/comments/reactions_controller.rb +++ b/app/controllers/cards/comments/reactions_controller.rb @@ -2,20 +2,26 @@ class Cards::Comments::ReactionsController < ApplicationController include CardScoped before_action :set_comment - before_action :set_reaction, only: %i[ destroy ] - before_action :ensure_permision_to_administer_reaction, only: %i[ destroy ] + before_action :set_reactable + + with_options only: :destroy do + before_action :set_reaction + before_action :ensure_permission_to_administer_reaction + end def index + render "reactions/index" end def new + render "reactions/new" end def create - @reaction = @comment.reactions.create!(params.expect(reaction: :content)) + @reaction = @reactable.reactions.create!(params.expect(reaction: :content)) respond_to do |format| - format.turbo_stream + format.turbo_stream { render "reactions/create" } format.json { head :created } end end @@ -24,7 +30,7 @@ class Cards::Comments::ReactionsController < ApplicationController @reaction.destroy respond_to do |format| - format.turbo_stream + format.turbo_stream { render "reactions/destroy" } format.json { head :no_content } end end @@ -34,11 +40,15 @@ class Cards::Comments::ReactionsController < ApplicationController @comment = @card.comments.find(params[:comment_id]) end - def set_reaction - @reaction = @comment.reactions.find(params[:id]) + def set_reactable + @reactable = @comment end - def ensure_permision_to_administer_reaction + def set_reaction + @reaction = @reactable.reactions.find(params[:id]) + end + + def ensure_permission_to_administer_reaction head :forbidden if Current.user != @reaction.reacter end end diff --git a/app/controllers/cards/comments_controller.rb b/app/controllers/cards/comments_controller.rb index 27c507abe..f2b318813 100644 --- a/app/controllers/cards/comments_controller.rb +++ b/app/controllers/cards/comments_controller.rb @@ -3,6 +3,7 @@ class Cards::CommentsController < ApplicationController before_action :set_comment, only: %i[ show edit update destroy ] before_action :ensure_creatorship, only: %i[ edit update destroy ] + before_action :ensure_card_is_commentable, only: :create def index set_page_and_extract_portion_from @card.comments.chronologically @@ -50,6 +51,10 @@ class Cards::CommentsController < ApplicationController head :forbidden if Current.user != @comment.creator end + def ensure_card_is_commentable + head :forbidden unless @card.commentable? + end + def comment_params params.expect(comment: [ :body, :created_at ]) end diff --git a/app/controllers/cards/pins_controller.rb b/app/controllers/cards/pins_controller.rb index f8da07da3..d46c6a48d 100644 --- a/app/controllers/cards/pins_controller.rb +++ b/app/controllers/cards/pins_controller.rb @@ -9,14 +9,22 @@ class Cards::PinsController < ApplicationController @pin = @card.pin_by Current.user broadcast_add_pin_to_tray - render_pin_button_replacement + + respond_to do |format| + format.turbo_stream { render_pin_button_replacement } + format.json { head :no_content } + end end def destroy @pin = @card.unpin_by Current.user broadcast_remove_pin_from_tray - render_pin_button_replacement + + respond_to do |format| + format.turbo_stream { render_pin_button_replacement } + format.json { head :no_content } + end end private diff --git a/app/controllers/cards/reactions_controller.rb b/app/controllers/cards/reactions_controller.rb new file mode 100644 index 000000000..4105a4121 --- /dev/null +++ b/app/controllers/cards/reactions_controller.rb @@ -0,0 +1,49 @@ +class Cards::ReactionsController < ApplicationController + include CardScoped + + before_action :set_reactable + + with_options only: :destroy do + before_action :set_reaction + before_action :ensure_permission_to_administer_reaction + end + + def index + render "reactions/index" + end + + def new + render "reactions/new" + end + + def create + @reaction = @reactable.reactions.create!(params.expect(reaction: :content)) + + respond_to do |format| + format.turbo_stream { render "reactions/create" } + format.json { head :created } + end + end + + def destroy + @reaction.destroy + + respond_to do |format| + format.turbo_stream { render "reactions/destroy" } + format.json { head :no_content } + end + end + + private + def set_reactable + @reactable = @card + end + + def set_reaction + @reaction = @reactable.reactions.find(params[:id]) + end + + def ensure_permission_to_administer_reaction + head :forbidden if Current.user != @reaction.reacter + end +end diff --git a/app/controllers/my/pins_controller.rb b/app/controllers/my/pins_controller.rb index 4468c75d5..18b58910f 100644 --- a/app/controllers/my/pins_controller.rb +++ b/app/controllers/my/pins_controller.rb @@ -1,6 +1,15 @@ class My::PinsController < ApplicationController def index - @pins = Current.user.pins.includes(:card).ordered.limit(20) + @pins = user_pins fresh_when etag: [ @pins, @pins.collect(&:card) ] end + + private + def user_pins + Current.user.pins.includes(:card).ordered.limit(pins_limit) + end + + def pins_limit + request.format.json? ? 100 : 20 + end end diff --git a/app/controllers/webhooks/activations_controller.rb b/app/controllers/webhooks/activations_controller.rb index cbe3e9e32..82166043e 100644 --- a/app/controllers/webhooks/activations_controller.rb +++ b/app/controllers/webhooks/activations_controller.rb @@ -1,8 +1,10 @@ class Webhooks::ActivationsController < ApplicationController + include BoardScoped + before_action :ensure_admin def create - webhook = Current.account.webhooks.find(params[:webhook_id]) + webhook = @board.webhooks.find(params[:webhook_id]) webhook.activate redirect_to webhook diff --git a/app/helpers/reactions_helper.rb b/app/helpers/reactions_helper.rb new file mode 100644 index 000000000..5c47781f6 --- /dev/null +++ b/app/helpers/reactions_helper.rb @@ -0,0 +1,10 @@ +module ReactionsHelper + def reaction_path_prefix_for(reactable) + case reactable + when Card then [ reactable ] + when Comment then [ reactable.card, reactable ] + else + raise ArgumentError, "Unknown reactable type: #{reactable.class}" + end + end +end diff --git a/app/javascript/controllers/notifications_controller.js b/app/javascript/controllers/notifications_controller.js index 02fce86d7..7104cfa8b 100644 --- a/app/javascript/controllers/notifications_controller.js +++ b/app/javascript/controllers/notifications_controller.js @@ -11,8 +11,7 @@ export default class extends Controller { switch(Notification.permission) { case "default": - this.subscribeButtonTarget.hidden = false - this.explainerTarget.hidden = true + this.#showButtonToSubscribe() break case "granted": const registration = await this.#getServiceWorkerRegistration() @@ -20,6 +19,8 @@ export default class extends Controller { if (registration && subscription) { this.element.classList.add(this.enabledClass) + } else { + this.#showButtonToSubscribe() } break } @@ -54,8 +55,9 @@ export default class extends Controller { return navigator.serviceWorker.getRegistration("/service-worker.js", { scope: "/" }) } - #registerServiceWorker() { - return navigator.serviceWorker.register("/service-worker.js", { scope: "/" }) + async #registerServiceWorker() { + await navigator.serviceWorker.register("/service-worker.js", { scope: "/" }) + return navigator.serviceWorker.ready } async #subscribe(registration) { @@ -76,6 +78,11 @@ export default class extends Controller { } } + #showButtonToSubscribe() { + this.subscribeButtonTarget.hidden = false + this.explainerTarget.hidden = true + } + async #requestPermissionAndSubscribe(registration) { const permission = await Notification.requestPermission() if (permission === "granted") this.#subscribe(registration) diff --git a/app/models/card.rb b/app/models/card.rb index d76512093..d422afa85 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,13 +1,13 @@ class Card < ApplicationRecord - include Accessible, Assignable, Attachments, Broadcastable, Closeable, Colored, Entropic, Eventable, - Exportable, Golden, Mentions, Multistep, Pinnable, Postponable, Promptable, + include Accessible, Assignable, Attachments, Broadcastable, Closeable, Colored, Commentable, + Entropic, Eventable, Exportable, Golden, Mentions, Multistep, Pinnable, Postponable, Promptable, Readable, Searchable, Stallable, Statuses, Storage::Tracked, Taggable, Triageable, Watchable belongs_to :account, default: -> { board.account } belongs_to :board belongs_to :creator, class_name: "User", default: -> { Current.user } - has_many :comments, dependent: :destroy + has_many :reactions, -> { order(:created_at) }, as: :reactable, dependent: :delete_all has_one_attached :image, dependent: :purge_later has_rich_text :description @@ -23,7 +23,7 @@ class Card < ApplicationRecord scope :chronologically, -> { order created_at: :asc, id: :asc } scope :latest, -> { order last_active_at: :desc, id: :desc } scope :with_users, -> { preload(creator: [ :avatar_attachment, :account ], assignees: [ :avatar_attachment, :account ]) } - scope :preloaded, -> { with_users.preload(:column, :tags, :steps, :closure, :goldness, :activity_spike, :image_attachment, board: [ :entropy, :columns ], not_now: [ :user ]).with_rich_text_description_and_embeds } + scope :preloaded, -> { with_users.preload(:column, :tags, :steps, :closure, :goldness, :activity_spike, :image_attachment, reactions: :reacter, board: [ :entropy, :columns ], not_now: [ :user ]).with_rich_text_description_and_embeds } scope :indexed_by, ->(index) do case index @@ -66,55 +66,6 @@ class Card < ApplicationRecord end private - STORAGE_BATCH_SIZE = 1000 - - # Override to include comments, but only load comments that have attachments. - # Cards can have thousands of comments; most won't have attachments. - # Streams in batches to avoid loading all IDs into memory at once. - def storage_transfer_records - comment_ids_with_attachments = storage_comment_ids_with_attachments - - if comment_ids_with_attachments.any? - [ self, *comments.where(id: comment_ids_with_attachments).to_a ] - else - [ self ] - end - end - - def storage_comment_ids_with_attachments - direct = [] - rich_text_map = {} - - # Stream comment IDs in batches to avoid loading all into memory - comments.in_batches(of: STORAGE_BATCH_SIZE) do |batch| - batch_ids = batch.pluck(:id) - - direct.concat \ - ActiveStorage::Attachment - .where(record_type: "Comment", record_id: batch_ids) - .distinct - .pluck(:record_id) - - ActionText::RichText - .where(record_type: "Comment", record_id: batch_ids) - .pluck(:id, :record_id) - .each { |rt_id, comment_id| rich_text_map[rt_id] = comment_id } - end - - embed_comment_ids = if rich_text_map.any? - rich_text_map.keys.each_slice(STORAGE_BATCH_SIZE).flat_map do |batch_ids| - ActiveStorage::Attachment - .where(record_type: "ActionText::RichText", record_id: batch_ids) - .distinct - .pluck(:record_id) - end.filter_map { |rt_id| rich_text_map[rt_id] } - else - [] - end - - (direct + embed_comment_ids).uniq - end - def set_default_title self.title = "Untitled" if title.blank? end diff --git a/app/models/card/commentable.rb b/app/models/card/commentable.rb new file mode 100644 index 000000000..4296a77a7 --- /dev/null +++ b/app/models/card/commentable.rb @@ -0,0 +1,61 @@ +module Card::Commentable + extend ActiveSupport::Concern + + included do + has_many :comments, dependent: :destroy + end + + def commentable? + published? + end + + private + STORAGE_BATCH_SIZE = 1000 + + # Override to include comments, but only load comments that have attachments. + # Cards can have thousands of comments; most won't have attachments. + # Streams in batches to avoid loading all IDs into memory at once. + def storage_transfer_records + comment_ids_with_attachments = storage_comment_ids_with_attachments + + if comment_ids_with_attachments.any? + [ self, *comments.where(id: comment_ids_with_attachments).to_a ] + else + [ self ] + end + end + + def storage_comment_ids_with_attachments + direct = [] + rich_text_map = {} + + # Stream comment IDs in batches to avoid loading all into memory + comments.in_batches(of: STORAGE_BATCH_SIZE) do |batch| + batch_ids = batch.pluck(:id) + + direct.concat \ + ActiveStorage::Attachment + .where(record_type: "Comment", record_id: batch_ids) + .distinct + .pluck(:record_id) + + ActionText::RichText + .where(record_type: "Comment", record_id: batch_ids) + .pluck(:id, :record_id) + .each { |rt_id, comment_id| rich_text_map[rt_id] = comment_id } + end + + embed_comment_ids = if rich_text_map.any? + rich_text_map.keys.each_slice(STORAGE_BATCH_SIZE).flat_map do |batch_ids| + ActiveStorage::Attachment + .where(record_type: "ActionText::RichText", record_id: batch_ids) + .distinct + .pluck(:record_id) + end.filter_map { |rt_id| rich_text_map[rt_id] } + else + [] + end + + (direct + embed_comment_ids).uniq + end +end diff --git a/app/models/card/searchable.rb b/app/models/card/searchable.rb index 8cc108b23..d5f53205d 100644 --- a/app/models/card/searchable.rb +++ b/app/models/card/searchable.rb @@ -25,4 +25,8 @@ module Card::Searchable def search_board_id board_id end + + def searchable? + published? + end end diff --git a/app/models/comment.rb b/app/models/comment.rb index 30706e8b5..70a36562f 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -4,10 +4,12 @@ class Comment < ApplicationRecord belongs_to :account, default: -> { card.account } belongs_to :card, touch: true belongs_to :creator, class_name: "User", default: -> { Current.user } - has_many :reactions, -> { order(:created_at) }, dependent: :delete_all + has_many :reactions, -> { order(:created_at) }, as: :reactable, dependent: :delete_all has_rich_text :body + validate :card_is_commentable + scope :chronologically, -> { order created_at: :asc, id: :desc } scope :preloaded, -> { with_rich_text_body.includes(reactions: :reacter) } scope :by_system, -> { joins(:creator).where(creator: { role: :system }) } @@ -22,6 +24,10 @@ class Comment < ApplicationRecord end private + def card_is_commentable + errors.add(:card, "does not allow comments") unless card.commentable? + end + def watch_card_by_creator card.watch_by creator end diff --git a/app/models/comment/searchable.rb b/app/models/comment/searchable.rb index 626d2be64..3194f9042 100644 --- a/app/models/comment/searchable.rb +++ b/app/models/comment/searchable.rb @@ -20,4 +20,8 @@ module Comment::Searchable def search_board_id card.board_id end + + def searchable? + card.published? + end end diff --git a/app/models/concerns/searchable.rb b/app/models/concerns/searchable.rb index 17d1f186a..2be34fe8d 100644 --- a/app/models/concerns/searchable.rb +++ b/app/models/concerns/searchable.rb @@ -15,11 +15,17 @@ module Searchable private def create_in_search_index - search_record_class.create!(search_record_attributes) + if searchable? + search_record_class.create!(search_record_attributes) + end end def update_in_search_index - search_record_class.upsert!(search_record_attributes) + if searchable? + search_record_class.upsert!(search_record_attributes) + else + remove_from_search_index + end end def remove_from_search_index @@ -53,4 +59,5 @@ module Searchable # - search_content: returns content string # - search_card_id: returns the card id (self.id for cards, card_id for comments) # - search_board_id: returns the board id + # - searchable?: returns whether this record should be indexed end diff --git a/app/models/reaction.rb b/app/models/reaction.rb index 76edf152e..be5fc5d7e 100644 --- a/app/models/reaction.rb +++ b/app/models/reaction.rb @@ -1,6 +1,6 @@ class Reaction < ApplicationRecord - belongs_to :account, default: -> { comment.account } - belongs_to :comment, touch: true + belongs_to :account, default: -> { reactable.account } + belongs_to :reactable, polymorphic: true, touch: true belongs_to :reacter, class_name: "User", default: -> { Current.user } scope :ordered, -> { order(:created_at) } @@ -11,6 +11,6 @@ class Reaction < ApplicationRecord private def register_card_activity - comment.card.touch_last_active_at + reactable.card.touch_last_active_at end end diff --git a/app/views/cards/_card.json.jbuilder b/app/views/cards/_card.json.jbuilder index 75d3ebcf2..0ceb7e339 100644 --- a/app/views/cards/_card.json.jbuilder +++ b/app/views/cards/_card.json.jbuilder @@ -20,4 +20,5 @@ json.cache! card do json.has_more_assignees card.assignees.size > 5 json.comments_url card_comments_url(card) + json.reactions_url card_reactions_url(card) end diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb index de3bc4b27..b6823bd76 100644 --- a/app/views/cards/_container.html.erb +++ b/app/views/cards/_container.html.erb @@ -21,11 +21,14 @@ <% if card.published? %> <%= render "cards/triage/columns", card: card %> <% end %> + + <%= render "cards/display/common/stamp", card: card %> -