Files
maxwarden/ui/autotable.go

376 lines
11 KiB
Go

package ui
import (
. "maxwarden/basic"
"maxwarden/query"
. "maragu.dev/gomponents"
hx "maragu.dev/gomponents-htmx"
. "maragu.dev/gomponents/html"
)
const TABLE_ABOVE_PREFIX = "_tableAbove"
const FORM_BIND_SUFFIX = "_form"
const FORM_PAGINATION_SUFFIX = "_paginationForm"
// FILTER COMPONENTS
// Autotable bind search to col
func BindSearch(elId string, identifier string) Node {
return Group{
FormAttr(elId + FORM_BIND_SUFFIX),
Name(query.SEARCH_URL_KEY_PREFIX + identifier),
}
}
const (
AUTOTABLE_HEADER_COLOR_DEFAULT = iota
)
type AutoTableOptions struct {
Compact bool
Shadow bool
Hover bool // highlight rows when hovering over them
Alternate bool // highlight alternating rows
HeaderBorderY bool
BorderX bool
BorderY bool
Color int // "enum"
}
// THE TABLE
// Note that "aboveTable" node is not swapped with HTMX, but "belowTable" is.
func AutoTable[E any](tableId string, url string, cols []query.ColInfo, f query.Filter, entities []E, aboveTable Node, rowComponent func(E) Node, belowTable Node, opts AutoTableOptions) Node {
paginationButton := func(icon string, page int) Node {
return Button(
InlineStyle(`
$me {
padding-left: $3;
padding-right: $3;
padding-top: $1;
padding-bottom: $1;
min-height: $9;
font-size: var(--text-sm);
font-weight: var(--font-weight-normal);
color: $color(neutral-800);
cursor: pointer;
}
`),
Icon(icon, 16),
hx.Get(url+query.QueryParamsFromPagenum(page, f)),
hx.Swap(CSSID(tableId)),
hx.Target(CSSID(tableId)),
hx.Select(CSSID(tableId)),
hx.Trigger("click"),
)
}
return Group{
If((aboveTable != nil) && (tableId != ""),
Div(ID(tableId+TABLE_ABOVE_PREFIX),
aboveTable,
),
),
Div(
If(tableId != "",
Group{
ID(tableId),
Form(ID(tableId+FORM_BIND_SUFFIX),
AutoComplete("off"),
hx.Get(url),
hx.Trigger("keyup delay:100ms from:(#"+tableId+TABLE_ABOVE_PREFIX+" input), change from:(#"+tableId+TABLE_ABOVE_PREFIX+" input[type=date]), change from:(#"+tableId+TABLE_ABOVE_PREFIX+" input[type=datetime-local]), change from:(#"+tableId+TABLE_ABOVE_PREFIX+" select)"),
hx.Swap("outerHTML"),
hx.Target(CSSID(tableId)),
hx.Select(CSSID(tableId)),
Input(Type("hidden"), Name(query.ORDER_BY_URL_KEY), Value(f.OrderBy)),
Input(Type("hidden"), Name(query.ORDER_DESC_URL_KEY), Value(ToString(f.OrderDescending))),
Input(Type("hidden"), Name(query.ITEMS_PER_PAGE_URL_KEY), Value(ToString(f.Pagination.MaxItemsPerPage))),
),
},
),
Div(
If(opts.Shadow,
InlineStyle(`
$me {
background-color: $color(white);
box-shadow: var(--shadow-md);
}
`),
),
InlineStyle(`
$me {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
color: $color(gray-700);
background-color: $color(white);
}
`),
Div(
InlineStyle(`
$me {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
`),
Table(
InlineStyle("$me { table-layout: fixed; width: 100%; }"),
// ---- TABLE HEADER ----
THead(
Tr(
Map(cols, func(col query.ColInfo) Node {
return Th(
If(col.DisplayPosition == query.COL_POS_RIGHT,
InlineStyle("$me { text-align: right; }"),
),
InlineStyle(`
$me {
border-bottom: 1px solid $color(neutral-200);
}
`),
If(opts.HeaderBorderY,
InlineStyle(`
$me:not(:first-child) {
border-left: 1px solid $color(neutral-200);
}
`),
),
IfElse(opts.Compact,
InlineStyle("$me { padding: $2 $3; }"),
InlineStyle("$me { padding: $4 $4; }"),
),
If(col.Sortable,
Group{
hx.Get(url + query.QueryParamsFromOrderBy(col.DbName, !f.OrderDescending && (col.DbName == f.OrderBy), f)),
hx.Swap(CSSID(tableId)),
hx.Target(CSSID(tableId)),
hx.Select(CSSID(tableId)),
hx.Trigger("click"),
InlineStyle(`
$me {
cursor: pointer;
}
$me:hover {
background-color: $color(neutral-200);
}
`),
},
),
P(
InlineStyle(`
$me {
display: flex;
justify-content: space-between;
align-items: center;
gap: $2;
font-size: var(--text-sm);
color: $color(neutral-800);
font-weight: var(--font-weight-bold);
}
`),
If(col.DisplayPosition == query.COL_POS_LEFT,
InlineStyle("$me { flex-direction: row; }"),
),
If(col.DisplayPosition == query.COL_POS_RIGHT,
InlineStyle("$me { flex-direction: row-reverse; }"),
),
Text(col.DisplayName),
If((f.OrderBy == col.DbName) && (col.DbName != ""),
Group{
InlineStyle("$me { color: $color(black); text-decoration: underline; }"),
IfElse(f.OrderDescending,
Icon(ICON_ARROW_DOWN, 16),
Icon(ICON_ARROW_UP, 16),
),
},
),
),
)
}),
),
),
// ---- TABLE BODY ----
TBody(
InlineStyle(`
$me > tr > td {
overflow-x: auto;
font-size: var(--text-sm);
color: $color(neutral-700);
}
`),
If(opts.Alternate,
InlineStyle("$me > tr:nth-child(even) { background: $color(neutral-100) }"),
),
If(opts.Hover && len(entities) > 0,
InlineStyle("$me > tr:hover {background-color: $color(neutral-100);}"),
),
If(opts.BorderX && f.Pagination.Enabled,
InlineStyle("$me > tr {border-bottom: 1px solid $color(neutral-200); }"),
),
If(opts.BorderX && !f.Pagination.Enabled,
InlineStyle("$me > tr:not(:last-child) {border-bottom: 1px solid $color(neutral-200); }"),
),
If(opts.BorderY,
InlineStyle(`
$me > tr > td:not(:first-child) {
border-left: 1px solid $color(neutral-200);
}
`),
),
IfElse(opts.Compact,
InlineStyle("$me > tr > td { padding: $1 $3; }"),
InlineStyle("$me > tr > td { padding: $4; }"),
),
IfElse(len(entities) > 0,
Map(entities, func(e E) Node {
return rowComponent(e)
}),
Tr(
Td(
Text("Dataset contains no entries."),
),
),
),
),
),
),
// ---- PAGINATION ----
If(f.Pagination.Enabled,
Div(InlineStyle("$me { display: flex; justify-content: space-between; align-items: center; }"),
IfElse(opts.Compact,
InlineStyle("$me { padding: $1 $4; }"),
InlineStyle("$me { padding: $3 $4; }"),
),
Div(
InlineStyle("$me { display: flex; align-items: center; font-size: var(--text-sm); color: $color(neutral-500); }"),
B(Icon(ICON_LIST_ORDERED, 16)),
Span(InlineStyle("$me {margin-right: $3;}")), ToText(f.Pagination.ViewRangeLower), Text("-"), ToText(f.Pagination.ViewRangeUpper), Text(" of "), ToText(f.Pagination.TotalItems),
),
Div(InlineStyle("$me { display: flex; align-items: center; }"),
Div(InlineStyle("$me { margin-right: $3; font-size: var(--text-sm); color: $color(neutral-500);}"),
Span(Text("Items per page:")),
),
Form(
ID(tableId+FORM_PAGINATION_SUFFIX),
hx.Get(url),
hx.Trigger("change from:(#"+tableId+FORM_PAGINATION_SUFFIX+" select)"),
hx.Target(CSSID(tableId)),
hx.Select(CSSID(tableId)),
hx.Swap("outerHTML"),
MapMapWithKey(f.Search, func(s string, v string) Node {
return Input(Type("hidden"), Name(query.SEARCH_URL_KEY_PREFIX+s), Value(v))
}),
Input(Type("hidden"), Name(query.ORDER_BY_URL_KEY), Value(f.OrderBy)),
Input(Type("hidden"), Name(query.ORDER_DESC_URL_KEY), Value(ToString(f.OrderDescending))),
Select(
IfElse(opts.Compact,
InlineStyle("$me { padding: $2; }"),
InlineStyle("$me { padding: $3; }"),
),
InlineStyle(`
$me {
background-color: $color(gray-50);
border: 1px solid $color(gray-50);
font-size: var(--text-sm);
display: block;
margin-right: $3;
box-shadow: var(--shadow-sm);
}
`),
Name(query.ITEMS_PER_PAGE_URL_KEY),
Option(If(f.Pagination.MaxItemsPerPage == 5, Selected()), Text("5"), Value("5")),
Option(If(f.Pagination.MaxItemsPerPage == 10, Selected()), Text("10"), Value("10")),
Option(If(f.Pagination.MaxItemsPerPage == 25, Selected()), Text("25"), Value("25")),
Option(If(f.Pagination.MaxItemsPerPage == 50, Selected()), Text("50"), Value("50")),
Option(If(f.Pagination.MaxItemsPerPage == 100, Selected()), Text("100"), Value("100")),
),
),
paginationButton(ICON_CHEVRON_FIRST, 1),
paginationButton(ICON_CHEVRON_LEFT, f.Pagination.PreviousPage),
Div(
InlineStyle("$me { font-size: var(--text-sm); display: flex; justify-content: center; color: $color(neutral-500); padding: $3; min-height: $9; }"),
Text("Page "),
ToText(f.Pagination.CurrentPage),
Text(" of "),
ToText(f.Pagination.TotalPages),
),
paginationButton(ICON_CHEVRON_RIGHT, f.Pagination.NextPage),
paginationButton(ICON_CHEVRON_LAST, f.Pagination.TotalPages),
),
),
),
),
belowTable,
),
}
}
func AutotableSearchGroup(children ...Node) Node {
return Div(InlineStyle("$me { width: 100%; display: flex; justify-content: space-between; margin-bottom: $3; margin-top: $1; }"),
Div(InlineStyle("$me { width: 100%; position: relative; }"),
Div(InlineStyle("$me { position: relative; display: flex; flex-direction: row; align-items: center; gap: $1;}"),
Group(children),
),
),
)
}
func AutotableSearchDropdown(c ...Node) Node {
return Div(
InlineStyle(`
$me {
width: 100%;
}
`),
FormSelect(Group(c)),
)
}
func AutotableSearch(c ...Node) Node {
return Div(
InlineStyle(`
$me {
width: 100%;
}
`),
FormInput(Group(c)),
)
}
// "premade" autotable for simple tables that don't feature dynamic filtering.
// We can still use the styling and general form of the fancy autotable defined above, but
// for simple datasets because why not. This also gives you the option to "upgrade"
// to the "full" table later on, since you are using the same api
func AutoTableLite[E any](columnNames []string, entities []E, rowComponent func(E) Node, opts AutoTableOptions) Node {
cols := []query.ColInfo{}
for _, v := range columnNames {
cols = append(cols, query.ColInfo{DisplayName: v})
}
return AutoTable(
"",
"",
cols,
query.Filter{},
entities,
nil,
rowComponent,
nil,
opts,
)
}