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:
@@ -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
|
||||
@@ -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 **
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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." }
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -6,8 +6,6 @@ module HotkeysHelper
|
||||
"⌘"
|
||||
elsif key == "enter"
|
||||
platform.mac? ? "return" : "enter"
|
||||
elsif key == "shift"
|
||||
"⇧"
|
||||
else
|
||||
key
|
||||
end.capitalize
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
class ExportAccountDataJob < ApplicationJob
|
||||
queue_as :backend
|
||||
|
||||
def perform(export)
|
||||
export.build
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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>There’s 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
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
@@ -9,7 +9,7 @@ module Card::Mentions
|
||||
end
|
||||
|
||||
def should_check_mentions?
|
||||
saved_change_to_status? && published?
|
||||
was_just_published?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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. You’ll need to <%= link_to "request a new export", account_settings_path, class: "txt-lnk" %>.</p>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
@@ -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">←</kbd>
|
||||
</span>
|
||||
<% end %>
|
||||
<%= back_link_to "Invite link", account_join_code_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -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">←</kbd>
|
||||
</span>
|
||||
<% end %>
|
||||
<%= back_link_to "Account Settings", account_settings_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -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 doesn’t 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 doesn’t 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, we’ll 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
<%= turbo_stream_from board %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= turbo_stream_from :all_boards %>
|
||||
<%= turbo_stream_from [ Current.account, :all_boards ] %>
|
||||
<% end %>
|
||||
|
||||
@@ -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" %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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">←</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,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 %>
|
||||
|
||||
@@ -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,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 %>
|
||||
|
||||
@@ -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,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">
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
|
||||
@@ -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? %>
|
||||
|
||||
@@ -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 we’ll 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 we’ll get you signed in.</p>
|
||||
|
||||
<button type="submit" id="log_in" class="btn btn--link center txt-medium">
|
||||
<span>Let’s go</span>
|
||||
|
||||
@@ -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">←</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">←</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,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">←</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,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" } %>
|
||||
|
||||
@@ -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,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" } %>
|
||||
|
||||
@@ -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">←</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
Reference in New Issue
Block a user