From 28a569c49bb3e77ce87ed10db06d06e4b85d95b9 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 3 Mar 2025 16:17:54 -0800 Subject: [PATCH 1/3] Tickets --- app/assets/images/bell-off.svg | 1 + app/assets/images/burst.svg | 1 - app/assets/images/home.svg | 2 +- app/assets/images/pinned.svg | 1 + app/assets/images/thumb-up.svg | 2 +- app/assets/images/unpinned.svg | 1 + app/assets/stylesheets/avatars.css | 4 + app/assets/stylesheets/bubbles.css | 76 +---- app/assets/stylesheets/cards.css | 317 ++++++++++++++++++ app/assets/stylesheets/colors.css | 23 +- app/assets/stylesheets/comments.css | 41 +-- app/assets/stylesheets/events.css | 83 ++--- app/assets/stylesheets/house.css | 2 +- app/assets/stylesheets/inputs.css | 6 +- app/assets/stylesheets/layout.css | 6 - app/assets/stylesheets/pins.css | 158 +++++++++ app/assets/stylesheets/popup.css | 6 +- app/assets/stylesheets/search.css | 3 + app/assets/stylesheets/tags.css | 9 + app/assets/stylesheets/utilities.css | 3 + app/assets/stylesheets/workflows.css | 29 +- .../bubbles/pins/trays_controller.rb | 5 + app/controllers/bubbles/pins_controller.rb | 16 + app/controllers/bubbles_controller.rb | 18 + app/controllers/events_controller.rb | 1 + app/controllers/workflows_controller.rb | 2 +- app/helpers/bubbles_helper.rb | 11 + app/helpers/comments_helper.rb | 2 +- app/helpers/events_helper.rb | 33 +- app/helpers/workflows_helper.rb | 2 +- app/models/bubble.rb | 10 +- app/models/bubble/colored.rb | 4 +- app/models/bubble/pinnable.rb | 19 ++ app/models/bucket.rb | 10 + app/models/concerns/filter/params.rb | 22 ++ app/models/pin.rb | 12 + app/models/user.rb | 3 + app/views/accounts/users/index.html.erb | 8 +- app/views/boosts/_boosts.html.erb | 21 +- app/views/bubbles/_assignments.html.erb | 9 - app/views/bubbles/_bubble.html.erb | 2 +- app/views/bubbles/_card.html.erb | 52 +++ app/views/bubbles/_color.html.erb | 4 +- .../bubbles/_display_count_selector.html.erb | 37 ++ app/views/bubbles/_filters.html.erb | 9 - app/views/bubbles/_image.html.erb | 20 +- app/views/bubbles/_messages.html.erb | 7 + app/views/bubbles/_meta.html.erb | 45 +++ app/views/bubbles/_people.html.erb | 17 + app/views/bubbles/_pop_toggle.html.erb | 13 +- app/views/bubbles/_publish.html.erb | 2 +- app/views/bubbles/_tags.html.erb | 17 +- app/views/bubbles/edit.html.erb | 12 +- app/views/bubbles/index.html.erb | 38 ++- app/views/bubbles/list/_bubble.html.erb | 44 ++- app/views/bubbles/pins/_pin.html.erb | 17 + app/views/bubbles/pins/_tray.html.erb | 24 ++ app/views/bubbles/pins/show.html.erb | 13 + app/views/bubbles/pins/trays/show.html.erb | 3 + app/views/bubbles/pins_trays/show.html.erb | 3 + app/views/bubbles/show.html.erb | 149 ++++---- .../bubbles/sidebar/_assignment.html.erb | 8 +- app/views/bubbles/sidebar/_tag.html.erb | 4 +- app/views/bubbles/stage_pickers/new.html.erb | 33 +- app/views/bubbles/watches/show.html.erb | 39 +-- app/views/buckets/edit.html.erb | 4 +- app/views/buckets/index.html.erb | 3 +- app/views/buckets/new.html.erb | 6 +- app/views/comments/_body.html.erb | 18 +- app/views/comments/_new.html.erb | 2 +- .../comments/reactions/_reaction.html.erb | 17 +- app/views/events/_day.html.erb | 4 +- app/views/events/_empty_days.html.erb | 8 +- app/views/events/_event.html.erb | 16 +- app/views/events/_filter.html.erb | 11 +- app/views/events/index.html.erb | 52 ++- app/views/filters/_assignees.html.erb | 89 +++-- app/views/filters/_buckets.html.erb | 75 ++--- app/views/filters/_dialog.html.erb | 85 +++-- app/views/filters/_settings.html.erb | 6 +- app/views/filters/_stages.html.erb | 15 +- app/views/filters/_tags.html.erb | 75 ++--- app/views/first_runs/show.html.erb | 9 +- app/views/notifications/index.html.erb | 16 +- .../notifications/settings/show.html.erb | 8 +- app/views/sessions/new.html.erb | 11 +- config/routes.rb | 10 +- db/cable_schema.rb | 4 +- db/cache_schema.rb | 8 +- db/migrate/20250318005138_create_pins.rb | 11 + db/queue_schema.rb | 78 ++--- db/schema.rb | 155 +++++---- db/schema_cache.yml | 75 ++++- .../bubbles/pins_controller_test.rb | 23 ++ test/fixtures/pins.yml | 7 + test/fixtures/workflow/stages.yml | 12 +- test/integration/bubble_messages.rb | 4 +- test/models/filter_test.rb | 6 +- 98 files changed, 1688 insertions(+), 829 deletions(-) create mode 100644 app/assets/images/bell-off.svg delete mode 100644 app/assets/images/burst.svg create mode 100644 app/assets/images/pinned.svg create mode 100644 app/assets/images/unpinned.svg create mode 100644 app/assets/stylesheets/cards.css create mode 100644 app/assets/stylesheets/pins.css create mode 100644 app/assets/stylesheets/search.css create mode 100644 app/controllers/bubbles/pins/trays_controller.rb create mode 100644 app/controllers/bubbles/pins_controller.rb create mode 100644 app/models/bubble/pinnable.rb create mode 100644 app/models/concerns/filter/params.rb create mode 100644 app/models/pin.rb delete mode 100644 app/views/bubbles/_assignments.html.erb create mode 100644 app/views/bubbles/_card.html.erb create mode 100644 app/views/bubbles/_display_count_selector.html.erb create mode 100644 app/views/bubbles/_meta.html.erb create mode 100644 app/views/bubbles/_people.html.erb create mode 100644 app/views/bubbles/pins/_pin.html.erb create mode 100644 app/views/bubbles/pins/_tray.html.erb create mode 100644 app/views/bubbles/pins/show.html.erb create mode 100644 app/views/bubbles/pins/trays/show.html.erb create mode 100644 app/views/bubbles/pins_trays/show.html.erb create mode 100644 db/migrate/20250318005138_create_pins.rb create mode 100644 test/controllers/bubbles/pins_controller_test.rb create mode 100644 test/fixtures/pins.yml diff --git a/app/assets/images/bell-off.svg b/app/assets/images/bell-off.svg new file mode 100644 index 000000000..b8fa159e8 --- /dev/null +++ b/app/assets/images/bell-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/burst.svg b/app/assets/images/burst.svg deleted file mode 100644 index dbc4481c4..000000000 --- a/app/assets/images/burst.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/assets/images/home.svg b/app/assets/images/home.svg index 8fcbf2376..57100f993 100644 --- a/app/assets/images/home.svg +++ b/app/assets/images/home.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/pinned.svg b/app/assets/images/pinned.svg new file mode 100644 index 000000000..51c0e3292 --- /dev/null +++ b/app/assets/images/pinned.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/thumb-up.svg b/app/assets/images/thumb-up.svg index c8552acac..f198909c7 100644 --- a/app/assets/images/thumb-up.svg +++ b/app/assets/images/thumb-up.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/unpinned.svg b/app/assets/images/unpinned.svg new file mode 100644 index 000000000..bada0866a --- /dev/null +++ b/app/assets/images/unpinned.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/stylesheets/avatars.css b/app/assets/stylesheets/avatars.css index 34a75c0bc..9ee11487d 100644 --- a/app/assets/stylesheets/avatars.css +++ b/app/assets/stylesheets/avatars.css @@ -17,6 +17,10 @@ max-inline-size: 100%; object-fit: cover; } + + + .avatar { + margin-inline-start: -0.8em; + } } .avatar__group { diff --git a/app/assets/stylesheets/bubbles.css b/app/assets/stylesheets/bubbles.css index 6b902d6ee..8395270fb 100644 --- a/app/assets/stylesheets/bubbles.css +++ b/app/assets/stylesheets/bubbles.css @@ -62,16 +62,6 @@ z-index: 1; } } - -.bubble__container { - inline-size: min(67ch, 75vw); - max-inline-size: 100dvw; - - @media (max-width: 70ch) { - inline-size: min(67ch, 100dvw); - } -} - .bubble__detail { --bubble-border-color: color-mix(in srgb, var(--bubble-color) 30%, var(--color-bg)); --bubble-border-width: 0.15em; @@ -101,51 +91,6 @@ color: var(--bubble-color); } - &.bubble__boosts { - --rotation: 45deg; - - aspect-ratio: 5/7; - border-radius: var(--bubble-shape); - inline-size: min-content; - inset: 70cqi 5cqi auto auto; - min-inline-size: 4.5ch; - padding: 0; - transform: rotate(var(--rotation)); - - - .boost__form { - transform: rotate(calc(var(--rotation) * -1)); - } - - .boost__btn { - --btn-background: var(--bubble-color); - --btn-border-radius: 50%; - --btn-color: var(--color-ink-reversed); - --btn-size: 0.5em; - --btn-padding: 0.3em 0.5em 0.4em; - - font-weight: 800; - } - - .boost__input { - color: var(--bubble-color); - font-size: inherit; - font-weight: 800; - padding: 0; - text-align: center; - - @supports (field-sizing: content) { - field-sizing: content; - } - } - - @media (hover: hover) { - .bubbles .bubble:not(.popped):hover & { - transform: rotate(calc(var(--rotation) + var(--bubble-shift) * 1)) translate(0rem, 1rem); - } - } - } - &.bubble__assignee { font-size: 4.5cqi; inset: 8em auto auto -1.7em; @@ -321,8 +266,14 @@ --divider-offset: 1.2em; --hover-size: 0; - font-size: var(--text-medium); - max-inline-size: 80ch; + container-type: inline-size; + font-size: 1.4cqi; + inline-size: min(90ch, 100dvw); + max-inline-size: 100dvw; + + @media (max-width: 70ch) { + inline-size: 100%; + } .bubble { order: 0; @@ -337,7 +288,7 @@ margin-inline: var(--divider-offset); li { - border-radius: 0.6em; + border-radius: 0; list-style: none; padding: 0.5em var(--inline-space); position: relative; @@ -350,7 +301,6 @@ &:hover { background-color: color(from var(--bubble-color) srgb r g b / 0.15); border: 0; - border-radius: 0.6em; } } } @@ -408,8 +358,9 @@ } .bubble__title-text { - font-weight: 600; + font-weight: 700; flex-grow: 1; + letter-spacing: -0.015em; white-space: nowrap; .drafted & { @@ -550,11 +501,6 @@ position: relative; z-index: 1; - .btn { - --btn-background: var(--bubble-color); - --btn-color: var(--color-ink-reversed); - } - .btn--plain { --btn-color: var(--bubble-color); diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css new file mode 100644 index 000000000..1f7243315 --- /dev/null +++ b/app/assets/stylesheets/cards.css @@ -0,0 +1,317 @@ +.cards { + --column-gap: 2cqi; + --row-gap: 2cqi; + + font-size: 0.9cqi; + display: grid; + background-color: var(--color-container); + grid-template-columns: repeat(auto-fit, minmax(min(100%, calc(100%/2 - var(--column-gap) * 2)), 1fr)); + justify-items: center; + margin-block: 0; + padding: 3cqi; + view-transition-name: cards-container; + + > * { + flex-shrink: 1; + } + + .card { + max-inline-size: 50cqi; + } + + @media (min-width: 70ch) { + grid-template-columns: repeat(auto-fit, minmax(min(100%, calc(100%/3 - var(--column-gap) * 2)), 1fr)); + font-size: 0.7cqi; + + .card { + max-inline-size: 33cqi; + } + } +} + +.card { + --block-space: 1em; + --column-gap: 0.5em; + --inline-space: 0.5em; + --row-gap: 0.5em; + + aspect-ratio: 15/7; + background-color: color-mix(in srgb, var(--bubble-color) 4%, var(--color-bg)); + color: color-mix(in srgb, var(--bubble-color) 40%, var(--color-ink)); + font-size: 1em; + max-inline-size: 100ch; + z-index: 1; + + .card__container & { + inline-size: 60cqi; + + @media (max-width: 70ch) { + inline-size: 100dvw; + } + } +} + +.card__actions { + position: absolute; + inset: 1.4em auto auto -4em; + z-index: 1; +} + +.card__actions--right { + inset: 1.4em -3.5em auto auto; +} + +.card__actions-container { + background-color: var(--color-bg); + margin-block: -1em 1em; + padding: 1em 2em; + position: relative; + z-index: 0; + + .btn { + --btn-background: var(--bubble-color); + --btn-color: var(--color-ink-reversed); + } + + .btn--plain { + --btn-color: var(--bubble-color); + + text-decoration: underline; + } + + .bubble__pop-message { + color: var(--bubble-color); + } +} + +.card__actions-wrapper { + font-size: 1.33cqi; +} + +.cards .card__assignees:not(:has(.avatar)) { + .txt-uppercase { + display: none; + } +} + +.card__background { + inset: 0; + position: absolute; + z-index: -1; + + img { + block-size: 100%; + inline-size: 100%; + object-fit: cover; + object-position: center; + opacity: 0.2; + transition: opacity 0.2s ease-in-out; + } + + @media (hover: hover) { + .card:hover & { + img { + opacity: 1; + } + } + } +} + +.card__body { + padding-inline: 1em; +} + +.card__body-header { + flex: 2 1 75%; + padding-block: 1em; +} + +.card__bucket { + background-color: var(--bubble-color); + font-weight: 800; + letter-spacing: 0.5ch; + padding: 0.5em 1em; +} +.card__container { + background-color: var(--color-container); + display: inline-block; + font-size: 1.33cqi; + padding: 2cqi 3cqi 3cqi; +} + +.card__container--pointing { + --arrow-size: 20em; + --aspect-ratio: 7; + + position: relative; + margin-block-end: calc(var(--arrow-size) / var(--aspect-ratio)); + + &::after { + aspect-ratio: var(--aspect-ratio); + background: var(--color-container); + clip-path: polygon(50% 100%, 100% 0, 0 0); + inline-size: var(--arrow-size); + content: ""; + display: block; + inset-block-start: 99%; + inset-inline-start: 50%; + position: absolute; + transform: translateX(-50%); + } +} + +.card__delete-btn { + .cards & { + display: none; + } +} + +.card__footer { + margin-block-end: 1em; + padding-inline-end: 1em; +} + +.card__header { + padding-inline-end: 0.7em; +} + +.card__hide-on-index { + .cards & { + display: none; + } +} + +.card__link { + --hover-size: 0; + + content: ""; + inset: 0; + position: absolute; + z-index: -1; + + .cards & { + z-index: 1; + } +} + +.card__meta { + --border-color: var(--bubble-color); + --row-gap: 0.25em; + + font-size: 0.9em; + margin-inline-start: 1em; + + strong { + font-size: 1.1em; + font-weight: 900; + } + + .separator { + height: calc(2lh + 2ex); + } +} + +.card__people { + margin-inline-start: 1em; + + span { + font-size: 0.9em; + } +} + +.card__stages { + flex: 1 1 25%; + font-size: 0.8em; + padding-block: 1em; +} + +.card__tags { + color: var(--bubble-color); +} + +.card__tag { + color: var(--bubble-color); + font-weight: 700; +} + +.card__title { + --hover-size: 0; + --input-border-radius: 0; + + font-size: 2.5em; + font-weight: 900; + line-height: 1.1; + min-block-size: 3lh; + text-wrap: balance; + + .cards & { + pointer-events: none; + } + + .card__title-field { + &:is(textarea) { + + &::placeholder { + color: inherit; + opacity: 0.5; + } + } + } + + .card__title-link { + --lines: 3; + + color: inherit; + } +} + +.card-count__selector { + font-size: 1cqi; + inset: 0.5cqi 0.5cqi auto auto; + position: absolute; + text-align: start; + + .popup { + inset: 1em 1em auto auto; + } +} + +.input.boost__input { + --input-border-radius: 0; + --input-border-size: 0; + --input-padding: 0; + + color: inherit; + font-size: 1.1em !important; + font-weight: inherit; + inline-size: min-content; + max-inline-size: 3ch; + min-inline-size: 1ch; + + @supports (field-sizing: content) { + field-sizing: content; + } +} + +.card__popped { + align-items: center; + aspect-ratio: 7/4; + border: 0.4rem solid var(--bubble-color); + position: absolute; + inset: auto 2em 2em auto; + justify-content: space-between; + padding: 0.5em 1em; + transform: rotate(-10deg); + z-index: 1; +} + +.card__popped-title { + color: var(--color-ink-reversed); + font-size: 1.5em; + font-weight: 900; + text-shadow: + -2px -2px 0 var(--bubble-color), + 2px -2px 0 var(--bubble-color), + -2px 2px 0 var(--bubble-color), + 2px 2px 0 var(--bubble-color); +} diff --git a/app/assets/stylesheets/colors.css b/app/assets/stylesheets/colors.css index 135b0d0cc..354c24011 100644 --- a/app/assets/stylesheets/colors.css +++ b/app/assets/stylesheets/colors.css @@ -1,6 +1,6 @@ :root { /* Named color values */ - --lch-black: 0% 0 0; + --lch-black: 27.13% 0.1035 284.77; --lch-white: 100% 0 0; --lch-gray-light: 96% 0.005 96; --lch-gray: 92% 0.005 96; @@ -12,9 +12,10 @@ --lch-red: 51% 0.2 31; --lch-green: 65.59% 0.234 142.49; --lch-green-light: 95% 0.03 142.49; - --lch-yellow: 96.01% 0.0348 81.29; + --lch-yellow: 92.62% 0.1 91.5; --lch-always-black: 0% 0 0; --lch-always-white: 100% 0 0; + --lch-light-blue: 84.94% 0.048 216.62; /* Abstractions */ --color-negative: oklch(var(--lch-red)); @@ -33,6 +34,7 @@ --color-marker: oklch(var(--lch-orange)); --color-always-black: oklch(var(--lch-always-black)); --color-always-white: oklch(var(--lch-always-white)); + --color-container: oklch(var(--lch-light-blue)); /* Redefine named color values for dark mode */ @media (prefers-color-scheme: dark) { @@ -47,7 +49,8 @@ --lch-red: 73.8% 0.184 29.18; --lch-green: 75% 0.21 141.89; --lch-green-light: 28.11% 0.02 142.49; - --lch-yellow: 35.29% 0.0493 84.59; + --lch-yellow: 40.9% 0.06 88.9; + --lch-light-blue: 20.62% 0.048 216.62; } } @@ -88,3 +91,17 @@ --btn-size: 2em; } } + +.color-picker__button { + font-size: 0.75em; + + .cards & { + display: none; + } + + + .panel { + --panel-size: auto; + + inset: 0 0 auto auto; + } +} diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index 21ec03987..ba89c6db6 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -1,13 +1,19 @@ .comments { --avatar-size: 2.33em; - --comment-padding-block: 0.65lh; + --comment-padding-block: 0.25lh; --comment-padding-inline: 3ch; - max-inline-size: 120ch; + font-size: 1.33cqi; + inline-size: min(67ch, 100dvw); + max-inline-size: 100dvw; row-gap: var(--comment-padding-block); + @media (max-width: 70ch) { + inline-size: 100%; + } + > :first-child { - margin-block-start: var(--comment-padding-block); + /* margin-block-start: var(--comment-padding-block); */ } } @@ -15,14 +21,10 @@ margin-block: calc(var(--comment-padding-block) * 0.3); margin-inline: 0 auto; position: relative; - - &:not(.comment--new) { - max-inline-size: 66ch; - } } .comment--new { - background-color: var(--color-subtle-light); + border-block: 1px solid var(--color-subtle-dark); } .comment__avatar { @@ -30,7 +32,7 @@ margin-inline: auto calc(var(--comment-padding-inline) * -0.75); z-index: 0; - .comment--mine & { + .comment--mine_ & { margin-inline: calc(var(--comment-padding-inline) * -0.75) auto; } } @@ -56,11 +58,10 @@ } .comment__content { - background-color: var(--color-subtle-light); - margin-inline: auto var(--avatar-size); + margin-inline: auto 0 auto var(--avatar-size); padding: var(--comment-padding-block) var(--comment-padding-inline); - - .comment--mine & { + + .comment--mine_ & { margin-inline: var(--avatar-size) auto; } @@ -71,12 +72,11 @@ .comment__input { --input-border-size: 0; + --input-padding: 0; line-height: inherit; min-block-size: calc(9lh + (2 * var(--comment-padding-block))); min-inline-size: 100%; - padding-block: var(--comment-padding-block); - padding-inline: calc(var(--comment-padding-inline) + calc((1lh - 1ex) / 2)); @supports (field-sizing: content) { field-sizing: content; @@ -84,7 +84,7 @@ } } -.comment--mine { +.comment--mine_ { flex-direction: row-reverse; margin-inline: auto 0; @@ -93,6 +93,11 @@ } } +.comment__author { + border-block-end: 1px solid var(--color-subtle-dark); + margin-block-end: 0.3lh; +} + .comment__submit { inset: auto 0.5em 0.5em auto; position: absolute; @@ -100,13 +105,11 @@ } .comment__timestamp { - color: var(--color-ink); - opacity: 0.5; + color: var(--color-subtle-dark); } .event-summary { color: var(--bubble-color); - font-size: 0.875em; font-weight: normal; gap: 0.5ch; opacity: 0.7; diff --git a/app/assets/stylesheets/events.css b/app/assets/stylesheets/events.css index f20f0b45e..c874660b4 100644 --- a/app/assets/stylesheets/events.css +++ b/app/assets/stylesheets/events.css @@ -1,28 +1,46 @@ .events { - --grid-lines: 0.3em; - - border-block-start: 1px solid var(--color-subtle); display: grid; gap: 0 var(--grid-lines); - grid-template-columns: repeat(4, 1fr); - padding-block-start: var(--block-space); + grid-template-columns: repeat(3, 1fr); + position: relative; + z-index: 0; +} - &:has(.event--related) { - .event:not(.event--related) { - box-shadow: none; - opacity: 0.75; - } - } +.events__container { + display: grid; + grid-template-columns: 5fr 2fr; + --column-gap: clamp(var(--inline-space), 3cqmin, calc(var(--inline-space) * 3)); +} + +.events__day-header { + background-color: var(--color-bg); + block-size: var(--grid-lines); + display: block; + position: relative; + z-index: 1; +} + +.events__day-header-content { + background-color: var(--color-bg); + border-radius: 2em; + display: inline-flex; + inset: calc(var(--grid-lines) - 1em) auto -1em; + padding: 0.3em 1.3em 0.3em; + position: absolute; + transform: translateX(-50%); + z-index: 2; } .events__index { - --row-gap: calc(4 * var(--block-space)); + --grid-lines: 0.2em; + --row-gap: var(--block-space); } .events--none { - .events + & { - padding-block-start: calc(4 * var(--block-space)); - } + background-color: var(--color-container); + border-block-start: var(--grid-lines) solid var(--color-bg); + padding-block: 3em; + margin: auto var(--grid-lines) calc(var(--grid-lines) * -1) auto; } .events__popup { @@ -40,6 +58,7 @@ .event { --column-gap: 0.7ch; + --panel-border-color: transparent; --panel-border-radius: 0.5em; --panel-padding: 0.6em 1.2em 0.6em 0.6em; --panel-size: auto; @@ -50,31 +69,13 @@ } .event--related { + --hover-color: var(--bubble-color); + z-index: 2; - - box-shadow: - 0 0 0 1px oklch(var(--lch-always-black) / 0.02), - 0 .2em 1.6em -0.8em oklch(var(--lch-always-black) / 0.2), - 0 .4em 2.4em -1em oklch(var(--lch-always-black) / 0.3), - 0 .4em .8em -1.2em oklch(var(--lch-always-black) / 0.4), - 0 .8em 1.2em -1.6em oklch(var(--lch-always-black) / 0.5), - 0 1.2em 1.6em -2em oklch(var(--lch-always-black) / 0.6), - 0 0 0.3em 0.3em var(--bubble-color) !important; - - @media (prefers-color-scheme: dark) { - box-shadow: - 0 0 0 1px oklch(var(--lch-always-black) / 0.42), - 0 .2em 1.6em -0.8em oklch(var(--lch-always-black) / 0.6), - 0 .4em 2.4em -1em oklch(var(--lch-always-black) / 0.7), - 0 .4em .8em -1.2em oklch(var(--lch-always-black) / 0.8), - 0 .8em 1.2em -1.6em oklch(var(--lch-always-black) / 0.9), - 0 1.2em 1.6em -2em oklch(var(--lch-always-black) / 1), - 0 0 0.3em 0.3em var(--bubble-color) !important; - } } .event__grid-item { - background-color: var(--color-selected); + background-color: var(--color-container); block-size: 100%; border-radius: 0; display: flex; @@ -84,8 +85,10 @@ .event__grid-column-title { --z: 3; - background-color: var(--color-bg); - padding-block: var(--grid-lines); + background-color: var(--color-container); + font-size: 0.9em; + padding: 2em 0 1em; + text-transform: uppercase; } .event__timestamp { @@ -103,10 +106,10 @@ .event__wrapper { align-content: end; display: grid; - gap: var(--grid-lines); + gap: calc(var(--grid-lines) * 2); justify-items: center; margin: var(--grid-lines); - padding: calc(var(--grid-lines) / 2); + padding: var(--grid-lines); .event { grid-column-start: unset !important; diff --git a/app/assets/stylesheets/house.css b/app/assets/stylesheets/house.css index 01aaed313..aa959a4bf 100644 --- a/app/assets/stylesheets/house.css +++ b/app/assets/stylesheets/house.css @@ -13,7 +13,7 @@ house-md { /* Toolbar */ house-md-toolbar { background-color: inherit; - border-block-end: 1px solid var(--color-subtle); + border-block-end: 1px solid var(--color-subtle-dark); border-radius: 0; color: currentColor; display: inline-flex; diff --git a/app/assets/stylesheets/inputs.css b/app/assets/stylesheets/inputs.css index 38612696b..a600b690a 100644 --- a/app/assets/stylesheets/inputs.css +++ b/app/assets/stylesheets/inputs.css @@ -95,18 +95,18 @@ } } -.input--textara { +.input--textarea { --input-padding: 0; line-height: inherit; - min-block-size: calc(4lh + (2 * var(--input-padding))); + min-block-size: calc(3lh + (2 * var(--input-padding))); min-inline-size: 100%; padding-block: var(--input-padding); padding-inline: calc(var(--input-padding) + calc((1lh - 1ex) / 2)); @supports (field-sizing: content) { field-sizing: content; - max-block-size: calc(5lh + (2 * var(--input-padding))); + max-block-size: calc(3lh + (2 * var(--input-padding))); min-block-size: calc(1lh + (2 * var(--input-padding))); } } diff --git a/app/assets/stylesheets/layout.css b/app/assets/stylesheets/layout.css index 4e11f96aa..daa758249 100644 --- a/app/assets/stylesheets/layout.css +++ b/app/assets/stylesheets/layout.css @@ -23,12 +23,6 @@ body { text-justify: distribute; widows: 2; } - - &:has(.bubble__perma) { - display: grid; - grid-template-columns: 1fr minmax(min-content, auto) 1fr; - gap: var(--inline-space); - } } :where(#footer) { diff --git a/app/assets/stylesheets/pins.css b/app/assets/stylesheets/pins.css new file mode 100644 index 000000000..f2c38435f --- /dev/null +++ b/app/assets/stylesheets/pins.css @@ -0,0 +1,158 @@ +.pin-tray { + --size: 38ch; + --height: calc(5ch + 1lh + var(--block-space)); + + block-size: var(--height); + inline-size: var(--size); + inset: auto auto var(--block-space-half) var(--inline-space); + padding: var(--block-space-half) var(--inline-space); + position: fixed; + z-index: 1; + + .pin { + --offset: calc((var(--position) - 1) * (var(--block-space) * -1)); + --position: 1; + --scale: calc(1 - var(--position) / 30); + --z: calc(6 - var(--position)); + + inset: auto auto 0; + inline-size: var(--size); + outline: 0; + pointer-events: none; + position: absolute; + scale: var(--scale); + transform: translateY(var(--offset)); + transform-origin: center; + transition: scale 0.2s ease-out, transform 0.2s ease-out; + z-index: var(--z); + + &:nth-child(1) { --position: 1; } + &:nth-child(2) { --position: 2; } + &:nth-child(3) { --position: 3; } + &:nth-child(4) { --position: 4; } + &:nth-child(5) { --position: 5; } + &:nth-child(6) { --position: 6; } + + &:nth-child(1n + 7) { display: none; } + + .pin-tray[open] & { + --scale: 1; + --offset: calc((100% + var(--block-space-half)) * (var(--position) * -1)); + + margin-block-end: calc(var(--block-space) * -1); + pointer-events: unset; + } + + .pin-tray:has(.pin:only-child) & { + pointer-events: unset; + } + } +} + +.pin-tray__actions { + opacity: 0; + scale: calc(1 - 1 / 30); + transform: translateY(100%); + transition: opacity 0.2s ease-out, scale 0.2s ease-out, transform 0.2s ease-out; + + .pin-tray[open] & { + opacity: 1; + scale: 1; + transform: translateY(0); + } + + .pin-tray:has(.pin) & { + .btn img { + display: none; + } + } + + .pin-tray:not(:has(.pin)) & { + opacity: 1; + scale: 1; + transform: none; + transition: none; + + .pin-tray__all_action { + --btn-border-radius: 50%; + --btn-padding: 0; + + aspect-ratio: 1; + block-size: var(--btn-size); + display: grid; + inline-size: var(--btn-size); + place-items: center; + + > * { + grid-area: 1/1; + } + + span { + display: none; + } + } + + .pin-tray__read_action { + display: none; + } + } +} + +.pin__content { + color: var(--color-ink); + border: 1px solid; + + .notificiations-list--read & { + border: 1px solid var(--color-subtle); + box-shadow: none; + } +} + +.pin__title { + padding-inline-end: calc(var(--inline-space-half) + 1.2em); +} + +.pin-tray__expander { + --hover-size: 0; + --outline-size: 0; + + cursor: pointer; + inset: 0; + position: absolute; + z-index: 6; + + .pin-tray[open] &, + .pin-tray:not(:has(.pin)) &, + .pin-tray:has(.pin:only-child) & { + display: none; + } +} + +.pin-tray__overflow { + --offset: calc((var(--position) - 1) * (var(--block-space) * -1)); + --position: 7; + --scale: calc(1 - var(--position) / 30); + --z: calc(6 - var(--position)); + + block-size: var(--height); + display: none; + inset: auto auto 0; + inline-size: var(--size); + pointer-events: none; + position: absolute; + scale: var(--scale); + transform: translateY(var(--offset)); + transition: scale 0.2s ease-out, transform 0.2s ease-out; + z-index: var(--z); + + .pin-tray:has(.pin:nth-child(7)) & { + display: flex; + } + + .pin-tray[open] & { + --offset: calc((100% + var(--block-space-half)) * (var(--position) * -1)); + --scale: 1; + + pointer-events: unset; + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/popup.css b/app/assets/stylesheets/popup.css index c2d4dfa1f..b63a3f4be 100644 --- a/app/assets/stylesheets/popup.css +++ b/app/assets/stylesheets/popup.css @@ -2,9 +2,11 @@ --panel-border-radius: 0.5em; --panel-padding: 0.4em 0.5em 0.5em; --panel-size: auto; - + + inline-size: auto; inset: 0 auto auto 0; - max-inline-size: 30ch; + min-inline-size: 20ch; + max-inline-size: 40ch; position: absolute; z-index: 2; diff --git a/app/assets/stylesheets/search.css b/app/assets/stylesheets/search.css new file mode 100644 index 000000000..6f4bd138f --- /dev/null +++ b/app/assets/stylesheets/search.css @@ -0,0 +1,3 @@ +.search { + min-inline-size: 38ch; +} \ No newline at end of file diff --git a/app/assets/stylesheets/tags.css b/app/assets/stylesheets/tags.css index ad5ced7fa..6254755fe 100644 --- a/app/assets/stylesheets/tags.css +++ b/app/assets/stylesheets/tags.css @@ -19,3 +19,12 @@ inline-size: 18ch; } } + +.tag-picker__button { + font-size: 0.7em; + margin-inline-start: 0.5em; + + + .panel { + --panel-size: 18ch; + } +} diff --git a/app/assets/stylesheets/utilities.css b/app/assets/stylesheets/utilities.css index a9fa653f4..be68638a4 100644 --- a/app/assets/stylesheets/utilities.css +++ b/app/assets/stylesheets/utilities.css @@ -51,6 +51,7 @@ .align-start { align-items: start; } .align-end { align-items: end; } +.align-self-end { align-self: end; } .align-self-start { align-self: start; } .contain { contain: inline-size; } @@ -166,6 +167,7 @@ .fill-white { background-color: var(--color-ink-reversed); } .fill-shade { background-color: var(--color-subtle-light); } .fill-selected { background-color: var(--color-selected); } +.fill-highlight { background-color: var(--color-highlight); } .fill-transparent { background-color: transparent; } .translucent { opacity: var(--opacity, 0.5); } @@ -219,6 +221,7 @@ /* Separators */ .separator { + block-size: 100%; border-block: 0; border-inline-end: 0; border-inline-start: var(--border-size, 1px) var(--border-style, solid) var(--border-color, currentColor); diff --git a/app/assets/stylesheets/workflows.css b/app/assets/stylesheets/workflows.css index f13e1c460..3bee79623 100644 --- a/app/assets/stylesheets/workflows.css +++ b/app/assets/stylesheets/workflows.css @@ -1,12 +1,22 @@ .workflow-stage { - --btn-border-radius: 0.5em; + --btn-background: transparent; + --btn-padding: 0.2em 0.5em; + --btn-border-size: 0; + --btn-border-radius: 0.2em; --hover-size: 0; + color: inherit; + text-transform: uppercase; + @media (hover: hover) { &:hover { background-color: color(from var(--bubble-color) srgb r g b / 0.15); } } + + .bubbles-list & { + display: none; + } } .workflow-stage--current { @@ -19,6 +29,23 @@ background-color: color(from var(--bubble-color) srgb r g b / 0.65); } } + + .ticket & { + --btn-background: var(--color-ink); + + background: var(--bubble-color); + border-radius: 0.2em; + color: var(--color-ink-reversed); + } + + .bubbles-list & { + display: flex; + background: transparent; + border-radius: 0.2em; + color: var(--color-ink); + padding: 0; + border: 0; + } } .workflow__popup { diff --git a/app/controllers/bubbles/pins/trays_controller.rb b/app/controllers/bubbles/pins/trays_controller.rb new file mode 100644 index 000000000..ef79beae2 --- /dev/null +++ b/app/controllers/bubbles/pins/trays_controller.rb @@ -0,0 +1,5 @@ +class Bubbles::Pins::TraysController < ApplicationController + def show + @pins = Current.user.pins.includes(:bubble).ordered.limit(20) + end +end diff --git a/app/controllers/bubbles/pins_controller.rb b/app/controllers/bubbles/pins_controller.rb new file mode 100644 index 000000000..a28526dea --- /dev/null +++ b/app/controllers/bubbles/pins_controller.rb @@ -0,0 +1,16 @@ +class Bubbles::PinsController < ApplicationController + include BubbleScoped, BucketScoped + + def show + end + + def create + @bubble.set_pinned(Current.user, true) + redirect_to bucket_bubble_pin_path(@bucket, @bubble) + end + + def destroy + @bubble.set_pinned(Current.user, false) + redirect_to bucket_bubble_pin_path(@bucket, @bubble) + end +end diff --git a/app/controllers/bubbles_controller.rb b/app/controllers/bubbles_controller.rb index d8a642d3c..7864ffe31 100644 --- a/app/controllers/bubbles_controller.rb +++ b/app/controllers/bubbles_controller.rb @@ -5,9 +5,14 @@ class BubblesController < ApplicationController before_action :set_filter, only: :index before_action :set_bubble, only: %i[ show edit update destroy ] + before_action :handle_display_count, only: :index + + DISPLAY_COUNT_OPTIONS = [ 6, 12, 18, 24 ].freeze + DEFAULT_DISPLAY_COUNT = 6 def index @bubbles = @filter.bubbles.published_or_drafted_by(Current.user) + @display_count = display_count end def create @@ -46,4 +51,17 @@ class BubblesController < ApplicationController def deleted_notice "Bubble deleted" unless @bubble.creating? end + + def handle_display_count + if params[:set_display_count].present? + cookies[:display_count] = params[:set_display_count] + redirect_to bubbles_path( + params.permit(*Filter::PERMITTED_PARAMS, :bucket_ids).except(:set_display_count) + ) + end + end + + def display_count + (cookies[:display_count] || DEFAULT_DISPLAY_COUNT).to_i + end end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 8400c1a07..b7ddbdcce 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -6,6 +6,7 @@ class EventsController < ApplicationController update_bucket_filter @buckets = Current.user.buckets.alphabetically @events = events_by_hour_and_column + @filters = Current.user.filters.all @next_day = latest_event_before_today&.created_at end diff --git a/app/controllers/workflows_controller.rb b/app/controllers/workflows_controller.rb index e3b8bd8a9..edc89a008 100644 --- a/app/controllers/workflows_controller.rb +++ b/app/controllers/workflows_controller.rb @@ -12,7 +12,7 @@ class WorkflowsController < ApplicationController def create @workflow = Current.account.workflows.create! workflow_params # FIXME: this should definitely change. - %w[ Triage WIP On-hold ].each { |name| @workflow.stages.create! name: name } + [ "Maybe?", "Not now", "Done" ].each { |name| @workflow.stages.create! name: name } redirect_to workflows_path end diff --git a/app/helpers/bubbles_helper.rb b/app/helpers/bubbles_helper.rb index b530cab2b..22f319362 100644 --- a/app/helpers/bubbles_helper.rb +++ b/app/helpers/bubbles_helper.rb @@ -10,4 +10,15 @@ module BubblesHelper "--bubble-rotate: #{value}deg;" end + + def display_count_options + BubblesController::DISPLAY_COUNT_OPTIONS.map do |count| + { + value: count, + label: count, + selected: @display_count == count, + id: "display-count-#{count}" + } + end + end end diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb index 02808cc69..03b3c9923 100644 --- a/app/helpers/comments_helper.rb +++ b/app/helpers/comments_helper.rb @@ -6,7 +6,7 @@ module CommentsHelper def new_comment_placeholder(bubble) if bubble.creator == Current.user && bubble.messages.comments.empty? - "Add some notes…" + "Next, add some notes, context, pictures, or video about this…" else "Type your comment…" end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 99dca5909..032a5aae0 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -13,10 +13,8 @@ module EventsHelper def event_column(event) case event.action when "popped" - 4 - when "published" 3 - when "commented" + when "published" 2 else 1 @@ -53,15 +51,14 @@ module EventsHelper .where(bubbles: { bucket_id: params[:bucket_ids].presence || Current.user.bucket_ids }) headers = { - "Touched" => nil, - "Discussed" => nil, + "Updated" => nil, "Added" => accessible_events.where(action: "published").count, "Popped" => accessible_events.where(action: "popped").joins(:creator).merge(User.without_system).count } headers.map do |header, count| title = count&.positive? ? "#{header} (#{count})" : header - content_tag(:h3, title, class: "event__grid-column-title margin-block-end-half position-sticky") + content_tag(:h3, title, class: "event__grid-column-title position-sticky") end.join.html_safe end @@ -69,30 +66,30 @@ module EventsHelper case event.action when "assigned" if event.assignees.include?(Current.user) - "#{ event.creator.name } will handle #{ event.bubble.title }".html_safe + "#{ event.creator.name } will handle #{ event.bubble.title }".html_safe else - "#{ event.creator.name } assigned #{ event.assignees.pluck(:name).to_sentence } to #{ event.bubble.title }".html_safe + "#{ event.creator.name } assigned #{ event.assignees.pluck(:name).to_sentence } to #{ event.bubble.title }".html_safe end when "unassigned" - "#{ event.creator.name } unassigned #{ event.assignees.pluck(:name).to_sentence } from #{ event.bubble.title }".html_safe + "#{ event.creator.name } unassigned #{ event.assignees.pluck(:name).to_sentence } from #{ event.bubble.title }".html_safe when "boosted" - "#{ event.creator.name } boosted #{ event.bubble.title }".html_safe + "#{ event.creator.name } boosted #{ event.bubble.title }".html_safe when "commented" - "#{ event.creator.name } commented on #{ event.bubble.title }".html_safe + "#{ event.creator.name } commented on #{ event.bubble.title }".html_safe when "published" - "#{ event.creator.name } added #{ event.bubble.title }".html_safe + "#{ event.creator.name } added #{ event.bubble.title }".html_safe when "popped" - "#{ event.creator.name } popped #{ event.bubble.title }".html_safe + "#{ event.creator.name } popped #{ event.bubble.title }".html_safe when "staged" - "#{event.creator.name} changed the stage to #{event.stage_name} on #{ event.bubble.title }".html_safe + "#{event.creator.name} changed the stage to #{event.stage_name} on #{ event.bubble.title }".html_safe when "due_date_added" - "#{event.creator.name} set the date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')} on #{ event.bubble.title }".html_safe + "#{event.creator.name} set the date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')} on #{ event.bubble.title }".html_safe when "due_date_changed" - "#{event.creator.name} changed the date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')} on #{ event.bubble.title }".html_safe + "#{event.creator.name} changed the date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')} on #{ event.bubble.title }".html_safe when "due_date_removed" - "#{event.creator.name} removed the date on #{ event.bubble.title }" + "#{event.creator.name} removed the date on #{ event.bubble.title }" when "title_changed" - "#{event.creator.name} renamed on #{ event.bubble.title } (was: '#{event.particulars.dig('particulars', 'old_title')})'".html_safe + "#{event.creator.name} renamed on #{ event.bubble.title } (was: '#{event.particulars.dig('particulars', 'old_title')})'".html_safe end end end diff --git a/app/helpers/workflows_helper.rb b/app/helpers/workflows_helper.rb index bd47523de..2c58ae9ef 100644 --- a/app/helpers/workflows_helper.rb +++ b/app/helpers/workflows_helper.rb @@ -5,7 +5,7 @@ module WorkflowsHelper def button_to_set_stage(bubble, stage) button_to bucket_bubble_stagings_path(bubble.bucket, bubble, stage_id: stage), - method: :post, class: [ "btn full-width justify-start borderless workflow-stage", { "workflow-stage--current": stage == bubble.stage } ], + method: :post, class: [ "btn full-width justify-start workflow-stage txt-uppercase", { "workflow-stage--current": stage == bubble.stage } ], form_class: "flex align-center gap-half", data: { turbo_frame: "_top" } do tag.span class: "overflow-ellipsis" do diff --git a/app/models/bubble.rb b/app/models/bubble.rb index 3c1454d30..7ef3b44e2 100644 --- a/app/models/bubble.rb +++ b/app/models/bubble.rb @@ -1,6 +1,6 @@ class Bubble < ApplicationRecord include Assignable, Boostable, Colored, Commentable, Eventable, - Messages, Notifiable, Poppable, Scorable, Searchable, Staged, Statuses, Taggable, Watchable + Messages, Notifiable, Pinnable, Poppable, Scorable, Searchable, Staged, Statuses, Taggable, Watchable belongs_to :bucket, touch: true belongs_to :creator, class_name: "User", default: -> { Current.user } @@ -11,6 +11,7 @@ class Bubble < ApplicationRecord after_save :track_due_date_change, if: :saved_change_to_due_on? after_save :track_title_change, if: :saved_change_to_title? + after_create :assign_initial_stage scope :reverse_chronologically, -> { order created_at: :desc, id: :desc } scope :chronologically, -> { order created_at: :asc, id: :asc } @@ -48,4 +49,11 @@ class Bubble < ApplicationRecord }) end end + + def assign_initial_stage + if workflow_stage = bucket.account.workflows.first&.stages&.first + update! stage: workflow_stage + track_event :staged, stage_id: workflow_stage.id, stage_name: workflow_stage.name + end + end end diff --git a/app/models/bubble/colored.rb b/app/models/bubble/colored.rb index 9d95b9c04..33939840b 100644 --- a/app/models/bubble/colored.rb +++ b/app/models/bubble/colored.rb @@ -1,9 +1,9 @@ module Bubble::Colored extend ActiveSupport::Concern - COLORS = %w[ #BF1B1B #ED3F1C #ED8008 #7C956B #266ec3 #3B3633 ] + COLORS = %w[ #b7462b #ff63a8 #eb7a32 #6ac967 #2c6da8 #663251 ] included do - attribute :color, default: "#266ec3" + attribute :color, default: "#2c6da8" end end diff --git a/app/models/bubble/pinnable.rb b/app/models/bubble/pinnable.rb new file mode 100644 index 000000000..2452fadfe --- /dev/null +++ b/app/models/bubble/pinnable.rb @@ -0,0 +1,19 @@ +module Bubble::Pinnable + extend ActiveSupport::Concern + + included do + has_many :pins, dependent: :destroy + end + + def pinned_by?(user) + pins.exists?(user: user) + end + + def set_pinned(user, pinned) + if pinned + pins.find_or_create_by!(user: user) + else + pins.find_by(user: user)&.destroy + end + end +end diff --git a/app/models/bucket.rb b/app/models/bucket.rb index 03ff87af8..16f1f1d63 100644 --- a/app/models/bucket.rb +++ b/app/models/bucket.rb @@ -9,5 +9,15 @@ class Bucket < ApplicationRecord validates_presence_of :name + after_create :ensure_workflow_exists + scope :alphabetically, -> { order(name: :asc) } + + private + def ensure_workflow_exists + unless account.workflows.exists? + workflow = account.workflows.create!(name: "Default Workflow") + [ "Maybe?", "Not now", "Done" ].each { |name| workflow.stages.create!(name: name) } + end + end end diff --git a/app/models/concerns/filter/params.rb b/app/models/concerns/filter/params.rb new file mode 100644 index 000000000..6c1ff2a8e --- /dev/null +++ b/app/models/concerns/filter/params.rb @@ -0,0 +1,22 @@ +module Filter::Params + extend ActiveSupport::Concern + + PERMITTED_PARAMS = %i[ + indexed_by assignment_status bucket_ids creator_ids + assignee_ids stage_ids tag_ids terms display_count + ].freeze + + def as_params + self.class.normalize_params( + indexed_by: indexed_by, + assignment_status: assignment_status, + bucket_ids: buckets.ids, + creator_ids: creators.ids, + assignee_ids: assignees.ids, + stage_ids: stages.ids, + tag_ids: tags.ids, + terms: terms, + display_count: display_count + ) + end +end diff --git a/app/models/pin.rb b/app/models/pin.rb new file mode 100644 index 000000000..5bf4635c8 --- /dev/null +++ b/app/models/pin.rb @@ -0,0 +1,12 @@ +class Pin < ApplicationRecord + belongs_to :bubble + belongs_to :user + + scope :ordered, -> { order(created_at: :desc) } + + after_create_commit -> { broadcast_prepend_later_to [ user, :pins ], + target: "pins", + partial: "bubbles/pins/pin" + } + after_destroy_commit -> { broadcast_remove_to [ user, :pins ] } +end diff --git a/app/models/user.rb b/app/models/user.rb index 3414ead00..a79e4729e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -23,6 +23,9 @@ class User < ApplicationRecord has_one_attached :avatar + has_many :pins, dependent: :destroy + has_many :pinned_bubbles, through: :pins, source: :bubble + normalizes :email_address, with: ->(value) { value.strip.downcase } after_create_commit :grant_access_to_buckets diff --git a/app/views/accounts/users/index.html.erb b/app/views/accounts/users/index.html.erb index f91446378..07339321d 100644 --- a/app/views/accounts/users/index.html.erb +++ b/app/views/accounts/users/index.html.erb @@ -2,20 +2,16 @@ <% content_for :header do %>