Merge branch 'main' into mobile-app/scoped-stylesheets

* main: (85 commits)
  Fix crash when inserting an emoji into a max length reaction
  change the object type in the example code (#1824)
  We can't hide public boards like this
  Consistent breakpoints for min- and max-width
  Repair tooltip z-index selector
  Fix bin/setup to explicitly use bash syntax for mise hook-env (#1813)
  Update AGENTS.md (#1818)
  Bundler: normalize platforms (#1822)
  Robots, begone (#1812)
  Account for input padding on autoresize pseudo-element
  Updated database.sqlite.yml
  (account): encode external_account_id in slug
  Allow typing magic links on mobile
  Add contributing guide
  Disconnect action cable when user is deactivated
  Disable ARM image builds
  Disable image builds for PRs
  Add libssl-dev
  Add automated Docker image builds
  Make the jump nav hotkeys look like the others
  ...
This commit is contained in:
Adrien Maston
2025-12-03 10:57:27 +01:00
130 changed files with 1302 additions and 310 deletions
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Features, Bug Reports, Questions
url: https://github.com/basecamp/fizzy/discussions/new/choose
about: Please use the discussions area to report issues or ask quest
+8
View File
@@ -0,0 +1,8 @@
---
name: Pre-Discussed and Approved Topics
about: |-
For topics already discussed and approved in the GitHub Discussions section.
---
** PLEASE START A DISCUSSION INSTEAD OF OPENING AN ISSUE **
** For more details see CONTRIBUTING.md **
+196
View File
@@ -0,0 +1,196 @@
name: Build and publish container image to GHCR
on:
push:
branches:
- main
tags:
- 'v*'
workflow_dispatch:
concurrency:
group: publish-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
packages: write
id-token: write
attestations: write
env:
IMAGE_DESCRIPTION: Fizzy is Kanban as it should be. Not as it has been.
SOURCE_URL: https://github.com/${{ github.repository }}
jobs:
build:
name: Build and push image (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
platform: linux/amd64
arch: amd64
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
steps:
- name: Checkout
uses: actions/checkout@v5.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1
- name: Log in to GHCR
uses: docker/login-action@v3.5.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute canonical image name (lowercase)
id: vars
shell: bash
run: |
set -eu
IMAGE_REF="${IMAGE_NAME:-$GITHUB_REPOSITORY}"
CANONICAL_IMAGE="${REGISTRY}/${IMAGE_REF,,}"
echo "canonical=${CANONICAL_IMAGE}" >> "$GITHUB_OUTPUT"
- name: Extract Docker metadata (tags, labels) with arch suffix
id: meta
uses: docker/metadata-action@v5.8.0
with:
images: ${{ steps.vars.outputs.canonical }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,format=short,prefix=sha-
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=semver,pattern={{major}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
flavor: |
latest=false
suffix=-${{ matrix.arch }}
labels: |
org.opencontainers.image.source=${{ env.SOURCE_URL }}
- name: Build and push (${{ matrix.platform }})
id: build
uses: docker/build-push-action@v6.18.0
with:
context: .
file: Dockerfile
build-args: |
OCI_SOURCE=${{ env.SOURCE_URL }}
OCI_DESCRIPTION=${{ env.IMAGE_DESCRIPTION }}
platforms: ${{ matrix.platform }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,scope=${{ matrix.platform }},mode=max
sbom: false
provenance: false
- name: Attest image provenance (per-arch)
if: github.event_name != 'pull_request'
uses: actions/attest-build-provenance@v3.0.0
with:
subject-name: ${{ steps.vars.outputs.canonical }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: false
manifest:
name: Create multi-arch manifest and sign
needs: build
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 20
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
steps:
- name: Set up Docker Buildx (for imagetools)
uses: docker/setup-buildx-action@v3.11.1
- name: Log in to GHCR
uses: docker/login-action@v3.5.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute canonical image name (lowercase)
id: vars
shell: bash
run: |
set -eu
IMAGE_REF="${IMAGE_NAME:-$GITHUB_REPOSITORY}"
CANONICAL_IMAGE="${REGISTRY}/${IMAGE_REF,,}"
echo "canonical=${CANONICAL_IMAGE}" >> "$GITHUB_OUTPUT"
- name: Compute base tags (no suffix)
id: meta
uses: docker/metadata-action@v5.8.0
with:
images: ${{ steps.vars.outputs.canonical }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,format=short,prefix=sha-
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=semver,pattern={{major}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
flavor: |
latest=false
labels: |
org.opencontainers.image.source=${{ env.SOURCE_URL }}
- name: Create multi-arch manifests
shell: bash
run: |
set -eu
tags="${{ steps.meta.outputs.tags }}"
echo "Creating manifests for tags:"
printf '%s\n' "$tags"
while IFS= read -r tag; do
[ -z "$tag" ] && continue
echo "Creating manifest for $tag"
src_tag="$tag"
if [[ "$tag" == *:latest && "${GITHUB_REF}" == refs/tags/* ]]; then
ref="${GITHUB_REF#refs/tags/}"
src_tag="${tag%:latest}:$ref"
fi
if [ -n "${IMAGE_DESCRIPTION:-}" ]; then
docker buildx imagetools create \
--tag "$tag" \
--annotation "index:org.opencontainers.image.description=${IMAGE_DESCRIPTION}" \
"${src_tag}-amd64"
else
docker buildx imagetools create \
--tag "$tag" \
"${src_tag}-amd64"
fi
done <<< "$tags"
- name: Install Cosign
uses: sigstore/cosign-installer@v3.9.2
- name: Cosign sign all tags (keyless OIDC)
shell: bash
run: |
set -eu
tags="${{ steps.meta.outputs.tags }}"
printf '%s\n' "$tags"
while IFS= read -r tag; do
[ -z "$tag" ] && continue
echo "Signing $tag"
cosign sign --yes "$tag"
done <<< "$tags"
+1 -1
View File
@@ -144,7 +144,7 @@ Key recurring tasks (via `config/recurring.yml`):
URL: `http://fizzy.localhost:3006`
Login: david@37signals.com (passwordless magic link auth - check rails console for link)
Use Chrome MCP tools to interact with the running dev app for UI testing and debugging.``
Use Chrome MCP tools to interact with the running dev app for UI testing and debugging.
## Coding style
+49
View File
@@ -0,0 +1,49 @@
# How to contribute to Fizzy
Fizzy uses GitHub
[discussions](https://github.com/basecamp/fizzy/discussions) to track
feature requests and questions, rather than [the issue
tracker](https://github.com/basecamp/fizzy/issues). If you're considering
opening an issue or pull request, please open a discussion instead.
Whenever a discussion leads to an actionable and well-understood task, we'll
move it to the issue tracker where it can be worked on.
This is a little different than how some other projects work, but it makes it
easier for us to triage and prioritise the work. It also means that the open
issues all represent agreed-upon tasks that are either being worked on, or are
ready to be worked on.
This should also make it easier to see what's in progress, and to find
something to work on if you'd like to do so.
## What this means in practice
### If you'd like to contribute to the code...
1. If you're interested in working on one of the open issues, please do! We are
grateful for the help!
2. You'll want to make sure someone else isn't already working on the same
issue. If they are, it will be tagged "in progress" and/or it should be clear
from the comments. When in doubt, you can always comment on the issue to ask.
3. Similarly, if you need any help or guidance on the issue, please comment on
the issue as you go, and we'll do our best to help.
4. When you have something ready for review or collaboration, open a PR.
### If you've found a bug...
1. If you don't have steps to reproduce the problem, or you're not certain it's a
bug, open a discussion.
2. If you have steps to reproduce, open an issue.
### If you have an idea for a feature...
1. Open a discussion.
### If you have a question, or are having trouble with configuration...
1. Open a discussion.
Hopefully this process makes it easier for everyone to be involved. Thanks for
helping! ❤️
+8 -1
View File
@@ -16,7 +16,7 @@ WORKDIR /rails
# Install base packages
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 libssl-dev && \
ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
@@ -59,6 +59,13 @@ RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
# Final stage for app image
FROM base
# Image metadata
ARG OCI_DESCRIPTION
LABEL org.opencontainers.image.description="${OCI_DESCRIPTION}"
ARG OCI_SOURCE
LABEL org.opencontainers.image.source="${OCI_SOURCE}"
LABEL org.opencontainers.image.licenses="O'Saasy"
# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash
+1
View File
@@ -33,6 +33,7 @@ gem "platform_agent"
gem "aws-sdk-s3", require: false
gem "web-push"
gem "net-http-persistent"
gem "rubyzip", require: "zip"
gem "mittens"
gem "useragent", bc: "useragent"
+38 -8
View File
@@ -144,8 +144,8 @@ GEM
brakeman (7.1.1)
racc
builder (3.3.0)
bundler-audit (0.9.2)
bundler (>= 1.2.0, < 3)
bundler-audit (0.9.3)
bundler (>= 1.2.0)
thor (~> 1.0)
capybara (3.40.0)
addressable
@@ -179,7 +179,13 @@ GEM
faker (3.5.2)
i18n (>= 1.8.11, < 2)
ffi (1.17.2)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu)
ffi (1.17.2-arm-linux-musl)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86-linux-gnu)
ffi (1.17.2-x86-linux-musl)
ffi (1.17.2-x86_64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
ffi (1.17.2-x86_64-linux-musl)
@@ -285,6 +291,14 @@ GEM
nokogiri (1.18.10)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.18.10-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.10-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.10-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-darwin)
@@ -347,7 +361,7 @@ GEM
io-console (~> 0.5)
rexml (3.4.4)
rouge (4.6.1)
rqrcode (3.1.0)
rqrcode (3.1.1)
chunky_png (~> 1.0)
rqrcode_core (~> 2.0)
rqrcode_core (2.0.1)
@@ -408,12 +422,18 @@ GEM
fugit (~> 1.11)
railties (>= 7.1)
thor (>= 1.3.1)
sqlite3 (2.8.0)
sqlite3 (2.8.1)
mini_portile2 (~> 2.8.0)
sqlite3 (2.8.0-arm64-darwin)
sqlite3 (2.8.0-x86_64-darwin)
sqlite3 (2.8.0-x86_64-linux-gnu)
sqlite3 (2.8.0-x86_64-linux-musl)
sqlite3 (2.8.1-aarch64-linux-gnu)
sqlite3 (2.8.1-aarch64-linux-musl)
sqlite3 (2.8.1-arm-linux-gnu)
sqlite3 (2.8.1-arm-linux-musl)
sqlite3 (2.8.1-arm64-darwin)
sqlite3 (2.8.1-x86-linux-gnu)
sqlite3 (2.8.1-x86-linux-musl)
sqlite3 (2.8.1-x86_64-darwin)
sqlite3 (2.8.1-x86_64-linux-gnu)
sqlite3 (2.8.1-x86_64-linux-musl)
sshkit (1.24.0)
base64
logger
@@ -426,6 +446,7 @@ GEM
stringio (3.1.8)
thor (1.4.0)
thruster (0.1.16)
thruster (0.1.16-aarch64-linux)
thruster (0.1.16-arm64-darwin)
thruster (0.1.16-x86_64-darwin)
thruster (0.1.16-x86_64-linux)
@@ -465,8 +486,16 @@ GEM
zeitwerk (2.7.3)
PLATFORMS
aarch64-linux
aarch64-linux-gnu
aarch64-linux-musl
arm-linux-gnu
arm-linux-musl
arm64-darwin
arm64-linux
ruby
x86-linux-gnu
x86-linux-musl
x86_64-darwin
x86_64-linux
x86_64-linux-gnu
@@ -503,6 +532,7 @@ DEPENDENCIES
rouge
rqrcode
rubocop-rails-omakase
rubyzip
selenium-webdriver
solid_cable (>= 3.0)
solid_cache (~> 1.0)
+14 -1
View File
@@ -1,6 +1,6 @@
GIT
remote: https://github.com/basecamp/fizzy-saas
revision: 130c8b23f9861a89feb59ca68f192bd05d51310c
revision: 6dd8bb616632edfabc3d814f88f0658d96486e17
specs:
fizzy-saas (0.1.0)
prometheus-client-mmap
@@ -239,6 +239,7 @@ GEM
ffi (1.17.2-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu)
ffi (1.17.2-arm-linux-musl)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
ffi (1.17.2-x86_64-linux-musl)
fugit (1.12.1)
@@ -347,6 +348,8 @@ GEM
racc (~> 1.4)
nokogiri (1.18.10-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.10-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-musl)
@@ -379,6 +382,11 @@ GEM
bigdecimal
logger
rb_sys (~> 0.9.117)
prometheus-client-mmap (1.3.0-arm64-darwin)
base64
bigdecimal
logger
rb_sys (~> 0.9.117)
prometheus-client-mmap (1.3.0-x86_64-linux-gnu)
base64
bigdecimal
@@ -508,6 +516,7 @@ GEM
sqlite3 (2.8.0-aarch64-linux-musl)
sqlite3 (2.8.0-arm-linux-gnu)
sqlite3 (2.8.0-arm-linux-musl)
sqlite3 (2.8.0-arm64-darwin)
sqlite3 (2.8.0-x86_64-linux-gnu)
sqlite3 (2.8.0-x86_64-linux-musl)
sshkit (1.24.0)
@@ -523,6 +532,7 @@ GEM
thor (1.4.0)
thruster (0.1.16)
thruster (0.1.16-aarch64-linux)
thruster (0.1.16-arm64-darwin)
thruster (0.1.16-x86_64-linux)
timeout (0.4.4)
trilogy (2.9.0)
@@ -593,6 +603,8 @@ PLATFORMS
aarch64-linux-musl
arm-linux-gnu
arm-linux-musl
arm64-darwin
x86_64-linux
x86_64-linux-gnu
x86_64-linux-musl
@@ -632,6 +644,7 @@ DEPENDENCIES
rouge
rqrcode
rubocop-rails-omakase
rubyzip
selenium-webdriver
sentry-rails
sentry-ruby
+5 -4
View File
@@ -35,14 +35,15 @@ The full continuous integration tests can be run with:
### Database configuration
Fizzy supports SQLite (default, recommended for most scenarios) and MySQL. You can switch adapters with the `DATABASE_ADAPTER` environment variable.
Fizzy works with SQLite by default and supports MySQL too. You can switch adapters with the `DATABASE_ADAPTER` environment variable. For example, to develop locally against MySQL:
```sh
DATABASE_ADAPTER=mysql bin/rails
DATABASE_ADAPTER=mysql bin/test
bin/ci # Runs tests against both SQLite and MySQL
DATABASE_ADAPTER=mysql bin/setup --reset
DATABASE_ADAPTER=mysql bin/ci
```
The remote CI pipeline will run tests against both SQLite and MySQL.
### Outbound Emails
You can view email previews at http://fizzy.localhost:3006/rails/mailers.
+1 -1
View File
@@ -126,7 +126,7 @@ end
If a module only has private methods, we mark it `private` at the top and add an extra new line after but don't indent.
```ruby
class SomeModule
module SomeModule
private
def some_private_method
+2 -1
View File
@@ -3,7 +3,7 @@
.autoresize__wrapper {
display: grid !important;
position: relative;
> *, &::after {
grid-area: 1 / 1 / 2 / 2;
}
@@ -12,6 +12,7 @@
content: attr(data-autoresize-clone-value) " ";
font: inherit;
line-height: inherit;
padding-block: var(--autosize-block-padding, 0);
visibility: hidden;
white-space: pre-wrap;
}
+1 -1
View File
@@ -53,7 +53,7 @@
}
::selection {
background-color: var(--color-selected);
background: var(--color-selected);
@media (prefers-color-scheme: dark) {
background-color: var(--color-selected-dark);
+1 -1
View File
@@ -104,7 +104,7 @@
}
/* Make a normal button circular on mobile */
@media (max-width: 640px) {
@media (max-width: 639px) {
.btn--circle-mobile {
aspect-ratio: 1;
padding: 0.5em;
+8 -5
View File
@@ -38,8 +38,10 @@
min-block-size: 20lh;
}
@media (max-width: 519px) {
display: none;
body:not(.public) & {
@media (max-width: 519px) {
display: none;
}
}
}
@@ -486,7 +488,6 @@
}
.card__title {
line-height: 1.2;
pointer-events: none;
}
@@ -728,8 +729,10 @@
text-transform: uppercase;
@media (hover: hover) {
&:hover {
--btn-background: color(from var(--card-color) srgb r g b / 0.15);
&:not(.card__column-name--current):hover {
--btn-background: color(from var(--column-color) srgb r g b / 0.15);
color: var(--column-color);
}
}
}
+21 -5
View File
@@ -58,11 +58,27 @@
.card__title {
font-size: clamp(var(--text-medium), 6vw, var(--text-x-large));
margin-block-end: 0.5ch;
/* With tight line spacing, Windows will cover over adjacent lines of text
* when input text is selected. Here, we're setting the selection color to a
* transparent value so the overlapping text lines are at least visible. */
::selection {
background: oklch(var(--lch-blue-light) / 0.5);
}
&:has(textarea) {
margin-block-end: 0;
@supports not (field-sizing: content) {
text-wrap: unset; /* Safari is annoying if you have text-wrap: balance in textareas */
}
}
}
.card__meta,
.card__stages {
@media (min-width: 639px) {
@media (min-width: 640px) {
font-size: var(--text-small);
}
}
@@ -103,14 +119,14 @@
inset: calc(var(--bubble-size) / -4) calc(var(--bubble-size) / 1.5) auto auto;
translate: 0 0;
@media (max-width: 800px) {
@media (max-width: 799px) {
--bubble-size: 4.5rem;
inset: calc(var(--bubble-size) / 1.5) 0 auto auto;
}
}
@media (max-width: 800px) {
@media (max-width: 799px) {
--padding-inline: var(--main-padding);
column-gap: 0;
@@ -145,7 +161,7 @@
z-index: 1;
}
&:has([data-controller="tooltip"]:hover) {
&:has([data-controller~="tooltip"]:hover) {
z-index: var(--z-tooltip);
}
}
@@ -153,7 +169,7 @@
.card-perma__actions--left { grid-area: actions-left; }
.card-perma__actions--right { grid-area: actions-right; }
@media (max-width: 800px) {
@media (max-width: 799px) {
.card-perma__actions {
display: flex;
padding-inline: var(--padding-inline);
+5 -11
View File
@@ -143,6 +143,7 @@
}
.card__title {
--autosize-block-padding: 0 0.5ch;
--input-border-radius: 0;
--input-color: var(--card-content-color);
--lines: 3;
@@ -150,18 +151,13 @@
color: var(--card-content-color);
font-size: var(--text-xx-large);
font-weight: 900;
line-height: 1.3;
/* We need a minimum line-height of 1.3 to avoid scrolling
in the card title input and cut-off descenders on Windows */
line-height: 1.15;
text-wrap: balance;
@supports not (field-sizing: content) {
&:has(textarea) {
text-wrap: unset; /* Safari is annoying if you have text-wrap: balance in textareas */
}
}
.card-field__title {
overflow: hidden; /* prevent scrolling on windows */
padding-block: var(--autosize-block-padding);
&:is(textarea)::placeholder {
color: inherit;
opacity: 0.66;
@@ -181,7 +177,6 @@
lexxy-toolbar {
border-block-start: 1px solid var(--color-ink-light);
margin-block-start: var(--block-space-half) !important;
}
& ~ .btn.btn--reversed {
@@ -366,7 +361,6 @@
color: var(--stamp-color);
font-size: 1.3em;
font-weight: 900;
line-height: 1.2;
position: relative;
text-align: center;
text-transform: uppercase;
-14
View File
@@ -45,10 +45,6 @@
.comment__avatar {
margin: calc(var(--comment-padding-block) * 0.75) calc(var(--comment-padding-inline) * -0.75);
z-index: 0;
.comment--mine_ & {
margin-inline: calc(var(--comment-padding-inline) * -0.75);
}
}
.comment__body {
@@ -82,16 +78,6 @@
.comment__edit {
background-color: var(--color-ink-lightest);
.comment:not(.comment--mine) & {
display: none;
}
}
.comment--mine {
.comment__reaction {
display: none;
}
}
.comment--system {
+14 -2
View File
@@ -102,6 +102,19 @@
}
}
.nav__section {
border-block-start: 1px solid var(--color-ink-lighter);
font-size: var(--text-small);
&[open] {
padding-block-end: 0.4rem;
}
}
.nav__section--secret:not([data-is-filtering]) {
display: none;
}
.nav__header {
--actions-width: 2rem;
@@ -176,9 +189,8 @@
text-align: center;
kbd {
border: none;
box-shadow: none;
inset: 0.66em 0.33em auto auto;
line-height: 1.4;
opacity: 0.5;
position: absolute;
+10 -6
View File
@@ -5,7 +5,7 @@
--panel-padding: var(--block-space);
--panel-size: auto;
--popup-icon-size: 24px;
--popup-item-padding-inline: 0.5rem;
--popup-item-padding-inline: 0.4rem;
--popup-display: flex;
inset: 0 auto auto 50%;
@@ -53,10 +53,10 @@
.popup__section-title {
background: var(--color-canvas);
font-size: var(--text-small);
font-weight: bold;
font-weight: 600;
inset-block-start: 0;
list-style: none;
padding: var(--block-space-half) var(--inline-space-half);
padding: 0.75ch var(--inline-space-half);
position: sticky;
text-transform: uppercase;
z-index: 1;
@@ -65,8 +65,7 @@
align-items: center;
cursor: pointer;
display: flex;
gap: 2ch;
justify-content: space-between;
gap: 0.5ch;
}
&::-webkit-details-marker {
@@ -74,7 +73,8 @@
}
.icon--caret-down {
opacity: 0.66;
font-size: 1ch;
margin-inline-start: -0.5ch;
transition: rotate 150ms ease-out;
}
@@ -94,6 +94,10 @@
max-inline-size: 100%;
padding: 0;
row-gap: 1px;
details & {
margin-inline-start: 0.75ch;
}
}
.popup__item {
+1 -1
View File
@@ -190,7 +190,7 @@
gap: var(--inline-space-half);
grid-template-columns: repeat(10, 1fr);
@media (max-width: 630px) {
@media (max-width: 639px) {
grid-template-columns: repeat(6, 1fr);
}
+4
View File
@@ -405,6 +405,10 @@
opacity: 1;
}
}
[aria-busy] & {
position: absolute !important;
}
}
.avatar,
@@ -0,0 +1,23 @@
class Account::ExportsController < ApplicationController
before_action :ensure_export_limit_not_exceeded, only: :create
before_action :set_export, only: :show
CURRENT_EXPORT_LIMIT = 10
def show
end
def create
Current.account.exports.create!(user: Current.user).build_later
redirect_to account_settings_path, notice: "Export started. You'll receive an email when it's ready."
end
private
def ensure_export_limit_not_exceeded
head :too_many_requests if Current.user.exports.current.count >= CURRENT_EXPORT_LIMIT
end
def set_export
@export = Current.account.exports.completed.find_by(id: params[:id], user: Current.user)
end
end
@@ -14,5 +14,7 @@ class Admin::StatsController < AdminController
.where("cards_count > 0")
.order(cards_count: :desc)
.limit(20)
@recent_accounts = Account.order(created_at: :desc).limit(10)
end
end
@@ -1,6 +1,7 @@
class ApplicationController < ActionController::Base
include Authentication
include Authorization
include BlockSearchEngineIndexing
include CurrentRequest, CurrentTimezone, SetPlatform
include RequestForgeryProtection
include TurboFlash, ViewTransitions
@@ -4,6 +4,7 @@ module Authentication
included do
before_action :require_account # Checking and setting account must happen first
before_action :require_authentication
after_action :ensure_development_magic_link_not_leaked
helper_method :authenticated?
etag { Current.session.id if authenticated? }
@@ -89,4 +90,16 @@ module Authentication
Current.session.destroy
cookies.delete(:session_token)
end
def ensure_development_magic_link_not_leaked
unless Rails.env.development?
raise "Leaking magic link via flash in #{Rails.env}?" if flash[:magic_link_code].present?
end
end
def serve_development_magic_link(magic_link)
if Rails.env.development?
flash[:magic_link_code] = magic_link&.code
end
end
end
@@ -0,0 +1,14 @@
# Tell crawlers like Googlebot to drop pages entirely from search results, even
# if other sites link to it
module BlockSearchEngineIndexing
extend ActiveSupport::Concern
included do
after_action :block_search_engine_indexing
end
private
def block_search_engine_indexing
headers["X-Robots-Tag"] = "none"
end
end
+10 -6
View File
@@ -20,12 +20,7 @@ class JoinCodesController < ApplicationController
elsif identity == Current.identity
redirect_to new_users_join_url(script_name: @join_code.account.slug)
else
terminate_session if Current.identity
magic_link = identity.send_magic_link
flash[:magic_link_code] = magic_link&.code if Rails.env.development?
session[:return_to_after_authenticating] = new_users_join_url(script_name: @join_code.account.slug)
logout_and_send_new_magic_link(identity)
redirect_to session_magic_link_url(script_name: nil)
end
end
@@ -42,4 +37,13 @@ class JoinCodesController < ApplicationController
render :inactive, status: :gone
end
end
def logout_and_send_new_magic_link(identity)
terminate_session if Current.identity
magic_link = identity.send_magic_link
serve_development_magic_link(magic_link)
session[:return_to_after_authenticating] = new_users_join_url(script_name: @join_code.account.slug)
end
end
+1 -1
View File
@@ -11,7 +11,7 @@ class SessionsController < ApplicationController
def create
if identity = Identity.find_by_email_address(email_address)
magic_link = identity.send_magic_link
flash[:magic_link_code] = magic_link&.code if Rails.env.development?
serve_development_magic_link(magic_link)
end
redirect_to session_magic_link_path
-8
View File
@@ -1,12 +1,4 @@
class SignupsController < ApplicationController
# FIXME: Remove this before launch!
unless Rails.env.local?
http_basic_authenticate_with \
name: Rails.application.credentials.account_signup_http_basic_auth.name,
password: Rails.application.credentials.account_signup_http_basic_auth.password,
realm: "Fizzy Signup"
end
disallow_account_scope
allow_unauthenticated_access
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_signup_path, alert: "Try again later." }
+1 -1
View File
@@ -22,7 +22,7 @@ module ApplicationHelper
def back_link_to(label, url, action, **options)
link_to url, class: "btn btn--back", data: { controller: "hotkey", action: action }, **options do
icon_tag("arrow-left") + tag.strong("Back to #{label}", class: "overflow-ellipsis") + tag.kbd("", class: "txt-x-small hide-on-touch").html_safe
icon_tag("arrow-left") + tag.strong("Back to #{label}", class: "overflow-ellipsis") + tag.kbd("ESC", class: "txt-x-small hide-on-touch").html_safe
end
end
end
+1 -1
View File
@@ -1,6 +1,6 @@
module BoardsHelper
def link_back_to_board(board)
back_link_to board.name, board, "keydown.left@document->hotkey#click click->turbo-navigation#backIfSamePath"
back_link_to board.name, board, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click click->turbo-navigation#backIfSamePath"
end
def link_to_edit_board(board)
+3 -3
View File
@@ -39,9 +39,9 @@ module CardsHelper
def card_social_tags(card)
tag.meta(property: "og:title", content: "#{card.title} | #{card.board.name}") +
tag.meta(property: "og:description", content: format_excerpt(@card&.description, length: 200)) +
tag.meta(property: "og:image", content: @card.image.attached? ? "#{request.base_url}#{url_for(@card.image)}" : "#{request.base_url}/app-icon.png") +
tag.meta(property: "og:url", content: card_url(@card))
tag.meta(property: "og:description", content: format_excerpt(card&.description, length: 200)) +
tag.meta(property: "og:image", content: card.image.attached? ? "#{request.base_url}#{url_for(card.image)}" : "#{request.base_url}/opengraph.png") +
tag.meta(property: "og:url", content: card_url(card))
end
def button_to_remove_card_image(card)
+1
View File
@@ -5,6 +5,7 @@ module ColumnsHelper
card_triage_path(card, column_id: column),
method: :post,
class: [ "card__column-name btn", { "card__column-name--current": column == card.column && card.open? } ],
style: "--column-color: #{column.color}",
form_class: "flex align-center gap-half",
data: { turbo_frame: "_top" }
end
+2 -2
View File
@@ -41,10 +41,10 @@ module FiltersHelper
end
def collapsible_nav_section(title, **properties, &block)
tag.details class: "popup__section", data: { action: "toggle->nav-section-expander#toggle", nav_section_expander_target: "section", nav_section_expander_key_value: title.parameterize }, open: true, **properties do
tag.details class: "nav__section popup__section", data: { action: "toggle->nav-section-expander#toggle", nav_section_expander_target: "section", nav_section_expander_key_value: title.parameterize }, open: true, **properties do
concat(tag.summary(class: "popup__section-title") do
concat title
concat icon_tag "caret-down"
concat title
end)
concat(tag.ul(class: "popup__list") do
capture(&block)
-2
View File
@@ -6,8 +6,6 @@ module HotkeysHelper
""
elsif key == "enter"
platform.mac? ? "return" : "enter"
elsif key == "shift"
""
else
key
end.capitalize
+1 -5
View File
@@ -3,10 +3,6 @@ module MessagesHelper
turbo_frame_tag dom_id(card, :messages),
class: "comments gap center",
style: "--card-color: #{card.color}",
role: "group", aria: { label: "Messages" },
data: {
controller: "created-by-current-user",
created_by_current_user_mine_class: "comment--mine"
}, &
role: "group", aria: { label: "Messages" }, &
end
end
+3 -1
View File
@@ -25,7 +25,9 @@ module My::MenuHelper
def my_menu_tag_item(the_tag)
my_menu_item("tag", tag) do
link_to(tag.span(the_tag.title, class: "overflow-ellipsis"), cards_path(tag_ids: [ the_tag ]), class: "popup__btn btn")
link_to(tag.span(class: "overflow-ellipsis") do
tag.span("##{the_tag.title}", class: "visually-hidden") + the_tag.title
end, cards_path(tag_ids: [ the_tag ]), class: "popup__btn btn", title: "##{the_tag.title}")
end
end
@@ -0,0 +1,7 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
this.element.click()
}
}
@@ -1,12 +0,0 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "creation" ]
static classes = [ "mine" ]
creationTargetConnected(element) {
if (element.dataset.creatorId == Current.user.id) {
element.classList.add(this.mineClass)
}
}
}
@@ -4,16 +4,12 @@ import { onNextEventLoopTick } from "helpers/timing_helpers"
export default class extends Controller {
static targets = [ "input" ]
submit(event) {
submit() {
onNextEventLoopTick(() => {
this.inputTarget.disabled = true
})
}
paste(event) {
onNextEventLoopTick(() => {
this.element.submit()
this.inputTarget.disabled = true
if (!this.inputTarget.disabled) {
this.element.submit()
this.inputTarget.disabled = true
}
})
}
}
@@ -26,12 +26,14 @@ export default class extends Controller {
refresh() {
this.labelTarget.textContent = this.#selectedLabel
this.#updateHiddenFields()
this.#updateFilterShow()
}
clear(event) {
this.#deselectAll()
this.#updateHiddenFields()
this.labelTarget.textContent = this.#selectedLabel
this.#updateFilterShow()
}
get #selectedLabel() {
@@ -80,6 +82,7 @@ export default class extends Controller {
#updateHiddenFields() {
this.#clearHiddenFields()
this.#addHiddenFields()
this.#updateFilterShow()
}
#deselectAll() {
@@ -122,4 +125,9 @@ export default class extends Controller {
this.element.appendChild(field)
})
}
#updateFilterShow() {
const hasSelection = this.#selectedValues().length > 0
this.element.setAttribute("data-filter-show", hasSelection)
}
}
@@ -6,7 +6,12 @@ export default class extends Controller {
insertEmoji(event) {
const emojiChar = event.target.getAttribute("data-emoji")
const value = this.inputTarget.value
this.inputTarget.value = `${value}${emojiChar}`
const newValue = `${value}${emojiChar}`
if (this.inputTarget.maxLength > 0 && newValue.length <= this.inputTarget.maxLength) {
this.inputTarget.value = newValue
}
this.inputTarget.focus()
}
}
+7
View File
@@ -0,0 +1,7 @@
class ExportAccountDataJob < ApplicationJob
queue_as :backend
def perform(export)
export.build
end
end
+8
View File
@@ -0,0 +1,8 @@
class ExportMailer < ApplicationMailer
def completed(export)
@export = export
@user = export.user
mail to: @user.identity.email_address, subject: "Your Fizzy data export is ready for download"
end
end
+2 -1
View File
@@ -8,6 +8,7 @@ class Account < ApplicationRecord
has_many :webhooks, dependent: :destroy
has_many :tags, dependent: :destroy
has_many :columns, dependent: :destroy
has_many :exports, class_name: "Account::Export", dependent: :destroy
has_many_attached :uploads
@@ -26,7 +27,7 @@ class Account < ApplicationRecord
end
def slug
"/#{external_account_id}"
"/#{AccountSlug.encode(external_account_id)}"
end
def account
+73
View File
@@ -0,0 +1,73 @@
class Account::Export < ApplicationRecord
belongs_to :account
belongs_to :user
has_one_attached :file
enum :status, %w[ pending processing completed failed ].index_by(&:itself), default: :pending
scope :current, -> { where(created_at: 24.hours.ago..) }
scope :expired, -> { where(completed_at: ...24.hours.ago) }
def self.cleanup
expired.destroy_all
end
def build_later
ExportAccountDataJob.perform_later(self)
end
def build
processing!
zipfile = generate_zip
file.attach io: File.open(zipfile.path), filename: "fizzy-export-#{id}.zip", content_type: "application/zip"
mark_completed
ExportMailer.completed(self).deliver_later
rescue => e
update!(status: :failed)
raise
ensure
zipfile&.close
zipfile&.unlink
end
def mark_completed
update!(status: :completed, completed_at: Time.current)
end
private
def generate_zip
Tempfile.new([ "export", ".zip" ]).tap do |tempfile|
Zip::File.open(tempfile.path, create: true) do |zip|
exportable_cards.find_each do |card|
add_card_to_zip(zip, card)
end
end
end
end
def exportable_cards
user.accessible_cards.includes(
:board,
creator: :identity,
comments: { creator: :identity },
rich_text_description: { embeds_attachments: :blob }
)
end
def add_card_to_zip(zip, card)
zip.get_output_stream("#{card.number}.json") do |f|
f.write(card.export_json)
end
card.export_attachments.each do |attachment|
zip.get_output_stream(attachment[:path], compression_method: Zip::Entry::STORED) do |f|
attachment[:blob].download { |chunk| f.write(chunk) }
end
rescue ActiveStorage::FileNotFoundError
# Skip attachments where the file is missing from storage
end
end
end
+1 -12
View File
@@ -30,43 +30,36 @@ class Account::Seeder
# Cards
playground.cards.create! creator: creator, title: "Finally, watch this Fizzy orientation video", status: "published", description: <<~HTML
<p>Theres a whole lot more you can do in Fizzy. In the video below 37signals founder and CEO, Jason Fried, will walk you through the basics in just 8 minutes.</p>
<p><br></p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/videos/fizzyorientation-4k.mp4" caption="Fizzy orientation" content-type="video/mp4" filename="fizzyorientation-4k.mp4"></action-text-attachment>
HTML
playground.cards.create! creator: creator, title: "Now, grab the invite link to invite someone else", status: "published", description: <<~HTML
<p>Open Fizzy menu, select <b><strong>+ Add people</b></strong>, then copy the invite link. You can give this link to someone else so they can make an login for themselves in your account.</p>
<p><br></p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/invite-link.gif" alt="Demo of copying invite link" caption="Get a link to invite co-workers" content-type="image/*" filename="invite-link.gif" presentation="gallery"></action-text-attachment>
HTML
playground.cards.create! creator: creator, title: "Then, head back home to check out activity", status: "published", description: <<~HTML
<p>Hit 1 or pull down the Fizzy menu and select Home.</p>
<p><br></p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/back-to-home.gif" alt="Demo of visiting Home" caption="Go back to Home for Latest Activity" content-type="image/*" filename="back-to-home.gif" presentation="gallery"></action-text-attachment>
HTML
playground.cards.create! creator: creator, title: "Now,check out all cards assigned to you", status: "published", description: <<~HTML
<p>Pull down the Fizzy menu at the top of the screen, and select <b><strong>Assigned to me</b></strong> or just hit 2 on your keyboard any time.</p>
<p><br></p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/all-assigned.gif" alt="Demo of navigating to 'Assigned to Me'" caption="See all cards assigned to me" content-type="image/*" filename="all-assigned.gif" presentation="gallery"></action-text-attachment>
HTML
playground.cards.create! creator: creator, title: "Then, open the Fizzy menu", status: "published", description: <<~HTML
<p>The Fizzy menu is how you get around the app. Click <b><strong>Fizzy</b></strong> at the top of the screen or hit the J key on your keyboard to pop it open.</p>
<p><br></p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/open-menu.gif" alt="Demo of opening the main menu" caption="Open the Fizzy menu" content-type="image/*" filename="open-menu.gif" presentation="gallery"></action-text-attachment>
HTML
playground.cards.create! creator: creator, title: "Next, assign this card to yourself", status: "published", description: <<~HTML
<p>Click the little head with the + next to it, pick yourself.</p>
<p><br></p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/assign-to-self.gif" alt="Demo of assigning a card" caption="Assign this to yourself" content-type="image/*" filename="assign-to-self.gif" presentation="gallery"></action-text-attachment>
HTML
playground.cards.create! creator: creator, title: "Now, tag this card “Design” then move it to YES", status: "published", description: <<~HTML
<p>Click the little Tag icon, type design, then <b><strong>Create tag</b></strong>. Then, move the card to the new YES column you created in the previous step.</p>
<p><br></p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/tag-design.gif" alt="Demo of tagging a card" caption="Tag this #design" content-type="image/*" filename="tag-design.gif" presentation="gallery"></action-text-attachment>
HTML
@@ -75,11 +68,8 @@ class Account::Seeder
<li>Make one called "Yes"</li>
<li>Make another called "Working on"</li>
</ol>
<p><br></p>
<p>Go back to the Board view, click the little + to the right of the DONE column, name the column, pick a color, then do it again.</p>
<p><br></p>
<p>After that, drag this card to DONE or select DONE in the sidebar.</p>
<p><br></p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/make-columns.gif" alt="Demo of adding columns" caption="Make two more columns" content-type="image/*" filename="make-columns.gif" presentation="gallery"></action-text-attachment>
HTML
@@ -93,9 +83,8 @@ class Account::Seeder
<ol>
<li>Click the title and you can rename the card, change the description, or add more information to the card.</li>
<li>Then, hit "Mark as Done" at the bottom of the card.</li>
<li>Finally, hit <b><strong>Playground</strong></b> in the top left of the screen to go back to the board.</li>
<li>Finally, hit <b><strong>Back to Playground</strong></b> in the top left of the screen to go back to the board.</li>
</ol>
<p><br></p>
<action-text-attachment url="https://videos.37signals.com/fizzy/assets/images/rename.gif" alt="Demo of renaming a card" caption="Rename this card" content-type="image/*" filename="rename.gif" presentation="gallery"></action-text-attachment>
HTML
end
+1 -1
View File
@@ -3,6 +3,6 @@ module Board::Broadcastable
included do
broadcasts_refreshes
broadcasts_refreshes_to ->(_) { :all_boards }
broadcasts_refreshes_to ->(board) { [ board.account, :all_boards ] }
end
end
+2 -1
View File
@@ -1,6 +1,6 @@
class Card < ApplicationRecord
include Assignable, Attachments, Broadcastable, Closeable, Colored, Entropic, Eventable,
Golden, Mentions, Multistep, Pinnable, Postponable, Promptable,
Exportable, Golden, Mentions, Multistep, Pinnable, Postponable, Promptable,
Readable, Searchable, Stallable, Statuses, Taggable, Triageable, Watchable
belongs_to :account, default: -> { board.account }
@@ -16,6 +16,7 @@ class Card < ApplicationRecord
before_create :assign_number
after_save -> { board.touch }, if: :published?
after_touch -> { board.touch }, if: :published?
after_update :handle_board_change, if: :saved_change_to_board_id?
scope :reverse_chronologically, -> { order created_at: :desc, id: :desc }
+85
View File
@@ -0,0 +1,85 @@
module Card::Exportable
extend ActiveSupport::Concern
include ActionView::Helpers::TagHelper
def export_json
JSON.pretty_generate({
number: number,
title: title,
board: board.name,
status: export_status,
creator: export_user(creator),
description: export_html(description),
created_at: created_at.iso8601,
updated_at: updated_at.iso8601,
comments: comments.chronologically.map do |comment|
{
id: comment.id,
body: export_html(comment.body),
creator: export_user(comment.creator),
created_at: comment.created_at.iso8601
}
end
})
end
def export_attachments
collect_attachments.map do |attachment|
{ path: export_attachment_path(attachment.blob), blob: attachment.blob }
end
end
private
def export_html(rich_text)
return "" if rich_text.blank?
rich_text.body.render_attachments do |attachment|
attachment_representation(attachment)
end.to_html
end
def attachment_representation(attachment)
case attachable = attachment.attachable
when ActiveStorage::Blob
path = export_attachment_path(attachable)
if attachable.image?
tag.img(src: path, alt: attachable.filename)
else
tag.a(attachable.filename, href: path)
end
when ActionText::Attachables::RemoteImage
tag.img(src: attachable.url, alt: "Remote image")
else
attachment.to_html
end
end
def export_user(user)
{
id: user.id,
name: user.name,
email: user.identity&.email_address
}
end
def export_attachment_path(blob)
"#{number}/#{blob.key}_#{blob.filename}"
end
def collect_attachments
attachments.to_a + comments.flat_map { |c| c.attachments.to_a }
end
def export_status
case
when closed?
"Done"
when postponed?
"Not now"
when column.present?
column.name
else
"Maybe?"
end
end
end
+1 -1
View File
@@ -9,7 +9,7 @@ module Card::Mentions
end
def should_check_mentions?
saved_change_to_status? && published?
was_just_published?
end
end
end
+14
View File
@@ -4,7 +4,10 @@ module Card::Statuses
included do
enum :status, %w[ drafted published ].index_by(&:itself)
attr_reader :initial_status
before_save :update_created_at_on_publication
before_save :remember_initial_status
after_create -> { track_event :published }, if: :published?
scope :published_or_drafted_by, ->(user) { where(status: :published).or(where(status: :drafted, creator: user)) }
@@ -17,10 +20,21 @@ module Card::Statuses
end
end
def was_just_published?
initial_status&.drafted? && status_in_database.inquiry.published?
end
private
def update_created_at_on_publication
if will_save_change_to_status? && status_in_database.inquiry.drafted?
self.created_at = Time.current
end
end
# So that we can check it in callbacks when other operations in the transaction clean the changes.
def remember_initial_status
if will_save_change_to_status?
@initial_status ||= status_in_database.to_s.inquiry
end
end
end
+7
View File
@@ -16,6 +16,7 @@ class User < ApplicationRecord
has_many :closures, dependent: :nullify
has_many :pins, dependent: :destroy
has_many :pinned_cards, through: :pins, source: :card
has_many :exports, class_name: "Account::Export", dependent: :destroy
scope :with_avatars, -> { preload(:account, :avatar_attachment) }
@@ -23,10 +24,16 @@ class User < ApplicationRecord
transaction do
accesses.destroy_all
update! active: false, identity: nil
close_remote_connections
end
end
def setup?
name != identity.email_address
end
private
def close_remote_connections
ActionCable.server.remote_connections.where(current_user: self).disconnect(reconnect: false)
end
end
+1 -3
View File
@@ -4,8 +4,6 @@ class Webhook::Delivery < ApplicationRecord
ENDPOINT_TIMEOUT = 7.seconds
DNS_RESOLUTION_TIMEOUT = 2
DISALLOWED_IP_RANGES = [
# IPv4 mapped to IPv6
IPAddr.new("::ffff:0:0/96"),
# Broadcasts
IPAddr.new("0.0.0.0/8")
].freeze
@@ -85,7 +83,7 @@ class Webhook::Delivery < ApplicationRecord
end
ip_addresses.any? do |ip|
ip.private? || ip.loopback? || DISALLOWED_IP_RANGES.any? { |range| range.include?(ip) }
ip.private? || ip.loopback? || ip.link_local? || ip.ipv4_mapped? || DISALLOWED_IP_RANGES.any? { |range| range.include?(ip) }
end
end
+29
View File
@@ -0,0 +1,29 @@
<% if @export.present? %>
<% @page_title = "Download Export" %>
<% else %>
<% @page_title = "Download Expired" %>
<% end %>
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= back_link_to "Account Settings", account_settings_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<% end %>
<div class="panel panel--wide shadow center flex flex-column gap">
<h2 class="txt-large margin-none font-weight-black"><%= @page_title %></h2>
<% if @export.present? %>
<p class="margin-none-block-start">Your export is ready. The download should start automatically.</p>
<%= link_to rails_blob_path(@export.file, disposition: "attachment"),
id: "download-link",
class: "btn btn--link",
data: { turbo: false, controller: "auto-click" } do %>
Download your data
<% end %>
<% else %>
<p class="margin-none-block-start">That download link has expired. Youll need to <%= link_to "request a new export", account_settings_path, class: "txt-lnk" %>.</p>
<% end %>
</div>
+1 -7
View File
@@ -2,13 +2,7 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= link_to account_join_code_path, class: "btn btn--back borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %>
<span class="overflow-ellipsis flex align-center">
<%= icon_tag "arrow-left" %>
<strong>Back to Invite link</strong>
<kbd class="txt-x-small margin-inline-start hide-on-touch">&larr;</kbd>
</span>
<% end %>
<%= back_link_to "Invite link", account_join_code_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<% end %>
+1 -7
View File
@@ -2,13 +2,7 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= link_to account_settings_path, class: "btn btn--back borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %>
<span class="overflow-ellipsis flex align-center">
<%= icon_tag "arrow-left" %>
<strong>Back to Account Settings</strong>
<kbd class="txt-x-small margin-inline-start hide-on-touch">&larr;</kbd>
</span>
<% end %>
<%= back_link_to "Account Settings", account_settings_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<% end %>
+6 -7
View File
@@ -1,8 +1,7 @@
<div class="settings__panel settings__panel--entropy panel shadow center">
<header>
<h2 class="divider txt-large">Auto close</h2>
<p class="margin-none-block-start">Fizzy doesnt let stale cards stick around forever. Cards automatically move to “Not Now” if there is no activity for specific period of time. <em>This is the default, global setting — you can override it on each board.</em></p>
</header>
<header>
<h2 class="divider txt-large">Auto close</h2>
<p class="margin-none-block-start">Fizzy doesnt let stale cards stick around forever. Cards automatically move to “Not Now” if there is no activity for specific period of time. <em>This is the default, global setting — you can override it on each board.</em></p>
</header>
<%= render "entropy/auto_close", model: account.entropy, url: account_entropy_path, disabled: !Current.user.admin? %>
<%= render "entropy/auto_close", model: account.entropy, url: account_entropy_path, disabled: !Current.user.admin? %>
</div>
@@ -0,0 +1,19 @@
<header class="margin-block-start-double">
<h2 class="divider txt-large">Export your data</h2>
<p class="margin-none-block">Download an archive of your Fizzy data.</p>
</header>
<div data-controller="dialog" data-dialog-modal-value="true">
<button type="button" class="btn" data-action="dialog#open">Begin export...</button>
<dialog class="dialog panel panel--wide shadow" data-dialog-target="dialog">
<h2 class="margin-none txt-large">Export your account data</h2>
<p class="margin-none-block-start">This will kick off a request to generate a ZIP archive of all the data in boards you have access to.</p>
<p>When the file is ready, well email you a link to download it. The link will expire after 24 hours.</p>
<div class="flex gap justify-center">
<%= button_to "Start export", account_exports_path, method: :post, class: "btn btn--link", form: { data: { action: "submit->dialog#close" } } %>
<button type="button" class="btn" data-action="dialog#close">Cancel</button>
</div>
</dialog>
</div>
+4 -1
View File
@@ -15,5 +15,8 @@
<%= render "account/settings/users", users: @users %>
</div>
<%= render "account/settings/entropy", account: @account %>
<div class="settings__panel settings__panel--entropy panel shadow center">
<%= render "account/settings/entropy", account: @account %>
<%= render "account/settings/export" %>
</div>
</section>
+31
View File
@@ -61,6 +61,37 @@
</div>
</div>
<div class="settings__panel panel shadow center">
<header>
<h2 class="divider txt-medium margin-block-start">
10 Most Recent Signups
</h2>
</header>
<ul class="margin-block-half">
<% @recent_accounts.each do |account| %>
<% admin_user = account.users.owner.first %>
<li class="flex align-start gap-half margin-block-start-half">
<div
class="flex-item-grow min-width overflow-ellipsis"
style="text-align: left"
>
<strong><%= account.name %></strong>
<br>
<span class="txt-x-small txt-ink-medium">
#<%= account.external_account_id %> •
<%= admin_user&.identity&.email_address || "No admin" %>
</span>
</div>
<div class="txt-medium txt-ink-medium" style="text-align: right">
<%= time_ago_in_words(account.created_at) %> ago
</div>
</li>
<% end %>
</ul>
</div>
<div class="settings__panel panel shadow center">
<header>
<h2 class="divider txt-medium margin-block-start">
+1 -1
View File
@@ -34,7 +34,7 @@
placeholder: "Add a public note about this board…",
data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" },
readonly: !Current.user.can_administer_board?(@board) %>
<%= form.button "Save changes", type: :submit, class: "btn txt-small" %>
<%= form.button "Save changes", type: :submit, class: "btn txt-small", title: "Save changes (#{ hotkey_label(["ctrl", "enter"]) })" %>
<% end %>
</div>
</div>
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="panel panel--centered">
<%= form_with model: @board, class: "flex flex-column gap", data: { controller: "form" } do |form| %>
<h1 class="txt-x-large margin-none font-weight-black"><%= @page_title %></h2>
<h1 class="txt-x-large margin-none font-weight-black"><%= @page_title %></h1>
<%= form.text_field :name, required: true, class: "input full-width", autofocus: true, autocomplete: "off",placeholder: "Name it…", data: { action: "keydown.esc@document->form#cancel" } %>
<button type="submit" class="btn btn--link center">
+1 -1
View File
@@ -3,5 +3,5 @@
<%= turbo_stream_from board %>
<% end %>
<% else %>
<%= turbo_stream_from :all_boards %>
<%= turbo_stream_from [ Current.account, :all_boards ] %>
<% end %>
+3
View File
@@ -4,12 +4,14 @@
<%= button_to "Not now", card_not_now_path(@card),
class: [ "card__column-name btn", { "card__column-name--current": @card.postponed? } ],
style: "--column-color: var(--color-card-complete)",
role: "radio",
aria: { checked: @card.postponed? },
form_class: "flex align-center gap-half" %>
<%= button_to "Maybe?", card_triage_path(@card), method: :delete,
class: [ "card__column-name card__column-name--stream btn", { "card__column-name--current": @card.awaiting_triage? } ],
style: "--column-color: var(--color-card-default)",
role: "radio",
aria: { checked: @card.awaiting_triage? },
form_class: "flex align-center gap-half" %>
@@ -20,6 +22,7 @@
<%= button_to "Done", card_closure_path(@card),
class: [ "card__column-name btn", { "card__column-name--current": @card.closed? } ],
style: "--column-color: var(--color-card-complete)",
role: "radio",
aria: { checked: @card.closed? },
form_class: "flex align-center gap-half" %>
+2 -2
View File
@@ -1,7 +1,7 @@
<% cache comment do %>
<%= turbo_frame_tag comment, :container do %>
<%# Bump for CSS changes: 2025-06-30 -%>
<div id="<%= dom_id(comment) %>" class="comment flex align-start full-width <%= "comment--system" if comment.creator.system? %>" data-creator-id="<%= comment.creator_id %>" data-created-by-current-user-target="creation">
<div id="<%= dom_id(comment) %>" data-creator-id="<%= comment.creator_id %>" class="comment flex align-start full-width <%= "comment--system" if comment.creator.system? %>">
<figure class="comment__avatar flex-item-no-shrink" aria-hidden="true">
<%= avatar_tag comment.creator, hidden_for_screen_reader: true %>
</figure>
@@ -20,7 +20,7 @@
</h3>
<%= link_to edit_card_comment_path(comment.card, comment),
class: "comment__edit btn btn--circle borderless translucent" do %>
class: "comment__edit btn btn--circle borderless translucent", data: { only_visible_to_you: true } do %>
<%= icon_tag "menu-dots-horizontal" %>
<span class="for-screen-reader">Edit this comment</span>
<% end %>
+1
View File
@@ -16,6 +16,7 @@
<span class="flex-inline align-center gap">
<%= form.button class: "comment__submit btn btn--reversed",
title: "Post this comment (#{ hotkey_label(["ctrl", "enter"]) })",
data: { form_target: "submit" }, disabled: true do %>
<span>Post</span>
<% end %>
+2 -2
View File
@@ -12,10 +12,10 @@
<%= general_prompts(@card.board) %>
<% end %>
<div class="flex gap-half justify-start align-center">
<%= form.button class: "btn btn--reversed", type: :submit do %>
<%= form.button class: "btn btn--reversed", type: :submit, title: "Save changes (#{ hotkey_label(["ctrl", "enter"]) })" do %>
<span>Save</span>
<% end %>
<%= link_to card_comment_path(@card, @comment), class: "btn", data: { form_target: "cancel" } do %>
<%= link_to card_comment_path(@card, @comment), class: "btn", data: { form_target: "cancel" },title: "Cancel (#{ hotkey_label(["esc"]) })" do %>
<span>Cancel</span>
<% end %>
+4 -4
View File
@@ -1,11 +1,11 @@
<% if card.golden? %>
<%= button_to card_goldness_path(card), method: :delete, class: "btn btn--reversed", data: { controller: "tooltip" } do %>
<%= button_to card_goldness_path(card), method: :delete, class: "btn btn--reversed", data: { controller: "tooltip hotkey", action: "keydown.shift+g@document->hotkey#click" } do %>
<%= icon_tag "golden-ticket" %>
<span class="for-screen-reader">Demote to normal</span>
<span class="for-screen-reader">Demote to normal (shift+g)</span>
<% end %>
<% else %>
<%= button_to card_goldness_path(card), class: "btn", data: { controller: "tooltip" } do %>
<%= button_to card_goldness_path(card), class: "btn", data: { controller: "tooltip hotkey", action: "keydown.shift+g@document->hotkey#click" } do %>
<%= icon_tag "golden-ticket" %>
<span class="for-screen-reader">Promote to golden</span>
<span class="for-screen-reader">Promote to golden (shift+g)</span>
<% end %>
<% end %>
@@ -1,7 +1,7 @@
<div class="flex gap-half">
<%= button_tag type: :submit, form: dom_id(card, :edit_form), class: "btn borderless",
title: "Save changes (#{ hotkey_label(["ctrl", "enter"]) })",
data: { controller: "hotkey", action: "keydown.ctrl+enter@document->hotkey#click keydown.meta+enter@document->hotkey#click" } do %>
<span>Save changes</span>
<kbd class="txt-x-small hide-on-touch"><%= hotkey_label(["ctrl", "enter"]) %></kbd>
<% end %>
</div>
@@ -1,14 +1,14 @@
<div class="card-perma__notch card-perma__notch--bottom">
<%= button_to card_publish_path(card), name: "creation_type", value: "add", class: "btn",
title: "Create card (#{ hotkey_label(["ctrl", "enter"]) })",
form: { data: { controller: "form" } },
data: { form_target: "submit", controller: "clicker", action: "keydown.ctrl+enter@document->clicker#click keydown.meta+enter@document->clicker#click" } do %>
<span>Create card</span>
<% end %>
<%= button_to card_publish_path(card), method: :post, class: "btn btn--reversed", name: "creation_type", value: "add_another",
title: "Create and add another (ctrl+shift+enter)", form: { data: { controller: "form" } },
title: "Create and add another (#{ hotkey_label(["ctrl", "shift", "enter"]) })", form: { data: { controller: "form" } },
data: { form_target: "submit", controller: "clicker", action: "keydown.ctrl+shift+enter@document->clicker#click keydown.meta+shift+enter@document->clicker#click" } do %>
<span>Create and add another</span>
<kbd class="txt-x-small hide-on-touch"><%= hotkey_label([ "ctrl", "shift", "enter" ]) %></kbd>
<% end %>
</div>
@@ -37,6 +37,6 @@
<% if card.entropic? %>
<%= render "cards/display/preview/bubble", card: card %>
<span class="card__board-name">
<span class="card__board-name"></span>
<% end %>
<% end %>
+4 -4
View File
@@ -1,11 +1,11 @@
<div id="<%= dom_id(card, :pin_button) %>">
<% if card.pinned_by? Current.user %>
<%= button_to card_pin_path(card), method: :delete, class: "btn btn--reversed", data: { controller: "tooltip" } do %>
<%= icon_tag "pinned" %> <span class="for-screen-reader">Un-pin this card</span>
<%= button_to card_pin_path(card), method: :delete, class: "btn btn--reversed", data: { controller: "tooltip hotkey", action: "keydown.shift+p@document->hotkey#click" } do %>
<%= icon_tag "pinned" %> <span class="for-screen-reader">Un-pin this card (shift+p)</span>
<% end %>
<% else %>
<%= button_to card_pin_path(card), class: "btn", data: { controller: "tooltip" } do %>
<%= icon_tag "unpinned" %> <span class="for-screen-reader">Pin this card</span>
<%= button_to card_pin_path(card), class: "btn", data: { controller: "tooltip hotkey", action: "keydown.shift+p@document->hotkey#click" } do %>
<%= icon_tag "unpinned" %> <span class="for-screen-reader">Pin this card (shift+p)</span>
<% end %>
<% end %>
</div>
@@ -1,13 +1,13 @@
<div id="<%= dom_id(card, :watch_button) %>">
<% if card.watched_by? Current.user %>
<%= button_to card_watch_path(card), method: :delete, class: "btn btn--reversed", data: { controller: "tooltip" } do %>
<%= button_to card_watch_path(card), method: :delete, class: "btn btn--reversed", data: { controller: "tooltip hotkey", action: "keydown.shift+n@document->hotkey#click" } do %>
<%= icon_tag "bell" %>
<span class="for-screen-reader">Stop watching</span>
<span class="for-screen-reader">Stop watching (shift+n)</span>
<% end %>
<% else %>
<%= button_to card_watch_path(card), class: "btn", data: { controller: "tooltip" } do %>
<%= button_to card_watch_path(card), class: "btn", data: { controller: "tooltip hotkey", action: "keydown.shift+n@document->hotkey#click" } do %>
<%= icon_tag "bell-off" %>
<span class="for-screen-reader">Watch this</span>
<span class="for-screen-reader">Watch this (shift+n)</span>
<% end %>
<% end %>
</div>
@@ -2,13 +2,7 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= link_to events_path, class: "btn btn--back", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %>
<span class="overflow-ellipsis flex align-center">
<%= icon_tag "arrow-left" %>
<strong>Back to Actvity</strong>
<kbd class="txt-x-small margin-inline-start hide-on-touch">&larr;</kbd>
</span>
<% end %>
<%= back_link_to "Actvity", root_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title">
+1 -3
View File
@@ -1,9 +1,7 @@
<% filter = user_filtering.filter %>
<div class="filters__manage gap-half">
<%= render "filters/filter_toggle", filter: filter %>
<%= link_to no_filtering_url, class: "btn btn--remove txt-x-small", data: { controller: "hotkey tooltip", action: "keydown.esc@document->hotkey#click"} do %>
<%= link_to root_path, class: "btn btn--remove txt-x-small", data: { controller: "hotkey tooltip", action: "keydown.esc@document->hotkey#click"} do %>
<%= icon_tag "close" %>
<span class="for-screen-reader">Clear all</span>
<% end %>
+1
View File
@@ -21,5 +21,6 @@
<footer id="footer">
<%= yield :footer %>
</footer>
</body>
</html>
@@ -0,0 +1,6 @@
<h1 class="title">Download your Fizzy data</h1>
<p class="subtitle">Your Fizzy data export has finished processing and is ready to download.</p>
<p><%= link_to "Download your data", account_export_url(@export) %></p>
<p class="footer">Need help? <%= mail_to "support@fizzy.do", "Send us an email" %>.</p>
@@ -0,0 +1,3 @@
Your Fizzy data export has finished processing and is ready to download.
Download your data: <%= account_export_url(@export) %>
@@ -9,6 +9,8 @@
<strong class="txt-large"><%= @magic_link.code %></strong>
<p>This code will work for <%= distance_of_time_in_words(MagicLink::EXPIRATION_TIME) %>.</p>
<p class="footer">Need help? <%= mail_to "support@fizzy.do", "Send us an email"%>.
</p>
@@ -5,3 +5,5 @@ Please enter this 6-character verification code on the Fizzy sign-up page to cre
<% end %>
<%= @magic_link.code %>
This code will work for <%= distance_of_time_in_words(MagicLink::EXPIRATION_TIME) %>.
+1
View File
@@ -1,6 +1,7 @@
<% if accounts.many? %>
<% cache [ Current.identity, accounts, Current.account ] do %>
<%= collapsible_nav_section "Accounts" do %>
<%# Bust cache 1 Dec 2025 %>
<% accounts.each do |account| %>
<%= filter_place_menu_item landing_url(script_name: account.slug), account.name, "marker", current: account == Current.account, turbo: false %>
<% end %>
+7
View File
@@ -0,0 +1,7 @@
<%= collapsible_nav_section "Shortcuts", class: "nav__section popup__section nav__section--secret" do %>
<%= filter_place_menu_item cards_path(indexed_by: :golden), "Golden cards", "filter" %>
<%= filter_place_menu_item cards_path(indexed_by: :stalled), "Stalled cards", "filter" %>
<%= filter_place_menu_item cards_path(indexed_by: :postponing_soon), "Cards closing soon", "filter" %>
<%= filter_place_menu_item cards_path(creation: "today"), "Added today", "filter" %>
<%= filter_place_menu_item cards_path(closure: "today"), "Done today", "filter" %>
<% end %>
+1 -1
View File
@@ -1,10 +1,10 @@
<%= turbo_frame_tag "my_menu", target: "_top" do %>
<%= render "my/menus/jump" do %>
<%= render "my/menus/boards", boards: @boards %>
<%= render "my/menus/custom_views", filters: @filters %>
<%= render "my/menus/tags", tags: @tags %>
<%= render "my/menus/people", users: @users %>
<%= render "my/menus/settings" %>
<%= render "my/menus/shortcuts" %>
<%= render "my/menus/accounts", accounts: Current.identity.accounts %>
<% end %>
@@ -2,7 +2,7 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= back_link_to @board.name, published_board_url(@board), "keydown.left@document->hotkey#click" %>
<%= back_link_to @board.name, published_board_url(@board), "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title divider divider--fade full-width">
@@ -2,7 +2,7 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= back_link_to @board.name, published_board_url(@board), "keydown.left@document->hotkey#click" %>
<%= back_link_to @board.name, published_board_url(@board), "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title divider divider--fade full-width">
@@ -2,7 +2,7 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= back_link_to @column.board.name, published_board_url(@column.board), "keydown.left@document->hotkey#click" %>
<%= back_link_to @column.board.name, published_board_url(@column.board), "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title divider divider--fade full-width">
@@ -2,7 +2,7 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= back_link_to @board.name, published_board_url(@board), "keydown.left@document->hotkey#click" %>
<%= back_link_to @board.name, published_board_url(@board), "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title divider divider--fade full-width">
+2 -2
View File
@@ -3,12 +3,12 @@
<% content_for :head do %>
<%= tag.meta property: "og:title", content: "#{@board.name} | #{Current.account.name}" %>
<%= tag.meta property: "og:description", content: format_excerpt(@board&.public_description, length: 200) %>
<%= tag.meta property: "og:image", content: "#{request.base_url}/app-icon.png" %>
<%= tag.meta property: "og:image", content: "#{request.base_url}/opengraph.png" %>
<%= tag.meta property: "og:url", content: published_board_url(@board) %>
<%= tag.meta property: "twitter:title", content: "#{@board.name} | #{Current.account.name}" %>
<%= tag.meta property: "twitter:description", content: format_excerpt(@board&.public_description, length: 200) %>
<%= tag.meta property: "twitter:image", content: "#{request.base_url}/app-icon.png" %>
<%= tag.meta property: "twitter:image", content: "#{request.base_url}/opengraph.png" %>
<%= tag.meta property: "twitter:card", content: "summary_large_image" %>
<% end %>
+1 -1
View File
@@ -14,7 +14,7 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= back_link_to @card.board.name, published_board_url(@card.board), "keydown.left@document->hotkey#click", style: "view-transistion-name: card-board-title;" %>
<%= back_link_to @card.board.name, published_board_url(@card.board), "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click", style: "view-transistion-name: card-board-title;" %>
</div>
<% end %>
+3 -1
View File
@@ -10,8 +10,10 @@
<%= form.text_field :code, required: true, class: "input center txt-align-enter txt-large",
autofocus: true, autocorrect: "off", autocapitalize: "off", spellcheck: "false", "data-1p-ignore": true,
autocomplete: "one-time-code", maxlength: "6", placeholder: "••••••", value: params[:code],
data: { magic_link_target: "input", action: "keydown.enter->magic-link#submit paste->magic-link#paste" } %>
data: { magic_link_target: "input", action: "keydown.enter->magic-link#submit paste->magic-link#submit" } %>
<% end %>
<p class="txt-small">The code you receive will work for <%= distance_of_time_in_words(MagicLink::EXPIRATION_TIME) %>.</p>
</div>
<% if Rails.env.development? %>
+1 -1
View File
@@ -10,7 +10,7 @@
</label>
</div>
<p><strong>New here?</strong> <%= link_to "sign up", new_signup_path %> to create an account. <strong>Already have an account?</strong> Enter your email and well get you signed in.</p>
<p><strong>New here?</strong> <%= link_to "Sign up", new_signup_path %> to create an account. <strong>Already have an account?</strong> Enter your email and well get you signed in.</p>
<button type="submit" id="log_in" class="btn btn--link center txt-medium">
<span>Lets go</span>
+1 -7
View File
@@ -2,13 +2,7 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= link_to user_path(@user), class: "btn btn--back borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %>
<span class="overflow-ellipsis flex align-center">
<%= icon_tag "arrow-left" %>
<strong>Back to profile</strong>
<kbd class="txt-x-small margin-inline-start hide-on-touch">&larr;</kbd>
</span>
<% end %>
<%= back_link_to "profile", user_path(@user), "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<% end %>
@@ -2,12 +2,7 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= link_to edit_user_path(@user, script_name: @user.account.slug), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %>
<span class="overflow-ellipsis flex align-center">
<strong>Back to My profile</strong>
<kbd class="txt-x-small margin-inline-start hide-on-touch">&larr;</kbd>
</span>
<% end %>
<%= back_link_to "My profile", edit_user_path(@user, script_name: @user.account.slug), "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<% end %>
+1 -7
View File
@@ -2,13 +2,7 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= link_to edit_user_path(@user, script_name: @user.account.slug), class: "btn btn--back borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %>
<span class="overflow-ellipsis flex align-center">
<%= icon_tag "arrow-left" %>
<strong>Back to My profile</strong>
<kbd class="txt-x-small margin-inline-start hide-on-touch">&larr;</kbd>
</span>
<% end %>
<%= back_link_to "My profile", edit_user_path(@user, script_name: @user.account.slug), "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<% end %>
+2 -2
View File
@@ -2,11 +2,11 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= back_link_to @page_title, @webhook, "keydown.left@document->hotkey#click" %>
<%= back_link_to @page_title, @webhook, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<% end %>
<article class="panel center txt-align-start" style="view-transition-name: <%= dom_id(@webhook) %>">
<article class="panel panel--wide center txt-align-start" style="view-transition-name: <%= dom_id(@webhook) %>">
<%= form_with model: @webhook, url: @webhook, method: :put, data: { controller: "form" }, html: { class: "flex flex-column gap" } do |form| %>
<h2 class="txt-large margin-none">
<%= form.text_field :name, required: true, autofocus: true, class: "input full-width", placeholder: "Name your Webhook…", data: { action: "keydown.esc@document->form#cancel" } %>
+1 -1
View File
@@ -8,7 +8,7 @@
<h1 class="header__title"><%= @page_title %></h1>
<% end %>
<%= tag.section class: "panel shadow center webhooks" do %>
<%= tag.section class: "panel panel--wide shadow center webhooks" do %>
<% if @page.records.any? %>
<ul class="flex flex-column align-start gap margin-none-block-start unpad webhooks-list">
<%= render partial: "webhooks/webhook", collection: @page.records %>
+2 -2
View File
@@ -2,13 +2,13 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= link_back_to_board(@board) %>
<%= back_link_to "Webhooks", board_webhooks_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title"><%= @page_title %></h1>
<% end %>
<article class="panel center txt-align-start" style="view-transition-name: <%= dom_id(@webhook) %>">
<article class="panel panel--wide center txt-align-start" style="view-transition-name: <%= dom_id(@webhook) %>">
<%= form_with model: @webhook, url: board_webhooks_path, data: { controller: "form" }, html: { class: "flex flex-column gap" } do |form| %>
<h2 class="txt-large margin-none">
<%= form.text_field :name, required: true, autofocus: true, class: "input", placeholder: "Name this Webhook…", data: { action: "keydown.esc@document->form#cancel" } %>
+7 -11
View File
@@ -2,13 +2,9 @@
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= link_to board_webhooks_path, class: "btn btn--back borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %>
<span class="overflow-ellipsis flex align-center">
<%= icon_tag "arrow-left" %>
<strong>Back to Webhooks</strong>
<kbd class="txt-x-small margin-inline-start hide-on-touch">&larr;</kbd>
</span>
<% end %>
<div class="header__actions header__actions--start">
<%= back_link_to "Webhooks", board_webhooks_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
</div>
<div class="header__actions header__actions--end">
@@ -16,10 +12,10 @@
<%= icon_tag "pencil" %>
<span class="for-screen-reader">Edit</span>
<% end %>
<div>
</div>
<% end %>
<div class="panel shadow center flex flex-column gap txt-align-start">
<div class="panel panel--wide shadow center flex flex-column gap txt-align-start">
<header>
<h1 class="txt-x-large margin-none"><%= @webhook.name %></h1>
<span class="txt-medium txt-break"><%= @webhook.url %></span>
@@ -43,7 +39,7 @@
<div class="flex flex-column">
<h2 class="margin-none txt-uppercase txt-medium">Subscribed to</h2>
<% if @webhook.subscribed_actions.empty? %>
<p class="margin-none txt-subtle">This Webhook isn't subscribed to any events. It will never trigger.<p/>
<p class="margin-none txt-subtle">This Webhook isn't subscribed to any events. It will never trigger.</p>
<% else %>
<ul class="margin-none unpad-block">
<% @webhook.subscribed_actions.each do |action| %>
@@ -56,7 +52,7 @@
<div class="flex flex-column">
<h2 class="margin-none txt-uppercase txt-medium">Deliveries</h2>
<% if @webhook.deliveries.empty? %>
<p class="margin-none txt-subtle">This Webhook hasn't been triggered yet<p/>
<p class="margin-none txt-subtle">This Webhook hasn't been triggered yet</p>
<% else %>
<ul class="margin-none unpad-block">
<%= render partial: "webhooks/delivery", collection: @webhook.deliveries.ordered.limit(20), as: :delivery %>

Some files were not shown because too many files have changed in this diff Show More