Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b4a7b63
Split Export model into user and whole-account export
monorkin Dec 12, 2025
b43b267
Implement full account exports
monorkin Dec 22, 2025
bef2016
Implement basic imports
monorkin Jan 9, 2026
6e1cd2f
Convert Account::SingleUserExport to User::DataExport
monorkin Jan 9, 2026
cb1f4de
Simplify settings layout and correct h2 levels
andyra Jan 14, 2026
b1c3391
Polish up notification settings to match
andyra Jan 14, 2026
59c3e68
Break import and export objects into record sets
monorkin Jan 16, 2026
2d696db
Regenerate schemas
monorkin Jan 16, 2026
fec5819
Convert double negatives into a positive question
monorkin Jan 16, 2026
3827dc1
Use convention to create boilerplate record sets
monorkin Jan 19, 2026
be608aa
Touch up export modals
andyra Jan 20, 2026
6e47be1
Polish the session menu page
andyra Jan 20, 2026
c8f7a51
Nicer file input
andyra Jan 20, 2026
6c16652
Better instructions
andyra Jan 20, 2026
2a00f2a
Touch up the Download Export page
andyra Jan 20, 2026
e8c3b52
Smaller border radius to handle long file names
andyra Jan 21, 2026
d22ea58
Fix crash when exporting ActiveStorage files
monorkin Jan 26, 2026
d38e8dc
Fix account creation and import updates
monorkin Jan 29, 2026
89046d8
Fix orphaned Entropy records upon Account destruction
monorkin Jan 29, 2026
5be35ac
Push updates via Turbo
monorkin Jan 29, 2026
aa46d16
Remove unused association
monorkin Jan 29, 2026
66c952f
Resolve import conflicts
monorkin Jan 29, 2026
4968a8b
Implement cursors for imports
monorkin Jan 29, 2026
9cf4956
Simplify the import test
monorkin Jan 29, 2026
a742b5d
Cleanup code
monorkin Jan 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 35 additions & 13 deletions app/assets/stylesheets/inputs.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,12 @@
}
}

.input--file {
.input--file,
.input--upload {
cursor: pointer;
display: grid;
inline-size: auto;
place-items: center;

> * {
grid-area: 1 / 1;
}

img {
border-radius: 0.4em;
&:has(input[type="file"]:focus-visible) {
outline: 0.15rem solid var(--color-selected-dark);
}

input[type="file"] {
Expand All @@ -88,10 +82,19 @@
opacity: 0;
}
}
}

&:has(input[type="file"]:focus),
&:has(input[type="file"]:focus-visible) {
outline: 0.15rem solid var(--color-selected-dark);
.input--file {
display: grid;
inline-size: auto;
place-items: center;

> * {
grid-area: 1 / 1;
}

img {
border-radius: 0.4em;
}

&:is(.avatar) {
Expand All @@ -101,6 +104,25 @@
}
}

.input--upload {
--btn-border-color: var(--color-ink);
--btn-border-radius: 1ch;

border-style: dashed;
position: relative;

input[type="file"] {
inset: 0;
outline: none;
position: absolute;
}

&:has([data-upload-preview-target="fileName"]:not([hidden])) {
--btn-border-color: var(--color-positive);
--btn-color: var(--color-positive);
}
}

.input--select {
--input-border-radius: 2em;
--input-padding: 0.5em 1.8em 0.5em 1.2em;
Expand Down
4 changes: 2 additions & 2 deletions app/assets/stylesheets/notifications.css
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,12 @@
display: none;

.notifications--on & {
display: block;
display: inline;
}
}

.notifications__off-message {
display: block;
display: inline;

.notifications--on & {
display: none;
Expand Down
16 changes: 0 additions & 16 deletions app/assets/stylesheets/profile-layout.css

This file was deleted.

23 changes: 20 additions & 3 deletions app/assets/stylesheets/settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,21 @@
}
}

/* Sections & Panels
/* -------------------------------------------------------------------------- */

.settings__panel {
--panel-size: 100%;
--panel-padding: calc(var(--settings-spacer) / 1);

display: flex;
flex-direction: column;
gap: calc(var(--settings-spacer) / 2);
gap: var(--panel-padding);
min-block-size: 100%;
min-inline-size: 0;

@media (min-width: 960px) {
--panel-padding: calc(var(--settings-spacer) * 1.5) calc(var(--settings-spacer) * 2);
@media (min-width: 640px) {
--panel-padding: calc(var(--settings-spacer) * 2);
}
}

Expand All @@ -36,6 +39,20 @@
}
}

.settings__section {
h2 {
font-size: var(--text-large);
}

> * + * {
margin-block-start: calc(var(--panel-padding) / 2);
}

&:is(:first-child):has(h2) {
margin-top: -0.33lh; /* Align h2 letters caps with panel padding */
}
}

/* Users
/* ------------------------------------------------------------------------ */

Expand Down
7 changes: 6 additions & 1 deletion app/controllers/account/exports_controller.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
class Account::ExportsController < ApplicationController
before_action :ensure_admin_or_owner
before_action :ensure_export_limit_not_exceeded, only: :create
before_action :set_export, only: :show

Expand All @@ -13,8 +14,12 @@ def create
end

private
def ensure_admin_or_owner
head :forbidden unless Current.user.admin? || Current.user.owner?
end

def ensure_export_limit_not_exceeded
head :too_many_requests if Current.user.exports.current.count >= CURRENT_EXPORT_LIMIT
head :too_many_requests if Current.account.exports.current.count >= CURRENT_EXPORT_LIMIT
end

def set_export
Expand Down
38 changes: 38 additions & 0 deletions app/controllers/imports_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
class ImportsController < ApplicationController
layout "public"

disallow_account_scope only: %i[ new create ]
before_action :set_import, only: %i[ show ]

def new
end

def create
signup = Signup.new(identity: Current.identity, full_name: "Import", skip_account_seeding: true)

if signup.complete
start_import(signup.account)
else
render :new, alert: "Couldn't create account."
end
end

def show
end

private
def set_import
@import = Current.account.imports.find(params[:id])
end

def start_import(account)
import = nil

Current.set(account: account) do
import = account.imports.create!(identity: Current.identity, file: params[:file])
import.process_later
end

redirect_to import_path(import, script_name: account.slug)
end
end
33 changes: 33 additions & 0 deletions app/controllers/users/data_exports_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
class Users::DataExportsController < ApplicationController
before_action :set_user
before_action :ensure_current_user
before_action :ensure_export_limit_not_exceeded, only: :create
before_action :set_export, only: :show

CURRENT_EXPORT_LIMIT = 10

def show
end

def create
@user.data_exports.create!(account: Current.account).build_later
redirect_to @user, notice: "Export started. You'll receive an email when it's ready."
end

private
def set_user
@user = Current.account.users.find(params[:user_id])
end

def ensure_current_user
head :forbidden unless @user == Current.user
end

def ensure_export_limit_not_exceeded
head :too_many_requests if @user.data_exports.current.count >= CURRENT_EXPORT_LIMIT
end

def set_export
@export = @user.data_exports.completed.find_by(id: params[:id])
end
end
27 changes: 22 additions & 5 deletions app/javascript/controllers/upload_preview_controller.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = [ "image", "input" ]
static targets = [ "image", "input", "fileName", "placeholder" ]

previewImage() {
const file = this.inputTarget.files[0]

if (file) {
this.imageTarget.src = URL.createObjectURL(file)
if (this.#file) {
this.imageTarget.src = URL.createObjectURL(this.#file)
this.imageTarget.onload = () => URL.revokeObjectURL(this.imageTarget.src)
}
}

previewFileName() {
this.#file ? this.#showFileName() : this.#showPlaceholder()
}

#showFileName() {
this.fileNameTarget.innerHTML = this.#file.name
this.fileNameTarget.removeAttribute("hidden")
this.placeholderTarget.setAttribute("hidden", true)
}

#showPlaceholder() {
this.placeholderTarget.removeAttribute("hidden")
this.fileNameTarget.setAttribute("hidden", true)
}

get #file() {
return this.inputTarget.files[0]
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class ExportAccountDataJob < ApplicationJob
class ExportDataJob < ApplicationJob
queue_as :backend

discard_on ActiveJob::DeserializationError
Expand Down
23 changes: 23 additions & 0 deletions app/jobs/import_account_data_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class ImportAccountDataJob < ApplicationJob
include ActiveJob::Continuable

queue_as :backend

def perform(import)
step :validate do
import.validate \
start: step.cursor,
callback: proc do |record_set:, file:|
step.set!([ record_set.model.name, file ])
end
end

step :process do
import.process \
start: step.cursor,
callback: proc do |record_set:, files:|
step.set!([ record_set.model.name, files.last ])
end
end
end
end
11 changes: 11 additions & 0 deletions app/mailers/export_mailer.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
class ExportMailer < ApplicationMailer
helper_method :export_download_url

def completed(export)
@export = export
@user = export.user

mail to: @user.identity.email_address, subject: "Your Fizzy data export is ready for download"
end

private
def export_download_url(export)
if export.is_a?(User::DataExport)
user_data_export_url(export.user, export)
else
account_export_url(export)
end
end
end
12 changes: 12 additions & 0 deletions app/mailers/import_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class ImportMailer < ApplicationMailer
def completed(identity, account)
@account = account
@landing_url = landing_url(script_name: account.slug)
mail to: identity.email_address, subject: "Your Fizzy account import is complete"
end

def failed(identity, account)
@account = account
mail to: identity.email_address, subject: "Your Fizzy account import failed"
end
end
2 changes: 2 additions & 0 deletions app/models/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ class Account < ApplicationRecord
has_many :webhooks, dependent: :destroy
has_many :tags, dependent: :destroy
has_many :columns, dependent: :destroy
has_many :entropies, dependent: :destroy
has_many :exports, class_name: "Account::Export", dependent: :destroy
has_many :imports, class_name: "Account::Import", dependent: :destroy

before_create :assign_external_account_id
after_create :create_join_code
Expand Down
Loading