Implement auto-fill variables for test plugin option fields
Add an AutoFill attribute to the Field struct that marks option fields as automatically resolved by the software based on test context, rather than requiring user input. Auto-fill always overrides any user-provided value at execution time.
This commit is contained in:
parent
276ca27cae
commit
3bf9cc189b
12 changed files with 260 additions and 114 deletions
|
|
@ -23,8 +23,6 @@ package controller
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"maps"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
|
@ -230,44 +228,12 @@ func (tc *TestResultController) TriggerTest(c *gin.Context) {
|
|||
serviceID = &targetID
|
||||
}
|
||||
|
||||
mergedOptions := make(happydns.PluginOptions)
|
||||
|
||||
// Fill opts with default plugin options
|
||||
plugin, err := tc.testPluginUC.GetTestPlugin(pluginName)
|
||||
if err != nil {
|
||||
log.Printf("Warning: unable to get plugin %q for default options: %v", pluginName, err)
|
||||
} else {
|
||||
availableOpts := plugin.AvailableOptions()
|
||||
|
||||
// Collect all option documentation from different scopes
|
||||
allOpts := []happydns.PluginOptionDocumentation{}
|
||||
allOpts = append(allOpts, availableOpts.RunOpts...)
|
||||
allOpts = append(allOpts, availableOpts.ServiceOpts...)
|
||||
allOpts = append(allOpts, availableOpts.DomainOpts...)
|
||||
allOpts = append(allOpts, availableOpts.UserOpts...)
|
||||
allOpts = append(allOpts, availableOpts.AdminOpts...)
|
||||
|
||||
// Fill defaults
|
||||
for _, opt := range allOpts {
|
||||
if opt.Default != nil {
|
||||
mergedOptions[opt.Id] = opt.Default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get merged options from upper levels
|
||||
baseOptions, err := tc.testPluginUC.GetTestPluginOptions(pluginName, &user.Id, domainID, serviceID)
|
||||
mergedOptions, err := tc.testPluginUC.BuildMergedTestPluginOptions(pluginName, &user.Id, domainID, serviceID, options.Options)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Merge request options on top of base options (request options override)
|
||||
if baseOptions != nil {
|
||||
maps.Copy(mergedOptions, *baseOptions)
|
||||
}
|
||||
maps.Copy(mergedOptions, options.Options)
|
||||
|
||||
// Trigger the test via scheduler (returns error if scheduler is disabled)
|
||||
executionID, err := tc.testScheduler.TriggerOnDemandTest(pluginName, tc.scope, targetID, user.Id, mergedOptions)
|
||||
if err != nil {
|
||||
|
|
@ -306,7 +272,7 @@ func (tc *TestResultController) GetTestPluginOptions(c *gin.Context) {
|
|||
serviceID = &targetID
|
||||
}
|
||||
|
||||
opts, err := tc.testPluginUC.GetTestPluginOptions(pluginName, &user.Id, domainID, serviceID)
|
||||
opts, err := tc.testPluginUC.GetStoredTestPluginOptionsNoDefault(pluginName, &user.Id, domainID, serviceID)
|
||||
if err != nil {
|
||||
middleware.ErrorResponse(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ func (app *App) initUsecases() {
|
|||
app.usecases.authUser = authUserService
|
||||
app.usecases.resolver = usecase.NewResolverUsecase(app.cfg)
|
||||
app.usecases.session = sessionService
|
||||
app.usecases.testPlugin = pluginUC.NewTestPluginUsecase(app.cfg, app.plugins, app.store)
|
||||
app.usecases.testPlugin = pluginUC.NewTestPluginUsecase(app.cfg, app.plugins, app.store, app.store)
|
||||
app.usecases.testResult = testresultUC.NewTestResultUsecase(app.store, app.cfg)
|
||||
app.usecases.testSchedule = testresultUC.NewTestScheduleUsecase(app.store, app.cfg, app.store, app.usecases.testPlugin)
|
||||
|
||||
|
|
|
|||
|
|
@ -562,11 +562,26 @@ func (w *worker) executeTest(item *queueItem) {
|
|||
return
|
||||
}
|
||||
|
||||
// Merge options: global defaults < user opts < domain/service opts < schedule opts
|
||||
mergedOptions, err := w.scheduler.scheduleUsecase.PrepareTestOptions(schedule)
|
||||
if err != nil {
|
||||
// Non-fatal: PrepareTestOptions already falls back to schedule-only options
|
||||
log.Printf("Worker %d: warning, could not prepare plugin options for %s: %v\n", w.id, schedule.PluginName, err)
|
||||
// For scheduled tests: merge plugin defaults < stored (user/domain/service) opts < schedule opts < auto-fill.
|
||||
// For on-demand tests the caller has already merged all options, so use them directly.
|
||||
var mergedOptions happydns.PluginOptions
|
||||
if item.execution.ScheduleId != nil {
|
||||
var domainId, serviceId *happydns.Identifier
|
||||
switch schedule.TargetType {
|
||||
case happydns.TestScopeDomain:
|
||||
domainId = &schedule.TargetId
|
||||
case happydns.TestScopeService:
|
||||
serviceId = &schedule.TargetId
|
||||
}
|
||||
var mergeErr error
|
||||
mergedOptions, mergeErr = w.scheduler.pluginUsecase.BuildMergedTestPluginOptions(schedule.PluginName, &schedule.OwnerId, domainId, serviceId, schedule.Options)
|
||||
if mergeErr != nil {
|
||||
// Non-fatal: fall back to schedule-only options
|
||||
log.Printf("Worker %d: warning, could not prepare plugin options for %s: %v\n", w.id, schedule.PluginName, mergeErr)
|
||||
mergedOptions = schedule.Options
|
||||
}
|
||||
} else {
|
||||
mergedOptions = schedule.Options
|
||||
}
|
||||
|
||||
// Prepare metadata
|
||||
|
|
@ -651,3 +666,4 @@ func (w *worker) executeTest(item *queueItem) {
|
|||
log.Printf("Worker %d: Completed test %s for target %s (status: %d, duration: %v)\n",
|
||||
w.id, schedule.PluginName, schedule.TargetId.String(), result.Status, duration)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,3 +44,19 @@ type PluginStorage interface {
|
|||
// ClearPluginConfigurations deletes all Providers present in the database.
|
||||
ClearPluginConfigurations() error
|
||||
}
|
||||
|
||||
// PluginAutoFillStorage provides the domain/zone/user lookups needed to
|
||||
// resolve auto-fill variables for test plugin options.
|
||||
type PluginAutoFillStorage interface {
|
||||
// GetDomain retrieves the Domain with the given identifier.
|
||||
GetDomain(domainid happydns.Identifier) (*happydns.Domain, error)
|
||||
|
||||
// GetUser retrieves the User with the given identifier.
|
||||
GetUser(userid happydns.Identifier) (*happydns.User, error)
|
||||
|
||||
// ListDomains retrieves all Domains associated to the given User.
|
||||
ListDomains(user *happydns.User) ([]*happydns.Domain, error)
|
||||
|
||||
// GetZone retrieves the full Zone (including Services and metadata) for the given identifier.
|
||||
GetZone(zoneid happydns.Identifier) (*happydns.ZoneMessage, error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ package plugin
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"maps"
|
||||
"sort"
|
||||
|
||||
|
|
@ -30,16 +31,18 @@ import (
|
|||
)
|
||||
|
||||
type testPluginUsecase struct {
|
||||
config *happydns.Options
|
||||
manager happydns.PluginManager
|
||||
store PluginStorage
|
||||
config *happydns.Options
|
||||
manager happydns.PluginManager
|
||||
store PluginStorage
|
||||
autoFillStore PluginAutoFillStorage
|
||||
}
|
||||
|
||||
func NewTestPluginUsecase(cfg *happydns.Options, manager happydns.PluginManager, store PluginStorage) happydns.TestPluginUsecase {
|
||||
func NewTestPluginUsecase(cfg *happydns.Options, manager happydns.PluginManager, store PluginStorage, autoFillStore PluginAutoFillStorage) happydns.TestPluginUsecase {
|
||||
return &testPluginUsecase{
|
||||
config: cfg,
|
||||
manager: manager,
|
||||
store: store,
|
||||
config: cfg,
|
||||
manager: manager,
|
||||
store: store,
|
||||
autoFillStore: autoFillStore,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -115,6 +118,156 @@ func (tu *testPluginUsecase) ListTestPlugins() ([]happydns.TestPlugin, error) {
|
|||
return tu.manager.GetTestPlugins(), nil
|
||||
}
|
||||
|
||||
// GetStoredTestPluginOptionsNoDefault returns the stored options (user/domain/service scopes)
|
||||
// with auto-fill variables applied, but without plugin-defined defaults or run-time overrides.
|
||||
func (tu *testPluginUsecase) GetStoredTestPluginOptionsNoDefault(pname string, userid *happydns.Identifier, domainid *happydns.Identifier, serviceid *happydns.Identifier) (happydns.PluginOptions, error) {
|
||||
stored, err := tu.GetTestPluginOptions(pname, userid, domainid, serviceid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var opts happydns.PluginOptions
|
||||
if stored != nil {
|
||||
opts = *stored
|
||||
} else {
|
||||
opts = make(happydns.PluginOptions)
|
||||
}
|
||||
|
||||
plugin, err := tu.GetTestPlugin(pname)
|
||||
if err != nil {
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
return tu.applyAutoFill(plugin, userid, domainid, serviceid, opts), nil
|
||||
}
|
||||
|
||||
// BuildMergedTestPluginOptions merges plugin options from all sources in priority order:
|
||||
// plugin defaults < stored (user/domain/service) options < runOpts < auto-fill variables.
|
||||
func (tu *testPluginUsecase) BuildMergedTestPluginOptions(pname string, userid *happydns.Identifier, domainid *happydns.Identifier, serviceid *happydns.Identifier, runOpts happydns.PluginOptions) (happydns.PluginOptions, error) {
|
||||
merged := make(happydns.PluginOptions)
|
||||
|
||||
// 1. Fill plugin defaults.
|
||||
plugin, err := tu.GetTestPlugin(pname)
|
||||
if err != nil {
|
||||
log.Printf("Warning: unable to get plugin %q for default options: %v", pname, err)
|
||||
} else {
|
||||
availableOpts := plugin.AvailableOptions()
|
||||
allOpts := []happydns.PluginOptionDocumentation{}
|
||||
allOpts = append(allOpts, availableOpts.RunOpts...)
|
||||
allOpts = append(allOpts, availableOpts.ServiceOpts...)
|
||||
allOpts = append(allOpts, availableOpts.DomainOpts...)
|
||||
allOpts = append(allOpts, availableOpts.UserOpts...)
|
||||
allOpts = append(allOpts, availableOpts.AdminOpts...)
|
||||
for _, opt := range allOpts {
|
||||
if opt.Default != nil {
|
||||
merged[opt.Id] = opt.Default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Override with stored options (user/domain/service scopes).
|
||||
baseOptions, err := tu.GetTestPluginOptions(pname, userid, domainid, serviceid)
|
||||
if err != nil {
|
||||
return merged, fmt.Errorf("could not fetch stored plugin options for %s: %w", pname, err)
|
||||
}
|
||||
if baseOptions != nil {
|
||||
maps.Copy(merged, *baseOptions)
|
||||
}
|
||||
|
||||
// 3. Override with caller-supplied run options.
|
||||
maps.Copy(merged, runOpts)
|
||||
|
||||
// 4. Inject auto-fill variables (always win over any user-supplied value).
|
||||
if plugin != nil {
|
||||
merged = tu.applyAutoFill(plugin, userid, domainid, serviceid, merged)
|
||||
}
|
||||
|
||||
return merged, nil
|
||||
}
|
||||
|
||||
// applyAutoFill resolves auto-fill fields declared by the plugin and injects
|
||||
// the context-resolved values into a copy of opts.
|
||||
func (tu *testPluginUsecase) applyAutoFill(
|
||||
plugin happydns.TestPlugin,
|
||||
userid *happydns.Identifier,
|
||||
domainid *happydns.Identifier,
|
||||
serviceid *happydns.Identifier,
|
||||
opts happydns.PluginOptions,
|
||||
) happydns.PluginOptions {
|
||||
allOpts := plugin.AvailableOptions()
|
||||
|
||||
// Collect which auto-fill keys are needed.
|
||||
needed := make(map[string]string) // autoFill constant → field id
|
||||
for _, groups := range [][]happydns.PluginOptionDocumentation{
|
||||
allOpts.RunOpts, allOpts.DomainOpts, allOpts.ServiceOpts,
|
||||
allOpts.UserOpts, allOpts.AdminOpts,
|
||||
} {
|
||||
for _, opt := range groups {
|
||||
if opt.AutoFill != "" {
|
||||
needed[opt.AutoFill] = opt.Id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(needed) == 0 || tu.autoFillStore == nil {
|
||||
return opts
|
||||
}
|
||||
|
||||
autoFillCtx := tu.buildAutoFillContext(userid, domainid, serviceid)
|
||||
|
||||
result := maps.Clone(opts)
|
||||
for autoFillKey, fieldId := range needed {
|
||||
if val, ok := autoFillCtx[autoFillKey]; ok {
|
||||
result[fieldId] = val
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// buildAutoFillContext resolves the available auto-fill values for the given
|
||||
// user/domain/service identifiers.
|
||||
func (tu *testPluginUsecase) buildAutoFillContext(userid *happydns.Identifier, domainid *happydns.Identifier, serviceid *happydns.Identifier) map[string]string {
|
||||
ctx := make(map[string]string)
|
||||
|
||||
if domainid != nil {
|
||||
if domain, err := tu.autoFillStore.GetDomain(*domainid); err == nil {
|
||||
ctx[happydns.AutoFillDomainName] = domain.DomainName
|
||||
}
|
||||
} else if serviceid != nil && userid != nil {
|
||||
// To resolve service context we need to find which domain/zone owns the service.
|
||||
user, err := tu.autoFillStore.GetUser(*userid)
|
||||
if err != nil {
|
||||
return ctx
|
||||
}
|
||||
domains, err := tu.autoFillStore.ListDomains(user)
|
||||
if err != nil {
|
||||
return ctx
|
||||
}
|
||||
for _, domain := range domains {
|
||||
if len(domain.ZoneHistory) == 0 {
|
||||
continue
|
||||
}
|
||||
// The first element in ZoneHistory is the current (most recent) zone.
|
||||
zoneMsg, err := tu.autoFillStore.GetZone(domain.ZoneHistory[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for subdomain, svcs := range zoneMsg.Services {
|
||||
for _, svc := range svcs {
|
||||
if svc.Id.Equals(*serviceid) {
|
||||
ctx[happydns.AutoFillDomainName] = domain.DomainName
|
||||
ctx[happydns.AutoFillSubdomain] = string(subdomain)
|
||||
ctx[happydns.AutoFillServiceType] = svc.Type
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (tu *testPluginUsecase) SetTestPluginOptions(pname string, userid *happydns.Identifier, domainid *happydns.Identifier, serviceid *happydns.Identifier, opts happydns.PluginOptions) error {
|
||||
// filter opts that correspond to the level set
|
||||
plugin, err := tu.GetTestPlugin(pname)
|
||||
|
|
|
|||
|
|
@ -223,39 +223,6 @@ func (u *TestScheduleUsecase) getDefaultInterval(targetType happydns.TestScopeTy
|
|||
}
|
||||
}
|
||||
|
||||
// MergePluginOptions merges plugin options from different scopes
|
||||
// Priority: schedule options > domain options > user options > global options
|
||||
func (u *TestScheduleUsecase) MergePluginOptions(
|
||||
globalOpts happydns.PluginOptions,
|
||||
userOpts happydns.PluginOptions,
|
||||
domainOpts happydns.PluginOptions,
|
||||
scheduleOpts happydns.PluginOptions,
|
||||
) happydns.PluginOptions {
|
||||
merged := make(happydns.PluginOptions)
|
||||
|
||||
// Start with global options
|
||||
for k, v := range globalOpts {
|
||||
merged[k] = v
|
||||
}
|
||||
|
||||
// Override with user options
|
||||
for k, v := range userOpts {
|
||||
merged[k] = v
|
||||
}
|
||||
|
||||
// Override with domain options
|
||||
for k, v := range domainOpts {
|
||||
merged[k] = v
|
||||
}
|
||||
|
||||
// Override with schedule options (highest priority)
|
||||
for k, v := range scheduleOpts {
|
||||
merged[k] = v
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
// ValidateScheduleOwnership checks if a user owns a schedule
|
||||
func (u *TestScheduleUsecase) ValidateScheduleOwnership(scheduleId happydns.Identifier, userId happydns.Identifier) error {
|
||||
schedule, err := u.storage.GetTestSchedule(scheduleId)
|
||||
|
|
@ -450,30 +417,3 @@ func (u *TestScheduleUsecase) DiscoverAndEnsureSchedules() error {
|
|||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// PrepareTestOptions fetches and merges plugin options for a scheduled test execution.
|
||||
// It combines stored options (global/user/domain/service scopes) with the
|
||||
// schedule-specific overrides, returning the final merged options.
|
||||
func (u *TestScheduleUsecase) PrepareTestOptions(schedule *happydns.TestSchedule) (happydns.PluginOptions, error) {
|
||||
if u.pluginUsecase == nil {
|
||||
return schedule.Options, nil
|
||||
}
|
||||
|
||||
var domainId, serviceId *happydns.Identifier
|
||||
switch schedule.TargetType {
|
||||
case happydns.TestScopeDomain:
|
||||
domainId = &schedule.TargetId
|
||||
case happydns.TestScopeService:
|
||||
serviceId = &schedule.TargetId
|
||||
}
|
||||
|
||||
baseOptions, err := u.pluginUsecase.GetTestPluginOptions(schedule.PluginName, &schedule.OwnerId, domainId, serviceId)
|
||||
if err != nil {
|
||||
// Non-fatal: fall back to schedule-only options and surface as a warning
|
||||
return schedule.Options, fmt.Errorf("could not fetch plugin options for %s: %w", schedule.PluginName, err)
|
||||
}
|
||||
|
||||
if baseOptions != nil {
|
||||
return u.MergePluginOptions(nil, nil, *baseOptions, schedule.Options), nil
|
||||
}
|
||||
return schedule.Options, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,11 @@ type Field struct {
|
|||
|
||||
// Description stores an helpfull sentence describing the field.
|
||||
Description string `json:"description,omitempty"`
|
||||
|
||||
// AutoFill indicates the field value is automatically resolved by the
|
||||
// software based on test context. When set, the value should not be
|
||||
// entered by the user.
|
||||
AutoFill string `json:"autoFill,omitempty"`
|
||||
}
|
||||
|
||||
type FormState struct {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,22 @@
|
|||
|
||||
package happydns
|
||||
|
||||
// Auto-fill variable identifiers for plugin option fields.
|
||||
const (
|
||||
// AutoFillDomainName fills the option with the fully qualified domain name
|
||||
// of the domain being tested (e.g. "example.com.").
|
||||
AutoFillDomainName = "domain_name"
|
||||
|
||||
// AutoFillSubdomain fills the option with the subdomain relative to the zone
|
||||
// (e.g. "www" for "www.example.com." in zone "example.com."). Only
|
||||
// applicable for service-scoped tests.
|
||||
AutoFillSubdomain = "subdomain"
|
||||
|
||||
// AutoFillServiceType fills the option with the service type identifier
|
||||
// (e.g. "abstract.MatrixIM"). Only applicable for service-scoped tests.
|
||||
AutoFillServiceType = "service_type"
|
||||
)
|
||||
|
||||
const (
|
||||
PluginResultStatusKO PluginResultStatus = iota
|
||||
PluginResultStatusWarn
|
||||
|
|
@ -93,6 +109,8 @@ type PluginManager interface {
|
|||
}
|
||||
|
||||
type TestPluginUsecase interface {
|
||||
BuildMergedTestPluginOptions(string, *Identifier, *Identifier, *Identifier, PluginOptions) (PluginOptions, error)
|
||||
GetStoredTestPluginOptionsNoDefault(string, *Identifier, *Identifier, *Identifier) (PluginOptions, error)
|
||||
GetTestPlugin(string) (TestPlugin, error)
|
||||
GetTestPluginOptions(string, *Identifier, *Identifier, *Identifier) (*PluginOptions, error)
|
||||
ListTestPlugins() ([]TestPlugin, error)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,18 @@
|
|||
|
||||
import { t } from "$lib/translations";
|
||||
|
||||
const AUTO_FILL_KEYS: Record<string, string> = {
|
||||
domain_name: "plugins.tests.auto-fill.domain_name",
|
||||
subdomain: "plugins.tests.auto-fill.subdomain",
|
||||
service_type: "plugins.tests.auto-fill.service_type",
|
||||
};
|
||||
|
||||
function getAutoFillLabel(autoFill: string): string {
|
||||
const tKey = AUTO_FILL_KEYS[autoFill];
|
||||
if (tKey) return $t(tKey);
|
||||
return $t("plugins.tests.auto-fill.generic", { key: autoFill });
|
||||
}
|
||||
|
||||
interface OptionDef {
|
||||
id?: string;
|
||||
label?: string;
|
||||
|
|
@ -34,6 +46,7 @@
|
|||
placeholder?: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
autoFill?: string;
|
||||
}
|
||||
|
||||
interface OptionGroup {
|
||||
|
|
@ -63,10 +76,16 @@
|
|||
{optDoc.label || optDoc.id}:
|
||||
</dt>
|
||||
<dd class="col-sm-8">
|
||||
{#if optDoc.default}
|
||||
<span class="text-muted d-block">{optDoc.default}</span>
|
||||
{:else if optDoc.placeholder}
|
||||
<em class="text-muted d-block">{optDoc.placeholder}</em>
|
||||
{#if optDoc.autoFill}
|
||||
<span class="badge bg-info me-1"
|
||||
>{getAutoFillLabel(optDoc.autoFill)}</span
|
||||
>
|
||||
{:else}
|
||||
{#if optDoc.default}
|
||||
<span class="text-muted d-block">{optDoc.default}</span>
|
||||
{:else if optDoc.placeholder}
|
||||
<em class="text-muted d-block">{optDoc.placeholder}</em>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if optDoc.description}
|
||||
<small class="text-muted d-block">{optDoc.description}</small>
|
||||
|
|
@ -76,7 +95,7 @@
|
|||
type: optDoc.type || "string",
|
||||
})}</small
|
||||
>
|
||||
{#if optDoc.required}
|
||||
{#if optDoc.required && !optDoc.autoFill}
|
||||
<small class="text-danger ms-2"
|
||||
>{$t("plugins.tests.option-groups.required")}</small
|
||||
>
|
||||
|
|
|
|||
|
|
@ -718,6 +718,12 @@
|
|||
"type": "Type: {{type}}",
|
||||
"required": "Required"
|
||||
},
|
||||
"auto-fill": {
|
||||
"domain_name": "auto-filled: domain name",
|
||||
"subdomain": "auto-filled: subdomain",
|
||||
"service_type": "auto-filled: service type",
|
||||
"generic": "auto-filled: {{key}}"
|
||||
},
|
||||
"messages": {
|
||||
"options-updated": "Plugin options updated successfully",
|
||||
"options-cleaned": "Orphaned options removed successfully",
|
||||
|
|
|
|||
|
|
@ -547,6 +547,12 @@
|
|||
"type": "Type : {{type}}",
|
||||
"required": "Requis"
|
||||
},
|
||||
"auto-fill": {
|
||||
"domain_name": "rempli automatiquement : nom de domaine",
|
||||
"subdomain": "rempli automatiquement : sous-domaine",
|
||||
"service_type": "rempli automatiquement : type de service",
|
||||
"generic": "rempli automatiquement : {{key}}"
|
||||
},
|
||||
"messages": {
|
||||
"options-updated": "Options du plugin mises à jour avec succès",
|
||||
"options-cleaned": "Options orphelines supprimées avec succès",
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export class Field {
|
|||
required? = $state<boolean>();
|
||||
secret? = $state<boolean>();
|
||||
textarea? = $state<boolean>();
|
||||
autoFill? = $state<string>();
|
||||
}
|
||||
|
||||
export class CustomForm {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue