diff --git a/Gemfile b/Gemfile index c72bd8326..9296044c0 100644 --- a/Gemfile +++ b/Gemfile @@ -71,7 +71,7 @@ group :test do end require_relative "lib/bootstrap" -unless Bootstrap.local_authentication? +unless Bootstrap.oss_config? eval_gemfile "gems/fizzy-saas/Gemfile" gem "fizzy-saas", path: "gems/fizzy-saas" end diff --git a/Gemfile.lock b/Gemfile.lock index 91da8a5b6..16dca130d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -359,7 +359,7 @@ GEM logger (~> 1.6) letter_opener (1.10.0) launchy (>= 2.2, < 4) - lexxy (0.1.9.beta) + lexxy (0.1.10.beta) rails (>= 8.0.2) lint_roller (1.1.0) logger (1.7.0) diff --git a/app/assets/images/collection.svg b/app/assets/images/collection.svg index 5d6249839..cdbe7d4da 100644 --- a/app/assets/images/collection.svg +++ b/app/assets/images/collection.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index f4ff6ac05..1a50f110f 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -73,10 +73,6 @@ z-index: 3; } - &:not(.cards--considering, .is-collapsed, .cards--grid) { - overflow: clip; - } - &.is-collapsed { border-radius: calc(var(--column-width-collapsed) / 2); flex: 0 0 var(--column-width-collapsed); diff --git a/app/assets/stylesheets/events.css b/app/assets/stylesheets/events.css index 28d6b7c80..42d129125 100644 --- a/app/assets/stylesheets/events.css +++ b/app/assets/stylesheets/events.css @@ -97,6 +97,17 @@ position: absolute; } + .events__filter-select { + font-weight: inherit; + text-decoration: underline; + + @media (any-hover: hover) { + &:hover { + --btn-color: var(--color-link); + } + } + } + .events__day { position: relative; diff --git a/app/assets/stylesheets/lexxy.css b/app/assets/stylesheets/lexxy.css index affd21fef..63f5f1f14 100644 --- a/app/assets/stylesheets/lexxy.css +++ b/app/assets/stylesheets/lexxy.css @@ -31,12 +31,6 @@ position: absolute; white-space: pre-line; } - - /* Disable the submit button when Lexical is empty */ - + .comment__submit { - opacity: 0.5; - pointer-events: none; - } } } diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 18641da1a..873ad8cd9 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -248,7 +248,7 @@ } .attachment--file { - --attachment-icon-color: var(--lexxy-color-text-subtle); + --attachment-icon-color: var(--color-ink-medium); align-items: center; display: flex; @@ -266,6 +266,15 @@ color: var(--color-ink); font-weight: bold; } + + /* Video attachments don't have an identifiable class, but we need to + * make sure the caption is always below the video */ + &:has(video) { + .attachment__caption { + flex: none; + inline-size: 100%; + } + } } .attachment--psd, diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb index 809428f91..526806879 100644 --- a/app/controllers/cards_controller.rb +++ b/app/controllers/cards_controller.rb @@ -27,7 +27,9 @@ class CardsController < ApplicationController end def update - @card.update! card_params + suppressing_broadcasts_unless_published(@card) do + @card.update! card_params + end if @card.published? render_card_replacement @@ -50,6 +52,14 @@ class CardsController < ApplicationController @card = Current.user.accessible_cards.find params[:id] end + def suppressing_broadcasts_unless_published(card, &block) + if card.published? + yield + else + Collection.suppressing_turbo_broadcasts(&block) + end + end + def card_params params.expect(card: [ :status, :title, :description, :image, tag_ids: [] ]) end diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index 89871a9f5..2ede3c108 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -37,7 +37,9 @@ module Authentication end def require_tenant - ApplicationRecord.current_tenant.present? || request_authentication + unless ApplicationRecord.current_tenant.present? + render "sessions/login_menu" + end end def require_authentication diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index d066632bd..11b793b17 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -24,7 +24,7 @@ class UsersController < ApplicationController def show @filter = Current.user.filters.new(creator_ids: [ @user.id ]) - @day_timeline = Current.user.timeline_for(Time.current, filter: @filter) + @day_timeline = Current.user.timeline_for(day_param, filter: @filter) end def update @@ -50,6 +50,14 @@ 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, :email_address, :password, :avatar ]) end diff --git a/app/helpers/accesses_helper.rb b/app/helpers/accesses_helper.rb index 8e3dee6a3..c446dc5a2 100644 --- a/app/helpers/accesses_helper.rb +++ b/app/helpers/accesses_helper.rb @@ -50,7 +50,7 @@ module AccessesHelper collection_involvement_path(collection), method: :put, aria: { labelledby: involvement_label_id }, - class: "btn", + class: class_names("btn", { "btn--reversed": access.involvement == "watching" && icon_only }), params: { show_watchers: show_watchers, involvement: next_involvement(access.involvement), icon_only: icon_only } ) do safe_join([ diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 0eecdb25f..9200f84b7 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -52,17 +52,6 @@ module EventsHelper tag.div class: "events__time-block", style: "grid-area: #{row}/#{col}", & end - def event_next_page_link(next_day) - if next_day - tag.div id: "next_page", - data: { controller: "fetch-on-visible", - fetch_on_visible_url_value: events_days_path( - day: next_day.strftime("%Y-%m-%d"), - **@filter.as_params - ) } - end - end - def event_action_sentence(event) if event.action.comment_created? comment_event_action_sentence(event) diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb index 8f2fc7988..6c67400c7 100644 --- a/app/helpers/pagination_helper.rb +++ b/app/helpers/pagination_helper.rb @@ -35,6 +35,30 @@ module PaginationHelper end end + def with_automatic_pagination(name, page, **properties) + pagination_list name, paginate_on_scroll: true, **properties do + concat(pagination_frame_tag(name, page) do + yield + concat link_to_next_page(name, page, activate_when_observed: true) + end) + end + end + + def day_timeline_pagination_frame_tag(day_timeline, &) + turbo_frame_tag day_timeline_pagination_frame_id_for(day_timeline.day), data: { timeline_target: "frame" }, role: "presentation", refresh: :morph, & + end + + def day_timeline_pagination_frame_id_for(day) + "day-timeline-pagination-contents-#{day.strftime("%Y-%m-%d")}" + end + + def day_timeline_pagination_link(day_timeline, filter) + if day_timeline.next_day + link_to "Load more…", events_days_path(day: day_timeline.next_day.strftime("%Y-%m-%d"), **filter.as_params), + class: "txt-reversed", data: { frame: day_timeline_pagination_frame_id_for(day_timeline.next_day), pagination_target: "paginationLink" } + end + end + private def pagination_list(name, tag_element: :div, paginate_on_scroll: false, **properties, &block) classes = properties.delete(:class) diff --git a/app/javascript/controllers/collapsible_columns_controller.js b/app/javascript/controllers/collapsible_columns_controller.js index 84ef0d315..5002c46e2 100644 --- a/app/javascript/controllers/collapsible_columns_controller.js +++ b/app/javascript/controllers/collapsible_columns_controller.js @@ -14,7 +14,7 @@ export default class extends Controller { } preventToggle(event) { - if (event.detail.attributeName === "class") { + if (event.target.hasAttribute("data-collapsible-columns-target") && event.detail.attributeName === "class") { event.preventDefault() } } diff --git a/app/javascript/controllers/filter_settings_controller.js b/app/javascript/controllers/filter_settings_controller.js index 7a207da80..6be549e5c 100644 --- a/app/javascript/controllers/filter_settings_controller.js +++ b/app/javascript/controllers/filter_settings_controller.js @@ -5,7 +5,7 @@ import { post } from "@rails/request.js" export default class extends Controller { static classes = ["filtersSet"] static targets = ["field", "form"] - static values = { refreshUrl: String, noFilteringUrl: String } + static values = { refreshUrl: String, noFilteringUrl: String, cardsUrl: String } initialize() { this.debouncedToggle = debounce(this.#toggle.bind(this), 50) @@ -31,6 +31,12 @@ export default class extends Controller { this.debouncedToggle() } + submitToGenericCardsView() { + this.formTarget.action = this.cardsUrlValue + this.formTarget.dataset.turboFrame = "top" + this.formTarget.requestSubmit() + } + #toggle() { this.element.classList.toggle(this.filtersSetClass, this.#hasFiltersSet) } diff --git a/app/javascript/controllers/form_controller.js b/app/javascript/controllers/form_controller.js index 4e59f6055..2b30c99b9 100644 --- a/app/javascript/controllers/form_controller.js +++ b/app/javascript/controllers/form_controller.js @@ -1,8 +1,8 @@ import { Controller } from "@hotwired/stimulus" -import { debounce } from "helpers/timing_helpers"; +import { debounce, nextFrame } from "helpers/timing_helpers"; export default class extends Controller { - static targets = [ "cancel" ] + static targets = [ "cancel", "submit" ] static values = { debounceTimeout: { type: Number, default: 300 } @@ -37,6 +37,16 @@ export default class extends Controller { event.preventDefault() } + async disableSubmitWhenInvalid(event) { + await nextFrame() + + if (this.element.checkValidity()) { + this.submitTarget.removeAttribute("disabled") + } else { + this.submitTarget.toggleAttribute("disabled", true) + } + } + select(event) { event.target.select() } diff --git a/app/javascript/controllers/multi_selection_combobox_controller.js b/app/javascript/controllers/multi_selection_combobox_controller.js index 70a853b2b..80d7f0826 100644 --- a/app/javascript/controllers/multi_selection_combobox_controller.js +++ b/app/javascript/controllers/multi_selection_combobox_controller.js @@ -82,15 +82,14 @@ export default class extends Controller { #clearHiddenFields() { this.element.querySelectorAll('input[type="hidden"]').forEach(field => { - if (field !== this.hiddenField) { - field.remove() - } + field.remove() }) } #addHiddenFields() { this.#selectedValues().forEach(value => { - const [field] = this.hiddenFieldTemplateTarget.content.cloneNode(true).children + const [ field ] = this.hiddenFieldTemplateTarget.content.cloneNode(true).children + field.removeAttribute("id") field.value = value this.element.appendChild(field) }) diff --git a/app/models/account.rb b/app/models/account.rb index 950c0a124..c29f61005 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -4,7 +4,7 @@ class Account < ApplicationRecord has_many_attached :uploads def slug - "/#{tenant_id}" + "/#{tenant}" end def setup_basic_template diff --git a/app/models/card.rb b/app/models/card.rb index 9175d6b96..c837c781a 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -22,7 +22,6 @@ class Card < ApplicationRecord case index when "stalled" then stalled when "postponing_soon" then postponing_soon - when "falling_back_soon" then falling_back_soon when "closed" then closed.recently_closed_first when "golden" then golden when "draft" then drafted diff --git a/app/models/filter/fields.rb b/app/models/filter/fields.rb index fc11752fc..027ae8ac6 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 stalled postponing_soon falling_back_soon golden draft ] + INDEXES = %w[ all stalled postponing_soon golden draft ] SORTED_BY = %w[ newest oldest latest ] delegate :default_value?, to: :class diff --git a/app/models/user/filtering.rb b/app/models/user/filtering.rb index 9f321bbed..6e00ecbc1 100644 --- a/app/models/user/filtering.rb +++ b/app/models/user/filtering.rb @@ -67,6 +67,10 @@ class User::Filtering filter.closers.any? end + def show_collections? + filter.collections.any? + end + def enable_collection_filtering(&block) @collection_filtering_route_resolver = block end diff --git a/app/views/cards/comments/_new.html.erb b/app/views/cards/comments/_new.html.erb index 7931cd244..015c93a83 100644 --- a/app/views/cards/comments/_new.html.erb +++ b/app/views/cards/comments/_new.html.erb @@ -10,10 +10,10 @@ local_save_key_value: "comment-#{card.id}", action: "turbo:submit-end->local-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do |form| %> <%= form.rich_textarea :body, required: true, placeholder: new_comment_placeholder(card), - data: { local_save_target: "input", action: "lexxy:change->local-save#save turbo:morph-element->local-save#restoreContent" } do %> + data: { local_save_target: "input", action: "lexxy:change->form#disableSubmitWhenInvalid lexxy:change->local-save#save turbo:morph-element->local-save#restoreContent" } do %> <%= general_prompts(@card.collection) %> <% end %> - <%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start" do %> + <%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start", data: { form_target: "submit" }, disabled: true do %> Post this comment <% end %> diff --git a/app/views/cards/display/_preview.html.erb b/app/views/cards/display/_preview.html.erb index 3f3b4b139..ce8f3d026 100644 --- a/app/views/cards/display/_preview.html.erb +++ b/app/views/cards/display/_preview.html.erb @@ -27,7 +27,7 @@ diff --git a/app/views/cards/index.html.erb b/app/views/cards/index.html.erb index 1e457c452..47d15dcb8 100644 --- a/app/views/cards/index.html.erb +++ b/app/views/cards/index.html.erb @@ -27,6 +27,8 @@ <%= turbo_frame_tag :cards_container do %>
- <%= render partial: "cards/display/preview", collection: @page.records, as: :card, locals: { draggable: true }, cached: ->(card) { cacheable_preview_parts_for(card) } %> + <%= with_automatic_pagination :cards_paginated_container, @page do %> + <%= render partial: "cards/display/preview", collection: @page.records, as: :card, locals: { draggable: true }, cached: ->(card) { cacheable_preview_parts_for(card) } %> + <% end %>
<% end %> diff --git a/app/views/collections/edit.html.erb b/app/views/collections/edit.html.erb index b6b0efe6c..f565cc972 100644 --- a/app/views/collections/edit.html.erb +++ b/app/views/collections/edit.html.erb @@ -3,7 +3,7 @@ <% content_for :header do %> <%= render "filters/menu", user_filtering: @user_filtering %>
- <%= link_to cards_path(collection_ids: [ @collection ]), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> + <%= link_to @collection, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> <%= @collection.name %> @@ -38,7 +38,6 @@
- <%# render "collections/edit/workflows", collection: @collection %> <%= render "collections/edit/auto_close", collection: @collection %> <%= render "collections/edit/publication", collection: @collection %> <%= render "collections/edit/delete", collection: @collection %> diff --git a/app/views/collections/show.html.erb b/app/views/collections/show.html.erb index ac7343885..96392f1d2 100644 --- a/app/views/collections/show.html.erb +++ b/app/views/collections/show.html.erb @@ -1,6 +1,8 @@ <% @page_title = @collection.name %> <% turbo_exempts_page_from_cache %> +<%= turbo_stream_from @collection %> + <% content_for :head do %> <%= tag.meta property: "og:title", content: @page_title %> <%= tag.meta property: "og:description", content: Account.sole.name %> diff --git a/app/views/collections/show/menu/_column.html.erb b/app/views/collections/show/menu/_column.html.erb index 4368cb5da..d53bdddb8 100644 --- a/app/views/collections/show/menu/_column.html.erb +++ b/app/views/collections/show/menu/_column.html.erb @@ -1,4 +1,4 @@ -
+
diff --git a/app/views/events/days/index.html.erb b/app/views/events/days/index.html.erb new file mode 100644 index 000000000..9529e8018 --- /dev/null +++ b/app/views/events/days/index.html.erb @@ -0,0 +1,4 @@ +<%= day_timeline_pagination_frame_tag @day_timeline do %> + <%= render "events/day", day_timeline: @day_timeline %> + <%= day_timeline_pagination_link(@day_timeline, @filter) %> +<% end %> diff --git a/app/views/events/days/index.turbo_stream.erb b/app/views/events/days/index.turbo_stream.erb deleted file mode 100644 index cceb2d352..000000000 --- a/app/views/events/days/index.turbo_stream.erb +++ /dev/null @@ -1,7 +0,0 @@ -<%= turbo_stream.append :activity do %> - <%= render "events/day", day_timeline: @day_timeline %> -<% end %> - -<%= turbo_stream.replace :next_page do %> - <%= event_next_page_link(@day_timeline.next_day) %> -<% end %> diff --git a/app/views/events/event/_layout.html.erb b/app/views/events/event/_layout.html.erb index 48b25d0bd..69a1560e6 100644 --- a/app/views/events/event/_layout.html.erb +++ b/app/views/events/event/_layout.html.erb @@ -1,9 +1,11 @@ <%= link_to event.notifiable_target, class: "event event--#{ event.action } #{ "golden-effect" if event.card.golden? } center-block flex flex-column full-width align-start justify-start position-relative", style: "--card-color: #{ card.closed? ? "var(--color-card-complete)" : card.color };", - data: { related_element_target: "related", - related_element_group_value: card.id, - action: "mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %> + target: "_top", + data: { + related_element_target: "related", + related_element_group_value: card.id, + action: "mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %>

<%= card.id %> diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb index dcc0101c7..8acc92214 100644 --- a/app/views/events/index.html.erb +++ b/app/views/events/index.html.erb @@ -1,20 +1,22 @@ <% @page_title = "Home" %> <% @header_class = "header--events" %> -<%= turbo_stream_from :activity_summaries %> +<%= render "filters/broadcasts", filter: @filter %> <% content_for :header do %> <%= render "filters/menu", user_filtering: @user_filtering %> <%= render "events/index/add_card_button", user_filtering: @user_filtering %> -

-
- Activity in <%= @user_filtering.selected_collections_label %> -
+

+ Activity in + <%= render "events/index/filter", user_filtering: @user_filtering %>

<% end %> -
- <%= render "events/day", day_timeline: @day_timeline %> -
-<%= event_next_page_link(@day_timeline.next_day) %> +<%= tag.div id: "activity", class: "events", data: { controller: "pagination", pagination_paginate_on_intersection_value: true } do %> + <%= day_timeline_pagination_frame_tag @day_timeline do %> + <%= render "events/day", day_timeline: @day_timeline %> + + <%= day_timeline_pagination_link(@day_timeline, @filter) %> + <% end %> +<% end %> diff --git a/app/views/events/index/_filter.html.erb b/app/views/events/index/_filter.html.erb new file mode 100644 index 000000000..f48ee4a6c --- /dev/null +++ b/app/views/events/index/_filter.html.erb @@ -0,0 +1,43 @@ +<% filter = user_filtering.filter %> + +<%= form_with url: events_path, + method: :get, class: "flex-inline position-relative", + data: { controller: "form", turbo_frame: "cards_container", turbo_action: "advance" } do |form| %> + <%= tag.div class: "flex-inline position-relative quick-filter", + data: { + controller: "dialog multi-selection-combobox", + action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside", + filter_show: user_filtering.show_collections?, + multi_selection_combobox_no_selection_label_value: user_filtering.selected_collections_label } do %> + + + + + <%= filter_dialog "Collection…" do %> + Collection… + + <% if user_filtering.collections.many? %> + <%= text_field_tag nil, nil, placeholder: "Filter…", class: "input input--transparent txt-small font-weight-normal", autofocus: true, + type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %> + <% end %> + + + <% end %> + <% end %> +<% end %> diff --git a/app/views/filters/_menu.html.erb b/app/views/filters/_menu.html.erb index 7b84cdd60..a117bdfa7 100644 --- a/app/views/filters/_menu.html.erb +++ b/app/views/filters/_menu.html.erb @@ -1,4 +1,4 @@ -