This is an automated email from the ASF dual-hosted git repository.

klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 691b14f01 feat: support teambition plugin (#8466)
691b14f01 is described below

commit 691b14f01750246db097b3c0466c9a34d470148a
Author: zfan <[email protected]>
AuthorDate: Fri Jun 20 12:12:41 2025 +0800

    feat: support teambition plugin (#8466)
    
    * feat: support teambition plugin
    
    * fix: run CI
    
    * fix: typo
---
 backend/plugins/teambition/api/blueprint_v200.go   |   4 +-
 backend/plugins/teambition/api/init.go             |  11 +-
 backend/plugins/teambition/api/remote_api.go       | 170 +++++++++++++++++++++
 backend/plugins/teambition/api/scope_api.go        | 107 +++++++++++++
 backend/plugins/teambition/api/scope_config_api.go | 111 ++++++++++++++
 backend/plugins/teambition/impl/impl.go            |  29 +++-
 .../migrationscripts/20250529_add_app_id_back.go   |  70 +++++++++
 .../teambition/models/migrationscripts/register.go |   1 +
 backend/plugins/teambition/models/project.go       |  14 +-
 backend/plugins/teambition/models/scope_config.go  |  40 +++++
 .../plugins/teambition/tasks/project_convertor.go  |  15 +-
 backend/plugins/teambition/tasks/shared.go         |  21 ---
 backend/plugins/teambition/tasks/task_collector.go |   5 +-
 backend/plugins/teambition/tasks/task_converter.go |  43 +++---
 backend/plugins/teambition/tasks/task_extractor.go |   3 +-
 .../teambition/tasks/task_scenario_collector.go    |   7 +-
 .../plugins/teambition/tasks/task_tag_extractor.go |   3 +-
 config-ui/src/plugins/register/index.ts            |   2 +
 .../plugins/register/teambition/assets/icon.svg    |   5 +
 .../src/plugins/register/teambition/config.tsx     |  97 ++++++++++++
 .../register/teambition/connection-fields/index.ts |  20 +++
 .../teambition/connection-fields/tenant-id.tsx     |  66 ++++++++
 .../teambition/connection-fields/tenant-type.tsx   |  66 ++++++++
 config-ui/src/plugins/register/teambition/index.ts |  19 +++
 24 files changed, 863 insertions(+), 66 deletions(-)

diff --git a/backend/plugins/teambition/api/blueprint_v200.go 
b/backend/plugins/teambition/api/blueprint_v200.go
index c71d5b644..e85a3dd46 100644
--- a/backend/plugins/teambition/api/blueprint_v200.go
+++ b/backend/plugins/teambition/api/blueprint_v200.go
@@ -53,7 +53,7 @@ func MakeDataSourcePipelinePlanV200(
 
 func makePipelinePlanV200(
        subtaskMetas []plugin.SubTaskMeta,
-       scopeDetails []*srvhelper.ScopeDetail[models.TeambitionProject, 
srvhelper.NoScopeConfig],
+       scopeDetails []*srvhelper.ScopeDetail[models.TeambitionProject, 
models.TeambitionScopeConfig],
        connection *models.TeambitionConnection,
 ) (coreModels.PipelinePlan, errors.Error) {
        plan := make(coreModels.PipelinePlan, len(scopeDetails))
@@ -85,7 +85,7 @@ func makePipelinePlanV200(
 }
 
 func makeScopesV200(
-       scopeDetails []*srvhelper.ScopeDetail[models.TeambitionProject, 
srvhelper.NoScopeConfig],
+       scopeDetails []*srvhelper.ScopeDetail[models.TeambitionProject, 
models.TeambitionScopeConfig],
        connection *models.TeambitionConnection,
 ) ([]plugin.Scope, errors.Error) {
        scopes := make([]plugin.Scope, 0, len(scopeDetails))
diff --git a/backend/plugins/teambition/api/init.go 
b/backend/plugins/teambition/api/init.go
index f1e3e0c8b..d429405af 100644
--- a/backend/plugins/teambition/api/init.go
+++ b/backend/plugins/teambition/api/init.go
@@ -21,14 +21,16 @@ import (
        "github.com/apache/incubator-devlake/core/context"
        "github.com/apache/incubator-devlake/core/plugin"
        "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
-       "github.com/apache/incubator-devlake/helpers/srvhelper"
        "github.com/apache/incubator-devlake/plugins/teambition/models"
        "github.com/go-playground/validator/v10"
 )
 
 var vld *validator.Validate
-var dsHelper *api.DsHelper[models.TeambitionConnection, 
models.TeambitionProject, srvhelper.NoScopeConfig]
+var dsHelper *api.DsHelper[models.TeambitionConnection, 
models.TeambitionProject, models.TeambitionScopeConfig]
 var basicRes context.BasicRes
+var raProxy *api.DsRemoteApiProxyHelper[models.TeambitionConnection]
+var raScopeList *api.DsRemoteApiScopeListHelper[models.TeambitionConnection, 
models.TeambitionProject, TeambitionPagination]
+var raScopeSearch 
*api.DsRemoteApiScopeSearchHelper[models.TeambitionConnection, 
models.TeambitionProject]
 
 func Init(br context.BasicRes, p plugin.PluginMeta) {
        basicRes = br
@@ -36,7 +38,7 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
        dsHelper = api.NewDataSourceHelper[
                models.TeambitionConnection,
                models.TeambitionProject,
-               srvhelper.NoScopeConfig,
+               models.TeambitionScopeConfig,
        ](
                br,
                p.Name(),
@@ -47,4 +49,7 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
                nil,
                nil,
        )
+       raProxy = api.NewDsRemoteApiProxyHelper(dsHelper.ConnApi.ModelApiHelper)
+       raScopeList = api.NewDsRemoteApiScopeListHelper(raProxy, 
listTeambitionRemoteScopes)
+       raScopeSearch = api.NewDsRemoteApiScopeSearchHelper(raProxy, 
searchTeambitionRemoteProjects)
 }
diff --git a/backend/plugins/teambition/api/remote_api.go 
b/backend/plugins/teambition/api/remote_api.go
new file mode 100644
index 000000000..97ced7352
--- /dev/null
+++ b/backend/plugins/teambition/api/remote_api.go
@@ -0,0 +1,170 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+       "fmt"
+       "net/url"
+
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       dsmodels 
"github.com/apache/incubator-devlake/helpers/pluginhelper/api/models"
+       "github.com/apache/incubator-devlake/plugins/teambition/models"
+)
+
+type TeambitionPagination struct {
+       PageToken string `json:"pageToken"`
+       PageSize  int    `json:"pageSize"`
+}
+
+func queryTeambitionProjects(
+       apiClient plugin.ApiClient,
+       keyword string,
+       page TeambitionPagination,
+) (
+       children []dsmodels.DsRemoteApiScopeListEntry[models.TeambitionProject],
+       nextPage *TeambitionPagination,
+       err errors.Error,
+) {
+       if page.PageSize == 0 {
+               page.PageSize = 50
+       }
+       res, err := apiClient.Get("v3/project/query", url.Values{
+               "name":      {keyword},
+               "pageSize":  {fmt.Sprintf("%v", page.PageSize)},
+               "pageToken": {page.PageToken},
+       }, nil)
+       if err != nil {
+               return
+       }
+       resBody := struct {
+               Result        []models.TeambitionProject `json:"result"`
+               NextPageToken string                     `json:"nextPageToken"`
+       }{}
+       err = api.UnmarshalResponse(res, &resBody)
+       if err != nil {
+               return
+       }
+       for _, project := range resBody.Result {
+               children = append(children, 
dsmodels.DsRemoteApiScopeListEntry[models.TeambitionProject]{
+                       Type:     api.RAS_ENTRY_TYPE_SCOPE,
+                       Id:       fmt.Sprintf("%v", project.Id),
+                       ParentId: nil,
+                       Name:     project.Name,
+                       FullName: project.Name,
+                       Data:     &project,
+               })
+       }
+       if resBody.NextPageToken != "" {
+               nextPage = &TeambitionPagination{
+                       PageToken: resBody.NextPageToken,
+                       PageSize:  page.PageSize,
+               }
+       }
+       return
+}
+
+func listTeambitionRemoteScopes(
+       connection *models.TeambitionConnection,
+       apiClient plugin.ApiClient,
+       groupId string,
+       page TeambitionPagination,
+) (
+       children []dsmodels.DsRemoteApiScopeListEntry[models.TeambitionProject],
+       nextPage *TeambitionPagination,
+       err errors.Error,
+) {
+       // construct the query and request
+       return queryTeambitionProjects(apiClient, "", page)
+}
+
+func searchTeambitionRemoteProjects(
+       apiClient plugin.ApiClient,
+       params *dsmodels.DsRemoteApiScopeSearchParams,
+) (
+       children []dsmodels.DsRemoteApiScopeListEntry[models.TeambitionProject],
+       err errors.Error,
+) {
+       if params.Page == 0 {
+               params.Page = 1
+       }
+       page := TeambitionPagination{
+               PageSize: params.PageSize,
+       }
+       children, _, err = queryTeambitionProjects(apiClient, params.Search, 
page)
+       return
+}
+
+type Entry = dsmodels.DsRemoteApiScopeListEntry[models.TeambitionProject]
+type Node struct {
+       entry *Entry
+}
+type Children []*Node
+
+func (a Children) Len() int { return len(a) }
+func (a Children) Less(i, j int) bool {
+       if a[i].entry.Type != a[j].entry.Type {
+               return a[i].entry.Type < a[j].entry.Type
+       }
+       return a[i].entry.Name < a[j].entry.Name
+}
+func (a Children) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+
+// RemoteScopes list all available scope for users
+// @Summary list all available scope for users
+// @Description list all available scope for users
+// @Tags plugins/tapd
+// @Accept application/json
+// @Param connectionId path int false "connection ID"
+// @Param groupId query string false "group ID"
+// @Param pageToken query string false "page Token"
+// @Success 200  {object} dsmodels.DsRemoteApiScopeList[models.TapdWorkspace]
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/teambition/connections/{connectionId}/remote-scopes [GET]
+func RemoteScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
+       return raScopeList.Get(input)
+}
+
+// SearchRemoteScopes searches scopes on the remote server
+// @Summary searches scopes on the remote server
+// @Description searches scopes on the remote server
+// @Accept application/json
+// @Param connectionId path int false "connection ID"
+// @Param search query string false "search"
+// @Param page query int false "page number"
+// @Param pageSize query int false "page size per page"
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Success 200  {object} 
dsmodels.DsRemoteApiScopeList[models.SonarqubeProject] "the parentIds are 
always null"
+// @Tags plugins/sonarqube
+// @Router /plugins/sonarqube/connections/{connectionId}/search-remote-scopes 
[GET]
+func SearchRemoteScopes(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return raScopeSearch.Get(input)
+}
+
+// @Summary Remote server API proxy
+// @Description Forward API requests to the specified remote server
+// @Param connectionId path int true "connection ID"
+// @Param path path string true "path to a API endpoint"
+// @Tags plugins/github
+// @Router /plugins/teambition/connections/{connectionId}/proxy/{path} [GET]
+func Proxy(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
+       return raProxy.Proxy(input)
+}
diff --git a/backend/plugins/teambition/api/scope_api.go 
b/backend/plugins/teambition/api/scope_api.go
new file mode 100644
index 000000000..918297269
--- /dev/null
+++ b/backend/plugins/teambition/api/scope_api.go
@@ -0,0 +1,107 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/teambition/models"
+)
+
+type PutScopesReqBody api.PutScopesReqBody[models.TeambitionProject]
+type ScopeDetail api.ScopeDetail[models.TeambitionProject, 
models.TeambitionScopeConfig]
+
+// PutScopes create or update Azure DevOps repo
+// @Summary create or update Azure DevOps repo
+// @Description Create or update Azure DevOps repo
+// @Tags plugins/teambition
+// @Accept application/json
+// @Param connectionId path int true "connection ID"
+// @Param scope body PutScopesReqBody true "json"
+// @Success 200  {object} []models.teambitionRepo
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/teambition/connections/{connectionId}/scopes [PUT]
+func PutScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
+       return dsHelper.ScopeApi.PutMultiple(input)
+}
+
+// PatchScope patch to Azure DevOps repo
+// @Summary patch to Azure DevOps repo
+// @Description patch to Azure DevOps repo
+// @Tags plugins/teambition
+// @Accept application/json
+// @Param connectionId path int true "connection ID"
+// @Param scopeId path int true "scope ID"
+// @Param scope body models.teambitionRepo true "json"
+// @Success 200  {object} models.teambitionRepo
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/teambition/connections/{connectionId}/scopes/{scopeId} 
[PATCH]
+func PatchScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
+       return dsHelper.ScopeApi.Patch(input)
+}
+
+// GetScopes get Azure DevOps repos
+// @Summary get Azure DevOps repos
+// @Description get Azure DevOps repos
+// @Tags plugins/teambition
+// @Param connectionId path int true "connection ID"
+// @Param searchTerm query string false "search term for scope name"
+// @Param pageSize query int false "page size, default 50"
+// @Param page query int false "page size, default 1"
+// @Param blueprints query bool false "also return blueprints using these 
scopes as part of the payload"
+// @Success 200  {object} []ScopeDetail
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/teambition/connections/{connectionId}/scopes [GET]
+func GetScopes(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
+       return dsHelper.ScopeApi.GetPage(input)
+}
+
+// GetScope get one Azure DevOps repo
+// @Summary get one Azure DevOps repo
+// @Description get one Azure DevOps repo
+// @Tags plugins/teambition
+// @Param connectionId path int true "connection ID"
+// @Param scopeId path int true "scope ID"
+// @Param blueprints query bool false "also return blueprints using these 
scopes as part of the payload"
+// @Success 200  {object} ScopeDetail
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/teambition/connections/{connectionId}/scopes/{scopeId} 
[GET]
+func GetScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
+       return dsHelper.ScopeApi.GetScopeDetail(input)
+}
+
+// DeleteScope delete plugin data associated with the scope and optionally the 
scope itself
+// @Summary delete plugin data associated with the scope and optionally the 
scope itself
+// @Description delete data associated with plugin scope
+// @Tags plugins/teambition
+// @Param connectionId path int true "connection ID"
+// @Param scopeId path int true "scope ID"
+// @Param delete_data_only query bool false "Only delete the scope data, not 
the scope itself"
+// @Success 200  {object} models.teambitionRepo
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 409  {object} srvhelper.DsRefs "References exist to this scope"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/teambition/connections/{connectionId}/scopes/{scopeId} 
[DELETE]
+func DeleteScope(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
+       return dsHelper.ScopeApi.Delete(input)
+}
diff --git a/backend/plugins/teambition/api/scope_config_api.go 
b/backend/plugins/teambition/api/scope_config_api.go
new file mode 100644
index 000000000..6294cb5c7
--- /dev/null
+++ b/backend/plugins/teambition/api/scope_config_api.go
@@ -0,0 +1,111 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+)
+
+// PostScopeConfig create scope config for Azure DevOps
+// @Summary create scope config for Azure DevOps
+// @Description create scope config for Azure DevOps
+// @Tags plugins/azuredevops
+// @Accept application/json
+// @Param connectionId path int true "connectionId"
+// @Param scopeConfig body models.AzuredevopsScopeConfig true "scope config"
+// @Success 200  {object} models.AzuredevopsScopeConfig
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/azuredevops/connections/{connectionId}/scope-configs [POST]
+func PostScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return dsHelper.ScopeConfigApi.Post(input)
+}
+
+// PatchScopeConfig update scope config for Azure DevOps
+// @Summary update scope config for Azure DevOps
+// @Description update scope config for Azure DevOps
+// @Tags plugins/azuredevops
+// @Accept application/json
+// @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
+// @Param scopeConfig body models.AzuredevopsScopeConfig true "scope config"
+// @Success 200  {object} models.AzuredevopsScopeConfig
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/azuredevops/connections/{connectionId}/scope-configs/{id} 
[PATCH]
+func PatchScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return dsHelper.ScopeConfigApi.Patch(input)
+}
+
+// GetScopeConfig return one scope config
+// @Summary return one scope config
+// @Description return one scope config
+// @Tags plugins/azuredevops
+// @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
+// @Success 200  {object} models.AzuredevopsScopeConfig
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/azuredevops/connections/{connectionId}/scope-configs/{id} 
[GET]
+func GetScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return dsHelper.ScopeConfigApi.GetDetail(input)
+}
+
+// GetScopeConfigList return all scope configs
+// @Summary return all scope configs
+// @Description return all scope configs
+// @Tags plugins/azuredevops
+// @Param pageSize query int false "page size, default 50"
+// @Param page query int false "page size, default 1"
+// @Param connectionId path int true "connectionId"
+// @Success 200  {object} []models.AzuredevopsScopeConfig
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/azuredevops/connections/{connectionId}/scope-configs [GET]
+func GetScopeConfigList(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return dsHelper.ScopeConfigApi.GetAll(input)
+}
+
+// GetProjectsByScopeConfig return projects details related by scope config
+// @Summary return all related projects
+// @Description return all related projects
+// @Tags plugins/azuredevops
+// @Param id path int true "id"
+// @Param scopeConfigId path int true "scopeConfigId"
+// @Success 200  {object} models.ProjectScopeOutput
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/azuredevops/scope-config/{scopeConfigId}/projects [GET]
+func GetProjectsByScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return dsHelper.ScopeConfigApi.GetProjectsByScopeConfig(input)
+}
+
+// DeleteScopeConfig delete a scope config
+// @Summary delete a scope config
+// @Description delete a scope config
+// @Tags plugins/azuredevops
+// @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
+// @Success 200
+// @Failure 400  {object} shared.ApiBody "Bad Request"
+// @Failure 500  {object} shared.ApiBody "Internal Error"
+// @Router /plugins/azuredevops/connections/{connectionId}/scope-configs/{id} 
[DELETE]
+func DeleteScopeConfig(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       return dsHelper.ScopeConfigApi.Delete(input)
+}
diff --git a/backend/plugins/teambition/impl/impl.go 
b/backend/plugins/teambition/impl/impl.go
index 2140dfe6d..a9aa53a72 100644
--- a/backend/plugins/teambition/impl/impl.go
+++ b/backend/plugins/teambition/impl/impl.go
@@ -39,8 +39,10 @@ var _ interface {
        plugin.PluginInit
        plugin.PluginTask
        plugin.PluginApi
-       plugin.CloseablePluginTask
+       plugin.PluginModel
        plugin.PluginSource
+       plugin.DataSourcePluginBlueprintV200
+       plugin.CloseablePluginTask
 } = (*Teambition)(nil)
 
 type Teambition struct{}
@@ -72,6 +74,7 @@ func (p Teambition) GetTablesInfo() []dal.Tabler {
                &models.TeambitionProject{},
                &models.TeambitionTaskFlowStatus{},
                &models.TeambitionTaskScenario{},
+               &models.TeambitionScopeConfig{},
        }
 }
 
@@ -180,6 +183,30 @@ func (p Teambition) ApiResources() 
map[string]map[string]plugin.ApiResourceHandl
                "connections/:connectionId/test": {
                        "POST": api.TestExistingConnection,
                },
+               "connections/:connectionId/remote-scopes": {
+                       "GET": api.RemoteScopes,
+               },
+               "connections/:connectionId/search-remote-scopes": {
+                       "GET": api.SearchRemoteScopes,
+               },
+               "connections/:connectionId/scopes": {
+                       "GET": api.GetScopes,
+                       "PUT": api.PutScopes,
+               },
+               "connections/:connectionId/scope-configs/:scopeConfigId": {
+                       "PATCH":  api.PatchScopeConfig,
+                       "GET":    api.GetScopeConfig,
+                       "DELETE": api.DeleteScopeConfig,
+               },
+               "connections/:connectionId/scope-configs": {
+                       "POST": api.PostScopeConfig,
+                       "GET":  api.GetScopeConfigList,
+               },
+               "connections/:connectionId/scopes/:scopeId": {
+                       "GET":    api.GetScope,
+                       "PATCH":  api.PatchScope,
+                       "DELETE": api.DeleteScope,
+               },
        }
 }
 
diff --git 
a/backend/plugins/teambition/models/migrationscripts/20250529_add_app_id_back.go
 
b/backend/plugins/teambition/models/migrationscripts/20250529_add_app_id_back.go
new file mode 100644
index 000000000..855bab0a3
--- /dev/null
+++ 
b/backend/plugins/teambition/models/migrationscripts/20250529_add_app_id_back.go
@@ -0,0 +1,70 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+       "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/migrationhelper"
+)
+
+var _ plugin.MigrationScript = (*addAppIdBack)(nil)
+
+type teambitionConnection20250529 struct {
+       AppId     string
+       SecretKey string `gorm:"serializer:encdec"`
+}
+
+func (teambitionConnection20250529) TableName() string {
+       return "_tool_teambition_connections"
+}
+
+type teambitionScopeConfig20250529 struct {
+       Entities          []string          `gorm:"type:json;serializer:json" 
json:"entities" mapstructure:"entities"`
+       ConnectionId      uint64            `json:"connectionId" gorm:"index" 
validate:"required" mapstructure:"connectionId,omitempty"`
+       Name              string            `mapstructure:"name" json:"name" 
gorm:"type:varchar(255);uniqueIndex" validate:"required"`
+       TypeMappings      map[string]string 
`mapstructure:"typeMappings,omitempty" json:"typeMappings" 
gorm:"serializer:json"`
+       StatusMappings    map[string]string 
`mapstructure:"statusMappings,omitempty" json:"statusMappings" 
gorm:"serializer:json"`
+       BugDueDateField   string            
`mapstructure:"bugDueDateField,omitempty" json:"bugDueDateField" 
gorm:"column:bug_due_date_field"`
+       TaskDueDateField  string            
`mapstructure:"taskDueDateField,omitempty" json:"taskDueDateField" 
gorm:"column:task_due_date_field"`
+       StoryDueDateField string            
`mapstructure:"storyDueDateField,omitempty" json:"storyDueDateField" 
gorm:"column:story_due_date_field"`
+}
+
+func (t teambitionScopeConfig20250529) TableName() string {
+       return "_tool_teambition_scope_configs"
+}
+
+type addAppIdBack struct{}
+
+func (*addAppIdBack) Up(basicRes context.BasicRes) errors.Error {
+       basicRes.GetLogger().Warn(nil, "*********")
+       err := migrationhelper.AutoMigrateTables(basicRes, 
&teambitionConnection20250529{})
+       basicRes.GetLogger().Warn(err, "err connection")
+       err = migrationhelper.AutoMigrateTables(basicRes, 
&teambitionScopeConfig20250529{})
+       basicRes.GetLogger().Warn(err, "err scope")
+       return err
+}
+
+func (*addAppIdBack) Version() uint64 {
+       return 20250529165745
+}
+
+func (*addAppIdBack) Name() string {
+       return "add app id back to teambition_connections"
+}
diff --git a/backend/plugins/teambition/models/migrationscripts/register.go 
b/backend/plugins/teambition/models/migrationscripts/register.go
index 0f5567d5b..f9914e7a1 100644
--- a/backend/plugins/teambition/models/migrationscripts/register.go
+++ b/backend/plugins/teambition/models/migrationscripts/register.go
@@ -25,5 +25,6 @@ func All() []plugin.MigrationScript {
                new(addInitTables),
                new(reCreateTeambitionConnections),
                new(addScopeConfigId),
+               new(addAppIdBack),
        }
 }
diff --git a/backend/plugins/teambition/models/project.go 
b/backend/plugins/teambition/models/project.go
index 0d50e8f0e..59531c34f 100644
--- a/backend/plugins/teambition/models/project.go
+++ b/backend/plugins/teambition/models/project.go
@@ -22,10 +22,12 @@ import (
        "github.com/apache/incubator-devlake/core/plugin"
 )
 
-var _ plugin.ToolLayerScope = (*TeambitionProject)(nil)
+func (t TeambitionProject) ConvertApiScope() plugin.ToolLayerScope {
+       return t
+}
 
 type TeambitionProject struct {
-       common.Scope
+       common.Scope   `mapstructure:",squash"`
        Id             string                         
`gorm:"primaryKey;type:varchar(100)" json:"id"`
        Name           string                         `gorm:"type:varchar(255)" 
json:"name"`
        Logo           string                         `gorm:"type:varchar(255)" 
json:"logo"`
@@ -88,3 +90,11 @@ type TeambitionApiParams struct {
        OrganizationId string
        ProjectId      string
 }
+
+type TeambitionProjectsResponse struct {
+       Status int `json:"status"`
+       Data   []struct {
+               TeambitionProject `json:"Workspace"`
+       } `json:"data"`
+       Info string `json:"info"`
+}
diff --git a/backend/plugins/teambition/models/scope_config.go 
b/backend/plugins/teambition/models/scope_config.go
new file mode 100644
index 000000000..d97585cdc
--- /dev/null
+++ b/backend/plugins/teambition/models/scope_config.go
@@ -0,0 +1,40 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+       "github.com/apache/incubator-devlake/core/models/common"
+)
+
+type TeambitionScopeConfig struct {
+       common.ScopeConfig `mapstructure:",squash" json:",inline" 
gorm:"embedded"`
+       TypeMappings       map[string]string 
`mapstructure:"typeMappings,omitempty" json:"typeMappings" 
gorm:"serializer:json"`
+       StatusMappings     map[string]string 
`mapstructure:"statusMappings,omitempty" json:"statusMappings" 
gorm:"serializer:json"`
+       BugDueDateField    string            
`mapstructure:"bugDueDateField,omitempty" json:"bugDueDateField" 
gorm:"column:bug_due_date_field"`
+       TaskDueDateField   string            
`mapstructure:"taskDueDateField,omitempty" json:"taskDueDateField" 
gorm:"column:task_due_date_field"`
+       StoryDueDateField  string            
`mapstructure:"storyDueDateField,omitempty" json:"storyDueDateField" 
gorm:"column:story_due_date_field"`
+}
+
+func (t TeambitionScopeConfig) TableName() string {
+       return "_tool_teambition_scope_configs"
+}
+
+func (t *TeambitionScopeConfig) SetConnectionId(c *TeambitionScopeConfig, 
connectionId uint64) {
+       c.ConnectionId = connectionId
+       c.ScopeConfig.ConnectionId = connectionId
+}
diff --git a/backend/plugins/teambition/tasks/project_convertor.go 
b/backend/plugins/teambition/tasks/project_convertor.go
index 53d6fe94c..abeddf245 100644
--- a/backend/plugins/teambition/tasks/project_convertor.go
+++ b/backend/plugins/teambition/tasks/project_convertor.go
@@ -19,6 +19,8 @@ package tasks
 
 import (
        "fmt"
+       "reflect"
+
        "github.com/apache/incubator-devlake/core/dal"
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/models/domainlayer"
@@ -26,11 +28,10 @@ import (
        "github.com/apache/incubator-devlake/core/plugin"
        helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
        "github.com/apache/incubator-devlake/plugins/teambition/models"
-       "reflect"
 )
 
 var ConvertProjectsMeta = plugin.SubTaskMeta{
-       Name:             "convertAccounts",
+       Name:             "convertProjects",
        EntryPoint:       ConvertProjects,
        EnabledByDefault: true,
        Description:      "convert teambition projects",
@@ -56,7 +57,7 @@ func ConvertProjects(taskCtx plugin.SubTaskContext) 
errors.Error {
                Input:              cursor,
                Convert: func(inputRow interface{}) ([]interface{}, 
errors.Error) {
                        userTool := inputRow.(*models.TeambitionProject)
-                       account := &ticket.Board{
+                       board := &ticket.Board{
                                DomainEntity: domainlayer.DomainEntity{
                                        Id: 
getProjectIdGen().Generate(data.Options.ConnectionId, userTool.Id),
                                },
@@ -65,9 +66,12 @@ func ConvertProjects(taskCtx plugin.SubTaskContext) 
errors.Error {
                                Url:         
fmt.Sprintf("https://www.teambition.com/project/%s";, userTool.Id),
                                CreatedDate: userTool.Created.ToNullableTime(),
                        }
-
+                       err := db.CreateOrUpdate(board)
+                       if err != nil {
+                               return nil, err
+                       }
                        return []interface{}{
-                               account,
+                               board,
                        }, nil
                },
        })
@@ -75,6 +79,5 @@ func ConvertProjects(taskCtx plugin.SubTaskContext) 
errors.Error {
        if err != nil {
                return err
        }
-
        return converter.Execute()
 }
diff --git a/backend/plugins/teambition/tasks/shared.go 
b/backend/plugins/teambition/tasks/shared.go
index 9831c69b0..7fbf56efe 100644
--- a/backend/plugins/teambition/tasks/shared.go
+++ b/backend/plugins/teambition/tasks/shared.go
@@ -18,8 +18,6 @@ limitations under the License.
 package tasks
 
 import (
-       "strings"
-
        "github.com/apache/incubator-devlake/core/dal"
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
@@ -103,25 +101,6 @@ func CreateRawDataSubTaskArgs(taskCtx 
plugin.SubTaskContext, rawTable string) (*
        return rawDataSubTaskArgs, &filteredData
 }
 
-func getStdTypeMappings(data *TeambitionTaskData) map[string]string {
-       stdTypeMappings := make(map[string]string)
-       for userType, stdType := range 
data.Options.TransformationRules.TypeMappings {
-               stdTypeMappings[userType] = 
strings.ToUpper(stdType.StandardType)
-       }
-       return stdTypeMappings
-}
-
-func getStatusMapping(data *TeambitionTaskData) map[string]string {
-       statusMapping := make(map[string]string)
-       mapping := data.Options.TransformationRules.StatusMappings
-       for std, orig := range mapping {
-               for _, v := range orig {
-                       statusMapping[v] = std
-               }
-       }
-       return statusMapping
-}
-
 func FindAccountById(db dal.Dal, accountId string) (*models.TeambitionAccount, 
errors.Error) {
        if accountId == "" {
                return nil, errors.Default.New("account id must not empty")
diff --git a/backend/plugins/teambition/tasks/task_collector.go 
b/backend/plugins/teambition/tasks/task_collector.go
index 14ab16557..429674990 100644
--- a/backend/plugins/teambition/tasks/task_collector.go
+++ b/backend/plugins/teambition/tasks/task_collector.go
@@ -20,11 +20,12 @@ package tasks
 import (
        "encoding/json"
        "fmt"
+       "net/http"
+       "net/url"
+
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/plugin"
        "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
-       "net/http"
-       "net/url"
 )
 
 const RAW_TASK_TABLE = "teambition_api_tasks"
diff --git a/backend/plugins/teambition/tasks/task_converter.go 
b/backend/plugins/teambition/tasks/task_converter.go
index fd212d379..aa2071d9c 100644
--- a/backend/plugins/teambition/tasks/task_converter.go
+++ b/backend/plugins/teambition/tasks/task_converter.go
@@ -21,6 +21,7 @@ import (
        "fmt"
        "reflect"
        "strconv"
+       "strings"
        "time"
 
        "github.com/apache/incubator-devlake/core/dal"
@@ -97,36 +98,30 @@ func ConvertTasks(taskCtx plugin.SubTaskContext) 
errors.Error {
                                issue.OriginalProject = p.Name
                        }
 
-                       stdStatusMappings := getStatusMapping(data)
                        if taskflowstatus, err := FindTaskFlowStatusById(db, 
userTool.TfsId); err == nil {
                                issue.OriginalStatus = taskflowstatus.Name
-                               if v, ok := 
stdStatusMappings[taskflowstatus.Name]; ok {
-                                       issue.Status = v
-                               } else {
-                                       switch taskflowstatus.Kind {
-                                       case "start":
-                                               issue.Status = ticket.TODO
-                                       case "unset":
-                                               issue.Status = 
ticket.IN_PROGRESS
-                                       case "end":
-                                               issue.Status = ticket.DONE
-                                       }
+                               switch strings.ToUpper(taskflowstatus.Kind) {
+                               case "START":
+                                       issue.Status = ticket.TODO
+                               case "UNSET":
+                                       issue.Status = ticket.IN_PROGRESS
+                               case "END":
+                                       issue.Status = ticket.DONE
+                               }
+                               if issue.Status == "" {
+                                       issue.Status = 
strings.ToUpper(taskflowstatus.Kind)
                                }
                        }
-                       stdTypeMappings := getStdTypeMappings(data)
+
                        if scenario, err := FindTaskScenarioById(db, 
userTool.SfcId); err == nil {
                                issue.OriginalType = scenario.Name
-                               if v, ok := stdTypeMappings[scenario.Name]; ok {
-                                       issue.Type = v
-                               } else {
-                                       switch scenario.Source {
-                                       case "application.bug":
-                                               issue.Type = ticket.BUG
-                                       case "application.story":
-                                               issue.Type = ticket.REQUIREMENT
-                                       case "application.risk":
-                                               issue.Type = ticket.INCIDENT
-                                       }
+                               switch scenario.Source {
+                               case "application.bug":
+                                       issue.Type = ticket.BUG
+                               case "application.story":
+                                       issue.Type = ticket.REQUIREMENT
+                               case "application.risk":
+                                       issue.Type = ticket.INCIDENT
                                }
                        }
 
diff --git a/backend/plugins/teambition/tasks/task_extractor.go 
b/backend/plugins/teambition/tasks/task_extractor.go
index a2c5a721b..abe14653c 100644
--- a/backend/plugins/teambition/tasks/task_extractor.go
+++ b/backend/plugins/teambition/tasks/task_extractor.go
@@ -19,6 +19,7 @@ package tasks
 
 import (
        "encoding/json"
+
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/plugin"
        "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
@@ -54,7 +55,7 @@ func ExtractTasks(taskCtx plugin.SubTaskContext) errors.Error 
{
                                        ConnectionId: data.Options.ConnectionId,
                                        TaskId:       userRes.Id,
                                        TaskTagId:    tagId,
-                                       ProjectId:    userRes.ProjectId,
+                                       ProjectId:    data.Options.ProjectId,
                                }
                                results = append(results, taskTag)
                        }
diff --git a/backend/plugins/teambition/tasks/task_scenario_collector.go 
b/backend/plugins/teambition/tasks/task_scenario_collector.go
index 5f89b1343..79348c54f 100644
--- a/backend/plugins/teambition/tasks/task_scenario_collector.go
+++ b/backend/plugins/teambition/tasks/task_scenario_collector.go
@@ -20,11 +20,12 @@ package tasks
 import (
        "encoding/json"
        "fmt"
+       "net/http"
+       "net/url"
+
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/plugin"
        "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
-       "net/http"
-       "net/url"
 )
 
 const RAW_TASK_SCENARIOS_TABLE = "teambition_api_task_scenarios"
@@ -32,7 +33,7 @@ const RAW_TASK_SCENARIOS_TABLE = 
"teambition_api_task_scenarios"
 var _ plugin.SubTaskEntryPoint = CollectTaskScenarios
 
 var CollectTaskScenariosMeta = plugin.SubTaskMeta{
-       Name:             "collect task flow status",
+       Name:             "collect task scenario",
        EntryPoint:       CollectTaskScenarios,
        EnabledByDefault: true,
        Description:      "collect teambition task flow scenarios",
diff --git a/backend/plugins/teambition/tasks/task_tag_extractor.go 
b/backend/plugins/teambition/tasks/task_tag_extractor.go
index 423d7432a..b7fdfbdcc 100644
--- a/backend/plugins/teambition/tasks/task_tag_extractor.go
+++ b/backend/plugins/teambition/tasks/task_tag_extractor.go
@@ -19,6 +19,7 @@ package tasks
 
 import (
        "encoding/json"
+
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/plugin"
        "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
@@ -49,7 +50,7 @@ func ExtractTaskTags(taskCtx plugin.SubTaskContext) 
errors.Error {
                                ConnectionId: data.Options.ConnectionId,
                                Id:           userRes.Id,
                                Name:         userRes.Name,
-                               ProjectId:    userRes.ProjectId,
+                               ProjectId:    data.Options.ProjectId,
                                IsArchived:   userRes.IsArchived,
                                Updated:      userRes.Updated,
                                Created:      userRes.Created,
diff --git a/config-ui/src/plugins/register/index.ts 
b/config-ui/src/plugins/register/index.ts
index ca06b5d1d..c97d72993 100644
--- a/config-ui/src/plugins/register/index.ts
+++ b/config-ui/src/plugins/register/index.ts
@@ -33,6 +33,7 @@ import { TAPDConfig } from './tapd';
 import { WebhookConfig } from './webhook';
 import { ZenTaoConfig } from './zentao';
 import { OpsgenieConfig } from './opsgenie';
+import { TeambitionConfig } from './teambition';
 
 export const pluginConfigs: IPluginConfig[] = [
   AzureConfig,
@@ -51,4 +52,5 @@ export const pluginConfigs: IPluginConfig[] = [
   ZenTaoConfig,
   WebhookConfig,
   OpsgenieConfig,
+  TeambitionConfig,
 ].sort((a, b) => a.sort - b.sort);
diff --git a/config-ui/src/plugins/register/teambition/assets/icon.svg 
b/config-ui/src/plugins/register/teambition/assets/icon.svg
new file mode 100644
index 000000000..93d227f72
--- /dev/null
+++ b/config-ui/src/plugins/register/teambition/assets/icon.svg
@@ -0,0 +1,5 @@
+<svg width="36" height="36" viewBox="0 0 36 36" fill="none" 
xmlns="http://www.w3.org/2000/svg";>
+<path d="M24.8601 7.75227C24.8606 6.58118 25.3105 5.45494 26.1169 
4.60578C26.9234 3.75663 28.025 3.24932 29.1945 3.18845C26.7937 3.06965 24.1226 
3 21.345 3C10.6606 3 2 3.98733 2 5.20407C2 6.42082 10.6606 7.40814 21.345 
7.40814V12.3243H28.9487C28.7076 12.3059 28.4704 12.2535 28.244 12.1686C27.2724 
11.9079 26.414 11.3336 25.8021 10.5351C25.1902 9.73651 24.8591 8.7583 24.8601 
7.75227Z" fill="#BDCEFB"/>
+<path d="M21.345 7.40329C10.6606 7.40329 2 6.41596 2 5.19922V13.1757C2 14.0892 
6.86288 14.8717 13.7987 15.2241V30.9066L21.4024 32.3609L21.345 7.40329Z" 
fill="#7497F7"/>
+<path d="M29.428 12.3237C31.953 12.3237 34 10.2768 34 7.7517C34 5.22665 31.953 
3.17969 29.428 3.17969C26.9029 3.17969 24.856 5.22665 24.856 7.7517C24.856 
10.2768 26.9029 12.3237 29.428 12.3237Z" fill="#7497F7"/>
+</svg>
diff --git a/config-ui/src/plugins/register/teambition/config.tsx 
b/config-ui/src/plugins/register/teambition/config.tsx
new file mode 100644
index 000000000..9653441cd
--- /dev/null
+++ b/config-ui/src/plugins/register/teambition/config.tsx
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { DOC_URL } from '@/release';
+
+import { IPluginConfig } from '@/types';
+
+import Icon from './assets/icon.svg?react';
+import { ConnectionTenantId, ConnectionTenantType } from './connection-fields';
+
+export const TeambitionConfig: IPluginConfig = {
+  plugin: 'teambition',
+  name: 'Teambition',
+  icon: ({ color }) => <Icon fill={color} />,
+  isBeta: true,
+  sort: 100,
+  connection: {
+    docLink: DOC_URL.PLUGIN.TEAMBITION.BASIS,
+    initialValues: {
+      endpoint: 'https://open.teambition.com/api/',
+      tenantType: 'organization',
+    },
+    fields: [
+      'name',
+      {
+        key: 'endpoint',
+        subLabel: 'Your Teambition endpoint URL.',
+      },
+      {
+        key: 'appId',
+        label: 'Application App Id',
+        subLabel: 'Your teambition application App Id.',
+      },
+      {
+        key: 'secretKey',
+        label: 'Application Secret Key',
+        subLabel: 'Your teambition application App Secret.',
+      },
+      ({ initialValues, values, errors, setValues, setErrors }: any) => (
+        <ConnectionTenantId
+          key="tenantId"
+          initialValue={initialValues.tenantId}
+          value={values.tenantId}
+          error={errors.tenantId}
+          setValue={(value) => setValues({ tenantId: value })}
+          setError={(error) => setErrors({ tenantId: error })}
+        />
+      ),
+      ({ initialValues, values, errors, setValues, setErrors }: any) => (
+        <ConnectionTenantType
+          key="tenantType"
+          initialValue={initialValues.tenantType}
+          value={values.tenantType}
+          error={errors.tenantType}
+          setValue={(value) => setValues({ tenantType: value })}
+          setError={(error) => setErrors({ tenantType: error })}
+        />
+      ),
+      'proxy',
+      {
+        key: 'rateLimitPerHour',
+        subLabel:
+          'By default, DevLake uses dynamic rate limit for optimized data 
collection for Teambition. But you can adjust the collection speed by entering 
a fixed value. Please note: the rate limit setting applies to all tokens you 
have entered above.',
+        learnMore: DOC_URL.PLUGIN.TEAMBITION.RATE_LIMIT,
+        externalInfo: 'Teambition specifies a maximum QPS of 40.',
+        defaultValue: 5000,
+      },
+    ],
+  },
+  dataScope: {
+    searchPlaceholder: 'Please enter at least 3 characters to search',
+    title: 'Projects',
+    millerColumn: {
+      columnCount: 2.5,
+      firstColumnTitle: 'Subgroups/Projects',
+    },
+  },
+  scopeConfig: {
+    entities: ['TICKET'],
+    transformation: {},
+  },
+};
diff --git 
a/config-ui/src/plugins/register/teambition/connection-fields/index.ts 
b/config-ui/src/plugins/register/teambition/connection-fields/index.ts
new file mode 100644
index 000000000..86a33adbd
--- /dev/null
+++ b/config-ui/src/plugins/register/teambition/connection-fields/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './tenant-type';
+export * from './tenant-id';
diff --git 
a/config-ui/src/plugins/register/teambition/connection-fields/tenant-id.tsx 
b/config-ui/src/plugins/register/teambition/connection-fields/tenant-id.tsx
new file mode 100644
index 000000000..0455f3883
--- /dev/null
+++ b/config-ui/src/plugins/register/teambition/connection-fields/tenant-id.tsx
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { useEffect } from 'react';
+import { Input } from 'antd';
+import { Block } from '@/components';
+
+interface Props {
+  initialValue: string;
+  value: string;
+  error: string;
+  setValue: (value: string) => void;
+  setError: (value: string) => void;
+}
+
+export const ConnectionTenantId = ({ initialValue, value, setValue, setError 
}: Props) => {
+  useEffect(() => {
+    setValue(initialValue);
+  }, [initialValue]);
+
+  useEffect(() => {
+    setError(value ? '' : 'TenantId is required');
+  }, [value]);
+
+  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    setValue(e.target.value);
+  };
+
+  return (
+    <Block title="Tenant Id" description="" required>
+      <Input style={{ width: 386 }} placeholder="Tenant Id" value={value} 
onChange={handleChange} />
+    </Block>
+  );
+};
diff --git 
a/config-ui/src/plugins/register/teambition/connection-fields/tenant-type.tsx 
b/config-ui/src/plugins/register/teambition/connection-fields/tenant-type.tsx
new file mode 100644
index 000000000..42088d375
--- /dev/null
+++ 
b/config-ui/src/plugins/register/teambition/connection-fields/tenant-type.tsx
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { useEffect } from 'react';
+import { Input } from 'antd';
+import { Block } from '@/components';
+
+interface Props {
+  initialValue: string;
+  value: string;
+  error: string;
+  setValue: (value: string) => void;
+  setError: (value: string) => void;
+}
+
+export const ConnectionTenantType = ({ initialValue, value, setValue, setError 
}: Props) => {
+  useEffect(() => {
+    setValue(initialValue);
+  }, [initialValue]);
+
+  useEffect(() => {
+    setError(value ? '' : 'TenantType is required');
+  }, [value]);
+
+  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    setValue(e.target.value);
+  };
+
+  return (
+    <Block title="Tenant type" description="" required>
+      <Input style={{ width: 386 }} placeholder="Tenant type" value={value} 
onChange={handleChange} />
+    </Block>
+  );
+};
diff --git a/config-ui/src/plugins/register/teambition/index.ts 
b/config-ui/src/plugins/register/teambition/index.ts
new file mode 100644
index 000000000..de415db39
--- /dev/null
+++ b/config-ui/src/plugins/register/teambition/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+export * from './config';

Reply via email to