diff --git a/cmd/database.go b/cmd/database.go index 8cb9eff6..bc2651dc 100644 --- a/cmd/database.go +++ b/cmd/database.go @@ -157,5 +157,13 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error { log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] user external auth table maintained successfully") + err = datastore.Container.UserDataStore.SyncStructs(new(models.InsightsExplorer)) + + if err != nil { + return err + } + + log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] insights explorer table maintained successfully") + return nil } diff --git a/cmd/webserver.go b/cmd/webserver.go index 79ac3bc7..307ecdfa 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -437,6 +437,15 @@ func startWebServer(c *core.CliContext) error { apiV1Route.POST("/transaction/templates/move.json", bindApi(api.TransactionTemplates.TemplateMoveHandler)) apiV1Route.POST("/transaction/templates/delete.json", bindApi(api.TransactionTemplates.TemplateDeleteHandler)) + // Insights Explorers + apiV1Route.GET("/insights/explorers/list.json", bindApi(api.InsightsExplorers.InsightsExplorerListHandler)) + apiV1Route.GET("/insights/explorers/get.json", bindApi(api.InsightsExplorers.InsightsExplorerGetHandler)) + apiV1Route.POST("/insights/explorers/add.json", bindApi(api.InsightsExplorers.InsightsExplorerCreateHandler)) + apiV1Route.POST("/insights/explorers/modify.json", bindApi(api.InsightsExplorers.InsightsExplorerModifyHandler)) + apiV1Route.POST("/insights/explorers/hide.json", bindApi(api.InsightsExplorers.InsightsExplorerHideHandler)) + apiV1Route.POST("/insights/explorers/move.json", bindApi(api.InsightsExplorers.InsightsExplorerMoveHandler)) + apiV1Route.POST("/insights/explorers/delete.json", bindApi(api.InsightsExplorers.InsightsExplorerDeleteHandler)) + // Large Language Models if config.ReceiptImageRecognitionLLMConfig != nil && config.ReceiptImageRecognitionLLMConfig.LLMProvider != "" { if config.TransactionFromAIImageRecognition { diff --git a/pkg/api/data_managements.go b/pkg/api/data_managements.go index 4416f66f..95b57944 100644 --- a/pkg/api/data_managements.go +++ b/pkg/api/data_managements.go @@ -31,6 +31,7 @@ type DataManagementsApi struct { pictures *services.TransactionPictureService templates *services.TransactionTemplateService userCustomExchangeRates *services.UserCustomExchangeRatesService + insightsExploreres *services.InsightsExplorerService } // Initialize a data management api singleton instance @@ -48,6 +49,7 @@ var ( pictures: services.TransactionPictures, templates: services.TransactionTemplates, userCustomExchangeRates: services.UserCustomExchangeRates, + insightsExploreres: services.InsightsExplorers, } ) @@ -190,6 +192,13 @@ func (a *DataManagementsApi) ClearAllDataHandler(c *core.WebContext) (any, *errs return nil, errs.Or(err, errs.ErrOperationFailed) } + err = a.insightsExploreres.DeleteAllInsightsExplorers(c, uid) + + if err != nil { + log.Errorf(c, "[data_managements.ClearAllDataHandler] failed to delete all insights explorers, because %s", err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + log.Infof(c, "[data_managements.ClearAllDataHandler] user \"uid:%d\" has cleared all data", uid) return true, nil } diff --git a/pkg/api/explorers.go b/pkg/api/explorers.go new file mode 100644 index 00000000..cedc96b7 --- /dev/null +++ b/pkg/api/explorers.go @@ -0,0 +1,274 @@ +package api + +import ( + "encoding/json" + "sort" + + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/errs" + "github.com/mayswind/ezbookkeeping/pkg/log" + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/services" +) + +// InsightsExplorersApi represents insights explorers api +type InsightsExplorersApi struct { + insightsExploreres *services.InsightsExplorerService +} + +// Initialize a insights explorers api singleton instance +var ( + InsightsExplorers = &InsightsExplorersApi{ + insightsExploreres: services.InsightsExplorers, + } +) + +// InsightsExplorerListHandler returns insights explorer list of current user +func (a *InsightsExplorersApi) InsightsExplorerListHandler(c *core.WebContext) (any, *errs.Error) { + uid := c.GetCurrentUid() + explorers, err := a.insightsExploreres.GetAllInsightsExplorerNamesByUid(c, uid) + + if err != nil { + log.Errorf(c, "[explorers.InsightsExplorerListHandler] failed to get insights explorers for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + explorerResps := make(models.InsightsExplorerInfoResponseSlice, len(explorers)) + + for i := 0; i < len(explorers); i++ { + explorerResps[i], err = explorers[i].ToInsightsExplorerInfoResponse() + + if err != nil { + log.Errorf(c, "[explorers.InsightsExplorerListHandler] failed to get insights explorer response for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrInsightsExplorerDataInvalid + } + } + + sort.Sort(explorerResps) + + return explorerResps, nil +} + +// InsightsExplorerGetHandler returns one specific insights explorer of current user +func (a *InsightsExplorersApi) InsightsExplorerGetHandler(c *core.WebContext) (any, *errs.Error) { + var explorerGetReq models.InsightsExplorerGetRequest + err := c.ShouldBindQuery(&explorerGetReq) + + if err != nil { + log.Warnf(c, "[explorers.InsightsExplorerGetHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + explorer, err := a.insightsExploreres.GetInsightsExplorerByExplorerId(c, uid, explorerGetReq.Id) + + if err != nil { + log.Errorf(c, "[explorers.InsightsExplorerGetHandler] failed to get insights explorer \"id:%d\" for user \"uid:%d\", because %s", explorerGetReq.Id, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + explorerResp, err := explorer.ToInsightsExplorerInfoResponse() + + if err != nil { + log.Errorf(c, "[explorers.InsightsExplorerGetHandler] failed to get insights explorer response for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrInsightsExplorerDataInvalid + } + + return explorerResp, nil +} + +// InsightsExplorerCreateHandler saves a new insights explorer by request parameters for current user +func (a *InsightsExplorersApi) InsightsExplorerCreateHandler(c *core.WebContext) (any, *errs.Error) { + var explorerCreateReq models.InsightsExplorerCreateRequest + err := c.ShouldBindJSON(&explorerCreateReq) + + if err != nil { + log.Warnf(c, "[explorers.InsightsExplorerCreateHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + + maxOrderId, err := a.insightsExploreres.GetMaxDisplayOrder(c, uid) + + if err != nil { + log.Errorf(c, "[explorers.InsightsExplorerCreateHandler] failed to get max display order for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + explorer, err := a.createNewInsightsExplorerModel(uid, &explorerCreateReq, maxOrderId+1) + + if err != nil { + log.Errorf(c, "[explorers.InsightsExplorerCreateHandler] failed to parse insights explorer data for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrInsightsExplorerDataInvalid + } + + err = a.insightsExploreres.CreateInsightsExplorer(c, explorer) + + if err != nil { + log.Errorf(c, "[explorers.InsightsExplorerCreateHandler] failed to create insights explorer \"id:%d\" for user \"uid:%d\", because %s", explorer.ExplorerId, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.Infof(c, "[explorers.InsightsExplorerCreateHandler] user \"uid:%d\" has created a new insights explorer \"id:%d\" successfully", uid, explorer.ExplorerId) + + explorerResp, err := explorer.ToInsightsExplorerInfoResponse() + + if err != nil { + log.Errorf(c, "[explorers.InsightsExplorerCreateHandler] failed to get insights explorer response for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrInsightsExplorerDataInvalid + } + + return explorerResp, nil +} + +// InsightsExplorerModifyHandler saves an existed insights explorer by request parameters for current user +func (a *InsightsExplorersApi) InsightsExplorerModifyHandler(c *core.WebContext) (any, *errs.Error) { + var explorerModifyReq models.InsightsExplorerModifyRequest + err := c.ShouldBindJSON(&explorerModifyReq) + + if err != nil { + log.Warnf(c, "[explorers.InsightsExplorerModifyHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + explorer, err := a.insightsExploreres.GetInsightsExplorerByExplorerId(c, uid, explorerModifyReq.Id) + + if err != nil { + log.Errorf(c, "[explorers.InsightsExplorerModifyHandler] failed to get insights explorer \"id:%d\" for user \"uid:%d\", because %s", explorerModifyReq.Id, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + newData, err := json.Marshal(explorerModifyReq.Data) + + if err != nil { + log.Errorf(c, "[explorers.InsightsExplorerModifyHandler] failed to parse insights explorer data for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrInsightsExplorerDataInvalid + } + + newExplorer := &models.InsightsExplorer{ + ExplorerId: explorer.ExplorerId, + Uid: uid, + Name: explorerModifyReq.Name, + Data: string(newData), + } + + if newExplorer.Name == explorer.Name && newExplorer.Data == explorer.Data { + return nil, errs.ErrNothingWillBeUpdated + } + + err = a.insightsExploreres.ModifyInsightsExplorer(c, newExplorer) + + if err != nil { + log.Errorf(c, "[explorers.InsightsExplorerModifyHandler] failed to update insights explorer \"id:%d\" for user \"uid:%d\", because %s", explorerModifyReq.Id, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.Infof(c, "[explorers.InsightsExplorerModifyHandler] user \"uid:%d\" has updated insights explorer \"id:%d\" successfully", uid, explorerModifyReq.Id) + + explorer.Name = newExplorer.Name + explorer.Data = newExplorer.Data + explorerResp, err := explorer.ToInsightsExplorerInfoResponse() + + if err != nil { + log.Errorf(c, "[explorers.InsightsExplorerModifyHandler] failed to get insights explorer response for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrInsightsExplorerDataInvalid + } + + return explorerResp, nil +} + +// InsightsExplorerHideHandler hides a insights explorer by request parameters for current user +func (a *InsightsExplorersApi) InsightsExplorerHideHandler(c *core.WebContext) (any, *errs.Error) { + var explorerHideReq models.InsightsExplorerHideRequest + err := c.ShouldBindJSON(&explorerHideReq) + + if err != nil { + log.Warnf(c, "[explorers.InsightsExplorerHideHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + err = a.insightsExploreres.HideInsightsExplorer(c, uid, []int64{explorerHideReq.Id}, explorerHideReq.Hidden) + + if err != nil { + log.Errorf(c, "[explorers.InsightsExplorerHideHandler] failed to hide insights explorer \"id:%d\" for user \"uid:%d\", because %s", explorerHideReq.Id, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.Infof(c, "[explorers.InsightsExplorerHideHandler] user \"uid:%d\" has hidden insights explorer \"id:%d\"", uid, explorerHideReq.Id) + return true, nil +} + +// InsightsExplorerMoveHandler moves display order of existed insights explorers by request parameters for current user +func (a *InsightsExplorersApi) InsightsExplorerMoveHandler(c *core.WebContext) (any, *errs.Error) { + var explorerMoveReq models.InsightsExplorerMoveRequest + err := c.ShouldBindJSON(&explorerMoveReq) + + if err != nil { + log.Warnf(c, "[explorers.InsightsExplorerMoveHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + explorers := make([]*models.InsightsExplorer, len(explorerMoveReq.NewDisplayOrders)) + + for i := 0; i < len(explorerMoveReq.NewDisplayOrders); i++ { + newDisplayOrder := explorerMoveReq.NewDisplayOrders[i] + explorer := &models.InsightsExplorer{ + Uid: uid, + ExplorerId: newDisplayOrder.Id, + DisplayOrder: newDisplayOrder.DisplayOrder, + } + + explorers[i] = explorer + } + + err = a.insightsExploreres.ModifyInsightsExplorerDisplayOrders(c, uid, explorers) + + if err != nil { + log.Errorf(c, "[explorers.InsightsExplorerMoveHandler] failed to move insights explorers for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.Infof(c, "[explorers.InsightsExplorerMoveHandler] user \"uid:%d\" has moved insights explorers", uid) + return true, nil +} + +// InsightsExplorerDeleteHandler deletes an existed insights explorer by request parameters for current user +func (a *InsightsExplorersApi) InsightsExplorerDeleteHandler(c *core.WebContext) (any, *errs.Error) { + var explorerDeleteReq models.InsightsExplorerDeleteRequest + err := c.ShouldBindJSON(&explorerDeleteReq) + + if err != nil { + log.Warnf(c, "[explorers.InsightsExplorerDeleteHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + err = a.insightsExploreres.DeleteInsightsExplorer(c, uid, explorerDeleteReq.Id) + + if err != nil { + log.Errorf(c, "[explorers.InsightsExplorerDeleteHandler] failed to delete insights explorer \"id:%d\" for user \"uid:%d\", because %s", explorerDeleteReq.Id, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.Infof(c, "[explorers.InsightsExplorerDeleteHandler] user \"uid:%d\" has deleted insights explorer \"id:%d\"", uid, explorerDeleteReq.Id) + return true, nil +} + +func (a *InsightsExplorersApi) createNewInsightsExplorerModel(uid int64, explorerCreateReq *models.InsightsExplorerCreateRequest, order int32) (*models.InsightsExplorer, error) { + data, err := json.Marshal(explorerCreateReq.Data) + + if err != nil { + return nil, err + } + + return &models.InsightsExplorer{ + Uid: uid, + Name: explorerCreateReq.Name, + Data: string(data), + DisplayOrder: order, + }, nil +} diff --git a/pkg/errs/error.go b/pkg/errs/error.go index e11024a6..6865a7d2 100644 --- a/pkg/errs/error.go +++ b/pkg/errs/error.go @@ -43,6 +43,7 @@ const ( NormalSubcategoryLargeLanguageModel = 15 NormalSubcategoryUserExternalAuth = 16 NormalSubcategoryOAuth2 = 17 + NormalSubcategoryInsightsExplorer = 18 ) // Error represents the specific error returned to user diff --git a/pkg/errs/explorer.go b/pkg/errs/explorer.go new file mode 100644 index 00000000..c13df289 --- /dev/null +++ b/pkg/errs/explorer.go @@ -0,0 +1,10 @@ +package errs + +import "net/http" + +// Error codes related to insights explorers +var ( + ErrInsightsExplorerIdInvalid = NewNormalError(NormalSubcategoryInsightsExplorer, 0, http.StatusBadRequest, "explorer id is invalid") + ErrInsightsExplorerNotFound = NewNormalError(NormalSubcategoryInsightsExplorer, 1, http.StatusBadRequest, "explorer not found") + ErrInsightsExplorerDataInvalid = NewNormalError(NormalSubcategoryInsightsExplorer, 2, http.StatusBadRequest, "explorer data is invalid") +) diff --git a/pkg/models/explorer.go b/pkg/models/explorer.go new file mode 100644 index 00000000..eaf9c656 --- /dev/null +++ b/pkg/models/explorer.go @@ -0,0 +1,108 @@ +package models + +import "encoding/json" + +// InsightsExplorer represents a saved insights explorer configuration +type InsightsExplorer struct { + ExplorerId int64 `xorm:"PK"` + Uid int64 `xorm:"INDEX(IDX_insights_explorer_uid_deleted_order) NOT NULL"` + Deleted bool `xorm:"INDEX(IDX_insights_explorer_uid_deleted_order) NOT NULL"` + Name string `xorm:"VARCHAR(64) NOT NULL"` + DisplayOrder int32 `xorm:"INDEX(IDX_insights_explorer_uid_deleted_order) NOT NULL"` + Data string `xorm:"MEDIUMBLOB"` + Hidden bool `xorm:"NOT NULL"` + CreatedUnixTime int64 + UpdatedUnixTime int64 + DeletedUnixTime int64 +} + +// InsightsExplorerCreateRequest represents all parameters of insights explorer creation request +type InsightsExplorerCreateRequest struct { + Name string `json:"name" binding:"required,notBlank,max=64"` + Data map[string]any `json:"data" binding:"required"` + ClientSessionId string `json:"clientSessionId"` +} + +// InsightsExplorerModifyRequest represents all parameters of insights explorer modification request +type InsightsExplorerModifyRequest struct { + Id int64 `json:"id,string" binding:"required,min=0"` + Name string `json:"name" binding:"required,notBlank,max=64"` + Data map[string]any `json:"data" binding:"required"` + Hidden bool `json:"hidden"` + ClientSessionId string `json:"clientSessionId"` +} + +// InsightsExplorerGetRequest represents all parameters of insights explorer getting request +type InsightsExplorerGetRequest struct { + Id int64 `form:"id,string" binding:"required,min=1"` +} + +// InsightsExplorerHideRequest represents all parameters of insights explorer hiding request +type InsightsExplorerHideRequest struct { + Id int64 `json:"id,string" binding:"required,min=1"` + Hidden bool `json:"hidden"` +} + +// InsightsExplorerMoveRequest represents all parameters of insights explorer moving request +type InsightsExplorerMoveRequest struct { + NewDisplayOrders []*InsightsExplorerNewDisplayOrderRequest `json:"newDisplayOrders" binding:"required,min=1"` +} + +// InsightsExplorerNewDisplayOrderRequest represents a data pair of id and display order +type InsightsExplorerNewDisplayOrderRequest struct { + Id int64 `json:"id,string" binding:"required,min=1"` + DisplayOrder int32 `json:"displayOrder"` +} + +// InsightsExplorerDeleteRequest represents all parameters of insights explorer deleting request +type InsightsExplorerDeleteRequest struct { + Id int64 `json:"id,string" binding:"required,min=1"` +} + +// InsightsExplorerInfoResponse represents a view-object of insights explorer info +type InsightsExplorerInfoResponse struct { + Id int64 `json:"id,string"` + Name string `json:"name"` + DisplayOrder int32 `json:"displayOrder"` + Hidden bool `json:"hidden"` + Data map[string]any `json:"data,omitempty"` +} + +// ToInsightsExplorerInfoResponse returns a view-object according to database model +func (a *InsightsExplorer) ToInsightsExplorerInfoResponse() (*InsightsExplorerInfoResponse, error) { + var data map[string]any = nil + + if a.Data != "" { + err := json.Unmarshal([]byte(a.Data), &data) + + if err != nil { + return nil, err + } + } + + return &InsightsExplorerInfoResponse{ + Id: a.ExplorerId, + Name: a.Name, + DisplayOrder: a.DisplayOrder, + Hidden: a.Hidden, + Data: data, + }, nil +} + +// InsightsExplorerInfoResponseSlice represents the slice data structure of InsightsExplorerInfoResponse +type InsightsExplorerInfoResponseSlice []*InsightsExplorerInfoResponse + +// Len returns the count of items +func (s InsightsExplorerInfoResponseSlice) Len() int { + return len(s) +} + +// Swap swaps two items +func (s InsightsExplorerInfoResponseSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less reports whether the first item is less than the second one +func (s InsightsExplorerInfoResponseSlice) Less(i, j int) bool { + return s[i].DisplayOrder < s[j].DisplayOrder +} diff --git a/pkg/models/explorer_test.go b/pkg/models/explorer_test.go new file mode 100644 index 00000000..af2fe0e5 --- /dev/null +++ b/pkg/models/explorer_test.go @@ -0,0 +1,30 @@ +package models + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestInsightsExplorerInfoResponseSliceLess(t *testing.T) { + var insightsExplorerRespSlice InsightsExplorerInfoResponseSlice + insightsExplorerRespSlice = append(insightsExplorerRespSlice, &InsightsExplorerInfoResponse{ + Id: 1, + DisplayOrder: 3, + }) + insightsExplorerRespSlice = append(insightsExplorerRespSlice, &InsightsExplorerInfoResponse{ + Id: 2, + DisplayOrder: 1, + }) + insightsExplorerRespSlice = append(insightsExplorerRespSlice, &InsightsExplorerInfoResponse{ + Id: 3, + DisplayOrder: 2, + }) + + sort.Sort(insightsExplorerRespSlice) + + assert.Equal(t, int64(2), insightsExplorerRespSlice[0].Id) + assert.Equal(t, int64(3), insightsExplorerRespSlice[1].Id) + assert.Equal(t, int64(1), insightsExplorerRespSlice[2].Id) +} diff --git a/pkg/services/explorer.go b/pkg/services/explorer.go new file mode 100644 index 00000000..a045338b --- /dev/null +++ b/pkg/services/explorer.go @@ -0,0 +1,230 @@ +package services + +import ( + "time" + + "xorm.io/xorm" + + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/datastore" + "github.com/mayswind/ezbookkeeping/pkg/errs" + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/uuid" +) + +// InsightsExplorerService represents insights explorer service +type InsightsExplorerService struct { + ServiceUsingDB + ServiceUsingUuid +} + +// Initialize a insights explorer service singleton instance +var ( + InsightsExplorers = &InsightsExplorerService{ + ServiceUsingDB: ServiceUsingDB{ + container: datastore.Container, + }, + ServiceUsingUuid: ServiceUsingUuid{ + container: uuid.Container, + }, + } +) + +// GetAllInsightsExplorerNamesByUid returns all insights explorer models of user without data +func (s *InsightsExplorerService) GetAllInsightsExplorerNamesByUid(c core.Context, uid int64) ([]*models.InsightsExplorer, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + var explorers []*models.InsightsExplorer + err := s.UserDataDB(uid).NewSession(c).Select("explorer_id, uid, name, display_order, hidden").Where("uid=? AND deleted=?", uid, false).Find(&explorers) + + return explorers, err +} + +// GetInsightsExplorerByExplorerId returns a insights explorer model according to insights explorer id +func (s *InsightsExplorerService) GetInsightsExplorerByExplorerId(c core.Context, uid int64, explorerId int64) (*models.InsightsExplorer, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + if explorerId <= 0 { + return nil, errs.ErrInsightsExplorerIdInvalid + } + + explorer := &models.InsightsExplorer{} + has, err := s.UserDataDB(uid).NewSession(c).ID(explorerId).Where("uid=? AND deleted=?", uid, false).Get(explorer) + + if err != nil { + return nil, err + } else if !has { + return nil, errs.ErrInsightsExplorerNotFound + } + + return explorer, nil +} + +// GetMaxDisplayOrder returns the max display order +func (s *InsightsExplorerService) GetMaxDisplayOrder(c core.Context, uid int64) (int32, error) { + if uid <= 0 { + return 0, errs.ErrUserIdInvalid + } + + explorer := &models.InsightsExplorer{} + has, err := s.UserDataDB(uid).NewSession(c).Cols("uid", "deleted", "display_order").Where("uid=? AND deleted=?", uid, false).OrderBy("display_order desc").Limit(1).Get(explorer) + + if err != nil { + return 0, err + } + + if has { + return explorer.DisplayOrder, nil + } else { + return 0, nil + } +} + +// CreateInsightsExplorer saves a new insights explorer model to database +func (s *InsightsExplorerService) CreateInsightsExplorer(c core.Context, explorer *models.InsightsExplorer) error { + if explorer.Uid <= 0 { + return errs.ErrUserIdInvalid + } + + explorer.ExplorerId = s.GenerateUuid(uuid.UUID_TYPE_EXPLORER) + + if explorer.ExplorerId < 1 { + return errs.ErrSystemIsBusy + } + + explorer.Deleted = false + explorer.CreatedUnixTime = time.Now().Unix() + explorer.UpdatedUnixTime = time.Now().Unix() + + return s.UserDataDB(explorer.Uid).DoTransaction(c, func(sess *xorm.Session) error { + _, err := sess.Insert(explorer) + return err + }) +} + +// ModifyInsightsExplorer saves an existed insights explorer model to database +func (s *InsightsExplorerService) ModifyInsightsExplorer(c core.Context, explorer *models.InsightsExplorer) error { + if explorer.Uid <= 0 { + return errs.ErrUserIdInvalid + } + + explorer.UpdatedUnixTime = time.Now().Unix() + + return s.UserDataDB(explorer.Uid).DoTransaction(c, func(sess *xorm.Session) error { + updatedRows, err := sess.ID(explorer.ExplorerId).Cols("name", "data", "updated_unix_time").Where("uid=? AND deleted=?", explorer.Uid, false).Update(explorer) + + if err != nil { + return err + } else if updatedRows < 1 { + return errs.ErrInsightsExplorerNotFound + } + + return err + }) +} + +// HideInsightsExplorer updates hidden field of given insights explorer ids +func (s *InsightsExplorerService) HideInsightsExplorer(c core.Context, uid int64, ids []int64, hidden bool) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + now := time.Now().Unix() + + updateModel := &models.InsightsExplorer{ + Hidden: hidden, + UpdatedUnixTime: now, + } + + return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error { + updatedRows, err := sess.Cols("hidden", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).In("explorer_id", ids).Update(updateModel) + + if err != nil { + return err + } else if updatedRows < 1 { + return errs.ErrInsightsExplorerNotFound + } + + return err + }) +} + +// ModifyInsightsExplorerDisplayOrders updates display order of given insights explorers +func (s *InsightsExplorerService) ModifyInsightsExplorerDisplayOrders(c core.Context, uid int64, explorers []*models.InsightsExplorer) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + for i := 0; i < len(explorers); i++ { + explorers[i].UpdatedUnixTime = time.Now().Unix() + } + + return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error { + for i := 0; i < len(explorers); i++ { + explorer := explorers[i] + updatedRows, err := sess.ID(explorer.ExplorerId).Cols("display_order", "updated_unix_time").Where("uid=? AND deleted=?", uid, false).Update(explorer) + + if err != nil { + return err + } else if updatedRows < 1 { + return errs.ErrInsightsExplorerNotFound + } + } + + return nil + }) +} + +// DeleteInsightsExplorer deletes an existed insights explorer from database +func (s *InsightsExplorerService) DeleteInsightsExplorer(c core.Context, uid int64, explorerId int64) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + now := time.Now().Unix() + + updateModel := &models.InsightsExplorer{ + Deleted: true, + DeletedUnixTime: now, + } + + return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error { + deletedRows, err := sess.ID(explorerId).Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel) + + if err != nil { + return err + } else if deletedRows < 1 { + return errs.ErrInsightsExplorerNotFound + } + + return err + }) +} + +// DeleteAllInsightsExplorers deletes all existed insights explorers from database +func (s *InsightsExplorerService) DeleteAllInsightsExplorers(c core.Context, uid int64) error { + if uid <= 0 { + return errs.ErrUserIdInvalid + } + + now := time.Now().Unix() + + updateModel := &models.InsightsExplorer{ + Deleted: true, + DeletedUnixTime: now, + } + + return s.UserDataDB(uid).DoTransaction(c, func(sess *xorm.Session) error { + _, err := sess.Cols("deleted", "deleted_unix_time").Where("uid=? AND deleted=?", uid, false).Update(updateModel) + + if err != nil { + return err + } + + return nil + }) +} diff --git a/pkg/uuid/uuid_type.go b/pkg/uuid/uuid_type.go index 6731db1a..92bde847 100644 --- a/pkg/uuid/uuid_type.go +++ b/pkg/uuid/uuid_type.go @@ -14,4 +14,5 @@ const ( UUID_TYPE_TAG_INDEX UuidType = 6 UUID_TYPE_TEMPLATE UuidType = 7 UUID_TYPE_PICTURE UuidType = 8 + UUID_TYPE_EXPLORER UuidType = 9 ) diff --git a/src/lib/services.ts b/src/lib/services.ts index 35423c74..5c2e41f6 100644 --- a/src/lib/services.ts +++ b/src/lib/services.ts @@ -117,6 +117,14 @@ import type { TransactionTemplateDeleteRequest, TransactionTemplateInfoResponse } from '@/models/transaction_template.ts'; +import type { + InsightsExplorerCreateRequest, + InsightsExplorerModifyRequest, + InsightsExplorerHideRequest, + InsightsExplorerMoveRequest, + InsightsExplorerDeleteRequest, + InsightsExplorerInfoResponse, +} from '@/models/explorer.ts'; import type { TokenGenerateAPIRequest, TokenGenerateMCPRequest, @@ -742,6 +750,27 @@ export default { deleteTransactionTemplate: (req: TransactionTemplateDeleteRequest): ApiResponsePromise => { return axios.post>('v1/transaction/templates/delete.json', req); }, + getAllInsightsExplorers: (): ApiResponsePromise => { + return axios.get>('v1/insights/explorers/list.json'); + }, + getInsightsExplorer: ({ id }: { id: string }): ApiResponsePromise => { + return axios.get>('v1/insights/explorers/get.json?id=' + id); + }, + addInsightsExplorer: (req: InsightsExplorerCreateRequest): ApiResponsePromise => { + return axios.post>('v1/insights/explorers/add.json', req); + }, + modifyInsightsExplorer: (req: InsightsExplorerModifyRequest): ApiResponsePromise => { + return axios.post>('v1/insights/explorers/modify.json', req); + }, + hideInsightsExplorer: (req: InsightsExplorerHideRequest): ApiResponsePromise => { + return axios.post>('v1/insights/explorers/hide.json', req); + }, + moveInsightsExplorer: (req: InsightsExplorerMoveRequest): ApiResponsePromise => { + return axios.post>('v1/insights/explorers/move.json', req); + }, + deleteInsightsExplorer: (req: InsightsExplorerDeleteRequest): ApiResponsePromise => { + return axios.post>('v1/insights/explorers/delete.json', req); + }, recognizeReceiptImage: ({ imageFile, cancelableUuid }: { imageFile: File, cancelableUuid?: string }): ApiResponsePromise => { return axios.postForm>('v1/llm/transactions/recognize_receipt_image.json', { image: imageFile diff --git a/src/locales/de.json b/src/locales/de.json index 13f4bd95..3c101b50 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "Invalid OAuth 2.0 token", "cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider", "oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user", + "explorer id is invalid": "Explorer ID is invalid", + "explorer not found": "Explorer is not found", + "explorer data is invalid": "Explorer data is invalid", "query items cannot be blank": "Abfrageelemente dürfen nicht leer sein", "query items too much": "Zu viele Abfrageelemente", "query items have invalid item": "Ungültiges Element in Abfrageelementen", @@ -1709,13 +1712,33 @@ "Query": "Query", "Data Table": "Data Table", "Chart": "Chart", - "New Exploration": "New Exploration", + "New Explorer": "New Explorer", + "Untitled Explorer": "Untitled Explorer", + "More Explorer": "More Explorer", + "Save Explorer": "Save Explorer", + "Save As New Explorer": "Save As New Explorer", + "Rename Explorer": "Rename Explorer", + "Delete Explorer": "Delete Explorer", + "Explorer Name": "Explorer Name", "Add Query": "Add Query", "Remove Query": "Remove Query", "Modify Query Name": "Modify Query Name", "Add Condition": "Add Condition", "Remove Condition": "Remove Condition", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", + "Unable to retrieve explorer list": "Unable to retrieve explorer list", + "Explorer list is up to date": "Explorer list is up to date", + "Explorer list has been updated": "Explorer list has been updated", + "Unable to retrieve explorer": "Unable to retrieve explorer", + "Unable to add explorer": "Unable to add explorer", + "Unable to save explorer": "Unable to save explorer", + "Unable to move explorer": "Unable to move explorer", + "Unable to hide this explorer": "Unable to hide this explorer", + "Unable to unhide this explorer": "Unable to unhide this explorer", + "Are you sure you want to delete this explorer?": "Are you sure you want to delete this explorer?", + "Unable to delete this explorer": "Unable to delete this explorer", + "Show Hidden Explorers": "Show Hidden Explorers", + "Hide Hidden Explorers": "Hide Hidden Explorers", "Editor": "Editor", "Expression": "Expression", "Failed to generate expression": "Failed to generate expression", diff --git a/src/locales/en.json b/src/locales/en.json index 0d3da445..357ff8ec 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "Invalid OAuth 2.0 token", "cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider", "oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user", + "explorer id is invalid": "Explorer ID is invalid", + "explorer not found": "Explorer is not found", + "explorer data is invalid": "Explorer data is invalid", "query items cannot be blank": "There are no query items", "query items too much": "There are too many query items", "query items have invalid item": "There is invalid item in query items", @@ -1709,13 +1712,33 @@ "Query": "Query", "Data Table": "Data Table", "Chart": "Chart", - "New Exploration": "New Exploration", + "New Explorer": "New Explorer", + "Untitled Explorer": "Untitled Explorer", + "More Explorer": "More Explorer", + "Save Explorer": "Save Explorer", + "Save As New Explorer": "Save As New Explorer", + "Rename Explorer": "Rename Explorer", + "Delete Explorer": "Delete Explorer", + "Explorer Name": "Explorer Name", "Add Query": "Add Query", "Remove Query": "Remove Query", "Modify Query Name": "Modify Query Name", "Add Condition": "Add Condition", "Remove Condition": "Remove Condition", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", + "Unable to retrieve explorer list": "Unable to retrieve explorer list", + "Explorer list is up to date": "Explorer list is up to date", + "Explorer list has been updated": "Explorer list has been updated", + "Unable to retrieve explorer": "Unable to retrieve explorer", + "Unable to add explorer": "Unable to add explorer", + "Unable to save explorer": "Unable to save explorer", + "Unable to move explorer": "Unable to move explorer", + "Unable to hide this explorer": "Unable to hide this explorer", + "Unable to unhide this explorer": "Unable to unhide this explorer", + "Are you sure you want to delete this explorer?": "Are you sure you want to delete this explorer?", + "Unable to delete this explorer": "Unable to delete this explorer", + "Show Hidden Explorers": "Show Hidden Explorers", + "Hide Hidden Explorers": "Hide Hidden Explorers", "Editor": "Editor", "Expression": "Expression", "Failed to generate expression": "Failed to generate expression", diff --git a/src/locales/es.json b/src/locales/es.json index 0db10720..454d7ed9 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "Invalid OAuth 2.0 token", "cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider", "oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user", + "explorer id is invalid": "Explorer ID is invalid", + "explorer not found": "Explorer is not found", + "explorer data is invalid": "Explorer data is invalid", "query items cannot be blank": "There are no query items", "query items too much": "There are too many query items", "query items have invalid item": "Hay un elemento no válido en los elementos de consulta", @@ -1709,13 +1712,33 @@ "Query": "Query", "Data Table": "Data Table", "Chart": "Chart", - "New Exploration": "New Exploration", + "New Explorer": "New Explorer", + "Untitled Explorer": "Untitled Explorer", + "More Explorer": "More Explorer", + "Save Explorer": "Save Explorer", + "Save As New Explorer": "Save As New Explorer", + "Rename Explorer": "Rename Explorer", + "Delete Explorer": "Delete Explorer", + "Explorer Name": "Explorer Name", "Add Query": "Add Query", "Remove Query": "Remove Query", "Modify Query Name": "Modify Query Name", "Add Condition": "Add Condition", "Remove Condition": "Remove Condition", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", + "Unable to retrieve explorer list": "Unable to retrieve explorer list", + "Explorer list is up to date": "Explorer list is up to date", + "Explorer list has been updated": "Explorer list has been updated", + "Unable to retrieve explorer": "Unable to retrieve explorer", + "Unable to add explorer": "Unable to add explorer", + "Unable to save explorer": "Unable to save explorer", + "Unable to move explorer": "Unable to move explorer", + "Unable to hide this explorer": "Unable to hide this explorer", + "Unable to unhide this explorer": "Unable to unhide this explorer", + "Are you sure you want to delete this explorer?": "Are you sure you want to delete this explorer?", + "Unable to delete this explorer": "Unable to delete this explorer", + "Show Hidden Explorers": "Show Hidden Explorers", + "Hide Hidden Explorers": "Hide Hidden Explorers", "Editor": "Editor", "Expression": "Expression", "Failed to generate expression": "Failed to generate expression", diff --git a/src/locales/fr.json b/src/locales/fr.json index d40f8ad2..c44cbcc0 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "Invalid OAuth 2.0 token", "cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider", "oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user", + "explorer id is invalid": "Explorer ID is invalid", + "explorer not found": "Explorer is not found", + "explorer data is invalid": "Explorer data is invalid", "query items cannot be blank": "Il n'y a pas d'éléments de requête", "query items too much": "Il y a trop d'éléments de requête", "query items have invalid item": "Il y a un élément invalide dans les éléments de requête", @@ -1709,13 +1712,33 @@ "Query": "Query", "Data Table": "Data Table", "Chart": "Chart", - "New Exploration": "New Exploration", + "New Explorer": "New Explorer", + "Untitled Explorer": "Untitled Explorer", + "More Explorer": "More Explorer", + "Save Explorer": "Save Explorer", + "Save As New Explorer": "Save As New Explorer", + "Rename Explorer": "Rename Explorer", + "Delete Explorer": "Delete Explorer", + "Explorer Name": "Explorer Name", "Add Query": "Add Query", "Remove Query": "Remove Query", "Modify Query Name": "Modify Query Name", "Add Condition": "Add Condition", "Remove Condition": "Remove Condition", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", + "Unable to retrieve explorer list": "Unable to retrieve explorer list", + "Explorer list is up to date": "Explorer list is up to date", + "Explorer list has been updated": "Explorer list has been updated", + "Unable to retrieve explorer": "Unable to retrieve explorer", + "Unable to add explorer": "Unable to add explorer", + "Unable to save explorer": "Unable to save explorer", + "Unable to move explorer": "Unable to move explorer", + "Unable to hide this explorer": "Unable to hide this explorer", + "Unable to unhide this explorer": "Unable to unhide this explorer", + "Are you sure you want to delete this explorer?": "Are you sure you want to delete this explorer?", + "Unable to delete this explorer": "Unable to delete this explorer", + "Show Hidden Explorers": "Show Hidden Explorers", + "Hide Hidden Explorers": "Hide Hidden Explorers", "Editor": "Editor", "Expression": "Expression", "Failed to generate expression": "Failed to generate expression", diff --git a/src/locales/it.json b/src/locales/it.json index 3710b935..030c1c99 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "Invalid OAuth 2.0 token", "cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider", "oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user", + "explorer id is invalid": "Explorer ID is invalid", + "explorer not found": "Explorer is not found", + "explorer data is invalid": "Explorer data is invalid", "query items cannot be blank": "Non ci sono elementi di query", "query items too much": "Ci sono troppi elementi di query", "query items have invalid item": "C'è un elemento non valido negli elementi di query", @@ -1709,13 +1712,33 @@ "Query": "Query", "Data Table": "Data Table", "Chart": "Chart", - "New Exploration": "New Exploration", + "New Explorer": "New Explorer", + "Untitled Explorer": "Untitled Explorer", + "More Explorer": "More Explorer", + "Save Explorer": "Save Explorer", + "Save As New Explorer": "Save As New Explorer", + "Rename Explorer": "Rename Explorer", + "Delete Explorer": "Delete Explorer", + "Explorer Name": "Explorer Name", "Add Query": "Add Query", "Remove Query": "Remove Query", "Modify Query Name": "Modify Query Name", "Add Condition": "Add Condition", "Remove Condition": "Remove Condition", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", + "Unable to retrieve explorer list": "Unable to retrieve explorer list", + "Explorer list is up to date": "Explorer list is up to date", + "Explorer list has been updated": "Explorer list has been updated", + "Unable to retrieve explorer": "Unable to retrieve explorer", + "Unable to add explorer": "Unable to add explorer", + "Unable to save explorer": "Unable to save explorer", + "Unable to move explorer": "Unable to move explorer", + "Unable to hide this explorer": "Unable to hide this explorer", + "Unable to unhide this explorer": "Unable to unhide this explorer", + "Are you sure you want to delete this explorer?": "Are you sure you want to delete this explorer?", + "Unable to delete this explorer": "Unable to delete this explorer", + "Show Hidden Explorers": "Show Hidden Explorers", + "Hide Hidden Explorers": "Hide Hidden Explorers", "Editor": "Editor", "Expression": "Expression", "Failed to generate expression": "Failed to generate expression", diff --git a/src/locales/ja.json b/src/locales/ja.json index 46240ef2..c0a0efea 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "Invalid OAuth 2.0 token", "cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider", "oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user", + "explorer id is invalid": "Explorer ID is invalid", + "explorer not found": "Explorer is not found", + "explorer data is invalid": "Explorer data is invalid", "query items cannot be blank": "クエリ項目がありません", "query items too much": "クエリ項目が多すぎます", "query items have invalid item": "クエリ項目に無効な項目があります", @@ -1709,13 +1712,33 @@ "Query": "Query", "Data Table": "Data Table", "Chart": "Chart", - "New Exploration": "New Exploration", + "New Explorer": "New Explorer", + "Untitled Explorer": "Untitled Explorer", + "More Explorer": "More Explorer", + "Save Explorer": "Save Explorer", + "Save As New Explorer": "Save As New Explorer", + "Rename Explorer": "Rename Explorer", + "Delete Explorer": "Delete Explorer", + "Explorer Name": "Explorer Name", "Add Query": "Add Query", "Remove Query": "Remove Query", "Modify Query Name": "Modify Query Name", "Add Condition": "Add Condition", "Remove Condition": "Remove Condition", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", + "Unable to retrieve explorer list": "Unable to retrieve explorer list", + "Explorer list is up to date": "Explorer list is up to date", + "Explorer list has been updated": "Explorer list has been updated", + "Unable to retrieve explorer": "Unable to retrieve explorer", + "Unable to add explorer": "Unable to add explorer", + "Unable to save explorer": "Unable to save explorer", + "Unable to move explorer": "Unable to move explorer", + "Unable to hide this explorer": "Unable to hide this explorer", + "Unable to unhide this explorer": "Unable to unhide this explorer", + "Are you sure you want to delete this explorer?": "Are you sure you want to delete this explorer?", + "Unable to delete this explorer": "Unable to delete this explorer", + "Show Hidden Explorers": "Show Hidden Explorers", + "Hide Hidden Explorers": "Hide Hidden Explorers", "Editor": "Editor", "Expression": "Expression", "Failed to generate expression": "Failed to generate expression", diff --git a/src/locales/kn.json b/src/locales/kn.json index c0d1488c..f3c05697 100644 --- a/src/locales/kn.json +++ b/src/locales/kn.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "OAuth 2.0 ಟೋಕನ್ ಅಮಾನ್ಯವಾಗಿದೆ", "cannot retrieve user info from oauth2 provider": "OAuth 2.0 ಪೂರೈಕೆದಾರರಿಂದ ಬಳಕೆದಾರ ಮಾಹಿತಿಯನ್ನು ಪಡೆಯಲು ಸಾಧ್ಯವಿಲ್ಲ", "oauth2 user already bound to another user": "OAuth 2.0 ಬಳಕೆದಾರ ಈಗಾಗಲೇ ಇನ್ನೊಬ್ಬ ಬಳಕೆದಾರನಿಗೆ ಬೌಂಡ್ ಆಗಿದ್ದಾನೆ", + "explorer id is invalid": "Explorer ID is invalid", + "explorer not found": "Explorer is not found", + "explorer data is invalid": "Explorer data is invalid", "query items cannot be blank": "ವಿಚರಣೆ ಐಟಂಗಳಿಲ್ಲ", "query items too much": "ವಿಚರಣೆ ಐಟಂಗಳ ಸಂಖ್ಯೆ ಹೆಚ್ಚು", "query items have invalid item": "ವಿಚರಣೆ ಐಟಂಗಳಲ್ಲಿ ಅಮಾನ್ಯ ಐಟಂ ಇದೆ", @@ -1709,13 +1712,33 @@ "Query": "Query", "Data Table": "Data Table", "Chart": "Chart", - "New Exploration": "New Exploration", + "New Explorer": "New Explorer", + "Untitled Explorer": "Untitled Explorer", + "More Explorer": "More Explorer", + "Save Explorer": "Save Explorer", + "Save As New Explorer": "Save As New Explorer", + "Rename Explorer": "Rename Explorer", + "Delete Explorer": "Delete Explorer", + "Explorer Name": "Explorer Name", "Add Query": "Add Query", "Remove Query": "Remove Query", "Modify Query Name": "Modify Query Name", "Add Condition": "Add Condition", "Remove Condition": "Remove Condition", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", + "Unable to retrieve explorer list": "Unable to retrieve explorer list", + "Explorer list is up to date": "Explorer list is up to date", + "Explorer list has been updated": "Explorer list has been updated", + "Unable to retrieve explorer": "Unable to retrieve explorer", + "Unable to add explorer": "Unable to add explorer", + "Unable to save explorer": "Unable to save explorer", + "Unable to move explorer": "Unable to move explorer", + "Unable to hide this explorer": "Unable to hide this explorer", + "Unable to unhide this explorer": "Unable to unhide this explorer", + "Are you sure you want to delete this explorer?": "Are you sure you want to delete this explorer?", + "Unable to delete this explorer": "Unable to delete this explorer", + "Show Hidden Explorers": "Show Hidden Explorers", + "Hide Hidden Explorers": "Hide Hidden Explorers", "Editor": "Editor", "Expression": "Expression", "Failed to generate expression": "Failed to generate expression", diff --git a/src/locales/ko.json b/src/locales/ko.json index 4ef37ac9..3f2da1b7 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "Invalid OAuth 2.0 token", "cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider", "oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user", + "explorer id is invalid": "Explorer ID is invalid", + "explorer not found": "Explorer is not found", + "explorer data is invalid": "Explorer data is invalid", "query items cannot be blank": "쿼리 항목이 비어 있을 수 없습니다.", "query items too much": "쿼리 항목이 너무 많습니다.", "query items have invalid item": "쿼리 항목에 유효하지 않은 항목이 있습니다.", @@ -1709,13 +1712,33 @@ "Query": "Query", "Data Table": "Data Table", "Chart": "Chart", - "New Exploration": "New Exploration", + "New Explorer": "New Explorer", + "Untitled Explorer": "Untitled Explorer", + "More Explorer": "More Explorer", + "Save Explorer": "Save Explorer", + "Save As New Explorer": "Save As New Explorer", + "Rename Explorer": "Rename Explorer", + "Delete Explorer": "Delete Explorer", + "Explorer Name": "Explorer Name", "Add Query": "Add Query", "Remove Query": "Remove Query", "Modify Query Name": "Modify Query Name", "Add Condition": "Add Condition", "Remove Condition": "Remove Condition", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", + "Unable to retrieve explorer list": "Unable to retrieve explorer list", + "Explorer list is up to date": "Explorer list is up to date", + "Explorer list has been updated": "Explorer list has been updated", + "Unable to retrieve explorer": "Unable to retrieve explorer", + "Unable to add explorer": "Unable to add explorer", + "Unable to save explorer": "Unable to save explorer", + "Unable to move explorer": "Unable to move explorer", + "Unable to hide this explorer": "Unable to hide this explorer", + "Unable to unhide this explorer": "Unable to unhide this explorer", + "Are you sure you want to delete this explorer?": "Are you sure you want to delete this explorer?", + "Unable to delete this explorer": "Unable to delete this explorer", + "Show Hidden Explorers": "Show Hidden Explorers", + "Hide Hidden Explorers": "Hide Hidden Explorers", "Editor": "Editor", "Expression": "Expression", "Failed to generate expression": "Failed to generate expression", diff --git a/src/locales/nl.json b/src/locales/nl.json index 494eed4c..6d45d423 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "Invalid OAuth 2.0 token", "cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider", "oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user", + "explorer id is invalid": "Explorer ID is invalid", + "explorer not found": "Explorer is not found", + "explorer data is invalid": "Explorer data is invalid", "query items cannot be blank": "Geen zoekitems opgegeven", "query items too much": "Te veel zoekitems", "query items have invalid item": "Ongeldig item in zoekitems", @@ -1709,13 +1712,33 @@ "Query": "Query", "Data Table": "Data Table", "Chart": "Chart", - "New Exploration": "New Exploration", + "New Explorer": "New Explorer", + "Untitled Explorer": "Untitled Explorer", + "More Explorer": "More Explorer", + "Save Explorer": "Save Explorer", + "Save As New Explorer": "Save As New Explorer", + "Rename Explorer": "Rename Explorer", + "Delete Explorer": "Delete Explorer", + "Explorer Name": "Explorer Name", "Add Query": "Add Query", "Remove Query": "Remove Query", "Modify Query Name": "Modify Query Name", "Add Condition": "Add Condition", "Remove Condition": "Remove Condition", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", + "Unable to retrieve explorer list": "Unable to retrieve explorer list", + "Explorer list is up to date": "Explorer list is up to date", + "Explorer list has been updated": "Explorer list has been updated", + "Unable to retrieve explorer": "Unable to retrieve explorer", + "Unable to add explorer": "Unable to add explorer", + "Unable to save explorer": "Unable to save explorer", + "Unable to move explorer": "Unable to move explorer", + "Unable to hide this explorer": "Unable to hide this explorer", + "Unable to unhide this explorer": "Unable to unhide this explorer", + "Are you sure you want to delete this explorer?": "Are you sure you want to delete this explorer?", + "Unable to delete this explorer": "Unable to delete this explorer", + "Show Hidden Explorers": "Show Hidden Explorers", + "Hide Hidden Explorers": "Hide Hidden Explorers", "Editor": "Editor", "Expression": "Expression", "Failed to generate expression": "Failed to generate expression", diff --git a/src/locales/pt_BR.json b/src/locales/pt_BR.json index 6cc51f34..143443a6 100644 --- a/src/locales/pt_BR.json +++ b/src/locales/pt_BR.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "Invalid OAuth 2.0 token", "cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider", "oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user", + "explorer id is invalid": "Explorer ID is invalid", + "explorer not found": "Explorer is not found", + "explorer data is invalid": "Explorer data is invalid", "query items cannot be blank": "Não há itens de consulta", "query items too much": "Há muitos itens de consulta", "query items have invalid item": "Há item inválido nos itens de consulta", @@ -1709,13 +1712,33 @@ "Query": "Query", "Data Table": "Data Table", "Chart": "Chart", - "New Exploration": "New Exploration", + "New Explorer": "New Explorer", + "Untitled Explorer": "Untitled Explorer", + "More Explorer": "More Explorer", + "Save Explorer": "Save Explorer", + "Save As New Explorer": "Save As New Explorer", + "Rename Explorer": "Rename Explorer", + "Delete Explorer": "Delete Explorer", + "Explorer Name": "Explorer Name", "Add Query": "Add Query", "Remove Query": "Remove Query", "Modify Query Name": "Modify Query Name", "Add Condition": "Add Condition", "Remove Condition": "Remove Condition", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", + "Unable to retrieve explorer list": "Unable to retrieve explorer list", + "Explorer list is up to date": "Explorer list is up to date", + "Explorer list has been updated": "Explorer list has been updated", + "Unable to retrieve explorer": "Unable to retrieve explorer", + "Unable to add explorer": "Unable to add explorer", + "Unable to save explorer": "Unable to save explorer", + "Unable to move explorer": "Unable to move explorer", + "Unable to hide this explorer": "Unable to hide this explorer", + "Unable to unhide this explorer": "Unable to unhide this explorer", + "Are you sure you want to delete this explorer?": "Are you sure you want to delete this explorer?", + "Unable to delete this explorer": "Unable to delete this explorer", + "Show Hidden Explorers": "Show Hidden Explorers", + "Hide Hidden Explorers": "Hide Hidden Explorers", "Editor": "Editor", "Expression": "Expression", "Failed to generate expression": "Failed to generate expression", diff --git a/src/locales/ru.json b/src/locales/ru.json index cb69c459..fdfdeae9 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "Invalid OAuth 2.0 token", "cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider", "oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user", + "explorer id is invalid": "Explorer ID is invalid", + "explorer not found": "Explorer is not found", + "explorer data is invalid": "Explorer data is invalid", "query items cannot be blank": "Нет элементов запроса", "query items too much": "Слишком много элементов запроса", "query items have invalid item": "В элементах запроса присутствует недопустимый элемент", @@ -1709,13 +1712,33 @@ "Query": "Query", "Data Table": "Data Table", "Chart": "Chart", - "New Exploration": "New Exploration", + "New Explorer": "New Explorer", + "Untitled Explorer": "Untitled Explorer", + "More Explorer": "More Explorer", + "Save Explorer": "Save Explorer", + "Save As New Explorer": "Save As New Explorer", + "Rename Explorer": "Rename Explorer", + "Delete Explorer": "Delete Explorer", + "Explorer Name": "Explorer Name", "Add Query": "Add Query", "Remove Query": "Remove Query", "Modify Query Name": "Modify Query Name", "Add Condition": "Add Condition", "Remove Condition": "Remove Condition", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", + "Unable to retrieve explorer list": "Unable to retrieve explorer list", + "Explorer list is up to date": "Explorer list is up to date", + "Explorer list has been updated": "Explorer list has been updated", + "Unable to retrieve explorer": "Unable to retrieve explorer", + "Unable to add explorer": "Unable to add explorer", + "Unable to save explorer": "Unable to save explorer", + "Unable to move explorer": "Unable to move explorer", + "Unable to hide this explorer": "Unable to hide this explorer", + "Unable to unhide this explorer": "Unable to unhide this explorer", + "Are you sure you want to delete this explorer?": "Are you sure you want to delete this explorer?", + "Unable to delete this explorer": "Unable to delete this explorer", + "Show Hidden Explorers": "Show Hidden Explorers", + "Hide Hidden Explorers": "Hide Hidden Explorers", "Editor": "Editor", "Expression": "Expression", "Failed to generate expression": "Failed to generate expression", diff --git a/src/locales/sl.json b/src/locales/sl.json index 34d3ac76..7848d840 100644 --- a/src/locales/sl.json +++ b/src/locales/sl.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "Neveljaven žeton OAuth 2.0", "cannot retrieve user info from oauth2 provider": "Ni mogoče pridobiti podatkov o uporabniku od ponudnika OAuth 2.0", "oauth2 user already bound to another user": "Uporabnik OAuth 2.0 je že povezan z drugim uporabnikom", + "explorer id is invalid": "Explorer ID is invalid", + "explorer not found": "Explorer is not found", + "explorer data is invalid": "Explorer data is invalid", "query items cannot be blank": "Poizvedbeni elementi ne morejo biti prazni", "query items too much": "Preveč poizvedbenih elementov", "query items have invalid item": "Med poizvedbenimi elementi je neveljaven element", @@ -1709,13 +1712,33 @@ "Query": "Poizvedba", "Data Table": "Podatkovna tabela", "Chart": "Grafikon", - "New Exploration": "Novo raziskovanje", + "New Explorer": "Novo raziskovanje", + "Untitled Explorer": "Untitled Explorer", + "More Explorer": "More Explorer", + "Save Explorer": "Save Explorer", + "Save As New Explorer": "Save As New Explorer", + "Rename Explorer": "Rename Explorer", + "Delete Explorer": "Delete Explorer", + "Explorer Name": "Explorer Name", "Add Query": "Dodaj poizvedbo", "Remove Query": "Odstrani poizvedbo", "Modify Query Name": "Spremeni ime poizvedbe", "Add Condition": "Dodaj pogoj", "Remove Condition": "Odstrani pogoj", "No conditions defined. All transactions will match.": "Ni določenih pogojev. Prikazane bodo vse transakcije.", + "Unable to retrieve explorer list": "Unable to retrieve explorer list", + "Explorer list is up to date": "Explorer list is up to date", + "Explorer list has been updated": "Explorer list has been updated", + "Unable to retrieve explorer": "Unable to retrieve explorer", + "Unable to add explorer": "Unable to add explorer", + "Unable to save explorer": "Unable to save explorer", + "Unable to move explorer": "Unable to move explorer", + "Unable to hide this explorer": "Unable to hide this explorer", + "Unable to unhide this explorer": "Unable to unhide this explorer", + "Are you sure you want to delete this explorer?": "Are you sure you want to delete this explorer?", + "Unable to delete this explorer": "Unable to delete this explorer", + "Show Hidden Explorers": "Show Hidden Explorers", + "Hide Hidden Explorers": "Hide Hidden Explorers", "Editor": "Urednik", "Expression": "Izraz", "Failed to generate expression": "Izraza ni bilo mogoče ustvariti", diff --git a/src/locales/th.json b/src/locales/th.json index 271b995c..893aed44 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "Invalid OAuth 2.0 token", "cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider", "oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user", + "explorer id is invalid": "Explorer ID is invalid", + "explorer not found": "Explorer is not found", + "explorer data is invalid": "Explorer data is invalid", "query items cannot be blank": "ไม่มีรายการสำหรับค้นหา", "query items too much": "รายการค้นหามากเกินไป", "query items have invalid item": "มีรายการไม่ถูกต้องในรายการค้นหา", @@ -1709,13 +1712,33 @@ "Query": "Query", "Data Table": "Data Table", "Chart": "Chart", - "New Exploration": "New Exploration", + "New Explorer": "New Explorer", + "Untitled Explorer": "Untitled Explorer", + "More Explorer": "More Explorer", + "Save Explorer": "Save Explorer", + "Save As New Explorer": "Save As New Explorer", + "Rename Explorer": "Rename Explorer", + "Delete Explorer": "Delete Explorer", + "Explorer Name": "Explorer Name", "Add Query": "Add Query", "Remove Query": "Remove Query", "Modify Query Name": "Modify Query Name", "Add Condition": "Add Condition", "Remove Condition": "Remove Condition", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", + "Unable to retrieve explorer list": "Unable to retrieve explorer list", + "Explorer list is up to date": "Explorer list is up to date", + "Explorer list has been updated": "Explorer list has been updated", + "Unable to retrieve explorer": "Unable to retrieve explorer", + "Unable to add explorer": "Unable to add explorer", + "Unable to save explorer": "Unable to save explorer", + "Unable to move explorer": "Unable to move explorer", + "Unable to hide this explorer": "Unable to hide this explorer", + "Unable to unhide this explorer": "Unable to unhide this explorer", + "Are you sure you want to delete this explorer?": "Are you sure you want to delete this explorer?", + "Unable to delete this explorer": "Unable to delete this explorer", + "Show Hidden Explorers": "Show Hidden Explorers", + "Hide Hidden Explorers": "Hide Hidden Explorers", "Editor": "Editor", "Expression": "Expression", "Failed to generate expression": "Failed to generate expression", diff --git a/src/locales/tr.json b/src/locales/tr.json index c5c5ca83..728fefdb 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "Geçersiz OAuth 2.0 jetonu", "cannot retrieve user info from oauth2 provider": "OAuth 2.0 sağlayıcısından kullanıcı bilgisi alınamıyor", "oauth2 user already bound to another user": "OAuth 2.0 kullanıcısı zaten başka bir kullanıcıya bağlı", + "explorer id is invalid": "Explorer ID is invalid", + "explorer not found": "Explorer is not found", + "explorer data is invalid": "Explorer data is invalid", "query items cannot be blank": "Sorgu öğeleri boş olamaz", "query items too much": "Çok fazla sorgu öğesi var", "query items have invalid item": "Sorgu öğelerinde geçersiz öğe var", @@ -1709,13 +1712,33 @@ "Query": "Query", "Data Table": "Data Table", "Chart": "Chart", - "New Exploration": "New Exploration", + "New Explorer": "New Explorer", + "Untitled Explorer": "Untitled Explorer", + "More Explorer": "More Explorer", + "Save Explorer": "Save Explorer", + "Save As New Explorer": "Save As New Explorer", + "Rename Explorer": "Rename Explorer", + "Delete Explorer": "Delete Explorer", + "Explorer Name": "Explorer Name", "Add Query": "Add Query", "Remove Query": "Remove Query", "Modify Query Name": "Modify Query Name", "Add Condition": "Add Condition", "Remove Condition": "Remove Condition", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", + "Unable to retrieve explorer list": "Unable to retrieve explorer list", + "Explorer list is up to date": "Explorer list is up to date", + "Explorer list has been updated": "Explorer list has been updated", + "Unable to retrieve explorer": "Unable to retrieve explorer", + "Unable to add explorer": "Unable to add explorer", + "Unable to save explorer": "Unable to save explorer", + "Unable to move explorer": "Unable to move explorer", + "Unable to hide this explorer": "Unable to hide this explorer", + "Unable to unhide this explorer": "Unable to unhide this explorer", + "Are you sure you want to delete this explorer?": "Are you sure you want to delete this explorer?", + "Unable to delete this explorer": "Unable to delete this explorer", + "Show Hidden Explorers": "Show Hidden Explorers", + "Hide Hidden Explorers": "Hide Hidden Explorers", "Editor": "Editor", "Expression": "Expression", "Failed to generate expression": "Failed to generate expression", diff --git a/src/locales/uk.json b/src/locales/uk.json index f1d9d7a1..93d0bdf8 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "Invalid OAuth 2.0 token", "cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider", "oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user", + "explorer id is invalid": "Explorer ID is invalid", + "explorer not found": "Explorer is not found", + "explorer data is invalid": "Explorer data is invalid", "query items cannot be blank": "Елементи запиту не можуть бути порожніми", "query items too much": "Занадто багато елементів запиту", "query items have invalid item": "Запит містить недійсний елемент", @@ -1709,13 +1712,33 @@ "Query": "Query", "Data Table": "Data Table", "Chart": "Chart", - "New Exploration": "New Exploration", + "New Explorer": "New Explorer", + "Untitled Explorer": "Untitled Explorer", + "More Explorer": "More Explorer", + "Save Explorer": "Save Explorer", + "Save As New Explorer": "Save As New Explorer", + "Rename Explorer": "Rename Explorer", + "Delete Explorer": "Delete Explorer", + "Explorer Name": "Explorer Name", "Add Query": "Add Query", "Remove Query": "Remove Query", "Modify Query Name": "Modify Query Name", "Add Condition": "Add Condition", "Remove Condition": "Remove Condition", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", + "Unable to retrieve explorer list": "Unable to retrieve explorer list", + "Explorer list is up to date": "Explorer list is up to date", + "Explorer list has been updated": "Explorer list has been updated", + "Unable to retrieve explorer": "Unable to retrieve explorer", + "Unable to add explorer": "Unable to add explorer", + "Unable to save explorer": "Unable to save explorer", + "Unable to move explorer": "Unable to move explorer", + "Unable to hide this explorer": "Unable to hide this explorer", + "Unable to unhide this explorer": "Unable to unhide this explorer", + "Are you sure you want to delete this explorer?": "Are you sure you want to delete this explorer?", + "Unable to delete this explorer": "Unable to delete this explorer", + "Show Hidden Explorers": "Show Hidden Explorers", + "Hide Hidden Explorers": "Hide Hidden Explorers", "Editor": "Editor", "Expression": "Expression", "Failed to generate expression": "Failed to generate expression", diff --git a/src/locales/vi.json b/src/locales/vi.json index 4f9d68b3..0361ae8a 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "Invalid OAuth 2.0 token", "cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider", "oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user", + "explorer id is invalid": "Explorer ID is invalid", + "explorer not found": "Explorer is not found", + "explorer data is invalid": "Explorer data is invalid", "query items cannot be blank": "Không có mục truy vấn", "query items too much": "Có quá nhiều mục truy vấn", "query items have invalid item": "Có mục không hợp lệ trong các mục truy vấn", @@ -1709,13 +1712,33 @@ "Query": "Query", "Data Table": "Data Table", "Chart": "Chart", - "New Exploration": "New Exploration", + "New Explorer": "New Explorer", + "Untitled Explorer": "Untitled Explorer", + "More Explorer": "More Explorer", + "Save Explorer": "Save Explorer", + "Save As New Explorer": "Save As New Explorer", + "Rename Explorer": "Rename Explorer", + "Delete Explorer": "Delete Explorer", + "Explorer Name": "Explorer Name", "Add Query": "Add Query", "Remove Query": "Remove Query", "Modify Query Name": "Modify Query Name", "Add Condition": "Add Condition", "Remove Condition": "Remove Condition", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", + "Unable to retrieve explorer list": "Unable to retrieve explorer list", + "Explorer list is up to date": "Explorer list is up to date", + "Explorer list has been updated": "Explorer list has been updated", + "Unable to retrieve explorer": "Unable to retrieve explorer", + "Unable to add explorer": "Unable to add explorer", + "Unable to save explorer": "Unable to save explorer", + "Unable to move explorer": "Unable to move explorer", + "Unable to hide this explorer": "Unable to hide this explorer", + "Unable to unhide this explorer": "Unable to unhide this explorer", + "Are you sure you want to delete this explorer?": "Are you sure you want to delete this explorer?", + "Unable to delete this explorer": "Unable to delete this explorer", + "Show Hidden Explorers": "Show Hidden Explorers", + "Hide Hidden Explorers": "Hide Hidden Explorers", "Editor": "Editor", "Expression": "Expression", "Failed to generate expression": "Failed to generate expression", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 68375f56..651cec15 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "无效的 OAuth 2.0 令牌", "cannot retrieve user info from oauth2 provider": "无法从 OAuth 2.0 提供者获取用户信息", "oauth2 user already bound to another user": "OAuth 2.0 用户已经绑定到另一个用户", + "explorer id is invalid": "探索ID无效", + "explorer not found": "探索不存在", + "explorer data is invalid": "探索数据无效", "query items cannot be blank": "请求项目不能为空", "query items too much": "请求项目过多", "query items have invalid item": "请求项目中有非法项目", @@ -1709,13 +1712,33 @@ "Query": "查询", "Data Table": "数据表格", "Chart": "图表", - "New Exploration": "新的探索", + "New Explorer": "新的探索", + "Untitled Explorer": "未命名的探索", + "More Explorer": "更多探索", + "Save Explorer": "保存探索", + "Save As New Explorer": "另存为新的探索", + "Rename Explorer": "重命名探索", + "Delete Explorer": "删除探索", + "Explorer Name": "探索名称", "Add Query": "添加查询", "Remove Query": "移除查询", "Modify Query Name": "修改查询名称", "Add Condition": "添加条件", "Remove Condition": "移除条件", "No conditions defined. All transactions will match.": "没有定义条件。所有交易都会匹配。", + "Unable to retrieve explorer list": "无法获取探索列表", + "Explorer list is up to date": "探索列表已是最新", + "Explorer list has been updated": "探索列表已更新", + "Unable to retrieve explorer": "无法获取探索", + "Unable to add explorer": "无法添加探索", + "Unable to save explorer": "无法保存探索", + "Unable to move explorer": "无法移动探索", + "Unable to hide this explorer": "无法隐藏该探索", + "Unable to unhide this explorer": "无法取消隐藏该探索", + "Are you sure you want to delete this explorer?": "您确定要删除该探索?", + "Unable to delete this explorer": "无法删除该探索", + "Show Hidden Explorers": "显示隐藏的探索", + "Hide Hidden Explorers": "不显示隐藏的探索", "Editor": "编辑器", "Expression": "表达式", "Failed to generate expression": "生成表达式失败", diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index 53853206..a982f179 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -1270,6 +1270,9 @@ "invalid oauth2 token": "無效的 OAuth 2.0 令牌", "cannot retrieve user info from oauth2 provider": "無法從 OAuth 2.0 提供者獲取使用者資訊", "oauth2 user already bound to another user": "OAuth 2.0 使用者已綁定到另一個使用者", + "explorer id is invalid": "探索ID無效", + "explorer not found": "探索不存在", + "explorer data is invalid": "探索資料無效", "query items cannot be blank": "查詢項目不能為空", "query items too much": "查詢項目過多", "query items have invalid item": "查詢項目中有非法項目", @@ -1709,13 +1712,33 @@ "Query": "查詢", "Data Table": "資料表格", "Chart": "圖表", - "New Exploration": "新的探索", + "New Explorer": "新的探索", + "Untitled Explorer": "無標題的探索", + "More Explorer": "更多探索", + "Save Explorer": "儲存探索", + "Save As New Explorer": "另存新探索", + "Rename Explorer": "重新命名探索", + "Delete Explorer": "刪除探索", + "Explorer Name": "探索名稱", "Add Query": "新增查詢", "Remove Query": "移除查詢", "Modify Query Name": "修改查詢名稱", "Add Condition": "新增條件", "Remove Condition": "移除條件", "No conditions defined. All transactions will match.": "沒有定義條件。所有交易都符合。", + "Unable to retrieve explorer list": "無法取得探索清單", + "Explorer list is up to date": "探索清單已是最新", + "Explorer list has been updated": "探索清單已更新", + "Unable to retrieve explorer": "無法取得探索", + "Unable to add explorer": "無法新增探索", + "Unable to save explorer": "無法儲存探索", + "Unable to move explorer": "無法移動探索", + "Unable to hide this explorer": "無法隱藏此探索", + "Unable to unhide this explorer": "無法取消隱藏此探索", + "Are you sure you want to delete this explorer?": "您確定要刪除此探索?", + "Unable to delete this explorer": "無法刪除此探索", + "Show Hidden Explorers": "顯示隱藏的探索", + "Hide Hidden Explorers": "不顯示隱藏的探索", "Editor": "編輯器", "Expression": "表達式", "Failed to generate expression": "產生表達式失敗", diff --git a/src/models/explorer.ts b/src/models/explorer.ts index 973c0f18..2efeb280 100644 --- a/src/models/explorer.ts +++ b/src/models/explorer.ts @@ -1,13 +1,21 @@ import { type PartialRecord, itemAndIndex, keysIfValueEquals } from '@/core/base.ts'; +import { TimezoneTypeForStatistics } from '@/core/timezone.ts'; import { AccountType } from '@/core/account.ts'; import { TransactionType } from '@/core/transaction.ts'; +import { ChartSortingType } from '@/core/statistics.ts'; import { TransactionExplorerConditionRelation, TransactionExplorerConditionRelationPriority, TransactionExplorerConditionFieldType, TransactionExplorerConditionField, TransactionExplorerConditionOperatorType, - TransactionExplorerConditionOperator + TransactionExplorerConditionOperator, + TransactionExplorerChartTypeValue, + TransactionExplorerChartType, + TransactionExplorerDataDimensionType, + TransactionExplorerDataDimension, + TransactionExplorerValueMetricType, + TransactionExplorerValueMetric } from '@/core/explorer.ts'; import { Account } from '@/models/account.ts'; @@ -15,6 +23,230 @@ import { TransactionCategory } from '@/models/transaction_category.ts'; import { TransactionTag } from '@/models/transaction_tag.ts'; import { type TransactionInsightDataItem } from '@/models/transaction.ts'; +export class InsightsExplorerBasicInfo implements InsightsExplorerInfoResponse { + public id: string; + public name: string; + public displayOrder: number; + public hidden: boolean; + public data: Record = {}; + + private constructor(id: string, name: string, displayOrder: number, hidden: boolean) { + this.id = id; + this.name = name; + this.displayOrder = displayOrder; + this.hidden = hidden; + } + + public static of(explorerResponse: InsightsExplorerInfoResponse): InsightsExplorerBasicInfo { + return new InsightsExplorerBasicInfo( + explorerResponse.id, + explorerResponse.name, + explorerResponse.displayOrder, + explorerResponse.hidden + ); + } + + public static ofMulti(explorerResponses: InsightsExplorerInfoResponse[]): InsightsExplorerBasicInfo[] { + const explorers: InsightsExplorerBasicInfo[] = []; + + for (const explorerResponse of explorerResponses) { + explorers.push(InsightsExplorerBasicInfo.of(explorerResponse)); + } + + return explorers; + } +} + +export class InsightsExplorer implements InsightsExplorerInfoResponse { + public id: string; + public name: string; + public displayOrder: number; + public hidden: boolean; + public queries: TransactionExplorerQuery[]; + public timezoneUsedForDateRange: number; + public chartType: TransactionExplorerChartTypeValue; + public categoryDimension: TransactionExplorerDataDimensionType; + public seriesDimension: TransactionExplorerDataDimensionType; + public valueMetric: TransactionExplorerValueMetricType; + public chartSortingType: number; + + public static readonly Default: InsightsExplorer = new InsightsExplorer( + '', + '', + 0, + false, + [], + TimezoneTypeForStatistics.Default.type, + TransactionExplorerChartType.Default.value, + TransactionExplorerDataDimension.CategoryDimensionDefault.value, + TransactionExplorerDataDimension.SeriesDimensionDefault.value, + TransactionExplorerValueMetric.Default.value, + ChartSortingType.Default.type + ); + + private constructor(id: string, name: string, displayOrder: number, hidden: boolean, queries: TransactionExplorerQuery[], timezoneUsedForDateRange: number, chartType: TransactionExplorerChartTypeValue, categoryDimension: TransactionExplorerDataDimensionType, seriesDimension: TransactionExplorerDataDimensionType, valueMetric: TransactionExplorerValueMetricType, chartSortingType: number) { + this.id = id; + this.name = name; + this.displayOrder = displayOrder; + this.hidden = hidden; + this.queries = queries; + this.timezoneUsedForDateRange = timezoneUsedForDateRange; + this.chartType = chartType; + this.categoryDimension = categoryDimension; + this.seriesDimension = seriesDimension; + this.valueMetric = valueMetric; + this.chartSortingType = chartSortingType; + } + + public get data(): Record { + return { + queries: this.queries.map(q => q.toJson()), + timezoneUsedForDateRange: this.timezoneUsedForDateRange, + chartType: this.chartType, + categoryDimension: this.categoryDimension, + seriesDimension: this.seriesDimension, + valueMetric: this.valueMetric, + chartSortingType: this.chartSortingType + }; + } + + public toCreateRequest(clientSessionId: string): InsightsExplorerCreateRequest { + return { + name: this.name, + data: this.data, + clientSessionId: clientSessionId + }; + } + + public toModifyRequest(): InsightsExplorerModifyRequest { + return { + id: this.id, + name: this.name, + data: this.data, + hidden: this.hidden + }; + } + + public static of(explorerResponse: InsightsExplorerInfoResponse): InsightsExplorer { + const data = explorerResponse.data; + const queries: TransactionExplorerQuery[] = []; + let timezoneUsedForDateRange = InsightsExplorer.Default.timezoneUsedForDateRange; + let chartType = InsightsExplorer.Default.chartType; + let categoryDimension = InsightsExplorer.Default.categoryDimension; + let seriesDimension = InsightsExplorer.Default.seriesDimension; + let valueMetric = InsightsExplorer.Default.valueMetric; + let chartSortingType = InsightsExplorer.Default.chartSortingType; + + if (data) { + if (Array.isArray(data['queries'])) { + const textualQueries = data['queries'] as string[]; + + for (const textualQuery of textualQueries) { + const query = TransactionExplorerQuery.parse(textualQuery); + + if (query) { + queries.push(query); + } + } + } + + if (typeof data['timezoneUsedForDateRange'] === 'number') { + timezoneUsedForDateRange = data['timezoneUsedForDateRange'] as number; + } + + if (typeof data['chartType'] === 'string') { + chartType = data['chartType'] as TransactionExplorerChartTypeValue; + } + + if (typeof data['categoryDimension'] === 'string') { + categoryDimension = data['categoryDimension'] as TransactionExplorerDataDimensionType; + } + + if (typeof data['seriesDimension'] === 'string') { + seriesDimension = data['seriesDimension'] as TransactionExplorerDataDimensionType; + } + + if (typeof data['valueMetric'] === 'string') { + valueMetric = data['valueMetric'] as TransactionExplorerValueMetricType; + } + + if (typeof data['chartSortingType'] === 'number') { + chartSortingType = data['chartSortingType'] as number; + } + } + + return new InsightsExplorer( + explorerResponse.id, + explorerResponse.name, + explorerResponse.displayOrder, + explorerResponse.hidden, + queries, + timezoneUsedForDateRange, + chartType, + categoryDimension, + seriesDimension, + valueMetric, + chartSortingType + ); + } + + public static createNewExplorer(name?: string): InsightsExplorer { + return new InsightsExplorer( + '', + name || '', + 0, + false, + [TransactionExplorerQuery.create()], + InsightsExplorer.Default.timezoneUsedForDateRange, + InsightsExplorer.Default.chartType, + InsightsExplorer.Default.categoryDimension, + InsightsExplorer.Default.seriesDimension, + InsightsExplorer.Default.valueMetric, + InsightsExplorer.Default.chartSortingType + ); + } +} + +export interface InsightsExplorerCreateRequest { + readonly name: string; + readonly data: Record; + readonly clientSessionId?: string; +} + +export interface InsightsExplorerModifyRequest { + readonly id: string; + readonly name: string; + readonly data: Record; + readonly hidden: boolean; + readonly clientSessionId?: string; +} + +export interface InsightsExplorerHideRequest { + readonly id: string; + readonly hidden: boolean; +} + +export interface InsightsExplorerMoveRequest { + readonly newDisplayOrders: InsightsExplorerNewDisplayOrderRequest[]; +} + +export interface InsightsExplorerNewDisplayOrderRequest { + readonly id: string; + readonly displayOrder: number; +} + +export interface InsightsExplorerDeleteRequest { + readonly id: string; +} + +export interface InsightsExplorerInfoResponse { + readonly id: string; + readonly name: string; + readonly displayOrder: number; + readonly hidden: boolean; + readonly data: Record; +} + interface ExpressionNode { textualExpression: string; operator?: TransactionExplorerConditionRelation; diff --git a/src/stores/explorer.ts b/src/stores/explorer.ts index 3bcb4cf1..89ac54d7 100644 --- a/src/stores/explorer.ts +++ b/src/stores/explorer.ts @@ -8,19 +8,15 @@ import { useTransactionCategoriesStore } from './transactionCategory.ts'; import { useTransactionTagsStore } from './transactionTag.ts'; import { useExchangeRatesStore } from './exchangeRates.ts'; -import { itemAndIndex, keys, values } from '@/core/base.ts'; +import { type BeforeResolveFunction, itemAndIndex, keys, values } from '@/core/base.ts'; import { AmountFilterType } from '@/core/numeral.ts'; import { DateRangeScene, DateRange } from '@/core/datetime.ts'; import { TimezoneTypeForStatistics } from '@/core/timezone.ts'; import { AccountCategory } from '@/core/account.ts'; import { TransactionType } from '@/core/transaction.ts'; -import { ChartSortingType } from '@/core/statistics.ts'; import { - TransactionExplorerChartTypeValue, TransactionExplorerChartType, - TransactionExplorerDataDimensionType, TransactionExplorerDataDimension, - TransactionExplorerValueMetricType, TransactionExplorerValueMetric, DEFAULT_TRANSACTION_EXPLORER_DATE_RANGE } from '@/core/explorer.ts'; @@ -34,7 +30,10 @@ import { type TransactionInsightDataItem } from '@/models/transaction.ts'; import { - TransactionExplorerQuery + type InsightsExplorerNewDisplayOrderRequest, + type InsightsExplorerInfoResponse, + InsightsExplorer, + InsightsExplorerBasicInfo } from '@/models/explorer.ts'; import { @@ -49,7 +48,7 @@ import { getDateRangeByDateType, getFiscalYearFromUnixTime } from '@/lib/datetime.ts'; -import services from '@/lib/services.ts'; +import services, { type ApiResponsePromise } from '@/lib/services.ts'; import logger from '@/lib/logger.ts'; export enum TransactionExplorerDimensionType { @@ -69,26 +68,12 @@ export interface TransactionExplorerPartialFilter { dateRangeType?: number; startTime?: number; endTime?: number; - queryId?: string; - timezoneUsedForDateRange?: number; - chartType?: TransactionExplorerChartTypeValue; - categoryDimension?: TransactionExplorerDataDimensionType; - seriesDimension?: TransactionExplorerDataDimensionType; - valueMetric?: TransactionExplorerValueMetricType; - chartSortingType?: number; } export interface TransactionExplorerFilter extends TransactionExplorerPartialFilter { dateRangeType: number; startTime: number; endTime: number; - query: TransactionExplorerQuery[]; - timezoneUsedForDateRange: number; - chartType: TransactionExplorerChartTypeValue; - categoryDimension: TransactionExplorerDataDimensionType; - seriesDimension: TransactionExplorerDataDimensionType; - valueMetric: TransactionExplorerValueMetricType; - chartSortingType: number; } export interface CategoriedInfo { @@ -152,7 +137,7 @@ export const useExplorersStore = defineStore('explorers', () => { } if (dimension === TransactionExplorerDataDimension.None) { - const valueMetric = TransactionExplorerValueMetric.valueOf(transactionExplorerFilter.value.valueMetric); + const valueMetric = TransactionExplorerValueMetric.valueOf(currentInsightsExplorer.value.valueMetric); return { categoryName: valueMetric?.name ?? 'Unknown', categoryNameNeedI18n: true, @@ -429,22 +414,68 @@ export const useExplorersStore = defineStore('explorers', () => { seriesedData.trasactions.push(transaction); } + function loadInsightsExplorerList(explorers: InsightsExplorerBasicInfo[]): void { + allInsightsExplorerBasicInfos.value = explorers; + allInsightsExplorerBasicInfosMap.value = {}; + + for (const explorer of explorers) { + allInsightsExplorerBasicInfosMap.value[explorer.id] = explorer; + } + } + + function addExplorerToInsightsExplorerList(explorer: InsightsExplorerBasicInfo): void { + allInsightsExplorerBasicInfos.value.push(explorer); + allInsightsExplorerBasicInfosMap.value[explorer.id] = explorer; + } + + function updateExplorerInInsightsExplorerList(currentExplorer: InsightsExplorerBasicInfo): void { + for (const [explorer, index] of itemAndIndex(allInsightsExplorerBasicInfos.value)) { + if (explorer.id === currentExplorer.id) { + allInsightsExplorerBasicInfos.value.splice(index, 1, currentExplorer); + break; + } + } + + allInsightsExplorerBasicInfosMap.value[currentExplorer.id] = currentExplorer; + } + + function updateExplorerDisplayOrderInInsightsExplorerList({ from, to }: { from: number, to: number }): void { + allInsightsExplorerBasicInfos.value.splice(to, 0, allInsightsExplorerBasicInfos.value.splice(from, 1)[0] as InsightsExplorer); + } + + function updateExplorerVisibilityInInsightsExplorerList({ explorer, hidden }: { explorer: InsightsExplorerBasicInfo, hidden: boolean }): void { + if (allInsightsExplorerBasicInfosMap.value[explorer.id]) { + allInsightsExplorerBasicInfosMap.value[explorer.id]!.hidden = hidden; + } + } + + function removeExplorerFromInsightsExplorerList(currentExplorer: InsightsExplorerBasicInfo): void { + for (const [insightsExplorer, index] of itemAndIndex(allInsightsExplorerBasicInfos.value)) { + if (insightsExplorer.id === currentExplorer.id) { + allInsightsExplorerBasicInfos.value.splice(index, 1); + break; + } + } + + if (allInsightsExplorerBasicInfosMap.value[currentExplorer.id]) { + delete allInsightsExplorerBasicInfosMap.value[currentExplorer.id]; + } + } + const transactionExplorerFilter = ref({ dateRangeType: DEFAULT_TRANSACTION_EXPLORER_DATE_RANGE.type, startTime: 0, - endTime: 0, - query: [], - timezoneUsedForDateRange: TimezoneTypeForStatistics.Default.type, - chartType: TransactionExplorerChartType.Default.value, - categoryDimension: TransactionExplorerDataDimension.CategoryDimensionDefault.value, - seriesDimension: TransactionExplorerDataDimension.SeriesDimensionDefault.value, - valueMetric: TransactionExplorerValueMetric.Default.value, - chartSortingType: ChartSortingType.Default.type + endTime: 0 }); const transactionExplorerAllData = ref([]); const transactionExplorerStateInvalid = ref(true); + const allInsightsExplorerBasicInfos = ref([]); + const allInsightsExplorerBasicInfosMap = ref>({}); + const currentInsightsExplorer = ref(InsightsExplorer.createNewExplorer()); + const insightsExplorerListStateInvalid = ref(true); + const allTransactions = computed(() => { if (!transactionExplorerAllData.value || transactionExplorerAllData.value.length < 1) { return []; @@ -524,14 +555,14 @@ export const useExplorersStore = defineStore('explorers', () => { return []; } - if (!transactionExplorerFilter.value.query || transactionExplorerFilter.value.query.length < 1) { + if (!currentInsightsExplorer.value.queries || currentInsightsExplorer.value.queries.length < 1) { return allTransactions.value; } const result: TransactionInsightDataItem[] = []; for (const transaction of allTransactions.value) { - for (const query of transactionExplorerFilter.value.query) { + for (const query of currentInsightsExplorer.value.queries) { if (query.match(transaction)) { result.push(transaction); break; @@ -547,9 +578,9 @@ export const useExplorersStore = defineStore('explorers', () => { return {}; } - const chartType = TransactionExplorerChartType.valueOf(transactionExplorerFilter.value.chartType); - const categoryDimension = TransactionExplorerDataDimension.valueOf(transactionExplorerFilter.value.categoryDimension); - const seriesDimension = chartType?.seriesDimensionRequired ? TransactionExplorerDataDimension.valueOf(transactionExplorerFilter.value.seriesDimension) : TransactionExplorerDataDimension.SeriesDimensionDefault; + const chartType = TransactionExplorerChartType.valueOf(currentInsightsExplorer.value.chartType); + const categoryDimension = TransactionExplorerDataDimension.valueOf(currentInsightsExplorer.value.categoryDimension); + const seriesDimension = chartType?.seriesDimensionRequired ? TransactionExplorerDataDimension.valueOf(currentInsightsExplorer.value.seriesDimension) : TransactionExplorerDataDimension.SeriesDimensionDefault; if (!chartType || !categoryDimension || !seriesDimension) { return {}; @@ -558,14 +589,14 @@ export const useExplorersStore = defineStore('explorers', () => { const categoriedDataMap: Record = {}; for (const transaction of allTransactions.value) { - if (!transactionExplorerFilter.value.query || transactionExplorerFilter.value.query.length < 1) { - addTransactionToCategoriedDataMap(transactionExplorerFilter.value.timezoneUsedForDateRange, categoriedDataMap, categoryDimension, seriesDimension, '', 0, transaction); + if (!currentInsightsExplorer.value.queries || currentInsightsExplorer.value.queries.length < 1) { + addTransactionToCategoriedDataMap(currentInsightsExplorer.value.timezoneUsedForDateRange, categoriedDataMap, categoryDimension, seriesDimension, '', 0, transaction); continue; } - for (const [query, index] of itemAndIndex(transactionExplorerFilter.value.query)) { + for (const [query, index] of itemAndIndex(currentInsightsExplorer.value.queries)) { if (query.match(transaction)) { - addTransactionToCategoriedDataMap(transactionExplorerFilter.value.timezoneUsedForDateRange, categoriedDataMap, categoryDimension, seriesDimension, query.name, index, transaction); + addTransactionToCategoriedDataMap(currentInsightsExplorer.value.timezoneUsedForDateRange, categoriedDataMap, categoryDimension, seriesDimension, query.name, index, transaction); if (categoryDimension !== TransactionExplorerDataDimension.Query) { break; @@ -582,10 +613,10 @@ export const useExplorersStore = defineStore('explorers', () => { return []; } - const chartType = TransactionExplorerChartType.valueOf(transactionExplorerFilter.value.chartType); - const categoryDimension = TransactionExplorerDataDimension.valueOf(transactionExplorerFilter.value.categoryDimension); - const seriesDimension = chartType?.seriesDimensionRequired ? TransactionExplorerDataDimension.valueOf(transactionExplorerFilter.value.seriesDimension) : TransactionExplorerDataDimension.SeriesDimensionDefault; - const valueMetric = TransactionExplorerValueMetric.valueOf(transactionExplorerFilter.value.valueMetric); + const chartType = TransactionExplorerChartType.valueOf(currentInsightsExplorer.value.chartType); + const categoryDimension = TransactionExplorerDataDimension.valueOf(currentInsightsExplorer.value.categoryDimension); + const seriesDimension = chartType?.seriesDimensionRequired ? TransactionExplorerDataDimension.valueOf(currentInsightsExplorer.value.seriesDimension) : TransactionExplorerDataDimension.SeriesDimensionDefault; + const valueMetric = TransactionExplorerValueMetric.valueOf(currentInsightsExplorer.value.valueMetric); if (!chartType || !categoryDimension || !seriesDimension || !valueMetric) { return []; @@ -699,18 +730,20 @@ export const useExplorersStore = defineStore('explorers', () => { transactionExplorerStateInvalid.value = invalidState; } + function updateInsightsExplorerListInvalidState(invalidState: boolean): void { + insightsExplorerListStateInvalid.value = invalidState; + } + + function updateCurrentInsightsExplorer(explorer: InsightsExplorer): void { + currentInsightsExplorer.value = explorer; + } + function resetTransactionExplorers(): void { transactionExplorerFilter.value.dateRangeType = DEFAULT_TRANSACTION_EXPLORER_DATE_RANGE.type; transactionExplorerFilter.value.startTime = 0; transactionExplorerFilter.value.endTime = 0; - transactionExplorerFilter.value.query = []; - transactionExplorerFilter.value.timezoneUsedForDateRange = TimezoneTypeForStatistics.Default.type; - transactionExplorerFilter.value.chartType = TransactionExplorerChartType.Default.value; - transactionExplorerFilter.value.categoryDimension = TransactionExplorerDataDimension.CategoryDimensionDefault.value; - transactionExplorerFilter.value.seriesDimension = TransactionExplorerDataDimension.SeriesDimensionDefault.value; - transactionExplorerFilter.value.valueMetric = TransactionExplorerValueMetric.Default.value; - transactionExplorerFilter.value.chartSortingType = ChartSortingType.Default.type; transactionExplorerAllData.value = []; + currentInsightsExplorer.value = InsightsExplorer.createNewExplorer(); transactionExplorerStateInvalid.value = true; } @@ -751,13 +784,7 @@ export const useExplorersStore = defineStore('explorers', () => { } if (resetQuery) { - transactionExplorerFilter.value.query = []; - transactionExplorerFilter.value.timezoneUsedForDateRange = TimezoneTypeForStatistics.Default.type; - transactionExplorerFilter.value.chartType = TransactionExplorerChartType.Default.value; - transactionExplorerFilter.value.categoryDimension = TransactionExplorerDataDimension.CategoryDimensionDefault.value; - transactionExplorerFilter.value.seriesDimension = TransactionExplorerDataDimension.SeriesDimensionDefault.value; - transactionExplorerFilter.value.valueMetric = TransactionExplorerValueMetric.Default.value; - transactionExplorerFilter.value.chartSortingType = ChartSortingType.Default.type; + currentInsightsExplorer.value = InsightsExplorer.createNewExplorer(); } } @@ -779,49 +806,14 @@ export const useExplorersStore = defineStore('explorers', () => { changed = true; } - if (filter && isDefined(filter.timezoneUsedForDateRange) && transactionExplorerFilter.value.timezoneUsedForDateRange !== filter.timezoneUsedForDateRange) { - transactionExplorerFilter.value.timezoneUsedForDateRange = filter.timezoneUsedForDateRange; - changed = true; - } - - if (filter && isDefined(filter.chartType) && transactionExplorerFilter.value.chartType !== filter.chartType) { - transactionExplorerFilter.value.chartType = filter.chartType; - changed = true; - } - - if (filter && isDefined(filter.categoryDimension) && transactionExplorerFilter.value.categoryDimension !== filter.categoryDimension) { - transactionExplorerFilter.value.categoryDimension = filter.categoryDimension; - changed = true; - } - - if (filter && isDefined(filter.seriesDimension) && transactionExplorerFilter.value.seriesDimension !== filter.seriesDimension) { - transactionExplorerFilter.value.seriesDimension = filter.seriesDimension; - changed = true; - } - - if (filter && isDefined(filter.valueMetric) && transactionExplorerFilter.value.valueMetric !== filter.valueMetric) { - transactionExplorerFilter.value.valueMetric = filter.valueMetric; - changed = true; - } - - if (filter && isDefined(filter.chartSortingType) && transactionExplorerFilter.value.chartSortingType !== filter.chartSortingType) { - transactionExplorerFilter.value.chartSortingType = filter.chartSortingType; - changed = true; - } - - if (transactionExplorerFilter.value.seriesDimension === transactionExplorerFilter.value.categoryDimension && transactionExplorerFilter.value.seriesDimension !== TransactionExplorerDataDimension.SeriesDimensionDefault.value) { - transactionExplorerFilter.value.seriesDimension = TransactionExplorerDataDimension.SeriesDimensionDefault.value; - changed = true; - } - return changed; } - function getTransactionExplorerPageParams(currentExplorationId: string, activeTab: string): string { + function getTransactionExplorerPageParams(currentExplorerId: string, activeTab: string): string { const querys: string[] = []; - if (currentExplorationId) { - querys.push('id=' + currentExplorationId); + if (currentExplorerId) { + querys.push('id=' + currentExplorerId); } if (activeTab) { @@ -897,20 +889,298 @@ export const useExplorersStore = defineStore('explorers', () => { }); } + function loadAllInsightsExplorerBasicInfos({ force }: { force?: boolean }): Promise { + if (!force && !insightsExplorerListStateInvalid.value) { + return new Promise((resolve) => { + resolve(allInsightsExplorerBasicInfos.value); + }); + } + + return new Promise((resolve, reject) => { + services.getAllInsightsExplorers().then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to retrieve explorer list' }); + return; + } + + if (insightsExplorerListStateInvalid.value) { + updateInsightsExplorerListInvalidState(false); + } + + const explorerBasicInfos = InsightsExplorerBasicInfo.ofMulti(data.result); + + if (force && data.result && isEquals(allInsightsExplorerBasicInfos.value, explorerBasicInfos)) { + reject({ message: 'Explorer list is up to date', isUpToDate: true }); + return; + } + + loadInsightsExplorerList(explorerBasicInfos); + + resolve(explorerBasicInfos); + }).catch(error => { + if (force) { + logger.error('failed to force load explorer list', error); + } else { + logger.error('failed to load explorer list', error); + } + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to retrieve explorer list' }); + } else { + reject(error); + } + }); + }); + } + + function getInsightsExplorer({ explorerId }: { explorerId: string }): Promise { + return new Promise((resolve, reject) => { + services.getInsightsExplorer({ + id: explorerId + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to retrieve explorer' }); + return; + } + + const transactionCategory = InsightsExplorer.of(data.result); + + resolve(transactionCategory); + }).catch(error => { + logger.error('failed to load explorer info', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to retrieve explorer' }); + } else { + reject(error); + } + }); + }); + } + + function saveInsightsExplorer({ explorer, saveAs, clientSessionId }: { explorer: InsightsExplorer, saveAs?: boolean, clientSessionId: string }): Promise { + return new Promise((resolve, reject) => { + let promise: ApiResponsePromise; + + if (!explorer.id || saveAs) { + promise = services.addInsightsExplorer(explorer.toCreateRequest(clientSessionId)); + } else { + promise = services.modifyInsightsExplorer(explorer.toModifyRequest()); + } + + promise.then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + if (!explorer.id) { + reject({ message: 'Unable to add explorer' }); + } else { + reject({ message: 'Unable to save explorer' }); + } + return; + } + + const explorerBasicInfo = InsightsExplorerBasicInfo.of(data.result); + + if (!explorer.id || saveAs) { + addExplorerToInsightsExplorerList(explorerBasicInfo); + } else { + updateExplorerInInsightsExplorerList(explorerBasicInfo); + } + + resolve(InsightsExplorer.of(data.result)); + }).catch(error => { + logger.error('failed to save explorer', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + if (!explorer.id) { + reject({ message: 'Unable to add explorer' }); + } else { + reject({ message: 'Unable to save explorer' }); + } + } else { + reject(error); + } + }); + }); + } + + function changeInsightsExplorerDisplayOrder({ explorerId, from, to }: { explorerId: string, from: number, to: number }): Promise { + return new Promise((resolve, reject) => { + let currentExplorer: InsightsExplorerBasicInfo | null = null; + + for (const insightsExplorer of allInsightsExplorerBasicInfos.value) { + if (insightsExplorer.id === explorerId) { + currentExplorer = insightsExplorer; + break; + } + } + + if (!currentExplorer || !allInsightsExplorerBasicInfos.value[to]) { + reject({ message: 'Unable to move explorer' }); + return; + } + + if (!insightsExplorerListStateInvalid.value) { + updateInsightsExplorerListInvalidState(true); + } + + updateExplorerDisplayOrderInInsightsExplorerList({ from, to }); + + resolve(); + }); + } + + function updateInsightsExplorerDisplayOrders(): Promise { + const newDisplayOrders: InsightsExplorerNewDisplayOrderRequest[] = []; + + for (const [insightsExplorer, index] of itemAndIndex(allInsightsExplorerBasicInfos.value)) { + newDisplayOrders.push({ + id: insightsExplorer.id, + displayOrder: index + 1 + }); + } + + return new Promise((resolve, reject) => { + services.moveInsightsExplorer({ + newDisplayOrders: newDisplayOrders + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to move explorer' }); + return; + } + + if (insightsExplorerListStateInvalid.value) { + updateInsightsExplorerListInvalidState(false); + } + + resolve(data.result); + }).catch(error => { + logger.error('failed to save explorers display order', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to move explorer' }); + } else { + reject(error); + } + }); + }); + } + + function hideInsightsExplorer({ explorer, hidden }: { explorer: InsightsExplorer, hidden: boolean }): Promise { + return new Promise((resolve, reject) => { + services.hideInsightsExplorer({ + id: explorer.id, + hidden: hidden + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + if (hidden) { + reject({ message: 'Unable to hide this explorer' }); + } else { + reject({ message: 'Unable to unhide this explorer' }); + } + return; + } + + updateExplorerVisibilityInInsightsExplorerList({ explorer: explorer, hidden }); + + resolve(data.result); + }).catch(error => { + logger.error('failed to change explorer visibility', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + if (hidden) { + reject({ message: 'Unable to hide this explorer' }); + } else { + reject({ message: 'Unable to unhide this explorer' }); + } + } else { + reject(error); + } + }); + }); + } + + function deleteInsightsExplorer({ explorer, beforeResolve }: { explorer: InsightsExplorer, beforeResolve?: BeforeResolveFunction }): Promise { + return new Promise((resolve, reject) => { + services.deleteInsightsExplorer({ + id: explorer.id + }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to delete this explorer' }); + return; + } + + if (beforeResolve) { + beforeResolve(() => { + removeExplorerFromInsightsExplorerList(explorer); + }); + } else { + removeExplorerFromInsightsExplorerList(explorer); + } + + resolve(data.result); + }).catch(error => { + logger.error('failed to delete explorer', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to delete this explorer' }); + } else { + reject(error); + } + }); + }); + } + return { // states - transactionExplorerFilter: transactionExplorerFilter, + transactionExplorerFilter, transactionExplorerStateInvalid, + allInsightsExplorerBasicInfos, + allInsightsExplorerBasicInfosMap, + currentInsightsExplorer, + insightsExplorerListStateInvalid, // computed filteredTransactions, categoriedTransactionExplorerData, // functions updateTransactionExplorerInvalidState, + updateInsightsExplorerListInvalidState, + updateCurrentInsightsExplorer, resetTransactionExplorers, initTransactionExplorerFilter, updateTransactionExplorerFilter, getTransactionExplorerPageParams, getTransactionListPageParams, - loadAllTransactions + loadAllTransactions, + loadAllInsightsExplorerBasicInfos, + getInsightsExplorer, + saveInsightsExplorer, + changeInsightsExplorerDisplayOrder, + updateInsightsExplorerDisplayOrders, + hideInsightsExplorer, + deleteInsightsExplorer }; }); diff --git a/src/views/desktop/insights/ExplorerPage.vue b/src/views/desktop/insights/ExplorerPage.vue index 34790462..6f4c0c0e 100644 --- a/src/views/desktop/insights/ExplorerPage.vue +++ b/src/views/desktop/insights/ExplorerPage.vue @@ -5,24 +5,22 @@
- +
-
- {{ tt('Transactions Per Page') }} - -
- - {{ tt('New Exploration') }} + :disabled="loading || updating" :model-value="currentExplorer.id"> + + {{ tt('New Explorer') }} + + {{ explorer.name || tt('Untitled Explorer') }} + + + +
@@ -36,23 +34,23 @@ {{ tt('Insights Explorer') }} - +
{{ dateRange.displayName }}
-
+
{{ displayQueryStartTime }}  - 
@@ -63,12 +61,12 @@ + class="ms-2" :icon="true" :loading="loading" :disabled="updating" @click="reload(true)"> @@ -76,8 +74,28 @@ {{ tt('Refresh') }} + + {{ tt('Save Explorer') }} + + + + + {{ tt('Save As New Explorer') }} + + + + {{ tt('Rename Explorer') }} + + + + {{ tt('Delete Explorer') }} + + + + + :disabled="loading || updating" :icon="true"> @@ -86,14 +104,14 @@ @@ -104,17 +122,16 @@ - + + :loading="loading" :disabled="loading || updating" /> @@ -125,25 +142,29 @@ + + + diff --git a/src/views/desktop/insights/tabs/ExplorerChartTab.vue b/src/views/desktop/insights/tabs/ExplorerChartTab.vue index 9ecc8697..1b350ac2 100644 --- a/src/views/desktop/insights/tabs/ExplorerChartTab.vue +++ b/src/views/desktop/insights/tabs/ExplorerChartTab.vue @@ -9,11 +9,11 @@ item-title="name" item-value="value" density="compact" - :disabled="loading" + :disabled="loading || disabled" :label="tt('Chart Type')" :items="allTransactionExplorerChartTypes" - :model-value="currentChartType" - @update:model-value="updateChartType($event as TransactionExplorerChartTypeValue)" + :model-value="currentExplorer.chartType" + @update:model-value="currentExplorer.chartType = $event as TransactionExplorerChartTypeValue" />