Implement data export and filtering modules with single-function files

- Added CSV export functionality with escape handling.
- Implemented JSON export functionality.
- Created utility functions for retrieving column labels and row values.
- Established a filtering system with state management and filter application.
- Refactored sorting logic into dedicated modules for better maintainability.
- Deprecated old filtering and sorting files, redirecting to new module structure.
- Introduced form field builders and validation utilities, also refactored into single-function files.
This commit is contained in:
2025-12-30 12:16:09 +00:00
parent b3067d8ba1
commit d65962eb98
78 changed files with 1415 additions and 790 deletions

View File

@@ -1,13 +1,66 @@
import React from 'react'
import { classNames } from '../utils/classNames'
export type SkeletonVariant = 'text' | 'rectangular' | 'circular'
export type SkeletonVariant = 'text' | 'rectangular' | 'circular' | 'rounded'
export type SkeletonAnimation = 'pulse' | 'wave' | false
export interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
export interface SkeletonProps extends React.HTMLAttributes<HTMLSpanElement> {
/** The type of skeleton shape */
variant?: SkeletonVariant
/** Width of the skeleton (accepts CSS units) */
width?: string | number
/** Height of the skeleton (accepts CSS units) */
height?: string | number
/** Animation type or false to disable */
animation?: SkeletonAnimation
/** If true, skeleton takes up full width of parent */
fullWidth?: boolean
/** Custom component to render as skeleton root */
component?: React.ElementType
}
export const Skeleton: React.FC<SkeletonProps> = ({ variant = 'text', width, height, className = '', ...props }) => (
<div className={`skeleton skeleton--${variant} ${className}`} style={{ width, height }} {...props} />
)
/**
* Loading placeholder with shimmer animation
*
* @example
* ```tsx
* // Basic text skeleton
* <Skeleton variant="text" width={210} />
*
* // Avatar placeholder
* <Skeleton variant="circular" width={40} height={40} />
*
* // Card image placeholder
* <Skeleton variant="rectangular" height={118} />
* ```
*/
export const Skeleton: React.FC<SkeletonProps> = ({
variant = 'text',
width,
height,
animation = 'pulse',
fullWidth = false,
component: Component = 'span',
className,
style,
...props
}) => {
const rootClassName = classNames(
'fakemui-skeleton',
`fakemui-skeleton--${variant}`,
animation && `fakemui-skeleton--${animation}`,
fullWidth && 'fakemui-skeleton--full-width',
className
)
const rootStyle: React.CSSProperties = {
...style,
width: fullWidth ? '100%' : width,
height,
}
return <Component className={rootClassName} style={rootStyle} {...props} />
}
export default Skeleton

View File

@@ -0,0 +1,9 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const ArrowLeft = (props: IconProps) => (
<Icon {...props}>
<line x1="216" y1="128" x2="40" y2="128" />
<polyline points="112 56 40 128 112 200" />
</Icon>
)

View File

@@ -0,0 +1,9 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const ArrowRight = (props: IconProps) => (
<Icon {...props}>
<line x1="40" y1="128" x2="216" y2="128" />
<polyline points="144 56 216 128 144 200" />
</Icon>
)

View File

@@ -0,0 +1,9 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const BookOpen = (props: IconProps) => (
<Icon {...props}>
<path d="M128 88a32 32 0 0 0-32-32H48a8 8 0 0 0-8 8v136a8 8 0 0 0 8 8h48a32 32 0 0 1 32 32" />
<path d="M128 88a32 32 0 0 1 32-32h48a8 8 0 0 1 8 8v136a8 8 0 0 1-8 8h-48a32 32 0 0 0-32 32" />
</Icon>
)

10
fakemui/icons/Broom.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Broom = (props: IconProps) => (
<Icon {...props}>
<path d="M40 216h40l24-56" />
<path d="M64 200l64-96 40 40-56 80" />
<path d="M128 104l20-20a56 56 0 0 1 79.2 79.2l-20 20" />
</Icon>
)

View File

@@ -0,0 +1,15 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Buildings = (props: IconProps) => (
<Icon {...props}>
<rect x="32" y="56" width="80" height="168" rx="8" />
<rect x="144" y="96" width="80" height="128" rx="8" />
<line x1="112" y1="104" x2="144" y2="104" />
<line x1="56" y1="88" x2="88" y2="88" />
<line x1="56" y1="120" x2="88" y2="120" />
<line x1="56" y1="152" x2="88" y2="152" />
<line x1="168" y1="128" x2="200" y2="128" />
<line x1="168" y1="160" x2="200" y2="160" />
</Icon>
)

View File

@@ -0,0 +1,8 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const CaretDown = (props: IconProps) => (
<Icon {...props}>
<polyline points="208 96 128 176 48 96" />
</Icon>
)

View File

@@ -0,0 +1,8 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const CaretRight = (props: IconProps) => (
<Icon {...props}>
<polyline points="96 48 176 128 96 208" />
</Icon>
)

View File

@@ -0,0 +1,8 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const ChatCircle = (props: IconProps) => (
<Icon {...props}>
<path d="M128 24a104 104 0 0 0-91.2 153.8L24 224l46.2-12.8A104 104 0 1 0 128 24Z" />
</Icon>
)

View File

@@ -0,0 +1,9 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const CheckCircle = (props: IconProps) => (
<Icon {...props}>
<circle cx="128" cy="128" r="96" />
<polyline points="88 136 112 160 168 104" />
</Icon>
)

View File

@@ -0,0 +1,13 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const ColumnResize = (props: IconProps) => (
<Icon {...props}>
<rect x="48" y="40" width="160" height="176" rx="8" />
<line x1="128" y1="40" x2="128" y2="216" strokeDasharray="8 8" />
<polyline points="104,88 80,128 104,168" />
<polyline points="152,88 176,128 152,168" />
</Icon>
)
export default ColumnResize

8
fakemui/icons/Crown.tsx Normal file
View File

@@ -0,0 +1,8 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Crown = (props: IconProps) => (
<Icon {...props}>
<path d="M232 80l-56 120H80L24 80l56 40 48-72 48 72Z" />
</Icon>
)

12
fakemui/icons/Csv.tsx Normal file
View File

@@ -0,0 +1,12 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Csv = (props: IconProps) => (
<Icon {...props}>
<path d="M200,224H56a8,8,0,0,1-8-8V40a8,8,0,0,1,8-8h96l56,56V216A8,8,0,0,1,200,224Z" />
<polyline points="152,32 152,88 208,88" />
<text x="76" y="168" fontSize="48" fontFamily="monospace" fill="currentColor" stroke="none">CSV</text>
</Icon>
)
export default Csv

View File

@@ -0,0 +1,9 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Envelope = (props: IconProps) => (
<Icon {...props}>
<rect x="24" y="56" width="208" height="144" rx="8" />
<polyline points="24 56 128 144 232 56" />
</Icon>
)

10
fakemui/icons/Export.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Export = (props: IconProps) => (
<Icon {...props}>
<polyline points="88 96 128 56 168 96" />
<line x1="128" y1="152" x2="128" y2="56" />
<path d="M216 152v48a8 8 0 0 1-8 8H48a8 8 0 0 1-8-8v-48" />
</Icon>
)

8
fakemui/icons/Funnel.tsx Normal file
View File

@@ -0,0 +1,8 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Funnel = (props: IconProps) => (
<Icon {...props}>
<path d="M40 48h176l-64 80v72l-48 24v-96Z" />
</Icon>
)

9
fakemui/icons/Gear.tsx Normal file
View File

@@ -0,0 +1,9 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Gear = (props: IconProps) => (
<Icon {...props}>
<circle cx="128" cy="128" r="40" />
<path d="M130.1 32H126l-8 24.7a72 72 0 0 0-33.2 19.2L56 67.3 41.4 93l20.4 15.5a72.2 72.2 0 0 0 0 38.9L41.4 163l14.6 25.7 28.8-8.6a72 72 0 0 0 33.2 19.2l8 24.7h4.1l4-.1 8-24.7a72 72 0 0 0 33.2-19.2l28.8 8.6 14.6-25.7-20.4-15.5a72.2 72.2 0 0 0 0-38.9l20.4-15.5L204 67.3l-28.8 8.6a72 72 0 0 0-33.2-19.2Z" />
</Icon>
)

View File

@@ -0,0 +1,8 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const GithubLogo = (props: IconProps) => (
<Icon {...props}>
<path d="M119.8 215.6c0-9.8-.2-41.4-.2-41.4a24 24 0 0 0-7-17c33.6-3.8 68.6-16.7 68.6-74.5a57.6 57.6 0 0 0-15.6-40.3c1.6-3.8 6.8-19.2-1.4-40-12.8-4.2-42.2 16-42.2 16a142.4 142.4 0 0 0-76.8 0S16.2 38.4 3.4 42.6c-8.2 20.8-3 36.2-1.4 40A57.6 57.6 0 0 0-13.6 123c0 57.6 35 70.7 68.4 74.5a23.6 23.6 0 0 0-7 14.6s-16.4 6-31.4-4.8c-17.2-12.4-23.6-39.8-40.2-39.8" />
</Icon>
)

8
fakemui/icons/House.tsx Normal file
View File

@@ -0,0 +1,8 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const House = (props: IconProps) => (
<Icon {...props}>
<path d="M219.3 108.3l-80-80a16 16 0 0 0-22.6 0l-80 80A15.9 15.9 0 0 0 32 120v96a8 8 0 0 0 8 8h64a8 8 0 0 0 8-8v-56h32v56a8 8 0 0 0 8 8h64a8 8 0 0 0 8-8v-96a15.9 15.9 0 0 0-4.7-11.7Z" />
</Icon>
)

View File

@@ -0,0 +1,9 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const MagnifyingGlass = (props: IconProps) => (
<Icon {...props}>
<circle cx="112" cy="112" r="80" />
<line x1="168" y1="168" x2="224" y2="224" />
</Icon>
)

13
fakemui/icons/Palette.tsx Normal file
View File

@@ -0,0 +1,13 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Palette = (props: IconProps) => (
<Icon {...props}>
<path d="M200.8 53.9A103.4 103.4 0 0 0 128 24a104 104 0 0 0 0 208c52.9 0 88-40 88-88 0-22.1-17.9-40-40-40h-32a24 24 0 0 1 0-48" />
<circle cx="80" cy="120" r="16" fill="currentColor" />
<circle cx="120" cy="80" r="16" fill="currentColor" />
<circle cx="160" cy="80" r="16" fill="currentColor" />
<circle cx="80" cy="168" r="16" fill="currentColor" />
<circle cx="128" cy="192" r="16" fill="currentColor" />
</Icon>
)

9
fakemui/icons/Power.tsx Normal file
View File

@@ -0,0 +1,9 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Power = (props: IconProps) => (
<Icon {...props}>
<line x1="128" y1="48" x2="128" y2="128" />
<path d="M176 56a96 96 0 1 1-96 0" fill="none" />
</Icon>
)

View File

@@ -0,0 +1,14 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const RowSelect = (props: IconProps) => (
<Icon {...props}>
<rect x="40" y="64" width="176" height="48" rx="4" />
<rect x="40" y="128" width="176" height="48" rx="4" fill="currentColor" fillOpacity="0.2" />
<line x1="56" y1="152" x2="72" y2="152" />
<polyline points="60,148 64,156 76,144" />
<line x1="88" y1="152" x2="200" y2="152" />
</Icon>
)
export default RowSelect

View File

@@ -0,0 +1,15 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const SelectAll = (props: IconProps) => (
<Icon {...props}>
<rect x="40" y="40" width="176" height="176" rx="8" />
<polyline points="80,128 112,160 176,96" />
<rect x="56" y="56" width="40" height="40" rx="4" />
<rect x="160" y="56" width="40" height="40" rx="4" />
<rect x="56" y="160" width="40" height="40" rx="4" />
<rect x="160" y="160" width="40" height="40" rx="4" />
</Icon>
)
export default SelectAll

10
fakemui/icons/SignIn.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const SignIn = (props: IconProps) => (
<Icon {...props}>
<polyline points="94 170 136 128 94 86" />
<line x1="24" y1="128" x2="136" y2="128" />
<path d="M136 40h72a8 8 0 0 1 8 8v160a8 8 0 0 1-8 8h-72" />
</Icon>
)

10
fakemui/icons/SignOut.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const SignOut = (props: IconProps) => (
<Icon {...props}>
<polyline points="174 86 216 128 174 170" />
<line x1="104" y1="128" x2="216" y2="128" />
<path d="M104 216H48a8 8 0 0 1-8-8V48a8 8 0 0 1 8-8h56" />
</Icon>
)

View File

@@ -0,0 +1,14 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const SortAscending = (props: IconProps) => (
<Icon {...props}>
<line x1="128" y1="40" x2="128" y2="216" />
<polyline points="56,112 128,40 200,112" />
<line x1="40" y1="176" x2="88" y2="176" />
<line x1="40" y1="208" x2="104" y2="208" />
<line x1="40" y1="144" x2="72" y2="144" />
</Icon>
)
export default SortAscending

View File

@@ -0,0 +1,14 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const SortDescending = (props: IconProps) => (
<Icon {...props}>
<line x1="128" y1="40" x2="128" y2="216" />
<polyline points="56,144 128,216 200,144" />
<line x1="40" y1="48" x2="88" y2="48" />
<line x1="40" y1="80" x2="104" y2="80" />
<line x1="40" y1="112" x2="72" y2="112" />
</Icon>
)
export default SortDescending

View File

@@ -0,0 +1,9 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Sparkle = (props: IconProps) => (
<Icon {...props}>
<path d="M80 36l12.8 38.4a8 8 0 0 0 5.1 5.1L136 92l-38.1 12.5a8 8 0 0 0-5.1 5.1L80 148l-12.8-38.4a8 8 0 0 0-5.1-5.1L24 92l38.1-12.5a8 8 0 0 0 5.1-5.1Z" />
<path d="M168 160l9.6 28.8a8 8 0 0 0 5.1 5.1L212 204l-29.3 10a8 8 0 0 0-5.1 5.1L168 248l-9.6-28.9a8 8 0 0 0-5.1-5.1L124 204l29.3-10a8 8 0 0 0 5.1-5.1Z" />
</Icon>
)

View File

@@ -0,0 +1,11 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const SquaresFour = (props: IconProps) => (
<Icon {...props}>
<rect x="32" y="32" width="72" height="72" rx="8" />
<rect x="152" y="32" width="72" height="72" rx="8" />
<rect x="32" y="152" width="72" height="72" rx="8" />
<rect x="152" y="152" width="72" height="72" rx="8" />
</Icon>
)

11
fakemui/icons/Table.tsx Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Table = (props: IconProps) => (
<Icon {...props}>
<rect x="32" y="48" width="192" height="160" rx="8" />
<line x1="32" y1="96" x2="224" y2="96" />
<line x1="32" y1="144" x2="224" y2="144" />
<line x1="96" y1="96" x2="96" y2="208" />
</Icon>
)

View File

@@ -0,0 +1,15 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const TableCells = (props: IconProps) => (
<Icon {...props}>
<rect x="40" y="40" width="176" height="176" rx="8" />
<line x1="40" y1="88" x2="216" y2="88" />
<line x1="40" y1="136" x2="216" y2="136" />
<line x1="40" y1="184" x2="216" y2="184" />
<line x1="104" y1="88" x2="104" y2="216" />
<line x1="168" y1="88" x2="168" y2="216" />
</Icon>
)
export default TableCells

9
fakemui/icons/Tree.tsx Normal file
View File

@@ -0,0 +1,9 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Tree = (props: IconProps) => (
<Icon {...props}>
<line x1="128" y1="232" x2="128" y2="88" />
<polygon points="128 24 48 120 88 120 48 200 208 200 168 120 208 120 128 24" fill="none" />
</Icon>
)

View File

@@ -0,0 +1,9 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const TrendUp = (props: IconProps) => (
<Icon {...props}>
<polyline points="232 56 136 152 96 112 24 184" />
<polyline points="232 120 232 56 168 56" />
</Icon>
)

View File

@@ -0,0 +1,10 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const UserCircle = (props: IconProps) => (
<Icon {...props}>
<circle cx="128" cy="128" r="96" />
<circle cx="128" cy="120" r="40" />
<path d="M63.8 199.4a72 72 0 0 1 128.4 0" fill="none" />
</Icon>
)

View File

@@ -0,0 +1,11 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const UserPlus = (props: IconProps) => (
<Icon {...props}>
<circle cx="108" cy="100" r="60" />
<path d="M22.2 200c21.6-38.6 62.8-64 85.8-64s64.2 25.4 85.8 64" />
<line x1="200" y1="40" x2="200" y2="96" />
<line x1="172" y1="68" x2="228" y2="68" />
</Icon>
)

11
fakemui/icons/Users.tsx Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Users = (props: IconProps) => (
<Icon {...props}>
<circle cx="84" cy="108" r="52" />
<path d="M10.2 200c20.8-37.2 60.2-60 93.8-60s73 22.8 93.8 60" />
<circle cx="172" cy="108" r="52" />
<path d="M172 152c33.6 0 73 22.8 93.8 60" />
</Icon>
)

10
fakemui/icons/XCircle.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const XCircle = (props: IconProps) => (
<Icon {...props}>
<circle cx="128" cy="128" r="96" />
<line x1="160" y1="96" x2="96" y2="160" />
<line x1="160" y1="160" x2="96" y2="96" />
</Icon>
)

View File

@@ -0,0 +1,95 @@
// Skeleton component styles
// Loading placeholder with shimmer animation
@keyframes skeleton-shimmer {
0% {
background-position: -200px 0;
}
100% {
background-position: calc(200px + 100%) 0;
}
}
.fakemui-skeleton {
display: inline-block;
background-color: var(--color-secondary, rgba(0, 0, 0, 0.11));
background-image: linear-gradient(
90deg,
transparent 0%,
var(--skeleton-shimmer-color, rgba(255, 255, 255, 0.5)) 50%,
transparent 100%
);
background-size: 200px 100%;
background-repeat: no-repeat;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
// Variants
&--text {
height: 1em;
border-radius: var(--radius-xs, 4px);
transform: scale(1, 0.6);
transform-origin: 0 60%;
&:empty::before {
content: '\00a0'; // Non-breaking space for height
}
}
&--rectangular {
border-radius: var(--radius-sm, 4px);
}
&--circular {
border-radius: 50%;
}
&--rounded {
border-radius: var(--radius-md, 8px);
}
// Wave animation variant
&--wave {
animation: none;
overflow: hidden;
position: relative;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
transparent,
var(--skeleton-shimmer-color, rgba(255, 255, 255, 0.5)),
transparent
);
animation: skeleton-shimmer 1.6s linear 0.5s infinite;
}
}
// Pulse animation variant
&--pulse {
animation: skeleton-pulse 1.5s ease-in-out 0.5s infinite;
}
}
@keyframes skeleton-pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.4;
}
100% {
opacity: 1;
}
}
// Dark theme adjustments
[data-theme="dark"] .fakemui-skeleton {
background-color: rgba(255, 255, 255, 0.11);
--skeleton-shimmer-color: rgba(255, 255, 255, 0.2);
}

View File

@@ -1,44 +1,5 @@
-- Admin settings dialog
-- DEPRECATED: This file redirects to settings/ directory
-- Functions are now split into single-function files
---@class SettingsDialog
local M = {}
---@class UIComponent
---@field type string
---@field props? table
---@field children? table
---@return UIComponent
function M.render_general()
return {
type = "dialog",
props = {
title = "General Settings",
size = "large"
},
children = {
{ type = "text_field", props = { label = "Site Name", name = "site_name" } },
{ type = "text_field", props = { label = "Admin Email", name = "admin_email", type = "email" } },
{ type = "switch", props = { label = "Maintenance Mode", name = "maintenance" } },
{ type = "switch", props = { label = "Allow Registration", name = "allow_registration" } }
}
}
end
---@return UIComponent
function M.render_security()
return {
type = "dialog",
props = {
title = "Security Settings",
size = "medium"
},
children = {
{ type = "number_field", props = { label = "Session Timeout (min)", name = "session_timeout", min = 5 } },
{ type = "number_field", props = { label = "Max Login Attempts", name = "max_attempts", min = 1 } },
{ type = "switch", props = { label = "Require 2FA", name = "require_2fa" } }
}
}
end
return M
return require("settings.init")

View File

@@ -1,52 +1,5 @@
-- User management dialog
-- DEPRECATED: This file redirects to user/ directory
-- Functions are now split into single-function files
---@class UserDialog
local M = {}
---@class User
---@field username string
---@field email string
---@field role string
---@field active boolean
---@class UIComponent
---@field type string
---@field props? table
---@field children? table
---@return UIComponent
function M.render_create()
return {
type = "dialog",
props = {
title = "Create User",
size = "medium"
},
children = {
{ type = "text_field", props = { label = "Username", name = "username", required = true } },
{ type = "text_field", props = { label = "Email", name = "email", type = "email", required = true } },
{ type = "password_field", props = { label = "Password", name = "password", required = true } },
{ type = "select", props = { label = "Role", name = "role", options = {"user", "admin"} } }
}
}
end
---@param user User
---@return UIComponent
function M.render_edit(user)
return {
type = "dialog",
props = {
title = "Edit User",
size = "medium"
},
children = {
{ type = "text_field", props = { label = "Username", name = "username", value = user.username } },
{ type = "text_field", props = { label = "Email", name = "email", value = user.email } },
{ type = "select", props = { label = "Role", name = "role", value = user.role, options = {"user", "admin"} } },
{ type = "checkbox", props = { label = "Active", name = "active", checked = user.active } }
}
}
end
return M
return require("user.init")

View File

@@ -0,0 +1,16 @@
-- User dialog module facade
-- Re-exports all user dialog functions for backward compatibility
-- Each function is defined in its own file following 1-function-per-file pattern
---@class UserDialog
local M = {}
-- Import all single-function modules
local renderCreate = require("user.render_create")
local renderEdit = require("user.render_edit")
-- Re-export all functions
M.render_create = renderCreate.render_create
M.render_edit = renderEdit.render_edit
return M

View File

@@ -0,0 +1,25 @@
-- User create dialog
-- Single function module for admin user dialogs
---@class RenderCreate
local M = {}
---Render create user dialog
---@return UIComponent
function M.render_create()
return {
type = "dialog",
props = {
title = "Create User",
size = "medium"
},
children = {
{ type = "text_field", props = { label = "Username", name = "username", required = true } },
{ type = "text_field", props = { label = "Email", name = "email", type = "email", required = true } },
{ type = "password_field", props = { label = "Password", name = "password", required = true } },
{ type = "select", props = { label = "Role", name = "role", options = {"user", "admin"} } }
}
}
end
return M

View File

@@ -0,0 +1,26 @@
-- User edit dialog
-- Single function module for admin user dialogs
---@class RenderEdit
local M = {}
---Render edit user dialog
---@param user User User data to edit
---@return UIComponent
function M.render_edit(user)
return {
type = "dialog",
props = {
title = "Edit User",
size = "medium"
},
children = {
{ type = "text_field", props = { label = "Username", name = "username", value = user.username } },
{ type = "text_field", props = { label = "Email", name = "email", value = user.email } },
{ type = "select", props = { label = "Role", name = "role", value = user.role, options = {"user", "admin"} } },
{ type = "checkbox", props = { label = "Active", name = "active", checked = user.active } }
}
}
end
return M

View File

@@ -0,0 +1,15 @@
-- Type definitions for user dialog module
-- Shared across all user dialog functions
---@class User
---@field username string
---@field email string
---@field role string
---@field active boolean
---@class UIComponent
---@field type string
---@field props? table
---@field children? table
return {}

View File

@@ -1,235 +1,5 @@
-- Export utilities for data tables
-- Provides CSV and JSON export functionality
-- DEPRECATED: This file redirects to export/ directory
-- Functions are now split into single-function files
---@class Export
local M = {}
---@class ExportOptions
---@field includeHeaders boolean Include column headers in export
---@field columns string[]|nil Specific columns to export (nil = all)
---@field delimiter string CSV delimiter (default: ",")
---@field lineEnding string Line ending (default: "\n")
---Escape a value for CSV
---@param value any Value to escape
---@return string Escaped CSV string
function M.escapeCsv(value)
if value == nil then
return ""
end
local str = tostring(value)
-- Check if escaping is needed
if string.find(str, '[,"\r\n]') then
-- Escape double quotes by doubling them
str = string.gsub(str, '"', '""')
-- Wrap in double quotes
str = '"' .. str .. '"'
end
return str
end
---Get column values from row data
---@param row table Row data object
---@param columns table[] Column definitions
---@param columnIds string[]|nil Specific column IDs (nil = all)
---@return string[] Array of values
function M.getRowValues(row, columns, columnIds)
local values = {}
if columnIds then
-- Specific columns only
for _, id in ipairs(columnIds) do
table.insert(values, row[id])
end
else
-- All columns
for _, col in ipairs(columns) do
if col.type ~= "actions" then
table.insert(values, row[col.id])
end
end
end
return values
end
---Get column labels for headers
---@param columns table[] Column definitions
---@param columnIds string[]|nil Specific column IDs (nil = all)
---@return string[] Array of labels
function M.getColumnLabels(columns, columnIds)
local labels = {}
if columnIds then
-- Specific columns only
for _, id in ipairs(columnIds) do
for _, col in ipairs(columns) do
if col.id == id then
table.insert(labels, col.label or col.id)
break
end
end
end
else
-- All columns (except actions)
for _, col in ipairs(columns) do
if col.type ~= "actions" then
table.insert(labels, col.label or col.id)
end
end
end
return labels
end
---Export data to CSV format
---@param data table[] Array of row data objects
---@param columns table[] Column definitions
---@param options? ExportOptions Export options
---@return string CSV formatted string
function M.exportToCsv(data, columns, options)
options = options or {}
local includeHeaders = options.includeHeaders ~= false
local columnIds = options.columns
local delimiter = options.delimiter or ","
local lineEnding = options.lineEnding or "\n"
local lines = {}
-- Add header row
if includeHeaders then
local headers = M.getColumnLabels(columns, columnIds)
local escapedHeaders = {}
for _, h in ipairs(headers) do
table.insert(escapedHeaders, M.escapeCsv(h))
end
table.insert(lines, table.concat(escapedHeaders, delimiter))
end
-- Add data rows
for _, row in ipairs(data) do
local values = M.getRowValues(row, columns, columnIds)
local escapedValues = {}
for _, v in ipairs(values) do
table.insert(escapedValues, M.escapeCsv(v))
end
table.insert(lines, table.concat(escapedValues, delimiter))
end
return table.concat(lines, lineEnding)
end
---Serialize a value to JSON
---@param value any Value to serialize
---@return string JSON string
function M.jsonEncode(value)
local t = type(value)
if value == nil then
return "null"
elseif t == "boolean" then
return value and "true" or "false"
elseif t == "number" then
return tostring(value)
elseif t == "string" then
-- Escape special characters
local escaped = value
escaped = string.gsub(escaped, '\\', '\\\\')
escaped = string.gsub(escaped, '"', '\\"')
escaped = string.gsub(escaped, '\n', '\\n')
escaped = string.gsub(escaped, '\r', '\\r')
escaped = string.gsub(escaped, '\t', '\\t')
return '"' .. escaped .. '"'
elseif t == "table" then
-- Check if array or object
local isArray = true
local maxIndex = 0
for k, _ in pairs(value) do
if type(k) ~= "number" or k < 1 or k ~= math.floor(k) then
isArray = false
break
end
if k > maxIndex then maxIndex = k end
end
isArray = isArray and maxIndex == #value
if isArray then
local items = {}
for _, v in ipairs(value) do
table.insert(items, M.jsonEncode(v))
end
return "[" .. table.concat(items, ",") .. "]"
else
local items = {}
for k, v in pairs(value) do
table.insert(items, M.jsonEncode(tostring(k)) .. ":" .. M.jsonEncode(v))
end
return "{" .. table.concat(items, ",") .. "}"
end
end
return "null"
end
---Export data to JSON format
---@param data table[] Array of row data objects
---@param columns? table[] Column definitions (optional, for column filtering)
---@param columnIds? string[] Specific columns to export (nil = all)
---@return string JSON formatted string
function M.exportToJson(data, columns, columnIds)
local result
if columns and columnIds then
-- Export only specified columns
result = {}
for _, row in ipairs(data) do
local filtered = {}
for _, id in ipairs(columnIds) do
filtered[id] = row[id]
end
table.insert(result, filtered)
end
else
result = data
end
return M.jsonEncode(result)
end
---Create a download-ready export object
---@param content string Export content
---@param filename string Suggested filename
---@param mimeType string MIME type
---@return table Export object with content, filename, mimeType
function M.createExport(content, filename, mimeType)
return {
content = content,
filename = filename,
mimeType = mimeType
}
end
---Export to CSV with download metadata
---@param data table[] Array of row data objects
---@param columns table[] Column definitions
---@param filename? string Suggested filename (default: "export.csv")
---@param options? ExportOptions Export options
---@return table Export object
function M.downloadCsv(data, columns, filename, options)
local csv = M.exportToCsv(data, columns, options)
return M.createExport(csv, filename or "export.csv", "text/csv")
end
---Export to JSON with download metadata
---@param data table[] Array of row data objects
---@param filename? string Suggested filename (default: "export.json")
---@return table Export object
function M.downloadJson(data, filename)
local json = M.exportToJson(data)
return M.createExport(json, filename or "export.json", "application/json")
end
return M
return require("export.init")

View File

@@ -0,0 +1,20 @@
-- Create a download-ready export object
-- Single function module for data table export
---@class CreateExport
local M = {}
---Create a download-ready export object
---@param content string Export content
---@param filename string Suggested filename
---@param mimeType string MIME type
---@return ExportObject Export object with content, filename, mimeType
function M.createExport(content, filename, mimeType)
return {
content = content,
filename = filename,
mimeType = mimeType
}
end
return M

View File

@@ -0,0 +1,21 @@
-- Export to CSV with download metadata
-- Single function module for data table export
local exportToCsv = require("export.export_to_csv")
local createExport = require("export.create_export")
---@class DownloadCsv
local M = {}
---Export to CSV with download metadata
---@param data table[] Array of row data objects
---@param columns table[] Column definitions
---@param filename? string Suggested filename (default: "export.csv")
---@param options? ExportOptions Export options
---@return ExportObject Export object
function M.downloadCsv(data, columns, filename, options)
local csv = exportToCsv.exportToCsv(data, columns, options)
return createExport.createExport(csv, filename or "export.csv", "text/csv")
end
return M

View File

@@ -0,0 +1,19 @@
-- Export to JSON with download metadata
-- Single function module for data table export
local exportToJson = require("export.export_to_json")
local createExport = require("export.create_export")
---@class DownloadJson
local M = {}
---Export to JSON with download metadata
---@param data table[] Array of row data objects
---@param filename? string Suggested filename (default: "export.json")
---@return ExportObject Export object
function M.downloadJson(data, filename)
local json = exportToJson.exportToJson(data)
return createExport.createExport(json, filename or "export.json", "application/json")
end
return M

View File

@@ -0,0 +1,28 @@
-- Escape a value for CSV
-- Single function module for data table export
---@class EscapeCsv
local M = {}
---Escape a value for CSV
---@param value any Value to escape
---@return string Escaped CSV string
function M.escapeCsv(value)
if value == nil then
return ""
end
local str = tostring(value)
-- Check if escaping is needed
if string.find(str, '[,"\r\n]') then
-- Escape double quotes by doubling them
str = string.gsub(str, '"', '""')
-- Wrap in double quotes
str = '"' .. str .. '"'
end
return str
end
return M

View File

@@ -0,0 +1,48 @@
-- Export data to CSV format
-- Single function module for data table export
local escapeCsv = require("export.escape_csv")
local getColumnLabels = require("export.get_column_labels")
local getRowValues = require("export.get_row_values")
---@class ExportToCsv
local M = {}
---Export data to CSV format
---@param data table[] Array of row data objects
---@param columns table[] Column definitions
---@param options? ExportOptions Export options
---@return string CSV formatted string
function M.exportToCsv(data, columns, options)
options = options or {}
local includeHeaders = options.includeHeaders ~= false
local columnIds = options.columns
local delimiter = options.delimiter or ","
local lineEnding = options.lineEnding or "\n"
local lines = {}
-- Add header row
if includeHeaders then
local headers = getColumnLabels.getColumnLabels(columns, columnIds)
local escapedHeaders = {}
for _, h in ipairs(headers) do
table.insert(escapedHeaders, escapeCsv.escapeCsv(h))
end
table.insert(lines, table.concat(escapedHeaders, delimiter))
end
-- Add data rows
for _, row in ipairs(data) do
local values = getRowValues.getRowValues(row, columns, columnIds)
local escapedValues = {}
for _, v in ipairs(values) do
table.insert(escapedValues, escapeCsv.escapeCsv(v))
end
table.insert(lines, table.concat(escapedValues, delimiter))
end
return table.concat(lines, lineEnding)
end
return M

View File

@@ -0,0 +1,34 @@
-- Export data to JSON format
-- Single function module for data table export
local jsonEncode = require("export.json_encode")
---@class ExportToJson
local M = {}
---Export data to JSON format
---@param data table[] Array of row data objects
---@param columns? table[] Column definitions (optional, for column filtering)
---@param columnIds? string[] Specific columns to export (nil = all)
---@return string JSON formatted string
function M.exportToJson(data, columns, columnIds)
local result
if columns and columnIds then
-- Export only specified columns
result = {}
for _, row in ipairs(data) do
local filtered = {}
for _, id in ipairs(columnIds) do
filtered[id] = row[id]
end
table.insert(result, filtered)
end
else
result = data
end
return jsonEncode.jsonEncode(result)
end
return M

View File

@@ -0,0 +1,36 @@
-- Get column labels for headers
-- Single function module for data table export
---@class GetColumnLabels
local M = {}
---Get column labels for headers
---@param columns table[] Column definitions
---@param columnIds string[]|nil Specific column IDs (nil = all)
---@return string[] Array of labels
function M.getColumnLabels(columns, columnIds)
local labels = {}
if columnIds then
-- Specific columns only
for _, id in ipairs(columnIds) do
for _, col in ipairs(columns) do
if col.id == id then
table.insert(labels, col.label or col.id)
break
end
end
end
else
-- All columns (except actions)
for _, col in ipairs(columns) do
if col.type ~= "actions" then
table.insert(labels, col.label or col.id)
end
end
end
return labels
end
return M

View File

@@ -0,0 +1,32 @@
-- Get column values from row data
-- Single function module for data table export
---@class GetRowValues
local M = {}
---Get column values from row data
---@param row table Row data object
---@param columns table[] Column definitions
---@param columnIds string[]|nil Specific column IDs (nil = all)
---@return string[] Array of values
function M.getRowValues(row, columns, columnIds)
local values = {}
if columnIds then
-- Specific columns only
for _, id in ipairs(columnIds) do
table.insert(values, row[id])
end
else
-- All columns
for _, col in ipairs(columns) do
if col.type ~= "actions" then
table.insert(values, row[col.id])
end
end
end
return values
end
return M

View File

@@ -0,0 +1,30 @@
-- Export module facade
-- Re-exports all export functions for backward compatibility
-- Each function is defined in its own file following 1-function-per-file pattern
---@class Export
local M = {}
-- Import all single-function modules
local escapeCsv = require("export.escape_csv")
local getRowValues = require("export.get_row_values")
local getColumnLabels = require("export.get_column_labels")
local exportToCsv = require("export.export_to_csv")
local jsonEncode = require("export.json_encode")
local exportToJson = require("export.export_to_json")
local createExport = require("export.create_export")
local downloadCsv = require("export.download_csv")
local downloadJson = require("export.download_json")
-- Re-export all functions
M.escapeCsv = escapeCsv.escapeCsv
M.getRowValues = getRowValues.getRowValues
M.getColumnLabels = getColumnLabels.getColumnLabels
M.exportToCsv = exportToCsv.exportToCsv
M.jsonEncode = jsonEncode.jsonEncode
M.exportToJson = exportToJson.exportToJson
M.createExport = createExport.createExport
M.downloadCsv = downloadCsv.downloadCsv
M.downloadJson = downloadJson.downloadJson
return M

View File

@@ -0,0 +1,59 @@
-- Serialize a value to JSON
-- Single function module for data table export
---@class JsonEncode
local M = {}
---Serialize a value to JSON
---@param value any Value to serialize
---@return string JSON string
function M.jsonEncode(value)
local t = type(value)
if value == nil then
return "null"
elseif t == "boolean" then
return value and "true" or "false"
elseif t == "number" then
return tostring(value)
elseif t == "string" then
-- Escape special characters
local escaped = value
escaped = string.gsub(escaped, '\\', '\\\\')
escaped = string.gsub(escaped, '"', '\\"')
escaped = string.gsub(escaped, '\n', '\\n')
escaped = string.gsub(escaped, '\r', '\\r')
escaped = string.gsub(escaped, '\t', '\\t')
return '"' .. escaped .. '"'
elseif t == "table" then
-- Check if array or object
local isArray = true
local maxIndex = 0
for k, _ in pairs(value) do
if type(k) ~= "number" or k < 1 or k ~= math.floor(k) then
isArray = false
break
end
if k > maxIndex then maxIndex = k end
end
isArray = isArray and maxIndex == #value
if isArray then
local items = {}
for _, v in ipairs(value) do
table.insert(items, M.jsonEncode(v))
end
return "[" .. table.concat(items, ",") .. "]"
else
local items = {}
for k, v in pairs(value) do
table.insert(items, M.jsonEncode(tostring(k)) .. ":" .. M.jsonEncode(v))
end
return "{" .. table.concat(items, ",") .. "}"
end
end
return "null"
end
return M

View File

@@ -0,0 +1,15 @@
-- Type definitions for export module
-- Shared across all export functions
---@class ExportOptions
---@field includeHeaders boolean Include column headers in export
---@field columns string[]|nil Specific columns to export (nil = all)
---@field delimiter string CSV delimiter (default: ",")
---@field lineEnding string Line ending (default: "\n")
---@class ExportObject
---@field content string Export content
---@field filename string Suggested filename
---@field mimeType string MIME type
return {}

View File

@@ -1,159 +1,5 @@
-- Filtering utilities for data tables
-- Provides filter application and state management
-- DEPRECATED: This file redirects to filtering/ directory
-- Functions are now split into single-function files
---@class Filtering
local M = {}
---@alias FilterOperator "equals" | "contains" | "startsWith" | "endsWith" | "gt" | "lt" | "gte" | "lte" | "between"
---@class Filter
---@field column_id string Column identifier to filter
---@field operator FilterOperator Filter operator
---@field value any Filter value (single value or {min, max} for between)
---@class FilterState
---@field filters Filter[] Active filters
---Create initial filter state
---@return FilterState
function M.createFilterState()
return {
filters = {}
}
end
---Check if a value matches a filter
---@param value any Value to check
---@param filter Filter Filter to apply
---@return boolean Whether value matches filter
function M.matchesFilter(value, filter)
local op = filter.operator
local filterVal = filter.value
-- Handle nil values
if value == nil then
return op == "equals" and filterVal == nil
end
if op == "equals" then
return value == filterVal
elseif op == "contains" then
return string.find(string.lower(tostring(value)), string.lower(tostring(filterVal)), 1, true) ~= nil
elseif op == "startsWith" then
local str = string.lower(tostring(value))
local prefix = string.lower(tostring(filterVal))
return string.sub(str, 1, #prefix) == prefix
elseif op == "endsWith" then
local str = string.lower(tostring(value))
local suffix = string.lower(tostring(filterVal))
return string.sub(str, -#suffix) == suffix
elseif op == "gt" then
return tonumber(value) and tonumber(value) > tonumber(filterVal)
elseif op == "lt" then
return tonumber(value) and tonumber(value) < tonumber(filterVal)
elseif op == "gte" then
return tonumber(value) and tonumber(value) >= tonumber(filterVal)
elseif op == "lte" then
return tonumber(value) and tonumber(value) <= tonumber(filterVal)
elseif op == "between" then
local num = tonumber(value)
return num and num >= filterVal[1] and num <= filterVal[2]
end
return false
end
---Check if a row matches all filters
---@param row table Row data object
---@param filters Filter[] Array of filters to apply
---@return boolean Whether row matches all filters
function M.matchesAllFilters(row, filters)
for _, filter in ipairs(filters) do
if not M.matchesFilter(row[filter.column_id], filter) then
return false
end
end
return true
end
---Apply filters to data
---@param data table[] Array of row data objects
---@param state FilterState Filter state with active filters
---@return table[] Filtered data array (new array, original unchanged)
function M.applyFilters(data, state)
if not state.filters or #state.filters == 0 then
return data
end
local filtered = {}
for _, row in ipairs(data) do
if M.matchesAllFilters(row, state.filters) then
table.insert(filtered, row)
end
end
return filtered
end
---Add a filter to state
---@param state FilterState Current filter state
---@param column_id string Column identifier
---@param operator FilterOperator Filter operator
---@param value any Filter value
---@return FilterState New filter state
function M.addFilter(state, column_id, operator, value)
local newFilters = {}
-- Copy existing filters (replacing any for same column)
for _, filter in ipairs(state.filters) do
if filter.column_id ~= column_id then
table.insert(newFilters, filter)
end
end
-- Add new filter
table.insert(newFilters, {
column_id = column_id,
operator = operator,
value = value
})
return { filters = newFilters }
end
---Remove a filter from state
---@param state FilterState Current filter state
---@param column_id string Column identifier to remove
---@return FilterState New filter state
function M.removeFilter(state, column_id)
local newFilters = {}
for _, filter in ipairs(state.filters) do
if filter.column_id ~= column_id then
table.insert(newFilters, filter)
end
end
return { filters = newFilters }
end
---Clear all filters
---@return FilterState Empty filter state
function M.clearFilters()
return { filters = {} }
end
---Get active filter for a column
---@param state FilterState Current filter state
---@param column_id string Column identifier
---@return Filter|nil Active filter for column, or nil
function M.getFilterForColumn(state, column_id)
for _, filter in ipairs(state.filters) do
if filter.column_id == column_id then
return filter
end
end
return nil
end
return M
return require("filtering.init")

View File

@@ -0,0 +1,33 @@
-- Add a filter to state
-- Single function module for data table filtering
---@class AddFilter
local M = {}
---Add a filter to state
---@param state FilterState Current filter state
---@param column_id string Column identifier
---@param operator FilterOperator Filter operator
---@param value any Filter value
---@return FilterState New filter state
function M.addFilter(state, column_id, operator, value)
local newFilters = {}
-- Copy existing filters (replacing any for same column)
for _, filter in ipairs(state.filters) do
if filter.column_id ~= column_id then
table.insert(newFilters, filter)
end
end
-- Add new filter
table.insert(newFilters, {
column_id = column_id,
operator = operator,
value = value
})
return { filters = newFilters }
end
return M

View File

@@ -0,0 +1,28 @@
-- Apply filters to data
-- Single function module for data table filtering
local matchesAllFilters = require("filtering.matches_all_filters")
---@class ApplyFilters
local M = {}
---Apply filters to data
---@param data table[] Array of row data objects
---@param state FilterState Filter state with active filters
---@return table[] Filtered data array (new array, original unchanged)
function M.applyFilters(data, state)
if not state.filters or #state.filters == 0 then
return data
end
local filtered = {}
for _, row in ipairs(data) do
if matchesAllFilters.matchesAllFilters(row, state.filters) then
table.insert(filtered, row)
end
end
return filtered
end
return M

View File

@@ -0,0 +1,13 @@
-- Clear all filters
-- Single function module for data table filtering
---@class ClearFilters
local M = {}
---Clear all filters
---@return FilterState Empty filter state
function M.clearFilters()
return { filters = {} }
end
return M

View File

@@ -0,0 +1,15 @@
-- Create initial filter state
-- Single function module for data table filtering
---@class CreateFilterState
local M = {}
---Create initial filter state
---@return FilterState
function M.createFilterState()
return {
filters = {}
}
end
return M

View File

@@ -0,0 +1,20 @@
-- Get active filter for a column
-- Single function module for data table filtering
---@class GetFilterForColumn
local M = {}
---Get active filter for a column
---@param state FilterState Current filter state
---@param column_id string Column identifier
---@return Filter|nil Active filter for column, or nil
function M.getFilterForColumn(state, column_id)
for _, filter in ipairs(state.filters) do
if filter.column_id == column_id then
return filter
end
end
return nil
end
return M

View File

@@ -0,0 +1,28 @@
-- Filtering module facade
-- Re-exports all filtering functions for backward compatibility
-- Each function is defined in its own file following 1-function-per-file pattern
---@class Filtering
local M = {}
-- Import all single-function modules
local createFilterState = require("filtering.create_filter_state")
local matchesFilter = require("filtering.matches_filter")
local matchesAllFilters = require("filtering.matches_all_filters")
local applyFilters = require("filtering.apply_filters")
local addFilter = require("filtering.add_filter")
local removeFilter = require("filtering.remove_filter")
local clearFilters = require("filtering.clear_filters")
local getFilterForColumn = require("filtering.get_filter_for_column")
-- Re-export all functions
M.createFilterState = createFilterState.createFilterState
M.matchesFilter = matchesFilter.matchesFilter
M.matchesAllFilters = matchesAllFilters.matchesAllFilters
M.applyFilters = applyFilters.applyFilters
M.addFilter = addFilter.addFilter
M.removeFilter = removeFilter.removeFilter
M.clearFilters = clearFilters.clearFilters
M.getFilterForColumn = getFilterForColumn.getFilterForColumn
return M

View File

@@ -0,0 +1,22 @@
-- Check if a row matches all filters
-- Single function module for data table filtering
local matchesFilter = require("filtering.matches_filter")
---@class MatchesAllFilters
local M = {}
---Check if a row matches all filters
---@param row table Row data object
---@param filters Filter[] Array of filters to apply
---@return boolean Whether row matches all filters
function M.matchesAllFilters(row, filters)
for _, filter in ipairs(filters) do
if not matchesFilter.matchesFilter(row[filter.column_id], filter) then
return false
end
end
return true
end
return M

View File

@@ -0,0 +1,48 @@
-- Check if a value matches a filter
-- Single function module for data table filtering
---@class MatchesFilter
local M = {}
---Check if a value matches a filter
---@param value any Value to check
---@param filter Filter Filter to apply
---@return boolean Whether value matches filter
function M.matchesFilter(value, filter)
local op = filter.operator
local filterVal = filter.value
-- Handle nil values
if value == nil then
return op == "equals" and filterVal == nil
end
if op == "equals" then
return value == filterVal
elseif op == "contains" then
return string.find(string.lower(tostring(value)), string.lower(tostring(filterVal)), 1, true) ~= nil
elseif op == "startsWith" then
local str = string.lower(tostring(value))
local prefix = string.lower(tostring(filterVal))
return string.sub(str, 1, #prefix) == prefix
elseif op == "endsWith" then
local str = string.lower(tostring(value))
local suffix = string.lower(tostring(filterVal))
return string.sub(str, -#suffix) == suffix
elseif op == "gt" then
return tonumber(value) and tonumber(value) > tonumber(filterVal)
elseif op == "lt" then
return tonumber(value) and tonumber(value) < tonumber(filterVal)
elseif op == "gte" then
return tonumber(value) and tonumber(value) >= tonumber(filterVal)
elseif op == "lte" then
return tonumber(value) and tonumber(value) <= tonumber(filterVal)
elseif op == "between" then
local num = tonumber(value)
return num and num >= filterVal[1] and num <= filterVal[2]
end
return false
end
return M

View File

@@ -0,0 +1,23 @@
-- Remove a filter from state
-- Single function module for data table filtering
---@class RemoveFilter
local M = {}
---Remove a filter from state
---@param state FilterState Current filter state
---@param column_id string Column identifier to remove
---@return FilterState New filter state
function M.removeFilter(state, column_id)
local newFilters = {}
for _, filter in ipairs(state.filters) do
if filter.column_id ~= column_id then
table.insert(newFilters, filter)
end
end
return { filters = newFilters }
end
return M

View File

@@ -0,0 +1,14 @@
-- Type definitions for filtering module
-- Shared across all filtering functions
---@alias FilterOperator "equals" | "contains" | "startsWith" | "endsWith" | "gt" | "lt" | "gte" | "lte" | "between"
---@class Filter
---@field column_id string Column identifier to filter
---@field operator FilterOperator Filter operator
---@field value any Filter value (single value or {min, max} for between)
---@class FilterState
---@field filters Filter[] Active filters
return {}

View File

@@ -1,131 +1,5 @@
-- Sorting utilities for data tables
-- Provides column sorting logic and state management
-- DEPRECATED: This file redirects to sorting/ directory
-- Functions are now split into single-function files
---@class Sorting
local M = {}
---@alias SortDirection "asc" | "desc" | nil
---@class SortState
---@field column_id string|nil Column being sorted
---@field direction SortDirection Sort direction
---@class SortedResult
---@field data table[] Sorted data array
---@field state SortState Current sort state
---Create initial sort state
---@return SortState
function M.createSortState()
return {
column_id = nil,
direction = nil
}
end
---Compare two values for sorting
---@param a any First value
---@param b any Second value
---@param direction SortDirection Sort direction
---@return boolean Whether a should come before b
function M.compare(a, b, direction)
local aVal = a
local bVal = b
-- Handle nil values (push to end)
if aVal == nil and bVal == nil then return false end
if aVal == nil then return direction == "desc" end
if bVal == nil then return direction == "asc" end
-- Handle different types
local aType = type(aVal)
local bType = type(bVal)
if aType ~= bType then
aVal = tostring(aVal)
bVal = tostring(bVal)
end
-- Compare based on type
if aType == "number" then
if direction == "desc" then
return aVal > bVal
else
return aVal < bVal
end
else
-- String comparison (case-insensitive)
local aLower = string.lower(tostring(aVal))
local bLower = string.lower(tostring(bVal))
if direction == "desc" then
return aLower > bLower
else
return aLower < bLower
end
end
end
---Sort data by a column
---@param data table[] Array of row data objects
---@param column_id string Column identifier to sort by
---@param direction SortDirection Sort direction ("asc" or "desc")
---@return table[] Sorted data array (new array, original unchanged)
function M.sortByColumn(data, column_id, direction)
if not column_id or not direction then
return data
end
-- Create a copy to avoid mutating original
local sorted = {}
for i, row in ipairs(data) do
sorted[i] = row
end
table.sort(sorted, function(a, b)
return M.compare(a[column_id], b[column_id], direction)
end)
return sorted
end
---Toggle sort state for a column
---@param state SortState Current sort state
---@param column_id string Column identifier clicked
---@return SortState New sort state
function M.toggleSort(state, column_id)
if state.column_id ~= column_id then
-- New column: start with ascending
return {
column_id = column_id,
direction = "asc"
}
elseif state.direction == "asc" then
-- Same column, was ascending: switch to descending
return {
column_id = column_id,
direction = "desc"
}
else
-- Same column, was descending: clear sort
return {
column_id = nil,
direction = nil
}
end
end
---Get sort indicator for a column
---@param state SortState Current sort state
---@param column_id string Column identifier to check
---@return string|nil Sort indicator ("▲", "▼", or nil)
function M.getSortIndicator(state, column_id)
if state.column_id ~= column_id then
return nil
elseif state.direction == "asc" then
return ""
else
return ""
end
end
return M
return require("sorting.init")

View File

@@ -0,0 +1,49 @@
-- Compare two values for sorting
-- Single function module for data table sorting
---@class Compare
local M = {}
---Compare two values for sorting
---@param a any First value
---@param b any Second value
---@param direction SortDirection Sort direction
---@return boolean Whether a should come before b
function M.compare(a, b, direction)
local aVal = a
local bVal = b
-- Handle nil values (push to end)
if aVal == nil and bVal == nil then return false end
if aVal == nil then return direction == "desc" end
if bVal == nil then return direction == "asc" end
-- Handle different types
local aType = type(aVal)
local bType = type(bVal)
if aType ~= bType then
aVal = tostring(aVal)
bVal = tostring(bVal)
end
-- Compare based on type
if aType == "number" then
if direction == "desc" then
return aVal > bVal
else
return aVal < bVal
end
else
-- String comparison (case-insensitive)
local aLower = string.lower(tostring(aVal))
local bLower = string.lower(tostring(bVal))
if direction == "desc" then
return aLower > bLower
else
return aLower < bLower
end
end
end
return M

View File

@@ -0,0 +1,16 @@
-- Create initial sort state
-- Single function module for data table sorting
---@class CreateSortState
local M = {}
---Create initial sort state
---@return SortState
function M.createSortState()
return {
column_id = nil,
direction = nil
}
end
return M

View File

@@ -0,0 +1,21 @@
-- Get sort indicator for a column
-- Single function module for data table sorting
---@class GetSortIndicator
local M = {}
---Get sort indicator for a column
---@param state SortState Current sort state
---@param column_id string Column identifier to check
---@return string|nil Sort indicator ("▲", "▼", or nil)
function M.getSortIndicator(state, column_id)
if state.column_id ~= column_id then
return nil
elseif state.direction == "asc" then
return ""
else
return ""
end
end
return M

View File

@@ -0,0 +1,22 @@
-- Sorting module facade
-- Re-exports all sorting functions for backward compatibility
-- Each function is defined in its own file following 1-function-per-file pattern
---@class Sorting
local M = {}
-- Import all single-function modules
local createSortState = require("sorting.create_sort_state")
local compare = require("sorting.compare")
local sortByColumn = require("sorting.sort_by_column")
local toggleSort = require("sorting.toggle_sort")
local getSortIndicator = require("sorting.get_sort_indicator")
-- Re-export all functions
M.createSortState = createSortState.createSortState
M.compare = compare.compare
M.sortByColumn = sortByColumn.sortByColumn
M.toggleSort = toggleSort.toggleSort
M.getSortIndicator = getSortIndicator.getSortIndicator
return M

View File

@@ -0,0 +1,32 @@
-- Sort data by a column
-- Single function module for data table sorting
local compare = require("sorting.compare")
---@class SortByColumn
local M = {}
---Sort data by a column
---@param data table[] Array of row data objects
---@param column_id string Column identifier to sort by
---@param direction SortDirection Sort direction ("asc" or "desc")
---@return table[] Sorted data array (new array, original unchanged)
function M.sortByColumn(data, column_id, direction)
if not column_id or not direction then
return data
end
-- Create a copy to avoid mutating original
local sorted = {}
for i, row in ipairs(data) do
sorted[i] = row
end
table.sort(sorted, function(a, b)
return compare.compare(a[column_id], b[column_id], direction)
end)
return sorted
end
return M

View File

@@ -0,0 +1,33 @@
-- Toggle sort state for a column
-- Single function module for data table sorting
---@class ToggleSort
local M = {}
---Toggle sort state for a column
---@param state SortState Current sort state
---@param column_id string Column identifier clicked
---@return SortState New sort state
function M.toggleSort(state, column_id)
if state.column_id ~= column_id then
-- New column: start with ascending
return {
column_id = column_id,
direction = "asc"
}
elseif state.direction == "asc" then
-- Same column, was ascending: switch to descending
return {
column_id = column_id,
direction = "desc"
}
else
-- Same column, was descending: clear sort
return {
column_id = nil,
direction = nil
}
end
end
return M

View File

@@ -0,0 +1,14 @@
-- Type definitions for sorting module
-- Shared across all sorting functions
---@alias SortDirection "asc" | "desc" | nil
---@class SortState
---@field column_id string|nil Column being sorted
---@field direction SortDirection Sort direction
---@class SortedResult
---@field data table[] Sorted data array
---@field state SortState Current sort state
return {}

View File

@@ -1,95 +1,5 @@
---@class Fields
local M = {}
-- Form field builders
-- DEPRECATED: This file redirects to fields/ directory
-- Functions are now split into single-function files
---@class UIComponent
---@field type string
---@field props? table
---@field children? UIComponent[]
---@class TextFieldProps
---@field name string
---@field label? string
---@field placeholder? string
---@field required? boolean
---@class EmailFieldProps
---@field name string
---@field label? string
---@class PasswordFieldProps
---@field name string
---@field label? string
---@class NumberFieldProps
---@field name string
---@field label? string
---@field min? number
---@field max? number
---@class TextAreaFieldProps
---@field name string
---@field label? string
---@field placeholder? string
---@field rows? number
---@param props TextFieldProps
---@return UIComponent
function M.text(props)
return {
type = "Box",
children = {
props.label and { type = "Label", props = { text = props.label, htmlFor = props.name } } or nil,
{ type = "Input", props = { name = props.name, placeholder = props.placeholder, required = props.required } }
}
}
end
---@param props EmailFieldProps
---@return UIComponent
function M.email(props)
return {
type = "Box",
children = {
{ type = "Label", props = { text = props.label or "Email", htmlFor = props.name } },
{ type = "Input", props = { name = props.name, type = "email", placeholder = "you@example.com" } }
}
}
end
---@param props PasswordFieldProps
---@return UIComponent
function M.password(props)
return {
type = "Box",
children = {
{ type = "Label", props = { text = props.label or "Password", htmlFor = props.name } },
{ type = "Input", props = { name = props.name, type = "password", placeholder = "••••••••" } }
}
}
end
---@param props NumberFieldProps
---@return UIComponent
function M.number(props)
return {
type = "Box",
children = {
props.label and { type = "Label", props = { text = props.label, htmlFor = props.name } } or nil,
{ type = "Input", props = { name = props.name, type = "number", min = props.min, max = props.max } }
}
}
end
---@param props TextAreaFieldProps
---@return UIComponent
function M.textarea(props)
return {
type = "Box",
children = {
props.label and { type = "Label", props = { text = props.label, htmlFor = props.name } } or nil,
{ type = "TextArea", props = { name = props.name, rows = props.rows or 4, placeholder = props.placeholder } }
}
}
end
return M
return require("fields.init")

View File

@@ -1,81 +1,5 @@
---@class Validate
local M = {}
-- Form validation utilities
-- DEPRECATED: This file redirects to validate/ directory
-- Functions are now split into single-function files
---@class ValidationRule
---@field type string
---@field message? string
---@field min? number
---@field max? number
---@field value? any
---@class ValidationResult
---@field valid boolean
---@field errors string[]
---@param value string|number|nil
---@return boolean
function M.required(value)
return value ~= nil and value ~= ""
end
---@param value string|nil
---@return boolean
function M.email(value)
if not value then return false end
return string.match(value, "^[^@]+@[^@]+%.[^@]+$") ~= nil
end
---@param value string|nil
---@param min number
---@return boolean
function M.minLength(value, min)
return value and #value >= min
end
---@param value string|nil
---@param max number
---@return boolean
function M.maxLength(value, max)
return not value or #value <= max
end
---@param value string|nil
---@param pat string
---@return boolean
function M.pattern(value, pat)
return value and string.match(value, pat) ~= nil
end
---@param value any
---@return boolean
function M.number(value)
return tonumber(value) ~= nil
end
---@param value any
---@param min number
---@param max number
---@return boolean
function M.range(value, min, max)
local n = tonumber(value)
return n and n >= min and n <= max
end
---@param value string|number|nil
---@param rules ValidationRule[]
---@return ValidationResult
function M.validate_field(value, rules)
local errors = {}
for _, rule in ipairs(rules) do
if rule.type == "required" and not M.required(value) then
errors[#errors + 1] = rule.message or "Required"
elseif rule.type == "email" and not M.email(value) then
errors[#errors + 1] = rule.message or "Invalid email"
elseif rule.type == "minLength" and not M.minLength(value, rule.min) then
errors[#errors + 1] = rule.message or ("Min " .. rule.min .. " chars")
end
end
return { valid = #errors == 0, errors = errors }
end
return M
return require("validate.init")