mirror of
https://github.com/we-promise/sure.git
synced 2026-04-19 20:14:08 +00:00
Reorganize Settings sections + add LLM model/prompt configs (#116)
* Reshuffle/organize settings UI * Settings: AI prompt display/minor touch-ups * API key settings tests * Moved import/export together * Collapsible LLM prompt DIVs * Add export tests
This commit is contained in:
@@ -3,13 +3,29 @@
|
||||
turbo_refresh_url: family_exports_path,
|
||||
turbo_refresh_interval: 3000
|
||||
} : {} do %>
|
||||
<div class="mt-4 space-y-3 max-h-96 overflow-y-auto">
|
||||
<div class="space-y-0 max-h-96 overflow-y-auto">
|
||||
<% if exports.any? %>
|
||||
<% exports.each do |export| %>
|
||||
<div class="flex items-center justify-between bg-container p-4 rounded-lg border border-primary">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-primary">Export from <%= export.created_at.strftime("%B %d, %Y at %I:%M %p") %></p>
|
||||
<p class="text-xs text-secondary"><%= export.filename %></p>
|
||||
<div class="flex items-center justify-between mx-4 py-4">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-primary">Export from <%= export.created_at.strftime("%B %d, %Y at %I:%M %p") %></p>
|
||||
<p class="text-xs text-secondary"><%= export.filename %></p>
|
||||
</div>
|
||||
|
||||
<% if export.processing? || export.pending? %>
|
||||
<span class="px-1 py text-xs rounded-full bg-gray-500/5 text-secondary border border-alpha-black-50">
|
||||
<%= t(".in_progress") %>
|
||||
</span>
|
||||
<% elsif export.completed? %>
|
||||
<span class="px-1 py text-xs rounded-full bg-green-500/5 text-green-500 border border-alpha-black-50">
|
||||
<%= t(".complete") %>
|
||||
</span>
|
||||
<% elsif export.failed? %>
|
||||
<span class="px-1 py text-xs rounded-full bg-red-500/5 text-red-500 border border-alpha-black-50">
|
||||
<%= t(".failed") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if export.processing? || export.pending? %>
|
||||
@@ -18,22 +34,44 @@
|
||||
<span class="text-sm">Exporting...</span>
|
||||
</div>
|
||||
<% elsif export.completed? %>
|
||||
<%= link_to download_family_export_path(export),
|
||||
class: "flex items-center gap-2 text-primary hover:text-primary-hover",
|
||||
data: { turbo_frame: "_top" } do %>
|
||||
<%= icon "download", class: "w-5 h-5" %>
|
||||
<span class="text-sm font-medium">Download</span>
|
||||
<% end %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= button_to family_export_path(export),
|
||||
method: :delete,
|
||||
class: "flex items-center gap-2 text-destructive hover:text-destructive-hover",
|
||||
data: {
|
||||
turbo_confirm: "Are you sure you want to delete this export? This action cannot be undone.",
|
||||
turbo_frame: "_top"
|
||||
} do %>
|
||||
<%= icon "trash-2", class: "w-5 h-5 text-destructive" %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to download_family_export_path(export),
|
||||
class: "flex items-center gap-2 text-primary hover:text-primary-hover",
|
||||
data: { turbo_frame: "_top" } do %>
|
||||
<%= icon "download", class: "w-5 h-5" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% elsif export.failed? %>
|
||||
<div class="flex items-center gap-2 text-destructive">
|
||||
<%= icon "alert-circle", class: "w-4 h-4" %>
|
||||
<span class="text-sm">Failed</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2 text-destructive">
|
||||
<%= icon "alert-circle", class: "w-4 h-4" %>
|
||||
</div>
|
||||
|
||||
<%= button_to family_export_path(export),
|
||||
method: :delete,
|
||||
class: "flex items-center gap-2 text-destructive hover:text-destructive-hover",
|
||||
data: {
|
||||
turbo_confirm: "Are you sure you want to delete this failed export?",
|
||||
turbo_frame: "_top"
|
||||
} do %>
|
||||
<%= icon "trash-2", class: "w-5 h-5 text-destructive" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p class="text-sm text-secondary text-center py-4">No exports yet</p>
|
||||
<p class="text-sm text-primary text-center py-4 mb-1 font-medium">No exports yet.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
<div class="flex justify-center items-center py-20">
|
||||
<div class="text-center flex flex-col items-center max-w-[300px] gap-4">
|
||||
<p class="text-primary mb-1 font-medium text-sm"><%= t(".message") %></p>
|
||||
|
||||
<%= render DS::Link.new(
|
||||
text: t(".new"),
|
||||
variant: "primary",
|
||||
href: new_import_path,
|
||||
icon: "plus",
|
||||
frame: :modal
|
||||
) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,30 +36,36 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% menu.with_item(variant: "link", text: t(".view"), href: import_path(import), icon: "eye") %>
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
|
||||
<% if import.complete? || import.revert_failed? %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".revert"),
|
||||
href: revert_import_path(import),
|
||||
icon: "rotate-ccw",
|
||||
method: :put,
|
||||
confirm: CustomConfirm.new(
|
||||
title: "Revert import?",
|
||||
body: "This will delete transactions that were imported, but you will still be able to review and re-import your data at any time.",
|
||||
btn_text: "Revert"
|
||||
)) %>
|
||||
<%= button_to revert_import_path(import),
|
||||
method: :put,
|
||||
class: "flex items-center gap-2 text-orange-500 hover:text-orange-600",
|
||||
data: {
|
||||
turbo_confirm: "This will delete transactions that were imported, but you will still be able to review and re-import your data at any time."
|
||||
} do %>
|
||||
<%= icon "rotate-ccw", class: "w-5 h-5 text-destructive" %>
|
||||
<% end %>
|
||||
|
||||
|
||||
|
||||
|
||||
<% else %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
href: import_path(import),
|
||||
icon: "trash-2",
|
||||
method: :delete,
|
||||
confirm: CustomConfirm.for_resource_deletion("import")) %>
|
||||
<%= button_to import_path(import),
|
||||
method: :delete,
|
||||
class: "flex items-center gap-2 text-destructive hover:text-destructive-hover",
|
||||
data: {
|
||||
turbo_confirm: CustomConfirm.for_resource_deletion("import")
|
||||
} do %>
|
||||
<%= icon "trash-2", class: "w-5 h-5 text-destructive" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to import_path(import),
|
||||
class: "flex items-center gap-2 text-primary hover:text-primary-hover" do %>
|
||||
<%= icon "eye", class: "w-5 h-5" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,39 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-medium text-primary"><%= t(".title") %></h1>
|
||||
|
||||
<%= render DS::Link.new(
|
||||
text: "New import",
|
||||
href: new_import_path,
|
||||
icon: "plus",
|
||||
variant: "primary",
|
||||
frame: :modal
|
||||
) %>
|
||||
</div>
|
||||
|
||||
<div class="bg-container shadow-border-xs rounded-xl p-4">
|
||||
<% if @imports.empty? %>
|
||||
<%= render partial: "imports/empty" %>
|
||||
<% else %>
|
||||
<div class="rounded-xl bg-container-inset p-1">
|
||||
<h2 class="uppercase px-4 py-2 text-secondary text-xs"><%= t(".imports") %> · <%= @imports.size %></h2>
|
||||
|
||||
<div class="border border-alpha-black-100 rounded-lg bg-container shadow-xs">
|
||||
<%= render partial: "imports/import", collection: @imports.ordered, spacer_template: "shared/ruler" %>
|
||||
<%= settings_section title: t(".imports") do %>
|
||||
<div class="space-y-4">
|
||||
<% if @imports.empty? %>
|
||||
<%= render partial: "imports/empty" %>
|
||||
<% else %>
|
||||
<div class="bg-container rounded-lg shadow-border-xs">
|
||||
<%= render partial: "imports/import", collection: @imports.ordered %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_import_path,
|
||||
class: "bg-container-inset flex items-center justify-center gap-2 text-secondary mt-1 hover:bg-container-inset-hover rounded-lg px-4 py-2 w-full text-center",
|
||||
data: { turbo_frame: :modal } do %>
|
||||
<%= icon("plus") %>
|
||||
<%= t(".new") %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if Current.user.admin? %>
|
||||
<%= settings_section title: t(".exports") do %>
|
||||
<div class="space-y-4">
|
||||
<div class="bg-container rounded-lg shadow-border-xs">
|
||||
<%= turbo_frame_tag "family_exports", src: family_exports_path, loading: :lazy do %>
|
||||
<div class="mt-4 text-center text-secondary py-8">
|
||||
<div class="animate-spin inline-block h-4 w-4 border-2 border-secondary border-t-transparent rounded-full"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= link_to new_family_export_path,
|
||||
class: "bg-container-inset flex items-center justify-center gap-2 text-secondary mt-1 hover:bg-container-inset-hover rounded-lg px-4 py-2 w-full text-center",
|
||||
data: { turbo_frame: :modal } do %>
|
||||
<%= icon("plus") %>
|
||||
<%= t(".new_export") %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -3,30 +3,37 @@ nav_sections = [
|
||||
{
|
||||
header: t(".general_section_title"),
|
||||
items: [
|
||||
{ label: t(".profile_label"), path: settings_profile_path, icon: "circle-user" },
|
||||
{ label: t(".preferences_label"), path: settings_preferences_path, icon: "bolt" },
|
||||
{ label: t(".security_label"), path: settings_security_path, icon: "shield-check" },
|
||||
{ label: t(".api_keys_label"), path: settings_api_key_path, icon: "key" },
|
||||
{ label: t(".self_hosting_label"), path: settings_hosting_path, icon: "database", if: self_hosted? },
|
||||
{ label: t(".billing_label"), path: settings_billing_path, icon: "circle-dollar-sign", if: !self_hosted? },
|
||||
{ label: t(".accounts_label"), path: accounts_path, icon: "layers" },
|
||||
{ label: t(".bank_sync_label"), path: settings_bank_sync_path, icon: "banknote" },
|
||||
{ label: "SimpleFin", path: simplefin_items_path, icon: "building-2" },
|
||||
{ label: t(".imports_label"), path: imports_path, icon: "download" }
|
||||
{ label: t(".preferences_label"), path: settings_preferences_path, icon: "bolt" },
|
||||
{ label: t(".profile_label"), path: settings_profile_path, icon: "circle-user" },
|
||||
{ label: t(".security_label"), path: settings_security_path, icon: "shield-check" },
|
||||
{ label: t(".billing_label"), path: settings_billing_path, icon: "circle-dollar-sign", if: !self_hosted? }
|
||||
]
|
||||
},
|
||||
{
|
||||
header: t(".transactions_section_title"),
|
||||
items: [
|
||||
{ label: t(".tags_label"), path: tags_path, icon: "tags" },
|
||||
{ label: t(".categories_label"), path: categories_path, icon: "shapes" },
|
||||
{ label: t(".tags_label"), path: tags_path, icon: "tags" },
|
||||
{ label: t(".rules_label"), path: rules_path, icon: "git-branch" },
|
||||
{ label: t(".merchants_label"), path: family_merchants_path, icon: "store" }
|
||||
]
|
||||
},
|
||||
{
|
||||
header: t(".advanced_section_title"),
|
||||
items: [
|
||||
{ label: t(".ai_prompts_label"), path: settings_ai_prompts_path, icon: "bot" },
|
||||
{ label: t(".api_keys_label"), path: settings_api_key_path, icon: "key" },
|
||||
{ label: t(".self_hosting_label"), path: settings_hosting_path, icon: "database", if: self_hosted? },
|
||||
{ label: t(".imports_label"), path: imports_path, icon: "download" },
|
||||
{ label: "SimpleFin", path: simplefin_items_path, icon: "building-2" }
|
||||
]
|
||||
},
|
||||
{
|
||||
header: t(".other_section_title"),
|
||||
items: [
|
||||
{ label: t(".guides_label"), path: settings_guides_path, icon: "book-open" },
|
||||
{ label: t(".whats_new_label"), path: changelog_path, icon: "box" },
|
||||
{ label: t(".feedback_label"), path: feedback_path, icon: "megaphone" }
|
||||
]
|
||||
|
||||
95
app/views/settings/ai_prompts/show.html.erb
Normal file
95
app/views/settings/ai_prompts/show.html.erb
Normal file
@@ -0,0 +1,95 @@
|
||||
<%= content_for :page_title, t(".page_title") %>
|
||||
|
||||
<div class="bg-container rounded-xl shadow-border-xs p-4">
|
||||
<div class="rounded-xl bg-container-inset space-y-1 p-1">
|
||||
<div class="flex items-center gap-1.5 px-4 py-2 text-xs font-medium text-secondary">
|
||||
<p><%= t(".openai_label") %></p>
|
||||
</div>
|
||||
|
||||
<div class="bg-container rounded-lg shadow-border-xs">
|
||||
<div class="space-y-4 p-4">
|
||||
<!-- Main System Prompt Section -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-full bg-gray-25 flex justify-center items-center">
|
||||
<%= icon "message-circle" %>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-primary"><%= t(".main_system_prompt.title") %></p>
|
||||
<p class="text-xs text-secondary"><%= t(".main_system_prompt.subtitle") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pl-12 space-y-2">
|
||||
<details class="group">
|
||||
<summary class="flex items-center gap-2 cursor-pointer">
|
||||
<span class="text-xs font-medium text-primary uppercase"><%= t(".prompt_instructions") %></span>
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
</summary>
|
||||
<pre class="whitespace-pre-wrap text-xs font-mono text-primary">[<%= Provider::Openai::MODELS.join(", ") %>]</pre>
|
||||
<div class="mt-2 px-3 py-2 bg-surface-default border border-primary rounded-lg">
|
||||
<pre class="whitespace-pre-wrap text-xs font-mono text-primary"><%= @assistant_config[:instructions] %></pre>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-primary"></div>
|
||||
|
||||
<!-- Auto-Categorization Section -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-full bg-gray-25 flex justify-center items-center">
|
||||
<%= icon "brain" %>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-primary"><%= t(".transaction_categorizer.title") %></p>
|
||||
<p class="text-xs text-secondary"><%= t(".transaction_categorizer.subtitle") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pl-12 space-y-2">
|
||||
<details class="group">
|
||||
<summary class="flex items-center gap-2 cursor-pointer">
|
||||
<span class="text-xs font-medium text-primary uppercase"><%= t(".prompt_instructions") %></span>
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
</summary>
|
||||
<pre class="whitespace-pre-wrap text-xs font-mono text-primary">[<%= Provider::Openai::AutoCategorizer::DEFAULT_MODEL %>]</pre>
|
||||
<div class="mt-2 px-3 py-2 bg-surface-default border border-primary rounded-lg">
|
||||
<pre class="whitespace-pre-wrap text-xs font-mono text-primary"><%= @assistant_config[:auto_categorizer]&.instructions || Provider::Openai::AutoCategorizer.new(nil).instructions %></pre>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-primary"></div>
|
||||
|
||||
<!-- Merchant Detection Section -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-full bg-gray-25 flex justify-center items-center">
|
||||
<%= icon "store" %>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-primary"><%= t(".merchant_detector.title") %></p>
|
||||
<p class="text-xs text-secondary"><%= t(".merchant_detector.subtitle") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pl-12 space-y-2">
|
||||
<details class="group">
|
||||
<summary class="flex items-center gap-2 cursor-pointer">
|
||||
<span class="text-xs font-medium text-primary uppercase"><%= t(".prompt_instructions") %></span>
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
</summary>
|
||||
<pre class="whitespace-pre-wrap text-xs font-mono text-primary">[<%= Provider::Openai::AutoMerchantDetector::DEFAULT_MODEL %>]</pre>
|
||||
<div class="mt-2 px-3 py-2 bg-surface-default border border-primary rounded-lg">
|
||||
<pre class="whitespace-pre-wrap text-xs font-mono text-primary"><%= @assistant_config[:auto_merchant]&.instructions || Provider::Openai::AutoMerchantDetector.new(nil, model: "", transactions: [], user_merchants: []).instructions %></pre>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
<%= content_for :page_title, "Create New API Key" %>
|
||||
|
||||
<%= settings_section title: "Create New API Key", subtitle: "Generate a new API key to access your Maybe data programmatically." do %>
|
||||
<%= settings_section title: nil, subtitle: "Generate a new API key to access your Maybe data programmatically." do %>
|
||||
<%= styled_form_with model: @api_key, url: settings_api_key_path, class: "space-y-4" do |form| %>
|
||||
<%= form.text_field :name,
|
||||
placeholder: "e.g., My Budget App, Portfolio Tracker",
|
||||
@@ -51,7 +51,7 @@
|
||||
) %>
|
||||
|
||||
<%= render DS::Button.new(
|
||||
text: "Create API Key",
|
||||
text: "Save API Key",
|
||||
variant: "primary",
|
||||
type: "submit"
|
||||
) %>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<%= content_for :page_title, "API Key" %>
|
||||
|
||||
<% if @newly_created && @plain_key %>
|
||||
<%= settings_section title: "API Key Created Successfully", subtitle: "Your new API key has been generated successfully." do %>
|
||||
<header class="flex items-center justify-between">
|
||||
<h1 class="text-primary text-xl font-medium">API Key Created Successfully</h1>
|
||||
</header>
|
||||
|
||||
<div class="bg-container rounded-xl shadow-border-xs p-4">
|
||||
<div class="space-y-4">
|
||||
<div class="p-3 shadow-border-xs bg-container rounded-lg">
|
||||
<div class="flex items-start gap-3">
|
||||
@@ -51,9 +53,18 @@
|
||||
) %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% elsif @current_api_key %>
|
||||
<%= settings_section title: "Your API Key", subtitle: "Manage your API key for programmatic access to your Maybe data." do %>
|
||||
<header class="flex items-center justify-between">
|
||||
<h1 class="text-primary text-xl font-medium">Your API Key</h1>
|
||||
<%= render DS::Link.new(
|
||||
text: "Create New Key",
|
||||
href: new_settings_api_key_path(regenerate: true),
|
||||
variant: "secondary"
|
||||
) %>
|
||||
</header>
|
||||
|
||||
<div class="bg-container rounded-xl shadow-border-xs p-4">
|
||||
<div class="space-y-4">
|
||||
<div class="p-3 shadow-border-xs bg-container rounded-lg flex justify-between items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -122,13 +133,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-3 pt-4 border-t border-primary">
|
||||
<%= render DS::Link.new(
|
||||
text: "Create New Key",
|
||||
href: new_settings_api_key_path(regenerate: true),
|
||||
variant: "secondary"
|
||||
) %>
|
||||
|
||||
<div class="flex justify-end pt-4 border-t border-primary">
|
||||
<%= render DS::Button.new(
|
||||
text: "Revoke Key",
|
||||
href: settings_api_key_path,
|
||||
@@ -140,9 +145,18 @@
|
||||
) %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= settings_section title: "Create Your API Key", subtitle: "Get programmatic access to your Maybe data" do %>
|
||||
<header class="flex items-center justify-between">
|
||||
<h1 class="text-primary text-xl font-medium">API Key</h1>
|
||||
<%= render DS::Link.new(
|
||||
text: "Create API Key",
|
||||
href: new_settings_api_key_path,
|
||||
variant: "primary"
|
||||
) %>
|
||||
</header>
|
||||
|
||||
<div class="bg-container rounded-xl shadow-border-xs p-4">
|
||||
<div class="space-y-4">
|
||||
<div class="p-3 shadow-border-xs bg-container rounded-lg">
|
||||
<div class="flex items-start gap-3">
|
||||
@@ -179,14 +193,6 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-start">
|
||||
<%= render DS::Link.new(
|
||||
text: "Create API Key",
|
||||
href: new_settings_api_key_path,
|
||||
variant: "primary"
|
||||
) %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
5
app/views/settings/guides/show.html.erb
Normal file
5
app/views/settings/guides/show.html.erb
Normal file
@@ -0,0 +1,5 @@
|
||||
<%= content_for :page_title, "Guides" %>
|
||||
|
||||
<div class="bg-container rounded-xl shadow-border-xs p-4 prose prose-sm max-w-none">
|
||||
<%= @guide_content.html_safe %>
|
||||
</div>
|
||||
@@ -122,29 +122,6 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if Current.user.admin? %>
|
||||
<%= settings_section title: "Data Import/Export" do %>
|
||||
<div class="space-y-4">
|
||||
<div class="flex gap-4 items-center">
|
||||
<%= render DS::Link.new(
|
||||
text: "Export data",
|
||||
icon: "database",
|
||||
href: new_family_export_path,
|
||||
variant: "secondary",
|
||||
full_width: true,
|
||||
data: { turbo_frame: :modal }
|
||||
) %>
|
||||
</div>
|
||||
|
||||
<%= turbo_frame_tag "family_exports", src: family_exports_path, loading: :lazy do %>
|
||||
<div class="mt-4 text-center text-secondary">
|
||||
<div class="animate-spin inline-block h-4 w-4 border-2 border-secondary border-t-transparent rounded-full"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= settings_section title: t(".danger_zone_title") do %>
|
||||
<div class="space-y-4">
|
||||
<% if Current.user.admin? %>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% menu.with_item(variant: "link", text: "Settings", icon: "settings", href: settings_profile_path(return_to: request.fullpath)) %>
|
||||
<% menu.with_item(variant: "link", text: "Settings", icon: "settings", href: accounts_path(return_to: request.fullpath)) %>
|
||||
<% menu.with_item(variant: "link", text: "Changelog", icon: "box", href: changelog_path) %>
|
||||
|
||||
<% if self_hosted? %>
|
||||
|
||||
Reference in New Issue
Block a user