376 lines
11 KiB
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,
|
|
)
|
|
}
|