Added the rel-picker which can quickly goto related tables
* New rel-picker that can be opened using Shift+O and allows for quickly going to related tables.
This commit is contained in:
parent
12909c89ee
commit
5d95d44a97
|
@ -4,6 +4,10 @@ import (
|
|||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
@ -30,9 +34,6 @@ import (
|
|||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles"
|
||||
bus "github.com/lmika/events"
|
||||
"github.com/lmika/gopkgs/cli"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -118,6 +119,7 @@ func main() {
|
|||
inputHistoryService,
|
||||
eventBus,
|
||||
pasteboardProvider,
|
||||
scriptManagerService,
|
||||
*flagTable,
|
||||
)
|
||||
tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore)
|
||||
|
@ -125,7 +127,7 @@ func main() {
|
|||
exportController := controllers.NewExportController(state, tableService, jobsController, columnsController, pasteboardProvider)
|
||||
settingsController := controllers.NewSettingsController(settingStore, eventBus)
|
||||
keyBindings := keybindings.Default()
|
||||
scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, settingsController, eventBus)
|
||||
scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, jobsController, settingsController, eventBus)
|
||||
|
||||
if *flagQuery != "" {
|
||||
if *flagTable == "" {
|
||||
|
|
|
@ -2,10 +2,12 @@ package controllers
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
|
||||
)
|
||||
|
||||
type SetTableItemView struct {
|
||||
|
@ -89,3 +91,9 @@ func (rs ResultSetUpdated) StatusMessage() string {
|
|||
|
||||
type ShowColumnOverlay struct{}
|
||||
type HideColumnOverlay struct{}
|
||||
|
||||
type ShowRelatedItemsOverlay struct {
|
||||
Items []relitems.RelatedItem
|
||||
OnSelected func(item relitems.RelatedItem) tea.Msg
|
||||
}
|
||||
type HideRelatedItemsOverlay struct{}
|
||||
|
|
|
@ -2,10 +2,12 @@ package controllers
|
|||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"io/fs"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
|
||||
)
|
||||
|
||||
type TableReadService interface {
|
||||
|
@ -33,3 +35,7 @@ type CustomKeyBindingSource interface {
|
|||
UnbindKey(key string)
|
||||
Rebind(bindingName string, newKey string) error
|
||||
}
|
||||
|
||||
type RelatedItemSupplier interface {
|
||||
RelatedItemOfItem(context.Context, *models.ResultSet, int) ([]relitems.RelatedItem, error)
|
||||
}
|
||||
|
|
|
@ -3,21 +3,24 @@ package controllers
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
|
||||
bus "github.com/lmika/events"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ScriptController struct {
|
||||
scriptManager *scriptmanager.Service
|
||||
tableReadController *TableReadController
|
||||
jobController *JobsController
|
||||
settingsController *SettingsController
|
||||
eventBus *bus.Bus
|
||||
sendMsg func(msg tea.Msg)
|
||||
|
@ -26,12 +29,14 @@ type ScriptController struct {
|
|||
func NewScriptController(
|
||||
scriptManager *scriptmanager.Service,
|
||||
tableReadController *TableReadController,
|
||||
jobController *JobsController,
|
||||
settingsController *SettingsController,
|
||||
eventBus *bus.Bus,
|
||||
) *ScriptController {
|
||||
sc := &ScriptController{
|
||||
scriptManager: scriptManager,
|
||||
tableReadController: tableReadController,
|
||||
jobController: jobController,
|
||||
settingsController: settingsController,
|
||||
eventBus: eventBus,
|
||||
}
|
||||
|
@ -162,7 +167,6 @@ func (s *sessionImpl) SetResultSet(ctx context.Context, newResultSet *models.Res
|
|||
}
|
||||
|
||||
func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanager.QueryOptions) (*models.ResultSet, error) {
|
||||
|
||||
// Parse the query
|
||||
expr, err := queryexpr.Parse(query)
|
||||
if err != nil {
|
||||
|
@ -179,11 +183,18 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage
|
|||
expr = expr.WithIndex(opts.IndexName)
|
||||
}
|
||||
|
||||
return s.sc.doQuery(ctx, expr, opts)
|
||||
}
|
||||
|
||||
func (s *ScriptController) doQuery(ctx context.Context, expr *queryexpr.QueryExpr, opts scriptmanager.QueryOptions) (*models.ResultSet, error) {
|
||||
// Get the table info
|
||||
var tableInfo *models.TableInfo
|
||||
var (
|
||||
tableInfo *models.TableInfo
|
||||
err error
|
||||
)
|
||||
|
||||
tableName := opts.TableName
|
||||
currentResultSet := s.sc.tableReadController.state.ResultSet()
|
||||
currentResultSet := s.tableReadController.state.ResultSet()
|
||||
|
||||
if tableName != "" {
|
||||
// Table specified. If it's the same as the current table, then use the existing table info
|
||||
|
@ -192,7 +203,7 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage
|
|||
}
|
||||
|
||||
// Otherwise, describe the table
|
||||
tableInfo, err = s.sc.tableReadController.tableService.Describe(ctx, tableName)
|
||||
tableInfo, err = s.tableReadController.tableService.Describe(ctx, tableName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "cannot describe table '%v'", tableName)
|
||||
}
|
||||
|
@ -204,7 +215,7 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage
|
|||
tableInfo = currentResultSet.TableInfo
|
||||
}
|
||||
|
||||
newResultSet, err := s.sc.tableReadController.tableService.ScanOrQuery(ctx, tableInfo, expr, nil)
|
||||
newResultSet, err := s.tableReadController.tableService.ScanOrQuery(ctx, tableInfo, expr, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -240,3 +251,31 @@ func (sc *ScriptController) LookupBinding(theKey string) string {
|
|||
func (sc *ScriptController) UnbindKey(key string) {
|
||||
sc.scriptManager.UnbindKey(key)
|
||||
}
|
||||
|
||||
func (c *ScriptController) LookupRelatedItems(idx int) (res tea.Msg) {
|
||||
rs := c.tableReadController.state.ResultSet()
|
||||
|
||||
relItems, err := c.scriptManager.RelatedItemOfItem(context.Background(), rs, idx)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
} else if len(relItems) == 0 {
|
||||
return events.StatusMsg("No related items available")
|
||||
}
|
||||
|
||||
return ShowRelatedItemsOverlay{
|
||||
Items: relItems,
|
||||
OnSelected: func(item relitems.RelatedItem) tea.Msg {
|
||||
if item.OnSelect != nil {
|
||||
return item.OnSelect()
|
||||
}
|
||||
|
||||
return NewJob(c.jobController, "Running query…", func(ctx context.Context) (*models.ResultSet, error) {
|
||||
return c.doQuery(ctx, item.Query, scriptmanager.QueryOptions{
|
||||
TableName: item.Table,
|
||||
})
|
||||
}).OnDone(func(rs *models.ResultSet) tea.Msg {
|
||||
return c.tableReadController.setResultSetAndFilter(rs, "", true, resultSetUpdateQuery)
|
||||
}).Submit()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ type TableReadController struct {
|
|||
tableName string
|
||||
loadFromLastView bool
|
||||
pasteboardProvider services.PasteboardProvider
|
||||
relatedItemSupplier RelatedItemSupplier
|
||||
|
||||
// state
|
||||
mutex *sync.Mutex
|
||||
|
@ -75,6 +76,7 @@ func NewTableReadController(
|
|||
inputHistoryService *inputhistory.Service,
|
||||
eventBus *bus.Bus,
|
||||
pasteboardProvider services.PasteboardProvider,
|
||||
relatedItemSupplier RelatedItemSupplier,
|
||||
tableName string,
|
||||
) *TableReadController {
|
||||
return &TableReadController{
|
||||
|
@ -87,6 +89,7 @@ func NewTableReadController(
|
|||
eventBus: eventBus,
|
||||
tableName: tableName,
|
||||
pasteboardProvider: pasteboardProvider,
|
||||
relatedItemSupplier: relatedItemSupplier,
|
||||
mutex: new(sync.Mutex),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -627,13 +627,14 @@ func newService(t *testing.T, cfg serviceConfig) *services {
|
|||
inputHistoryService,
|
||||
eventBus,
|
||||
pasteboardprovider.NilProvider{},
|
||||
nil,
|
||||
cfg.tableName,
|
||||
)
|
||||
writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore)
|
||||
settingsController := controllers.NewSettingsController(settingStore, eventBus)
|
||||
columnsController := controllers.NewColumnsController(eventBus)
|
||||
exportController := controllers.NewExportController(state, service, jobsController, columnsController, pasteboardprovider.NilProvider{})
|
||||
scriptController := controllers.NewScriptController(scriptService, readController, settingsController, eventBus)
|
||||
scriptController := controllers.NewScriptController(scriptService, readController, jobsController, settingsController, eventBus)
|
||||
|
||||
commandController := commandctrl.NewCommandController(inputHistoryService)
|
||||
commandController.AddCommandLookupExtension(scriptController)
|
||||
|
|
12
internal/dynamo-browse/models/relitems/relitem.go
Normal file
12
internal/dynamo-browse/models/relitems/relitem.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package relitems
|
||||
|
||||
import (
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||
)
|
||||
|
||||
type RelatedItem struct {
|
||||
Name string
|
||||
Table string
|
||||
Query *queryexpr.QueryExpr
|
||||
OnSelect func() error
|
||||
}
|
|
@ -8,8 +8,10 @@ package scriptmanager
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/risor-io/risor/object"
|
||||
"log"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/risor-io/risor/object"
|
||||
)
|
||||
|
||||
func printBuiltin(ctx context.Context, args ...object.Object) object.Object {
|
||||
|
@ -70,3 +72,31 @@ func require(funcName string, count int, args []object.Object) *object.Error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func bindArgs(funcName string, args []object.Object, bindArgs ...any) *object.Error {
|
||||
if err := require(funcName, len(bindArgs), args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, bindArg := range bindArgs {
|
||||
switch t := bindArg.(type) {
|
||||
case *string:
|
||||
str, err := object.AsString(args[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*t = str
|
||||
case **object.Function:
|
||||
fnRes, isFnRes := args[i].(*object.Function)
|
||||
if !isFnRes {
|
||||
return object.NewError(errors.Errorf("expected arg %v to be a function, was %T", i, bindArg))
|
||||
}
|
||||
|
||||
*t = fnRes
|
||||
default:
|
||||
return object.NewError(errors.Errorf("unhandled arg type %v", i))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -3,9 +3,13 @@ package scriptmanager
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/risor-io/risor/object"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -20,6 +24,7 @@ func (m *extModule) register() *object.Module {
|
|||
return object.NewBuiltinsModule("ext", map[string]object.Object{
|
||||
"command": object.NewBuiltin("command", m.command),
|
||||
"key_binding": object.NewBuiltin("key_binding", m.keyBinding),
|
||||
"related_items": object.NewBuiltin("related_items", m.relatedItem),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -136,3 +141,130 @@ func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) objec
|
|||
m.scriptPlugin.keyToKeyBinding[defaultKey] = fullBindingName
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *extModule) relatedItem(ctx context.Context, args ...object.Object) object.Object {
|
||||
thisEnv := scriptEnvFromCtx(ctx)
|
||||
|
||||
var (
|
||||
tableName string
|
||||
callbackFn *object.Function
|
||||
)
|
||||
if err := bindArgs("ext.related_items", args, &tableName, &callbackFn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
callFn, hasCallFn := object.GetCallFunc(ctx)
|
||||
if !hasCallFn {
|
||||
return object.NewError(errors.New("no callFn found in context"))
|
||||
}
|
||||
|
||||
newHandler := func(ctx context.Context, rs *models.ResultSet, index int) ([]relatedItem, error) {
|
||||
newEnv := thisEnv
|
||||
ctx = ctxWithScriptEnv(ctx, newEnv)
|
||||
|
||||
res, err := callFn(ctx, callbackFn, []object.Object{
|
||||
newItemProxy(newResultSetProxy(rs), index),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("script error '%v':related_item - %v", m.scriptPlugin.name, err)
|
||||
} else if object.IsError(res) {
|
||||
errObj := res.(*object.Error)
|
||||
return nil, errors.Errorf("script error '%v':related_item - %v", m.scriptPlugin.name, errObj.Inspect())
|
||||
}
|
||||
|
||||
itr, objErr := object.AsIterator(res)
|
||||
if err != nil {
|
||||
return nil, objErr.Value()
|
||||
}
|
||||
|
||||
var relItems []relatedItem
|
||||
for next, hasNext := itr.Next(ctx); hasNext; next, hasNext = itr.Next(ctx) {
|
||||
var newRelItem relatedItem
|
||||
|
||||
itemMap, objErr := object.AsMap(next)
|
||||
if err != nil {
|
||||
return nil, objErr.Value()
|
||||
}
|
||||
|
||||
labelName, objErr := object.AsString(itemMap.Get("label"))
|
||||
if objErr != nil {
|
||||
continue
|
||||
}
|
||||
newRelItem.label = labelName
|
||||
|
||||
var tableStr = ""
|
||||
if itemMap.Get("table") != object.Nil {
|
||||
tableStr, objErr = object.AsString(itemMap.Get("table"))
|
||||
if objErr != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
newRelItem.table = tableStr
|
||||
|
||||
if selectFn, ok := itemMap.Get("on_select").(*object.Function); ok {
|
||||
newRelItem.onSelect = func() error {
|
||||
thisNewEnv := thisEnv
|
||||
ctx = ctxWithScriptEnv(ctx, thisNewEnv)
|
||||
|
||||
res, err := callFn(ctx, selectFn, []object.Object{})
|
||||
if err != nil {
|
||||
return errors.Errorf("rel error '%v' - %v", m.scriptPlugin.name, err)
|
||||
} else if object.IsError(res) {
|
||||
errObj := res.(*object.Error)
|
||||
return errors.Errorf("rel error '%v' - %v", m.scriptPlugin.name, errObj.Inspect())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
queryExprStr, objErr := object.AsString(itemMap.Get("query"))
|
||||
if objErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
query, err := queryexpr.Parse(queryExprStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Placeholders
|
||||
if argsVal, isArgsValMap := object.AsMap(itemMap.Get("args")); isArgsValMap == nil {
|
||||
namePlaceholders := make(map[string]string)
|
||||
valuePlaceholders := make(map[string]types.AttributeValue)
|
||||
|
||||
for k, val := range argsVal.Value() {
|
||||
switch v := val.(type) {
|
||||
case *object.String:
|
||||
namePlaceholders[k] = v.Value()
|
||||
valuePlaceholders[k] = &types.AttributeValueMemberS{Value: v.Value()}
|
||||
case *object.Int:
|
||||
valuePlaceholders[k] = &types.AttributeValueMemberN{Value: fmt.Sprint(v.Value())}
|
||||
case *object.Float:
|
||||
valuePlaceholders[k] = &types.AttributeValueMemberN{Value: fmt.Sprint(v.Value())}
|
||||
case *object.Bool:
|
||||
valuePlaceholders[k] = &types.AttributeValueMemberBOOL{Value: v.Value()}
|
||||
case *object.NilType:
|
||||
valuePlaceholders[k] = &types.AttributeValueMemberNULL{Value: true}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
query = query.WithNameParams(namePlaceholders).WithValueParams(valuePlaceholders)
|
||||
}
|
||||
newRelItem.query = query
|
||||
}
|
||||
|
||||
relItems = append(relItems, newRelItem)
|
||||
}
|
||||
|
||||
return relItems, nil
|
||||
}
|
||||
|
||||
m.scriptPlugin.relatedItems = append(m.scriptPlugin.relatedItems, &relatedItemBuilder{
|
||||
table: tableName,
|
||||
itemProduction: newHandler,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
151
internal/dynamo-browse/services/scriptmanager/modext_test.go
Normal file
151
internal/dynamo-browse/services/scriptmanager/modext_test.go
Normal file
|
@ -0,0 +1,151 @@
|
|||
package scriptmanager_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExtModule_RelatedItems(t *testing.T) {
|
||||
t.Run("should register a function which will return related items for an item", func(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
desc string
|
||||
code string
|
||||
}{
|
||||
{
|
||||
desc: "single function, table name match",
|
||||
code: `
|
||||
ext.related_items("test-table", func(item) {
|
||||
print("Hello")
|
||||
return [
|
||||
{"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}},
|
||||
{"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}},
|
||||
]
|
||||
})
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "single function, table prefix match",
|
||||
code: `
|
||||
ext.related_items("test-*", func(item) {
|
||||
print("Hello")
|
||||
return [
|
||||
{"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}},
|
||||
{"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}},
|
||||
]
|
||||
})
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "multi function, table name match",
|
||||
code: `
|
||||
ext.related_items("test-table", func(item) {
|
||||
print("Hello")
|
||||
return [
|
||||
{"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}},
|
||||
]
|
||||
})
|
||||
|
||||
ext.related_items("test-table", func(item) {
|
||||
return [
|
||||
{"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}},
|
||||
]
|
||||
})
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "multi function, table name prefix",
|
||||
code: `
|
||||
ext.related_items("test-*", func(item) {
|
||||
print("Hello")
|
||||
return [
|
||||
{"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}},
|
||||
]
|
||||
})
|
||||
|
||||
ext.related_items("test-*", func(item) {
|
||||
return [
|
||||
{"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}},
|
||||
]
|
||||
})
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.desc, func(t *testing.T) {
|
||||
// Load the script
|
||||
srv := scriptmanager.New(scriptmanager.WithFS(testScriptFile(t, "test.tm", scenario.code)))
|
||||
|
||||
ctx := context.Background()
|
||||
plugin, err := srv.LoadScript(ctx, "test.tm")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, plugin)
|
||||
|
||||
// Get related items of result set
|
||||
rs := &models.ResultSet{
|
||||
TableInfo: &models.TableInfo{
|
||||
Name: "test-table",
|
||||
},
|
||||
}
|
||||
rs.SetItems([]models.Item{
|
||||
{"pk": &types.AttributeValueMemberS{Value: "abc"}},
|
||||
{"pk": &types.AttributeValueMemberS{Value: "1232"}},
|
||||
})
|
||||
|
||||
relItems, err := srv.RelatedItemOfItem(context.Background(), rs, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, relItems, 2)
|
||||
|
||||
assert.Equal(t, "Customer", relItems[0].Name)
|
||||
assert.Equal(t, "pk=$foo", relItems[0].Query.String())
|
||||
assert.Equal(t, "foo", relItems[0].Query.ValueParamOrNil("foo").(*types.AttributeValueMemberS).Value)
|
||||
|
||||
assert.Equal(t, "Payment", relItems[1].Name)
|
||||
assert.Equal(t, "fla=$daa", relItems[1].Query.String())
|
||||
assert.Equal(t, "Hello", relItems[1].Query.ValueParamOrNil("daa").(*types.AttributeValueMemberS).Value)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should support rel_items with on select", func(t *testing.T) {
|
||||
// Load the script
|
||||
srv := scriptmanager.New(scriptmanager.WithFS(testScriptFile(t, "test.tm", `
|
||||
ext.related_items("test-table", func(item) {
|
||||
print("Hello")
|
||||
return [
|
||||
{"label": "Customer", "on_select": func() {
|
||||
print("Selected")
|
||||
}},
|
||||
]
|
||||
})
|
||||
`)))
|
||||
|
||||
ctx := context.Background()
|
||||
plugin, err := srv.LoadScript(ctx, "test.tm")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, plugin)
|
||||
|
||||
// Get related items of result set
|
||||
rs := &models.ResultSet{
|
||||
TableInfo: &models.TableInfo{
|
||||
Name: "test-table",
|
||||
},
|
||||
}
|
||||
rs.SetItems([]models.Item{
|
||||
{"pk": &types.AttributeValueMemberS{Value: "abc"}},
|
||||
{"pk": &types.AttributeValueMemberS{Value: "1232"}},
|
||||
})
|
||||
|
||||
relItems, err := srv.RelatedItemOfItem(context.Background(), rs, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, relItems, 1)
|
||||
|
||||
assert.Equal(t, "Customer", relItems[0].Name)
|
||||
assert.NoError(t, relItems[0].OnSelect())
|
||||
})
|
||||
}
|
57
internal/dynamo-browse/services/scriptmanager/relitem.go
Normal file
57
internal/dynamo-browse/services/scriptmanager/relitem.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package scriptmanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"path"
|
||||
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
|
||||
)
|
||||
|
||||
type relatedItem struct {
|
||||
label string
|
||||
table string
|
||||
query *queryexpr.QueryExpr
|
||||
onSelect func() error
|
||||
}
|
||||
|
||||
type relatedItemBuilder struct {
|
||||
table string
|
||||
itemProduction func(ctx context.Context, rs *models.ResultSet, index int) ([]relatedItem, error)
|
||||
}
|
||||
|
||||
func (s *Service) RelatedItemOfItem(ctx context.Context, rs *models.ResultSet, index int) ([]relitems.RelatedItem, error) {
|
||||
riModels := []relitems.RelatedItem{}
|
||||
|
||||
for _, plugin := range s.plugins {
|
||||
for _, rb := range plugin.relatedItems {
|
||||
// TODO: should support matching
|
||||
match, _ := tableMatchesGlob(rb.table, rs.TableInfo.Name)
|
||||
log.Printf("RelatedItemOfItem: table = '%v', pattern = '%v', match = '%v'", rb.table, rs.TableInfo.Name, match)
|
||||
if match {
|
||||
relatedItems, err := rb.itemProduction(ctx, rs, index)
|
||||
if err != nil {
|
||||
// TODO: should probably return error if no rel items were found and an error was raised
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: make this nicer
|
||||
for _, ri := range relatedItems {
|
||||
riModels = append(riModels, relitems.RelatedItem{
|
||||
Name: ri.label,
|
||||
Query: ri.query,
|
||||
Table: ri.table,
|
||||
OnSelect: ri.onSelect,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return riModels, nil
|
||||
}
|
||||
|
||||
func tableMatchesGlob(tableName, pattern string) (bool, error) {
|
||||
return path.Match(tableName, pattern)
|
||||
}
|
|
@ -17,6 +17,10 @@ type resultSetProxy struct {
|
|||
resultSet *models.ResultSet
|
||||
}
|
||||
|
||||
func newResultSetProxy(rs *models.ResultSet) *resultSetProxy {
|
||||
return &resultSetProxy{resultSet: rs}
|
||||
}
|
||||
|
||||
func (r *resultSetProxy) SetAttr(name string, value object.Object) error {
|
||||
return errors.Errorf("attribute error: %v", name)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,10 @@ import (
|
|||
"github.com/risor-io/risor/object"
|
||||
)
|
||||
|
||||
var (
|
||||
relPrefix = "." + string(filepath.Separator)
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
lookupPaths []fs.FS
|
||||
ifaces Ifaces
|
||||
|
@ -150,7 +154,7 @@ func (s *Service) readScript(filename string, allowCwd bool) (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(filename, string(filepath.Separator)) {
|
||||
if strings.HasPrefix(filename, string(filepath.Separator)) || strings.HasPrefix(filename, relPrefix) {
|
||||
code, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package scriptmanager
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type ScriptPlugin struct {
|
||||
scriptService *Service
|
||||
|
@ -8,6 +10,7 @@ type ScriptPlugin struct {
|
|||
definedCommands map[string]*Command
|
||||
definedKeyBindings map[string]*Command
|
||||
keyToKeyBinding map[string]string
|
||||
relatedItems []*relatedItemBuilder
|
||||
}
|
||||
|
||||
func (sp *ScriptPlugin) Name() string {
|
||||
|
@ -26,3 +29,7 @@ func (c *Command) Invoke(ctx context.Context, args []string, errChan chan error)
|
|||
errChan <- c.cmdFn(ctx, args)
|
||||
})
|
||||
}
|
||||
|
||||
//func (c *Command) LookupRelevantItems(ctx context.Context, table *models.TableInfo, item *models.Item) error {
|
||||
//
|
||||
//}
|
||||
|
|
|
@ -37,6 +37,7 @@ func Default() *KeyBindings {
|
|||
CycleLayoutBackwards: key.NewBinding(key.WithKeys("W"), key.WithHelp("W", "cycle layout backward")),
|
||||
PromptForCommand: key.NewBinding(key.WithKeys(":"), key.WithHelp(":", "prompt for command")),
|
||||
ShowColumnOverlay: key.NewBinding(key.WithKeys("f"), key.WithHelp("f", "show column overlay")),
|
||||
ShowRelItemsOverlay: key.NewBinding(key.WithKeys("O"), key.WithHelp("O", "show related items overlay")),
|
||||
CancelRunningJob: key.NewBinding(key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "cancel running job or quit")),
|
||||
Quit: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")),
|
||||
},
|
||||
|
|
|
@ -45,6 +45,7 @@ type ViewKeyBindings struct {
|
|||
CycleLayoutBackwards key.Binding `keymap:"cycle-layout-backwards"`
|
||||
PromptForCommand key.Binding `keymap:"prompt-for-command"`
|
||||
ShowColumnOverlay key.Binding `keymap:"show-fields-popup"`
|
||||
ShowRelItemsOverlay key.Binding `keymap:"show-rel-items-popup"`
|
||||
CancelRunningJob key.Binding `keymap:"cancel-running-job"`
|
||||
Quit key.Binding `keymap:"quit"`
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
|
||||
|
@ -16,15 +20,13 @@ import (
|
|||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/dynamoitemview"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/dynamotableview"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/relselector"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/statusandprompt"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/tableselect"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils"
|
||||
bus "github.com/lmika/events"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -48,6 +50,7 @@ type Model struct {
|
|||
scriptController *controllers.ScriptController
|
||||
jobController *controllers.JobsController
|
||||
colSelector *colselector.Model
|
||||
relSelector *relselector.Model
|
||||
itemEdit *dynamoitemedit.Model
|
||||
statusAndPrompt *statusandprompt.StatusAndPrompt
|
||||
tableSelect *tableselect.Model
|
||||
|
@ -85,7 +88,8 @@ func NewModel(
|
|||
mainView := layout.NewVBox(layout.LastChildFixedAt(14), dtv, div)
|
||||
|
||||
colSelector := colselector.New(mainView, defaultKeyMap, columnsController)
|
||||
itemEdit := dynamoitemedit.NewModel(colSelector)
|
||||
relSelector := relselector.New(colSelector)
|
||||
itemEdit := dynamoitemedit.NewModel(relSelector)
|
||||
statusAndPrompt := statusandprompt.New(itemEdit, pasteboardProvider, "", uiStyles.StatusAndPrompt)
|
||||
dialogPrompt := dialogprompt.New(statusAndPrompt)
|
||||
tableSelect := tableselect.New(dialogPrompt, uiStyles)
|
||||
|
@ -244,6 +248,7 @@ func NewModel(
|
|||
jobController: jobController,
|
||||
itemEdit: itemEdit,
|
||||
colSelector: colSelector,
|
||||
relSelector: relSelector,
|
||||
statusAndPrompt: statusAndPrompt,
|
||||
tableSelect: tableSelect,
|
||||
root: root,
|
||||
|
@ -267,7 +272,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
)
|
||||
case tea.KeyMsg:
|
||||
// TODO: use modes here
|
||||
if !m.statusAndPrompt.InPrompt() && !m.tableSelect.Visible() && !m.colSelector.ColSelectorVisible() {
|
||||
if !m.statusAndPrompt.InPrompt() && !m.tableSelect.Visible() && !m.colSelector.ColSelectorVisible() && !m.relSelector.SelectorVisible() {
|
||||
switch {
|
||||
case key.Matches(msg, m.keyMap.Mark):
|
||||
if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
|
||||
|
@ -302,6 +307,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
// return m, nil
|
||||
case key.Matches(msg, m.keyMap.ShowColumnOverlay):
|
||||
return m, events.SetTeaMessage(controllers.ShowColumnOverlay{})
|
||||
case key.Matches(msg, m.keyMap.ShowRelItemsOverlay):
|
||||
if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
|
||||
return m, events.SetTeaMessage(m.scriptController.LookupRelatedItems(idx))
|
||||
}
|
||||
case key.Matches(msg, m.keyMap.PromptForCommand):
|
||||
return m, m.commandController.Prompt
|
||||
case key.Matches(msg, m.keyMap.PromptForTable):
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
|
||||
table "github.com/lmika/go-bubble-table"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -49,7 +48,6 @@ func (m *colListModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
switch msg := msg.(type) {
|
||||
case controllers.SetSelectedColumnInColSelector:
|
||||
// HACK: this needs to work for all cases
|
||||
log.Printf("%d == %d?", int(msg), m.table.Cursor()+1)
|
||||
if int(msg) == m.table.Cursor()+1 {
|
||||
m.table.GoDown()
|
||||
}
|
||||
|
|
17
internal/dynamo-browse/ui/teamodels/relselector/itemmdl.go
Normal file
17
internal/dynamo-browse/ui/teamodels/relselector/itemmdl.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package relselector
|
||||
|
||||
type relItemModel struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (ti relItemModel) FilterValue() string {
|
||||
return ti.name
|
||||
}
|
||||
|
||||
func (ti relItemModel) Title() string {
|
||||
return ti.name
|
||||
}
|
||||
|
||||
func (ti relItemModel) Description() string {
|
||||
return ti.name
|
||||
}
|
132
internal/dynamo-browse/ui/teamodels/relselector/listmdl.go
Normal file
132
internal/dynamo-browse/ui/teamodels/relselector/listmdl.go
Normal file
|
@ -0,0 +1,132 @@
|
|||
package relselector
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
frameColor = lipgloss.Color("63")
|
||||
|
||||
frameStyle = lipgloss.NewStyle().
|
||||
Foreground(frameColor)
|
||||
style = lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(frameColor)
|
||||
|
||||
keyEsc = key.NewBinding(key.WithKeys(tea.KeyEsc.String()))
|
||||
keyEnter = key.NewBinding(key.WithKeys(tea.KeyEnter.String()))
|
||||
)
|
||||
|
||||
type listModel struct {
|
||||
event controllers.ShowRelatedItemsOverlay
|
||||
list list.Model
|
||||
height int
|
||||
}
|
||||
|
||||
func newListModel() *listModel {
|
||||
items := []list.Item{}
|
||||
|
||||
delegate := list.NewDefaultDelegate()
|
||||
delegate.ShowDescription = false
|
||||
delegate.Styles.SelectedTitle = lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||
BorderForeground(lipgloss.Color("#2c5fb7")).
|
||||
Foreground(lipgloss.Color("#2c5fb7")).
|
||||
Padding(0, 0, 0, 1)
|
||||
delegate.Styles.SelectedDesc = lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||
BorderForeground(lipgloss.Color("#2c5fb7")).
|
||||
Foreground(lipgloss.Color("#5277b7")).
|
||||
Padding(0, 0, 0, 1)
|
||||
|
||||
list := list.New(items, delegate, overlayWidth, overlayHeight-4)
|
||||
list.KeyMap.CursorUp = key.NewBinding(
|
||||
key.WithKeys("up", "i"),
|
||||
key.WithHelp("↑/i", "up"),
|
||||
)
|
||||
list.KeyMap.CursorDown = key.NewBinding(
|
||||
key.WithKeys("down", "k"),
|
||||
key.WithHelp("↓/k", "down"),
|
||||
)
|
||||
list.KeyMap.PrevPage = key.NewBinding(
|
||||
key.WithKeys("left", "j", "pgup", "b", "u"),
|
||||
key.WithHelp("←/j/pgup", "prev page"),
|
||||
)
|
||||
list.KeyMap.NextPage = key.NewBinding(
|
||||
key.WithKeys("right", "l", "pgdown", "f", "d"),
|
||||
key.WithHelp("→/l/pgdn", "next page"),
|
||||
)
|
||||
list.SetShowTitle(false)
|
||||
list.SetShowHelp(false)
|
||||
//list.DisableQuitKeybindings()
|
||||
|
||||
return &listModel{
|
||||
list: list,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *listModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cc utils.CmdCollector
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, keyEnter):
|
||||
if onSel := m.event.OnSelected; onSel != nil {
|
||||
cc.Add(events.SetTeaMessage(onSel(m.event.Items[m.list.Index()])))
|
||||
}
|
||||
return m, events.SetTeaMessage(controllers.HideColumnOverlay{})
|
||||
case key.Matches(msg, keyEsc):
|
||||
return m, events.SetTeaMessage(controllers.HideColumnOverlay{})
|
||||
default:
|
||||
m.list = cc.Collect(m.list.Update(msg)).(list.Model)
|
||||
}
|
||||
default:
|
||||
m.list = cc.Collect(m.list.Update(msg)).(list.Model)
|
||||
}
|
||||
return m, cc.Cmd()
|
||||
}
|
||||
|
||||
func (m *listModel) View() string {
|
||||
innerView := lipgloss.JoinVertical(
|
||||
lipgloss.Top,
|
||||
lipgloss.PlaceHorizontal(overlayWidth-2, lipgloss.Center, "Related Items"),
|
||||
frameStyle.Render(strings.Repeat(lipgloss.NormalBorder().Top, overlayWidth-2)),
|
||||
m.list.View(),
|
||||
)
|
||||
|
||||
view := style.Width(overlayWidth - 2).Height(m.height - 2).Render(innerView)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func (m *listModel) Resize(w, h int) layout.ResizingModel {
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *listModel) setItems(event controllers.ShowRelatedItemsOverlay, newHeight int) {
|
||||
listItems := sliceutils.Map(event.Items, func(item relitems.RelatedItem) list.Item {
|
||||
return relItemModel{name: item.Name}
|
||||
})
|
||||
m.event = event
|
||||
m.list.SetItems(listItems)
|
||||
m.list.Select(0)
|
||||
m.list.SetHeight(newHeight - 4)
|
||||
|
||||
m.height = newHeight
|
||||
}
|
73
internal/dynamo-browse/ui/teamodels/relselector/model.go
Normal file
73
internal/dynamo-browse/ui/teamodels/relselector/model.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package relselector
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
overlayWidth = 50
|
||||
|
||||
overlayHeight = 8
|
||||
overlayHeightExtra2 = 2
|
||||
maxItems = 8
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
subModel tea.Model
|
||||
compositor *layout.Compositor
|
||||
listModel *listModel
|
||||
w, h int
|
||||
}
|
||||
|
||||
func New(subModel tea.Model) *Model {
|
||||
compositor := layout.NewCompositor(subModel)
|
||||
listModel := newListModel()
|
||||
|
||||
return &Model{
|
||||
subModel: subModel,
|
||||
listModel: listModel,
|
||||
compositor: compositor,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) Init() tea.Cmd {
|
||||
return m.compositor.Init()
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cc utils.CmdCollector
|
||||
switch msg := msg.(type) {
|
||||
case controllers.ShowRelatedItemsOverlay:
|
||||
newHeight := overlayHeight + utils.Min(len(msg.Items), maxItems)*overlayHeightExtra2
|
||||
|
||||
m.listModel.setItems(msg, newHeight)
|
||||
m.compositor.SetOverlay(m.listModel, m.w/2-overlayWidth/2, m.h/2-newHeight/2, overlayWidth, newHeight)
|
||||
case controllers.HideColumnOverlay:
|
||||
m.compositor.ClearOverlay()
|
||||
case tea.KeyMsg:
|
||||
m.compositor = cc.Collect(m.compositor.Update(msg)).(*layout.Compositor)
|
||||
default:
|
||||
m.subModel = cc.Collect(m.subModel.Update(msg)).(tea.Model)
|
||||
}
|
||||
return m, cc.Cmd()
|
||||
}
|
||||
|
||||
func (m *Model) View() string {
|
||||
return m.compositor.View()
|
||||
}
|
||||
|
||||
func (m *Model) Resize(w, h int) layout.ResizingModel {
|
||||
m.w, m.h = w, h
|
||||
m.compositor.MoveOverlay(m.w/2-overlayWidth/2, m.h/2-overlayHeight/2)
|
||||
m.listModel.Resize(w, h)
|
||||
m.subModel = layout.Resize(m.subModel, w, h)
|
||||
m.listModel = layout.Resize(m.listModel, w, h).(*listModel)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Model) SelectorVisible() bool {
|
||||
return m.compositor.HasOverlay()
|
||||
}
|
|
@ -1,5 +1,12 @@
|
|||
package utils
|
||||
|
||||
func Min(x, y int) int {
|
||||
if x < y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
func Max(x, y int) int {
|
||||
if x > y {
|
||||
return x
|
||||
|
|
8
test.tm
Normal file
8
test.tm
Normal file
|
@ -0,0 +1,8 @@
|
|||
ext.related_items("business-addresses", func(item) {
|
||||
print("Hello")
|
||||
return [
|
||||
{"label": "Customer", "query": `city="Austin"`, "args": {"foo": "foo"}},
|
||||
{"label": "Payment", "query": `officeOpened=false`, "args": {"daa": "Hello"}},
|
||||
{"label": "Thing", "query": `colors.door^="P"`, "args": {"daa": "Hello"}},
|
||||
]
|
||||
})
|
Loading…
Reference in a new issue