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 ...
@@ -122,10 +122,6 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.events__popup {
|
||||
margin-block-start: var(--block-space-half);
|
||||
}
|
||||
|
||||
/* Event
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
}
|
||||
|
||||
&:has(input:checked) {
|
||||
--btn-background: var(--color-ink-lightest);
|
||||
--btn-background: transparent;
|
||||
--btn-color: var(--color-ink);
|
||||
|
||||
.icon {
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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 Fizzy’s 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 user’s 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 user’s 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 creator’s 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 — creator’s 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 user’s 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 don’ts
|
||||
--------------
|
||||
* **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 person’s 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 person’s profile or activity:
|
||||
|
||||
• visit user mike → /user mike*
|
||||
• view user kevin → /user kevin*
|
||||
• see mike’s profile → /user mike
|
||||
|
||||
---------------------------- CRUCIAL DON’TS ---------------------------
|
||||
|
||||
* Don’t 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 what’s 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 Andy’s 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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -8,7 +8,7 @@ class Command::GoToUser < Command
|
||||
end
|
||||
|
||||
def execute
|
||||
redirect_to user
|
||||
redirect_to user_path(user)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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 %>
|
||||
@@ -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™</a></strong>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 117 KiB |
@@ -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" },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||