Merge branch 'main' into collection-nav

* main: (54 commits)
  Remove creation and closure params
  Support viewing user profiles and activity
  Support downcase
  refresh fixtures
  Always downcase names
  Remove vcr_record call
  Support closing soon and falling back soon via natural language
  Add scopes and filters for falling back soon and auto closing soon
  Fix label
  Add chip filters for the new time window filters
  Fix: creator_ids is an array, not a single value
  Add support for filtering by creation or completion windows of times
  Adjust spacing
  Use new logo placeholder
  Update logo assets
  Need to return all collections so it's not blank
  Button to add a new filter
  Update test for new redirect
  Comparing params is fragile, use `id` for saved filters
  Update summary tests
  ...
This commit is contained in:
Andy Smith
2025-06-30 12:57:10 -05:00
82 changed files with 1066521 additions and 324 deletions
-4
View File
@@ -122,10 +122,6 @@
text-align: center;
}
.events__popup {
margin-block-start: var(--block-space-half);
}
/* Event
/* ------------------------------------------------------------------------ */
+5 -13
View File
@@ -1,8 +1,11 @@
@layer components {
.filters {
view-transition-name: "filters";
z-index: var(--z-popup);
#header:has(&) {
position: relative;
z-index: 1;
z-index: var(--z-popup);
}
.btn {
@@ -94,19 +97,8 @@
}
}
.panel:is(.filter__popup) {
--panel-size: 100ch;
--panel-padding: var(--block-space);
inline-size: auto !important;
min-inline-size: var(--panel-size);
max-block-size: calc(90dvh - (2 * var(--block-space-double)));
max-inline-size: calc(100dvw - (2 * var(--block-space-double)));
z-index: 1;
}
.quick-filter {
&:has(input:not(.default-value):checked) {
&:not(.default-value):has(.checked) {
.input--select {
--input-background: var(--color-selected);
}
+1 -1
View File
@@ -22,7 +22,7 @@
svg {
block-size: auto;
inline-size: 1.3em;
inline-size: 1.1em;
margin-block-end: 0.2em;
margin-inline-end: 0.8ch;
}
+1 -1
View File
@@ -94,7 +94,7 @@
}
&:has(input:checked) {
--btn-background: var(--color-ink-lightest);
--btn-background: transparent;
--btn-color: var(--color-ink);
.icon {
+6 -1
View File
@@ -9,6 +9,11 @@ module FilterScoped
DEFAULT_PARAMS = { indexed_by: "latest" }
def set_filter
@filter = Current.user.filters.from_params params.reverse_merge(**DEFAULT_PARAMS).permit(*Filter::PERMITTED_PARAMS)
@expand_all = params[:expand_all]
if params[:filter_id].present?
@filter = Current.user.filters.find(params[:filter_id])
else
@filter = Current.user.filters.from_params params.reverse_merge(**DEFAULT_PARAMS).permit(*Filter::PERMITTED_PARAMS)
end
end
end
+3 -2
View File
@@ -3,16 +3,17 @@ class FiltersController < ApplicationController
def create
@filter = Current.user.filters.remember filter_params
redirect_to cards_path(@filter.as_params)
redirect_to cards_path(filter_id: @filter.id)
end
def destroy
filter_params = @filter.as_params
@filter.destroy!
if request.referer == root_url
redirect_to root_path
else
redirect_to cards_path(@filter.as_params)
redirect_to cards_path(filter_params)
end
end
+8
View File
@@ -21,6 +21,14 @@ module FiltersHelper
hidden_field_tag name, value, id: nil
end
def filter_selected_collections_sentence(filter)
if filter.collections.any?
filter.collections.collect { "<strong>#{it.name}</strong>" }.uniq.sort.to_sentence
else
tag.strong "All collections"
end
end
def filter_selected_collections_label(filter)
selected_collections = if filter.collections.any?
filter.collections.collect { "<strong>#{it.name}</strong>" }.uniq.sort.to_sentence
@@ -27,6 +27,10 @@ export default class extends Controller {
} else {
this.#hideHelpMenu()
}
if (!this.inputTarget.value.trim()) {
event.preventDefault()
}
}
hideMenus() {
+2
View File
@@ -24,6 +24,8 @@ class Card < ApplicationRecord
when "oldest" then chronologically
when "latest" then latest
when "stalled" then stalled.chronologically
when "closing_soon" then closing_soon.chronologically
when "falling_back_soon" then falling_back_soon.chronologically
when "closed" then closed
end
end
+1
View File
@@ -8,6 +8,7 @@ module Card::Closeable
scope :open, -> { where.missing(:closure) }
scope :recently_closed_first, -> { closed.order("closures.created_at": :desc) }
scope :closed_at_window, ->(window) { closed.where("closures.created_at": window) }
end
def closed?
+16
View File
@@ -9,7 +9,23 @@ module Card::Entropic
end
scope :stagnated, -> { doing.entropic_by(:auto_reconsider_period) }
scope :due_to_be_closed, -> { considering.entropic_by(:auto_close_period) }
scope :closing_soon, -> do
considering
.left_outer_joins(collection: :entropy_configuration)
.where("last_active_at > DATETIME('now', '-' || COALESCE(entropy_configurations.auto_close_period, ?) || ' seconds')", Entropy::Configuration.default.auto_close_period)
.where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropy_configurations.auto_close_period, ?) * 0.75 AS INTEGER) || ' seconds')", Entropy::Configuration.default.auto_close_period)
end
scope :falling_back_soon, -> do
doing
.left_outer_joins(collection: :entropy_configuration)
.where("last_active_at > DATETIME('now', '-' || COALESCE(entropy_configurations.auto_reconsider_period, ?) || ' seconds')", Entropy::Configuration.default.auto_reconsider_period)
.where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropy_configurations.auto_reconsider_period, ?) * 0.75 AS INTEGER) || ' seconds')", Entropy::Configuration.default.auto_reconsider_period)
end
delegate :auto_close_period, :auto_reconsider_period, to: :collection
end
+39
View File
@@ -0,0 +1,39 @@
class Command::AddCard < Command
store_accessor :data, :card_title, :collection_id, :created_card_id
validates :collection, presence: true
def title
"Create a new card with title '#{card_title}'"
end
def execute
transaction do
card = collection.cards.create!(title: card_title)
update! created_card_id: card.id
redirect_to collection_card_path(collection, card, focus_on_title: true)
end
end
def undo
created_card&.destroy
end
def undoable?
true
end
private
def closed_cards
user.accessible_cards.where(id: closed_card_ids)
end
def collection
user.collections.find_by_id(collection_id)
end
def created_card
user.accessible_cards.find_by_id(created_card_id)
end
end
+2 -2
View File
@@ -43,7 +43,7 @@ class Command::Ai::Parser
normalized_query.tap do |query_json|
if query_context = query_json[:context].presence
query_context[:assignee_ids] = query_context[:assignee_ids]&.filter_map { |name| assignee_from(name)&.id }
query_context[:creator_id] = assignee_from(query_context[:creator_id])&.id if query_context[:creator_id]
query_context[:creator_ids] = query_context[:creator_ids]&.filter_map { |name| assignee_from(name)&.id }
query_context[:collection_ids] = query_context[:collection_ids]&.filter_map { |name| Collection.where("lower(name) like ?", "%#{name.downcase}%").first&.id }
query_context[:tag_ids] = query_context[:tag_ids]&.filter_map { |name| ::Tag.find_by_title(name)&.id }
query_context.compact!
@@ -53,7 +53,7 @@ class Command::Ai::Parser
def assignee_from(string)
string_without_at = string.delete_prefix("@")
User.all.find { |user| user.mentionable_handles.include?(string_without_at) }
User.all.find { |user| user.mentionable_handles.include?(string_without_at.downcase) }
end
def context_from_query(query_json)
+236 -84
View File
@@ -1,4 +1,6 @@
class Command::Ai::Translator
include Rails.application.routes.url_helpers
attr_reader :context
delegate :user, to: :context
@@ -9,6 +11,7 @@ class Command::Ai::Translator
def translate(query)
response = translate_query_with_llm(query)
Rails.logger.info "AI Translate: #{query} => #{response}"
normalize JSON.parse(response)
end
@@ -23,7 +26,7 @@ class Command::Ai::Translator
end
def chat
chat = ::RubyLLM.chat
chat = RubyLLM.chat.with_temperature(0)
chat.with_instructions(prompt + custom_context)
end
@@ -31,117 +34,266 @@ class Command::Ai::Translator
<<~PROMPT
You are Fizzys command translator.
OUTPUT FORMAT
Return one **valid JSON** object that matches exactly this type:
--------------------------- OUTPUT FORMAT ---------------------------
Return ONE valid JSON object matching **exactly**:
type FizzyOutput = {
context?: {
terms?: string[];
indexed_by?: "newest" | "oldest" | "latest" | "stalled" | "closed";
assignee_ids?: string[];
assignment_status?: "unassigned";
engagement_status?: "considering" | "doing";
card_ids?: number[];
creator_id?: string;
collection_ids?: string[];
tag_ids?: string[];
};
commands?: string[]; // each entry starts with '/' exactly
{
"context": { /* REQUIRED unless empty */
"terms": string[],
"indexed_by": "newest" | "oldest" | "latest" | "stalled" | "closed" | "closing_soon" | "falling_back_soon"
"assignee_ids": string[],
"assignment_status": "unassigned",
"card_ids": number[],
"creator_ids": string[],
"collection_ids": string[],
"tag_ids": string[],
"creation": "today" | "yesterday" | "thisweek" | "thismonth" | "thisyear"
| "lastweek" | "lastmonth" | "lastyear",
"closure": "today" | "yesterday" | "thisweek" | "thismonth" | "thisyear"
| "lastweek" | "lastmonth" | "lastyear"
},
"commands": string[] /* OPTIONAL, each starts with "/" */
}
If neither `context` nor `commands` is appropriate, output **exactly**:
{ "commands": ["/search <query>"] }
❗ If any filter key appears outside "context", the response is **INVALID**.
-- Do **NOT** add any other top-level keys.
-- Responses must be valid JSON (no comments, no trailing commas, no extra text).
If neither context nor commands is appropriate, output **exactly**:
{ "commands": ["/search <user request>"] }
INTERNAL THINKING STEPS
Do NOT add any other top-level keys.
Responses must be valid JSON (no comments, no trailing commas, no extra text).
----------------------- INTERNAL THINKING STEPS ----------------------
(Do **not** output these steps.)
1. Decide whether the users request:
a. only filters existing cards fill `context`,
b. requires actions add `commands` in spoken order,
c. matches neither fallback search.
2. Emit the FizzyOutput object.
DOMAIN KNOWLEDGE & INTERPRETATION RULES
1. Decide whether the users request
a. only filters existing cards → fill context
b. requires actions → add commands in spoken order
c. matches neither → fallback search
2. Emit the FizzyOutput object.
------------------ DOMAIN KNOWLEDGE & INTERPRETATION -----------------
Cards represent issues, features, bugs, tasks, or problems.
Cards have comments and live inside collections.
Context filters describe card **state already true**.
Commands (`/assign`, `/tag`, `/close`, `/search`, `/clear`) apply **new actions**.
Context filters describe card state already true.
Commands (/assign, /tag, /close, /search, /clear, /do, /consider, /stage, /visit, /add_card, /user) apply new actions.
Context properties you may use:
Context properties you may use
* terms — array of keywords
* indexed_by — "newest", "oldest", "latest", "stalled", "closed", "closing_soon", "falling_back_soon"
* assignee_ids — array of assignee names
* assignment_status — "unassigned". Important: ONLY when the user asks for unassigned cards.
* card_ids — array of card IDs
* creator_ids — array of creators names
* collection_ids — array of collections
* tag_ids — array of tag names
* creation — relative range when the card was **created** (values listed above). Use it only
when the user asks for cards created in a specific timeframe.
* closure — relative range when the card was **completed/closed** (values listed above). Use it
only when the user asks for cards completed/closed in a specific timeframe.
* "Falling back soon" cards are cards in "Doing" that are going to be moved back to "Reconsidering" automatically soon.
- Falling back soon means to be reconsidered soon too.
* "Closing soon" cards are cards in "Considering" that are going to be closed automatically soon.
* terms array of keywords
* indexed_by "newest", "oldest", "latest", "stalled", "closed"
* assignee_ids array of assignee names
* assignment_status "unassigned"
* engagement_status "considering" | "doing"
* card_ids array of card IDs
* creator_id creators name
* collection_ids array of collections
* tag_ids array of tag names
---------------------- EXPLICIT FILTERING RULES ----------------------
Explicit filtering rules
------------------------
* Use **terms** only if the query explicitly refers to cards; plain text searches go to `/search`.
* Numbers without the word card(s) default to `terms`.
* "123" terms: ["123"]
* "card 1,2" card_ids: [1, 2]
* X collection collection_ids: ["X"]
* Assigned to X assignee_ids: ["X"]
* Created by X creator_id: "X"
* Tagged with X, #X cards” → tag_ids: ["X"]
* Unassigned cards assignment_status: "unassigned"
* My cards assignee_ids of requester (if identifiable)
* Recent cards indexed_by: "newest"
* Cards with recent activity indexed_by: "latest"
* Completed/closed cards indexed_by: "closed"
* If cards are described as assigned to X (state) and later assign X (action), **only** the first is a filter.
* Use terms only if the query explicitly refers to cards; plain-text searches go to /search.
* Numbers without the word "card(s)" default to terms **unless the number is the direct object of an
action verb that operates on cards (move, assign, tag, close, stage, consider, do, etc.).**
"123" (with no action verb) → context: { terms: ["123"] }
"card 123" → context: { card_ids: [123] }
"card 1,2" → context: { card_ids: [1, 2] }
"move 1 and 2 to doing" → context: { card_ids: [1, 2] }, commands: ["/do"]
Command interpretation rules
----------------------------
* Unless a clear command applies, fallback to `/search` with the verbatim text.
* When searching for nouns (non-person), prefer `/search` over `terms`.
* Respect the order of commands in the users sentence.
* tag with #design” → `/tag #design` (not a filter)
* #design cards” → context.tag_ids = ["design"] (no `/tag`)
* Assign cards tagged with #design to jz” → context.tag_ids = ["design"]; command `/assign jz`
* close as [reason] or close because [reason] `/close [reason]`
* Lone close `/close` (acts on current context)
Quick mnemonic
WORD “card(s)” present? → card_ids
ACTION verb present? → card_ids + command
Otherwise → terms
Crucial donts
--------------
* **Never** use names or tags mentioned inside commands as filters.
* **Never** add properties tied to UI view (card, list, etc.).
* **All** filters, including terms, must live inside `context`.
* "Completed/closed cards" ( **and NO words like
today, yesterday, thisweek, thismonth, thisyear,
lastweek, lastmonth, lastyear** ) → indexed_by: "closed"
Never add "closure" unless one of the eight timeframe tokens is present.
* Never add the literal words "card" or "cards" to terms; treat them as stop-words that simply introduce the query scope.
* "X collection" → collection_ids: ["X"]
* **Past-tense** “assigned to X” → assignee_ids: ["X"] (filter)
* **Imperative** “assign to X”, “assign to me” → command /assign X
Never use assignee_ids when the user gives an imperative assignment
* "Created by X" → creator_ids: ["X"]
* "Stagnated or stalled cards" → indexed_by: "stalled"
* "Closing soon" cards → indexed_by: "closing_soon"
* "Falling back soon" cards → indexed_by: "falling_back_soon"
* **Past-tense** “tagged with #X”, “#X cards” → tag_ids: ["X"] (filter)
* **Imperative** “tag …”, “tag with #X”, “add the #X tag”, “apply #X” → command /tag #X (never a filter)
* "Unassigned cards" (or “not assigned”, “with no assignee”) → assignment_status: "unassigned".
IMPORTANT: Only set assignment_status when the user **explicitly** asks for an unassigned state
Do NOT infer unassigned just because an assignment follows
* **Possessive “my” in front of “card” or “cards”***
→ assignee_ids: [ #{user.name} ] — applies **even when other filters are present***
(e.g., “my cards closing soon”, “my stalled cards”, “my cards created yesterday”).
* “Recent cards” (i.e., newly created) → indexed_by: "newest"
* “Cards with recent activity”, “recently updated cards” → indexed_by: "latest"
Only use "latest" if the user mentions activity, updates, or changes
Otherwise, prefer "newest" for generic mentions of “recent”
* "Completed/closed cards" (no date range) → indexed_by: "closed"
VERY IMPORTANT: Do **not** set "closure" filter unless the user explicitly supplies a timeframe
(e.g., “completed this month”, “closed last week”).
(If the timeframe is supplied with “closed” instead of “completed”, treat it the same way.)
* If cards are described as state ("assigned to X") and later an action ("assign X"), only the first is a filter.
* ❗ Once you produce a valid context **or** command list, do not add a fallback /search.
-------------------- COMMAND INTERPRETATION RULES --------------------
* /user <Name> → open that persons profile or activity feed.
Phrases like “visit user <Name>”, “view user <Name>”, “see <Name>s profile” must map to **/user**, **never** to /visit.
* /visit <url|path> → open any other URL or internal path (cards, settings, etc.).
* /do → engage with card and move it to "doing"
* /consider → move card back to "considering" (reconsider)
* Unless a clear command applies, fallback to /search with the verbatim text.
* When searching for nouns (non-person), prefer /search over terms.
* Respect the spoken order of commands.
* "close as [reason]" or "close because [reason]" → /close [reason]
Remove "as" or "because" from the actual command
* Lone "close" → /close (acts on current context)
* /close must **only** be produced if the request explicitly contains the verb “close”.
* /stage [workflow stage]→ assign the card to the given stage (never takes card IDs).
* “Move <ID(s)> to <Stage>” → context.card_ids = [IDs]; command /stage <Stage>
* “Move <ID(s)> to doing” → context.card_ids = [IDs]; command /do
* “Move <ID(s)> to considering” → context.card_ids = [IDs]; command /consider
* /add_card → Create a new card with a blank title
* /add_card [title] → Create a new card with the provided title
---------------------------- VISIT SCREENS ---------------------------
You can open these screens by using /visit:
* "View my profile" → /visit #{user_path(user)}.
* "My profile" → /visit #{user_path(user)}.
* "Edit my profile" (including your name and avatar) → /visit #{edit_user_path(user)}.
* Manage users → /visit #{account_settings_path}
* Account settings → /visit #{account_settings_path}
------------------------- VISIT USER PROFILES ------------------------
Use **/user <Name>** (not /visit) whenever the request is about viewing a persons profile or activity:
• visit user mike → /user mike*
• view user kevin → /user kevin*
• see mikes profile → /user mike
---------------------------- CRUCIAL DONTS ---------------------------
* Dont output “/visit /users/<name>”. Profile requests must use **/user <name>**.
* Never use names, tags, or stage names mentioned **inside commands** (like /assign, /tag, /stage) as filters.
* Never duplicate the assignee in both commands and context.
* Never add properties tied to UI view ("card", "list", etc.).
* To filter completed or closed cards, use "indexed_by: closed"; don't set a "closure" filter unless the user is asking for cards completed in a specific window of time.
* When you see a word with a # prefix, assume it refers to a tag (either a filter or a command argument, but don't search for it).
* All filters, including terms, must live **inside** context.
* Do not duplicate terms across properties.
* Don't use "creation" and "closure" filters at the same time.
* Avoid redundant terms.
Positive & negative examples
----------------------------
**User:** assign andy to the current #design cards assigned to jz and tag them with #v2
**Output:**
---------------------------- OUTPUT CLEANLINESS ----------------------------
* Only include context keys that have a meaningful, non-empty value.
* Do NOT include empty arrays, empty strings, or default values that don't apply.
---------------------- POSITIVE & NEGATIVE EXAMPLES -------------------
User: assign andy to the current #design cards assigned to jz and tag them with #v2*
Output:
{
"context": { "assignee_ids": ["jz"], "tag_ids": ["design"] },
"commands": ["/assign andy", "/tag #v2"]
}
**Incorrect (do NOT do this):**
User: assign to jz*
Output:
{
"context": { "assignee_ids": ["andy"], "tag_ids": ["v2"] },
"commands": ["/assign andy", "/tag #v2"]
"commands": ["/assign jz"]
}
Additional examples:
{ "context": { "assignee_ids": ["jorge"] }, "commands": ["/close"] }
{ "context": { "tag_ids": ["design"] } }
{ "commands": ["/assign jorge", "/tag #design"] }
User: cards assigned to jz*
Output:
{
"context": { "assignee_ids": ["jz"] }
}
Fallback search example:
{ "commands": ["/search whats blocking deploy"] }
User: tag with #design*
Output:
{
"commands": ["/tag #design"]
}
END OF PROMPT
User: completed cards*
Output:
{
"context": { "indexed_by": "closed" }
}
User: completed cards yesterday*
Output:
{
"context": { "indexed_by": "closed", "closure": "yesterday" }
}
User: "cards tagged with #design"*
Output:
{
"context": { "tag_ids": ["design"] }
}
User: Unassigned cards*
Output:
{
"context": { "assignment_status": "unassigned" }
}
User: Close Andys cards, then assign them to Kevin*
Output:
{
"context": { "assignee_ids": ["andy"] },
"commands": ["/close", "/assign kevin"]
}
User: cards created yesterday*
Output:
{
"context": { "creation": "yesterday" }
}
User: cards completed last week*
Output:
{
"context": { "closure": "lastweek", "indexed_by": "closed" }
}
User: my cards that are going to be auto closed*
Output:
{
"context": { "assignee_ids": ["<current user>"], "indexed_by": "closing_soon" }
}
User: visit user kevin*
Output:
{
"commands": ["/user kevin"]
}
User: visit /users/kevin*
Output:
{
"commands": ["/visit /users/kevin"]
}
Fallback search example (when nothing matches):*
{ "commands": ["/search what's blocking deploy"] }
---------------------------- END OF PROMPT ---------------------------
PROMPT
end
+34
View File
@@ -0,0 +1,34 @@
class Command::Consider < Command
include Command::Cards
store_accessor :data, :statuses_by_card_id
def title
"Move #{cards_description} to Considering"
end
def execute
statuses_by_card_id = {}
transaction do
cards.find_each do |card|
statuses_by_card_id[card.id] = { closed: card.closed?, doing: card.doing?, considering: card.considering? }
card.reconsider
end
update! statuses_by_card_id: statuses_by_card_id
end
end
def undo
transaction do
statuses_by_card_id.each do |card_id, data|
if card = user.accessible_cards.find_by_id(card_id)
card.close if data["closed"]
card.engage if data["doing"]
card.reconsider if data["considering"]
end
end
end
end
end
+34
View File
@@ -0,0 +1,34 @@
class Command::Do < Command
include Command::Cards
store_accessor :data, :statuses_by_card_id
def title
"Move #{cards_description} to Doing"
end
def execute
statuses_by_card_id = {}
transaction do
cards.find_each do |card|
statuses_by_card_id[card.id] = { closed: card.closed?, doing: card.doing?, considering: card.considering? }
card.engage
end
update! statuses_by_card_id: statuses_by_card_id
end
end
def undo
transaction do
statuses_by_card_id.each do |card_id, data|
if card = user.accessible_cards.find_by_id(card_id)
card.close if data["closed"]
card.engage if data["doing"]
card.reconsider if data["considering"]
end
end
end
end
end
+1 -1
View File
@@ -8,7 +8,7 @@ class Command::GoToUser < Command
end
def execute
redirect_to user
redirect_to user_path(user)
end
private
+28 -3
View File
@@ -25,16 +25,26 @@ class Command::Parser
Command::FilterByTag.new(tag_title: tag_title_from(string), params: filter.as_params)
when /^@/
Command::GoToUser.new(user_id: assignee_from(command_name)&.id)
when "/user"
Command::GoToUser.new(user_id: assignee_from(combined_arguments)&.id)
when "/assign", "/assignto"
Command::Assign.new(assignee_ids: assignees_from(command_arguments).collect(&:id), card_ids: cards.ids)
when "/clear"
Command::ClearFilters.new(params: filter.as_params)
when "/close"
Command::Close.new(card_ids: cards.ids, reason: combined_arguments)
when "/consider", "/reconsider"
Command::Consider.new(card_ids: cards.ids)
when "/do"
Command::Do.new(card_ids: cards.ids)
when "/insight"
Command::GetInsight.new(query: combined_arguments, card_ids: cards.ids)
when "/add_card"
Command::AddCard.new(card_title: combined_arguments, collection_id: guess_collection&.id)
when "/search"
Command::Search.new(terms: combined_arguments)
when "/stage"
Command::Stage.new(stage_id: stage_from(combined_arguments)&.id, card_ids: cards.ids)
when "/visit"
Command::VisitUrl.new(url: command_arguments.first)
when "/tag"
@@ -55,7 +65,21 @@ class Command::Parser
# under the hood instead, as determined by the user picker. E.g: @1234.
def assignee_from(string)
string_without_at = string.delete_prefix("@")
User.all.find { |user| user.mentionable_handles.include?(string_without_at) }
User.all.find { |user| user.mentionable_handles.include?(string_without_at.downcase) }
end
def stage_from(combined_arguments)
candidate_stages.find do |stage|
stage.name.downcase.include?(combined_arguments.downcase)
end
end
def guess_collection
cards.first&.collection || Collection.first
end
def candidate_stages
Workflow::Stage.where(workflow_id: cards.joins(:collection).select("collections.workflow_id").distinct)
end
def tag_title_from(string)
@@ -73,8 +97,9 @@ class Command::Parser
end
def multiple_cards_from(string)
if tokens = string.split(/[\s,]+/).filter { it =~ /\A\d+\z/ }.presence
user.accessible_cards.where(id: tokens).presence if tokens.many?
if string.match?(/^[\d\s,]+$/)
tokens = string.split(/[\s,]+/)
user.accessible_cards.where(id: tokens).presence if tokens&.many?
end
end
+55
View File
@@ -0,0 +1,55 @@
class Command::Stage < Command
include Command::Cards
store_accessor :data, :stage_id, :original_stage_ids_by_card_id
validates_presence_of :stage
def title
"Move #{cards_description} to stage '#{stage&.name || stage_id}'"
end
def execute
original_stage_ids_by_card_id = {}
transaction do
cards.find_each do |card|
next unless card_compatible_with_stage?(card)
original_stage_ids_by_card_id[card.id] = card.stage_id
card.change_stage_to stage
end
update! original_stage_ids_by_card_id: original_stage_ids_by_card_id
end
end
def undo
transaction do
affected_cards_by_id = user.accessible_cards.where(id: original_stage_ids_by_card_id.keys).index_by(&:id)
stages_by_id = Workflow::Stage.where(id: original_stage_ids_by_card_id.values).uniq.index_by(&:id)
original_stage_ids_by_card_id.each do |card_id, original_stage_id|
card = affected_cards_by_id[card_id.to_i]
stage = stages_by_id[original_stage_id.to_i]
next unless card && stage
card.change_stage_to stage
end
end
end
private
def stage
Workflow::Stage.find_by(id: stage_id)
end
def card_compatible_with_stage?(card)
stage&.workflow && card.collection.workflow == stage.workflow
end
def closed_cards
user.accessible_cards.where(id: closed_card_ids)
end
end
+3 -1
View File
@@ -20,7 +20,7 @@ class Filter < ApplicationRecord
@cards ||= begin
result = creator.accessible_cards.indexed_by(indexed_by)
result = result.where(id: card_ids) if card_ids.present?
result = result.open unless indexed_by.closed? || card_ids.present?
result = result.open unless indexed_by.closed? || closure_window || card_ids.present?
result = result.by_engagement_status(engagement_status) if engagement_status.present?
result = result.unassigned if assignment_status.unassigned?
result = result.assigned_to(assignees.ids) if assignees.present?
@@ -28,6 +28,8 @@ class Filter < ApplicationRecord
result = result.where(collection: collections.ids) if collections.present?
result = result.in_stage(stages.ids) if stages.present?
result = result.tagged_with(tags.ids) if tags.present?
result = result.where("cards.created_at": creation_window) if creation_window
result = result.closed_at_window(closure_window) if closure_window
result = terms.reduce(result) do |result, term|
result.mentioning(term)
end
+11 -2
View File
@@ -1,7 +1,7 @@
module Filter::Fields
extend ActiveSupport::Concern
INDEXES = %w[ newest oldest latest stalled ]
INDEXES = %w[ newest oldest latest stalled closing_soon falling_back_soon ]
delegate :default_value?, to: :class
@@ -16,7 +16,8 @@ module Filter::Fields
end
included do
store_accessor :fields, :assignment_status, :indexed_by, :terms, :engagement_status, :card_ids
store_accessor :fields, :assignment_status, :indexed_by, :terms,
:engagement_status, :card_ids, :creation, :closure
def assignment_status
super.to_s.inquiry
@@ -30,6 +31,14 @@ module Filter::Fields
super&.inquiry
end
def creation_window
TimeWindowParser.parse(creation)
end
def closure_window
TimeWindowParser.parse(closure)
end
def terms
Array(super)
end
+4
View File
@@ -5,6 +5,8 @@ module Filter::Params
:assignment_status,
:indexed_by,
:engagement_status,
:creation,
:closure,
card_ids: [],
assignee_ids: [],
creator_ids: [],
@@ -38,6 +40,8 @@ module Filter::Params
{}.tap do |params|
params[:indexed_by] = indexed_by
params[:engagement_status] = engagement_status
params[:creation] = creation
params[:closure] = closure
params[:assignment_status] = assignment_status
params[:terms] = terms
params[:tag_ids] = tags.ids
+5 -9
View File
@@ -1,16 +1,18 @@
module Filter::Summarized
def summary
[ index_summary, tag_summary, assignee_summary, creator_summary, stage_summary, terms_summary ].compact.to_sentence + " #{collection_summary}"
[ index_summary, tag_summary, assignee_summary, creator_summary, stage_summary, terms_summary ].compact.to_sentence
end
private
def index_summary
indexed_by.humanize
unless indexed_by.latest?
indexed_by.humanize
end
end
def tag_summary
if tags.any?
"tagged #{tags.map(&:hashtag).to_choice_sentence}"
"#{tags.map(&:hashtag).to_choice_sentence}"
end
end
@@ -28,12 +30,6 @@ module Filter::Summarized
end
end
def collection_summary
if collections.any?
"in #{collections.pluck(:name).to_choice_sentence}"
end
end
def terms_summary
if terms.any?
"matching #{terms.map { |term| %Q("#{term}") }.to_sentence}"
+58
View File
@@ -0,0 +1,58 @@
class TimeWindowParser
attr_reader :now
HUMAN_NAMES_BY_VALUE = {
"today" => "Today",
"yesterday" => "Yesterday",
"thisweek" => "This week",
"thismonth" => "This month",
"thisyear" => "This year",
"lastweek" => "Last week",
"lastmonth" => "Last month",
"lastyear" => "Last year"
}
VALUES = HUMAN_NAMES_BY_VALUE.keys
class << self
def parse(string)
new.parse(string)
end
def human_name_for(value)
HUMAN_NAMES_BY_VALUE[value]
end
end
def initialize(now: Time.current)
@now = now
end
def parse(string)
case normalize(string)
when "today"
now.beginning_of_day..now.end_of_day
when "yesterday"
(now - 1.day).beginning_of_day..(now - 1.day).end_of_day
when "thisweek"
now.beginning_of_week..now.end_of_week
when "thismonth"
now.beginning_of_month..now.end_of_month
when "thisyear"
now.beginning_of_year..now.end_of_year
when "lastweek"
(now - 1.week).beginning_of_week..(now - 1.week).end_of_week
when "lastmonth"
(now - 1.month).beginning_of_month..(now - 1.month).end_of_month
when "lastyear"
(now - 1.year).beginning_of_year..(now - 1.year).end_of_year
end
end
private
def normalize(string)
if string
string.downcase.gsub(/[\s_\-]/, "")
end
end
end
+2 -2
View File
@@ -1,4 +1,4 @@
<% if card.published? %>
<% if card.published? && !params[:focus_on_title]%>
<%= turbo_frame_tag card, :edit do %>
<div class="card__content">
<h1 class="card__title flex align-start gap-half">
@@ -14,7 +14,7 @@
<h1 class="card__title autoresize__wrapper" data-autoresize-target="wrapper" data-autoresize-clone-value="">
<%= form.text_area :title, placeholder: "Name it…",
class: "input input--textarea full-width borderless txt-align-start autoresize__textarea",
autofocus: card.title.blank?, rows: 1,
autofocus: card.title.blank? || params[:focus_on_title], rows: 1,
data: { autoresize_target: "textarea", action: "input->autoresize#resize auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %>
</h1>
<div class="card__description rich-text-content margin-block-start-half margin-block-end">
@@ -3,7 +3,7 @@
Recently closed
</button>
<dialog class="events__popup popup panel flex-column align-start gap-half fill-white shadow"
<dialog class="margin-block-start-half popup panel flex-column align-start gap-half fill-white shadow"
aria-label="Closed with reason…" aria-description="Closed with reason…"
data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
<strong class="popup__title margin-block-start-half pad-inline-half">Closed with reason…</strong>
+12 -5
View File
@@ -5,11 +5,18 @@
data: {
action: "click->dialog#open:stop keydown.meta+j@document->hotkey#click keydown.ctrl+j@document->hotkey#click",
controller: "hotkey" } do %>
<svg fill="none" height="275" viewBox="0 0 202 275" width="202" xmlns="http://www.w3.org/2000/svg">
<path d="m201 258c0 11.159-44.772 16-100 16-55.2285 0-100-4.841-100-16s44.7715-16 100-16c55.228 0 100 4.841 100 16z" fill="var(--color-ink)" opacity=".08"/>
<path clip-rule="evenodd" d="m144.805 2.24222c2.294-1.843841 5.681-1.617368 7.696.56152 2.014 2.17904 1.917 5.51121-.174 7.57326l-.209.1953-.012.0117c-.009.0075-.022.0181-.039.0332-.035.0309-.087.078-.156.1397-.138.1235-.344.309-.614.5508-.54.484-1.336 1.1979-2.353 2.1152-2.033 1.835-4.952 4.4821-8.487 7.7256-7.073 6.4897-16.602 15.357-26.441 24.8759-2.543 2.4612-4.066 5.7749-4.269 9.2891-.383 6.654-.719 11.235-1.012 17.6885 27.89.2759 50.213 1.9831 62.831 3.1963l2.598.2558.018.002c7.816.8155 13.555 7.8838 12.741 15.4687l.001.001c-3.418 33.6092-7.351 63.0722-10.988 87.6142.005.178.001.359-.012.54-.03.417-.11.819-.228 1.203-3.37 24.477-6.469 42.647-8.652 56.28-2.231 13.914-13.656 24.673-28.256 25.45h-.011c-24.343 1.247-48.8273 1.346-73.2036.126l-2.3589-.122c-14.5946-.782-26.0187-11.539-28.2492-25.453-2.1836-13.632-5.2839-31.801-8.6543-56.276-.1189-.385-.1974-.789-.2278-1.208-.0133-.182-.0178-.363-.013-.542-3.6384-24.542-7.5712-54.004-10.9942-87.6112l.0009-.001c-.814-7.5854 4.9246-14.6542 12.7413-15.4697l.0179-.002c12.5608-1.2701 37.7453-3.3397 69.6834-3.4824.3004-6.6424.644-11.4215 1.0396-18.2793.3592-6.2258 3.0569-12.1147 7.5909-16.5019 9.93-9.6079 19.535-18.5451 26.654-25.0772 3.561-3.2672 6.504-5.93628 8.557-7.78905 1.027-.9262 1.831-1.64876 2.38-2.14063.274-.24594.485-.43526.628-.56249.07-.06313.124-.111.161-.14356.018-.01633.033-.02867.043-.03711.004-.00412.007-.00855.01-.01074l.003-.00195.001-.00098zm-43.801 180.81978c-19.4 0-36.6494.529-49.0474 1.058-5.7553.246-10.4622.491-13.8495.684 3.0876 22.131 5.9039 38.413 7.9281 51.05l.0726.422c1.6067 8.833 8.807 15.28 17.7176 15.757l2.319.12c22.4196 1.123 44.9546 1.111 67.4046.099l4.644-.225c9.05-.486 16.331-7.128 17.781-16.173 2.024-12.637 4.838-28.919 7.925-51.05-3.387-.193-8.094-.438-13.849-.684-12.398-.529-29.647-1.058-49.046-1.058zm-3.9481-99.0874c-31.2286.1604-55.8254 2.1836-68.0558 3.42-1.5771.1659-2.8714 1.698-2.7906 3.2392l.0119.1494.005.0489c3.2299 31.7109 6.9166 59.5089 10.3775 83.0379.0293-.002.0589-.003.0885-.005 3.4785-.201 8.5176-.468 14.7777-.735 11.3621-.485 26.7597-.968 44.1622-1.054-.0045-.387-.0095-.861-.015-1.42-.017-1.739-.0366-4.292-.0487-7.56-.0243-6.536-.0177-13.933.1084-25.385.1704-15.474.5585-32.713 1.3789-53.7364zm11.2131.0235c-.826 21.0519-1.215 38.3339-1.386 53.8319-.125 11.393-.133 18.735-.109 25.226.012 3.245.032 5.777.049 7.495.006.614.011 1.125.016 1.527 17.212.092 32.437.571 43.698 1.052 6.26.267 11.3.534 14.779.735.029.002.057.003.086.005 3.46-23.529 7.146-51.326 10.372-83.037l.005-.0498c.176-1.5891-1.149-3.2156-2.776-3.3886-11.793-1.1922-35.083-3.1168-64.734-3.3965z" fill="var(--color-ink)" fill-rule="evenodd"/>
<path d="m101.001 183.062c19.505 0 36.849.529 49.315 1.058 5.786.246 10.519.491 13.924.684-3.103 22.131-5.933 38.413-7.968 51.05-1.458 9.045-8.779 15.687-17.879 16.173l-4.669.225c-22.573 1.012-45.231 1.024-67.7731-.099l-2.3317-.12c-8.9591-.477-16.199-6.924-17.8145-15.757l-.073-.422c-2.0352-12.637-4.867-28.919-7.9714-51.05 3.4057-.193 8.1386-.438 13.9252-.684 12.4657-.529 29.8098-1.058 49.3155-1.058z" fill="#68abe9"/>
<path d="m56.6279 193.649c2.9632-.313 5.6326 1.728 6.0947 4.602l.0376.281.2601 2.344c1.2836 11.298 2.3362 16.481 4.1927 23.957.6454 2.598.9475 4.032 1.2977 5.222.0161.055.0326.106.0475.155.8538.102 2.113.162 4.2422.244 11.6052.444 23.1581.591 34.6066.001 3.073-.159 5.693 2.172 5.854 5.206.16 3.033-2.2 5.621-5.272 5.779-11.9181.615-23.8417.456-35.6199.005-2.1972-.084-4.2851-.159-5.9622-.427-1.7844-.284-3.9749-.903-5.7881-2.707-1.6614-1.653-2.3709-3.704-2.807-5.186-.4338-1.474-.8976-3.572-1.4193-5.673-1.9779-7.965-3.1089-13.606-4.441-25.321l-.269-2.427-.0238-.282c-.1652-2.905 2.0059-5.459 4.9692-5.773z" fill="var(--color-ink-inverted)" opacity=".15"/>
<svg fill="none" height="282" viewBox="0 0 178 282" width="178" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="m14.1582 85.2218s66.3075-9.7823 75.9322-9.7823c9.6242 0 73.9516 9.7823 73.9516 9.7823-6.535 65.4602-17.171 132.0422-21.661 160.5982 0 0-16.941 11.257-51.3278 11.257s-55.9319-12.654-55.9319-12.654c-4.4901-28.557-14.4184-93.741-20.9631-159.2012z" fill="#d2edf8"/>
<path d="m14.1582 85.2218s66.3075-9.7823 75.9322-9.7823c9.6242 0 73.9516 9.7823 73.9516 9.7823-6.535 65.4602-17.171 132.0422-21.661 160.5982 0 0-16.941 11.257-51.3278 11.257s-55.9319-12.654-55.9319-12.654c-4.4901-28.557-14.4184-93.741-20.9631-159.2012z" fill="#9ee0f5"/>
<path d="m162.52 88.4775c0 4.8269-32.987 8.7398-73.6772 8.7398-40.6906 0-73.6768-3.9129-73.6768-8.7398 0-4.8268 32.9862-8.7397 73.6768-8.7397 40.6902 0 73.6772 3.9129 73.6772 8.7397z" fill="var(--color-ink-inverted)" opacity=".5"/>
<path d="m173.1 251.625c0 14.179-37.652 25.673-84.0996 25.673-46.4472 0-84.10001-11.494-84.10001-25.673s37.65281-25.673 84.10001-25.673c46.4476 0 84.0996 11.494 84.0996 25.673z" fill="#0389e7" opacity=".1"/>
<path d="m24.2217 177.256s38.4462-4.239 64.6324-4.007c26.1859.231 64.1959 4 64.1959 4l-10.523 68.435-52.3439 12.493-54.9675-12.493z" fill="#68abe9"/>
<path d="m24.2217 177.256s38.4462-4.239 64.6324-4.007c26.1859.231 64.1959 4 64.1959 4l-10.523 68.435-52.3439 12.493-54.9675-12.493z" fill="#0389e7" fill-opacity=".5"/>
<path d="m31.9728 75.6129 26.9049-2.4572 8.3153 185.5113-20.7855-7.968z" fill="var(--color-ink-inverted)" opacity=".4"/>
<g fill="var(--color-ink)">
<path d="m166.018 87.1859-29.1-7.4482-13.857 172.0003 21.825-9.873z" opacity=".4"/>
<path clip-rule="evenodd" d="m134.688 6.04931c2.622-2.02596 6.39-1.54288 8.416 1.0791 2.026 2.62197 1.543 6.38979-1.079 8.41599l-33.119 25.5928c-7.228 5.5854-9.2089 7.2942-10.2917 9.4668-1.0828 2.1727-1.2547 4.7835-1.3623 13.918l-.0694 5.8564c18.6914.3301 35.5314 1.7469 48.2624 3.7139 7.263 1.1221 13.39 2.4538 17.812 3.9404 2.173.7307 4.237 1.5967 5.866 2.669 1.262.8305 4.077 2.9626 4.077 6.6669 0 .0156-.002.0314-.002.0469 0 .016.002.0319.002.0479 0 3.7135-.923 13.4206-2.304 25.8626-1.402 12.621-3.326 28.477-5.419 44.795-4.179 32.563-9.068 67.234-11.851 81.71-.046.237-.109.468-.183.692-.032.175-.072.35-.122.524-1.229 4.292-4.359 7.657-8.069 10.23-3.752 2.601-8.563 4.727-14.062 6.419-11.014 3.389-25.643 5.296-42.3492 5.296-16.706 0-31.3348-1.907-42.3496-5.296-5.4985-1.692-10.3096-3.818-14.0615-6.419-3.71-2.573-6.8402-5.938-8.0693-10.23-.0387-.135-.0707-.271-.0987-.406-.0925-.26-.1669-.531-.2207-.81-2.783-14.476-7.674-49.147-11.8545-81.71-2.0948-16.318-4.01971-32.173-5.42185-44.794-1.3822-12.442-2.30567-22.1498-2.30567-25.8636.00001-.4033.04361-.7963.12598-1.1748.47348-3.0438 2.86076-4.8407 3.99414-5.5869 1.6287-1.0723 3.693-1.9383 5.8662-2.669 4.4217-1.4866 10.5486-2.8183 17.8115-3.9404 13.7816-2.1293 32.378-3.6122 52.9258-3.7715l.0703-5.9404c.094-7.9734.0242-13.918 2.6211-19.1289 2.597-5.2108 7.3849-8.7338 13.694-13.6094zm-45.8579 169.74969c-19.504 0-35.8061.996-47.2246 1.991-5.7088.497-10.196.994-13.25 1.366-.8757.106-1.6335.203-2.2686.286 3.2005 23.744 6.4063 45.645 8.4971 56.933l.251 1.336c.0354.102.0696.205.0996.309.2702.943 1.2026 2.441 3.7636 4.217 2.5198 1.747 6.1902 3.456 11.0284 4.945 9.6594 2.972 23.1288 4.81 39.1142 4.81 15.9852 0 29.4552-1.838 39.1142-4.81 4.838-1.489 8.509-3.198 11.028-4.945 2.561-1.776 3.494-3.274 3.764-4.217.022-.076.045-.151.069-.225.003-.013.006-.027.008-.041 2.079-10.812 5.418-33.561 8.753-58.312-.636-.083-1.395-.179-2.272-.286-3.054-.372-7.542-.869-13.25-1.366-11.419-.995-27.721-1.991-47.2249-1.991zm-73.0235-82.4509c.3681 4.4633 1.0167 10.8969 1.8907 18.7639 1.3931 12.541 3.3106 28.331 5.4004 44.608.7587 5.91 1.5387 11.876 2.3242 17.767.6532-.086 1.4316-.185 2.3301-.294 3.1063-.378 7.6495-.881 13.4189-1.384 10.5805-.922 25.2901-1.842 42.8271-1.988l.8379-71.1193c-17.783-.1251-35.3045-1.0712-48.7177-2.5948-7.1949-.8172-13.2978-1.8095-17.6455-2.957-.9507-.2509-1.8445-.5188-2.6661-.8018zm146.0444.0147c-.81.2772-1.689.5407-2.622.7871-4.348 1.1475-10.45 2.1398-17.645 2.957-12.413 1.41-28.346 2.3238-44.7471 2.5518l-.8389 71.1843c16.509.205 30.384 1.086 40.491 1.966 5.77.503 10.313 1.006 13.419 1.384.9.109 1.68.208 2.334.294.785-5.891 1.566-11.857 2.324-17.767 2.089-16.277 4.005-32.067 5.397-44.608.872-7.858 1.519-14.2865 1.888-18.7492zm-76.7983-12.042c-20.0274.1645-37.9999 1.6161-51.1162 3.6426-7.0056 1.0824-12.4271 2.2988-15.9853 3.4951-.1716.0577-.3364.1141-.4942.1699.6524.2235 1.4146.4549 2.292.6865 3.999 1.0555 9.8205 2.0164 16.9326 2.8243 13.2071 1.5001 30.5582 2.44 48.2139 2.5634zm11.8438 13.3369c16.2245-.2291 31.9375-1.1344 44.1235-2.5185 7.112-.8079 12.933-1.7688 16.932-2.8243.878-.2315 1.64-.4629 2.292-.6865-.157-.0557-.322-.1123-.493-.1699-3.558-1.1963-8.98-2.4127-15.985-3.4951-12.148-1.8768-28.46-3.2601-46.7133-3.585z" fill-rule="evenodd"/>
</g>
</svg>
<strong class="txt-medium overflow-ellipsis margin-none">Fizzy</strong>
<% end %>
@@ -7,7 +7,7 @@
<% end %>
</button>
<dialog class="events__popup popup panel flex-column align-start gap-half fill-white shadow"
<dialog class="margin-block-start-half popup panel flex-column align-start gap-half fill-white shadow"
aria-label="In stage…" aria-description="In stage…"
data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
<strong class="popup__title margin-block-start-half pad-inline-half">In stage(s)…</strong>
+12 -5
View File
@@ -5,11 +5,18 @@
data: {
action: "click->dialog#open:stop keydown.meta+j@document->hotkey#click keydown.ctrl+j@document->hotkey#click",
controller: "hotkey" } do %>
<svg fill="none" height="275" viewBox="0 0 202 275" width="202" xmlns="http://www.w3.org/2000/svg">
<path d="m201 258c0 11.159-44.772 16-100 16-55.2285 0-100-4.841-100-16s44.7715-16 100-16c55.228 0 100 4.841 100 16z" fill="var(--color-ink)" opacity=".08"/>
<path clip-rule="evenodd" d="m144.805 2.24222c2.294-1.843841 5.681-1.617368 7.696.56152 2.014 2.17904 1.917 5.51121-.174 7.57326l-.209.1953-.012.0117c-.009.0075-.022.0181-.039.0332-.035.0309-.087.078-.156.1397-.138.1235-.344.309-.614.5508-.54.484-1.336 1.1979-2.353 2.1152-2.033 1.835-4.952 4.4821-8.487 7.7256-7.073 6.4897-16.602 15.357-26.441 24.8759-2.543 2.4612-4.066 5.7749-4.269 9.2891-.383 6.654-.719 11.235-1.012 17.6885 27.89.2759 50.213 1.9831 62.831 3.1963l2.598.2558.018.002c7.816.8155 13.555 7.8838 12.741 15.4687l.001.001c-3.418 33.6092-7.351 63.0722-10.988 87.6142.005.178.001.359-.012.54-.03.417-.11.819-.228 1.203-3.37 24.477-6.469 42.647-8.652 56.28-2.231 13.914-13.656 24.673-28.256 25.45h-.011c-24.343 1.247-48.8273 1.346-73.2036.126l-2.3589-.122c-14.5946-.782-26.0187-11.539-28.2492-25.453-2.1836-13.632-5.2839-31.801-8.6543-56.276-.1189-.385-.1974-.789-.2278-1.208-.0133-.182-.0178-.363-.013-.542-3.6384-24.542-7.5712-54.004-10.9942-87.6112l.0009-.001c-.814-7.5854 4.9246-14.6542 12.7413-15.4697l.0179-.002c12.5608-1.2701 37.7453-3.3397 69.6834-3.4824.3004-6.6424.644-11.4215 1.0396-18.2793.3592-6.2258 3.0569-12.1147 7.5909-16.5019 9.93-9.6079 19.535-18.5451 26.654-25.0772 3.561-3.2672 6.504-5.93628 8.557-7.78905 1.027-.9262 1.831-1.64876 2.38-2.14063.274-.24594.485-.43526.628-.56249.07-.06313.124-.111.161-.14356.018-.01633.033-.02867.043-.03711.004-.00412.007-.00855.01-.01074l.003-.00195.001-.00098zm-43.801 180.81978c-19.4 0-36.6494.529-49.0474 1.058-5.7553.246-10.4622.491-13.8495.684 3.0876 22.131 5.9039 38.413 7.9281 51.05l.0726.422c1.6067 8.833 8.807 15.28 17.7176 15.757l2.319.12c22.4196 1.123 44.9546 1.111 67.4046.099l4.644-.225c9.05-.486 16.331-7.128 17.781-16.173 2.024-12.637 4.838-28.919 7.925-51.05-3.387-.193-8.094-.438-13.849-.684-12.398-.529-29.647-1.058-49.046-1.058zm-3.9481-99.0874c-31.2286.1604-55.8254 2.1836-68.0558 3.42-1.5771.1659-2.8714 1.698-2.7906 3.2392l.0119.1494.005.0489c3.2299 31.7109 6.9166 59.5089 10.3775 83.0379.0293-.002.0589-.003.0885-.005 3.4785-.201 8.5176-.468 14.7777-.735 11.3621-.485 26.7597-.968 44.1622-1.054-.0045-.387-.0095-.861-.015-1.42-.017-1.739-.0366-4.292-.0487-7.56-.0243-6.536-.0177-13.933.1084-25.385.1704-15.474.5585-32.713 1.3789-53.7364zm11.2131.0235c-.826 21.0519-1.215 38.3339-1.386 53.8319-.125 11.393-.133 18.735-.109 25.226.012 3.245.032 5.777.049 7.495.006.614.011 1.125.016 1.527 17.212.092 32.437.571 43.698 1.052 6.26.267 11.3.534 14.779.735.029.002.057.003.086.005 3.46-23.529 7.146-51.326 10.372-83.037l.005-.0498c.176-1.5891-1.149-3.2156-2.776-3.3886-11.793-1.1922-35.083-3.1168-64.734-3.3965z" fill="var(--color-ink)" fill-rule="evenodd"/>
<path d="m101.001 183.062c19.505 0 36.849.529 49.315 1.058 5.786.246 10.519.491 13.924.684-3.103 22.131-5.933 38.413-7.968 51.05-1.458 9.045-8.779 15.687-17.879 16.173l-4.669.225c-22.573 1.012-45.231 1.024-67.7731-.099l-2.3317-.12c-8.9591-.477-16.199-6.924-17.8145-15.757l-.073-.422c-2.0352-12.637-4.867-28.919-7.9714-51.05 3.4057-.193 8.1386-.438 13.9252-.684 12.4657-.529 29.8098-1.058 49.3155-1.058z" fill="#68abe9"/>
<path d="m56.6279 193.649c2.9632-.313 5.6326 1.728 6.0947 4.602l.0376.281.2601 2.344c1.2836 11.298 2.3362 16.481 4.1927 23.957.6454 2.598.9475 4.032 1.2977 5.222.0161.055.0326.106.0475.155.8538.102 2.113.162 4.2422.244 11.6052.444 23.1581.591 34.6066.001 3.073-.159 5.693 2.172 5.854 5.206.16 3.033-2.2 5.621-5.272 5.779-11.9181.615-23.8417.456-35.6199.005-2.1972-.084-4.2851-.159-5.9622-.427-1.7844-.284-3.9749-.903-5.7881-2.707-1.6614-1.653-2.3709-3.704-2.807-5.186-.4338-1.474-.8976-3.572-1.4193-5.673-1.9779-7.965-3.1089-13.606-4.441-25.321l-.269-2.427-.0238-.282c-.1652-2.905 2.0059-5.459 4.9692-5.773z" fill="var(--color-ink-inverted)" opacity=".15"/>
<svg fill="none" height="282" viewBox="0 0 178 282" width="178" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="m14.1582 85.2218s66.3075-9.7823 75.9322-9.7823c9.6242 0 73.9516 9.7823 73.9516 9.7823-6.535 65.4602-17.171 132.0422-21.661 160.5982 0 0-16.941 11.257-51.3278 11.257s-55.9319-12.654-55.9319-12.654c-4.4901-28.557-14.4184-93.741-20.9631-159.2012z" fill="#d2edf8"/>
<path d="m14.1582 85.2218s66.3075-9.7823 75.9322-9.7823c9.6242 0 73.9516 9.7823 73.9516 9.7823-6.535 65.4602-17.171 132.0422-21.661 160.5982 0 0-16.941 11.257-51.3278 11.257s-55.9319-12.654-55.9319-12.654c-4.4901-28.557-14.4184-93.741-20.9631-159.2012z" fill="#9ee0f5"/>
<path d="m162.52 88.4775c0 4.8269-32.987 8.7398-73.6772 8.7398-40.6906 0-73.6768-3.9129-73.6768-8.7398 0-4.8268 32.9862-8.7397 73.6768-8.7397 40.6902 0 73.6772 3.9129 73.6772 8.7397z" fill="var(--color-ink-inverted)" opacity=".5"/>
<path d="m173.1 251.625c0 14.179-37.652 25.673-84.0996 25.673-46.4472 0-84.10001-11.494-84.10001-25.673s37.65281-25.673 84.10001-25.673c46.4476 0 84.0996 11.494 84.0996 25.673z" fill="#0389e7" opacity=".1"/>
<path d="m24.2217 177.256s38.4462-4.239 64.6324-4.007c26.1859.231 64.1959 4 64.1959 4l-10.523 68.435-52.3439 12.493-54.9675-12.493z" fill="#68abe9"/>
<path d="m24.2217 177.256s38.4462-4.239 64.6324-4.007c26.1859.231 64.1959 4 64.1959 4l-10.523 68.435-52.3439 12.493-54.9675-12.493z" fill="#0389e7" fill-opacity=".5"/>
<path d="m31.9728 75.6129 26.9049-2.4572 8.3153 185.5113-20.7855-7.968z" fill="var(--color-ink-inverted)" opacity=".4"/>
<g fill="var(--color-ink)">
<path d="m166.018 87.1859-29.1-7.4482-13.857 172.0003 21.825-9.873z" opacity=".4"/>
<path clip-rule="evenodd" d="m134.688 6.04931c2.622-2.02596 6.39-1.54288 8.416 1.0791 2.026 2.62197 1.543 6.38979-1.079 8.41599l-33.119 25.5928c-7.228 5.5854-9.2089 7.2942-10.2917 9.4668-1.0828 2.1727-1.2547 4.7835-1.3623 13.918l-.0694 5.8564c18.6914.3301 35.5314 1.7469 48.2624 3.7139 7.263 1.1221 13.39 2.4538 17.812 3.9404 2.173.7307 4.237 1.5967 5.866 2.669 1.262.8305 4.077 2.9626 4.077 6.6669 0 .0156-.002.0314-.002.0469 0 .016.002.0319.002.0479 0 3.7135-.923 13.4206-2.304 25.8626-1.402 12.621-3.326 28.477-5.419 44.795-4.179 32.563-9.068 67.234-11.851 81.71-.046.237-.109.468-.183.692-.032.175-.072.35-.122.524-1.229 4.292-4.359 7.657-8.069 10.23-3.752 2.601-8.563 4.727-14.062 6.419-11.014 3.389-25.643 5.296-42.3492 5.296-16.706 0-31.3348-1.907-42.3496-5.296-5.4985-1.692-10.3096-3.818-14.0615-6.419-3.71-2.573-6.8402-5.938-8.0693-10.23-.0387-.135-.0707-.271-.0987-.406-.0925-.26-.1669-.531-.2207-.81-2.783-14.476-7.674-49.147-11.8545-81.71-2.0948-16.318-4.01971-32.173-5.42185-44.794-1.3822-12.442-2.30567-22.1498-2.30567-25.8636.00001-.4033.04361-.7963.12598-1.1748.47348-3.0438 2.86076-4.8407 3.99414-5.5869 1.6287-1.0723 3.693-1.9383 5.8662-2.669 4.4217-1.4866 10.5486-2.8183 17.8115-3.9404 13.7816-2.1293 32.378-3.6122 52.9258-3.7715l.0703-5.9404c.094-7.9734.0242-13.918 2.6211-19.1289 2.597-5.2108 7.3849-8.7338 13.694-13.6094zm-45.8579 169.74969c-19.504 0-35.8061.996-47.2246 1.991-5.7088.497-10.196.994-13.25 1.366-.8757.106-1.6335.203-2.2686.286 3.2005 23.744 6.4063 45.645 8.4971 56.933l.251 1.336c.0354.102.0696.205.0996.309.2702.943 1.2026 2.441 3.7636 4.217 2.5198 1.747 6.1902 3.456 11.0284 4.945 9.6594 2.972 23.1288 4.81 39.1142 4.81 15.9852 0 29.4552-1.838 39.1142-4.81 4.838-1.489 8.509-3.198 11.028-4.945 2.561-1.776 3.494-3.274 3.764-4.217.022-.076.045-.151.069-.225.003-.013.006-.027.008-.041 2.079-10.812 5.418-33.561 8.753-58.312-.636-.083-1.395-.179-2.272-.286-3.054-.372-7.542-.869-13.25-1.366-11.419-.995-27.721-1.991-47.2249-1.991zm-73.0235-82.4509c.3681 4.4633 1.0167 10.8969 1.8907 18.7639 1.3931 12.541 3.3106 28.331 5.4004 44.608.7587 5.91 1.5387 11.876 2.3242 17.767.6532-.086 1.4316-.185 2.3301-.294 3.1063-.378 7.6495-.881 13.4189-1.384 10.5805-.922 25.2901-1.842 42.8271-1.988l.8379-71.1193c-17.783-.1251-35.3045-1.0712-48.7177-2.5948-7.1949-.8172-13.2978-1.8095-17.6455-2.957-.9507-.2509-1.8445-.5188-2.6661-.8018zm146.0444.0147c-.81.2772-1.689.5407-2.622.7871-4.348 1.1475-10.45 2.1398-17.645 2.957-12.413 1.41-28.346 2.3238-44.7471 2.5518l-.8389 71.1843c16.509.205 30.384 1.086 40.491 1.966 5.77.503 10.313 1.006 13.419 1.384.9.109 1.68.208 2.334.294.785-5.891 1.566-11.857 2.324-17.767 2.089-16.277 4.005-32.067 5.397-44.608.872-7.858 1.519-14.2865 1.888-18.7492zm-76.7983-12.042c-20.0274.1645-37.9999 1.6161-51.1162 3.6426-7.0056 1.0824-12.4271 2.2988-15.9853 3.4951-.1716.0577-.3364.1141-.4942.1699.6524.2235 1.4146.4549 2.292.6865 3.999 1.0555 9.8205 2.0164 16.9326 2.8243 13.2071 1.5001 30.5582 2.44 48.2139 2.5634zm11.8438 13.3369c16.2245-.2291 31.9375-1.1344 44.1235-2.5185 7.112-.8079 12.933-1.7688 16.932-2.8243.878-.2315 1.64-.4629 2.292-.6865-.157-.0557-.322-.1123-.493-.1699-3.558-1.1963-8.98-2.4127-15.985-3.4951-12.148-1.8768-28.46-3.2601-46.7133-3.585z" fill-rule="evenodd"/>
</g>
</svg>
<strong class="txt-medium overflow-ellipsis margin-none">Fizzy</strong>
<% end %>
+1 -1
View File
@@ -6,6 +6,6 @@
<% if command.undoable? %>
<%= button_to "Undo", command_undo_path(command), class: "btn btn--plain terminal__button flex-item-justify-end",
data: { turbo_confirm: "Are you sure you want to undo '#{command.title}'?", turbo_frame: "_top" } %>
data: { action: "toggle-class#remove", turbo_confirm: "Are you sure you want to undo '#{command.title}'?", turbo_frame: "_top" } %>
<% end %>
<% end %>
+8
View File
@@ -18,11 +18,19 @@
<dd>Clear all filters</dd>
<h3>Cards</h3>
<dt><code>/add_card [title]</code></dt>
<dd>Create a new card</dd>
<dt><code>/assign [person] [person]…</code></dt>
<dd>Assign visible card(s)</dd>
<dt><code>/close [reason]</code></dt>
<dd>Close visible card(s)</dd>
<dt><code>/tag [tag]</code></dt>
<dd>Tag visible card(s)</dd>
<dt><code>/do</code></dt>
<dd>Move card(s) to doing</dd>
<dt><code>/reconsider</code></dt>
<dd>Move card(s) back to considering</dd>
<dt><code>/stage [workflow stage name]</code></dt>
<dd>Assign card(s) to a workflow stage</dd>
</dl>
</section>
+14 -7
View File
@@ -4,16 +4,23 @@
data: {
action: "click->dialog#open:stop keydown.meta+j@document->hotkey#click keydown.ctrl+j@document->hotkey#click",
controller: "hotkey" } do %>
<svg fill="none" height="275" viewBox="0 0 202 275" width="202" xmlns="http://www.w3.org/2000/svg">
<path d="m201 258c0 11.159-44.772 16-100 16-55.2285 0-100-4.841-100-16s44.7715-16 100-16c55.228 0 100 4.841 100 16z" fill="var(--color-ink)" opacity=".08"/>
<path clip-rule="evenodd" d="m144.805 2.24222c2.294-1.843841 5.681-1.617368 7.696.56152 2.014 2.17904 1.917 5.51121-.174 7.57326l-.209.1953-.012.0117c-.009.0075-.022.0181-.039.0332-.035.0309-.087.078-.156.1397-.138.1235-.344.309-.614.5508-.54.484-1.336 1.1979-2.353 2.1152-2.033 1.835-4.952 4.4821-8.487 7.7256-7.073 6.4897-16.602 15.357-26.441 24.8759-2.543 2.4612-4.066 5.7749-4.269 9.2891-.383 6.654-.719 11.235-1.012 17.6885 27.89.2759 50.213 1.9831 62.831 3.1963l2.598.2558.018.002c7.816.8155 13.555 7.8838 12.741 15.4687l.001.001c-3.418 33.6092-7.351 63.0722-10.988 87.6142.005.178.001.359-.012.54-.03.417-.11.819-.228 1.203-3.37 24.477-6.469 42.647-8.652 56.28-2.231 13.914-13.656 24.673-28.256 25.45h-.011c-24.343 1.247-48.8273 1.346-73.2036.126l-2.3589-.122c-14.5946-.782-26.0187-11.539-28.2492-25.453-2.1836-13.632-5.2839-31.801-8.6543-56.276-.1189-.385-.1974-.789-.2278-1.208-.0133-.182-.0178-.363-.013-.542-3.6384-24.542-7.5712-54.004-10.9942-87.6112l.0009-.001c-.814-7.5854 4.9246-14.6542 12.7413-15.4697l.0179-.002c12.5608-1.2701 37.7453-3.3397 69.6834-3.4824.3004-6.6424.644-11.4215 1.0396-18.2793.3592-6.2258 3.0569-12.1147 7.5909-16.5019 9.93-9.6079 19.535-18.5451 26.654-25.0772 3.561-3.2672 6.504-5.93628 8.557-7.78905 1.027-.9262 1.831-1.64876 2.38-2.14063.274-.24594.485-.43526.628-.56249.07-.06313.124-.111.161-.14356.018-.01633.033-.02867.043-.03711.004-.00412.007-.00855.01-.01074l.003-.00195.001-.00098zm-43.801 180.81978c-19.4 0-36.6494.529-49.0474 1.058-5.7553.246-10.4622.491-13.8495.684 3.0876 22.131 5.9039 38.413 7.9281 51.05l.0726.422c1.6067 8.833 8.807 15.28 17.7176 15.757l2.319.12c22.4196 1.123 44.9546 1.111 67.4046.099l4.644-.225c9.05-.486 16.331-7.128 17.781-16.173 2.024-12.637 4.838-28.919 7.925-51.05-3.387-.193-8.094-.438-13.849-.684-12.398-.529-29.647-1.058-49.046-1.058zm-3.9481-99.0874c-31.2286.1604-55.8254 2.1836-68.0558 3.42-1.5771.1659-2.8714 1.698-2.7906 3.2392l.0119.1494.005.0489c3.2299 31.7109 6.9166 59.5089 10.3775 83.0379.0293-.002.0589-.003.0885-.005 3.4785-.201 8.5176-.468 14.7777-.735 11.3621-.485 26.7597-.968 44.1622-1.054-.0045-.387-.0095-.861-.015-1.42-.017-1.739-.0366-4.292-.0487-7.56-.0243-6.536-.0177-13.933.1084-25.385.1704-15.474.5585-32.713 1.3789-53.7364zm11.2131.0235c-.826 21.0519-1.215 38.3339-1.386 53.8319-.125 11.393-.133 18.735-.109 25.226.012 3.245.032 5.777.049 7.495.006.614.011 1.125.016 1.527 17.212.092 32.437.571 43.698 1.052 6.26.267 11.3.534 14.779.735.029.002.057.003.086.005 3.46-23.529 7.146-51.326 10.372-83.037l.005-.0498c.176-1.5891-1.149-3.2156-2.776-3.3886-11.793-1.1922-35.083-3.1168-64.734-3.3965z" fill="var(--color-ink)" fill-rule="evenodd"/>
<path d="m101.001 183.062c19.505 0 36.849.529 49.315 1.058 5.786.246 10.519.491 13.924.684-3.103 22.131-5.933 38.413-7.968 51.05-1.458 9.045-8.779 15.687-17.879 16.173l-4.669.225c-22.573 1.012-45.231 1.024-67.7731-.099l-2.3317-.12c-8.9591-.477-16.199-6.924-17.8145-15.757l-.073-.422c-2.0352-12.637-4.867-28.919-7.9714-51.05 3.4057-.193 8.1386-.438 13.9252-.684 12.4657-.529 29.8098-1.058 49.3155-1.058z" fill="#68abe9"/>
<path d="m56.6279 193.649c2.9632-.313 5.6326 1.728 6.0947 4.602l.0376.281.2601 2.344c1.2836 11.298 2.3362 16.481 4.1927 23.957.6454 2.598.9475 4.032 1.2977 5.222.0161.055.0326.106.0475.155.8538.102 2.113.162 4.2422.244 11.6052.444 23.1581.591 34.6066.001 3.073-.159 5.693 2.172 5.854 5.206.16 3.033-2.2 5.621-5.272 5.779-11.9181.615-23.8417.456-35.6199.005-2.1972-.084-4.2851-.159-5.9622-.427-1.7844-.284-3.9749-.903-5.7881-2.707-1.6614-1.653-2.3709-3.704-2.807-5.186-.4338-1.474-.8976-3.572-1.4193-5.673-1.9779-7.965-3.1089-13.606-4.441-25.321l-.269-2.427-.0238-.282c-.1652-2.905 2.0059-5.459 4.9692-5.773z" fill="var(--color-ink-inverted)" opacity=".15"/>
<svg fill="none" height="282" viewBox="0 0 178 282" width="178" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="m14.1582 85.2218s66.3075-9.7823 75.9322-9.7823c9.6242 0 73.9516 9.7823 73.9516 9.7823-6.535 65.4602-17.171 132.0422-21.661 160.5982 0 0-16.941 11.257-51.3278 11.257s-55.9319-12.654-55.9319-12.654c-4.4901-28.557-14.4184-93.741-20.9631-159.2012z" fill="#d2edf8"/>
<path d="m14.1582 85.2218s66.3075-9.7823 75.9322-9.7823c9.6242 0 73.9516 9.7823 73.9516 9.7823-6.535 65.4602-17.171 132.0422-21.661 160.5982 0 0-16.941 11.257-51.3278 11.257s-55.9319-12.654-55.9319-12.654c-4.4901-28.557-14.4184-93.741-20.9631-159.2012z" fill="#9ee0f5"/>
<path d="m162.52 88.4775c0 4.8269-32.987 8.7398-73.6772 8.7398-40.6906 0-73.6768-3.9129-73.6768-8.7398 0-4.8268 32.9862-8.7397 73.6768-8.7397 40.6902 0 73.6772 3.9129 73.6772 8.7397z" fill="var(--color-ink-inverted)" opacity=".5"/>
<path d="m173.1 251.625c0 14.179-37.652 25.673-84.0996 25.673-46.4472 0-84.10001-11.494-84.10001-25.673s37.65281-25.673 84.10001-25.673c46.4476 0 84.0996 11.494 84.0996 25.673z" fill="#0389e7" opacity=".1"/>
<path d="m24.2217 177.256s38.4462-4.239 64.6324-4.007c26.1859.231 64.1959 4 64.1959 4l-10.523 68.435-52.3439 12.493-54.9675-12.493z" fill="#68abe9"/>
<path d="m24.2217 177.256s38.4462-4.239 64.6324-4.007c26.1859.231 64.1959 4 64.1959 4l-10.523 68.435-52.3439 12.493-54.9675-12.493z" fill="#0389e7" fill-opacity=".5"/>
<path d="m31.9728 75.6129 26.9049-2.4572 8.3153 185.5113-20.7855-7.968z" fill="var(--color-ink-inverted)" opacity=".4"/>
<g fill="var(--color-ink)">
<path d="m166.018 87.1859-29.1-7.4482-13.857 172.0003 21.825-9.873z" opacity=".4"/>
<path clip-rule="evenodd" d="m134.688 6.04931c2.622-2.02596 6.39-1.54288 8.416 1.0791 2.026 2.62197 1.543 6.38979-1.079 8.41599l-33.119 25.5928c-7.228 5.5854-9.2089 7.2942-10.2917 9.4668-1.0828 2.1727-1.2547 4.7835-1.3623 13.918l-.0694 5.8564c18.6914.3301 35.5314 1.7469 48.2624 3.7139 7.263 1.1221 13.39 2.4538 17.812 3.9404 2.173.7307 4.237 1.5967 5.866 2.669 1.262.8305 4.077 2.9626 4.077 6.6669 0 .0156-.002.0314-.002.0469 0 .016.002.0319.002.0479 0 3.7135-.923 13.4206-2.304 25.8626-1.402 12.621-3.326 28.477-5.419 44.795-4.179 32.563-9.068 67.234-11.851 81.71-.046.237-.109.468-.183.692-.032.175-.072.35-.122.524-1.229 4.292-4.359 7.657-8.069 10.23-3.752 2.601-8.563 4.727-14.062 6.419-11.014 3.389-25.643 5.296-42.3492 5.296-16.706 0-31.3348-1.907-42.3496-5.296-5.4985-1.692-10.3096-3.818-14.0615-6.419-3.71-2.573-6.8402-5.938-8.0693-10.23-.0387-.135-.0707-.271-.0987-.406-.0925-.26-.1669-.531-.2207-.81-2.783-14.476-7.674-49.147-11.8545-81.71-2.0948-16.318-4.01971-32.173-5.42185-44.794-1.3822-12.442-2.30567-22.1498-2.30567-25.8636.00001-.4033.04361-.7963.12598-1.1748.47348-3.0438 2.86076-4.8407 3.99414-5.5869 1.6287-1.0723 3.693-1.9383 5.8662-2.669 4.4217-1.4866 10.5486-2.8183 17.8115-3.9404 13.7816-2.1293 32.378-3.6122 52.9258-3.7715l.0703-5.9404c.094-7.9734.0242-13.918 2.6211-19.1289 2.597-5.2108 7.3849-8.7338 13.694-13.6094zm-45.8579 169.74969c-19.504 0-35.8061.996-47.2246 1.991-5.7088.497-10.196.994-13.25 1.366-.8757.106-1.6335.203-2.2686.286 3.2005 23.744 6.4063 45.645 8.4971 56.933l.251 1.336c.0354.102.0696.205.0996.309.2702.943 1.2026 2.441 3.7636 4.217 2.5198 1.747 6.1902 3.456 11.0284 4.945 9.6594 2.972 23.1288 4.81 39.1142 4.81 15.9852 0 29.4552-1.838 39.1142-4.81 4.838-1.489 8.509-3.198 11.028-4.945 2.561-1.776 3.494-3.274 3.764-4.217.022-.076.045-.151.069-.225.003-.013.006-.027.008-.041 2.079-10.812 5.418-33.561 8.753-58.312-.636-.083-1.395-.179-2.272-.286-3.054-.372-7.542-.869-13.25-1.366-11.419-.995-27.721-1.991-47.2249-1.991zm-73.0235-82.4509c.3681 4.4633 1.0167 10.8969 1.8907 18.7639 1.3931 12.541 3.3106 28.331 5.4004 44.608.7587 5.91 1.5387 11.876 2.3242 17.767.6532-.086 1.4316-.185 2.3301-.294 3.1063-.378 7.6495-.881 13.4189-1.384 10.5805-.922 25.2901-1.842 42.8271-1.988l.8379-71.1193c-17.783-.1251-35.3045-1.0712-48.7177-2.5948-7.1949-.8172-13.2978-1.8095-17.6455-2.957-.9507-.2509-1.8445-.5188-2.6661-.8018zm146.0444.0147c-.81.2772-1.689.5407-2.622.7871-4.348 1.1475-10.45 2.1398-17.645 2.957-12.413 1.41-28.346 2.3238-44.7471 2.5518l-.8389 71.1843c16.509.205 30.384 1.086 40.491 1.966 5.77.503 10.313 1.006 13.419 1.384.9.109 1.68.208 2.334.294.785-5.891 1.566-11.857 2.324-17.767 2.089-16.277 4.005-32.067 5.397-44.608.872-7.858 1.519-14.2865 1.888-18.7492zm-76.7983-12.042c-20.0274.1645-37.9999 1.6161-51.1162 3.6426-7.0056 1.0824-12.4271 2.2988-15.9853 3.4951-.1716.0577-.3364.1141-.4942.1699.6524.2235 1.4146.4549 2.292.6865 3.999 1.0555 9.8205 2.0164 16.9326 2.8243 13.2071 1.5001 30.5582 2.44 48.2139 2.5634zm11.8438 13.3369c16.2245-.2291 31.9375-1.1344 44.1235-2.5185 7.112-.8079 12.933-1.7688 16.932-2.8243.878-.2315 1.64-.4629 2.292-.6865-.157-.0557-.322-.1123-.493-.1699-3.558-1.1963-8.98-2.4127-15.985-3.4951-12.148-1.8768-28.46-3.2601-46.7133-3.585z" fill-rule="evenodd"/>
</g>
</svg>
<strong class="txt-medium overflow-ellipsis margin-none">Fizzy</strong>
<% end %>
<%= tag.dialog class: "fizzy-dialog filter events__popup popup popup--animated panel flex-column align-start fill-white",
<%= tag.dialog class: "fizzy-dialog filter margin-block-start-half popup popup--animated panel flex-column align-start fill-white",
data: {
action: "turbo:before-cache@document->dialog#close keydown->navigable-list#navigate filter:changed->navigable-list#reset toggle->filter#filter",
controller: "filter navigable-list",
@@ -25,12 +32,12 @@
<%= render "events/filter/header" %>
<strong class="popup__group-title">Collections</strong>
<%= form_with url: events_path, method: :get, class: "popup__list",
data: { controller: "form" } do |form| %>
<% if Current.user.collections.many? %>
<ul class="popup__list">
<%= render "events/filter/custom_collections" %>
<strong class="popup__group-title">Collections</strong>
<%= render "events/filter/all_collections_option", form: form, filter: filter %>
<%= render partial: "events/filter/collection_option", collection: Current.user.collections.ordered_by_recently_accessed, as: :collection, locals: { form: form, filter: filter } %>
</ul>
@@ -1,11 +1,22 @@
<div class="popup__list">
<% @filters.each do |filter| %>
<div class="popup__group flex align-center full-width gap-half overflow-ellipsis" data-filter-target="item" data-navigable-list-target="item">
<%= link_to cards_path(**filter.as_params), class: "btn popup__new popup__item" do %>
<%= icon_tag "filter" %>
<span class="overflow-ellipsis"><%= filter.summary %></span>
<span class="translucent flex-item-no-shrink flex-item-justify-end"> </span>
<% end %>
<strong class="popup__group-title">Saved filters</strong>
<div class="popup__group flex align-center full-width gap-half overflow-ellipsis" data-navigable-list-target="item">
<%= link_to cards_path(expand_all: true), class: "btn popup__new popup__item" do %>
<%= icon_tag "bookmark" %>
<div class="flex flex-column txt-tight-lines min-width txt-small">
<strong class="overflow-ellipsis">Create a new filter</strong>
</div>
<span class="translucent flex-item-no-shrink flex-item-justify-end"> </span>
<% end %>
</div>
<% @filters.each do |filter| %>
<div class="popup__group flex align-center full-width gap-half overflow-ellipsis" data-navigable-list-target="item">
<%= link_to cards_path(filter_id: filter.id), class: "btn popup__new popup__item" do %>
<%= icon_tag "bookmark" %>
<div class="flex flex-column txt-tight-lines min-width txt-small">
<span class="overflow-ellipsis"><%= filter_selected_collections_sentence(filter).html_safe %></span>
<span class="overflow-ellipsis"><%= filter.summary %></span>
</div>
<span class="translucent flex-item-no-shrink flex-item-justify-end"> </span>
<% end %>
</div>
<% end %>
+69 -37
View File
@@ -1,53 +1,85 @@
<% if filter.assignees.any? %>
<% if filter.assignees.any? || @expand_all %>
<div class="quick-filter position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn input input--select flex-inline txt-x-small" data-action="click->dialog#open:stop">
<span class="overflow-ellipsis">
<% if filter.assignment_status.unassigned? %>
No one
<% elsif filter.assignees.any? %>
Assigned to <%= filter.assignees.map(&:familiar_name).to_sentence %>
Assigned to <%= filter.assignees.map(&:familiar_name).to_sentence(two_words_connector: " or ", last_word_connector: ", or ") %>
<% else %>
Assigned to…
<% end %>
</span>
</button>
<dialog class="events__popup popup panel flex-column align-start gap-half fill-white shadow" data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
<strong class="popup__title margin-block-start-half pad-inline-half overflow-ellipsis">Assigned to…</strong>
<%= form_with url: cards_path, method: :get, class: "popup__list",
data: { controller: "form" } do |form| %>
<% filter.as_params.except(:assignee_ids, :assignment_status).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<%= tag.dialog class: "margin-block-start-half popup panel flex-column align-start gap-half fill-white shadow txt-small", data: {
action: "turbo:before-cache@document->dialog#close keydown->navigable-list#navigate filter:changed->navigable-list#reset toggle->filter#filter",
aria: { label: "Assigned to…", aria_description: "Assigned to…" },
controller: "filter navigable-list",
dialog_target: "dialog",
navigable_list_focus_on_selection_value: false,
navigable_list_actionable_items_value: true } do %>
<strong class="popup__title txt-nowrap">Assigned to…</strong>
<%= link_to cards_path(filter.as_params.except(:assignee_ids, :assignment_status)), class: "btn popup__item" do %>
<span class="overflow-ellipsis">Clear all</span>
<% end %>
<div class="btn popup__item">
<%= form.check_box "assignment_status", {
checked: filter.assignment_status.unassigned?,
data: { action: "change->form#submit" },
include_hidden: false,
}, "unassigned" %>
<%= form.label :assignment_status, "No one", class: "overflow-ellipsis" %>
<%= icon_tag "check", class: "checked flex-item-justify-end" %>
</div>
<% User.active.order(:name).each do |user| %>
<div class="btn popup__item">
<%= form.check_box "assignee_ids[]", {
checked: filter.assignees.include?(user),
data: { action: "change->form#submit" },
include_hidden: false,
}, user.id %>
<%= form.label "assignee_ids[]", user.name, for: dom_id(user, :filter), class: "overflow-ellipsis" %>
<%= icon_tag "check", class: "checked flex-item-justify-end" %>
</div>
<% end %>
<% if User.active.many? %>
<%= text_field_tag :search, nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
<% end %>
</dialog>
<ul class="popup__list" data-filter-target="list">
<li class="popup__group flex align-center" data-value="no one" data-filter-target="item" data-navigable-list-target="item">
<%= form_with url: cards_path, method: :get, class: "full-width", data: { controller: "form" } do |form| %>
<% filter.as_params.except(:assignee_ids, :assignment_status).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<% unless filter.assignment_status.unassigned? %>
<%= hidden_field_tag :assignment_status, "unassigned" %>
<% end %>
<% if @expand_all %>
<%= hidden_field_tag :expand_all, @expand_all %>
<% end %>
<%= form.button type: "submit", class: "btn popup__item min-width full-width unpad-inline" do %>
<span class="overflow-ellipsis flex-item-grow">No one</span>
<% if filter.assignment_status.unassigned? %>
<%= icon_tag "check", class: "checked flex-item-justify-end" %>
<% end %>
<% end %>
<% end %>
</li>
<% User.active.order(:name).each do |user| %>
<li class="popup__group flex align-center" data-value="<%= user.name.downcase %>" data-filter-target="item" data-navigable-list-target="item">
<%= form_with url: cards_path, method: :get, class: "full-width", data: { controller: "form" } do |form| %>
<% filter.as_params.except(:assignee_ids, :assignment_status).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<% filter.assignees.each do |existing_assignee| %>
<% unless existing_assignee == user %>
<%= hidden_field_tag "assignee_ids[]", existing_assignee.id %>
<% end %>
<% end %>
<% unless filter.assignees.include?(user) %>
<%= hidden_field_tag "assignee_ids[]", user.id %>
<% end %>
<% if @expand_all %>
<%= hidden_field_tag :expand_all, @expand_all %>
<% end %>
<%= form.button type: "submit", class: "btn popup__item min-width full-width unpad-inline" do %>
<span class="overflow-ellipsis flex-item-grow"><%= user.name %></span>
<% if filter.assignees.include?(user) %>
<%= icon_tag "check", class: "checked flex-item-justify-end" %>
<% end %>
<% end %>
<% end %>
</li>
<% end %>
</ul>
<% end %>
</div>
<% end %>
+47 -41
View File
@@ -1,56 +1,62 @@
<% if filter.creators.any? %>
<% if filter.creators.any? || @expand_all %>
<div class="quick-filter position-relative" data-controller="dialog"
data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn input input--select flex-inline txt-x-small" data-action="click->dialog#open:stop">
<span class="overflow-ellipsis">
<% if filter.creators.any? %>
<%= "Added by #{filter.creators.map(&:familiar_name).to_sentence}" %>
<%= "Added by #{filter.creators.map(&:familiar_name).to_sentence(two_words_connector: " or ", last_word_connector: ", or ")}" %>
<% else %>
Added by…
<% end %>
</span>
</button>
<dialog class="events__popup popup panel flex-column align-start gap-half fill-white shadow"
aria-label="Added by…" aria-description="Added by…"
data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
<strong class="popup__title margin-block-start-half pad-inline-half">Added by…</strong>
<%= form_with url: cards_path, method: :get, class: "popup__list",
data: { controller: "form" } do |form| %>
<% filter.as_params.except(:creator_ids).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<%= tag.dialog class: "margin-block-start-half popup panel flex-column align-start gap-half fill-white shadow txt-small", data: {
action: "turbo:before-cache@document->dialog#close keydown->navigable-list#navigate filter:changed->navigable-list#reset toggle->filter#filter",
aria: { label: "Added by…", aria_description: "Added by…" },
controller: "filter navigable-list",
dialog_target: "dialog",
navigable_list_focus_on_selection_value: false,
navigable_list_actionable_items_value: true } do %>
<strong class="popup__title txt-nowrap">Added by…</strong>
<%= link_to cards_path(filter.as_params.except(:creator_ids)), class: "btn popup__item" do %>
<span class="overflow-ellipsis">Clear all</span>
<% end %>
<div class="btn popup__item">
<%= form.check_box :creator_ids, {
multiple: true,
checked: filter.creators.include?(Current.user),
data: { action: "change->form#submit" },
include_hidden: false,
}, Current.user.id %>
<%= form.label :creator_ids, "Me", for: form.field_id(:creator_ids, Current.user.id), class: "overflow-ellipsis" %>
<%= icon_tag "check", class: "checked flex-item-justify-end" %>
</div>
<% User.active.without(Current.user).order(:name).each do |user| %>
<div class="btn popup__item">
<%= form.check_box :creator_ids, {
multiple: true,
checked: filter.creators.include?(user),
data: { action: "change->form#submit" },
include_hidden: false,
}, user.id %>
<%= form.label :creator_ids, user.name, for: form.field_id(:creator_ids, user.id), class: "overflow-ellipsis" %>
<%= icon_tag "check", class: "checked flex-item-justify-end" %>
</div>
<% end %>
<% if User.active.many? %>
<%= text_field_tag :search, nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
<% end %>
</dialog>
<ul class="popup__list" data-filter-target="list">
<% User.active.order(:name).each do |user| %>
<li class="popup__group flex align-center" data-value="<%= user.name.downcase %>" data-filter-target="item" data-navigable-list-target="item">
<%= form_with url: cards_path, method: :get, class: "full-width", data: { controller: "form" } do |form| %>
<% filter.as_params.except(:creator_ids).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<% filter.creators.each do |existing_creators| %>
<% unless existing_creators == user %>
<%= hidden_field_tag "creator_ids[]", existing_creators.id %>
<% end %>
<% end %>
<% unless filter.creators.include?(user) %>
<%= hidden_field_tag "creator_ids[]", user.id %>
<% end %>
<% if @expand_all %>
<%= hidden_field_tag :expand_all, @expand_all %>
<% end %>
<%= form.button type: "submit", class: "btn popup__item min-width full-width unpad-inline" do %>
<span class="overflow-ellipsis flex-item-grow"><%= user.name %></span>
<% if filter.creators.include?(user) %>
<%= icon_tag "check", class: "checked flex-item-justify-end" %>
<% end %>
<% end %>
<% end %>
</li>
<% end %>
</ul>
<% end %>
</div>
<% end %>
+1 -1
View File
@@ -1,5 +1,5 @@
<%# Don't delete this file just yet, it's coming back soon. %>
<dialog class="filter__popup dialog panel fill-white shadow" aria-label="Filter" aria-description="Filter" data-dialog-target="dialog">
<dialog class="dialog panel fill-white shadow" aria-label="Filter" aria-description="Filter" data-dialog-target="dialog">
<div class="flex flex-column gap-half">
<div class="flex align-center gap-half justify-space-between">
<span class="btn btn--placeholder txt-small" aria-hidden="true"></span>
+34 -22
View File
@@ -1,32 +1,44 @@
<% if !filter.indexed_by.latest? %>
<div class="quick-filter position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn input input--select flex-inline txt-small" data-action="click->dialog#open:stop">
<% if !filter.indexed_by.latest? || @expand_all %>
<div class="quick-filter position-relative <%= "default-value" if filter.indexed_by.latest? %>" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn input input--select flex-inline txt-x-small" data-action="click->dialog#open:stop">
<span class="overflow-ellipsis">
<%= filter.indexed_by.humanize %>
</span>
</button>
<dialog class="events__popup popup panel flex-column align-start gap-half fill-white shadow"
aria-label="Sort by…" aria-description="Sort by…"
data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
<strong class="popup__title margin-block-start-half pad-inline-half">Sort by…</strong>
<%= form_with url: cards_path, method: :get, class: "popup__list",
data: { controller: "form" } do |form| %>
<% filter.as_params.except(:indexed_by).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<%= tag.dialog class: "margin-block-start-half popup panel flex-column align-start gap-half fill-white shadow txt-small", data: {
action: "turbo:before-cache@document->dialog#close keydown->navigable-list#navigate",
aria: { label: "Sort by…", aria_description: "Sort by…" },
controller: "navigable-list",
dialog_target: "dialog",
navigable_list_focus_on_selection_value: false,
navigable_list_actionable_items_value: true } do %>
<strong class="popup__title txt-nowrap">Sort by…</strong>
<ul class="popup__list" data-filter-target="list">
<% Filter::INDEXES.each do |index| %>
<div class="btn popup__item">
<%= form.radio_button :indexed_by, index,
checked: filter.indexed_by == index,
data: { action: "change->form#submit" } %>
<li class="popup__group flex align-center" data-navigable-list-target="item">
<%= form_with url: cards_path, method: :get, class: "full-width", data: { controller: "form" } do |form| %>
<% filter.as_params.except(:indexed_by).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<%= form.label :indexed_by, index.humanize, value: index, class: "overflow-ellipsis" %>
<%= icon_tag "check", class: "checked flex-item-justify-end" %>
</div>
<%= hidden_field_tag :indexed_by, index %>
<% if @expand_all %>
<%= hidden_field_tag :expand_all, @expand_all %>
<% end %>
<%= form.button type: "submit", class: "btn popup__item min-width full-width unpad-inline" do %>
<span class="overflow-ellipsis flex-item-grow"><%= index.humanize %></span>
<% if filter.indexed_by == index %>
<%= icon_tag "check", class: "checked flex-item-justify-end" %>
<% end %>
<% end %>
<% end %>
</li>
<% end %>
<% end %>
</dialog>
</ul>
<% end %>
</div>
<% end %>
+34 -3
View File
@@ -1,18 +1,49 @@
<aside class="filters flex align-center gap-half justify-center center margin-block-end">
<div class="flex-inline center align-center gap-half">
<% if any_filters?(filter) %>
<% if @expand_all %>
<%= link_to cards_path(filter.as_params), class: "btn txt-x-small btn--reversed", aria: { selected: true} do %>
<%= icon_tag "filter" %>
<span class="for-screen-reader">Edit filters</span>
<% end %>
<% else %>
<%= link_to cards_path(filter.as_params.merge(expand_all: true)), class: "btn txt-x-small" do %>
<%= icon_tag "filter" %>
<span class="for-screen-reader">Edit filters</span>
<% end %>
<% end %>
<% end %>
<%= render "filters/indexed_by", filter: filter %>
<%= render "filters/tags", filter: filter %>
<%= render "filters/assignees", filter: filter %>
<%= render "filters/creators", filter: filter %>
<%= render "filters/stages", filter: filter %>
<%= render "filters/time_window", filter: filter, name: :creation, label: "Created" %>
<%= render "filters/time_window", filter: filter, name: :closure, label: "Closed" %>
<%# FIXME LATER: this needs to be able to allow selection of unique stages across all active workflows %>
<%#= render "filters/stages", filter: filter %>
<%= render "filters/cards", filter: filter %>
<%= render "filters/indexed_by", filter: filter %>
<% filter.terms.each do |term| %>
<%= filter_chip_tag %Q("#{term}"), filter.as_params_without(:terms, term) %>
<% end %>
<% if any_filters?(filter) %>
<%= link_to cards_path(filter.as_params.except(:assignee_ids, :assignment_status, :card_ids, :creator_ids, :stage_ids, :tag_ids, :terms, :indexed_by)), class: "btn btn--remove txt-x-small" do %>
<% if filter.persisted? %>
<%= button_to filter_path(filter), method: :delete, class: "btn txt-x-small btn--reversed", form_class: "inline" do %>
<%= icon_tag "bookmark" %>
<span class="for-screen-reader">Remove saved filter</span>
<% end %>
<% else %>
<% filter_params = filter.as_params.transform_values { |v| v.respond_to?(:to_str) ? v.to_str : v } %>
<%= button_to filters_path, method: :post, params: filter_params, class: "btn txt-x-small", form_class: "inline" do %>
<%= icon_tag "bookmark" %>
<span class="for-screen-reader">Save filter</span>
<% end %>
<% end %>
<%= link_to cards_path(filter.as_params.except(:assignee_ids, :assignment_status, :card_ids, :creator_ids, :stage_ids, :tag_ids, :terms, :indexed_by, :creation, :closure)), class: "btn btn--remove txt-x-small" do %>
<%= icon_tag "close" %>
<span class="for-screen-reader">Clear all</span>
<% end %>
+6 -2
View File
@@ -1,4 +1,4 @@
<% if filter.stages.any? %>
<% if filter.stages.any? || @expand_all %>
<div class="quick-filter position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn input input--select flex-inline txt-x-small" data-action="click->dialog#open:stop">
<span class="overflow-ellipsis">
@@ -10,7 +10,7 @@
</span>
</button>
<dialog class="events__popup popup panel flex-column align-start gap-half fill-white shadow"
<dialog class="margin-block-start-half popup panel flex-column align-start gap-half fill-white shadow"
aria-label="In stage…" aria-description="In stage…"
data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
<strong class="popup__title margin-block-start-half pad-inline-half overflow-ellipsis">In stage…</strong>
@@ -20,6 +20,10 @@
<%= filter_hidden_field_tag key, value %>
<% end %>
<% if @expand_all %>
<%= hidden_field_tag :expand_all, @expand_all %>
<% end %>
<% filter.stages.first.workflow.stages.each do |stage| %>
<div class="btn popup__item">
<%= form.check_box :stage_ids, {
+47 -26
View File
@@ -1,40 +1,61 @@
<% if filter.tags.any? %>
<% if filter.tags.any?|| @expand_all %>
<div class="quick-filter position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn input input--select flex-inline txt-x-small" data-action="click->dialog#open:stop">
<span class="overflow-ellipsis">
<% if filter.tags.any? %>
<%= filter.tags.map(&:hashtag).to_sentence %>
<%= filter.tags.map(&:hashtag).to_sentence(two_words_connector: " or ", last_word_connector: ", or ") %>
<% else %>
Tagged…
<% end %>
</span>
</button>
<dialog class="events__popup popup panel flex-column align-start gap-half fill-white shadow" data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
<strong class="popup__title margin-block-start-half pad-inline-half overflow-ellipsis">Tagged…</strong>
<%= form_with url: cards_path, method: :get, class: "popup__list",
data: { controller: "form" } do |form| %>
<% filter.as_params.except(:tag_ids).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<%= tag.dialog class: "margin-block-start-half popup panel flex-column align-start gap-half fill-white shadow txt-small", data: {
action: "turbo:before-cache@document->dialog#close keydown->navigable-list#navigate filter:changed->navigable-list#reset toggle->filter#filter",
aria: { label: "Tagged…", aria_description: "Tagged…" },
controller: "filter navigable-list",
dialog_target: "dialog",
navigable_list_focus_on_selection_value: false,
navigable_list_actionable_items_value: true } do %>
<strong class="popup__title txt-nowrap">Tagged…</strong>
<%= link_to cards_path(filter.as_params.except(:tag_ids)), class: "btn popup__item" do %>
<span class="overflow-ellipsis">Clear all</span>
<% end %>
<% Tag.order(:title).each do |tag| %>
<div class="btn popup__item">
<%= form.check_box "tag_ids[]", {
checked: filter.tags.include?(tag),
data: { action: "change->form#submit" },
include_hidden: false,
}, tag.id %>
<%= form.label "tag_ids[]", tag.hashtag, for: dom_id(tag, :filter), class: "overflow-ellipsis" %>
<%= icon_tag "check", class: "checked flex-item-justify-end" %>
</div>
<% end %>
<% if Tag.many? %>
<%= text_field_tag :search, nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
<% end %>
</dialog>
<ul class="popup__list" data-filter-target="list">
<% Tag.order(:title).each do |tag| %>
<li class="popup__group flex align-center" data-value="<%= tag.title.downcase %>" data-filter-target="item" data-navigable-list-target="item">
<%= form_with url: cards_path, method: :get, class: "full-width", data: { controller: "form" } do |form| %>
<% filter.as_params.except(:tag_ids).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<% filter.tags.each do |existing_tag| %>
<% unless existing_tag == tag %>
<%= hidden_field_tag "tag_ids[]", existing_tag.id %>
<% end %>
<% end %>
<% unless filter.tags.include?(tag) %>
<%= hidden_field_tag "tag_ids[]", tag.id %>
<% end %>
<% if @expand_all %>
<%= hidden_field_tag :expand_all, @expand_all %>
<% end %>
<%= form.button type: "submit", class: "btn popup__item min-width full-width unpad-inline" do %>
<span class="overflow-ellipsis flex-item-grow"><%= tag.hashtag %></span>
<% if filter.tags.include?(tag) %>
<%= icon_tag "check", class: "checked flex-item-justify-end" %>
<% end %>
<% end %>
<% end %>
</li>
<% end %>
</ul>
<% end %>
</div>
<% end %>
+32
View File
@@ -0,0 +1,32 @@
<% if filter.public_send("#{name}_window").present? %>
<div class="quick-filter position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn input input--select flex-inline txt-x-small" data-action="click->dialog#open:stop">
<span class="overflow-ellipsis">
<%= "#{label} #{TimeWindowParser.human_name_for(filter.public_send(name))&.downcase}" %>
</span>
</button>
<dialog class="events__popup popup panel flex-column align-start gap-half fill-white shadow"
aria-label="Created…" aria-description="Created…"
data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
<strong class="popup__title margin-block-start-half pad-inline-half"><%= label %>…</strong>
<%= form_with url: cards_path, method: :get, class: "popup__list",
data: { controller: "form" } do |form| %>
<% filter.as_params.except(name).each do |key, value| %>
<%= filter_hidden_field_tag key, value %>
<% end %>
<% TimeWindowParser::VALUES.each do |value| %>
<div class="btn popup__item">
<%= form.radio_button name, value,
checked: filter.public_send(name) == value,
data: { action: "change->form#submit" } %>
<%= form.label name, TimeWindowParser.human_name_for(value), value: value, class: "overflow-ellipsis" %>
<%= icon_tag "check", class: "checked flex-item-justify-end" %>
</div>
<% end %>
<% end %>
</dialog>
</div>
<% end %>
+13 -1
View File
@@ -23,7 +23,19 @@
<% content_for :footer do %>
<div class="justify-center center margin-block-double flex align-end gap-half">
<%= image_tag "logo-color.svg", aria: { hidden: true }, size: 30 %>
<svg fill="none" height="30" viewBox="0 0 178 282" width="30" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="m14.1582 85.2218s66.3075-9.7823 75.9322-9.7823c9.6242 0 73.9516 9.7823 73.9516 9.7823-6.535 65.4602-17.171 132.0422-21.661 160.5982 0 0-16.941 11.257-51.3278 11.257s-55.9319-12.654-55.9319-12.654c-4.4901-28.557-14.4184-93.741-20.9631-159.2012z" fill="#d2edf8"/>
<path d="m14.1582 85.2218s66.3075-9.7823 75.9322-9.7823c9.6242 0 73.9516 9.7823 73.9516 9.7823-6.535 65.4602-17.171 132.0422-21.661 160.5982 0 0-16.941 11.257-51.3278 11.257s-55.9319-12.654-55.9319-12.654c-4.4901-28.557-14.4184-93.741-20.9631-159.2012z" fill="#9ee0f5"/>
<path d="m162.52 88.4775c0 4.8269-32.987 8.7398-73.6772 8.7398-40.6906 0-73.6768-3.9129-73.6768-8.7398 0-4.8268 32.9862-8.7397 73.6768-8.7397 40.6902 0 73.6772 3.9129 73.6772 8.7397z" fill="var(--color-ink-inverted)" opacity=".5"/>
<path d="m173.1 251.625c0 14.179-37.652 25.673-84.0996 25.673-46.4472 0-84.10001-11.494-84.10001-25.673s37.65281-25.673 84.10001-25.673c46.4476 0 84.0996 11.494 84.0996 25.673z" fill="#0389e7" opacity=".1"/>
<path d="m24.2217 177.256s38.4462-4.239 64.6324-4.007c26.1859.231 64.1959 4 64.1959 4l-10.523 68.435-52.3439 12.493-54.9675-12.493z" fill="#68abe9"/>
<path d="m24.2217 177.256s38.4462-4.239 64.6324-4.007c26.1859.231 64.1959 4 64.1959 4l-10.523 68.435-52.3439 12.493-54.9675-12.493z" fill="#0389e7" fill-opacity=".5"/>
<path d="m31.9728 75.6129 26.9049-2.4572 8.3153 185.5113-20.7855-7.968z" fill="var(--color-ink-inverted)" opacity=".4"/>
<g fill="var(--color-ink)">
<path d="m166.018 87.1859-29.1-7.4482-13.857 172.0003 21.825-9.873z" opacity=".4"/>
<path clip-rule="evenodd" d="m134.688 6.04931c2.622-2.02596 6.39-1.54288 8.416 1.0791 2.026 2.62197 1.543 6.38979-1.079 8.41599l-33.119 25.5928c-7.228 5.5854-9.2089 7.2942-10.2917 9.4668-1.0828 2.1727-1.2547 4.7835-1.3623 13.918l-.0694 5.8564c18.6914.3301 35.5314 1.7469 48.2624 3.7139 7.263 1.1221 13.39 2.4538 17.812 3.9404 2.173.7307 4.237 1.5967 5.866 2.669 1.262.8305 4.077 2.9626 4.077 6.6669 0 .0156-.002.0314-.002.0469 0 .016.002.0319.002.0479 0 3.7135-.923 13.4206-2.304 25.8626-1.402 12.621-3.326 28.477-5.419 44.795-4.179 32.563-9.068 67.234-11.851 81.71-.046.237-.109.468-.183.692-.032.175-.072.35-.122.524-1.229 4.292-4.359 7.657-8.069 10.23-3.752 2.601-8.563 4.727-14.062 6.419-11.014 3.389-25.643 5.296-42.3492 5.296-16.706 0-31.3348-1.907-42.3496-5.296-5.4985-1.692-10.3096-3.818-14.0615-6.419-3.71-2.573-6.8402-5.938-8.0693-10.23-.0387-.135-.0707-.271-.0987-.406-.0925-.26-.1669-.531-.2207-.81-2.783-14.476-7.674-49.147-11.8545-81.71-2.0948-16.318-4.01971-32.173-5.42185-44.794-1.3822-12.442-2.30567-22.1498-2.30567-25.8636.00001-.4033.04361-.7963.12598-1.1748.47348-3.0438 2.86076-4.8407 3.99414-5.5869 1.6287-1.0723 3.693-1.9383 5.8662-2.669 4.4217-1.4866 10.5486-2.8183 17.8115-3.9404 13.7816-2.1293 32.378-3.6122 52.9258-3.7715l.0703-5.9404c.094-7.9734.0242-13.918 2.6211-19.1289 2.597-5.2108 7.3849-8.7338 13.694-13.6094zm-45.8579 169.74969c-19.504 0-35.8061.996-47.2246 1.991-5.7088.497-10.196.994-13.25 1.366-.8757.106-1.6335.203-2.2686.286 3.2005 23.744 6.4063 45.645 8.4971 56.933l.251 1.336c.0354.102.0696.205.0996.309.2702.943 1.2026 2.441 3.7636 4.217 2.5198 1.747 6.1902 3.456 11.0284 4.945 9.6594 2.972 23.1288 4.81 39.1142 4.81 15.9852 0 29.4552-1.838 39.1142-4.81 4.838-1.489 8.509-3.198 11.028-4.945 2.561-1.776 3.494-3.274 3.764-4.217.022-.076.045-.151.069-.225.003-.013.006-.027.008-.041 2.079-10.812 5.418-33.561 8.753-58.312-.636-.083-1.395-.179-2.272-.286-3.054-.372-7.542-.869-13.25-1.366-11.419-.995-27.721-1.991-47.2249-1.991zm-73.0235-82.4509c.3681 4.4633 1.0167 10.8969 1.8907 18.7639 1.3931 12.541 3.3106 28.331 5.4004 44.608.7587 5.91 1.5387 11.876 2.3242 17.767.6532-.086 1.4316-.185 2.3301-.294 3.1063-.378 7.6495-.881 13.4189-1.384 10.5805-.922 25.2901-1.842 42.8271-1.988l.8379-71.1193c-17.783-.1251-35.3045-1.0712-48.7177-2.5948-7.1949-.8172-13.2978-1.8095-17.6455-2.957-.9507-.2509-1.8445-.5188-2.6661-.8018zm146.0444.0147c-.81.2772-1.689.5407-2.622.7871-4.348 1.1475-10.45 2.1398-17.645 2.957-12.413 1.41-28.346 2.3238-44.7471 2.5518l-.8389 71.1843c16.509.205 30.384 1.086 40.491 1.966 5.77.503 10.313 1.006 13.419 1.384.9.109 1.68.208 2.334.294.785-5.891 1.566-11.857 2.324-17.767 2.089-16.277 4.005-32.067 5.397-44.608.872-7.858 1.519-14.2865 1.888-18.7492zm-76.7983-12.042c-20.0274.1645-37.9999 1.6161-51.1162 3.6426-7.0056 1.0824-12.4271 2.2988-15.9853 3.4951-.1716.0577-.3364.1141-.4942.1699.6524.2235 1.4146.4549 2.292.6865 3.999 1.0555 9.8205 2.0164 16.9326 2.8243 13.2071 1.5001 30.5582 2.44 48.2139 2.5634zm11.8438 13.3369c16.2245-.2291 31.9375-1.1344 44.1235-2.5185 7.112-.8079 12.933-1.7688 16.932-2.8243.878-.2315 1.64-.4629 2.292-.6865-.157-.0557-.322-.1123-.493-.1699-3.558-1.1963-8.98-2.4127-15.985-3.4951-12.148-1.8768-28.46-3.2601-46.7133-3.585z" fill-rule="evenodd"/>
</g>
</svg>
<strong><a href="http://37signals.com" class="txt-ink">Made with Fizzy&trade;</a></strong>
</div>
<% end %>
+31 -7
View File
@@ -1,22 +1,27 @@
servers:
web:
hosts:
- fizzy.37signals.works
- fizzy-beta-app-01
jobs:
hosts:
- fizzy.37signals.works
- fizzy-beta-app-01
web-replica:
hosts:
- fizzy-beta-app-101
proxy: true
proxy:
hosts:
- fizzy.37signals.works # the single tenant is named "fizzy"
ssl: false
ssh:
user: app
env:
clear:
RAILS_ENV: beta
LOCAL_AUTHENTICATION: t
x-beamer-accessory: &beamer-accessory
image: basecamp/beamer
image: basecamp/beamer:vfs
registry:
username: bcbot
password:
@@ -26,11 +31,30 @@ x-beamer-accessory: &beamer-accessory
volumes:
- fizzy:/storage
accessories:
beamer-primary:
<<: *beamer-accessory
service: beamer-primary
cmd: beamer primary --remove-after=1h --dir=/storage
cmd: beamer primary --remove-after=1h --dir=/storage --disable-tls
port: 5000:80
roles:
- web
beamer-replica:
<<: *beamer-accessory
service: beamer-replica
cmd: beamer replica --primary=http://fizzy-beta-app-01:5000/ --dir=/storage
roles:
- web-replica
load-balancer:
image: basecamp/kamal-proxy:lb
roles:
- web
options:
publish:
- 80:80
- 443:443
volumes:
- load-balancer:/home/kamal-proxy/.config/kamal-proxy
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

+3 -17
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 117 KiB

+1
View File
@@ -3,6 +3,7 @@
require_relative "../config/environment"
CHANGES = [
{ from: "kevin@37signals.com", to: "kevin@basecamp.com" },
{ from: "david@37signals.com", to: "david@hey.com" },
{ from: "jay@37signals.com", to: "jay@basecamp.com" },
{ from: "jeremy@37signals.com", to: "jeremy@basecamp.com" },
+6 -3
View File
@@ -14,7 +14,7 @@ class FiltersControllerTest < ActionDispatch::IntegrationTest
assignee_ids: [ users(:jz).id ],
collection_ids: [ collections(:writebook).id ] }
end
assert_redirected_to cards_path(Filter.last.as_params)
assert_redirected_to cards_path(filter_id: Filter.last.id)
filter = Filter.last
assert_predicate filter.indexed_by, :closed?
@@ -25,9 +25,12 @@ class FiltersControllerTest < ActionDispatch::IntegrationTest
end
test "destroy" do
filter = filters(:jz_assignments)
expected_params = filter.as_params
assert_difference "users(:david).filters.count", -1 do
delete filter_path(filters(:jz_assignments))
delete filter_path(filter)
end
assert_redirected_to cards_path(filters(:jz_assignments).as_params)
assert_redirected_to cards_path(expected_params)
end
end
+22
View File
@@ -70,6 +70,16 @@ class Card::EntropicTest < ActiveSupport::TestCase
assert_not cards(:shipping).reload.closed?
end
test "closing soon scope" do
cards(:logo, :shipping).each(&:published!).each(&:reconsider)
cards(:logo).update!(last_active_at: entropy_configurations(:writebook_collection).auto_close_period.seconds.ago + 2.days)
cards(:shipping).update!(last_active_at: entropy_configurations(:writebook_collection).auto_close_period.seconds.ago - 2.days)
assert_includes Card.closing_soon, cards(:logo)
assert_not_includes Card.closing_soon, cards(:shipping)
end
test "auto consider all stagnated using the default account entropy configuration" do
travel_to Time.current
@@ -104,4 +114,16 @@ class Card::EntropicTest < ActiveSupport::TestCase
assert cards(:logo).reload.considering?
assert_equal Time.current, cards(:logo).last_active_at
end
test "falling back scope" do
travel_to Time.current
cards(:logo, :shipping).each(&:engage)
cards(:logo).update!(last_active_at: 1.day.ago - entropy_configurations("writebook_collection").auto_close_period)
cards(:shipping).update!(last_active_at: 1.day.from_now - entropy_configurations("writebook_collection").auto_close_period)
assert_includes Card.falling_back_soon, cards(:shipping)
assert_not_includes Card.falling_back_soon, cards(:logo)
end
end
+41
View File
@@ -0,0 +1,41 @@
require "test_helper"
class Command::AddCardTest < ActionDispatch::IntegrationTest
include CommandTestHelper
setup do
Current.session = sessions(:david)
@card = cards(:text)
end
test "create a new untitled card" do
result = assert_difference -> { users(:david).accessible_cards.count }, +1 do
execute_command "/add_card", context_url: collection_card_url(@card.collection, @card)
end
new_card = users(:david).accessible_cards.last
assert_equal "", new_card.title
assert_equal @card.collection, new_card.collection
assert_equal collection_card_path(new_card.collection, new_card, focus_on_title: true), result.url
end
test "create a new titled card" do
result = assert_difference -> { users(:david).accessible_cards.count }, +1 do
execute_command "/add_card Review report", context_url: collection_card_url(@card.collection, @card)
end
new_card = users(:david).accessible_cards.last
assert_equal "Review report", new_card.title
assert_equal @card.collection, new_card.collection
assert_equal collection_card_path(new_card.collection, new_card, focus_on_title: true), result.url
end
test "undo card creation" do
command = parse_command "/add_card", context_url: collection_cards_url(@card.collection)
command.execute
assert_difference -> { users(:david).accessible_cards.count }, -1 do
command.undo
end
end
end
+63 -5
View File
@@ -4,7 +4,7 @@ class Command::Ai::TranslatorTest < ActionDispatch::IntegrationTest
include VcrTestHelper
setup do
@user= users(:david)
@user = users(:david)
end
test "filter by assignments" do
@@ -66,7 +66,7 @@ class Command::Ai::TranslatorTest < ActionDispatch::IntegrationTest
test "assign cards" do
# List context
assert_command({ commands: [ "/assign jz" ] }, "assign to jz")
assert_command({ context: { tag_ids: [ "design" ] }, commands: [ "/assign jz" ] }, "assign cards agged with #design to jz", context: :card)
assert_command({ context: { tag_ids: [ "design" ] }, commands: [ "/assign jz" ] }, "assign cards tagged with #design to jz", context: :card)
end
test "tag cards" do
@@ -74,10 +74,68 @@ class Command::Ai::TranslatorTest < ActionDispatch::IntegrationTest
assert_command({ commands: [ "/tag #design" ] }, "tag with #design")
end
test "move cards between considering and doing" do
assert_command({ commands: [ "/consider" ] }, "consider")
assert_command({ commands: [ "/consider" ] }, "move to consider")
assert_command({ commands: [ "/do" ] }, "do")
assert_command({ commands: [ "/do" ] }, "move to doing")
end
test "assign stages to card" do
assert_command({ commands: [ "/stage in progress" ] }, "move to stage in progress")
assert_command({ commands: [ "/stage in progress" ] }, "move to in progress")
end
test "combine commands and filters" do
assert_command({ context: { assignee_ids: [ "jz" ], tag_ids: [ "design" ] }, commands: [ "/assign andy", "/tag #v2" ] }, "assign andy to the current #design cards assigned to jz and tag them with #v2")
assert_command({ context: { assignee_ids: [ "andy" ] }, commands: [ "/close", "/assign kevin" ] }, "close cards assigned to andy and assign them to kevin")
assert_command({ context: { tag_ids: [ "design" ], assignee_ids: [ "jz" ] }, commands: [ "/assign andy", "/tag #v2" ] }, "assign cards tagged with #design assigned to jz to andy and tag them with #v2")
assert_command(
{ context: { card_ids: [ 176, 170 ] }, commands: [ "/do", "/assign david", "/stage Investigating" ] },
"Move 176 and 170 to doing, assign to me and set the stage to Investigating")
assert_command(
{ context: { assignee_ids: [ "jz" ], tag_ids: [ "design" ] }, commands: [ "/assign andy", "/tag #v2" ] },
"assign andy to the current #design cards assigned to jz and tag them with #v2")
assert_command(
{ context: { assignee_ids: [ "andy" ] }, commands: [ "/close", "/assign kevin" ] },
"close cards assigned to andy and assign them to kevin")
assert_command(
{ context: { tag_ids: [ "design" ], assignee_ids: [ "jz" ] }, commands: [ "/assign andy", "/tag #v2" ] },
"assign cards tagged with #design assigned to jz to andy and tag them with #v2")
end
test "default to search" do
assert_command({ commands: [ "/search backups" ] }, "backups")
assert_command({ context: { terms: [ "backups" ] } }, "cards about backups")
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")
end
test "create cards" do
assert_command({ commands: [ "/add_card" ] }, "add card")
assert_command({ commands: [ "/add_card new task" ] }, "add card new task")
assert_command({ commands: [ "/add_card" ] }, "create card")
assert_command({ commands: [ "/add_card urgent issue" ] }, "create card urgent issue")
end
test "filter by time ranges" do
assert_command({ context: { closure: "thisweek", indexed_by: "closed" } }, "cards completed this week")
assert_command({ context: { creation: "thisweek", tag_ids: [ "design" ], creator_ids: [ "jz" ] } }, "cards created this week by jz tagged as #design")
assert_command({ context: { creation: "thismonth", creator_ids: [ "Gabriel", "Michael" ] } }, "cards created by Gabriel or Michael this month")
end
test "closing soon and falling back soon" do
assert_command({ context: { indexed_by: "falling_back_soon" } }, "cards to be reconsidered soon")
assert_command({ context: { indexed_by: "falling_back_soon" } }, "cards to be reconsidered soon")
assert_command({ context: { assignee_ids: [ "david" ], indexed_by: "closing_soon" } }, "my cards that are going to be auto closed")
end
test "view users profiles" do
assert_command({ commands: [ "/user jz" ] }, "check what jz has been up to")
assert_command({ commands: [ "/user kevin" ] }, "view kevin")
end
private
+40
View File
@@ -0,0 +1,40 @@
require "test_helper"
class Command::ConsiderTest < ActionDispatch::IntegrationTest
include CommandTestHelper
setup do
Current.session = sessions(:david)
@card = cards(:text)
@card.engage
end
test "consider card on perma" do
assert_changes -> { @card.reload.considering? }, from: false, to: true do
execute_command "/consider", context_url: collection_card_url(@card.collection, @card)
end
end
test "consider cards on index page" do
cards = cards(:logo, :text, :layout)
cards.each(&:engage)
execute_command "/consider", context_url: collection_cards_url(@card.collection)
assert cards.map(&:reload).all?(&:considering?)
end
test "undo consider" do
cards = cards(:logo, :text, :layout)
cards.each(&:engage)
command = parse_command "/consider", context_url: collection_cards_url(@card.collection)
command.execute
assert cards.map(&:reload).all?(&:considering?)
command.undo
assert cards.map(&:reload).all?(&:doing?)
end
end
+40
View File
@@ -0,0 +1,40 @@
require "test_helper"
class Command::DoTest < ActionDispatch::IntegrationTest
include CommandTestHelper
setup do
Current.session = sessions(:david)
@card = cards(:text)
@card.reconsider
end
test "do card on perma" do
assert_changes -> { @card.reload.doing? }, from: false, to: true do
execute_command "/do", context_url: collection_card_url(@card.collection, @card)
end
end
test "do cards on index page" do
cards = cards(:logo, :text, :layout)
cards.each(&:reconsider)
execute_command "/do", context_url: collection_cards_url(@card.collection)
assert cards.map(&:reload).all?(&:doing?)
end
test "undo do" do
cards = cards(:logo, :text, :layout)
cards.each(&:reconsider)
command = parse_command "/do", context_url: collection_cards_url(@card.collection)
command.execute
assert cards.map(&:reload).all?(&:doing?)
command.undo
assert cards.map(&:reload).all?(&:considering?)
end
end
+1 -1
View File
@@ -6,7 +6,7 @@ class Command::GoToUserTest < ActionDispatch::IntegrationTest
test "redirect to the user perma" do
result = execute_command "@kevin"
assert_equal users(:kevin), result.url
assert_equal user_path(users(:kevin)), result.url
end
test "result in an invalid command if the user does not exist" do
+47
View File
@@ -0,0 +1,47 @@
require "test_helper"
class Command::StageTest < ActionDispatch::IntegrationTest
include CommandTestHelper
setup do
Current.session = sessions(:david)
@card = cards(:text)
@new_stage = workflow_stages(:qa_review)
@original_stage = @card.stage
end
test "move card to a new stage on perma" do
assert_changes -> { @card.reload.stage }, from: @original_stage, to: @new_stage do
execute_command "/stage #{@new_stage.name}", context_url: collection_card_url(@card.collection, @card)
end
end
test "move cards on cards' index page" do
cards = [ cards(:logo), cards(:layout), cards(:text) ]
execute_command "/stage #{@new_stage.name}", context_url: collection_cards_url(@card.collection)
cards.each do |card|
assert_equal @new_stage, card.reload.stage
end
end
test "undo stage change" do
cards = [ cards(:logo), cards(:layout), cards(:text) ]
cards.each { it.change_stage_to @original_stage }
command = parse_command "/stage #{@new_stage.name}", context_url: collection_cards_url(@card.collection)
command.execute
cards.each do |card|
assert_equal @new_stage, card.reload.stage
end
command.undo
# Verify cards moved back to original stages
cards.each do |card|
assert_equal @original_stage, card.reload.stage
end
end
end
+24 -4
View File
@@ -114,16 +114,16 @@ class FilterTest < ActiveSupport::TestCase
end
test "summary" do
assert_equal "Newest, tagged #mobile, and assigned to JZ ", filters(:jz_assignments).summary
assert_equal "Newest, #mobile, and assigned to JZ", filters(:jz_assignments).summary
filters(:jz_assignments).update!(stages: workflow_stages(:qa_triage, :qa_in_progress))
assert_equal "Newest, tagged #mobile, assigned to JZ, and staged in Triage or In progress ", filters(:jz_assignments).summary
assert_equal "Newest, #mobile, assigned to JZ, and staged in Triage or In progress", filters(:jz_assignments).summary
filters(:jz_assignments).update!(stages: [], assignees: [], tags: [], collections: [ collections(:writebook) ])
assert_equal "Newest in Writebook", filters(:jz_assignments).summary
assert_equal "Newest", filters(:jz_assignments).summary
filters(:jz_assignments).update!(indexed_by: "stalled")
assert_equal "Stalled in Writebook", filters(:jz_assignments).summary
assert_equal "Stalled", filters(:jz_assignments).summary
end
test "get a clone with some changed params" do
@@ -133,4 +133,24 @@ class FilterTest < ActiveSupport::TestCase
assert filter.indexed_by.closed?
assert_equal [ "haggis" ], filter.terms
end
test "creation window" do
filter = users(:david).filters.new creation: "this week"
cards(:logo).update_columns created_at: 2.weeks.ago
assert_not_includes filter.cards, cards(:logo)
cards(:logo).update_columns created_at: Time.current
assert_includes filter.cards, cards(:logo)
end
test "closure window" do
filter = users(:david).filters.new closure: "this week"
cards(:shipping).closure.update_columns created_at: 2.weeks.ago
assert_not_includes filter.cards, cards(:shipping)
cards(:shipping).closure.update_columns created_at: Time.current
assert_includes filter.cards, cards(:shipping)
end
end
+64
View File
@@ -0,0 +1,64 @@
require "test_helper"
class TimeWindowParserTest < ActiveSupport::TestCase
setup do
@now = Time.zone.parse("2023-06-15 9am")
@parser = TimeWindowParser.new(now: @now)
end
test "parse today" do
assert_equal @now.beginning_of_day..@now.end_of_day,
@parser.parse("today")
end
test "parse yesterday" do
yesterday = @now - 1.day
assert_equal yesterday.beginning_of_day..yesterday.end_of_day,
@parser.parse("yesterday")
end
test "parse this week" do
assert_equal @now.beginning_of_week..@now.end_of_week,
@parser.parse("this week")
end
test "parse this month" do
assert_equal @now.beginning_of_month..@now.end_of_month,
@parser.parse("this month")
end
test "parse this year" do
assert_equal @now.beginning_of_year..@now.end_of_year,
@parser.parse("this year")
end
test "parse last week" do
last_week = @now - 1.week
assert_equal last_week.beginning_of_week..last_week.end_of_week,
@parser.parse("last week")
end
test "parse last month" do
last_month = @now - 1.month
assert_equal last_month.beginning_of_month..last_month.end_of_month,
@parser.parse("last month")
end
test "parse last year" do
last_year = @now - 1.year
assert_equal last_year.beginning_of_year..last_year.end_of_year,
@parser.parse("last year")
end
test "parse with unknown string returns nil" do
assert_nil @parser.parse("unknown time window")
end
test "returns nil for nil" do
assert_nil @parser.parse(nil)
end
end
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff