From 2b7225c269247281273de531b7bc74dc2025464c Mon Sep 17 00:00:00 2001 From: Amy Gale Ruth Bowersox Date: Wed, 4 Mar 2026 22:59:34 -0700 Subject: [PATCH] added external directories for E-mail templates and dialog templates --- config/config.go | 2 + config/default.yaml | 2 + email/sender.go | 22 ++++++-- ui/dialog.go | 126 +++++++++++++++++++++++++++++++------------- ui/setup.go | 1 + 5 files changed, 112 insertions(+), 41 deletions(-) diff --git a/config/config.go b/config/config.go index 6b20fdf..05a0b6e 100644 --- a/config/config.go +++ b/config/config.go @@ -104,6 +104,8 @@ type AmConfig struct { } `yaml:"rendering"` Resources struct { ViewTemplateDir string `yaml:"viewTemplateDir"` + DialogTemplateDir string `yaml:"dialogTemplateDir"` + EmailTemplateDir string `yaml:"emailTemplateDir"` ExternalContentPath string `yaml:"externalContentPath"` ExternalResourcePath string `yaml:"externalResourcePath"` } `yaml:"resources"` diff --git a/config/default.yaml b/config/default.yaml index 620cb9f..e219c28 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -49,6 +49,8 @@ rendering: veniceCompatibleImageURLs: false resources: viewTemplateDir: "" + dialogTemplateDir: "" + emailTemplateDir: "" externalContentPath: "" externalResourcePath: "" posting: diff --git a/email/sender.go b/email/sender.go index 924acc5..cd17a21 100644 --- a/email/sender.go +++ b/email/sender.go @@ -26,6 +26,7 @@ import ( "git.erbosoft.com/amy/amsterdam/database" "github.com/CloudyKit/jet/v6" "github.com/CloudyKit/jet/v6/loaders/embedfs" + "github.com/CloudyKit/jet/v6/loaders/multi" log "github.com/sirupsen/logrus" ) @@ -210,11 +211,24 @@ func SetupMailSender() func() { disclaimerLines = strings.Split(config.GlobalConfig.Email.Disclaimer, "\n") signatureLines = strings.Split(config.GlobalConfig.Email.Signature, "\n") + // Locate the external template directory and build the loaders. + templateLoaders := make([]jet.Loader, 0, 2) + if config.GlobalConfig.Resources.EmailTemplateDir != "" { + finfo, err := os.Stat(config.GlobalConfig.Resources.EmailTemplateDir) + if err == nil { + if finfo.IsDir() { + templateLoaders = append(templateLoaders, jet.NewOSFileSystemLoader(config.GlobalConfig.Resources.EmailTemplateDir)) + } else { + log.Errorf("email template directory %s is not a directory, ignored", config.GlobalConfig.Resources.EmailTemplateDir) + } + } else { + log.Errorf("email template directory %s is not valid, ignored (%v)", config.GlobalConfig.Resources.EmailTemplateDir, err) + } + } + templateLoaders = append(templateLoaders, embedfs.NewLoader("templates/", emailTemplates)) + // Initialize the template engine. - emailRenderer = jet.NewSet( - embedfs.NewLoader("templates/", emailTemplates), - jet.DevelopmentMode(true), - ) + emailRenderer = jet.NewSet(multi.NewLoader(templateLoaders...), jet.DevelopmentMode(true)) emailRenderer.AddGlobal("AmsterdamVersion", config.AMSTERDAM_VERSION) emailRenderer.AddGlobal("AmsterdamCopyright", config.AMSTERDAM_COPYRIGHT) emailRenderer.AddGlobal("GlobalConfig", config.GlobalConfig) diff --git a/ui/dialog.go b/ui/dialog.go index 57fe11e..a281bd1 100644 --- a/ui/dialog.go +++ b/ui/dialog.go @@ -13,9 +13,12 @@ package ui import ( "embed" "fmt" + "io" + "io/fs" "math" "net" "net/mail" + "os" "regexp" "strconv" "strings" @@ -24,6 +27,7 @@ import ( "git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/util" + log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) @@ -78,51 +82,99 @@ func (vr *VRange) IsEmpty() bool { //go:embed dialogs/* var dialogs embed.FS +// extDialogs is the external dialogs directory filesystem. +var extDialogs fs.FS = nil + +// setupDialogs sets up the external dialog filesystem. +func setupDialogs() { + // Open the external dialog path. + if config.GlobalConfig.Resources.DialogTemplateDir != "" { + finfo, err := os.Stat(config.GlobalConfig.Resources.DialogTemplateDir) + if err == nil { + if finfo.IsDir() { + root, err := os.OpenRoot(config.GlobalConfig.Resources.DialogTemplateDir) + if err != nil { + panic(err) + } + extDialogs = root.FS() + } else { + log.Errorf("external resource path \"%s\" is not a directory, ignored", config.GlobalConfig.Resources.DialogTemplateDir) + } + } else { + log.Errorf("external resource path \"%s\" is not valid, ignored (%v)", config.GlobalConfig.Resources.DialogTemplateDir, err) + } + } +} + /* AmLoadDialog loads a dialog definition. * Parameters: * name - The name of the dialog to load */ func AmLoadDialog(name string) (*Dialog, error) { - b, err := dialogs.ReadFile(fmt.Sprintf("dialogs/%s.yaml", name)) - if err == nil { - var d Dialog - err = yaml.Unmarshal(b, &d) - if err == nil { - // "nil-patch" certain fields and create the fast-lookup map - if d.MenuSelector == "" { - d.MenuSelector = "nochange" + var f fs.File = nil + var err error + if extDialogs != nil { + f, err = extDialogs.Open(fmt.Sprintf("%s.yaml", name)) + if err != nil { + f = nil + pe := err.(*fs.PathError) + if pe.Err == os.ErrInvalid || pe.Err == os.ErrNotExist { + err = nil } - d.fldmap = make(map[string]*DialogItem) - for i, fld := range d.Fields { - d.fldmap[fld.Name] = &(d.Fields[i]) - if fld.Type == "button" && fld.Param == "" { - d.Fields[i].Param = "blue" - } - if fld.Type == "date" && fld.Param == "" { - d.Fields[i].Param = "year:-100" - } - if fld.Type == "integer" && fld.Size == 0 { - vr := fld.ValueRange() - if !vr.IsEmpty() { - // compute the number of digits in each end of the range and take the maximum as the size - dlow := int(math.Floor(math.Log10(float64(vr.Low)))) + 1 - dhigh := int(math.Floor(math.Log10(float64(vr.High)))) + 1 - d.Fields[i].Size = max(dlow, dhigh) - d.Fields[i].MaxLength = d.Fields[i].Size - } - } - if fld.Type == "ipaddress" { - d.Fields[i].Size = 15 // max IPv4 - d.Fields[i].MaxLength = 39 // max IPv6 - } - if fld.Type == "dropdown" && len(fld.Choices) == 0 { - return nil, fmt.Errorf("dropdown field %s in dialog %s has no choices", fld.Name, name) - } - } - return &d, nil + } + if err != nil { + return nil, err } } - return nil, err + if f == nil { + f, err = dialogs.Open(fmt.Sprintf("dialogs/%s.yaml", name)) + if err != nil { + return nil, err + } + } + b, err := io.ReadAll(f) + f.Close() + if err != nil { + return nil, err + } + + d := new(Dialog) + err = yaml.Unmarshal(b, d) + if err != nil { + return nil, err + } + // "nil-patch" certain fields and create the fast-lookup map + if d.MenuSelector == "" { + d.MenuSelector = "nochange" + } + d.fldmap = make(map[string]*DialogItem) + for i, fld := range d.Fields { + d.fldmap[fld.Name] = &(d.Fields[i]) + if fld.Type == "button" && fld.Param == "" { + d.Fields[i].Param = "blue" + } + if fld.Type == "date" && fld.Param == "" { + d.Fields[i].Param = "year:-100" + } + if fld.Type == "integer" && fld.Size == 0 { + vr := fld.ValueRange() + if !vr.IsEmpty() { + // compute the number of digits in each end of the range and take the maximum as the size + dlow := int(math.Floor(math.Log10(float64(vr.Low)))) + 1 + dhigh := int(math.Floor(math.Log10(float64(vr.High)))) + 1 + d.Fields[i].Size = max(dlow, dhigh) + d.Fields[i].MaxLength = d.Fields[i].Size + } + } + if fld.Type == "ipaddress" { + d.Fields[i].Size = 15 // max IPv4 + d.Fields[i].MaxLength = 39 // max IPv6 + } + if fld.Type == "dropdown" && len(fld.Choices) == 0 { + return nil, fmt.Errorf("dropdown field %s in dialog %s has no choices", fld.Name, name) + } + } + return d, nil } // DateValues returns the date values stored in a date field. diff --git a/ui/setup.go b/ui/setup.go index 242a279..8494ade 100644 --- a/ui/setup.go +++ b/ui/setup.go @@ -16,6 +16,7 @@ import "slices" func SetupUILayer() func() { exitfuncs := make([]func(), 0, 2) setupTemplates() + setupDialogs() setupMenuCache() setupResources() exitfuncs = append(exitfuncs, setupSessionManager())