mirror of
https://github.com/oarkflow/mq.git
synced 2025-10-09 06:00:04 +08:00
update
This commit is contained in:
148
examples/app/jsonschema_renderer.go
Normal file
148
examples/app/jsonschema_renderer.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// FieldInfo represents metadata for a field extracted from JSONSchema
|
||||
type FieldInfo struct {
|
||||
Name string
|
||||
Order int
|
||||
Definition map[string]interface{}
|
||||
}
|
||||
|
||||
// JSONSchemaRenderer is responsible for rendering HTML fields based on JSONSchema
|
||||
type JSONSchemaRenderer struct {
|
||||
Schema map[string]interface{}
|
||||
HTMLLayout string
|
||||
}
|
||||
|
||||
// NewJSONSchemaRenderer creates a new instance of JSONSchemaRenderer
|
||||
func NewJSONSchemaRenderer(schema map[string]interface{}, htmlLayout string) *JSONSchemaRenderer {
|
||||
return &JSONSchemaRenderer{
|
||||
Schema: schema,
|
||||
HTMLLayout: htmlLayout,
|
||||
}
|
||||
}
|
||||
|
||||
// RenderFields generates HTML for fields based on the JSONSchema
|
||||
func (r *JSONSchemaRenderer) RenderFields() (string, error) {
|
||||
fields := parseFieldsFromSchema(r.Schema)
|
||||
requiredFields := make(map[string]bool)
|
||||
if requiredList, ok := r.Schema["required"].([]interface{}); ok {
|
||||
for _, field := range requiredList {
|
||||
if fieldName, ok := field.(string); ok {
|
||||
requiredFields[fieldName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(fields, func(i, j int) bool {
|
||||
return fields[i].Order < fields[j].Order
|
||||
})
|
||||
|
||||
var fieldHTML bytes.Buffer
|
||||
for _, field := range fields {
|
||||
fieldHTML.WriteString(renderField(field, requiredFields))
|
||||
}
|
||||
|
||||
tmpl, err := template.New("layout").Funcs(template.FuncMap{
|
||||
"form_fields": func() template.HTML {
|
||||
return template.HTML(fieldHTML.String())
|
||||
},
|
||||
}).Parse(r.HTMLLayout)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse HTML layout: %w", err)
|
||||
}
|
||||
|
||||
var renderedHTML bytes.Buffer
|
||||
err = tmpl.Execute(&renderedHTML, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to execute HTML template: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`<form>%s</form>`, renderedHTML.String()), nil
|
||||
}
|
||||
|
||||
// parseFieldsFromSchema extracts and sorts fields from schema
|
||||
func parseFieldsFromSchema(schema map[string]interface{}) []FieldInfo {
|
||||
properties, ok := schema["properties"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var fields []FieldInfo
|
||||
for name, definition := range properties {
|
||||
order := 0
|
||||
if defMap, ok := definition.(map[string]interface{}); ok {
|
||||
if ord, exists := defMap["order"].(float64); exists {
|
||||
order = int(ord) // Ensure consistent type for sorting
|
||||
}
|
||||
fields = append(fields, FieldInfo{
|
||||
Name: name,
|
||||
Order: order,
|
||||
Definition: defMap,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(fields, func(i, j int) bool {
|
||||
return fields[i].Order < fields[j].Order
|
||||
})
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
// renderField generates HTML for a single field
|
||||
func renderField(field FieldInfo, requiredFields map[string]bool) string {
|
||||
ui, ok := field.Definition["ui"].(map[string]interface{})
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
control, _ := ui["element"].(string)
|
||||
class, _ := ui["class"].(string)
|
||||
name, _ := ui["name"].(string)
|
||||
title, _ := field.Definition["title"].(string)
|
||||
placeholder, _ := field.Definition["placeholder"].(string)
|
||||
|
||||
isRequired := requiredFields[name]
|
||||
required := ""
|
||||
if isRequired {
|
||||
required = "required"
|
||||
title += " *" // Add asterisk for required fields
|
||||
}
|
||||
|
||||
additionalAttributes := ""
|
||||
for key, value := range field.Definition {
|
||||
if key != "title" && key != "ui" && key != "placeholder" {
|
||||
additionalAttributes += fmt.Sprintf(` %s="%v"`, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
switch control {
|
||||
case "input":
|
||||
return fmt.Sprintf(`<div class="%s"><label for="%s">%s</label><input type="text" id="%s" name="%s" placeholder="%s" %s %s /></div>`, class, name, title, name, name, placeholder, required, additionalAttributes)
|
||||
case "textarea":
|
||||
return fmt.Sprintf(`<div class="%s"><label for="%s">%s</label><textarea id="%s" name="%s" placeholder="%s" %s %s></textarea></div>`, class, name, title, name, name, placeholder, required, additionalAttributes)
|
||||
case "select":
|
||||
options, _ := ui["options"].([]interface{})
|
||||
var optionsHTML bytes.Buffer
|
||||
for _, option := range options {
|
||||
optionsHTML.WriteString(fmt.Sprintf(`<option value="%v">%v</option>`, option, option))
|
||||
}
|
||||
return fmt.Sprintf(`<div class="%s"><label for="%s">%s</label><select id="%s" name="%s" %s %s>%s</select></div>`, class, name, title, name, name, required, additionalAttributes, optionsHTML.String())
|
||||
case "h1", "h2", "h3", "h4", "h5", "h6":
|
||||
return fmt.Sprintf(`<%s class="%s" id="%s" %s>%s</%s>`, control, class, name, additionalAttributes, title, control)
|
||||
case "p":
|
||||
return fmt.Sprintf(`<p class="%s" id="%s" %s>%s</p>`, class, name, additionalAttributes, title)
|
||||
case "a":
|
||||
href, _ := ui["href"].(string)
|
||||
return fmt.Sprintf(`<a class="%s" id="%s" href="%s" %s>%s</a>`, class, name, href, additionalAttributes, title)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
@@ -3,30 +3,30 @@
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string",
|
||||
"title": "👤 First Name",
|
||||
"title": "First Name",
|
||||
"order": 1,
|
||||
"ui": {
|
||||
"control": "input",
|
||||
"element": "input",
|
||||
"class": "form-group",
|
||||
"name": "first_name"
|
||||
}
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string",
|
||||
"title": "👤 Last Name",
|
||||
"title": "Last Name",
|
||||
"order": 2,
|
||||
"ui": {
|
||||
"control": "input",
|
||||
"element": "input",
|
||||
"class": "form-group",
|
||||
"name": "last_name"
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"type": "email",
|
||||
"title": "📧 Email Address",
|
||||
"title": "Email Address",
|
||||
"order": 3,
|
||||
"ui": {
|
||||
"control": "input",
|
||||
"element": "input",
|
||||
"type": "email",
|
||||
"class": "form-group",
|
||||
"name": "email"
|
||||
@@ -34,10 +34,10 @@
|
||||
},
|
||||
"user_type": {
|
||||
"type": "string",
|
||||
"title": "👥 User Type",
|
||||
"title": "User Type",
|
||||
"order": 4,
|
||||
"ui": {
|
||||
"control": "select",
|
||||
"element": "select",
|
||||
"class": "form-group",
|
||||
"name": "user_type",
|
||||
"options": [ "new", "premium", "standard" ]
|
||||
@@ -45,10 +45,10 @@
|
||||
},
|
||||
"priority": {
|
||||
"type": "string",
|
||||
"title": "🚨 Priority Level",
|
||||
"title": "Priority Level",
|
||||
"order": 5,
|
||||
"ui": {
|
||||
"control": "select",
|
||||
"element": "select",
|
||||
"class": "form-group",
|
||||
"name": "priority",
|
||||
"options": [ "low", "medium", "high", "urgent" ]
|
||||
@@ -56,24 +56,50 @@
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"title": "📋 Subject",
|
||||
"title": "Subject",
|
||||
"order": 6,
|
||||
"ui": {
|
||||
"control": "input",
|
||||
"element": "input",
|
||||
"class": "form-group",
|
||||
"name": "subject"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"type": "textarea",
|
||||
"title": "💬 Message",
|
||||
"title": "Message",
|
||||
"order": 7,
|
||||
"ui": {
|
||||
"control": "textarea",
|
||||
"element": "textarea",
|
||||
"class": "form-group",
|
||||
"name": "message"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [ "first_name", "last_name", "email", "user_type", "priority", "subject", "message" ]
|
||||
"required": [ "first_name", "last_name", "email", "user_type", "priority", "subject", "message" ],
|
||||
"form": {
|
||||
"class": "form-horizontal",
|
||||
"action": "/submit",
|
||||
"method": "POST",
|
||||
"enctype": "application/x-www-form-urlencoded",
|
||||
"groups": [
|
||||
{
|
||||
"title": "User Information",
|
||||
"fields": [ "first_name", "last_name", "email" ]
|
||||
},
|
||||
{
|
||||
"title": "Ticket Details",
|
||||
"fields": [ "user_type", "priority", "subject", "message" ]
|
||||
}
|
||||
],
|
||||
"submit": {
|
||||
"type": "submit",
|
||||
"label": "Submit",
|
||||
"class": "btn btn-primary"
|
||||
},
|
||||
"reset": {
|
||||
"type": "reset",
|
||||
"label": "Reset",
|
||||
"class": "btn btn-secondary"
|
||||
}
|
||||
}
|
||||
}
|
53
examples/app/server.go
Normal file
53
examples/app/server.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
schemaContent, err := os.ReadFile("schema.json")
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading schema file: %v\n", err)
|
||||
return
|
||||
}
|
||||
var schema map[string]interface{}
|
||||
if err := json.Unmarshal(schemaContent, &schema); err != nil {
|
||||
fmt.Printf("Error parsing schema: %v\n", err)
|
||||
return
|
||||
}
|
||||
http.Handle("/form.css", http.FileServer(http.Dir("templates")))
|
||||
http.HandleFunc("/render", func(w http.ResponseWriter, r *http.Request) {
|
||||
templateName := r.URL.Query().Get("template")
|
||||
if templateName == "" {
|
||||
templateName = "basic"
|
||||
}
|
||||
|
||||
templatePath := fmt.Sprintf("templates/%s.html", templateName)
|
||||
htmlLayout, err := os.ReadFile(templatePath)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to load template: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
renderer := NewJSONSchemaRenderer(schema, string(htmlLayout))
|
||||
renderedHTML, err := renderer.RenderFields()
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to render fields: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(renderedHTML))
|
||||
})
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
fmt.Printf("Server running on port %s\n", port)
|
||||
http.ListenAndServe(fmt.Sprintf(":%s", port), nil)
|
||||
}
|
26
examples/app/templates/advanced.html
Normal file
26
examples/app/templates/advanced.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Advanced Template</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="form.css">
|
||||
<style>
|
||||
.form-container {
|
||||
padding: 20px;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-200">
|
||||
<div class="form-container rounded-lg">
|
||||
<h1 class="text-xl font-bold mb-4">Form Fields</h1>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
{{form_fields}}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
16
examples/app/templates/basic.html
Normal file
16
examples/app/templates/basic.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Basic Template</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="form.css">
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100">
|
||||
<div class="form-container p-4 bg-white shadow-md rounded">
|
||||
{{form_fields}}
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
116
examples/app/templates/form.css
Normal file
116
examples/app/templates/form.css
Normal file
@@ -0,0 +1,116 @@
|
||||
/* Normalize and style form controls */
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="email"],
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 1rem;
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group input[type="text"]:focus,
|
||||
.form-group input[type="email"]:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
border-color: #007bff;
|
||||
outline: none;
|
||||
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
|
||||
}
|
||||
|
||||
.form-group select {
|
||||
appearance: none;
|
||||
background: url('data:image/svg+xml;charset=US-ASCII,%3Csvg xmlns%3D%22http%3A//www.w3.org/2000/svg%22 viewBox%3D%220 0 4 5%22%3E%3Cpath fill%3D%22%23000%22 d%3D%22M2 0L0 2h4z%22/%3E%3C/svg%3E') no-repeat right 0.75rem center;
|
||||
background-size: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-group .form-control-error {
|
||||
color: #dc3545;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background-color: #007bff;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Additional layout-specific styles */
|
||||
.bg-gray-100 {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.bg-gray-200 {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.shadow-md {
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.text-xl {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
Reference in New Issue
Block a user