Merge branch 'main' into fizzy-do-prompts
* main: (57 commits) Untenanted access in authenticated controllers should request auth dep: bundle update dep: update Rails dep: bump AR::Tenanted Clean up the cable meta tag Fix activity feed pagination to fetch on demand Create a "published" event if a card is created published Scatter Honcho seeds over a 30 day period Fix the PWA manifest to use slugged URLs Collection publication edits render a turbo frame Collection workflow edits render a turbo frame Collection entropy config edits render a turbo frame Use automatic sizing where supported Introduce a separate controller for collection entropy config Add `card_id` to index Move to scope and inline method Prefer modern syntax Fix that stage should be visible above card background Indicate opens menu Hide dialog targets from screen readers when closed ... # Conflicts: # db/schema.rb # db/schema_cache.yml
This commit is contained in:
@@ -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"
|
||||
|
||||
+30
-29
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -37,8 +37,14 @@
|
||||
}
|
||||
|
||||
.comment__author {
|
||||
.comment--system & strong {
|
||||
display: none;
|
||||
.comment--system & {
|
||||
h3 {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
|
||||
@@ -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 "); }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,6 +315,7 @@
|
||||
.card__meta .btn,
|
||||
.card__meta-item:not(.card__meta-item--updated),
|
||||
.card__stages,
|
||||
.card__steps,
|
||||
.card__closed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ class SQLiteBackupsJob < ApplicationJob
|
||||
end
|
||||
|
||||
def db_path(tenant)
|
||||
db_config.database
|
||||
db_config.database_path
|
||||
end
|
||||
|
||||
def db_config
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module Card::Multistep
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many :steps, dependent: :destroy
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
class Step < ApplicationRecord
|
||||
belongs_to :card, touch: true
|
||||
|
||||
scope :completed, -> { where(completed: true) }
|
||||
|
||||
def completed?
|
||||
completed
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<li class="flex align-center gap-half pad-inline" data-filter-target="item">
|
||||
<%= avatar_tag user %>
|
||||
<%= avatar_tag user, hidden_for_screen_reader: true %>
|
||||
|
||||
<strong class="overflow-ellipsis">
|
||||
<%= link_to user.name, user, class: "txt-ink btn btn--plain" %>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="panel shadow center flex flex-column">
|
||||
<strong class="divider txt-large margin-block-end">People on the account</strong>
|
||||
<%= render "accounts/invite" %>
|
||||
<%#= render "accounts/invite" # Scheduled work in https://3.basecamp.com/2914079/buckets/37331921/todos/8746162337 %>
|
||||
|
||||
<div class="flex flex-column" data-controller="filter">
|
||||
<div class="pad">
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
<%= render "cards/stagings/stages", card: card if card.doing? %>
|
||||
</div>
|
||||
|
||||
<%= render "cards/display/perma/steps", card: card %>
|
||||
|
||||
<footer class="card__footer full-width flex align-start gap">
|
||||
<%= render "cards/display/perma/meta", card: card %>
|
||||
<%= render "cards/display/perma/background", card: card %>
|
||||
|
||||
@@ -8,14 +8,16 @@
|
||||
|
||||
<div class="comment__content flex flex-column flex-item-grow full-width">
|
||||
<div class="comment__author flex align-center gap-half">
|
||||
<strong>
|
||||
<%= link_to comment.creator.name, comment.creator, class: "txt-ink btn btn--plain fill-transparent", data: { turbo_frame: "_top" } %>
|
||||
</strong>
|
||||
<h3 class="font-weight-normal txt-normal flex-item-justify-start">
|
||||
<strong>
|
||||
<%= link_to comment.creator.name, comment.creator, class: "txt-ink btn btn--plain fill-transparent", data: { turbo_frame: "_top" } %>
|
||||
</strong>
|
||||
|
||||
<%= 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 %>
|
||||
</h3>
|
||||
|
||||
<%= link_to edit_card_comment_path(comment.card, comment),
|
||||
class: "comment__edit btn btn--circle borderless fill-transparent translucent" do %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="quick-reaction" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
|
||||
<div class="quick-reaction" data-controller="dialog" data-action="keydown.esc->dialog#close:stop click@document->dialog#closeOnClickOutside">
|
||||
<%= yield %>
|
||||
|
||||
<dialog class="quick-reaction__popup popup panel fill-white shadow" data-dialog-target="dialog">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<details class="expander" data-controller="details" data-details-target="details" data-action="keydown.esc->details#close click@document->details#closeOnClickOutside">
|
||||
<details class="expander" data-controller="details" data-details-target="details" data-action="keydown.esc->details#close:stop click@document->details#closeOnClickOutside">
|
||||
<summary class="expander__button btn borderless">
|
||||
Close as
|
||||
<%= icon_tag "caret-down" %>
|
||||
|
||||
@@ -5,13 +5,12 @@
|
||||
<% end %>
|
||||
<% elsif !card.closed? %>
|
||||
<%= form_with model: card, url: collection_card_path(card.collection, card), data: { controller: "form" } do |form| %>
|
||||
<label class="btn">
|
||||
<label class="btn input--file">
|
||||
<%= icon_tag "picture-add" %>
|
||||
<span class="for-screen-reader">Add background image</span>
|
||||
<%= form.file_field :image, class: "input",
|
||||
accept: "image/png, image/jpeg, image/jpg, image/webp",
|
||||
data: { action: "upload-preview#previewImage form#submit", upload_preview_target: "input" },
|
||||
hidden: true %>
|
||||
data: { action: "upload-preview#previewImage form#submit", upload_preview_target: "input" } %>
|
||||
</label>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<header class="card__header">
|
||||
<%= render "cards/display/preview/collection", card: card %>
|
||||
<%= render "cards/display/preview/tags", card: card %>
|
||||
<%= render "cards/display/preview/steps", card: card %>
|
||||
|
||||
<% if card.staged? && card.doing? %>
|
||||
<span class="btn justify-start workflow-stage workflow-stage--current txt-uppercase min-width max-width">
|
||||
@@ -16,9 +17,9 @@
|
||||
|
||||
<div class="card__body justify-space-between">
|
||||
<div class="card__content">
|
||||
<h1 class="card__title overflow-line-clamp">
|
||||
<h3 class="card__title overflow-line-clamp">
|
||||
<%= card.title %>
|
||||
</h1>
|
||||
</h3>
|
||||
|
||||
<%= link_to collection_card_path(card.collection, card), draggable: false, class: "card__link", title: card_title_tag(card), data: { action: "dialog#close", turbo_frame: "_top" } do %>
|
||||
<span class="for-screen-reader"><%= card.title %></span>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<ol class="steps txt-small margin-block">
|
||||
<%= render partial: "cards/steps/step", collection: card.steps, as: :step %>
|
||||
|
||||
<div class="step">
|
||||
<input type="checkbox" class="step__checkbox" disabled>
|
||||
<%= form_with model: [card, Step.new], url: card_steps_path(card) do |form| %>
|
||||
<%= form.text_field :content, class: "input step__content", placeholder: "Add a step…", required: true, autocomplete: "off" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</ol>
|
||||
@@ -0,0 +1,8 @@
|
||||
<% if card.steps.any? %>
|
||||
<div class="card__steps align-center gap-half flex-item-justify-end flex-item-no-shrink">
|
||||
<span class="steps__icon">
|
||||
<%= icon_tag "check" %>
|
||||
</span>
|
||||
<strong><%= "#{card.steps.completed.count}/#{card.steps.count}" %></strong>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -7,9 +7,9 @@
|
||||
<% end %>
|
||||
|
||||
<header class="margin-block-end">
|
||||
<span class="txt-large overflow-ellipsis margin-none font-black" style="view-transistion-name: card-collection-title">
|
||||
<h1 class="txt-large overflow-ellipsis margin-none font-black" style="view-transistion-name: card-collection-title">
|
||||
<span><%= filter_title @filter %></span>
|
||||
</span>
|
||||
</h1>
|
||||
<%= render "filters/settings", filter: @filter %>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<%= turbo_frame_tag step do %>
|
||||
<li class="step">
|
||||
<%= form_with model: [step.card, step], data: { controller: "form" } do |form| %>
|
||||
<%= form.check_box :completed, { class: "step__checkbox", data: { action: "change->form#submit" } } %>
|
||||
<% end %>
|
||||
<%= link_to step.content, edit_card_step_path(step.card, step), class: "step__content" %>
|
||||
</li>
|
||||
<% end %>
|
||||
@@ -0,0 +1,18 @@
|
||||
<%= turbo_frame_tag @step do %>
|
||||
<div class="flex align-center gap-half">
|
||||
<%= form_with model: [@card, @step], class: "step", data: { controller: "form" } do |form| %>
|
||||
<%= form.check_box :completed, { class: "step__checkbox", checked: @step.completed?, disabled: true } %>
|
||||
<%= form.text_field :content, class: "input step__content step__content--edit", placeholder: "Name this step…", required: true, autofocus: true, autocomplete: "off",
|
||||
data: { action: "keydown.esc->form#cancel focus->form#select" } %>
|
||||
<%= form.button type: "submit", class: "btn btn--positive txt-xx-small" do %>
|
||||
<%= icon_tag "check" %>
|
||||
<span class="for-screen-reader">Save changes</span>
|
||||
<% end %>
|
||||
<%= link_to "Cancel changes", card_step_path(@card, @step), data: { form_target: "cancel" }, hidden: true %>
|
||||
<% end %>
|
||||
<%= button_to card_step_path(@card, @step), method: :delete, class: "btn btn--negative txt-xx-small" do %>
|
||||
<%= icon_tag "trash" %>
|
||||
<span class="for-screen-reader">Delete this step</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,3 @@
|
||||
<%= turbo_frame_tag @step do %>
|
||||
<%= render "cards/steps/step", step: @step %>
|
||||
<% end %>
|
||||
@@ -1,4 +1,6 @@
|
||||
<div class="margin-block-end">
|
||||
<strong class="divider txt-large">Do or Die</strong>
|
||||
<%= render "entropy/auto_close", model: collection %>
|
||||
</div>
|
||||
<%= turbo_frame_tag @collection, :entropy_configuration do %>
|
||||
<div class="margin-block-end">
|
||||
<strong class="divider txt-large">Do or Die</strong>
|
||||
<%= render "entropy/auto_close", model: collection, url: collection_entropy_configuration_path(collection) %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -1,54 +1,55 @@
|
||||
<div class="margin-block">
|
||||
<strong class="divider txt-large">Public link</strong>
|
||||
<p class="margin-none-block-start">Turn on the Public link to share this collection with anyone in the world. They won’t need to log in and they won’t be able to see anything else in Fizzy.</p>
|
||||
<%= turbo_frame_tag @collection, :publication do %>
|
||||
<div class="margin-block">
|
||||
<strong class="divider txt-large">Public link</strong>
|
||||
<p class="margin-none-block-start">Turn on the Public link to share this collection with anyone in the world. They won’t need to log in and they won’t be able to see anything else in Fizzy.</p>
|
||||
|
||||
<% if collection.published? %>
|
||||
<div class="border-radius pad fill-selected">
|
||||
<div class="flex-inline center justify-between gap">
|
||||
<span class="txt-large"><%= icon_tag "lock" %></span>
|
||||
<label class="switch flex align-center justify-between">
|
||||
<%= form_with url: collection_publication_path(collection), method: :delete, data: { controller: "form" } do |form| %>
|
||||
<%= form.check_box :published, class: "switch__input", checked: true, data: { action: "change->form#submit" } %>
|
||||
<span class="switch__btn round"></span>
|
||||
<span class="for-screen-reader">Turn off the public link</span>
|
||||
<% end %>
|
||||
</label>
|
||||
<span class="txt-large"><%= icon_tag "world" %></span>
|
||||
</div>
|
||||
<% if collection.published? %>
|
||||
<div class="border-radius pad fill-selected">
|
||||
<div class="flex-inline center justify-between gap">
|
||||
<span class="txt-large"><%= icon_tag "lock" %></span>
|
||||
<label class="switch flex align-center justify-between">
|
||||
<%= form_with url: collection_publication_path(collection), method: :delete, data: { controller: "form" } do |form| %>
|
||||
<%= form.check_box :published, class: "switch__input", checked: true, data: { action: "change->form#submit" } %>
|
||||
<span class="switch__btn round"></span>
|
||||
<span class="for-screen-reader">Turn off the public link</span>
|
||||
<% end %>
|
||||
</label>
|
||||
<span class="txt-large"><%= icon_tag "world" %></span>
|
||||
</div>
|
||||
|
||||
<div class="flex align-center gap-half margin-block">
|
||||
<%= text_field_tag :publication_url, published_collection_url(collection), disabled: true, class: "full-width input fill-white" %>
|
||||
<div class="flex align-center justify-center gap-half">
|
||||
<%= button_to_copy_to_clipboard(published_collection_url(collection)) do %>
|
||||
<%= icon_tag "copy-paste" %>
|
||||
<span class="for-screen-reader">Copy public link</span>
|
||||
<div class="flex align-center gap-half margin-block">
|
||||
<%= text_field_tag :publication_url, published_collection_url(collection), disabled: true, class: "full-width input fill-white" %>
|
||||
<div class="flex align-center justify-center gap-half">
|
||||
<%= button_to_copy_to_clipboard(published_collection_url(collection)) do %>
|
||||
<%= icon_tag "copy-paste" %>
|
||||
<span class="for-screen-reader">Copy public link</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<strong class="margin-block-end-half flex justify-center txt-small">Add an optional description to the public page</strong>
|
||||
<div class="border-radius input fill-white">
|
||||
<%= form_with model: collection, class: "txt-align-start", data: { controller: "form" } do |form| %>
|
||||
<%= form.rich_textarea :public_description, class: "rich-text-content txt-small",
|
||||
placeholder: "Add a public note about this collection…",
|
||||
data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } %>
|
||||
<%= form.button "Save changes", type: :submit, class: "btn txt-small" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<strong class="margin-block-end-half flex justify-center txt-small">Add an optional description to the public page</strong>
|
||||
<div class="border-radius input fill-white">
|
||||
<%= form_with model: collection, class: "txt-align-start", data: { controller: "form" } do |form| %>
|
||||
<%= form.rich_textarea :public_description, class: "rich-text-content txt-small",
|
||||
placeholder: "Add a public note about this collection…",
|
||||
data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } %>
|
||||
<%= form.button "Save changes", type: :submit, class: "btn txt-small" %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="border-radius pad fill-shade">
|
||||
<div class="flex-inline center justify-between gap">
|
||||
<span class="txt-large"><%= icon_tag "lock" %></span>
|
||||
<label class="switch flex align-center justify-between">
|
||||
<%= form_with url: collection_publication_path(collection), method: :post, data: { controller: "form" } do |form| %>
|
||||
<%= form.check_box :published, class: "switch__input", checked: false, data: { action: "change->form#submit" } %>
|
||||
<span class="switch__btn round"></span>
|
||||
<span class="for-screen-reader">Turn on the public link</span>
|
||||
<% end %>
|
||||
</label>
|
||||
<span class="txt-large"><%= icon_tag "world" %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="border-radius pad fill-shade">
|
||||
<div class="flex-inline center justify-between gap">
|
||||
<span class="txt-large"><%= icon_tag "lock" %></span>
|
||||
<label class="switch flex align-center justify-between">
|
||||
<%= form_with url: collection_publication_path(collection), method: :post, data: { controller: "form" } do |form| %>
|
||||
<%= form.check_box :published, class: "switch__input", checked: false, data: { action: "change->form#submit" } %>
|
||||
<span class="switch__btn round"></span>
|
||||
<span class="for-screen-reader">Turn on the public link</span>
|
||||
<% end %>
|
||||
</label>
|
||||
<span class="txt-large"><%= icon_tag "world" %></span>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
<strong class="txt-large">Workflows</strong>
|
||||
<div class="flex justify-center margin-block-end">Use a Workflow to track progress in this Collection.</div>
|
||||
<div class="flex flex-wrap gap justify-center">
|
||||
<% Workflow.all.each do |workflow| %>
|
||||
<div class="workflow-preview txt-align-start border border-radius <%= 'workflow-preview--selected' if workflow == collection.workflow %>">
|
||||
<%= button_to collection_workflow_path(collection), method: :patch do %>
|
||||
<%= hidden_field_tag "collection[workflow_id]", workflow.id %>
|
||||
<strong class="txt-small overflow-ellipsis flex-item-no-shrink"><%= workflow.name %></strong>
|
||||
<ul class="list-style-none txt-xx-small margin-none flex flex-column gap-half">
|
||||
<% workflow.stages.each do |stage| %>
|
||||
<li class="overflow-ellipsis flex align-center gap-half min-width">
|
||||
<span class="workflow-preview__swatch btn btn--circle" style="--btn-background: <%= stage.color.present? ? stage.color : Card::Colored::DEFAULT_COLOR %>;"></span>
|
||||
<span class="overflow-ellipsis"><%= stage.name %></span>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="margin-block-end-double"><%= link_to "Manage workflows", workflows_path, class: "btn btn--plain txt-link" %></p>
|
||||
<%= turbo_frame_tag @collection, :workflows do %>
|
||||
<strong class="txt-large">Workflows</strong>
|
||||
<div class="flex justify-center margin-block-end">Use a Workflow to track progress in this Collection.</div>
|
||||
<div class="flex flex-wrap gap justify-center">
|
||||
<% Workflow.all.each do |workflow| %>
|
||||
<div class="workflow-preview txt-align-start border border-radius <%= 'workflow-preview--selected' if workflow == collection.workflow %>">
|
||||
<%= button_to collection_workflow_path(collection), method: :patch do %>
|
||||
<%= hidden_field_tag "collection[workflow_id]", workflow.id %>
|
||||
<strong class="txt-small overflow-ellipsis flex-item-no-shrink"><%= workflow.name %></strong>
|
||||
<ul class="list-style-none txt-xx-small margin-none flex flex-column gap-half">
|
||||
<% workflow.stages.each do |stage| %>
|
||||
<li class="overflow-ellipsis flex align-center gap-half min-width">
|
||||
<span class="workflow-preview__swatch btn btn--circle" style="--btn-background: <%= stage.color.present? ? stage.color : Card::Colored::DEFAULT_COLOR %>;"></span>
|
||||
<span class="overflow-ellipsis"><%= stage.name %></span>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="margin-block-end-double"><%= link_to "Manage workflows", workflows_path, class: "btn btn--plain txt-link" %></p>
|
||||
<% end %>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
turbo:submit-end->terminal#handleCommandResponse
|
||||
"
|
||||
} do %>
|
||||
<label class="terminal__label txt-nowrap" for="fizzy_do">Fizzy do ></label>
|
||||
<label class="terminal__label txt-nowrap" for="command">Fizzy do ></label>
|
||||
|
||||
<%= rich_textarea_tag "command", nil,
|
||||
toolbar: false,
|
||||
@@ -35,5 +35,5 @@
|
||||
<%= commands_prompt %>
|
||||
<% end %>
|
||||
|
||||
<%= hidden_field_tag "confirmed", nil, data: { terminal_target: "confirmation" } %>
|
||||
<%= hidden_field_tag "confirmed", nil, data: { terminal_target: "confirmation" } %>
|
||||
<% end %>
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
related_element_group_value: card.id,
|
||||
action: "mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %>
|
||||
<div class="card__header flex align-start gap full-width">
|
||||
<div class="card__collection flex-inline align-start">
|
||||
<h4 class="card__collection flex-inline align-start">
|
||||
<span class="card__id"><%= card.id %></span>
|
||||
<span class="card__collection-name">
|
||||
<span class="overflow-ellipsis"><%= event.collection.name %></span>
|
||||
</span>
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
<% if event_column(event) == 2 %>
|
||||
<span class="event__icon flex-item-justify-end txt-reversed">
|
||||
|
||||
@@ -36,5 +36,5 @@
|
||||
</header>
|
||||
<div class="events" id="activity">
|
||||
<%= render "events/day", day_timeline: @day_timeline %>
|
||||
<%= event_next_page_link(@day_timeline.next_day) %>
|
||||
</div>
|
||||
<%= event_next_page_link(@day_timeline.next_day) %>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<body data-controller="lightbox local-time timezone-cookie" data-action="turbo:morph@window->local-time#refreshAll">
|
||||
<header id="header">
|
||||
<a href="#main-content" class="skip-navigation btn">Skip to main content</a>
|
||||
<a href="#main" class="skip-navigation btn" data-turbo="false">Skip to main content</a>
|
||||
<%= yield :header %>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<body class="public" data-controller="local-time timezone-cookie" data-action="turbo:morph@window->local-time#refreshAll">
|
||||
<header id="header">
|
||||
<a href="#main-content" class="skip-navigation btn">Skip to main content</a>
|
||||
<a href="#main" class="skip-navigation btn" data-turbo="false">Skip to main content</a>
|
||||
<%= yield :header %>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||
<%= javascript_importmap_tags %>
|
||||
|
||||
<%= tenanted_action_cable_meta_tag %>
|
||||
<%= yield :head %>
|
||||
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="manifest" href="<%= pwa_manifest_path(format: :json) %>">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="icon" href="/favicon.png" type="image/png">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="tray__expander" data-action="dialog#toggle" aria-label="Expand pins stack">
|
||||
<button class="tray__expander" data-action="dialog#toggle" aria-label="Expand pins stack" aria-haspopup="true">
|
||||
<%= icon_tag "pinned" %>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
<%= avatar_image_tag notification.creator %>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column min-width flex-item-grow">
|
||||
<h3 class="flex flex-column min-width flex-item-grow font-weight-normal">
|
||||
<%= render "notifications/notification/#{notification.source_type.underscore}/body", notification: notification %>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="tray__actions">
|
||||
<%= 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 %>
|
||||
</div>
|
||||
|
||||
<button class="tray__expander" data-action="dialog#toggle" aria-label="Expand notifications stack">
|
||||
<button class="tray__expander" data-action="dialog#toggle" aria-label="Expand notifications stack" aria-haspopup="true">
|
||||
<%= icon_tag "bell" %>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
@@ -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" }]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
<%= auto_submit_form_with method: :put %>
|
||||
<%= auto_submit_form_with(method: :put) do |form| %>
|
||||
<%= form.hidden_field(:sig, value: @sig) %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<% @page_title = "Sign in" %>
|
||||
|
||||
<div class="panel shadow center margin-block-double <%= "shake" if flash[:alert] %>" style="--panel-size: 55ch;">
|
||||
<h1 class="txt-xx-large margin-block-end-double">Fizzy</h1>
|
||||
|
||||
<%= form_with url: session_path, class: "flex flex-column gap txt-large" do |form| %>
|
||||
<div class="flex align-center gap">
|
||||
<label class="flex align-center gap input input--actor">
|
||||
<%= form.email_field :email_address, required: true, class: "input full-width", autofocus: true, autocomplete: "username", placeholder: "Enter your email address" %>
|
||||
<%= icon_tag "email", class: "txt-large" %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex align-center gap">
|
||||
<label class="flex align-center gap input input--actor">
|
||||
<%= form.password_field :password, required: true, class: "input full-width", autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %>
|
||||
<%= icon_tag "password", class: "txt-large" %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="log_in" class="btn btn--reversed center">
|
||||
<%= icon_tag "arrow-right" %>
|
||||
<span class="for-screen-reader">Sign in</span>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% content_for :footer do %>
|
||||
<div class="txt-align-center center margin-block-double txt-subtle">Fizzy™ version 0</div>
|
||||
<% end %>
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<% content_for :header do %>
|
||||
<nav>
|
||||
<%= link_to new_session_path, class: "btn flex-item-justify-end", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
|
||||
<%= link_to Launchpad.login_url(account: Current.account), class: "btn flex-item-justify-end", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
|
||||
<span class="for-screen-reader">Sign in instead</span>
|
||||
<% end %>
|
||||
</nav>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</div>
|
||||
|
||||
<div class="panel borderless center margin-block-double">
|
||||
<%= 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" %>
|
||||
<span>Sign out</span>
|
||||
<% end %>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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!
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
ActiveSupport.on_load(:action_controller_base) do
|
||||
before_action { logger.struct tenant: ApplicationRecord.current_tenant }
|
||||
end
|
||||
@@ -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
|
||||
+4
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
class RemovePasswordFromUsers < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
remove_column :users, :password, :string
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddNotNullToAccountsQueenbeeId < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
change_column_null :accounts, :queenbee_id, false
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
Generated
+13
-9
@@ -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"
|
||||
|
||||
+73
-59
@@ -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
|
||||
|
||||
+4
-1
@@ -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
|
||||
|
||||
+15
-5
@@ -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
|
||||
|
||||
Executable
+21
@@ -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
|
||||
Executable
+70
@@ -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
|
||||
Executable
+33
@@ -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 ]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user