mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
Add inline category selection (#541)
* Implement inline category selection * Add turbo frame to refresh updated transaction * Improve styles * Fix category assignment * Reorganize code * Revert event propagation * Remove unused frames * Make only the transaction name clickable * Add custom scrollbar class
This commit is contained in:
parent
2c3752668a
commit
2c257a2a4b
13 changed files with 177 additions and 205 deletions
|
@ -57,4 +57,16 @@
|
|||
|
||||
/* Small, single purpose classes that should take precedence over other styles */
|
||||
@layer utilities {
|
||||
.scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #d6d6d6;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: #a6a6a6;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ class TransactionsController < ApplicationController
|
|||
|
||||
respond_to do |format|
|
||||
if @transaction.save
|
||||
format.html { redirect_to transaction_url(@transaction), notice: "Transaction was successfully created." }
|
||||
format.html { redirect_to transaction_url(@transaction), notice: t(".success") }
|
||||
else
|
||||
format.html { render :new, status: :unprocessable_entity }
|
||||
end
|
||||
|
@ -45,7 +45,13 @@ class TransactionsController < ApplicationController
|
|||
def update
|
||||
respond_to do |format|
|
||||
if @transaction.update(transaction_params)
|
||||
format.html { redirect_to transaction_url(@transaction), notice: "Transaction was successfully updated." }
|
||||
format.html { redirect_to transaction_url(@transaction), notice: t(".success") }
|
||||
format.turbo_stream do
|
||||
render turbo_stream: [
|
||||
turbo_stream.append("notification-tray", partial: "shared/notification", locals: { type: "success", content: t(".success") }),
|
||||
turbo_stream.replace("transaction_#{@transaction.id}", partial: "transactions/transaction", locals: { transaction: @transaction })
|
||||
]
|
||||
end
|
||||
else
|
||||
format.html { render :edit, status: :unprocessable_entity }
|
||||
end
|
||||
|
@ -56,7 +62,7 @@ class TransactionsController < ApplicationController
|
|||
@transaction.destroy!
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to transactions_url, notice: "Transaction was successfully destroyed." }
|
||||
format.html { redirect_to transactions_url, notice: t(".success") }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -68,6 +74,6 @@ class TransactionsController < ApplicationController
|
|||
|
||||
# Only allow a list of trusted parameters through.
|
||||
def transaction_params
|
||||
params.require(:transaction).permit(:name, :date, :amount, :currency, :notes, :excluded)
|
||||
params.require(:transaction).permit(:name, :date, :amount, :currency, :notes, :excluded, :category_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,20 +4,32 @@ import { Controller } from "@hotwired/stimulus"
|
|||
export default class extends Controller {
|
||||
static targets = ["menu"]
|
||||
|
||||
toggleMenu(event) {
|
||||
event.stopPropagation(); // Prevent event from closing the menu immediately
|
||||
this.menuTarget.classList.toggle("hidden");
|
||||
toggleMenu = (e) => {
|
||||
e.stopPropagation(); // Prevent event from closing the menu immediately
|
||||
this.menuTarget.classList.contains("hidden") ? this.showMenu() : this.hideMenu();
|
||||
}
|
||||
|
||||
showMenu = () => {
|
||||
document.addEventListener("click", this.onDocumentClick);
|
||||
this.menuTarget.classList.remove("hidden");
|
||||
}
|
||||
|
||||
hideMenu = () => {
|
||||
document.removeEventListener("click", this.onDocumentClick);
|
||||
this.menuTarget.classList.add("hidden");
|
||||
}
|
||||
|
||||
connect() {
|
||||
document.addEventListener("click", this.hideMenu);
|
||||
disconnect = () => {
|
||||
this.hideMenu();
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
document.removeEventListener("click", this.hideMenu);
|
||||
onDocumentClick = (e) => {
|
||||
if (this.menuTarget.contains(e.target)) {
|
||||
// user has clicked inside of the dropdown
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
this.hideMenu();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
// Hide the dialog when the user clicks outside of it
|
||||
click_outside(e) {
|
||||
clickOutside(e) {
|
||||
if (e.target === this.element) {
|
||||
this.element.close();
|
||||
}
|
||||
|
|
|
@ -2,6 +2,22 @@
|
|||
* To keep consistent styling across the app, this file can be imported in
|
||||
* Stimulus controllers to reference our color palette. Mostly used for D3 charts.
|
||||
*/
|
||||
|
||||
export const categoryColors = [
|
||||
"#e99537",
|
||||
"#4da568",
|
||||
"#6471eb",
|
||||
"#db5a54",
|
||||
"#df4e92",
|
||||
"#c44fe9",
|
||||
"#eb5429",
|
||||
"#61c9ea",
|
||||
"#805dee",
|
||||
"#6ad28a"
|
||||
]
|
||||
|
||||
export const categoryDefaultColor = "#737373"
|
||||
|
||||
export default {
|
||||
transparent: "transparent",
|
||||
current: "currentColor",
|
||||
|
@ -138,4 +154,43 @@ export default {
|
|||
600: "#7839EE",
|
||||
700: "#6927DA",
|
||||
},
|
||||
fuchsia: {
|
||||
25: "#FEFAFF",
|
||||
50: "#FDF4FF",
|
||||
100: "#FBE8FF",
|
||||
200: "#F6D0FE",
|
||||
300: "#EEAAFD",
|
||||
400: "#E478FA",
|
||||
500: "#D444F1",
|
||||
600: "#BA24D5",
|
||||
700: "#9F1AB1",
|
||||
800: "#821890",
|
||||
900: "#6F1877",
|
||||
},
|
||||
pink: {
|
||||
25: "#FFFAFC",
|
||||
50: "#FEF0F7",
|
||||
100: "#FFD1E2",
|
||||
200: "#FFB1CE",
|
||||
300: "#FD8FBA",
|
||||
400: "#F86BA7",
|
||||
500: "#F23E94",
|
||||
600: "#D5327F",
|
||||
700: "#BA256B",
|
||||
800: "#9E1958",
|
||||
900: "#840B45",
|
||||
},
|
||||
orange: {
|
||||
25: "#FFF9F5",
|
||||
50: "#FFF4ED",
|
||||
100: "#FFE6D5",
|
||||
200: "#FFD6AE",
|
||||
300: "#FF9C66",
|
||||
400: "#FF692E",
|
||||
500: "#FF4405",
|
||||
600: "#E62E05",
|
||||
700: "#BC1B06",
|
||||
800: "#97180C",
|
||||
900: "#771A0D",
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,14 +5,14 @@ class Transaction::Category < ApplicationRecord
|
|||
before_update :clear_internal_category, if: :name_changed?
|
||||
|
||||
DEFAULT_CATEGORIES = [
|
||||
{ internal_category: "income", color: "#fd7f6f" },
|
||||
{ internal_category: "food_and_drink", color: "#7eb0d5" },
|
||||
{ internal_category: "entertainment", color: "#b2e061" },
|
||||
{ internal_category: "personal_care", color: "#bd7ebe" },
|
||||
{ internal_category: "general_services", color: "#ffb55a" },
|
||||
{ internal_category: "auto_and_transport", color: "#ffee65" },
|
||||
{ internal_category: "rent_and_utilities", color: "#beb9db" },
|
||||
{ internal_category: "home_improvement", color: "#fdcce5" }
|
||||
{ internal_category: "income", color: "#e99537" },
|
||||
{ internal_category: "food_and_drink", color: "#4da568" },
|
||||
{ internal_category: "entertainment", color: "#6471eb" },
|
||||
{ internal_category: "personal_care", color: "#db5a54" },
|
||||
{ internal_category: "general_services", color: "#df4e92" },
|
||||
{ internal_category: "auto_and_transport", color: "#c44fe9" },
|
||||
{ internal_category: "rent_and_utilities", color: "#eb5429" },
|
||||
{ internal_category: "home_improvement", color: "#61c9ea" }
|
||||
]
|
||||
|
||||
def self.ransackable_attributes(auth_object = nil)
|
||||
|
|
4
app/views/shared/_category_badge.html.erb
Normal file
4
app/views/shared/_category_badge.html.erb
Normal file
|
@ -0,0 +1,4 @@
|
|||
<%# locals: (name: "Uncategorized", color: "#737373") %>
|
||||
<% background_color = "color-mix(in srgb, #{color} 5%, white)" %>
|
||||
<% border_color = "color-mix(in srgb, #{color} 10%, white)" %>
|
||||
<span class="border text-sm font-medium px-2.5 py-1 rounded-full cursor-pointer" style="background-color: <%= background_color %>; border-color: <%= border_color %>; color: <%= color %>"><%= name %></span>
|
|
@ -1,5 +1,5 @@
|
|||
<%= turbo_frame_tag "modal" do %>
|
||||
<dialog class="bg-white border border-alpha-black-25 rounded-2xl max-h-[calc(100vh-32px)] max-w-[480px] w-full shadow-xs h-full mt-4 mr-4" data-controller="modal" data-action="click->modal#click_outside">
|
||||
<dialog class="bg-white border border-alpha-black-25 rounded-2xl max-h-[calc(100vh-32px)] max-w-[480px] w-full shadow-xs h-full mt-4 mr-4" data-controller="modal" data-action="click->modal#clickOutside">
|
||||
<div class="flex flex-col h-full p-4">
|
||||
<div class="flex justify-end items-center h-9">
|
||||
<div data-action="click->modal#close" class="cursor-pointer">
|
||||
|
|
42
app/views/transactions/_category_dropdown.html.erb
Normal file
42
app/views/transactions/_category_dropdown.html.erb
Normal file
|
@ -0,0 +1,42 @@
|
|||
<%# locals: (transaction:) %>
|
||||
<div class="relative" data-controller="dropdown">
|
||||
<div class="flex cursor-pointer" data-action="click->dropdown#toggleMenu">
|
||||
<%= render partial: "shared/category_badge", locals: transaction.category.nil? ? {} : { name: transaction.category.name, color: transaction.category.color } %>
|
||||
</div>
|
||||
<div class="absolute z-10 hidden w-screen mt-2 max-w-min cursor-default" data-dropdown-target="menu">
|
||||
<div class="w-64 text-sm font-semibold leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<div class="flex flex-col">
|
||||
<div class="grow p-1.5 cursor-not-allowed">
|
||||
<div class="relative flex items-center bg-white border border-gray-200 rounded-lg">
|
||||
<input placeholder="Search" class="placeholder:text-sm placeholder:text-gray-500 h-10 relative pl-10 w-full border-none rounded-lg cursor-not-allowed" disabled/>
|
||||
<%= lucide_icon("search", class: "w-5 h-5 text-gray-500 ml-2 absolute inset-0 transform top-1/2 -translate-y-1/2") %>
|
||||
</div>
|
||||
</div>
|
||||
<%= form_with model: transaction, namespace: dom_id(transaction), html: { data: { controller: "auto-submit-form" }, class: "flex flex-col gap-0.5 p-1.5 mt-0.5 mr-2 max-h-64 overflow-y-scroll scrollbar" } do |form| %>
|
||||
<% Current.family.transaction_categories.each do |category| %>
|
||||
<% is_selected = (!transaction.category.nil? and category.id == transaction.category.id) %>
|
||||
<div class="flex items-center <%= class_names("bg-gray-25": is_selected) %> hover:bg-gray-25 border-none rounded-lg px-2 py-1 group">
|
||||
<%= form.radio_button :category_id, category.id, class: "hidden" %>
|
||||
<%= label dom_id(transaction), :transaction_category_id, value: category.id, class: "flex w-full items-center gap-1.5 cursor-pointer" do %>
|
||||
<span class="w-5 h-5">
|
||||
<%= lucide_icon("check", class: "w-5 h-5 text-gray-500") if is_selected %>
|
||||
</span>
|
||||
<%= render partial: "shared/category_badge", locals: { name: category.name, color: category.color } %>
|
||||
<% end %>
|
||||
<button class="ml-auto flex items-center justify-center hover:bg-gray-50 w-8 h-8 rounded-lg invisible group-hover:visible cursor-not-allowed" type="button" disabled>
|
||||
<%= lucide_icon("more-horizontal", class: "w-5 h-5 text-gray-500") %>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<hr/>
|
||||
<div class="p-1.5 w-full">
|
||||
<button class="cursor-not-allowed flex text-sm font-medium items-center gap-2 text-gray-500 w-full rounded-lg p-2 hover:bg-gray-100" disabled>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,7 +1,12 @@
|
|||
<%= link_to transaction_path(transaction), data: { turbo_frame: "modal" }, class: "text-gray-900 flex items-center gap-6 py-4 text-sm font-medium hover:bg-gray-50 px-4", id: dom_id(transaction) do %>
|
||||
<div class="w-96 flex items-center gap-2">
|
||||
<div class="w-8 h-8 flex items-center justify-center rounded-full bg-gray-600/5 text-gray-600"><%= transaction.name[0].upcase %></div>
|
||||
<p class="text-gray-900"><%= transaction.name %></p>
|
||||
<%= turbo_frame_tag dom_id(transaction), class:"text-gray-900 flex items-center gap-6 py-4 text-sm font-medium px-4" do %>
|
||||
<%= link_to transaction_path(transaction), data: { turbo_frame: "modal" }, class: "group", id: dom_id(transaction) do %>
|
||||
<div class="w-96 flex items-center gap-2">
|
||||
<div class="w-8 h-8 flex items-center justify-center rounded-full bg-gray-600/5 text-gray-600"><%= transaction.name[0].upcase %></div>
|
||||
<p class="text-gray-900 group-hover:underline group-hover:text-gray-800"><%= transaction.name %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="w-48">
|
||||
<%= render partial: "transactions/category_dropdown", locals: { transaction: } %>
|
||||
</div>
|
||||
<div>
|
||||
<p><%= transaction.account.name %></p>
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
</div>
|
||||
<div>
|
||||
<%= form.select :date, options_for_select([['All', 'all'], ['7D', 'last_7_days'], ['1M', 'last_30_days'], ["1Y", "last_365_days"]], selected: params.dig(:q, :date)), {}, { class: "block h-full w-full border border-gray-200 rounded-lg text-sm py-2 pr-8 pl-2", "data-transactions-search-target": "date" } %>
|
||||
|
||||
|
||||
<!-- Hidden fields for date range -->
|
||||
<%= form.hidden_field :date_gteq, value: '', "data-transactions-search-target": "dateGteq" %>
|
||||
<%= form.hidden_field :date_lteq, value: '', "data-transactions-search-target": "dateLteq" %>
|
||||
|
@ -68,12 +68,15 @@
|
|||
<div class="w-96">
|
||||
<p class="uppercase">transaction</p>
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<p class="uppercase">category</p>
|
||||
</div>
|
||||
<div class="grow uppercase flex justify-between items-center gap-5 text-xs font-medium text-gray-500">
|
||||
<p>account</p>
|
||||
<p>amount</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="space-y-6">
|
||||
<% @transactions.group_by { |transaction| transaction.date }.each do |date, grouped_transactions| %>
|
||||
<%= render partial: "transactions/transaction_group", locals: { date: date, transactions: grouped_transactions } %>
|
||||
|
|
9
config/locales/views/transaction/en.yml
Normal file
9
config/locales/views/transaction/en.yml
Normal file
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
en:
|
||||
transactions:
|
||||
create:
|
||||
success: New transaction created successfully
|
||||
destroy:
|
||||
success: Transaction deleted successfully
|
||||
update:
|
||||
success: Transaction updated successfully
|
|
@ -1,4 +1,5 @@
|
|||
const defaultTheme = require("tailwindcss/defaultTheme");
|
||||
const colors = require("../app/javascript/tailwindColors");
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
|
@ -8,184 +9,7 @@ module.exports = {
|
|||
"./app/views/**/*.{erb,haml,html,slim}",
|
||||
],
|
||||
theme: {
|
||||
colors: {
|
||||
transparent: "transparent",
|
||||
current: "currentColor",
|
||||
white: "#ffffff",
|
||||
black: "#0B0B0B",
|
||||
success: "#10A861",
|
||||
warning: "#F79009",
|
||||
error: "#F13636",
|
||||
gray: {
|
||||
25: "#FAFAFA",
|
||||
50: "#F5F5F5",
|
||||
100: "#F0F0F0",
|
||||
200: "#E5E5E5",
|
||||
300: "#D6D6D6",
|
||||
400: "#A3A3A3 ",
|
||||
500: "#737373",
|
||||
600: "#525252",
|
||||
700: "#3D3D3D",
|
||||
800: "#212121",
|
||||
900: "#141414",
|
||||
},
|
||||
"alpha-white": {
|
||||
25: "rgba(255, 255, 255, 0.03)",
|
||||
50: "rgba(255, 255, 255, 0.05)",
|
||||
100: "rgba(255, 255, 255, 0.08)",
|
||||
200: "rgba(255, 255, 255, 0.1)",
|
||||
300: "rgba(255, 255, 255, 0.15)",
|
||||
400: "rgba(255, 255, 255, 0.2)",
|
||||
500: "rgba(255, 255, 255, 0.3)",
|
||||
600: "rgba(255, 255, 255, 0.4)",
|
||||
700: "rgba(255, 255, 255, 0.5)",
|
||||
800: "rgba(255, 255, 255, 0.6)",
|
||||
900: "rgba(255, 255, 255, 0.7)",
|
||||
},
|
||||
"alpha-black": {
|
||||
25: "rgba(20, 20, 20, 0.03)",
|
||||
50: "rgba(20, 20, 20, 0.05)",
|
||||
100: "rgba(20, 20, 20, 0.08)",
|
||||
200: "rgba(20, 20, 20, 0.1)",
|
||||
300: "rgba(20, 20, 20, 0.15)",
|
||||
400: "rgba(20, 20, 20, 0.2)",
|
||||
500: "rgba(20, 20, 20, 0.3)",
|
||||
600: "rgba(20, 20, 20, 0.4)",
|
||||
700: "rgba(20, 20, 20, 0.5)",
|
||||
800: "rgba(20, 20, 20, 0.6)",
|
||||
900: "rgba(20, 20, 20, 0.7)",
|
||||
},
|
||||
red: {
|
||||
25: "#FFFBFB",
|
||||
50: "#FFF1F0",
|
||||
100: "#FFDEDB",
|
||||
200: "#FEB9B3",
|
||||
300: "#F88C86",
|
||||
400: "#ED4E4E",
|
||||
500: "#F13636",
|
||||
600: "#EC2222",
|
||||
700: "#C91313",
|
||||
800: "#A40E0E",
|
||||
900: "#7E0707",
|
||||
},
|
||||
green: {
|
||||
25: "#F6FEF9",
|
||||
50: "#ECFDF3",
|
||||
100: "#D1FADF",
|
||||
200: "#A6F4C5",
|
||||
300: "#6CE9A6",
|
||||
400: "#32D583",
|
||||
500: "#12B76A",
|
||||
600: "#10A861",
|
||||
700: "#078C52",
|
||||
800: "#05603A",
|
||||
900: "#054F31",
|
||||
},
|
||||
yellow: {
|
||||
25: "#FFFCF5",
|
||||
50: "#FFFAEB",
|
||||
100: "#FEF0C7",
|
||||
200: "#FEDF89",
|
||||
300: "#FEC84B",
|
||||
400: "#FDB022",
|
||||
500: "#F79009",
|
||||
600: "#DC6803",
|
||||
700: "#B54708",
|
||||
800: "#93370D",
|
||||
900: "#7A2E0E",
|
||||
},
|
||||
cyan: {
|
||||
25: "#F5FEFF",
|
||||
50: "#ECFDFF",
|
||||
100: "#CFF9FE",
|
||||
200: "#A5F0FC",
|
||||
300: "#67E3F9",
|
||||
400: "#22CCEE",
|
||||
500: "#06AED4",
|
||||
600: "#088AB2",
|
||||
700: "#0E7090",
|
||||
800: "#155B75",
|
||||
900: "#155B75",
|
||||
},
|
||||
blue: {
|
||||
25: "#F5FAFF",
|
||||
50: "#EFF8FF",
|
||||
100: "#D1E9FF",
|
||||
200: "#B2DDFF",
|
||||
300: "#84CAFF",
|
||||
400: "#53B1FD",
|
||||
500: "#2E90FA",
|
||||
600: "#1570EF",
|
||||
700: "#175CD3",
|
||||
800: "#1849A9",
|
||||
900: "#194185",
|
||||
},
|
||||
indigo: {
|
||||
25: "#F5F8FF",
|
||||
50: "#EFF4FF",
|
||||
100: "#E0EAFF",
|
||||
200: "#C7D7FE",
|
||||
300: "#A4BCFD",
|
||||
400: "#8098F9",
|
||||
500: "#6172F3",
|
||||
600: "#444CE7",
|
||||
700: "#3538CD",
|
||||
800: "#2D31A6",
|
||||
900: "#2D3282",
|
||||
},
|
||||
violet: {
|
||||
25: "#FBFAFF",
|
||||
50: "#F5F3FF",
|
||||
100: "#ECE9FE",
|
||||
200: "#DDD6FE",
|
||||
300: "#C3B5FD",
|
||||
400: "#A48AFB",
|
||||
500: "#875BF7",
|
||||
600: "#7839EE",
|
||||
700: "#6927DA",
|
||||
800: "#5720B7",
|
||||
900: "#491C96",
|
||||
},
|
||||
fuchsia: {
|
||||
25: "#FEFAFF",
|
||||
50: "#FDF4FF",
|
||||
100: "#FBE8FF",
|
||||
200: "#F6D0FE",
|
||||
300: "#EEAAFD",
|
||||
400: "#E478FA",
|
||||
500: "#D444F1",
|
||||
600: "#BA24D5",
|
||||
700: "#9F1AB1",
|
||||
800: "#821890",
|
||||
900: "#6F1877",
|
||||
},
|
||||
pink: {
|
||||
25: "#FFFAFC",
|
||||
50: "#FEF0F7",
|
||||
100: "#FFD1E2",
|
||||
200: "#FFB1CE",
|
||||
300: "#FD8FBA",
|
||||
400: "#F86BA7",
|
||||
500: "#F23E94",
|
||||
600: "#D5327F",
|
||||
700: "#BA256B",
|
||||
800: "#9E1958",
|
||||
900: "#840B45",
|
||||
},
|
||||
orange: {
|
||||
25: "#FFF9F5",
|
||||
50: "#FFF4ED",
|
||||
100: "#FFE6D5",
|
||||
200: "#FFD6AE",
|
||||
300: "#FF9C66",
|
||||
400: "#FF692E",
|
||||
500: "#FF4405",
|
||||
600: "#E62E05",
|
||||
700: "#BC1B06",
|
||||
800: "#97180C",
|
||||
900: "#771A0D",
|
||||
},
|
||||
},
|
||||
colors,
|
||||
boxShadow: {
|
||||
none: "0 0 #0000",
|
||||
inner: "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue