Clarify category creation flows
This commit is contained in:
+35
-5
@@ -5,6 +5,7 @@ import {
|
||||
GitMerge,
|
||||
Trash2,
|
||||
FolderTree,
|
||||
FolderPlus,
|
||||
Tag as TagIcon,
|
||||
Store,
|
||||
CandlestickChart,
|
||||
@@ -32,6 +33,10 @@ import {
|
||||
import type { Mutate } from "./ui";
|
||||
type Entity = "category" | "tag" | "merchant" | "instrument";
|
||||
type Item = Category | Tag | Merchant | Instrument;
|
||||
const protectedCategoryIDs = new Set([
|
||||
"cat_expenses_unclassified",
|
||||
"cat_income_unclassified",
|
||||
]);
|
||||
const titles = {
|
||||
category: "Categories",
|
||||
tag: "Tags",
|
||||
@@ -82,7 +87,17 @@ export function Registry({
|
||||
? { id: "", isin: "", name: "", currency: "EUR", symbol: "" }
|
||||
: { id: "", name: "" },
|
||||
);
|
||||
const row = (item: Item, depth = 0) => (
|
||||
const createChild = (parent: Category) =>
|
||||
setEditing({
|
||||
id: "",
|
||||
name: "",
|
||||
parent_id: parent.id,
|
||||
kind: parent.kind,
|
||||
hint: "",
|
||||
});
|
||||
const row = (item: Item, depth = 0) => {
|
||||
const category = entity === "category" && "kind" in item ? item : null;
|
||||
return (
|
||||
<div className="registry-row" key={item.id}>
|
||||
<div
|
||||
className="registry-label"
|
||||
@@ -130,6 +145,17 @@ export function Registry({
|
||||
</span>
|
||||
)}
|
||||
<div className="row-actions">
|
||||
{category && !protectedCategoryIDs.has(category.id) && (
|
||||
<button
|
||||
className="button subtle category-child-action"
|
||||
title={`Add a child category under ${category.name}`}
|
||||
aria-label={`Add a child category under ${category.name}`}
|
||||
onClick={() => createChild(category)}
|
||||
>
|
||||
<FolderPlus size={15} />
|
||||
Add child
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="icon-button"
|
||||
title={`Edit ${item.name}`}
|
||||
@@ -159,6 +185,7 @@ export function Registry({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const tree = (
|
||||
parent: string | undefined,
|
||||
depth = 0,
|
||||
@@ -181,7 +208,7 @@ export function Registry({
|
||||
<h2>{titles[entity]}</h2>
|
||||
<p>
|
||||
{entity === "category"
|
||||
? "A clear home for every transaction. Parent categories roll up their children."
|
||||
? "Organize spending and income into a tree. Use Add child on any category to create a nested category."
|
||||
: entity === "tag"
|
||||
? "Flexible labels that work across your accounts and categories."
|
||||
: entity === "instrument"
|
||||
@@ -549,15 +576,18 @@ function RegistryEditor({
|
||||
<option value="income">Income</option>
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Parent category">
|
||||
<Field
|
||||
label="Parent category"
|
||||
hint="Choose an existing category to nest this one. Use Add child on the category list when you want to add a nested category."
|
||||
>
|
||||
<CategoryCombobox
|
||||
data={data}
|
||||
kind={kind}
|
||||
exclude={[...descendants]}
|
||||
emptyLabel="No parent (root)"
|
||||
mutate={mutate}
|
||||
emptyLabel="No parent (top level)"
|
||||
value={parent}
|
||||
onChange={setParent}
|
||||
placeholder="Choose a parent category"
|
||||
/>
|
||||
</Field>
|
||||
<p className="muted">
|
||||
|
||||
@@ -496,13 +496,17 @@ function TransactionEditor({
|
||||
)}
|
||||
</div>
|
||||
{value.kind !== "transfer" && value.kind !== "investment" && (
|
||||
<Field label="Category">
|
||||
<Field
|
||||
label="Category"
|
||||
hint="Choose a leaf category. To add one now, type a new name and choose “Create category”. Use “Parent / Child” to nest it under an existing category."
|
||||
>
|
||||
<CategoryCombobox
|
||||
data={data}
|
||||
kind={value.kind}
|
||||
leavesOnly
|
||||
required
|
||||
mutate={mutate}
|
||||
placeholder="Select or type to add a category"
|
||||
value={value.category_id || ""}
|
||||
onChange={(category_id) =>
|
||||
setValue((v) => ({ ...v, category_id }))
|
||||
|
||||
@@ -903,6 +903,11 @@ tbody tr:hover {
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
.category-child-action {
|
||||
min-height: 32px;
|
||||
padding: 6px 9px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.modal {
|
||||
border: 1px solid #dce5eb;
|
||||
border-radius: 12px;
|
||||
|
||||
+5
-5
@@ -730,9 +730,9 @@ export function CategoryOptions({
|
||||
}
|
||||
// CategoryCombobox is the one category picker: options are full paths, and
|
||||
// with a mutate handle an unmatched name can be created in place. A bare name
|
||||
// lands under the kind's root; "Parent / Name" creates under that parent.
|
||||
// leavesOnly matches the server rule that assigned categories must be leaves;
|
||||
// a freshly created category is always a leaf.
|
||||
// is offered under each matching root; "Parent / Name" creates under that
|
||||
// existing parent. leavesOnly matches the server rule that assigned categories
|
||||
// must be leaves; a freshly created category is always a leaf.
|
||||
export function CategoryCombobox({
|
||||
data,
|
||||
value,
|
||||
@@ -782,7 +782,7 @@ export function CategoryCombobox({
|
||||
const name = segments[segments.length - 1];
|
||||
const row = (parent: Category): ComboCreate => ({
|
||||
key: parent.id,
|
||||
label: `Create "${name}" in ${categoryPath(data, parent.id)}`,
|
||||
label: `Create category "${name}" in ${categoryPath(data, parent.id)}`,
|
||||
run: async () =>
|
||||
onChange(
|
||||
await createCategory(mutate, data, {
|
||||
@@ -813,7 +813,7 @@ export function CategoryCombobox({
|
||||
placeholder={placeholder}
|
||||
emptyText={
|
||||
mutate
|
||||
? "No matching category. Type a name to create it."
|
||||
? "No match. Type a category name and choose Create category."
|
||||
: "No matching category."
|
||||
}
|
||||
create={create}
|
||||
|
||||
Reference in New Issue
Block a user