mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
feat: Add Toast context and provider for notification management
This commit is contained in:
235
fakemui/fakemui/utils/ToastContext.tsx
Normal file
235
fakemui/fakemui/utils/ToastContext.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import React, { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react'
|
||||
import { Snackbar, SnackbarContent } from '../feedback/Snackbar'
|
||||
|
||||
export type ToastSeverity = 'success' | 'error' | 'warning' | 'info'
|
||||
|
||||
export interface ToastOptions {
|
||||
/** Toast message content */
|
||||
message: string
|
||||
/** Severity level for styling */
|
||||
severity?: ToastSeverity
|
||||
/** Auto hide duration in ms (null to disable) */
|
||||
autoHideDuration?: number | null
|
||||
/** Action button content */
|
||||
action?: React.ReactNode
|
||||
/** Custom key for deduplication */
|
||||
key?: string
|
||||
/** Callback when toast closes */
|
||||
onClose?: () => void
|
||||
/** Anchor position */
|
||||
anchorOrigin?: {
|
||||
vertical: 'top' | 'bottom'
|
||||
horizontal: 'left' | 'center' | 'right'
|
||||
}
|
||||
}
|
||||
|
||||
interface Toast extends Required<Pick<ToastOptions, 'message' | 'severity' | 'autoHideDuration'>> {
|
||||
id: string
|
||||
action?: React.ReactNode
|
||||
onClose?: () => void
|
||||
anchorOrigin: NonNullable<ToastOptions['anchorOrigin']>
|
||||
}
|
||||
|
||||
interface ToastContextValue {
|
||||
/** Show a toast notification */
|
||||
toast: (options: ToastOptions | string) => string
|
||||
/** Show a success toast */
|
||||
success: (message: string, options?: Omit<ToastOptions, 'message' | 'severity'>) => string
|
||||
/** Show an error toast */
|
||||
error: (message: string, options?: Omit<ToastOptions, 'message' | 'severity'>) => string
|
||||
/** Show a warning toast */
|
||||
warning: (message: string, options?: Omit<ToastOptions, 'message' | 'severity'>) => string
|
||||
/** Show an info toast */
|
||||
info: (message: string, options?: Omit<ToastOptions, 'message' | 'severity'>) => string
|
||||
/** Close a specific toast by ID */
|
||||
close: (id: string) => void
|
||||
/** Close all toasts */
|
||||
closeAll: () => void
|
||||
}
|
||||
|
||||
const ToastContext = createContext<ToastContextValue | null>(null)
|
||||
|
||||
let toastIdCounter = 0
|
||||
const generateId = () => `toast-${++toastIdCounter}`
|
||||
|
||||
export interface ToastProviderProps {
|
||||
children: React.ReactNode
|
||||
/** Default auto hide duration in ms */
|
||||
defaultAutoHideDuration?: number
|
||||
/** Maximum number of toasts to show at once */
|
||||
maxToasts?: number
|
||||
/** Default anchor position */
|
||||
defaultAnchorOrigin?: ToastOptions['anchorOrigin']
|
||||
}
|
||||
|
||||
export const ToastProvider: React.FC<ToastProviderProps> = ({
|
||||
children,
|
||||
defaultAutoHideDuration = 5000,
|
||||
maxToasts = 3,
|
||||
defaultAnchorOrigin = { vertical: 'bottom', horizontal: 'left' },
|
||||
}) => {
|
||||
const [toasts, setToasts] = useState<Toast[]>([])
|
||||
const timersRef = useRef<Map<string, NodeJS.Timeout>>(new Map())
|
||||
|
||||
// Clear timer when toast is removed
|
||||
const clearTimer = useCallback((id: string) => {
|
||||
const timer = timersRef.current.get(id)
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
timersRef.current.delete(id)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Close a specific toast
|
||||
const close = useCallback((id: string) => {
|
||||
clearTimer(id)
|
||||
setToasts(prev => {
|
||||
const toast = prev.find(t => t.id === id)
|
||||
if (toast?.onClose) {
|
||||
toast.onClose()
|
||||
}
|
||||
return prev.filter(t => t.id !== id)
|
||||
})
|
||||
}, [clearTimer])
|
||||
|
||||
// Close all toasts
|
||||
const closeAll = useCallback(() => {
|
||||
timersRef.current.forEach((_, id) => clearTimer(id))
|
||||
setToasts([])
|
||||
}, [clearTimer])
|
||||
|
||||
// Main toast function
|
||||
const toast = useCallback((options: ToastOptions | string): string => {
|
||||
const opts: ToastOptions = typeof options === 'string' ? { message: options } : options
|
||||
const id = opts.key || generateId()
|
||||
|
||||
// Check if toast with same key already exists
|
||||
setToasts(prev => {
|
||||
const existingIndex = prev.findIndex(t => t.id === id)
|
||||
const newToast: Toast = {
|
||||
id,
|
||||
message: opts.message,
|
||||
severity: opts.severity || 'info',
|
||||
autoHideDuration: opts.autoHideDuration ?? defaultAutoHideDuration,
|
||||
action: opts.action,
|
||||
onClose: opts.onClose,
|
||||
anchorOrigin: opts.anchorOrigin || defaultAnchorOrigin,
|
||||
}
|
||||
|
||||
let newToasts: Toast[]
|
||||
if (existingIndex >= 0) {
|
||||
// Update existing toast
|
||||
newToasts = [...prev]
|
||||
newToasts[existingIndex] = newToast
|
||||
} else {
|
||||
// Add new toast (respecting maxToasts)
|
||||
newToasts = [...prev, newToast]
|
||||
if (newToasts.length > maxToasts) {
|
||||
const removed = newToasts.shift()
|
||||
if (removed) clearTimer(removed.id)
|
||||
}
|
||||
}
|
||||
return newToasts
|
||||
})
|
||||
|
||||
// Set up auto-hide timer
|
||||
const duration = opts.autoHideDuration ?? defaultAutoHideDuration
|
||||
if (duration !== null && duration > 0) {
|
||||
clearTimer(id) // Clear existing timer if updating
|
||||
const timer = setTimeout(() => close(id), duration)
|
||||
timersRef.current.set(id, timer)
|
||||
}
|
||||
|
||||
return id
|
||||
}, [defaultAutoHideDuration, defaultAnchorOrigin, maxToasts, clearTimer, close])
|
||||
|
||||
// Helper methods for each severity
|
||||
const success = useCallback((message: string, options?: Omit<ToastOptions, 'message' | 'severity'>) => {
|
||||
return toast({ ...options, message, severity: 'success' })
|
||||
}, [toast])
|
||||
|
||||
const error = useCallback((message: string, options?: Omit<ToastOptions, 'message' | 'severity'>) => {
|
||||
return toast({ ...options, message, severity: 'error' })
|
||||
}, [toast])
|
||||
|
||||
const warning = useCallback((message: string, options?: Omit<ToastOptions, 'message' | 'severity'>) => {
|
||||
return toast({ ...options, message, severity: 'warning' })
|
||||
}, [toast])
|
||||
|
||||
const info = useCallback((message: string, options?: Omit<ToastOptions, 'message' | 'severity'>) => {
|
||||
return toast({ ...options, message, severity: 'info' })
|
||||
}, [toast])
|
||||
|
||||
// Cleanup timers on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
timersRef.current.forEach(timer => clearTimeout(timer))
|
||||
}
|
||||
}, [])
|
||||
|
||||
const contextValue: ToastContextValue = {
|
||||
toast,
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
info,
|
||||
close,
|
||||
closeAll,
|
||||
}
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={contextValue}>
|
||||
{children}
|
||||
{/* Render toasts */}
|
||||
{toasts.map(t => (
|
||||
<Snackbar
|
||||
key={t.id}
|
||||
open={true}
|
||||
autoHideDuration={null} // Handled by context
|
||||
onClose={() => close(t.id)}
|
||||
anchorOrigin={t.anchorOrigin}
|
||||
>
|
||||
<SnackbarContent
|
||||
message={t.message}
|
||||
severity={t.severity}
|
||||
action={t.action}
|
||||
onClose={() => close(t.id)}
|
||||
/>
|
||||
</Snackbar>
|
||||
))}
|
||||
</ToastContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access toast notifications
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { toast, success, error } = useToast()
|
||||
*
|
||||
* // Simple usage
|
||||
* toast('Hello world')
|
||||
*
|
||||
* // With severity helpers
|
||||
* success('Operation completed!')
|
||||
* error('Something went wrong')
|
||||
*
|
||||
* // With options
|
||||
* toast({
|
||||
* message: 'Custom toast',
|
||||
* severity: 'warning',
|
||||
* autoHideDuration: 3000,
|
||||
* action: <button onClick={() => {}}>Undo</button>
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const useToast = (): ToastContextValue => {
|
||||
const context = useContext(ToastContext)
|
||||
if (!context) {
|
||||
throw new Error('useToast must be used within a ToastProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export default ToastProvider
|
||||
@@ -1,22 +1,105 @@
|
||||
--- Media pane logic facade for quick guides
|
||||
--- Re-exports media functions for backward compatibility
|
||||
---@module media
|
||||
-- Media pane logic for quick guides
|
||||
|
||||
local is_valid_url = require("is_valid_url")
|
||||
local is_image_url = require("is_image_url")
|
||||
local is_video_url = require("is_video_url")
|
||||
local prepare_media_state = require("prepare_media_state")
|
||||
local handle_thumbnail_change = require("handle_thumbnail_change")
|
||||
local handle_video_change = require("handle_video_change")
|
||||
---@class MediaState
|
||||
---@field thumbnailUrl string Thumbnail URL
|
||||
---@field videoUrl string Video URL
|
||||
---@field thumbnailValid boolean Whether thumbnail URL is valid
|
||||
---@field videoValid boolean Whether video URL is valid
|
||||
---@field thumbnailIsImage boolean Whether thumbnail is an image URL
|
||||
---@field videoIsVideo boolean Whether video is a video URL
|
||||
|
||||
---@class MediaProps
|
||||
---@field thumbnailUrl? string Initial thumbnail URL
|
||||
---@field videoUrl? string Initial video URL
|
||||
|
||||
---@class MediaModule
|
||||
local M = {}
|
||||
|
||||
M.isValidUrl = is_valid_url
|
||||
M.isImageUrl = is_image_url
|
||||
M.isVideoUrl = is_video_url
|
||||
M.prepareMediaState = prepare_media_state
|
||||
M.handleThumbnailChange = handle_thumbnail_change
|
||||
M.handleVideoChange = handle_video_change
|
||||
---Validate a URL (basic check)
|
||||
---@param url? string URL to validate
|
||||
---@return boolean Whether URL is valid
|
||||
function M.isValidUrl(url)
|
||||
if not url or url == "" then
|
||||
return false
|
||||
end
|
||||
return string.match(url, "^https?://") ~= nil
|
||||
end
|
||||
|
||||
---Check if URL is an image
|
||||
---@param url? string URL to check
|
||||
---@return boolean Whether URL points to an image
|
||||
function M.isImageUrl(url)
|
||||
if not M.isValidUrl(url) then
|
||||
return false
|
||||
end
|
||||
local patterns = { "%.png$", "%.jpg$", "%.jpeg$", "%.gif$", "%.webp$", "%.svg$" }
|
||||
for _, pattern in ipairs(patterns) do
|
||||
if string.match(url:lower(), pattern) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---Check if URL is a video embed
|
||||
---@param url? string URL to check
|
||||
---@return boolean Whether URL points to a video
|
||||
function M.isVideoUrl(url)
|
||||
if not M.isValidUrl(url) then
|
||||
return false
|
||||
end
|
||||
local patterns = { "youtube%.com", "vimeo%.com", "%.mp4$", "%.webm$" }
|
||||
for _, pattern in ipairs(patterns) do
|
||||
if string.match(url:lower(), pattern) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---Prepare media state from props
|
||||
---@param props? MediaProps Input props
|
||||
---@return MediaState Initial media state
|
||||
function M.prepareMediaState(props)
|
||||
props = props or {}
|
||||
return {
|
||||
thumbnailUrl = props.thumbnailUrl or "",
|
||||
videoUrl = props.videoUrl or "",
|
||||
thumbnailValid = M.isValidUrl(props.thumbnailUrl),
|
||||
videoValid = M.isValidUrl(props.videoUrl),
|
||||
thumbnailIsImage = M.isImageUrl(props.thumbnailUrl),
|
||||
videoIsVideo = M.isVideoUrl(props.videoUrl)
|
||||
}
|
||||
end
|
||||
|
||||
---Handle thumbnail URL change
|
||||
---@param state MediaState Current state
|
||||
---@param newUrl string New thumbnail URL
|
||||
---@return MediaState Updated state
|
||||
function M.handleThumbnailChange(state, newUrl)
|
||||
return {
|
||||
thumbnailUrl = newUrl,
|
||||
videoUrl = state.videoUrl,
|
||||
thumbnailValid = M.isValidUrl(newUrl),
|
||||
videoValid = state.videoValid,
|
||||
thumbnailIsImage = M.isImageUrl(newUrl),
|
||||
videoIsVideo = state.videoIsVideo
|
||||
}
|
||||
end
|
||||
|
||||
---Handle video URL change
|
||||
---@param state MediaState Current state
|
||||
---@param newUrl string New video URL
|
||||
---@return MediaState Updated state
|
||||
function M.handleVideoChange(state, newUrl)
|
||||
return {
|
||||
thumbnailUrl = state.thumbnailUrl,
|
||||
videoUrl = newUrl,
|
||||
thumbnailValid = state.thumbnailValid,
|
||||
videoValid = M.isValidUrl(newUrl),
|
||||
thumbnailIsImage = state.thumbnailIsImage,
|
||||
videoIsVideo = M.isVideoUrl(newUrl)
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -1,26 +1,146 @@
|
||||
--- Steps editor logic facade for quick guides
|
||||
--- Re-exports step functions for backward compatibility
|
||||
---@module steps
|
||||
-- Steps editor logic for quick guides
|
||||
|
||||
local generate_step_id = require("generate_step_id")
|
||||
local create_step = require("create_step")
|
||||
local update_step = require("update_step")
|
||||
local remove_step = require("remove_step")
|
||||
local add_step = require("add_step")
|
||||
local reset_ordering = require("reset_ordering")
|
||||
local validate_step = require("validate_step")
|
||||
local validate_all_steps = require("validate_all_steps")
|
||||
---@class Step
|
||||
---@field id string Unique step identifier
|
||||
---@field title string Step title
|
||||
---@field description string Step description
|
||||
---@field duration string Estimated duration
|
||||
---@field mediaUrl? string Optional media URL
|
||||
|
||||
---@class StepValidationErrors
|
||||
---@field title? string Title error message
|
||||
---@field description? string Description error message
|
||||
|
||||
---@class StepValidationResult
|
||||
---@field valid boolean Whether step is valid
|
||||
---@field errors StepValidationErrors Validation errors
|
||||
|
||||
---@class AllStepsValidationResult
|
||||
---@field valid boolean Whether all steps are valid
|
||||
---@field errors table<string, StepValidationErrors> Errors by step ID
|
||||
|
||||
---@class StepsModule
|
||||
local M = {}
|
||||
|
||||
M.generateStepId = generate_step_id
|
||||
M.createStep = create_step
|
||||
M.updateStep = update_step
|
||||
M.removeStep = remove_step
|
||||
M.addStep = add_step
|
||||
M.resetOrdering = reset_ordering
|
||||
M.validateStep = validate_step
|
||||
M.validateAllSteps = validate_all_steps
|
||||
---Generate a unique step ID
|
||||
---@return string Unique step identifier
|
||||
function M.generateStepId()
|
||||
return "step_" .. tostring(os.time()) .. "_" .. math.random(1000, 9999)
|
||||
end
|
||||
|
||||
---Create a new empty step
|
||||
---@return Step New step with default values
|
||||
function M.createStep()
|
||||
return {
|
||||
id = M.generateStepId(),
|
||||
title = "New step",
|
||||
description = "Describe what happens in this step.",
|
||||
duration = "1-2 min",
|
||||
mediaUrl = nil
|
||||
}
|
||||
end
|
||||
|
||||
---Update a step in the list
|
||||
---@param steps Step[] Array of steps
|
||||
---@param stepId string ID of step to update
|
||||
---@param updates table Partial step updates
|
||||
---@return Step[] Updated steps array
|
||||
function M.updateStep(steps, stepId, updates)
|
||||
local result = {}
|
||||
for i, step in ipairs(steps) do
|
||||
if step.id == stepId then
|
||||
local updatedStep = {}
|
||||
for k, v in pairs(step) do
|
||||
updatedStep[k] = v
|
||||
end
|
||||
for k, v in pairs(updates) do
|
||||
updatedStep[k] = v
|
||||
end
|
||||
result[i] = updatedStep
|
||||
else
|
||||
result[i] = step
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---Remove a step from the list
|
||||
---@param steps Step[] Array of steps
|
||||
---@param stepId string ID of step to remove
|
||||
---@return Step[] Updated steps array
|
||||
function M.removeStep(steps, stepId)
|
||||
local result = {}
|
||||
for _, step in ipairs(steps) do
|
||||
if step.id ~= stepId then
|
||||
result[#result + 1] = step
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---Add a new step to the list
|
||||
---@param steps Step[] Array of steps
|
||||
---@return Step[] Updated steps array
|
||||
---@return Step New step that was added
|
||||
function M.addStep(steps)
|
||||
local newStep = M.createStep()
|
||||
local result = {}
|
||||
for i, step in ipairs(steps) do
|
||||
result[i] = step
|
||||
end
|
||||
result[#result + 1] = newStep
|
||||
return result, newStep
|
||||
end
|
||||
|
||||
---Reset step IDs to sequential order
|
||||
---@param steps Step[] Array of steps
|
||||
---@return Step[] Steps with reset IDs
|
||||
function M.resetOrdering(steps)
|
||||
local result = {}
|
||||
for i, step in ipairs(steps) do
|
||||
local resetStep = {}
|
||||
for k, v in pairs(step) do
|
||||
resetStep[k] = v
|
||||
end
|
||||
resetStep.id = "step_" .. tostring(i)
|
||||
result[i] = resetStep
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---Validate a single step
|
||||
---@param step Step Step to validate
|
||||
---@return StepValidationResult Validation result
|
||||
function M.validateStep(step)
|
||||
local errors = {}
|
||||
|
||||
if not step.title or step.title == "" then
|
||||
errors.title = "Title is required"
|
||||
end
|
||||
|
||||
if not step.description or step.description == "" then
|
||||
errors.description = "Description is required"
|
||||
end
|
||||
|
||||
return { valid = next(errors) == nil, errors = errors }
|
||||
end
|
||||
|
||||
---Validate all steps
|
||||
---@param steps Step[] Array of steps to validate
|
||||
---@return AllStepsValidationResult Validation result for all steps
|
||||
function M.validateAllSteps(steps)
|
||||
local allErrors = {}
|
||||
local valid = true
|
||||
|
||||
for i, step in ipairs(steps) do
|
||||
local result = M.validateStep(step)
|
||||
if not result.valid then
|
||||
valid = false
|
||||
allErrors[step.id] = result.errors
|
||||
end
|
||||
end
|
||||
|
||||
return { valid = valid, errors = allErrors }
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -1,70 +1,20 @@
|
||||
-- User management actions
|
||||
--- User management actions facade
|
||||
--- Re-exports user action functions for backward compatibility
|
||||
---@module actions
|
||||
|
||||
---@class UserAction
|
||||
---@field action string
|
||||
---@field user_id string?
|
||||
---@field data table?
|
||||
---@field confirm boolean?
|
||||
---@field level number?
|
||||
---@field active boolean?
|
||||
local create_user = require("create_user")
|
||||
local update_user = require("update_user")
|
||||
local delete_user = require("delete_user")
|
||||
local change_level = require("change_level")
|
||||
local toggle_active = require("toggle_active")
|
||||
|
||||
---@class ActionsModule
|
||||
local M = {}
|
||||
|
||||
---Create a new user
|
||||
---@param data table
|
||||
---@return UserAction
|
||||
function M.create(data)
|
||||
return {
|
||||
action = "create_user",
|
||||
data = data
|
||||
}
|
||||
end
|
||||
|
||||
---Update an existing user
|
||||
---@param user_id string
|
||||
---@param data table
|
||||
---@return UserAction
|
||||
function M.update(user_id, data)
|
||||
return {
|
||||
action = "update_user",
|
||||
user_id = user_id,
|
||||
data = data
|
||||
}
|
||||
end
|
||||
|
||||
---Delete a user
|
||||
---@param user_id string
|
||||
---@return UserAction
|
||||
function M.delete(user_id)
|
||||
return {
|
||||
action = "delete_user",
|
||||
user_id = user_id,
|
||||
confirm = true
|
||||
}
|
||||
end
|
||||
|
||||
---Change user access level
|
||||
---@param user_id string
|
||||
---@param new_level number
|
||||
---@return UserAction
|
||||
function M.change_level(user_id, new_level)
|
||||
return {
|
||||
action = "change_level",
|
||||
user_id = user_id,
|
||||
level = new_level
|
||||
}
|
||||
end
|
||||
|
||||
---Toggle user active status
|
||||
---@param user_id string
|
||||
---@param active boolean
|
||||
---@return UserAction
|
||||
function M.toggle_active(user_id, active)
|
||||
return {
|
||||
action = "toggle_active",
|
||||
user_id = user_id,
|
||||
active = active
|
||||
}
|
||||
end
|
||||
M.create = create_user
|
||||
M.update = update_user
|
||||
M.delete = delete_user
|
||||
M.change_level = change_level
|
||||
M.toggle_active = toggle_active
|
||||
|
||||
return M
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
-- User Manager initialization
|
||||
--- User Manager initialization
|
||||
--- Package entry point for user management utilities
|
||||
---@module init
|
||||
|
||||
---@class UserManagerModule
|
||||
---@field name string Package name
|
||||
---@field version string Package version
|
||||
---@field init fun(): table Initialize the module
|
||||
local M = {}
|
||||
|
||||
M.name = "user_manager"
|
||||
M.version = "1.0.0"
|
||||
|
||||
--- Initialize the user manager module
|
||||
---@return table Module info
|
||||
function M.init()
|
||||
return {
|
||||
name = M.name,
|
||||
|
||||
@@ -1,38 +1,16 @@
|
||||
-- User list rendering
|
||||
--- User list rendering facade
|
||||
--- Re-exports user list functions for backward compatibility
|
||||
---@module list
|
||||
|
||||
local get_columns = require("get_columns")
|
||||
local render_row = require("render_row")
|
||||
local render_users = require("render_users")
|
||||
|
||||
---@class ListModule
|
||||
local M = {}
|
||||
|
||||
function M.columns()
|
||||
return {
|
||||
{ id = "username", label = "Username", sortable = true },
|
||||
{ id = "email", label = "Email", sortable = true },
|
||||
{ id = "role", label = "Role", sortable = true },
|
||||
{ id = "level", label = "Level", sortable = true },
|
||||
{ id = "active", label = "Status", type = "badge" },
|
||||
{ id = "actions", label = "", type = "actions" }
|
||||
}
|
||||
end
|
||||
|
||||
function M.render_row(user)
|
||||
return {
|
||||
username = user.username,
|
||||
email = user.email,
|
||||
role = user.role,
|
||||
level = user.level,
|
||||
active = user.active and "Active" or "Inactive",
|
||||
actions = { "edit", "delete" }
|
||||
}
|
||||
end
|
||||
|
||||
function M.render(users)
|
||||
local rows = {}
|
||||
for _, user in ipairs(users) do
|
||||
table.insert(rows, M.render_row(user))
|
||||
end
|
||||
return {
|
||||
type = "data_table",
|
||||
columns = M.columns(),
|
||||
rows = rows
|
||||
}
|
||||
end
|
||||
M.columns = get_columns
|
||||
M.render_row = render_row
|
||||
M.render = render_users
|
||||
|
||||
return M
|
||||
|
||||
Reference in New Issue
Block a user