diff --git a/Gemfile b/Gemfile index 2f56fa81d..61515a304 100644 --- a/Gemfile +++ b/Gemfile @@ -10,7 +10,7 @@ gem "stimulus-rails" gem "turbo-rails" # Deployment and drivers -gem "active_record-tenanted", bc: "active_record-tenanted" +gem "active_record-tenanted", bc: "active_record-tenanted", branch: "flavorjones/path-based-tenanting" gem "bootsnap", require: false gem "kamal", bc: "kamal", ref: "344e2d79", require: false gem "puma", ">= 5.0" diff --git a/Gemfile.lock b/Gemfile.lock index bdf346d74..d2d32a4b8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,7 +7,8 @@ GIT GIT remote: https://github.com/basecamp/active_record-tenanted - revision: 81a4d02a363fa749de517c90e19588365f934913 + revision: 8e33088cdbd69ab7b4cf4100d4202a54aa97e4c0 + branch: flavorjones/path-based-tenanting specs: active_record-tenanted (0.1.0) activerecord (>= 8.1.alpha) @@ -17,9 +18,9 @@ GIT GIT remote: https://github.com/basecamp/file_repository - revision: 17e90fc35ccb9f0a0dae1ed8623f01386696002d + revision: 8c8d1594f3ec7e8c0938e6a7f7def521692344e4 specs: - file_repository (1.4.5) + file_repository (1.4.6) GIT remote: https://github.com/basecamp/kamal @@ -40,7 +41,7 @@ GIT GIT remote: https://github.com/basecamp/portfolio - revision: 26dd0ccbe4aad5a5c539f90c00e82b6175443636 + revision: 34b48dd3c6dc78abd162ef26481971a228084d04 specs: portfolio (5.2.0) activesupport @@ -81,7 +82,7 @@ GIT GIT remote: https://github.com/basecamp/signal_id - revision: 8908b2550ff55325b04c108146e00b4408a64e93 + revision: 32758d0bff9f17e938905ff5bec53fff359e21b0 branch: rails4 specs: signal_id (4.3.2) @@ -111,7 +112,7 @@ GIT GIT remote: https://github.com/rails/rails.git - revision: 53a9a3181a59f4e6dff73ba9453734edd7f7e076 + revision: 58a2187f7b2ac391eba841e9ec6bb1a6d4e486dd branch: main specs: actioncable (8.1.0.alpha) @@ -226,18 +227,18 @@ GEM public_suffix (>= 2.0.2, < 7.0) ast (2.4.3) aws-eventstream (1.4.0) - aws-partitions (1.1119.0) - aws-sdk-core (3.226.0) + aws-partitions (1.1124.0) + aws-sdk-core (3.226.2) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) base64 jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.105.0) + aws-sdk-kms (1.106.0) aws-sdk-core (~> 3, >= 3.225.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.190.0) + aws-sdk-s3 (1.192.0) aws-sdk-core (~> 3, >= 3.225.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -290,11 +291,11 @@ GEM faraday-net_http (>= 2.0, < 3.5) json logger - faraday-multipart (1.1.0) + faraday-multipart (1.1.1) multipart-post (~> 2.0) - faraday-net_http (3.4.0) + faraday-net_http (3.4.1) net-http (>= 0.5.0) - faraday-retry (2.3.1) + faraday-retry (2.3.2) faraday (~> 2.0) ffi (1.17.2) ffi (1.17.2-arm64-darwin) @@ -330,7 +331,7 @@ GEM activesupport (>= 5.0.0) jmespath (1.6.2) json (2.12.2) - jwt (2.10.1) + jwt (3.1.2) base64 language_server-protocol (3.17.0.5) lint_roller (1.1.0) @@ -344,7 +345,7 @@ GEM net-pop net-smtp marcel (1.0.4) - matrix (0.4.2) + matrix (0.4.3) mini_magick (5.2.0) benchmark logger @@ -393,7 +394,7 @@ GEM racc (~> 1.4) nokogiri (1.18.8-x86_64-linux-gnu) racc (~> 1.4) - ostruct (0.6.1) + ostruct (0.6.2) parallel (1.27.0) parser (3.3.8.0) ast (~> 2.4.1) @@ -456,7 +457,7 @@ GEM chunky_png (~> 1.0) rqrcode_core (~> 2.0) rqrcode_core (2.0.0) - rubocop (1.75.8) + rubocop (1.77.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -464,10 +465,10 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.44.0, < 2.0) + rubocop-ast (>= 1.45.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.44.1) + rubocop-ast (1.45.1) parser (>= 3.3.7.2) prism (~> 1.4) rubocop-performance (1.25.0) @@ -485,25 +486,25 @@ GEM rubocop-performance (>= 1.24) rubocop-rails (>= 2.30) ruby-progressbar (1.13.0) - ruby-vips (2.2.3) + ruby-vips (2.2.4) ffi (~> 1.12) logger ruby2_keywords (0.0.5) rubyzip (2.4.1) securerandom (0.4.1) - selenium-webdriver (4.33.0) + selenium-webdriver (4.34.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - sentry-rails (5.25.0) + sentry-rails (5.26.0) railties (>= 5.0) - sentry-ruby (~> 5.25.0) - sentry-ruby (5.25.0) + sentry-ruby (~> 5.26.0) + sentry-ruby (5.26.0) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) - solid_cable (3.0.8) + solid_cable (3.0.11) actioncable (>= 7.2) activejob (>= 7.2) activerecord (>= 7.2) @@ -519,11 +520,11 @@ GEM fugit (~> 1.11.0) railties (>= 7.1) thor (~> 1.3.1) - sqlite3 (2.7.0) + sqlite3 (2.7.1) mini_portile2 (~> 2.8.0) - sqlite3 (2.7.0-arm64-darwin) - sqlite3 (2.7.0-x86_64-darwin) - sqlite3 (2.7.0-x86_64-linux-gnu) + sqlite3 (2.7.1-arm64-darwin) + sqlite3 (2.7.1-x86_64-darwin) + sqlite3 (2.7.1-x86_64-linux-gnu) sshkit (1.24.0) base64 logger diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index bdbad0bcc..4e028663f 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -85,7 +85,7 @@ .card__background { .card:has(& img:not([src=""])) { - .card__body, + .card__content, .card__meta, .workflow-stage:not(.workflow-stage--current) { opacity: 0; @@ -108,6 +108,10 @@ } } } + + .card__steps { + display: flex; + } } /* Column elements @@ -197,7 +201,7 @@ .card__header { color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); - margin-inline-start: calc(-0.5 * var(--card-padding-inline)); + margin-inline: calc(-0.5 * var(--card-padding-inline)) calc(-0.25 * var(--card-padding-inline)); } .card__people-label { diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index 9595b98ee..4462dfb6e 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -119,8 +119,8 @@ flex-wrap: nowrap; gap: var(--inline-space); margin-block-start: calc(-1 * var(--card-padding-block)); - margin-inline-start: calc(-1 * var(--card-padding-inline)); - max-inline-size: 100%; + margin-inline: calc(-1 * var(--card-padding-inline)) calc(-0.5 * var(--card-padding-inline)); + max-inline-size: unset; min-inline-size: 0; .workflow-stage { diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index bba7f8a34..a8b8da16b 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -37,8 +37,14 @@ } .comment__author { - .comment--system & strong { - display: none; + .comment--system & { + h3 { + margin-inline: auto; + } + + strong { + display: none; + } } .btn { diff --git a/app/assets/stylesheets/icons.css b/app/assets/stylesheets/icons.css index fd12630d9..2870088c2 100644 --- a/app/assets/stylesheets/icons.css +++ b/app/assets/stylesheets/icons.css @@ -30,6 +30,7 @@ .icon--camera { --svg: url("camera.svg "); } .icon--caret-down { --svg: url("caret-down.svg "); } .icon--check { --svg: url("check.svg "); } + .icon--check-all { --svg: url("check-all.svg "); } .icon--clipboard { --svg: url("clipboard.svg "); } .icon--close { --svg: url("close.svg "); } .icon--collection { --svg: url("collection.svg "); } diff --git a/app/assets/stylesheets/inputs.css b/app/assets/stylesheets/inputs.css index 3acda5855..ed4a51072 100644 --- a/app/assets/stylesheets/inputs.css +++ b/app/assets/stylesheets/inputs.css @@ -37,7 +37,6 @@ } .input--file { - border: 0; cursor: pointer; display: grid; inline-size: auto; @@ -52,6 +51,7 @@ } input[type="file"] { + --hover-size: 0; --input-border-color: transparent; --input-border-radius: 8px; @@ -69,9 +69,9 @@ &:focus, &:focus-visible { - --input-border-color: var(--color-selected-dark); - --input-border-radius: 8px; - --input-border-size: 0.15rem; + .input--file:has(&) { + outline: 0.15rem solid var(--color-selected-dark); + } } } diff --git a/app/assets/stylesheets/steps.css b/app/assets/stylesheets/steps.css new file mode 100644 index 000000000..3e271137f --- /dev/null +++ b/app/assets/stylesheets/steps.css @@ -0,0 +1,108 @@ +@layer components { + .step { + display: grid; + grid-template-columns: 1em auto auto; + gap: calc(var(--inline-space) * 2/3); + inline-size: auto; + } + + .step__checkbox { + --hover-color: var(--card-color); + + appearance: none; + background-color: var(--color-canvas); + block-size: 1.1em; + border: 1px solid currentColor; + border-radius: 0.15em; + color: currentColor; + display: grid; + font: inherit; + inline-size: 1.1em; + margin: 0; + place-content: center; + transform: translateY(0.1em); + + &::before { + background-color: CanvasText; + block-size: 0.65em; + box-shadow: inset 1em 1em currentColor; + clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); + content: ""; + inline-size: 0.65em; + transform: scale(0); + transform-origin: center; + transition: 150ms transform ease-in-out; + } + + &:checked::before { + transform: scale(1) rotate(10deg); + } + + &:where([disabled]):not(:hover):not(:active) { + filter: none; + opacity: 0.5; + } + } + + .step__content { + --input-border-radius: 0; + --input-border-size: 0; + --input-padding: 0; + + border-bottom: 1px solid transparent; + color: currentColor; + font-weight: 500; + margin-block-end: calc(var(--block-space) * 1/3); + + &:is(a, input[type=text]) { + --hover-size: 0; + } + + .step:has(:checked) & { + opacity: 0.7; + text-decoration: line-through; + } + + &::placeholder { + color: var(--card-color); + } + + &:is(input) { + max-inline-size: 70ch; + min-inline-size: 30ch; + + @supports (field-sizing: content) { + field-sizing: content; + min-inline-size: 15ch; + } + } + } + + .step__content--edit { + border-bottom-color: currentColor; + } + + .steps { + contain: inline-size; + display: grid; + list-style: none; + margin: 0; + max-inline-size: 100%; + padding: 0; + } + + .steps__icon { + background-color: var(--card-color); + block-size: 1.3em; + border-radius: 50%; + display: grid; + inline-size: 1.3em; + place-content: center; + + .icon { + background-color: var(--color-ink-inverted); + block-size: 0.8em; + inline-size: 0.8em; + } + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/trays.css b/app/assets/stylesheets/trays.css index d90c56ea8..48ae50b31 100644 --- a/app/assets/stylesheets/trays.css +++ b/app/assets/stylesheets/trays.css @@ -315,6 +315,7 @@ .card__meta .btn, .card__meta-item:not(.card__meta-item--updated), .card__stages, + .card__steps, .card__closed { display: none; } diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index b39e7a216..0f936d05b 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -2,8 +2,9 @@ module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :current_user - tenanted_connection do - set_current_user || reject_unauthorized_connection + def connect + super + ApplicationRecord.with_tenant(current_tenant) { set_current_user || reject_unauthorized_connection } end private diff --git a/app/controllers/cards/steps_controller.rb b/app/controllers/cards/steps_controller.rb new file mode 100644 index 000000000..4f3434632 --- /dev/null +++ b/app/controllers/cards/steps_controller.rb @@ -0,0 +1,35 @@ +class Cards::StepsController < ApplicationController + include CardScoped + + before_action :set_step, only: %i[ show edit update destroy ] + + def create + @step = @card.steps.create!(step_params) + render_card_replacement + end + + def show + end + + def edit + end + + def update + @step.update!(step_params) + render_card_replacement + end + + def destroy + @step.destroy! + render_card_replacement + end + + private + def set_step + @step = @card.steps.find(params[:id]) + end + + def step_params + params.expect(step: [ :content, :completed ]) + end +end diff --git a/app/controllers/collections/entropy_configurations_controller.rb b/app/controllers/collections/entropy_configurations_controller.rb new file mode 100644 index 000000000..98a2b1b0f --- /dev/null +++ b/app/controllers/collections/entropy_configurations_controller.rb @@ -0,0 +1,14 @@ +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 }) + end + + private + def entropy_configuration_params + params.expect(collection: [ :auto_close_period, :auto_reconsider_period ]) + end +end diff --git a/app/controllers/collections/publications_controller.rb b/app/controllers/collections/publications_controller.rb index feb5ae4b7..2ded7eda2 100644 --- a/app/controllers/collections/publications_controller.rb +++ b/app/controllers/collections/publications_controller.rb @@ -3,11 +3,12 @@ class Collections::PublicationsController < ApplicationController def create @collection.publish - redirect_to edit_collection_path(@collection) + render turbo_stream: turbo_stream.replace([ @collection, :publication ], partial: "collections/edit/publication", locals: { collection: @collection }) end def destroy @collection.unpublish - redirect_to edit_collection_path(@collection) + @collection.reload + render turbo_stream: turbo_stream.replace([ @collection, :publication ], partial: "collections/edit/publication", locals: { collection: @collection }) end end diff --git a/app/controllers/collections/workflows_controller.rb b/app/controllers/collections/workflows_controller.rb index 28b691f11..fd503473c 100644 --- a/app/controllers/collections/workflows_controller.rb +++ b/app/controllers/collections/workflows_controller.rb @@ -5,7 +5,7 @@ class Collections::WorkflowsController < ApplicationController def update @collection.update! workflow: @workflow - redirect_to cards_path(collection_ids: [ @collection ]) + render turbo_stream: turbo_stream.replace([ @collection, :workflows ], partial: "collections/edit/workflows", locals: { collection: @collection }) end private diff --git a/app/controllers/commands_controller.rb b/app/controllers/commands_controller.rb index adae98eb1..a5f6b90f0 100644 --- a/app/controllers/commands_controller.rb +++ b/app/controllers/commands_controller.rb @@ -29,7 +29,7 @@ class CommandsController < ApplicationController end def parsing_context - @parsing_context ||= Command::Parser::Context.new(Current.user, url: request.referrer) + @parsing_context ||= Command::Parser::Context.new(Current.user, url: request.referrer, script_name: request.script_name) end def confirmed?(command) diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index dc3ee7bb7..db21f5c7e 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -2,6 +2,9 @@ module Authentication extend ActiveSupport::Concern included do + # Checking for tenant must happen first so we redirect before trying to access the db. + before_action :require_tenant + before_action :set_current_account before_action :require_authentication helper_method :authenticated? @@ -19,6 +22,7 @@ module Authentication end def require_untenanted_access(**options) + skip_before_action :require_tenant, **options skip_before_action :set_current_account, **options skip_before_action :require_authentication, **options before_action :redirect_tenanted_request, **options @@ -30,6 +34,10 @@ module Authentication Current.session.present? end + def require_tenant + ApplicationRecord.current_tenant.present? || request_authentication + end + def require_authentication resume_session || request_authentication end @@ -48,11 +56,7 @@ module Authentication def request_authentication session[:return_to_after_authenticating] = request.url - if Rails.application.config.x.local_authentication - redirect_to new_session_path - else - redirect_to Launchpad.login_url(account: Current.account), allow_other_host: true - end + redirect_to Launchpad.login_url(product: true, account: Current.account), allow_other_host: true end def after_authentication_url diff --git a/app/controllers/sessions/launchpad_controller.rb b/app/controllers/sessions/launchpad_controller.rb index 0e2ad16a7..8a9a082dd 100644 --- a/app/controllers/sessions/launchpad_controller.rb +++ b/app/controllers/sessions/launchpad_controller.rb @@ -1,8 +1,7 @@ class Sessions::LaunchpadController < ApplicationController require_unauthenticated_access - before_action :store_sig, only: :show - before_action :restore_and_clear_sig, only: :update + before_action :require_sig def show end @@ -17,11 +16,7 @@ class Sessions::LaunchpadController < ApplicationController end private - def store_sig - cookies[:_fizzy_launchpad_sig] = params.expect(:sig) - end - - def restore_and_clear_sig - @sig = cookies.delete :_fizzy_launchpad_sig + def require_sig + @sig = params.expect(:sig) end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 428c5a7f4..fa7d98974 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,28 +1,6 @@ class SessionsController < ApplicationController - require_unauthenticated_access only: %i[ new create ] - rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." } - - before_action :require_first_run_completion - - def new - end - - def create - if user = User.active.authenticate_by(params.permit(:email_address, :password)) - start_new_session_for user - redirect_to after_authentication_url - else - redirect_to new_session_path, alert: "Try another email address or password." - end - end - def destroy terminate_session - redirect_to new_session_path + redirect_to Launchpad.login_url, allow_other_host: true end - - private - def require_first_run_completion - redirect_to first_run_path if Account.none? - end end diff --git a/app/controllers/signup/base_controller.rb b/app/controllers/signup/base_controller.rb index 40ae83e92..088937b04 100644 --- a/app/controllers/signup/base_controller.rb +++ b/app/controllers/signup/base_controller.rb @@ -1,6 +1,5 @@ class Signup::BaseController < ApplicationController require_untenanted_access - before_action :redirect_if_local_auth # TODO: Remove this auth before launch. http_basic_authenticate_with( @@ -34,8 +33,4 @@ class Signup::BaseController < ApplicationController redirect_to account.signal_account.owner.remote_login_url(proceed_to: root_path), allow_other_host: true end - - def redirect_if_local_auth - render plain: "Unauthorized", status: :unauthorized if Rails.application.config.x.local_authentication - end end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 6b4ffd47d..51e69e94b 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -37,7 +37,7 @@ module EventsHelper def event_cluster_tag(hour, col, &) row = 25 - hour - tag.ul class: "events__time-block", style: "grid-area: #{row}/#{col}", & + tag.div class: "events__time-block", style: "grid-area: #{row}/#{col}", & end def event_next_page_link(next_day) diff --git a/app/helpers/tenanting_helper.rb b/app/helpers/tenanting_helper.rb new file mode 100644 index 000000000..b5c50e80d --- /dev/null +++ b/app/helpers/tenanting_helper.rb @@ -0,0 +1,7 @@ +module TenantingHelper + def tenanted_action_cable_meta_tag + tag "meta", + name: "action-cable-url", + content: "#{request.script_name}#{ActionCable.server.config.mount_path}" + end +end diff --git a/app/javascript/controllers/dialog_controller.js b/app/javascript/controllers/dialog_controller.js index fd4d122e0..123c2384e 100644 --- a/app/javascript/controllers/dialog_controller.js +++ b/app/javascript/controllers/dialog_controller.js @@ -6,6 +6,10 @@ export default class extends Controller { modal: { type: Boolean, default: false } } + connect() { + this.dialogTarget.setAttribute('aria-hidden', 'true') + } + open() { const modal = this.modalValue @@ -14,6 +18,7 @@ export default class extends Controller { } else { this.dialogTarget.show() } + this.dialogTarget.setAttribute('aria-hidden', 'false') } toggle() { @@ -26,6 +31,7 @@ export default class extends Controller { close() { this.dialogTarget.close() + this.dialogTarget.setAttribute('aria-hidden', 'true') this.dialogTarget.blur() } diff --git a/app/jobs/sqlite_backups_job.rb b/app/jobs/sqlite_backups_job.rb index 6e7559d80..10b0efe37 100644 --- a/app/jobs/sqlite_backups_job.rb +++ b/app/jobs/sqlite_backups_job.rb @@ -92,7 +92,7 @@ class SQLiteBackupsJob < ApplicationJob end def db_path(tenant) - db_config.database + db_config.database_path end def db_config diff --git a/app/models/account/signal_account.rb b/app/models/account/signal_account.rb index 610a4ed9c..a01af7ad2 100644 --- a/app/models/account/signal_account.rb +++ b/app/models/account/signal_account.rb @@ -2,10 +2,7 @@ module Account::SignalAccount extend ActiveSupport::Concern included do - unless Rails.application.config.x.local_authentication - # TODO: remove the "optional: true" once we've populated the accounts properly - belongs_to :signal_account, class_name: "SignalId::Account", primary_key: :queenbee_id, foreign_key: :queenbee_id, optional: true - end + belongs_to :signal_account, class_name: "SignalId::Account", primary_key: :queenbee_id, foreign_key: :queenbee_id end class_methods do diff --git a/app/models/card.rb b/app/models/card.rb index 94ee043d2..c83c1325b 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,6 +1,6 @@ class Card < ApplicationRecord include Assignable, Colored, Engageable, Entropic, Eventable, - Golden, Mentions, Pinnable, Closeable, Readable, Searchable, + Golden, Mentions, Multistep, Pinnable, Closeable, Readable, Searchable, Staged, Stallable, Statuses, Taggable, Watchable belongs_to :collection, touch: true diff --git a/app/models/card/multistep.rb b/app/models/card/multistep.rb new file mode 100644 index 000000000..e69b07e6f --- /dev/null +++ b/app/models/card/multistep.rb @@ -0,0 +1,7 @@ +module Card::Multistep + extend ActiveSupport::Concern + + included do + has_many :steps, dependent: :destroy + end +end diff --git a/app/models/card/statuses.rb b/app/models/card/statuses.rb index 636d9127d..73d3f122a 100644 --- a/app/models/card/statuses.rb +++ b/app/models/card/statuses.rb @@ -4,6 +4,8 @@ module Card::Statuses included do enum :status, %w[ creating 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 diff --git a/app/models/command/ai/parser.rb b/app/models/command/ai/parser.rb index b1dece523..ab973ca02 100644 --- a/app/models/command/ai/parser.rb +++ b/app/models/command/ai/parser.rb @@ -7,6 +7,7 @@ class Command::Ai::Parser def initialize(context) @context = context + self.default_url_options[:script_name] = context.script_name end def parse(query) @@ -71,7 +72,7 @@ class Command::Ai::Parser def context_from_query(query_json) if context_properties = query_json[:context].presence url = cards_path(**context_properties) - Command::Parser::Context.new(user, url: url) + Command::Parser::Context.new(user, url: url, script_name: context.script_name) end end end diff --git a/app/models/command/parser.rb b/app/models/command/parser.rb index a7aace31f..297d307d9 100644 --- a/app/models/command/parser.rb +++ b/app/models/command/parser.rb @@ -1,7 +1,7 @@ class Command::Parser attr_reader :context - delegate :user, :cards, :filter, to: :context + delegate :user, :cards, :filter, :script_name, to: :context def initialize(context) @context = context @@ -12,6 +12,7 @@ class Command::Parser command.user = user command.line ||= as_plain_text(string) command.context ||= context + command.default_url_options[:script_name] = script_name end end diff --git a/app/models/command/parser/context.rb b/app/models/command/parser/context.rb index c5148bd0c..0c4441ba7 100644 --- a/app/models/command/parser/context.rb +++ b/app/models/command/parser/context.rb @@ -1,9 +1,10 @@ class Command::Parser::Context - attr_reader :user, :url + attr_reader :user, :url, :script_name - def initialize(user, url:) + def initialize(user, url:, script_name: "") @user = user @url = url + @script_name = script_name extract_url_components end @@ -81,7 +82,8 @@ class Command::Parser::Context def extract_url_components uri = URI.parse(url || "") - route = Rails.application.routes.recognize_path(uri.path) + path = uri.path.delete_prefix(script_name) + route = Rails.application.routes.recognize_path(path) @controller = route[:controller] @action = route[:action] @params = ActionController::Parameters.new(Rack::Utils.parse_nested_query(uri.query).merge(route.except(:controller, :action))) diff --git a/app/models/command/visit_url.rb b/app/models/command/visit_url.rb index 6066ea0ed..dabd9f52d 100644 --- a/app/models/command/visit_url.rb +++ b/app/models/command/visit_url.rb @@ -6,6 +6,20 @@ class Command::VisitUrl < Command end def execute - redirect_to url + redirect_to real_url end + + private + def real_url + case url + when String + if url.start_with?(context.script_name) + url + else + [ context&.script_name, url ].compact.join + end + else + url + end + end end diff --git a/app/models/step.rb b/app/models/step.rb new file mode 100644 index 000000000..c4e35f33e --- /dev/null +++ b/app/models/step.rb @@ -0,0 +1,9 @@ +class Step < ApplicationRecord + belongs_to :card, touch: true + + scope :completed, -> { where(completed: true) } + + def completed? + completed + end +end diff --git a/app/models/user/signal_user.rb b/app/models/user/signal_user.rb index 924cfe87d..16bee49bc 100644 --- a/app/models/user/signal_user.rb +++ b/app/models/user/signal_user.rb @@ -2,8 +2,6 @@ module User::SignalUser extend ActiveSupport::Concern included do - unless Rails.application.config.x.local_authentication - belongs_to :signal_user, dependent: :destroy, class_name: "SignalId::User", optional: true - end + belongs_to :signal_user, dependent: :destroy, class_name: "SignalId::User", optional: true end end diff --git a/app/views/accounts/_user.html.erb b/app/views/accounts/_user.html.erb index cdf3c9beb..f176236c3 100644 --- a/app/views/accounts/_user.html.erb +++ b/app/views/accounts/_user.html.erb @@ -1,5 +1,5 @@
  • - <%= avatar_tag user %> + <%= avatar_tag user, hidden_for_screen_reader: true %> <%= link_to user.name, user, class: "txt-ink btn btn--plain" %> diff --git a/app/views/accounts/settings/_users.html.erb b/app/views/accounts/settings/_users.html.erb index 550715545..7ffd3c5a5 100644 --- a/app/views/accounts/settings/_users.html.erb +++ b/app/views/accounts/settings/_users.html.erb @@ -1,6 +1,6 @@
    People on the account - <%= render "accounts/invite" %> + <%#= render "accounts/invite" # Scheduled work in https://3.basecamp.com/2914079/buckets/37331921/todos/8746162337 %>
    diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb index f43466a2b..df7d6c917 100644 --- a/app/views/cards/_container.html.erb +++ b/app/views/cards/_container.html.erb @@ -19,6 +19,8 @@ <%= render "cards/stagings/stages", card: card if card.doing? %>
    + <%= render "cards/display/perma/steps", card: card %> +
    <%= render "cards/display/perma/meta", card: card %> <%= render "cards/display/perma/background", card: card %> diff --git a/app/views/cards/comments/_comment.html.erb b/app/views/cards/comments/_comment.html.erb index 8a66b2fcd..daa3eda11 100644 --- a/app/views/cards/comments/_comment.html.erb +++ b/app/views/cards/comments/_comment.html.erb @@ -8,14 +8,16 @@
    - - <%= link_to comment.creator.name, comment.creator, class: "txt-ink btn btn--plain fill-transparent", data: { turbo_frame: "_top" } %> - +

    + + <%= link_to comment.creator.name, comment.creator, class: "txt-ink btn btn--plain fill-transparent", data: { turbo_frame: "_top" } %> + - <%= link_to comment, class: "txt-undecorated txt-ink translucent txt-normal txt-capitalize flex-item-justify-start" do %> - <%= local_datetime_tag comment.created_at, style: :agoorweekday %>, - <%= local_datetime_tag comment.created_at, style: :time %> - <% end %> + <%= link_to comment, class: "txt-undecorated txt-ink translucent txt-normal txt-capitalize" do %> + <%= local_datetime_tag comment.created_at, style: :agoorweekday %>, + <%= local_datetime_tag comment.created_at, style: :time %> + <% end %> +

    <%= link_to edit_card_comment_path(comment.card, comment), class: "comment__edit btn btn--circle borderless fill-transparent translucent" do %> diff --git a/app/views/cards/comments/reactions/_quick_reaction.html.erb b/app/views/cards/comments/reactions/_quick_reaction.html.erb index b93931813..d316e8ef7 100644 --- a/app/views/cards/comments/reactions/_quick_reaction.html.erb +++ b/app/views/cards/comments/reactions/_quick_reaction.html.erb @@ -1,4 +1,4 @@ -
    +
    <%= yield %> diff --git a/app/views/cards/container/_closure.html.erb b/app/views/cards/container/_closure.html.erb index d8df5173f..7d5914a9a 100644 --- a/app/views/cards/container/_closure.html.erb +++ b/app/views/cards/container/_closure.html.erb @@ -7,7 +7,7 @@ <% end %>
    <% else %> -
    +
    Close as <%= icon_tag "caret-down" %> diff --git a/app/views/cards/container/_image.html.erb b/app/views/cards/container/_image.html.erb index c1617792e..773f7f949 100644 --- a/app/views/cards/container/_image.html.erb +++ b/app/views/cards/container/_image.html.erb @@ -5,13 +5,12 @@ <% end %> <% elsif !card.closed? %> <%= form_with model: card, url: collection_card_path(card.collection, card), data: { controller: "form" } do |form| %> -
    - diff --git a/app/views/notifications/_notification.html.erb b/app/views/notifications/_notification.html.erb index 4e5e184b7..b945960b8 100644 --- a/app/views/notifications/_notification.html.erb +++ b/app/views/notifications/_notification.html.erb @@ -10,9 +10,9 @@ <%= avatar_image_tag notification.creator %>
    -
    +

    <%= render "notifications/notification/#{notification.source_type.underscore}/body", notification: notification %> -

    +
    <% end %> <% end %> diff --git a/app/views/notifications/_tray.html.erb b/app/views/notifications/_tray.html.erb index 7cb14390b..671da7f72 100644 --- a/app/views/notifications/_tray.html.erb +++ b/app/views/notifications/_tray.html.erb @@ -16,7 +16,7 @@
    <%= button_to read_all_notifications_path, class: "tray__notification-read-action btn btn--reversed fill-transparent borderless flex-item-justify-start txt-xx-small", - title: "Mark all as read", + title: "Mark all notifications as read", data: { action: "click->dialog#close", turbo_frame: "notifications" } do %> <%= icon_tag "check" %> <% end %> @@ -34,7 +34,7 @@ <% end %>
    - diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb index 3ff58f971..c30714c8b 100644 --- a/app/views/pwa/manifest.json.erb +++ b/app/views/pwa/manifest.json.erb @@ -18,7 +18,7 @@ "purpose": "maskable" } ], - "start_url": "/", + "start_url": "<%= root_url %>", "display": "standalone", "scope": "/", "description": "Card-up the biggest issues in your projects", @@ -27,13 +27,13 @@ { "name": "Notifications", "description": "Catch up on recent notifications", - "url": "/notifications", + "url": "<%= notifications_path %>", "icons": [{ "src": "<%= image_url("bell.svg") %>", "sizes": "any" }] }, { "name": "Latest Activity", "description": "See what’s new", - "url": "/", + "url": "<%= root_url %>", "icons": [{ "src": "<%= image_url("activity.svg") %>", "sizes": "any" }] } ], diff --git a/app/views/sessions/launchpad/show.html.erb b/app/views/sessions/launchpad/show.html.erb index 897e39922..c9acf341d 100644 --- a/app/views/sessions/launchpad/show.html.erb +++ b/app/views/sessions/launchpad/show.html.erb @@ -1 +1,3 @@ -<%= auto_submit_form_with method: :put %> +<%= auto_submit_form_with(method: :put) do |form| %> + <%= form.hidden_field(:sig, value: @sig) %> +<% end %> diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb deleted file mode 100644 index d806a904f..000000000 --- a/app/views/sessions/new.html.erb +++ /dev/null @@ -1,30 +0,0 @@ -<% @page_title = "Sign in" %> - -
    " style="--panel-size: 55ch;"> -

    Fizzy

    - - <%= form_with url: session_path, class: "flex flex-column gap txt-large" do |form| %> -
    - -
    - -
    - -
    - - - <% end %> -
    - -<% content_for :footer do %> -
    Fizzy™ version 0
    -<% end %> diff --git a/app/views/users/new.html.erb b/app/views/users/new.html.erb index 847782db8..2111155d6 100644 --- a/app/views/users/new.html.erb +++ b/app/views/users/new.html.erb @@ -2,7 +2,7 @@ <% content_for :header do %> diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index d7825ad91..50e905996 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -43,7 +43,7 @@
    - <%= button_to session_path, method: :delete, class: "btn center txt-medium" do %> + <%= button_to session_path, method: :delete, class: "btn center txt-medium", data: { turbo: false } do %> <%= icon_tag "logout" %> Sign out <% end %> diff --git a/bin/dev b/bin/dev index e5fc62f6c..106cdeb6d 100755 --- a/bin/dev +++ b/bin/dev @@ -1,7 +1,8 @@ #!/usr/bin/env sh -echo "Access with david@37signals.com / secret123456 on http://37signals.fizzy.localhost:3006" -echo "Access with david@37signals.com / secret123456 on http://honcho.fizzy.localhost:3006" +echo "Login with david@37signals.com / secret123456 to:" +echo " - 37signals: http://fizzy.localhost:3006/735464785" +echo " - Honcho: http://fizzy.localhost:3006/175932900" if [ -f tmp/solid-queue.txt ]; then export SOLID_QUEUE_IN_PUMA=1 diff --git a/config/initializers/mission_control.rb b/config/initializers/mission_control.rb new file mode 100644 index 000000000..c2098204e --- /dev/null +++ b/config/initializers/mission_control.rb @@ -0,0 +1,5 @@ +Rails.application.config.before_initialize do + # We don't want normal tenanted authentication on mission control. + # Note that we're using HTTP basic auth configured via credentials. + MissionControl::Jobs.base_controller_class = "ActionController::Base" +end diff --git a/config/initializers/signal_id.rb b/config/initializers/signal_id.rb index afb6ff60c..b356ef881 100644 --- a/config/initializers/signal_id.rb +++ b/config/initializers/signal_id.rb @@ -1,18 +1,11 @@ require "signal_id" -Rails.application.config.x.local_authentication = ENV["LOCAL_AUTHENTICATION"].present? - ENV["SIGNAL_ID_SECRET"] = Rails.application.credentials.signal_id_secret Rails.application.config.to_prepare do SignalId.product = "fizzy" db_config = SignalId::Database.default_configuration - if Rails.application.config.x.local_authentication - db_config.each do |name, config| - config["connect_timeout"] = 1 - end - end SignalId::Database.load_configuration db_config SignalId::Database.enable_rw_splitting! diff --git a/config/initializers/tenanting.rb b/config/initializers/tenanting.rb deleted file mode 100644 index 85ef97ba7..000000000 --- a/config/initializers/tenanting.rb +++ /dev/null @@ -1,21 +0,0 @@ -Rails.application.config.after_initialize do - # in production and staging, we're using a two-level subdomain like "tenant.fizzy.37signals.com". - # in development and beta it's only a single-level subdomain. - Rails.application.config.active_record_tenanted.tenant_resolver = ->(request) do - tld_length = [ "37signals.com", "37signals-staging.com" ].include?(request.domain) ? 2 : 1 - - if request.path == "/up" - nil - elsif subdomain = request.subdomain(tld_length).presence - subdomain - elsif request.path =~ %r{^/(queenbee|signup)\b} - nil - else - "return-404" # this is a quick fix for now - end - end -end - -ActiveSupport.on_load(:action_controller_base) do - before_action { logger.struct tenant: ApplicationRecord.current_tenant } -end diff --git a/config/initializers/tenanting/account_slug.rb b/config/initializers/tenanting/account_slug.rb new file mode 100644 index 000000000..21043a428 --- /dev/null +++ b/config/initializers/tenanting/account_slug.rb @@ -0,0 +1,35 @@ +module AccountSlug + PATTERN = /(\d{7,})/ + FORMAT = "%07d" + PATH_INFO_MATCH = /\A(\/#{AccountSlug::PATTERN})/ + + # We're using account id prefixes in the URL path. Rather than namespace + # all our routes, we're "mounting" the Rails app at this URL prefix. + def self.extract(request) + # $1, $2, $' == script_name, slug, path_info + if request.script_name && request.script_name =~ PATH_INFO_MATCH + # Likely due to restarting the action cable connection after upgrade + AccountSlug.decode($2) + elsif request.path_info =~ PATH_INFO_MATCH + # Yanks the prefix off PATH_INFO and move it to SCRIPT_NAME + request.engine_script_name = request.script_name = $1 + request.path_info = $'.empty? ? "/" : $' + + # Limit session cookies to the slug path. + # TODO TEST ME + request.env["rack.session.options"][:path] = $1 + + # Return the account id for tenanting. + AccountSlug.decode($2) + end + end + + def self.decode(slug) slug.to_i end + def self.encode(id) FORMAT % id end +end + +Rails.application.config.after_initialize do + Rails.application.config.active_record_tenanted.tenant_resolver = ->(request) do + AccountSlug.extract(request) + end +end diff --git a/config/initializers/tenanting/active_storage.rb b/config/initializers/tenanting/active_storage.rb new file mode 100644 index 000000000..278b92de8 --- /dev/null +++ b/config/initializers/tenanting/active_storage.rb @@ -0,0 +1,19 @@ +module ActiveStorageControllerExtensions + extend ActiveSupport::Concern + + included do + before_action do + # Add script_name so that Disk Service will generate correct URLs for uploads + ActiveStorage::Current.url_options = { + protocol: request.protocol, + host: request.host, + port: request.port, + script_name: request.script_name + } + end + end +end + +Rails.application.config.after_initialize do + ActiveStorage::BaseController.include ActiveStorageControllerExtensions +end diff --git a/config/initializers/tenanting/logging.rb b/config/initializers/tenanting/logging.rb new file mode 100644 index 000000000..d6675942e --- /dev/null +++ b/config/initializers/tenanting/logging.rb @@ -0,0 +1,3 @@ +ActiveSupport.on_load(:action_controller_base) do + before_action { logger.struct tenant: ApplicationRecord.current_tenant } +end diff --git a/config/initializers/tenanting/turbo.rb b/config/initializers/tenanting/turbo.rb new file mode 100644 index 000000000..d740ff8f6 --- /dev/null +++ b/config/initializers/tenanting/turbo.rb @@ -0,0 +1,18 @@ +module TurboStreamsJobExtensions + extend ActiveSupport::Concern + + class_methods do + def render_format(format, **rendering) + if ApplicationRecord.current_tenant + script_name = "/#{ApplicationRecord.current_tenant}" + ApplicationController.renderer.new(script_name: script_name).render(formats: [ format ], **rendering) + else + super + end + end + end +end + +Rails.application.config.after_initialize do + Turbo::StreamsChannel.prepend TurboStreamsJobExtensions +end diff --git a/config/routes.rb b/config/routes.rb index 4e17f0819..99213b019 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,6 +18,7 @@ Rails.application.routes.draw do resource :workflow, only: :update resource :involvement resource :publication + resource :entropy_configuration end resources :cards @@ -44,6 +45,7 @@ Rails.application.routes.draw do resources :assignments resources :taggings + resources :steps resources :comments do resources :reactions, module: :comments @@ -84,11 +86,11 @@ Rails.application.routes.draw do get "join/:join_code", to: "users#new", as: :join post "join/:join_code", to: "users#create" - resource :session do + resource :session, only: :destroy do scope module: "sessions" do resources :transfers, only: %i[ show update ] + resource :launchpad, only: %i[ show update ], controller: "launchpad" end - resource :launchpad, only: %i[ show update ], controller: "sessions/launchpad" end namespace :signup do diff --git a/db/migrate/20250625120000_add_steps_to_cards.rb b/db/migrate/20250625120000_add_steps_to_cards.rb new file mode 100644 index 000000000..938df396e --- /dev/null +++ b/db/migrate/20250625120000_add_steps_to_cards.rb @@ -0,0 +1,13 @@ +class AddStepsToCards < ActiveRecord::Migration[8.1] + def change + create_table :steps do |t| + t.references :card, null: false, foreign_key: true + t.text :content, null: false + t.boolean :completed, default: false, null: false + + t.timestamps + end + + add_index :steps, :completed + end +end diff --git a/db/migrate/20250702154620_remove_password_from_users.rb b/db/migrate/20250702154620_remove_password_from_users.rb new file mode 100644 index 000000000..75173ad1a --- /dev/null +++ b/db/migrate/20250702154620_remove_password_from_users.rb @@ -0,0 +1,5 @@ +class RemovePasswordFromUsers < ActiveRecord::Migration[8.1] + def change + remove_column :users, :password, :string + end +end diff --git a/db/migrate/20250702160230_add_not_null_to_accounts_queenbee_id.rb b/db/migrate/20250702160230_add_not_null_to_accounts_queenbee_id.rb new file mode 100644 index 000000000..9698302bf --- /dev/null +++ b/db/migrate/20250702160230_add_not_null_to_accounts_queenbee_id.rb @@ -0,0 +1,5 @@ +class AddNotNullToAccountsQueenbeeId < ActiveRecord::Migration[8.1] + def change + change_column_null :accounts, :queenbee_id, false + end +end diff --git a/db/migrate/20250702211937_change_steps_index.rb b/db/migrate/20250702211937_change_steps_index.rb new file mode 100644 index 000000000..2af0f50d4 --- /dev/null +++ b/db/migrate/20250702211937_change_steps_index.rb @@ -0,0 +1,6 @@ +class ChangeStepsIndex < ActiveRecord::Migration[8.1] + def change + remove_index :steps, :completed + add_index :steps, %i[ card_id completed ] + end +end diff --git a/db/schema.rb b/db/schema.rb index 01911c54c..0edc58ac5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2025_06_26_000000) do +ActiveRecord::Schema[8.1].define(version: 2025_07_02_211937) do create_table "accesses", force: :cascade do |t| t.datetime "accessed_at" t.integer "collection_id", null: false @@ -28,7 +28,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_26_000000) do t.datetime "created_at", null: false t.string "join_code" t.string "name", null: false - t.integer "queenbee_id" + t.integer "queenbee_id", null: false t.datetime "updated_at", null: false t.index ["queenbee_id"], name: "index_accounts_on_queenbee_id", unique: true end @@ -133,13 +133,6 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_26_000000) do t.index ["stage_id"], name: "index_cards_on_stage_id" end - create_table "closers_filters", id: false, force: :cascade do |t| - t.integer "closer_id", null: false - t.integer "filter_id", null: false - t.index ["closer_id"], name: "index_closers_filters_on_closer_id" - t.index ["filter_id"], name: "index_closers_filters_on_filter_id" - end - create_table "closure_reasons", force: :cascade do |t| t.datetime "created_at", null: false t.string "label" @@ -332,6 +325,16 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_26_000000) do t.index ["user_id"], name: "index_sessions_on_user_id" end + create_table "steps", force: :cascade do |t| + t.integer "card_id", null: false + t.boolean "completed", default: false, null: false + t.text "content", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["card_id", "completed"], name: "index_steps_on_card_id_and_completed" + t.index ["card_id"], name: "index_steps_on_card_id" + end + create_table "taggings", force: :cascade do |t| t.integer "card_id", null: false t.datetime "created_at", null: false @@ -407,6 +410,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_26_000000) do add_foreign_key "pins", "users" add_foreign_key "search_queries", "users" add_foreign_key "sessions", "users" + add_foreign_key "steps", "cards" add_foreign_key "taggings", "cards" add_foreign_key "taggings", "tags" add_foreign_key "watches", "cards" diff --git a/db/schema_cache.yml b/db/schema_cache.yml index c68ffad4a..b53c5f750 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -128,7 +128,7 @@ columns: name: queenbee_id cast_type: *3 sql_type_metadata: *4 - 'null': true + 'null': false default: default_function: collation: @@ -451,7 +451,7 @@ columns: default_function: collation: comment: - - &31 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &33 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: title cast_type: *7 @@ -462,18 +462,6 @@ columns: collation: comment: - *9 - closers_filters: - - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column - auto_increment: - name: closer_id - cast_type: *3 - sql_type_metadata: *4 - 'null': false - default: - default_function: - collation: - comment: - - *19 closure_reasons: - *5 - *6 @@ -532,11 +520,11 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: all_access - cast_type: &32 !ruby/object:ActiveModel::Type::Boolean + cast_type: &30 !ruby/object:ActiveModel::Type::Boolean precision: scale: limit: - sql_type_metadata: &33 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &31 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: boolean type: :boolean limit: @@ -756,7 +744,7 @@ columns: comment: filters_tags: - *19 - - &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &32 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: tag_id cast_type: *3 @@ -952,23 +940,48 @@ columns: collation: comment: - *25 + steps: + - *22 + - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + auto_increment: + name: completed + cast_type: *30 + sql_type_metadata: *31 + 'null': false + default: false + default_function: + collation: + comment: + - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + auto_increment: + name: content + cast_type: *15 + sql_type_metadata: *16 + 'null': false + default: + default_function: + collation: + comment: + - *5 + - *6 + - *9 taggings: - *22 - *5 - *6 - - *30 + - *32 - *9 tags: - *5 - *6 - - *31 + - *33 - *9 users: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: active - cast_type: *32 - sql_type_metadata: *33 + cast_type: *30 + sql_type_metadata: *31 'null': false default: true default_function: @@ -1027,8 +1040,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: watching - cast_type: *32 - sql_type_metadata: *33 + cast_type: *30 + sql_type_metadata: *31 'null': false default: true default_function: @@ -1079,7 +1092,6 @@ primary_keys: card_engagements: id card_goldnesses: id cards: id - closers_filters: closure_reasons: id closures: id collection_publications: id @@ -1101,6 +1113,7 @@ primary_keys: search_queries: id search_results: id sessions: id + steps: id taggings: id tags: id users: id @@ -1122,7 +1135,6 @@ data_sources: card_engagements: true card_goldnesses: true cards: true - closers_filters: true closure_reasons: true closures: true collection_publications: true @@ -1144,6 +1156,7 @@ data_sources: search_queries: true search_results: true sessions: true + steps: true taggings: true tags: true users: true @@ -1542,39 +1555,6 @@ indexes: nulls_not_distinct: comment: valid: true - closers_filters: - - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition - table: closers_filters - name: index_closers_filters_on_closer_id - unique: false - columns: - - closer_id - lengths: {} - orders: {} - opclasses: {} - where: - type: - using: - include: - nulls_not_distinct: - comment: - valid: true - - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition - table: closers_filters - name: index_closers_filters_on_filter_id - unique: false - columns: - - filter_id - lengths: {} - orders: {} - opclasses: {} - where: - type: - using: - include: - nulls_not_distinct: - comment: - valid: true closure_reasons: [] closures: - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition @@ -2303,6 +2283,40 @@ indexes: nulls_not_distinct: comment: valid: true + steps: + - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition + table: steps + name: index_steps_on_card_id + unique: false + columns: + - card_id + lengths: {} + orders: {} + opclasses: {} + where: + type: + using: + include: + nulls_not_distinct: + comment: + valid: true + - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition + table: steps + name: index_steps_on_card_id_and_completed + unique: false + columns: + - card_id + - completed + lengths: {} + orders: {} + opclasses: {} + where: + type: + using: + include: + nulls_not_distinct: + comment: + valid: true taggings: - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition table: taggings @@ -2438,4 +2452,4 @@ indexes: comment: valid: true workflows: [] -version: 20250626000000 +version: 20250702211937 diff --git a/db/seeds.rb b/db/seeds.rb index a04ee160e..f299e949f 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,5 +1,8 @@ raise "Seeding is just for development" unless Rails.env.development? +require "active_support/testing/time_helpers" +include ActiveSupport::Testing::TimeHelpers + # Seed DSL def seed_account(name) print " #{name}…" @@ -9,7 +12,7 @@ end def create_tenant(signal_account_name) signal_account = SignalId::Account.find_by_product_and_name!("fizzy", signal_account_name) - tenant_name = signal_account.subdomain + tenant_name = signal_account.queenbee_id ApplicationRecord.destroy_tenant tenant_name ApplicationRecord.create_tenant(tenant_name) do diff --git a/db/seeds/honcho.rb b/db/seeds/honcho.rb index 0276a2a8a..70cbf958d 100644 --- a/db/seeds/honcho.rb +++ b/db/seeds/honcho.rb @@ -50,19 +50,29 @@ collections = [ "Documentation" ] +time_range = (60 .. 30.days.in_minutes) + collections.each_with_index do |collection_name, index| create_collection(collection_name, access_to: authors.sample(3)).tap do |collection| # Create 20 unique cards for each collection card_titles.each do |title| - create_card(title, - description: "#{title} for #{collection_name} phase #{index + 1}.", - collection: collection - ).tap do |card| + travel(-rand(time_range).minutes) do + card = create_card title, + description: "#{title} for #{collection_name} phase #{index + 1}.", + collection: collection, + creator: authors.sample + # Randomly assign to 1-2 authors + travel rand(0..20).minutes card.toggle_assignment(authors.sample) - card.toggle_assignment(authors.sample) if rand > 0.5 + + if rand > 0.5 + travel rand(0..20).minutes + card.toggle_assignment(authors.sample) + end # Randomly set card state + travel rand(0..20).minutes case rand(3) when 0 card.engage diff --git a/script/create-accounts-in-staging.rb b/script/create-accounts-in-staging.rb new file mode 100755 index 000000000..4ba31e6f1 --- /dev/null +++ b/script/create-accounts-in-staging.rb @@ -0,0 +1,21 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" + +ApplicationRecord.with_each_tenant do |tenant| + account = Account.first + signal_account = account.signal_account + + signal_users = SignalId::User.where(account_id: signal_account.id) + + signal_users.each do |signal_user| + unless User.find_by(signal_user_id: signal_user.id) + User.create!( + name: signal_user.identity.name, + email_address: signal_user.identity.email_address, + signal_user_id: signal_user.id, + password: SecureRandom.hex(36) # TODO: remove password column? + ) + end + end +end diff --git a/script/migrate-content-to-slugged-urls.rb b/script/migrate-content-to-slugged-urls.rb new file mode 100755 index 000000000..5022858ec --- /dev/null +++ b/script/migrate-content-to-slugged-urls.rb @@ -0,0 +1,70 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" + +domains = { + "production" => "fizzy.37signals.com", + "beta" => "fizzy-beta.37signals.com", + "staging" => "fizzy.37signals-staging.com" +} + +def fix_attachments(rich_text) + if rich_text.body + rich_text.body.send(:attachment_nodes).each do |node| + sgid = SignedGlobalID.parse(node["sgid"], for: ActionText::Attachable::LOCATOR_NAME) + if sgid + puts "Fixing attachment node: #{node.to_html}" + model = sgid.model_class.find(sgid.model_id) + node["sgid"] = model.attachable_sgid + else + puts "Skipping attachment node without valid sgid: #{node.to_html}" + end + end + rich_text.save! + end +end + +ApplicationRecord.with_each_tenant do |tenant| + account_id = Account.first.queenbee_id + + unless account_id + puts "Skipping URL fixup for tenant: #{tenant}" + next + end + + puts "\n## Processing tenant: #{tenant}\n" + + domain = domains[Rails.env] || domains["production"] + regex = %r{://\w+\.#{domain}/} + + pp [ Account.first.name, account_id, domain, regex ] + puts + + Card.find_each do |card| + puts "### Processing card #{card.id} in #{Rails.application.routes.url_helpers.collection_card_path(card.collection, card)}" + fix_attachments(card.description) + card.reload + + old_body = card.description.body.to_s + if match = regex.match(old_body) + puts "URL found in card #{card.id} in #{Rails.application.routes.url_helpers.collection_card_path(card.collection, card)}" + new_body = old_body.gsub(regex, "://#{domain}/#{account_id}/") + + card.description.update(body: new_body) || raise("Failed to update card description for card #{card.id}") + end + end + + Comment.find_each do |comment| + puts "### Processing comment #{comment.id} in #{Rails.application.routes.url_helpers.collection_card_path(comment.card.collection, comment.card)}" + fix_attachments(comment.body) + comment.reload + + old_body = comment.body.body.to_s + if match = regex.match(old_body) + puts "URL found in comment #{comment.id} in #{Rails.application.routes.url_helpers.collection_card_path(comment.card.collection, comment.card)}" + new_body = old_body.gsub(regex, "://#{domain}/#{account_id}/") + + comment.body.update(body: new_body) || raise("Failed to update comment body for comment #{comment.id}") + end + end +end diff --git a/script/rename-tenants-to-qb-account-id.rb b/script/rename-tenants-to-qb-account-id.rb new file mode 100755 index 000000000..83401107e --- /dev/null +++ b/script/rename-tenants-to-qb-account-id.rb @@ -0,0 +1,33 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" + +tenant_names = [] + +ApplicationRecord.with_each_tenant do |tenant| + next if tenant == "#{Rails.env}-tenant" + + account = Account.first + queenbee_id = account.queenbee_id + tenant_names << { from: tenant, to: queenbee_id } + + ApplicationRecord.remove_connection +end + +pp [ "Tenant name changes:", tenant_names ] + +root_config = ApplicationRecord.tenanted_root_config +tenant_names.each do |name| + from_db_path = root_config.database_path_for(name[:from]) + to_db_path = root_config.database_path_for(name[:to]) + + from_path = from_db_path.split("/").take(4).join("/") + to_path = to_db_path.split("/").take(4).join("/") + + unless from_path == to_path + FileUtils.move from_path, to_path, verbose: true + end +end + +puts +pp [ "Tenants after renaming:", ApplicationRecord.tenants ] diff --git a/test/controllers/cards/steps_controller_test.rb b/test/controllers/cards/steps_controller_test.rb new file mode 100644 index 000000000..677279a2a --- /dev/null +++ b/test/controllers/cards/steps_controller_test.rb @@ -0,0 +1,55 @@ +require "test_helper" + +class Cards::StepsControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in_as :kevin + end + + test "create" do + card = cards(:logo) + + assert_difference -> { card.steps.count }, +1 do + post card_steps_path(card), params: { step: { content: "Research alternatives" } }, as: :turbo_stream + assert_card_container_rerendered(card) + end + + assert_equal "Research alternatives", card.steps.last.content + end + + test "update" do + card = cards(:logo) + step = card.steps.create!(content: "Original content") + + assert_changes -> { step.reload.content }, from: "Original content", to: "Updated content" do + put card_step_path(card, step), params: { step: { content: "Updated content" } }, as: :turbo_stream + assert_card_container_rerendered(card) + end + end + + test "destroy" do + card = cards(:logo) + step = card.steps.create!(content: "Step to delete") + + assert_difference -> { card.steps.count }, -1 do + delete card_step_path(card, step), as: :turbo_stream + assert_card_container_rerendered(card) + end + end + + test "toggle completion" do + card = cards(:logo) + step = card.steps.create!(content: "Test step", completed: false) + + # Toggle to completed + assert_changes -> { step.reload.completed? }, from: false, to: true do + put card_step_path(card, step), params: { step: { completed: "1" } }, as: :turbo_stream + assert_card_container_rerendered(card) + end + + # Toggle back to incomplete + assert_changes -> { step.reload.completed? }, from: true, to: false do + put card_step_path(card, step), params: { step: { completed: "0" } }, as: :turbo_stream + assert_card_container_rerendered(card) + end + end +end diff --git a/test/controllers/collections/entropy_configurations_controller_test.rb b/test/controllers/collections/entropy_configurations_controller_test.rb new file mode 100644 index 000000000..b96c3b198 --- /dev/null +++ b/test/controllers/collections/entropy_configurations_controller_test.rb @@ -0,0 +1,17 @@ +require "test_helper" + +class Collections::EntropyConfigurationsControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in_as :kevin + @collection = collections(:writebook) + end + + test "update" do + put collection_entropy_configuration_path(@collection), params: { collection: { auto_close_period: 1.day, auto_reconsider_period: 2.days } } + + assert_equal 1.day, @collection.entropy_configuration.reload.auto_close_period + assert_equal 2.days, @collection.entropy_configuration.reload.auto_reconsider_period + + assert_turbo_stream action: :replace, target: dom_id(@collection, :entropy_configuration) + end +end diff --git a/test/controllers/collections/publications_controller_test.rb b/test/controllers/collections/publications_controller_test.rb index 2bd9ec1dc..da24c22da 100644 --- a/test/controllers/collections/publications_controller_test.rb +++ b/test/controllers/collections/publications_controller_test.rb @@ -13,7 +13,7 @@ class Collections::PublicationsControllerTest < ActionDispatch::IntegrationTest post collection_publication_path(@collection) end - assert_redirected_to edit_collection_path(@collection) + assert_turbo_stream action: :replace, target: dom_id(@collection, :publication) end test "unpublish a collection" do @@ -24,6 +24,6 @@ class Collections::PublicationsControllerTest < ActionDispatch::IntegrationTest delete collection_publication_path(@collection) end - assert_redirected_to edit_collection_path(@collection) + assert_turbo_stream action: :replace, target: dom_id(@collection, :publication) end end diff --git a/test/controllers/collections/workflows_controller_test.rb b/test/controllers/collections/workflows_controller_test.rb index 8fb0e8dce..e4e739720 100644 --- a/test/controllers/collections/workflows_controller_test.rb +++ b/test/controllers/collections/workflows_controller_test.rb @@ -10,7 +10,7 @@ class Collections::WorkflowsControllerTest < ActionDispatch::IntegrationTest patch collection_workflow_path(collection), params: { collection: { workflow_id: workflows(:on_call).id } } - assert_redirected_to cards_path(collection_ids: [ collection.id ]) + assert_turbo_stream action: :replace, target: dom_id(collection, :workflows) assert_equal workflows(:on_call), collection.reload.workflow end end diff --git a/test/controllers/controller_authentication_test.rb b/test/controllers/controller_authentication_test.rb new file mode 100644 index 000000000..5ed34292f --- /dev/null +++ b/test/controllers/controller_authentication_test.rb @@ -0,0 +1,25 @@ +require "test_helper" + +class ControllerAuthenticationTest < ActionDispatch::IntegrationTest + test "access without an account slug redirects to launchpad" do + integration_session.default_url_options[:script_name] = "" # no tenant + + get cards_path + + assert_redirected_to Launchpad.login_url(product: true) + end + + test "access with an account slug but no session redirects to launchpad" do + get cards_path + + assert_redirected_to Launchpad.login_url(product: true, account: Account.sole) + end + + test "access with an account slug and a session allows functional access" do + sign_in_as :kevin + + get cards_path + + assert_response :success + end +end diff --git a/test/controllers/events_controller_test.rb b/test/controllers/events_controller_test.rb index d89d953f4..c89c06540 100644 --- a/test/controllers/events_controller_test.rb +++ b/test/controllers/events_controller_test.rb @@ -11,7 +11,7 @@ class EventsControllerTest < ActionDispatch::IntegrationTest test "index" do get events_path - assert_select "ul.events__time-block[style='grid-area: 17/2']" do + assert_select "div.events__time-block[style='grid-area: 17/2']" do assert_select "strong", text: "David assigned JZ to Layout is broken" end end @@ -21,7 +21,7 @@ class EventsControllerTest < ActionDispatch::IntegrationTest get events_path - assert_select "ul.events__time-block[style='grid-area: 22/2']" do + assert_select "div.events__time-block[style='grid-area: 22/2']" do assert_select "strong", text: "David assigned JZ to Layout is broken" end end diff --git a/test/controllers/sessions/launchpad_controller_test.rb b/test/controllers/sessions/launchpad_controller_test.rb index 2ab50b2a0..c67ac8ee2 100644 --- a/test/controllers/sessions/launchpad_controller_test.rb +++ b/test/controllers/sessions/launchpad_controller_test.rb @@ -6,14 +6,16 @@ class Sessions::LaunchpadControllerTest < ActionDispatch::IntegrationTest assert_response :success - assert_equal cookies[:_fizzy_launchpad_sig], "test-sig" + assert_select "form input#sig" do |node| + assert_equal node.length, 1 + assert_equal node.first["value"], "test-sig" + end end test "create establishes a session when the sig is valid" do user = users(:david) - cookies[:_fizzy_launchpad_sig] = user.signal_user.perishable_signature - put session_launchpad_path + put session_launchpad_path(params: { sig: user.signal_user.perishable_signature }) assert_redirected_to root_url assert parsed_cookies.signed[:session_token] @@ -21,9 +23,8 @@ class Sessions::LaunchpadControllerTest < ActionDispatch::IntegrationTest test "returns 401 when the sig is invalid" do user = users(:david) - cookies[:sig] = "not-valid" - put session_launchpad_path + put session_launchpad_path(params: { sig: "invalid" }) assert_response :unauthorized end diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb index 1cf460c6c..35a6b0385 100644 --- a/test/controllers/sessions_controller_test.rb +++ b/test/controllers/sessions_controller_test.rb @@ -1,38 +1,10 @@ require "test_helper" class SessionsControllerTest < ActionDispatch::IntegrationTest - ALLOWED_BROWSER = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15" - DISALLOWED_BROWSER = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0" - - test "new" do - get new_session_path - assert_response :success - end - - test "new enforces browser compatibility" do - get new_session_path, env: { "HTTP_USER_AGENT" => DISALLOWED_BROWSER } - assert_select "svg", message: /Your browser is not supported/ - - get new_session_path, env: { "HTTP_USER_AGENT" => ALLOWED_BROWSER } - assert_select "svg", text: /Your browser is not supported/, count: 0 - end - - test "create with valid credentials" do - post session_path, params: { email_address: "david@37signals.com", password: "secret123456" } - assert_redirected_to root_path - assert cookies[:session_token].present? - end - - test "create with invalid credentials" do - post session_path, params: { email_address: "david@37signals.com", password: "wrong" } - assert_redirected_to new_session_path - assert_not cookies[:session_token].present? - end - test "destroy" do sign_in_as :kevin delete session_path - assert_redirected_to new_session_path + assert_redirected_to Launchpad.login_url assert_not cookies[:session_token].present? end end diff --git a/test/controllers/signup/accounts_controller_test.rb b/test/controllers/signup/accounts_controller_test.rb index 8c14e4326..78a1c1af5 100644 --- a/test/controllers/signup/accounts_controller_test.rb +++ b/test/controllers/signup/accounts_controller_test.rb @@ -1,14 +1,14 @@ require "test_helper" class Signup::AccountsControllerTest < ActionDispatch::IntegrationTest - test "new under a tenanted domain redirects to the root" do + test "new under a tenanted URL redirects to the root" do get new_signup_account_url assert_redirected_to root_url end - test "new under an untenanted domain is OK" do - integration_session.host = "example.com" # no subdomain + test "new under an untenanted URL is OK" do + integration_session.default_url_options[:script_name] = "" # no tenant get new_signup_account_url, headers: auth_headers @@ -16,7 +16,7 @@ class Signup::AccountsControllerTest < ActionDispatch::IntegrationTest end test "create with invalid params" do - integration_session.host = "example.com" # no subdomain + integration_session.default_url_options[:script_name] = "" # no tenant post signup_accounts_url, headers: auth_headers, @@ -27,7 +27,7 @@ class Signup::AccountsControllerTest < ActionDispatch::IntegrationTest end test "create for a new " do - integration_session.host = "example.com" # no subdomain + integration_session.default_url_options[:script_name] = "" # no tenant assert_difference -> { SignalId::Identity.count }, +1 do assert_difference -> { SignalId::Account.count }, +1 do @@ -48,7 +48,7 @@ class Signup::AccountsControllerTest < ActionDispatch::IntegrationTest end test "create for an existing identity" do - integration_session.host = "example.com" # no subdomain + integration_session.default_url_options[:script_name] = "" # no tenant identity = signal_identities(:david) @@ -72,7 +72,7 @@ class Signup::AccountsControllerTest < ActionDispatch::IntegrationTest end test "actions require HTTP basic authentication while we're in internal-only mode" do - integration_session.host = "example.com" # no subdomain + integration_session.default_url_options[:script_name] = "" # no tenant get new_signup_account_url diff --git a/test/models/card/statuses_test.rb b/test/models/card/statuses_test.rb index 4f0c54312..5a3e2cb8b 100644 --- a/test/models/card/statuses_test.rb +++ b/test/models/card/statuses_test.rb @@ -25,6 +25,33 @@ class Card::StatusesTest < ActiveSupport::TestCase assert_includes Card.published_or_drafted_by(users(:jz)), card end + test "an event is created when a card is created in the published state" do + Current.session = sessions(:david) + + assert_no_difference(-> { Event.count }) do + collections(:writebook).cards.create! creator: users(:kevin), title: "Draft Card" + end + + assert_difference(-> { Event.count } => +1) do + @card = collections(:writebook).cards.create! creator: users(:kevin), title: "Published Card", status: :published + end + + assert_equal @card, Event.last.eventable + assert_equal "card_published", Event.last.action + end + + test "an event is created when a card is published" do + Current.session = sessions(:david) + + card = collections(:writebook).cards.create! creator: users(:kevin), title: "Published Card" + assert_difference(-> { Event.count } => +1) do + card.publish + end + + assert_equal card, Event.last.eventable + assert_equal "card_published", Event.last.action + end + test "can_recover_abandoned_creation?" do card = collections(:writebook).cards.create! creator: users(:kevin) unsaved_card = collections(:writebook).cards.new creator: users(:kevin) diff --git a/test/models/command/ai/translator_test.rb b/test/models/command/ai/translator_test.rb index e81edc27c..e72aeefd4 100644 --- a/test/models/command/ai/translator_test.rb +++ b/test/models/command/ai/translator_test.rb @@ -123,10 +123,10 @@ class Command::Ai::TranslatorTest < ActionDispatch::IntegrationTest end test "visit screens" do - assert_command({ commands: [ "/visit #{user_path(@user)}" ] }, "my profile") - assert_command({ commands: [ "/visit #{edit_user_path(@user)}" ] }, "edit my profile") - assert_command({ commands: [ "/visit #{account_settings_path}" ] }, "manage users") - assert_command({ commands: [ "/visit #{account_settings_path}" ] }, "account settings") + assert_command({ commands: [ "/visit #{user_path(@user, script_name: nil)}" ] }, "my profile") + assert_command({ commands: [ "/visit #{edit_user_path(@user, script_name: nil)}" ] }, "edit my profile") + assert_command({ commands: [ "/visit #{account_settings_path(script_name: nil)}" ] }, "manage users") + assert_command({ commands: [ "/visit #{account_settings_path(script_name: nil)}" ] }, "account settings") end test "create cards" do @@ -165,7 +165,7 @@ class Command::Ai::TranslatorTest < ActionDispatch::IntegrationTest def translate(query, user: @user, context: :list) raise "Context must be :card or _list" unless context.in?(%i[ card list ]) url = context == :card ? card_url(cards(:logo)) : cards_url - context = Command::Parser::Context.new(user, url: url) + context = Command::Parser::Context.new(user, url: url, script_name: integration_session.default_url_options[:script_name]) translator = Command::Ai::Translator.new(context) translator.translate(query) end diff --git a/test/models/command/visit_url_test.rb b/test/models/command/visit_url_test.rb new file mode 100644 index 000000000..12d89035a --- /dev/null +++ b/test/models/command/visit_url_test.rb @@ -0,0 +1,36 @@ +require "test_helper" + +class Command::VisitUrlTest < ActionDispatch::IntegrationTest + setup do + @user = users(:david) + @card = cards(:logo) + @script_name = integration_session.default_url_options[:script_name] + end + + test "visit a path without the account slug" do + result = visit_command("/foo/1234/bar").execute + + assert_kind_of Command::Result::Redirection, result + assert_equal "#{@script_name}/foo/1234/bar", result.url + end + + test "visit a path with the account slug" do + result = visit_command("#{@script_name}/foo/1234/bar").execute + + assert_kind_of Command::Result::Redirection, result + assert_equal "#{@script_name}/foo/1234/bar", result.url + end + + test "visit an object path" do + result = visit_command(@card).execute + + assert_kind_of Command::Result::Redirection, result + assert_equal @card, result.url + end + + private + def visit_command(url) + context = Command::Parser::Context.new(@user, url: collection_card_url(@card.collection, @card), script_name: integration_session.default_url_options[:script_name]) + Command::VisitUrl.new(url: url, context: context) + end +end diff --git a/test/test_helpers/command_test_helper.rb b/test/test_helpers/command_test_helper.rb index a0d760e8b..7492bc4cd 100644 --- a/test/test_helpers/command_test_helper.rb +++ b/test/test_helpers/command_test_helper.rb @@ -4,7 +4,7 @@ module CommandTestHelper end def parse_command(string, context_url: nil, user: users(:david)) - context = Command::Parser::Context.new(user, url: context_url) + context = Command::Parser::Context.new(user, url: context_url, script_name: integration_session.default_url_options[:script_name]) parser = Command::Parser.new(context) parser.parse(string).tap do |command| command.user = user if command diff --git a/test/test_helpers/session_test_helper.rb b/test/test_helpers/session_test_helper.rb index 5e4f7eaf0..f4bb6aa45 100644 --- a/test/test_helpers/session_test_helper.rb +++ b/test/test_helpers/session_test_helper.rb @@ -6,7 +6,7 @@ module SessionTestHelper def sign_in_as(user) cookies[:session_token] = nil user = users(user) unless user.is_a? User - post session_path, params: { email_address: user.email_address, password: "secret123456" } + put session_launchpad_path, params: { sig: user.signal_user.perishable_signature } assert cookies[:session_token].present? end