mirror of https://github.com/langgenius/dify.git
feat: add editing support for trigger subscriptions (#29957)
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
parent
5896bc89f5
commit
02e0fadef7
|
|
@ -1,5 +1,6 @@
|
||||||
import io
|
import io
|
||||||
from typing import Literal
|
from collections.abc import Mapping
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
from flask import request, send_file
|
from flask import request, send_file
|
||||||
from flask_restx import Resource
|
from flask_restx import Resource
|
||||||
|
|
@ -141,6 +142,15 @@ class ParserDynamicOptions(BaseModel):
|
||||||
provider_type: Literal["tool", "trigger"]
|
provider_type: Literal["tool", "trigger"]
|
||||||
|
|
||||||
|
|
||||||
|
class ParserDynamicOptionsWithCredentials(BaseModel):
|
||||||
|
plugin_id: str
|
||||||
|
provider: str
|
||||||
|
action: str
|
||||||
|
parameter: str
|
||||||
|
credential_id: str
|
||||||
|
credentials: Mapping[str, Any]
|
||||||
|
|
||||||
|
|
||||||
class PluginPermissionSettingsPayload(BaseModel):
|
class PluginPermissionSettingsPayload(BaseModel):
|
||||||
install_permission: TenantPluginPermission.InstallPermission = TenantPluginPermission.InstallPermission.EVERYONE
|
install_permission: TenantPluginPermission.InstallPermission = TenantPluginPermission.InstallPermission.EVERYONE
|
||||||
debug_permission: TenantPluginPermission.DebugPermission = TenantPluginPermission.DebugPermission.EVERYONE
|
debug_permission: TenantPluginPermission.DebugPermission = TenantPluginPermission.DebugPermission.EVERYONE
|
||||||
|
|
@ -183,6 +193,7 @@ reg(ParserGithubUpgrade)
|
||||||
reg(ParserUninstall)
|
reg(ParserUninstall)
|
||||||
reg(ParserPermissionChange)
|
reg(ParserPermissionChange)
|
||||||
reg(ParserDynamicOptions)
|
reg(ParserDynamicOptions)
|
||||||
|
reg(ParserDynamicOptionsWithCredentials)
|
||||||
reg(ParserPreferencesChange)
|
reg(ParserPreferencesChange)
|
||||||
reg(ParserExcludePlugin)
|
reg(ParserExcludePlugin)
|
||||||
reg(ParserReadme)
|
reg(ParserReadme)
|
||||||
|
|
@ -657,6 +668,37 @@ class PluginFetchDynamicSelectOptionsApi(Resource):
|
||||||
return jsonable_encoder({"options": options})
|
return jsonable_encoder({"options": options})
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/workspaces/current/plugin/parameters/dynamic-options-with-credentials")
|
||||||
|
class PluginFetchDynamicSelectOptionsWithCredentialsApi(Resource):
|
||||||
|
@console_ns.expect(console_ns.models[ParserDynamicOptionsWithCredentials.__name__])
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@is_admin_or_owner_required
|
||||||
|
@account_initialization_required
|
||||||
|
def post(self):
|
||||||
|
"""Fetch dynamic options using credentials directly (for edit mode)."""
|
||||||
|
current_user, tenant_id = current_account_with_tenant()
|
||||||
|
user_id = current_user.id
|
||||||
|
|
||||||
|
args = ParserDynamicOptionsWithCredentials.model_validate(console_ns.payload)
|
||||||
|
|
||||||
|
try:
|
||||||
|
options = PluginParameterService.get_dynamic_select_options_with_credentials(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
user_id=user_id,
|
||||||
|
plugin_id=args.plugin_id,
|
||||||
|
provider=args.provider,
|
||||||
|
action=args.action,
|
||||||
|
parameter=args.parameter,
|
||||||
|
credential_id=args.credential_id,
|
||||||
|
credentials=args.credentials,
|
||||||
|
)
|
||||||
|
except PluginDaemonClientSideError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
return jsonable_encoder({"options": options})
|
||||||
|
|
||||||
|
|
||||||
@console_ns.route("/workspaces/current/plugin/preferences/change")
|
@console_ns.route("/workspaces/current/plugin/preferences/change")
|
||||||
class PluginChangePreferencesApi(Resource):
|
class PluginChangePreferencesApi(Resource):
|
||||||
@console_ns.expect(console_ns.models[ParserPreferencesChange.__name__])
|
@console_ns.expect(console_ns.models[ParserPreferencesChange.__name__])
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import logging
|
import logging
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from flask import make_response, redirect, request
|
from flask import make_response, redirect, request
|
||||||
from flask_restx import Resource, reqparse
|
from flask_restx import Resource, reqparse
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from werkzeug.exceptions import BadRequest, Forbidden
|
from werkzeug.exceptions import BadRequest, Forbidden
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
|
from constants import HIDDEN_VALUE, UNKNOWN_VALUE
|
||||||
from controllers.web.error import NotFoundError
|
from controllers.web.error import NotFoundError
|
||||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||||
from core.plugin.entities.plugin_daemon import CredentialType
|
from core.plugin.entities.plugin_daemon import CredentialType
|
||||||
|
|
@ -32,6 +36,32 @@ from ..wraps import (
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TriggerSubscriptionUpdateRequest(BaseModel):
|
||||||
|
"""Request payload for updating a trigger subscription"""
|
||||||
|
|
||||||
|
name: str | None = Field(default=None, description="The name for the subscription")
|
||||||
|
credentials: Mapping[str, Any] | None = Field(default=None, description="The credentials for the subscription")
|
||||||
|
parameters: Mapping[str, Any] | None = Field(default=None, description="The parameters for the subscription")
|
||||||
|
properties: Mapping[str, Any] | None = Field(default=None, description="The properties for the subscription")
|
||||||
|
|
||||||
|
|
||||||
|
class TriggerSubscriptionVerifyRequest(BaseModel):
|
||||||
|
"""Request payload for verifying subscription credentials."""
|
||||||
|
|
||||||
|
credentials: Mapping[str, Any] = Field(description="The credentials to verify")
|
||||||
|
|
||||||
|
|
||||||
|
console_ns.schema_model(
|
||||||
|
TriggerSubscriptionUpdateRequest.__name__,
|
||||||
|
TriggerSubscriptionUpdateRequest.model_json_schema(ref_template="#/definitions/{model}"),
|
||||||
|
)
|
||||||
|
|
||||||
|
console_ns.schema_model(
|
||||||
|
TriggerSubscriptionVerifyRequest.__name__,
|
||||||
|
TriggerSubscriptionVerifyRequest.model_json_schema(ref_template="#/definitions/{model}"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@console_ns.route("/workspaces/current/trigger-provider/<path:provider>/icon")
|
@console_ns.route("/workspaces/current/trigger-provider/<path:provider>/icon")
|
||||||
class TriggerProviderIconApi(Resource):
|
class TriggerProviderIconApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
|
|
@ -155,16 +185,16 @@ parser_api = (
|
||||||
|
|
||||||
|
|
||||||
@console_ns.route(
|
@console_ns.route(
|
||||||
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/verify/<path:subscription_builder_id>",
|
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/verify-and-update/<path:subscription_builder_id>",
|
||||||
)
|
)
|
||||||
class TriggerSubscriptionBuilderVerifyApi(Resource):
|
class TriggerSubscriptionBuilderVerifyAndUpdateApi(Resource):
|
||||||
@console_ns.expect(parser_api)
|
@console_ns.expect(parser_api)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@edit_permission_required
|
@edit_permission_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def post(self, provider, subscription_builder_id):
|
def post(self, provider, subscription_builder_id):
|
||||||
"""Verify a subscription instance for a trigger provider"""
|
"""Verify and update a subscription instance for a trigger provider"""
|
||||||
user = current_user
|
user = current_user
|
||||||
assert user.current_tenant_id is not None
|
assert user.current_tenant_id is not None
|
||||||
|
|
||||||
|
|
@ -289,6 +319,83 @@ class TriggerSubscriptionBuilderBuildApi(Resource):
|
||||||
raise ValueError(str(e)) from e
|
raise ValueError(str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route(
|
||||||
|
"/workspaces/current/trigger-provider/<path:subscription_id>/subscriptions/update",
|
||||||
|
)
|
||||||
|
class TriggerSubscriptionUpdateApi(Resource):
|
||||||
|
@console_ns.expect(console_ns.models[TriggerSubscriptionUpdateRequest.__name__])
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@edit_permission_required
|
||||||
|
@account_initialization_required
|
||||||
|
def post(self, subscription_id: str):
|
||||||
|
"""Update a subscription instance"""
|
||||||
|
user = current_user
|
||||||
|
assert user.current_tenant_id is not None
|
||||||
|
|
||||||
|
args = TriggerSubscriptionUpdateRequest.model_validate(console_ns.payload)
|
||||||
|
|
||||||
|
subscription = TriggerProviderService.get_subscription_by_id(
|
||||||
|
tenant_id=user.current_tenant_id,
|
||||||
|
subscription_id=subscription_id,
|
||||||
|
)
|
||||||
|
if not subscription:
|
||||||
|
raise NotFoundError(f"Subscription {subscription_id} not found")
|
||||||
|
|
||||||
|
provider_id = TriggerProviderID(subscription.provider_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# rename only
|
||||||
|
if (
|
||||||
|
args.name is not None
|
||||||
|
and args.credentials is None
|
||||||
|
and args.parameters is None
|
||||||
|
and args.properties is None
|
||||||
|
):
|
||||||
|
TriggerProviderService.update_trigger_subscription(
|
||||||
|
tenant_id=user.current_tenant_id,
|
||||||
|
subscription_id=subscription_id,
|
||||||
|
name=args.name,
|
||||||
|
)
|
||||||
|
return 200
|
||||||
|
|
||||||
|
# rebuild for create automatically by the provider
|
||||||
|
match subscription.credential_type:
|
||||||
|
case CredentialType.UNAUTHORIZED:
|
||||||
|
TriggerProviderService.update_trigger_subscription(
|
||||||
|
tenant_id=user.current_tenant_id,
|
||||||
|
subscription_id=subscription_id,
|
||||||
|
name=args.name,
|
||||||
|
properties=args.properties,
|
||||||
|
)
|
||||||
|
return 200
|
||||||
|
case CredentialType.API_KEY | CredentialType.OAUTH2:
|
||||||
|
if args.credentials:
|
||||||
|
new_credentials: dict[str, Any] = {
|
||||||
|
key: value if value != HIDDEN_VALUE else subscription.credentials.get(key, UNKNOWN_VALUE)
|
||||||
|
for key, value in args.credentials.items()
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
new_credentials = subscription.credentials
|
||||||
|
|
||||||
|
TriggerProviderService.rebuild_trigger_subscription(
|
||||||
|
tenant_id=user.current_tenant_id,
|
||||||
|
name=args.name,
|
||||||
|
provider_id=provider_id,
|
||||||
|
subscription_id=subscription_id,
|
||||||
|
credentials=new_credentials,
|
||||||
|
parameters=args.parameters or subscription.parameters,
|
||||||
|
)
|
||||||
|
return 200
|
||||||
|
case _:
|
||||||
|
raise BadRequest("Invalid credential type")
|
||||||
|
except ValueError as e:
|
||||||
|
raise BadRequest(str(e))
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Error updating subscription", exc_info=e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
@console_ns.route(
|
@console_ns.route(
|
||||||
"/workspaces/current/trigger-provider/<path:subscription_id>/subscriptions/delete",
|
"/workspaces/current/trigger-provider/<path:subscription_id>/subscriptions/delete",
|
||||||
)
|
)
|
||||||
|
|
@ -576,3 +683,38 @@ class TriggerOAuthClientManageApi(Resource):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Error removing OAuth client", exc_info=e)
|
logger.exception("Error removing OAuth client", exc_info=e)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route(
|
||||||
|
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/verify/<path:subscription_id>",
|
||||||
|
)
|
||||||
|
class TriggerSubscriptionVerifyApi(Resource):
|
||||||
|
@console_ns.expect(console_ns.models[TriggerSubscriptionVerifyRequest.__name__])
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@edit_permission_required
|
||||||
|
@account_initialization_required
|
||||||
|
def post(self, provider, subscription_id):
|
||||||
|
"""Verify credentials for an existing subscription (edit mode only)"""
|
||||||
|
user = current_user
|
||||||
|
assert user.current_tenant_id is not None
|
||||||
|
|
||||||
|
verify_request: TriggerSubscriptionVerifyRequest = TriggerSubscriptionVerifyRequest.model_validate(
|
||||||
|
console_ns.payload
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = TriggerProviderService.verify_subscription_credentials(
|
||||||
|
tenant_id=user.current_tenant_id,
|
||||||
|
user_id=user.id,
|
||||||
|
provider_id=TriggerProviderID(provider),
|
||||||
|
subscription_id=subscription_id,
|
||||||
|
credentials=verify_request.credentials,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning("Credential verification failed", exc_info=e)
|
||||||
|
raise BadRequest(str(e)) from e
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Error verifying subscription credentials", exc_info=e)
|
||||||
|
raise BadRequest(str(e)) from e
|
||||||
|
|
|
||||||
|
|
@ -67,12 +67,16 @@ def create_trigger_provider_encrypter_for_subscription(
|
||||||
|
|
||||||
|
|
||||||
def delete_cache_for_subscription(tenant_id: str, provider_id: str, subscription_id: str):
|
def delete_cache_for_subscription(tenant_id: str, provider_id: str, subscription_id: str):
|
||||||
cache = TriggerProviderCredentialsCache(
|
TriggerProviderCredentialsCache(
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
provider_id=provider_id,
|
provider_id=provider_id,
|
||||||
credential_id=subscription_id,
|
credential_id=subscription_id,
|
||||||
)
|
).delete()
|
||||||
cache.delete()
|
TriggerProviderPropertiesCache(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
provider_id=provider_id,
|
||||||
|
subscription_id=subscription_id,
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
|
||||||
def create_trigger_provider_encrypter_for_properties(
|
def create_trigger_provider_encrypter_for_properties(
|
||||||
|
|
|
||||||
|
|
@ -105,3 +105,49 @@ class PluginParameterService:
|
||||||
)
|
)
|
||||||
.options
|
.options
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_dynamic_select_options_with_credentials(
|
||||||
|
tenant_id: str,
|
||||||
|
user_id: str,
|
||||||
|
plugin_id: str,
|
||||||
|
provider: str,
|
||||||
|
action: str,
|
||||||
|
parameter: str,
|
||||||
|
credential_id: str,
|
||||||
|
credentials: Mapping[str, Any],
|
||||||
|
) -> Sequence[PluginParameterOption]:
|
||||||
|
"""
|
||||||
|
Get dynamic select options using provided credentials directly.
|
||||||
|
Used for edit mode when credentials have been modified but not yet saved.
|
||||||
|
|
||||||
|
Security: credential_id is validated against tenant_id to ensure
|
||||||
|
users can only access their own credentials.
|
||||||
|
"""
|
||||||
|
from constants import HIDDEN_VALUE
|
||||||
|
|
||||||
|
# Get original subscription to replace hidden values (with tenant_id check for security)
|
||||||
|
original_subscription = TriggerProviderService.get_subscription_by_id(tenant_id, credential_id)
|
||||||
|
if not original_subscription:
|
||||||
|
raise ValueError(f"Subscription {credential_id} not found")
|
||||||
|
|
||||||
|
# Replace [__HIDDEN__] with original values
|
||||||
|
resolved_credentials: dict[str, Any] = {
|
||||||
|
key: (original_subscription.credentials.get(key) if value == HIDDEN_VALUE else value)
|
||||||
|
for key, value in credentials.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
DynamicSelectClient()
|
||||||
|
.fetch_dynamic_select_options(
|
||||||
|
tenant_id,
|
||||||
|
user_id,
|
||||||
|
plugin_id,
|
||||||
|
provider,
|
||||||
|
action,
|
||||||
|
resolved_credentials,
|
||||||
|
CredentialType.API_KEY.value,
|
||||||
|
parameter,
|
||||||
|
)
|
||||||
|
.options
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -94,16 +94,23 @@ class TriggerProviderService:
|
||||||
|
|
||||||
provider_controller = TriggerManager.get_trigger_provider(tenant_id, provider_id)
|
provider_controller = TriggerManager.get_trigger_provider(tenant_id, provider_id)
|
||||||
for subscription in subscriptions:
|
for subscription in subscriptions:
|
||||||
encrypter, _ = create_trigger_provider_encrypter_for_subscription(
|
credential_encrypter, _ = create_trigger_provider_encrypter_for_subscription(
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
controller=provider_controller,
|
controller=provider_controller,
|
||||||
subscription=subscription,
|
subscription=subscription,
|
||||||
)
|
)
|
||||||
subscription.credentials = dict(
|
subscription.credentials = dict(
|
||||||
encrypter.mask_credentials(dict(encrypter.decrypt(subscription.credentials)))
|
credential_encrypter.mask_credentials(dict(credential_encrypter.decrypt(subscription.credentials)))
|
||||||
)
|
)
|
||||||
subscription.properties = dict(encrypter.mask_credentials(dict(encrypter.decrypt(subscription.properties))))
|
properties_encrypter, _ = create_trigger_provider_encrypter_for_properties(
|
||||||
subscription.parameters = dict(encrypter.mask_credentials(dict(encrypter.decrypt(subscription.parameters))))
|
tenant_id=tenant_id,
|
||||||
|
controller=provider_controller,
|
||||||
|
subscription=subscription,
|
||||||
|
)
|
||||||
|
subscription.properties = dict(
|
||||||
|
properties_encrypter.mask_credentials(dict(properties_encrypter.decrypt(subscription.properties)))
|
||||||
|
)
|
||||||
|
subscription.parameters = dict(subscription.parameters)
|
||||||
count = workflows_in_use_map.get(subscription.id)
|
count = workflows_in_use_map.get(subscription.id)
|
||||||
subscription.workflows_in_use = count if count is not None else 0
|
subscription.workflows_in_use = count if count is not None else 0
|
||||||
|
|
||||||
|
|
@ -209,6 +216,101 @@ class TriggerProviderService:
|
||||||
logger.exception("Failed to add trigger provider")
|
logger.exception("Failed to add trigger provider")
|
||||||
raise ValueError(str(e))
|
raise ValueError(str(e))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_trigger_subscription(
|
||||||
|
cls,
|
||||||
|
tenant_id: str,
|
||||||
|
subscription_id: str,
|
||||||
|
name: str | None = None,
|
||||||
|
properties: Mapping[str, Any] | None = None,
|
||||||
|
parameters: Mapping[str, Any] | None = None,
|
||||||
|
credentials: Mapping[str, Any] | None = None,
|
||||||
|
credential_expires_at: int | None = None,
|
||||||
|
expires_at: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Update an existing trigger subscription.
|
||||||
|
|
||||||
|
:param tenant_id: Tenant ID
|
||||||
|
:param subscription_id: Subscription instance ID
|
||||||
|
:param name: Optional new name for this subscription
|
||||||
|
:param properties: Optional new properties
|
||||||
|
:param parameters: Optional new parameters
|
||||||
|
:param credentials: Optional new credentials
|
||||||
|
:param credential_expires_at: Optional new credential expiration timestamp
|
||||||
|
:param expires_at: Optional new expiration timestamp
|
||||||
|
:return: Success response with updated subscription info
|
||||||
|
"""
|
||||||
|
with Session(db.engine, expire_on_commit=False) as session:
|
||||||
|
# Use distributed lock to prevent race conditions on the same subscription
|
||||||
|
lock_key = f"trigger_subscription_update_lock:{tenant_id}_{subscription_id}"
|
||||||
|
with redis_client.lock(lock_key, timeout=20):
|
||||||
|
subscription: TriggerSubscription | None = (
|
||||||
|
session.query(TriggerSubscription).filter_by(tenant_id=tenant_id, id=subscription_id).first()
|
||||||
|
)
|
||||||
|
if not subscription:
|
||||||
|
raise ValueError(f"Trigger subscription {subscription_id} not found")
|
||||||
|
|
||||||
|
provider_id = TriggerProviderID(subscription.provider_id)
|
||||||
|
provider_controller = TriggerManager.get_trigger_provider(tenant_id, provider_id)
|
||||||
|
|
||||||
|
# Check for name uniqueness if name is being updated
|
||||||
|
if name is not None and name != subscription.name:
|
||||||
|
existing = (
|
||||||
|
session.query(TriggerSubscription)
|
||||||
|
.filter_by(tenant_id=tenant_id, provider_id=str(provider_id), name=name)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
raise ValueError(f"Subscription name '{name}' already exists for this provider")
|
||||||
|
subscription.name = name
|
||||||
|
|
||||||
|
# Update properties if provided
|
||||||
|
if properties is not None:
|
||||||
|
properties_encrypter, _ = create_provider_encrypter(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
config=provider_controller.get_properties_schema(),
|
||||||
|
cache=NoOpProviderCredentialCache(),
|
||||||
|
)
|
||||||
|
# Handle hidden values - preserve original encrypted values
|
||||||
|
original_properties = properties_encrypter.decrypt(subscription.properties)
|
||||||
|
new_properties: dict[str, Any] = {
|
||||||
|
key: value if value != HIDDEN_VALUE else original_properties.get(key, UNKNOWN_VALUE)
|
||||||
|
for key, value in properties.items()
|
||||||
|
}
|
||||||
|
subscription.properties = dict(properties_encrypter.encrypt(new_properties))
|
||||||
|
|
||||||
|
# Update parameters if provided
|
||||||
|
if parameters is not None:
|
||||||
|
subscription.parameters = dict(parameters)
|
||||||
|
|
||||||
|
# Update credentials if provided
|
||||||
|
if credentials is not None:
|
||||||
|
credential_type = CredentialType.of(subscription.credential_type)
|
||||||
|
credential_encrypter, _ = create_provider_encrypter(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
config=provider_controller.get_credential_schema_config(credential_type),
|
||||||
|
cache=NoOpProviderCredentialCache(),
|
||||||
|
)
|
||||||
|
subscription.credentials = dict(credential_encrypter.encrypt(dict(credentials)))
|
||||||
|
|
||||||
|
# Update credential expiration timestamp if provided
|
||||||
|
if credential_expires_at is not None:
|
||||||
|
subscription.credential_expires_at = credential_expires_at
|
||||||
|
|
||||||
|
# Update expiration timestamp if provided
|
||||||
|
if expires_at is not None:
|
||||||
|
subscription.expires_at = expires_at
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Clear subscription cache
|
||||||
|
delete_cache_for_subscription(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
provider_id=subscription.provider_id,
|
||||||
|
subscription_id=subscription.id,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_subscription_by_id(cls, tenant_id: str, subscription_id: str | None = None) -> TriggerSubscription | None:
|
def get_subscription_by_id(cls, tenant_id: str, subscription_id: str | None = None) -> TriggerSubscription | None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -258,7 +360,9 @@ class TriggerProviderService:
|
||||||
|
|
||||||
credential_type: CredentialType = CredentialType.of(subscription.credential_type)
|
credential_type: CredentialType = CredentialType.of(subscription.credential_type)
|
||||||
is_auto_created: bool = credential_type in [CredentialType.OAUTH2, CredentialType.API_KEY]
|
is_auto_created: bool = credential_type in [CredentialType.OAUTH2, CredentialType.API_KEY]
|
||||||
if is_auto_created:
|
if not is_auto_created:
|
||||||
|
return None
|
||||||
|
|
||||||
provider_id = TriggerProviderID(subscription.provider_id)
|
provider_id = TriggerProviderID(subscription.provider_id)
|
||||||
provider_controller: PluginTriggerProviderController = TriggerManager.get_trigger_provider(
|
provider_controller: PluginTriggerProviderController = TriggerManager.get_trigger_provider(
|
||||||
tenant_id=tenant_id, provider_id=provider_id
|
tenant_id=tenant_id, provider_id=provider_id
|
||||||
|
|
@ -280,8 +384,8 @@ class TriggerProviderService:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Error unsubscribing trigger", exc_info=e)
|
logger.exception("Error unsubscribing trigger", exc_info=e)
|
||||||
|
|
||||||
# Clear cache
|
|
||||||
session.delete(subscription)
|
session.delete(subscription)
|
||||||
|
# Clear cache
|
||||||
delete_cache_for_subscription(
|
delete_cache_for_subscription(
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
provider_id=subscription.provider_id,
|
provider_id=subscription.provider_id,
|
||||||
|
|
@ -688,3 +792,125 @@ class TriggerProviderService:
|
||||||
)
|
)
|
||||||
subscription.properties = dict(properties_encrypter.decrypt(subscription.properties))
|
subscription.properties = dict(properties_encrypter.decrypt(subscription.properties))
|
||||||
return subscription
|
return subscription
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def verify_subscription_credentials(
|
||||||
|
cls,
|
||||||
|
tenant_id: str,
|
||||||
|
user_id: str,
|
||||||
|
provider_id: TriggerProviderID,
|
||||||
|
subscription_id: str,
|
||||||
|
credentials: Mapping[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Verify credentials for an existing subscription without updating it.
|
||||||
|
|
||||||
|
This is used in edit mode to validate new credentials before rebuild.
|
||||||
|
|
||||||
|
:param tenant_id: Tenant ID
|
||||||
|
:param user_id: User ID
|
||||||
|
:param provider_id: Provider identifier
|
||||||
|
:param subscription_id: Subscription ID
|
||||||
|
:param credentials: New credentials to verify
|
||||||
|
:return: dict with 'verified' boolean
|
||||||
|
"""
|
||||||
|
provider_controller = TriggerManager.get_trigger_provider(tenant_id, provider_id)
|
||||||
|
if not provider_controller:
|
||||||
|
raise ValueError(f"Provider {provider_id} not found")
|
||||||
|
|
||||||
|
subscription = cls.get_subscription_by_id(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
subscription_id=subscription_id,
|
||||||
|
)
|
||||||
|
if not subscription:
|
||||||
|
raise ValueError(f"Subscription {subscription_id} not found")
|
||||||
|
|
||||||
|
credential_type = CredentialType.of(subscription.credential_type)
|
||||||
|
|
||||||
|
# For API Key, validate the new credentials
|
||||||
|
if credential_type == CredentialType.API_KEY:
|
||||||
|
new_credentials: dict[str, Any] = {
|
||||||
|
key: value if value != HIDDEN_VALUE else subscription.credentials.get(key, UNKNOWN_VALUE)
|
||||||
|
for key, value in credentials.items()
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
provider_controller.validate_credentials(user_id, credentials=new_credentials)
|
||||||
|
return {"verified": True}
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Invalid credentials: {e}") from e
|
||||||
|
|
||||||
|
return {"verified": True}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def rebuild_trigger_subscription(
|
||||||
|
cls,
|
||||||
|
tenant_id: str,
|
||||||
|
provider_id: TriggerProviderID,
|
||||||
|
subscription_id: str,
|
||||||
|
credentials: Mapping[str, Any],
|
||||||
|
parameters: Mapping[str, Any],
|
||||||
|
name: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Create a subscription builder for rebuilding an existing subscription.
|
||||||
|
|
||||||
|
This method creates a builder pre-filled with data from the rebuild request,
|
||||||
|
keeping the same subscription_id and endpoint_id so the webhook URL remains unchanged.
|
||||||
|
|
||||||
|
:param tenant_id: Tenant ID
|
||||||
|
:param name: Name for the subscription
|
||||||
|
:param subscription_id: Subscription ID
|
||||||
|
:param provider_id: Provider identifier
|
||||||
|
:param credentials: Credentials for the subscription
|
||||||
|
:param parameters: Parameters for the subscription
|
||||||
|
:return: SubscriptionBuilderApiEntity
|
||||||
|
"""
|
||||||
|
provider_controller = TriggerManager.get_trigger_provider(tenant_id, provider_id)
|
||||||
|
if not provider_controller:
|
||||||
|
raise ValueError(f"Provider {provider_id} not found")
|
||||||
|
|
||||||
|
subscription = TriggerProviderService.get_subscription_by_id(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
subscription_id=subscription_id,
|
||||||
|
)
|
||||||
|
if not subscription:
|
||||||
|
raise ValueError(f"Subscription {subscription_id} not found")
|
||||||
|
|
||||||
|
credential_type = CredentialType.of(subscription.credential_type)
|
||||||
|
if credential_type not in [CredentialType.OAUTH2, CredentialType.API_KEY]:
|
||||||
|
raise ValueError("Credential type not supported for rebuild")
|
||||||
|
|
||||||
|
# TODO: Trying to invoke update api of the plugin trigger provider
|
||||||
|
|
||||||
|
# FALLBACK: If the update api is not implemented, delete the previous subscription and create a new one
|
||||||
|
|
||||||
|
# Delete the previous subscription
|
||||||
|
user_id = subscription.user_id
|
||||||
|
TriggerManager.unsubscribe_trigger(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
user_id=user_id,
|
||||||
|
provider_id=provider_id,
|
||||||
|
subscription=subscription.to_entity(),
|
||||||
|
credentials=subscription.credentials,
|
||||||
|
credential_type=credential_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a new subscription with the same subscription_id and endpoint_id
|
||||||
|
new_subscription: TriggerSubscriptionEntity = TriggerManager.subscribe_trigger(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
user_id=user_id,
|
||||||
|
provider_id=provider_id,
|
||||||
|
endpoint=generate_plugin_trigger_endpoint_url(subscription.endpoint_id),
|
||||||
|
parameters=parameters,
|
||||||
|
credentials=credentials,
|
||||||
|
credential_type=credential_type,
|
||||||
|
)
|
||||||
|
TriggerProviderService.update_trigger_subscription(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
subscription_id=subscription.id,
|
||||||
|
name=name,
|
||||||
|
parameters=parameters,
|
||||||
|
credentials=credentials,
|
||||||
|
properties=new_subscription.properties,
|
||||||
|
expires_at=new_subscription.expires_at,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -453,11 +453,12 @@ class TriggerSubscriptionBuilderService:
|
||||||
if not subscription_builder:
|
if not subscription_builder:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
# response to validation endpoint
|
# response to validation endpoint
|
||||||
controller: PluginTriggerProviderController = TriggerManager.get_trigger_provider(
|
controller: PluginTriggerProviderController = TriggerManager.get_trigger_provider(
|
||||||
tenant_id=subscription_builder.tenant_id, provider_id=TriggerProviderID(subscription_builder.provider_id)
|
tenant_id=subscription_builder.tenant_id,
|
||||||
|
provider_id=TriggerProviderID(subscription_builder.provider_id),
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
dispatch_response: TriggerDispatchResponse = controller.dispatch(
|
dispatch_response: TriggerDispatchResponse = controller.dispatch(
|
||||||
request=request,
|
request=request,
|
||||||
subscription=subscription_builder.to_subscription(),
|
subscription=subscription_builder.to_subscription(),
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ const PluginDetailPanel: FC<Props> = ({
|
||||||
name: detail.name,
|
name: detail.name,
|
||||||
id: detail.id,
|
id: detail.id,
|
||||||
})
|
})
|
||||||
}, [detail])
|
}, [detail, setDetail])
|
||||||
|
|
||||||
if (!detail)
|
if (!detail)
|
||||||
return null
|
return null
|
||||||
|
|
@ -69,7 +69,7 @@ const PluginDetailPanel: FC<Props> = ({
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
{detail.declaration.category === PluginCategoryEnum.trigger && (
|
{detail.declaration.category === PluginCategoryEnum.trigger && (
|
||||||
<>
|
<>
|
||||||
<SubscriptionList />
|
<SubscriptionList pluginDetail={detail} />
|
||||||
<TriggerEventsList />
|
<TriggerEventsList />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import {
|
||||||
useCreateTriggerSubscriptionBuilder,
|
useCreateTriggerSubscriptionBuilder,
|
||||||
useTriggerSubscriptionBuilderLogs,
|
useTriggerSubscriptionBuilderLogs,
|
||||||
useUpdateTriggerSubscriptionBuilder,
|
useUpdateTriggerSubscriptionBuilder,
|
||||||
useVerifyTriggerSubscriptionBuilder,
|
useVerifyAndUpdateTriggerSubscriptionBuilder,
|
||||||
} from '@/service/use-triggers'
|
} from '@/service/use-triggers'
|
||||||
import { parsePluginErrorMessage } from '@/utils/error-parser'
|
import { parsePluginErrorMessage } from '@/utils/error-parser'
|
||||||
import { isPrivateOrLocalAddress } from '@/utils/urlValidation'
|
import { isPrivateOrLocalAddress } from '@/utils/urlValidation'
|
||||||
|
|
@ -40,6 +40,15 @@ const CREDENTIAL_TYPE_MAP: Record<SupportedCreationMethods, TriggerCredentialTyp
|
||||||
[SupportedCreationMethods.MANUAL]: TriggerCredentialTypeEnum.Unauthorized,
|
[SupportedCreationMethods.MANUAL]: TriggerCredentialTypeEnum.Unauthorized,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MODAL_TITLE_KEY_MAP: Record<
|
||||||
|
SupportedCreationMethods,
|
||||||
|
'pluginTrigger.modal.apiKey.title' | 'pluginTrigger.modal.oauth.title' | 'pluginTrigger.modal.manual.title'
|
||||||
|
> = {
|
||||||
|
[SupportedCreationMethods.APIKEY]: 'pluginTrigger.modal.apiKey.title',
|
||||||
|
[SupportedCreationMethods.OAUTH]: 'pluginTrigger.modal.oauth.title',
|
||||||
|
[SupportedCreationMethods.MANUAL]: 'pluginTrigger.modal.manual.title',
|
||||||
|
}
|
||||||
|
|
||||||
enum ApiKeyStep {
|
enum ApiKeyStep {
|
||||||
Verify = 'verify',
|
Verify = 'verify',
|
||||||
Configuration = 'configuration',
|
Configuration = 'configuration',
|
||||||
|
|
@ -104,7 +113,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
||||||
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>(builder)
|
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>(builder)
|
||||||
const isInitializedRef = useRef(false)
|
const isInitializedRef = useRef(false)
|
||||||
|
|
||||||
const { mutate: verifyCredentials, isPending: isVerifyingCredentials } = useVerifyTriggerSubscriptionBuilder()
|
const { mutate: verifyCredentials, isPending: isVerifyingCredentials } = useVerifyAndUpdateTriggerSubscriptionBuilder()
|
||||||
const { mutateAsync: createBuilder /* isPending: isCreatingBuilder */ } = useCreateTriggerSubscriptionBuilder()
|
const { mutateAsync: createBuilder /* isPending: isCreatingBuilder */ } = useCreateTriggerSubscriptionBuilder()
|
||||||
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
|
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
|
||||||
const { mutate: updateBuilder } = useUpdateTriggerSubscriptionBuilder()
|
const { mutate: updateBuilder } = useUpdateTriggerSubscriptionBuilder()
|
||||||
|
|
@ -117,13 +126,13 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
||||||
const autoCommonParametersSchema = detail?.declaration.trigger?.subscription_constructor?.parameters || [] // apikey and oauth
|
const autoCommonParametersSchema = detail?.declaration.trigger?.subscription_constructor?.parameters || [] // apikey and oauth
|
||||||
const autoCommonParametersFormRef = React.useRef<FormRefObject>(null)
|
const autoCommonParametersFormRef = React.useRef<FormRefObject>(null)
|
||||||
|
|
||||||
const rawApiKeyCredentialsSchema = detail?.declaration.trigger?.subscription_constructor?.credentials_schema || []
|
|
||||||
const apiKeyCredentialsSchema = useMemo(() => {
|
const apiKeyCredentialsSchema = useMemo(() => {
|
||||||
return rawApiKeyCredentialsSchema.map(schema => ({
|
const rawSchema = detail?.declaration?.trigger?.subscription_constructor?.credentials_schema || []
|
||||||
|
return rawSchema.map(schema => ({
|
||||||
...schema,
|
...schema,
|
||||||
tooltip: schema.help,
|
tooltip: schema.help,
|
||||||
}))
|
}))
|
||||||
}, [rawApiKeyCredentialsSchema])
|
}, [detail?.declaration?.trigger?.subscription_constructor?.credentials_schema])
|
||||||
const apiKeyCredentialsFormRef = React.useRef<FormRefObject>(null)
|
const apiKeyCredentialsFormRef = React.useRef<FormRefObject>(null)
|
||||||
|
|
||||||
const { data: logData } = useTriggerSubscriptionBuilderLogs(
|
const { data: logData } = useTriggerSubscriptionBuilderLogs(
|
||||||
|
|
@ -163,7 +172,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
||||||
if (form)
|
if (form)
|
||||||
form.setFieldValue('callback_url', subscriptionBuilder.endpoint)
|
form.setFieldValue('callback_url', subscriptionBuilder.endpoint)
|
||||||
if (isPrivateOrLocalAddress(subscriptionBuilder.endpoint)) {
|
if (isPrivateOrLocalAddress(subscriptionBuilder.endpoint)) {
|
||||||
console.log('isPrivateOrLocalAddress', isPrivateOrLocalAddress(subscriptionBuilder.endpoint))
|
console.warn('callback_url is private or local address', subscriptionBuilder.endpoint)
|
||||||
subscriptionFormRef.current?.setFields([{
|
subscriptionFormRef.current?.setFields([{
|
||||||
name: 'callback_url',
|
name: 'callback_url',
|
||||||
warnings: [t('pluginTrigger.modal.form.callbackUrl.privateAddressWarning')],
|
warnings: [t('pluginTrigger.modal.form.callbackUrl.privateAddressWarning')],
|
||||||
|
|
@ -179,7 +188,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
||||||
}, [subscriptionBuilder?.endpoint, currentStep, t])
|
}, [subscriptionBuilder?.endpoint, currentStep, t])
|
||||||
|
|
||||||
const debouncedUpdate = useMemo(
|
const debouncedUpdate = useMemo(
|
||||||
() => debounce((provider: string, builderId: string, properties: Record<string, any>) => {
|
() => debounce((provider: string, builderId: string, properties: Record<string, unknown>) => {
|
||||||
updateBuilder(
|
updateBuilder(
|
||||||
{
|
{
|
||||||
provider,
|
provider,
|
||||||
|
|
@ -187,11 +196,12 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
||||||
properties,
|
properties,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onError: (error: any) => {
|
onError: async (error: unknown) => {
|
||||||
|
const errorMessage = await parsePluginErrorMessage(error) || t('pluginTrigger.modal.errors.updateFailed')
|
||||||
console.error('Failed to update subscription builder:', error)
|
console.error('Failed to update subscription builder:', error)
|
||||||
Toast.notify({
|
Toast.notify({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: error?.message || t('pluginTrigger.modal.errors.updateFailed'),
|
message: errorMessage,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -246,7 +256,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
||||||
})
|
})
|
||||||
setCurrentStep(ApiKeyStep.Configuration)
|
setCurrentStep(ApiKeyStep.Configuration)
|
||||||
},
|
},
|
||||||
onError: async (error: any) => {
|
onError: async (error: unknown) => {
|
||||||
const errorMessage = await parsePluginErrorMessage(error) || t('pluginTrigger.modal.apiKey.verify.error')
|
const errorMessage = await parsePluginErrorMessage(error) || t('pluginTrigger.modal.apiKey.verify.error')
|
||||||
apiKeyCredentialsFormRef.current?.setFields([{
|
apiKeyCredentialsFormRef.current?.setFields([{
|
||||||
name: Object.keys(credentials)[0],
|
name: Object.keys(credentials)[0],
|
||||||
|
|
@ -303,7 +313,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
||||||
onClose()
|
onClose()
|
||||||
refetch?.()
|
refetch?.()
|
||||||
},
|
},
|
||||||
onError: async (error: any) => {
|
onError: async (error: unknown) => {
|
||||||
const errorMessage = await parsePluginErrorMessage(error) || t('pluginTrigger.subscription.createFailed')
|
const errorMessage = await parsePluginErrorMessage(error) || t('pluginTrigger.subscription.createFailed')
|
||||||
Toast.notify({
|
Toast.notify({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
|
@ -328,14 +338,17 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
||||||
}])
|
}])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const confirmButtonText = useMemo(() => {
|
||||||
|
if (currentStep === ApiKeyStep.Verify)
|
||||||
|
return isVerifyingCredentials ? t('pluginTrigger.modal.common.verifying') : t('pluginTrigger.modal.common.verify')
|
||||||
|
|
||||||
|
return isBuilding ? t('pluginTrigger.modal.common.creating') : t('pluginTrigger.modal.common.create')
|
||||||
|
}, [currentStep, isVerifyingCredentials, isBuilding, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={t(`pluginTrigger.modal.${createType === SupportedCreationMethods.APIKEY ? 'apiKey' : createType.toLowerCase()}.title` as any)}
|
title={t(MODAL_TITLE_KEY_MAP[createType])}
|
||||||
confirmButtonText={
|
confirmButtonText={confirmButtonText}
|
||||||
currentStep === ApiKeyStep.Verify
|
|
||||||
? isVerifyingCredentials ? t('pluginTrigger.modal.common.verifying') : t('pluginTrigger.modal.common.verify')
|
|
||||||
: isBuilding ? t('pluginTrigger.modal.common.creating') : t('pluginTrigger.modal.common.create')
|
|
||||||
}
|
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
onConfirm={handleConfirm}
|
onConfirm={handleConfirm}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import {
|
||||||
useConfigureTriggerOAuth,
|
useConfigureTriggerOAuth,
|
||||||
useDeleteTriggerOAuth,
|
useDeleteTriggerOAuth,
|
||||||
useInitiateTriggerOAuth,
|
useInitiateTriggerOAuth,
|
||||||
useVerifyTriggerSubscriptionBuilder,
|
useVerifyAndUpdateTriggerSubscriptionBuilder,
|
||||||
} from '@/service/use-triggers'
|
} from '@/service/use-triggers'
|
||||||
import { usePluginStore } from '../../store'
|
import { usePluginStore } from '../../store'
|
||||||
|
|
||||||
|
|
@ -65,10 +65,29 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose, showOAuthCreate
|
||||||
|
|
||||||
const providerName = detail?.provider || ''
|
const providerName = detail?.provider || ''
|
||||||
const { mutate: initiateOAuth } = useInitiateTriggerOAuth()
|
const { mutate: initiateOAuth } = useInitiateTriggerOAuth()
|
||||||
const { mutate: verifyBuilder } = useVerifyTriggerSubscriptionBuilder()
|
const { mutate: verifyBuilder } = useVerifyAndUpdateTriggerSubscriptionBuilder()
|
||||||
const { mutate: configureOAuth } = useConfigureTriggerOAuth()
|
const { mutate: configureOAuth } = useConfigureTriggerOAuth()
|
||||||
const { mutate: deleteOAuth } = useDeleteTriggerOAuth()
|
const { mutate: deleteOAuth } = useDeleteTriggerOAuth()
|
||||||
|
|
||||||
|
const confirmButtonText = useMemo(() => {
|
||||||
|
if (authorizationStatus === AuthorizationStatusEnum.Pending)
|
||||||
|
return t('pluginTrigger.modal.common.authorizing')
|
||||||
|
if (authorizationStatus === AuthorizationStatusEnum.Success)
|
||||||
|
return t('pluginTrigger.modal.oauth.authorization.waitingJump')
|
||||||
|
return t('plugin.auth.saveAndAuth')
|
||||||
|
}, [authorizationStatus, t])
|
||||||
|
|
||||||
|
const getErrorMessage = (error: unknown, fallback: string) => {
|
||||||
|
if (error instanceof Error && error.message)
|
||||||
|
return error.message
|
||||||
|
if (typeof error === 'object' && error && 'message' in error) {
|
||||||
|
const message = (error as { message?: string }).message
|
||||||
|
if (typeof message === 'string' && message)
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
const handleAuthorization = () => {
|
const handleAuthorization = () => {
|
||||||
setAuthorizationStatus(AuthorizationStatusEnum.Pending)
|
setAuthorizationStatus(AuthorizationStatusEnum.Pending)
|
||||||
initiateOAuth(providerName, {
|
initiateOAuth(providerName, {
|
||||||
|
|
@ -130,10 +149,10 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose, showOAuthCreate
|
||||||
message: t('pluginTrigger.modal.oauth.remove.success'),
|
message: t('pluginTrigger.modal.oauth.remove.success'),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: unknown) => {
|
||||||
Toast.notify({
|
Toast.notify({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: error?.message || t('pluginTrigger.modal.oauth.remove.failed'),
|
message: getErrorMessage(error, t('pluginTrigger.modal.oauth.remove.failed')),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -179,9 +198,7 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose, showOAuthCreate
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={t('pluginTrigger.modal.oauth.title')}
|
title={t('pluginTrigger.modal.oauth.title')}
|
||||||
confirmButtonText={authorizationStatus === AuthorizationStatusEnum.Pending
|
confirmButtonText={confirmButtonText}
|
||||||
? t('pluginTrigger.modal.common.authorizing')
|
|
||||||
: authorizationStatus === AuthorizationStatusEnum.Success ? t('pluginTrigger.modal.oauth.authorization.waitingJump') : t('plugin.auth.saveAndAuth')}
|
|
||||||
cancelButtonText={t('plugin.auth.saveOnly')}
|
cancelButtonText={t('plugin.auth.saveOnly')}
|
||||||
extraButtonText={t('common.operation.cancel')}
|
extraButtonText={t('common.operation.cancel')}
|
||||||
showExtraButton
|
showExtraButton
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,349 @@
|
||||||
|
'use client'
|
||||||
|
import type { FormRefObject, FormSchema } from '@/app/components/base/form/types'
|
||||||
|
import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types'
|
||||||
|
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||||
|
import { isEqual } from 'lodash-es'
|
||||||
|
import { useMemo, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { EncryptedBottom } from '@/app/components/base/encrypted-bottom'
|
||||||
|
import { BaseForm } from '@/app/components/base/form/components/base'
|
||||||
|
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||||
|
import Modal from '@/app/components/base/modal/modal'
|
||||||
|
import Toast from '@/app/components/base/toast'
|
||||||
|
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
|
||||||
|
import { useUpdateTriggerSubscription, useVerifyTriggerSubscription } from '@/service/use-triggers'
|
||||||
|
import { parsePluginErrorMessage } from '@/utils/error-parser'
|
||||||
|
import { ReadmeShowType } from '../../../readme-panel/store'
|
||||||
|
import { usePluginStore } from '../../store'
|
||||||
|
import { useSubscriptionList } from '../use-subscription-list'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void
|
||||||
|
subscription: TriggerSubscription
|
||||||
|
pluginDetail?: PluginDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
enum EditStep {
|
||||||
|
EditCredentials = 'edit_credentials',
|
||||||
|
EditConfiguration = 'edit_configuration',
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeFormType = (type: string): FormTypeEnum => {
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
case 'text':
|
||||||
|
return FormTypeEnum.textInput
|
||||||
|
case 'password':
|
||||||
|
case 'secret':
|
||||||
|
return FormTypeEnum.secretInput
|
||||||
|
case 'number':
|
||||||
|
case 'integer':
|
||||||
|
return FormTypeEnum.textNumber
|
||||||
|
case 'boolean':
|
||||||
|
return FormTypeEnum.boolean
|
||||||
|
case 'select':
|
||||||
|
return FormTypeEnum.select
|
||||||
|
default:
|
||||||
|
if (Object.values(FormTypeEnum).includes(type as FormTypeEnum))
|
||||||
|
return type as FormTypeEnum
|
||||||
|
return FormTypeEnum.textInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const HIDDEN_SECRET_VALUE = '[__HIDDEN__]'
|
||||||
|
|
||||||
|
// Check if all credential values are hidden (meaning nothing was changed)
|
||||||
|
const areAllCredentialsHidden = (credentials: Record<string, unknown>): boolean => {
|
||||||
|
return Object.values(credentials).every(value => value === HIDDEN_SECRET_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusStep = ({ isActive, text, onClick, clickable }: {
|
||||||
|
isActive: boolean
|
||||||
|
text: string
|
||||||
|
onClick?: () => void
|
||||||
|
clickable?: boolean
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`system-2xs-semibold-uppercase flex items-center gap-1 ${isActive
|
||||||
|
? 'text-state-accent-solid'
|
||||||
|
: 'text-text-tertiary'} ${clickable ? 'cursor-pointer hover:text-text-secondary' : ''}`}
|
||||||
|
onClick={clickable ? onClick : undefined}
|
||||||
|
>
|
||||||
|
{isActive && (
|
||||||
|
<div className="h-1 w-1 rounded-full bg-state-accent-solid"></div>
|
||||||
|
)}
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const MultiSteps = ({ currentStep, onStepClick }: { currentStep: EditStep, onStepClick?: (step: EditStep) => void }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return (
|
||||||
|
<div className="mb-6 flex w-1/3 items-center gap-2">
|
||||||
|
<StatusStep
|
||||||
|
isActive={currentStep === EditStep.EditCredentials}
|
||||||
|
text={t('pluginTrigger.modal.steps.verify')}
|
||||||
|
onClick={() => onStepClick?.(EditStep.EditCredentials)}
|
||||||
|
clickable={currentStep === EditStep.EditConfiguration}
|
||||||
|
/>
|
||||||
|
<div className="h-px w-3 shrink-0 bg-divider-deep"></div>
|
||||||
|
<StatusStep
|
||||||
|
isActive={currentStep === EditStep.EditConfiguration}
|
||||||
|
text={t('pluginTrigger.modal.steps.configuration')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ApiKeyEditModal = ({ onClose, subscription, pluginDetail }: Props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const detail = usePluginStore(state => state.detail)
|
||||||
|
const { refetch } = useSubscriptionList()
|
||||||
|
|
||||||
|
const [currentStep, setCurrentStep] = useState<EditStep>(EditStep.EditCredentials)
|
||||||
|
const [verifiedCredentials, setVerifiedCredentials] = useState<Record<string, unknown> | null>(null)
|
||||||
|
|
||||||
|
const { mutate: updateSubscription, isPending: isUpdating } = useUpdateTriggerSubscription()
|
||||||
|
const { mutate: verifyCredentials, isPending: isVerifying } = useVerifyTriggerSubscription()
|
||||||
|
|
||||||
|
const parametersSchema = useMemo<ParametersSchema[]>(
|
||||||
|
() => detail?.declaration?.trigger?.subscription_constructor?.parameters || [],
|
||||||
|
[detail?.declaration?.trigger?.subscription_constructor?.parameters],
|
||||||
|
)
|
||||||
|
|
||||||
|
const apiKeyCredentialsSchema = useMemo(() => {
|
||||||
|
const rawSchema = detail?.declaration?.trigger?.subscription_constructor?.credentials_schema || []
|
||||||
|
return rawSchema.map(schema => ({
|
||||||
|
...schema,
|
||||||
|
tooltip: schema.help,
|
||||||
|
}))
|
||||||
|
}, [detail?.declaration?.trigger?.subscription_constructor?.credentials_schema])
|
||||||
|
|
||||||
|
const basicFormRef = useRef<FormRefObject>(null)
|
||||||
|
const parametersFormRef = useRef<FormRefObject>(null)
|
||||||
|
const credentialsFormRef = useRef<FormRefObject>(null)
|
||||||
|
|
||||||
|
const handleVerifyCredentials = () => {
|
||||||
|
const credentialsFormValues = credentialsFormRef.current?.getFormValues({
|
||||||
|
needTransformWhenSecretFieldIsPristine: true,
|
||||||
|
}) || { values: {}, isCheckValidated: false }
|
||||||
|
|
||||||
|
if (!credentialsFormValues.isCheckValidated)
|
||||||
|
return
|
||||||
|
|
||||||
|
const credentials = credentialsFormValues.values
|
||||||
|
|
||||||
|
verifyCredentials(
|
||||||
|
{
|
||||||
|
provider: subscription.provider,
|
||||||
|
subscriptionId: subscription.id,
|
||||||
|
credentials,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
Toast.notify({
|
||||||
|
type: 'success',
|
||||||
|
message: t('pluginTrigger.modal.apiKey.verify.success'),
|
||||||
|
})
|
||||||
|
// Only save credentials if any field was modified (not all hidden)
|
||||||
|
setVerifiedCredentials(areAllCredentialsHidden(credentials) ? null : credentials)
|
||||||
|
setCurrentStep(EditStep.EditConfiguration)
|
||||||
|
},
|
||||||
|
onError: async (error: unknown) => {
|
||||||
|
const errorMessage = await parsePluginErrorMessage(error) || t('pluginTrigger.modal.apiKey.verify.error')
|
||||||
|
Toast.notify({
|
||||||
|
type: 'error',
|
||||||
|
message: errorMessage,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdate = () => {
|
||||||
|
const basicFormValues = basicFormRef.current?.getFormValues({})
|
||||||
|
if (!basicFormValues?.isCheckValidated)
|
||||||
|
return
|
||||||
|
|
||||||
|
const name = basicFormValues.values.subscription_name as string
|
||||||
|
|
||||||
|
let parameters: Record<string, unknown> | undefined
|
||||||
|
|
||||||
|
if (parametersSchema.length > 0) {
|
||||||
|
const paramsFormValues = parametersFormRef.current?.getFormValues({
|
||||||
|
needTransformWhenSecretFieldIsPristine: true,
|
||||||
|
})
|
||||||
|
if (!paramsFormValues?.isCheckValidated)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Only send parameters if changed
|
||||||
|
const hasChanged = !isEqual(paramsFormValues.values, subscription.parameters || {})
|
||||||
|
parameters = hasChanged ? paramsFormValues.values : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSubscription(
|
||||||
|
{
|
||||||
|
subscriptionId: subscription.id,
|
||||||
|
name,
|
||||||
|
parameters,
|
||||||
|
credentials: verifiedCredentials || undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
Toast.notify({
|
||||||
|
type: 'success',
|
||||||
|
message: t('pluginTrigger.subscription.list.item.actions.edit.success'),
|
||||||
|
})
|
||||||
|
refetch?.()
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
onError: async (error: unknown) => {
|
||||||
|
const errorMessage = await parsePluginErrorMessage(error) || t('pluginTrigger.subscription.list.item.actions.edit.error')
|
||||||
|
Toast.notify({
|
||||||
|
type: 'error',
|
||||||
|
message: errorMessage,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (currentStep === EditStep.EditCredentials)
|
||||||
|
handleVerifyCredentials()
|
||||||
|
else
|
||||||
|
handleUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const basicFormSchemas: FormSchema[] = useMemo(() => [
|
||||||
|
{
|
||||||
|
name: 'subscription_name',
|
||||||
|
label: t('pluginTrigger.modal.form.subscriptionName.label'),
|
||||||
|
placeholder: t('pluginTrigger.modal.form.subscriptionName.placeholder'),
|
||||||
|
type: FormTypeEnum.textInput,
|
||||||
|
required: true,
|
||||||
|
default: subscription.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'callback_url',
|
||||||
|
label: t('pluginTrigger.modal.form.callbackUrl.label'),
|
||||||
|
placeholder: t('pluginTrigger.modal.form.callbackUrl.placeholder'),
|
||||||
|
type: FormTypeEnum.textInput,
|
||||||
|
required: false,
|
||||||
|
default: subscription.endpoint || '',
|
||||||
|
disabled: true,
|
||||||
|
tooltip: t('pluginTrigger.modal.form.callbackUrl.tooltip'),
|
||||||
|
showCopy: true,
|
||||||
|
},
|
||||||
|
], [t, subscription.name, subscription.endpoint])
|
||||||
|
|
||||||
|
const credentialsFormSchemas: FormSchema[] = useMemo(() => {
|
||||||
|
return apiKeyCredentialsSchema.map(schema => ({
|
||||||
|
...schema,
|
||||||
|
type: normalizeFormType(schema.type as string),
|
||||||
|
tooltip: schema.help,
|
||||||
|
default: subscription.credentials?.[schema.name] || schema.default,
|
||||||
|
}))
|
||||||
|
}, [apiKeyCredentialsSchema, subscription.credentials])
|
||||||
|
|
||||||
|
const parametersFormSchemas: FormSchema[] = useMemo(() => {
|
||||||
|
return parametersSchema.map((schema: ParametersSchema) => {
|
||||||
|
const normalizedType = normalizeFormType(schema.type as string)
|
||||||
|
return {
|
||||||
|
...schema,
|
||||||
|
type: normalizedType,
|
||||||
|
tooltip: schema.description,
|
||||||
|
default: subscription.parameters?.[schema.name] || schema.default,
|
||||||
|
dynamicSelectParams: normalizedType === FormTypeEnum.dynamicSelect
|
||||||
|
? {
|
||||||
|
plugin_id: detail?.plugin_id || '',
|
||||||
|
provider: detail?.provider || '',
|
||||||
|
action: 'provider',
|
||||||
|
parameter: schema.name,
|
||||||
|
credential_id: subscription.id,
|
||||||
|
credentials: verifiedCredentials || undefined,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
fieldClassName: schema.type === FormTypeEnum.boolean ? 'flex items-center justify-between' : undefined,
|
||||||
|
labelClassName: schema.type === FormTypeEnum.boolean ? 'mb-0' : undefined,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [parametersSchema, subscription.parameters, subscription.id, detail?.plugin_id, detail?.provider, verifiedCredentials])
|
||||||
|
|
||||||
|
const getConfirmButtonText = () => {
|
||||||
|
if (currentStep === EditStep.EditCredentials)
|
||||||
|
return isVerifying ? t('pluginTrigger.modal.common.verifying') : t('pluginTrigger.modal.common.verify')
|
||||||
|
|
||||||
|
return isUpdating ? t('common.operation.saving') : t('common.operation.save')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
setCurrentStep(EditStep.EditCredentials)
|
||||||
|
setVerifiedCredentials(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('pluginTrigger.subscription.list.item.actions.edit.title')}
|
||||||
|
confirmButtonText={getConfirmButtonText()}
|
||||||
|
onClose={onClose}
|
||||||
|
onCancel={onClose}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
disabled={isUpdating || isVerifying}
|
||||||
|
showExtraButton={currentStep === EditStep.EditConfiguration}
|
||||||
|
extraButtonText={t('pluginTrigger.modal.common.back')}
|
||||||
|
extraButtonVariant="secondary"
|
||||||
|
onExtraButtonClick={handleBack}
|
||||||
|
clickOutsideNotClose
|
||||||
|
wrapperClassName="!z-[101]"
|
||||||
|
bottomSlot={currentStep === EditStep.EditCredentials ? <EncryptedBottom /> : null}
|
||||||
|
>
|
||||||
|
{pluginDetail && (
|
||||||
|
<ReadmeEntrance pluginDetail={pluginDetail} showType={ReadmeShowType.modal} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Multi-step indicator */}
|
||||||
|
<MultiSteps currentStep={currentStep} onStepClick={handleBack} />
|
||||||
|
|
||||||
|
{/* Step 1: Edit Credentials */}
|
||||||
|
{currentStep === EditStep.EditCredentials && (
|
||||||
|
<div className="mb-4">
|
||||||
|
{credentialsFormSchemas.length > 0 && (
|
||||||
|
<BaseForm
|
||||||
|
formSchemas={credentialsFormSchemas}
|
||||||
|
ref={credentialsFormRef}
|
||||||
|
labelClassName="system-sm-medium mb-2 flex items-center gap-1 text-text-primary"
|
||||||
|
formClassName="space-y-4"
|
||||||
|
preventDefaultSubmit={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 2: Edit Configuration */}
|
||||||
|
{currentStep === EditStep.EditConfiguration && (
|
||||||
|
<div className="max-h-[70vh]">
|
||||||
|
{/* Basic form: subscription name and callback URL */}
|
||||||
|
<BaseForm
|
||||||
|
formSchemas={basicFormSchemas}
|
||||||
|
ref={basicFormRef}
|
||||||
|
labelClassName="system-sm-medium mb-2 flex items-center gap-1 text-text-primary"
|
||||||
|
formClassName="space-y-4 mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Parameters */}
|
||||||
|
{parametersFormSchemas.length > 0 && (
|
||||||
|
<BaseForm
|
||||||
|
formSchemas={parametersFormSchemas}
|
||||||
|
ref={parametersFormRef}
|
||||||
|
labelClassName="system-sm-medium mb-2 flex items-center gap-1 text-text-primary"
|
||||||
|
formClassName="space-y-4"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
'use client'
|
||||||
|
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||||
|
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||||
|
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
|
||||||
|
import { ApiKeyEditModal } from './apikey-edit-modal'
|
||||||
|
import { ManualEditModal } from './manual-edit-modal'
|
||||||
|
import { OAuthEditModal } from './oauth-edit-modal'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void
|
||||||
|
subscription: TriggerSubscription
|
||||||
|
pluginDetail?: PluginDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditModal = ({ onClose, subscription, pluginDetail }: Props) => {
|
||||||
|
const credentialType = subscription.credential_type
|
||||||
|
|
||||||
|
switch (credentialType) {
|
||||||
|
case TriggerCredentialTypeEnum.Unauthorized:
|
||||||
|
return <ManualEditModal onClose={onClose} subscription={subscription} pluginDetail={pluginDetail} />
|
||||||
|
case TriggerCredentialTypeEnum.Oauth2:
|
||||||
|
return <OAuthEditModal onClose={onClose} subscription={subscription} pluginDetail={pluginDetail} />
|
||||||
|
case TriggerCredentialTypeEnum.ApiKey:
|
||||||
|
return <ApiKeyEditModal onClose={onClose} subscription={subscription} pluginDetail={pluginDetail} />
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
'use client'
|
||||||
|
import type { FormRefObject, FormSchema } from '@/app/components/base/form/types'
|
||||||
|
import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types'
|
||||||
|
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||||
|
import { isEqual } from 'lodash-es'
|
||||||
|
import { useMemo, useRef } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { BaseForm } from '@/app/components/base/form/components/base'
|
||||||
|
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||||
|
import Modal from '@/app/components/base/modal/modal'
|
||||||
|
import Toast from '@/app/components/base/toast'
|
||||||
|
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
|
||||||
|
import { useUpdateTriggerSubscription } from '@/service/use-triggers'
|
||||||
|
import { ReadmeShowType } from '../../../readme-panel/store'
|
||||||
|
import { usePluginStore } from '../../store'
|
||||||
|
import { useSubscriptionList } from '../use-subscription-list'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void
|
||||||
|
subscription: TriggerSubscription
|
||||||
|
pluginDetail?: PluginDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeFormType = (type: string): FormTypeEnum => {
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
case 'text':
|
||||||
|
return FormTypeEnum.textInput
|
||||||
|
case 'password':
|
||||||
|
case 'secret':
|
||||||
|
return FormTypeEnum.secretInput
|
||||||
|
case 'number':
|
||||||
|
case 'integer':
|
||||||
|
return FormTypeEnum.textNumber
|
||||||
|
case 'boolean':
|
||||||
|
return FormTypeEnum.boolean
|
||||||
|
case 'select':
|
||||||
|
return FormTypeEnum.select
|
||||||
|
default:
|
||||||
|
if (Object.values(FormTypeEnum).includes(type as FormTypeEnum))
|
||||||
|
return type as FormTypeEnum
|
||||||
|
return FormTypeEnum.textInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ManualEditModal = ({ onClose, subscription, pluginDetail }: Props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const detail = usePluginStore(state => state.detail)
|
||||||
|
const { refetch } = useSubscriptionList()
|
||||||
|
|
||||||
|
const { mutate: updateSubscription, isPending: isUpdating } = useUpdateTriggerSubscription()
|
||||||
|
|
||||||
|
const getErrorMessage = (error: unknown, fallback: string) => {
|
||||||
|
if (error instanceof Error && error.message)
|
||||||
|
return error.message
|
||||||
|
if (typeof error === 'object' && error && 'message' in error) {
|
||||||
|
const message = (error as { message?: string }).message
|
||||||
|
if (typeof message === 'string' && message)
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
const propertiesSchema = useMemo<ParametersSchema[]>(
|
||||||
|
() => detail?.declaration?.trigger?.subscription_schema || [],
|
||||||
|
[detail?.declaration?.trigger?.subscription_schema],
|
||||||
|
)
|
||||||
|
|
||||||
|
const formRef = useRef<FormRefObject>(null)
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
const formValues = formRef.current?.getFormValues({
|
||||||
|
needTransformWhenSecretFieldIsPristine: true,
|
||||||
|
})
|
||||||
|
if (!formValues?.isCheckValidated)
|
||||||
|
return
|
||||||
|
|
||||||
|
const name = formValues.values.subscription_name as string
|
||||||
|
|
||||||
|
// Extract properties (exclude subscription_name and callback_url)
|
||||||
|
const newProperties = { ...formValues.values }
|
||||||
|
delete newProperties.subscription_name
|
||||||
|
delete newProperties.callback_url
|
||||||
|
|
||||||
|
// Only send properties if changed
|
||||||
|
const hasChanged = !isEqual(newProperties, subscription.properties || {})
|
||||||
|
const properties = hasChanged ? newProperties : undefined
|
||||||
|
|
||||||
|
updateSubscription(
|
||||||
|
{
|
||||||
|
subscriptionId: subscription.id,
|
||||||
|
name,
|
||||||
|
properties,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
Toast.notify({
|
||||||
|
type: 'success',
|
||||||
|
message: t('pluginTrigger.subscription.list.item.actions.edit.success'),
|
||||||
|
})
|
||||||
|
refetch?.()
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
Toast.notify({
|
||||||
|
type: 'error',
|
||||||
|
message: getErrorMessage(error, t('pluginTrigger.subscription.list.item.actions.edit.error')),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formSchemas: FormSchema[] = useMemo(() => [
|
||||||
|
{
|
||||||
|
name: 'subscription_name',
|
||||||
|
label: t('pluginTrigger.modal.form.subscriptionName.label'),
|
||||||
|
placeholder: t('pluginTrigger.modal.form.subscriptionName.placeholder'),
|
||||||
|
type: FormTypeEnum.textInput,
|
||||||
|
required: true,
|
||||||
|
default: subscription.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'callback_url',
|
||||||
|
label: t('pluginTrigger.modal.form.callbackUrl.label'),
|
||||||
|
placeholder: t('pluginTrigger.modal.form.callbackUrl.placeholder'),
|
||||||
|
type: FormTypeEnum.textInput,
|
||||||
|
required: false,
|
||||||
|
default: subscription.endpoint || '',
|
||||||
|
disabled: true,
|
||||||
|
tooltip: t('pluginTrigger.modal.form.callbackUrl.tooltip'),
|
||||||
|
showCopy: true,
|
||||||
|
},
|
||||||
|
...propertiesSchema.map((schema: ParametersSchema) => ({
|
||||||
|
...schema,
|
||||||
|
type: normalizeFormType(schema.type as string),
|
||||||
|
tooltip: schema.description,
|
||||||
|
default: subscription.properties?.[schema.name] || schema.default,
|
||||||
|
})),
|
||||||
|
], [t, subscription.name, subscription.endpoint, subscription.properties, propertiesSchema])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('pluginTrigger.subscription.list.item.actions.edit.title')}
|
||||||
|
confirmButtonText={isUpdating ? t('common.operation.saving') : t('common.operation.save')}
|
||||||
|
onClose={onClose}
|
||||||
|
onCancel={onClose}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
disabled={isUpdating}
|
||||||
|
clickOutsideNotClose
|
||||||
|
wrapperClassName="!z-[101]"
|
||||||
|
>
|
||||||
|
{pluginDetail && (
|
||||||
|
<ReadmeEntrance pluginDetail={pluginDetail} showType={ReadmeShowType.modal} />
|
||||||
|
)}
|
||||||
|
<BaseForm
|
||||||
|
formSchemas={formSchemas}
|
||||||
|
ref={formRef}
|
||||||
|
labelClassName="system-sm-medium mb-2 flex items-center gap-1 text-text-primary"
|
||||||
|
formClassName="space-y-4"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
'use client'
|
||||||
|
import type { FormRefObject, FormSchema } from '@/app/components/base/form/types'
|
||||||
|
import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types'
|
||||||
|
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||||
|
import { isEqual } from 'lodash-es'
|
||||||
|
import { useMemo, useRef } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { BaseForm } from '@/app/components/base/form/components/base'
|
||||||
|
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||||
|
import Modal from '@/app/components/base/modal/modal'
|
||||||
|
import Toast from '@/app/components/base/toast'
|
||||||
|
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
|
||||||
|
import { useUpdateTriggerSubscription } from '@/service/use-triggers'
|
||||||
|
import { ReadmeShowType } from '../../../readme-panel/store'
|
||||||
|
import { usePluginStore } from '../../store'
|
||||||
|
import { useSubscriptionList } from '../use-subscription-list'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void
|
||||||
|
subscription: TriggerSubscription
|
||||||
|
pluginDetail?: PluginDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeFormType = (type: string): FormTypeEnum => {
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
case 'text':
|
||||||
|
return FormTypeEnum.textInput
|
||||||
|
case 'password':
|
||||||
|
case 'secret':
|
||||||
|
return FormTypeEnum.secretInput
|
||||||
|
case 'number':
|
||||||
|
case 'integer':
|
||||||
|
return FormTypeEnum.textNumber
|
||||||
|
case 'boolean':
|
||||||
|
return FormTypeEnum.boolean
|
||||||
|
case 'select':
|
||||||
|
return FormTypeEnum.select
|
||||||
|
default:
|
||||||
|
if (Object.values(FormTypeEnum).includes(type as FormTypeEnum))
|
||||||
|
return type as FormTypeEnum
|
||||||
|
return FormTypeEnum.textInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OAuthEditModal = ({ onClose, subscription, pluginDetail }: Props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const detail = usePluginStore(state => state.detail)
|
||||||
|
const { refetch } = useSubscriptionList()
|
||||||
|
|
||||||
|
const { mutate: updateSubscription, isPending: isUpdating } = useUpdateTriggerSubscription()
|
||||||
|
|
||||||
|
const getErrorMessage = (error: unknown, fallback: string) => {
|
||||||
|
if (error instanceof Error && error.message)
|
||||||
|
return error.message
|
||||||
|
if (typeof error === 'object' && error && 'message' in error) {
|
||||||
|
const message = (error as { message?: string }).message
|
||||||
|
if (typeof message === 'string' && message)
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
const parametersSchema = useMemo<ParametersSchema[]>(
|
||||||
|
() => detail?.declaration?.trigger?.subscription_constructor?.parameters || [],
|
||||||
|
[detail?.declaration?.trigger?.subscription_constructor?.parameters],
|
||||||
|
)
|
||||||
|
|
||||||
|
const formRef = useRef<FormRefObject>(null)
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
const formValues = formRef.current?.getFormValues({
|
||||||
|
needTransformWhenSecretFieldIsPristine: true,
|
||||||
|
})
|
||||||
|
if (!formValues?.isCheckValidated)
|
||||||
|
return
|
||||||
|
|
||||||
|
const name = formValues.values.subscription_name as string
|
||||||
|
|
||||||
|
// Extract parameters (exclude subscription_name and callback_url)
|
||||||
|
const newParameters = { ...formValues.values }
|
||||||
|
delete newParameters.subscription_name
|
||||||
|
delete newParameters.callback_url
|
||||||
|
|
||||||
|
// Only send parameters if changed
|
||||||
|
const hasChanged = !isEqual(newParameters, subscription.parameters || {})
|
||||||
|
const parameters = hasChanged ? newParameters : undefined
|
||||||
|
|
||||||
|
updateSubscription(
|
||||||
|
{
|
||||||
|
subscriptionId: subscription.id,
|
||||||
|
name,
|
||||||
|
parameters,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
Toast.notify({
|
||||||
|
type: 'success',
|
||||||
|
message: t('pluginTrigger.subscription.list.item.actions.edit.success'),
|
||||||
|
})
|
||||||
|
refetch?.()
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
Toast.notify({
|
||||||
|
type: 'error',
|
||||||
|
message: getErrorMessage(error, t('pluginTrigger.subscription.list.item.actions.edit.error')),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formSchemas: FormSchema[] = useMemo(() => [
|
||||||
|
{
|
||||||
|
name: 'subscription_name',
|
||||||
|
label: t('pluginTrigger.modal.form.subscriptionName.label'),
|
||||||
|
placeholder: t('pluginTrigger.modal.form.subscriptionName.placeholder'),
|
||||||
|
type: FormTypeEnum.textInput,
|
||||||
|
required: true,
|
||||||
|
default: subscription.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'callback_url',
|
||||||
|
label: t('pluginTrigger.modal.form.callbackUrl.label'),
|
||||||
|
placeholder: t('pluginTrigger.modal.form.callbackUrl.placeholder'),
|
||||||
|
type: FormTypeEnum.textInput,
|
||||||
|
required: false,
|
||||||
|
default: subscription.endpoint || '',
|
||||||
|
disabled: true,
|
||||||
|
tooltip: t('pluginTrigger.modal.form.callbackUrl.tooltip'),
|
||||||
|
showCopy: true,
|
||||||
|
},
|
||||||
|
...parametersSchema.map((schema: ParametersSchema) => {
|
||||||
|
const normalizedType = normalizeFormType(schema.type as string)
|
||||||
|
return {
|
||||||
|
...schema,
|
||||||
|
type: normalizedType,
|
||||||
|
tooltip: schema.description,
|
||||||
|
default: subscription.parameters?.[schema.name] || schema.default,
|
||||||
|
dynamicSelectParams: normalizedType === FormTypeEnum.dynamicSelect
|
||||||
|
? {
|
||||||
|
plugin_id: detail?.plugin_id || '',
|
||||||
|
provider: detail?.provider || '',
|
||||||
|
action: 'provider',
|
||||||
|
parameter: schema.name,
|
||||||
|
credential_id: subscription.id,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
fieldClassName: schema.type === FormTypeEnum.boolean ? 'flex items-center justify-between' : undefined,
|
||||||
|
labelClassName: schema.type === FormTypeEnum.boolean ? 'mb-0' : undefined,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
], [t, subscription.name, subscription.endpoint, subscription.parameters, subscription.id, parametersSchema, detail?.plugin_id, detail?.provider])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('pluginTrigger.subscription.list.item.actions.edit.title')}
|
||||||
|
confirmButtonText={isUpdating ? t('common.operation.saving') : t('common.operation.save')}
|
||||||
|
onClose={onClose}
|
||||||
|
onCancel={onClose}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
disabled={isUpdating}
|
||||||
|
clickOutsideNotClose
|
||||||
|
wrapperClassName="!z-[101]"
|
||||||
|
>
|
||||||
|
{pluginDetail && (
|
||||||
|
<ReadmeEntrance pluginDetail={pluginDetail} showType={ReadmeShowType.modal} />
|
||||||
|
)}
|
||||||
|
<BaseForm
|
||||||
|
formSchemas={formSchemas}
|
||||||
|
ref={formRef}
|
||||||
|
labelClassName="system-sm-medium mb-2 flex items-center gap-1 text-text-primary"
|
||||||
|
formClassName="space-y-4"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,31 +1,27 @@
|
||||||
|
import type { SimpleSubscription } from './types'
|
||||||
|
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||||
import { withErrorBoundary } from '@/app/components/base/error-boundary'
|
import { withErrorBoundary } from '@/app/components/base/error-boundary'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import { SubscriptionListView } from './list-view'
|
import { SubscriptionListView } from './list-view'
|
||||||
import { SubscriptionSelectorView } from './selector-view'
|
import { SubscriptionSelectorView } from './selector-view'
|
||||||
|
import { SubscriptionListMode } from './types'
|
||||||
import { useSubscriptionList } from './use-subscription-list'
|
import { useSubscriptionList } from './use-subscription-list'
|
||||||
|
|
||||||
export enum SubscriptionListMode {
|
|
||||||
PANEL = 'panel',
|
|
||||||
SELECTOR = 'selector',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SimpleSubscription = {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubscriptionListProps = {
|
type SubscriptionListProps = {
|
||||||
mode?: SubscriptionListMode
|
mode?: SubscriptionListMode
|
||||||
selectedId?: string
|
selectedId?: string
|
||||||
onSelect?: (v: SimpleSubscription, callback?: () => void) => void
|
onSelect?: (v: SimpleSubscription, callback?: () => void) => void
|
||||||
|
pluginDetail?: PluginDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
export { SubscriptionSelectorEntry } from './selector-entry'
|
export { SubscriptionSelectorEntry } from './selector-entry'
|
||||||
|
export type { SimpleSubscription } from './types'
|
||||||
|
|
||||||
export const SubscriptionList = withErrorBoundary(({
|
export const SubscriptionList = withErrorBoundary(({
|
||||||
mode = SubscriptionListMode.PANEL,
|
mode = SubscriptionListMode.PANEL,
|
||||||
selectedId,
|
selectedId,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
pluginDetail,
|
||||||
}: SubscriptionListProps) => {
|
}: SubscriptionListProps) => {
|
||||||
const { isLoading, refetch } = useSubscriptionList()
|
const { isLoading, refetch } = useSubscriptionList()
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|
@ -47,5 +43,5 @@ export const SubscriptionList = withErrorBoundary(({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <SubscriptionListView />
|
return <SubscriptionListView pluginDetail={pluginDetail} />
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
|
|
@ -9,10 +10,12 @@ import { useSubscriptionList } from './use-subscription-list'
|
||||||
|
|
||||||
type SubscriptionListViewProps = {
|
type SubscriptionListViewProps = {
|
||||||
showTopBorder?: boolean
|
showTopBorder?: boolean
|
||||||
|
pluginDetail?: PluginDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SubscriptionListView: React.FC<SubscriptionListViewProps> = ({
|
export const SubscriptionListView: React.FC<SubscriptionListViewProps> = ({
|
||||||
showTopBorder = false,
|
showTopBorder = false,
|
||||||
|
pluginDetail,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { subscriptions } = useSubscriptionList()
|
const { subscriptions } = useSubscriptionList()
|
||||||
|
|
@ -41,6 +44,7 @@ export const SubscriptionListView: React.FC<SubscriptionListViewProps> = ({
|
||||||
<SubscriptionCard
|
<SubscriptionCard
|
||||||
key={subscription.id}
|
key={subscription.id}
|
||||||
data={subscription}
|
data={subscription}
|
||||||
|
pluginDetail={pluginDetail}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
'use client'
|
'use client'
|
||||||
import type { SimpleSubscription } from '@/app/components/plugins/plugin-detail-panel/subscription-list'
|
import type { SimpleSubscription } from './types'
|
||||||
import { RiArrowDownSLine, RiWebhookLine } from '@remixicon/react'
|
import { RiArrowDownSLine, RiWebhookLine } from '@remixicon/react'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
@ -8,8 +8,9 @@ import {
|
||||||
PortalToFollowElemContent,
|
PortalToFollowElemContent,
|
||||||
PortalToFollowElemTrigger,
|
PortalToFollowElemTrigger,
|
||||||
} from '@/app/components/base/portal-to-follow-elem'
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
import { SubscriptionList, SubscriptionListMode } from '@/app/components/plugins/plugin-detail-panel/subscription-list'
|
import { SubscriptionList } from '@/app/components/plugins/plugin-detail-panel/subscription-list'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { SubscriptionListMode } from './types'
|
||||||
import { useSubscriptionList } from './use-subscription-list'
|
import { useSubscriptionList } from './use-subscription-list'
|
||||||
|
|
||||||
type SubscriptionTriggerButtonProps = {
|
type SubscriptionTriggerButtonProps = {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||||
import {
|
import {
|
||||||
RiDeleteBinLine,
|
RiDeleteBinLine,
|
||||||
|
RiEditLine,
|
||||||
RiWebhookLine,
|
RiWebhookLine,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import { useBoolean } from 'ahooks'
|
import { useBoolean } from 'ahooks'
|
||||||
|
|
@ -10,17 +12,23 @@ import ActionButton from '@/app/components/base/action-button'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
import { DeleteConfirm } from './delete-confirm'
|
import { DeleteConfirm } from './delete-confirm'
|
||||||
|
import { EditModal } from './edit'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: TriggerSubscription
|
data: TriggerSubscription
|
||||||
|
pluginDetail?: PluginDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
const SubscriptionCard = ({ data }: Props) => {
|
const SubscriptionCard = ({ data, pluginDetail }: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [isShowDeleteModal, {
|
const [isShowDeleteModal, {
|
||||||
setTrue: showDeleteModal,
|
setTrue: showDeleteModal,
|
||||||
setFalse: hideDeleteModal,
|
setFalse: hideDeleteModal,
|
||||||
}] = useBoolean(false)
|
}] = useBoolean(false)
|
||||||
|
const [isShowEditModal, {
|
||||||
|
setTrue: showEditModal,
|
||||||
|
setFalse: hideEditModal,
|
||||||
|
}] = useBoolean(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -40,13 +48,21 @@ const SubscriptionCard = ({ data }: Props) => {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden items-center gap-1 group-hover:flex">
|
||||||
|
<ActionButton
|
||||||
|
onClick={showEditModal}
|
||||||
|
className="transition-colors hover:bg-state-base-hover"
|
||||||
|
>
|
||||||
|
<RiEditLine className="h-4 w-4" />
|
||||||
|
</ActionButton>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={showDeleteModal}
|
onClick={showDeleteModal}
|
||||||
className="subscription-delete-btn hidden transition-colors hover:bg-state-destructive-hover hover:text-text-destructive group-hover:block"
|
className="subscription-delete-btn transition-colors hover:bg-state-destructive-hover hover:text-text-destructive"
|
||||||
>
|
>
|
||||||
<RiDeleteBinLine className="h-4 w-4" />
|
<RiDeleteBinLine className="h-4 w-4" />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-1 flex items-center justify-between">
|
<div className="mt-1 flex items-center justify-between">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|
@ -78,6 +94,14 @@ const SubscriptionCard = ({ data }: Props) => {
|
||||||
workflowsInUse={data.workflows_in_use}
|
workflowsInUse={data.workflows_in_use}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isShowEditModal && (
|
||||||
|
<EditModal
|
||||||
|
onClose={hideEditModal}
|
||||||
|
subscription={data}
|
||||||
|
pluginDetail={pluginDetail}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
export enum SubscriptionListMode {
|
||||||
|
PANEL = 'panel',
|
||||||
|
SELECTOR = 'selector',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SimpleSubscription = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
@ -131,7 +131,7 @@ export type ParametersSchema = {
|
||||||
scope: any
|
scope: any
|
||||||
required: boolean
|
required: boolean
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
default?: string[]
|
default?: string | string[]
|
||||||
min: any
|
min: any
|
||||||
max: any
|
max: any
|
||||||
precision: any
|
precision: any
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,9 @@ export type TriggerDefaultValue = PluginCommonDefaultValue & {
|
||||||
title: string
|
title: string
|
||||||
plugin_unique_identifier: string
|
plugin_unique_identifier: string
|
||||||
is_team_authorization: boolean
|
is_team_authorization: boolean
|
||||||
params: Record<string, any>
|
params: Record<string, unknown>
|
||||||
paramSchemas: Record<string, any>[]
|
paramSchemas: Record<string, unknown>[]
|
||||||
output_schema: Record<string, any>
|
output_schema: Record<string, unknown>
|
||||||
subscription_id?: string
|
subscription_id?: string
|
||||||
meta?: PluginMeta
|
meta?: PluginMeta
|
||||||
}
|
}
|
||||||
|
|
@ -52,9 +52,9 @@ export type ToolDefaultValue = PluginCommonDefaultValue & {
|
||||||
tool_description: string
|
tool_description: string
|
||||||
title: string
|
title: string
|
||||||
is_team_authorization: boolean
|
is_team_authorization: boolean
|
||||||
params: Record<string, any>
|
params: Record<string, unknown>
|
||||||
paramSchemas: Record<string, any>[]
|
paramSchemas: Record<string, unknown>[]
|
||||||
output_schema?: Record<string, any>
|
output_schema?: Record<string, unknown>
|
||||||
credential_id?: string
|
credential_id?: string
|
||||||
meta?: PluginMeta
|
meta?: PluginMeta
|
||||||
plugin_id?: string
|
plugin_id?: string
|
||||||
|
|
@ -82,10 +82,10 @@ export type ToolValue = {
|
||||||
tool_name: string
|
tool_name: string
|
||||||
tool_label: string
|
tool_label: string
|
||||||
tool_description?: string
|
tool_description?: string
|
||||||
settings?: Record<string, any>
|
settings?: Record<string, unknown>
|
||||||
parameters?: Record<string, any>
|
parameters?: Record<string, unknown>
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
extra?: Record<string, any>
|
extra?: { description?: string } & Record<string, unknown>
|
||||||
credential_id?: string
|
credential_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,7 +94,7 @@ export type DataSourceItem = {
|
||||||
plugin_unique_identifier: string
|
plugin_unique_identifier: string
|
||||||
provider: string
|
provider: string
|
||||||
declaration: {
|
declaration: {
|
||||||
credentials_schema: any[]
|
credentials_schema: unknown[]
|
||||||
provider_type: string
|
provider_type: string
|
||||||
identity: {
|
identity: {
|
||||||
author: string
|
author: string
|
||||||
|
|
@ -113,10 +113,10 @@ export type DataSourceItem = {
|
||||||
name: string
|
name: string
|
||||||
provider: string
|
provider: string
|
||||||
}
|
}
|
||||||
parameters: any[]
|
parameters: unknown[]
|
||||||
output_schema?: {
|
output_schema?: {
|
||||||
type: string
|
type: string
|
||||||
properties: Record<string, any>
|
properties: Record<string, unknown>
|
||||||
}
|
}
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
|
@ -133,15 +133,15 @@ export type TriggerParameter = {
|
||||||
| 'model-selector' | 'app-selector' | 'object' | 'array' | 'dynamic-select'
|
| 'model-selector' | 'app-selector' | 'object' | 'array' | 'dynamic-select'
|
||||||
auto_generate?: {
|
auto_generate?: {
|
||||||
type: string
|
type: string
|
||||||
value?: any
|
value?: unknown
|
||||||
} | null
|
} | null
|
||||||
template?: {
|
template?: {
|
||||||
type: string
|
type: string
|
||||||
value?: any
|
value?: unknown
|
||||||
} | null
|
} | null
|
||||||
scope?: string | null
|
scope?: string | null
|
||||||
required?: boolean
|
required?: boolean
|
||||||
default?: any
|
default?: unknown
|
||||||
min?: number | null
|
min?: number | null
|
||||||
max?: number | null
|
max?: number | null
|
||||||
precision?: number | null
|
precision?: number | null
|
||||||
|
|
@ -158,7 +158,7 @@ export type TriggerCredentialField = {
|
||||||
name: string
|
name: string
|
||||||
scope?: string | null
|
scope?: string | null
|
||||||
required: boolean
|
required: boolean
|
||||||
default?: string | number | boolean | Array<any> | null
|
default?: string | number | boolean | Array<unknown> | null
|
||||||
options?: Array<{
|
options?: Array<{
|
||||||
value: string
|
value: string
|
||||||
label: TypeWithI18N
|
label: TypeWithI18N
|
||||||
|
|
@ -191,7 +191,7 @@ export type TriggerApiEntity = {
|
||||||
identity: TriggerIdentity
|
identity: TriggerIdentity
|
||||||
description: TypeWithI18N
|
description: TypeWithI18N
|
||||||
parameters: TriggerParameter[]
|
parameters: TriggerParameter[]
|
||||||
output_schema?: Record<string, any>
|
output_schema?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TriggerProviderApiEntity = {
|
export type TriggerProviderApiEntity = {
|
||||||
|
|
@ -237,32 +237,15 @@ type TriggerSubscriptionStructure = {
|
||||||
name: string
|
name: string
|
||||||
provider: string
|
provider: string
|
||||||
credential_type: TriggerCredentialTypeEnum
|
credential_type: TriggerCredentialTypeEnum
|
||||||
credentials: TriggerSubCredentials
|
credentials: Record<string, unknown>
|
||||||
endpoint: string
|
endpoint: string
|
||||||
parameters: TriggerSubParameters
|
parameters: Record<string, unknown>
|
||||||
properties: TriggerSubProperties
|
properties: Record<string, unknown>
|
||||||
workflows_in_use: number
|
workflows_in_use: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TriggerSubscription = TriggerSubscriptionStructure
|
export type TriggerSubscription = TriggerSubscriptionStructure
|
||||||
|
|
||||||
export type TriggerSubCredentials = {
|
|
||||||
access_tokens: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TriggerSubParameters = {
|
|
||||||
repository: string
|
|
||||||
webhook_secret?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TriggerSubProperties = {
|
|
||||||
active: boolean
|
|
||||||
events: string[]
|
|
||||||
external_id: string
|
|
||||||
repository: string
|
|
||||||
webhook_secret?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TriggerSubscriptionBuilder = TriggerSubscriptionStructure
|
export type TriggerSubscriptionBuilder = TriggerSubscriptionStructure
|
||||||
|
|
||||||
// OAuth configuration types
|
// OAuth configuration types
|
||||||
|
|
@ -275,7 +258,7 @@ export type TriggerOAuthConfig = {
|
||||||
params: {
|
params: {
|
||||||
client_id: string
|
client_id: string
|
||||||
client_secret: string
|
client_secret: string
|
||||||
[key: string]: any
|
[key: string]: string
|
||||||
}
|
}
|
||||||
system_configured: boolean
|
system_configured: boolean
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import {
|
||||||
useBuildTriggerSubscription,
|
useBuildTriggerSubscription,
|
||||||
useCreateTriggerSubscriptionBuilder,
|
useCreateTriggerSubscriptionBuilder,
|
||||||
useUpdateTriggerSubscriptionBuilder,
|
useUpdateTriggerSubscriptionBuilder,
|
||||||
useVerifyTriggerSubscriptionBuilder,
|
useVerifyAndUpdateTriggerSubscriptionBuilder,
|
||||||
} from '@/service/use-triggers'
|
} from '@/service/use-triggers'
|
||||||
|
|
||||||
// Helper function to serialize complex values to strings for backend encryption
|
// Helper function to serialize complex values to strings for backend encryption
|
||||||
const serializeFormValues = (values: Record<string, any>): Record<string, string> => {
|
const serializeFormValues = (values: Record<string, unknown>): Record<string, string> => {
|
||||||
const result: Record<string, string> = {}
|
const result: Record<string, string> = {}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(values)) {
|
for (const [key, value] of Object.entries(values)) {
|
||||||
|
|
@ -23,6 +23,17 @@ const serializeFormValues = (values: Record<string, any>): Record<string, string
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getErrorMessage = (error: unknown, fallback: string) => {
|
||||||
|
if (error instanceof Error && error.message)
|
||||||
|
return error.message
|
||||||
|
if (typeof error === 'object' && error && 'message' in error) {
|
||||||
|
const message = (error as { message?: string }).message
|
||||||
|
if (typeof message === 'string' && message)
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
export type AuthFlowStep = 'auth' | 'params' | 'complete'
|
export type AuthFlowStep = 'auth' | 'params' | 'complete'
|
||||||
|
|
||||||
export type AuthFlowState = {
|
export type AuthFlowState = {
|
||||||
|
|
@ -34,8 +45,8 @@ export type AuthFlowState = {
|
||||||
|
|
||||||
export type AuthFlowActions = {
|
export type AuthFlowActions = {
|
||||||
startAuth: () => Promise<void>
|
startAuth: () => Promise<void>
|
||||||
verifyAuth: (credentials: Record<string, any>) => Promise<void>
|
verifyAuth: (credentials: Record<string, unknown>) => Promise<void>
|
||||||
completeConfig: (parameters: Record<string, any>, properties?: Record<string, any>, name?: string) => Promise<void>
|
completeConfig: (parameters: Record<string, unknown>, properties?: Record<string, unknown>, name?: string) => Promise<void>
|
||||||
reset: () => void
|
reset: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,7 +58,7 @@ export const useTriggerAuthFlow = (provider: TriggerWithProvider): AuthFlowState
|
||||||
|
|
||||||
const createBuilder = useCreateTriggerSubscriptionBuilder()
|
const createBuilder = useCreateTriggerSubscriptionBuilder()
|
||||||
const updateBuilder = useUpdateTriggerSubscriptionBuilder()
|
const updateBuilder = useUpdateTriggerSubscriptionBuilder()
|
||||||
const verifyBuilder = useVerifyTriggerSubscriptionBuilder()
|
const verifyBuilder = useVerifyAndUpdateTriggerSubscriptionBuilder()
|
||||||
const buildSubscription = useBuildTriggerSubscription()
|
const buildSubscription = useBuildTriggerSubscription()
|
||||||
|
|
||||||
const startAuth = useCallback(async () => {
|
const startAuth = useCallback(async () => {
|
||||||
|
|
@ -64,8 +75,8 @@ export const useTriggerAuthFlow = (provider: TriggerWithProvider): AuthFlowState
|
||||||
setBuilderId(response.subscription_builder.id)
|
setBuilderId(response.subscription_builder.id)
|
||||||
setStep('auth')
|
setStep('auth')
|
||||||
}
|
}
|
||||||
catch (err: any) {
|
catch (err: unknown) {
|
||||||
setError(err.message || 'Failed to start authentication flow')
|
setError(getErrorMessage(err, 'Failed to start authentication flow'))
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
|
@ -73,7 +84,7 @@ export const useTriggerAuthFlow = (provider: TriggerWithProvider): AuthFlowState
|
||||||
}
|
}
|
||||||
}, [provider.name, createBuilder, builderId])
|
}, [provider.name, createBuilder, builderId])
|
||||||
|
|
||||||
const verifyAuth = useCallback(async (credentials: Record<string, any>) => {
|
const verifyAuth = useCallback(async (credentials: Record<string, unknown>) => {
|
||||||
if (!builderId) {
|
if (!builderId) {
|
||||||
setError('No builder ID available')
|
setError('No builder ID available')
|
||||||
return
|
return
|
||||||
|
|
@ -96,8 +107,8 @@ export const useTriggerAuthFlow = (provider: TriggerWithProvider): AuthFlowState
|
||||||
|
|
||||||
setStep('params')
|
setStep('params')
|
||||||
}
|
}
|
||||||
catch (err: any) {
|
catch (err: unknown) {
|
||||||
setError(err.message || 'Authentication verification failed')
|
setError(getErrorMessage(err, 'Authentication verification failed'))
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
|
@ -106,8 +117,8 @@ export const useTriggerAuthFlow = (provider: TriggerWithProvider): AuthFlowState
|
||||||
}, [provider.name, builderId, updateBuilder, verifyBuilder])
|
}, [provider.name, builderId, updateBuilder, verifyBuilder])
|
||||||
|
|
||||||
const completeConfig = useCallback(async (
|
const completeConfig = useCallback(async (
|
||||||
parameters: Record<string, any>,
|
parameters: Record<string, unknown>,
|
||||||
properties: Record<string, any> = {},
|
properties: Record<string, unknown> = {},
|
||||||
name?: string,
|
name?: string,
|
||||||
) => {
|
) => {
|
||||||
if (!builderId) {
|
if (!builderId) {
|
||||||
|
|
@ -134,8 +145,8 @@ export const useTriggerAuthFlow = (provider: TriggerWithProvider): AuthFlowState
|
||||||
|
|
||||||
setStep('complete')
|
setStep('complete')
|
||||||
}
|
}
|
||||||
catch (err: any) {
|
catch (err: unknown) {
|
||||||
setError(err.message || 'Configuration failed')
|
setError(getErrorMessage(err, 'Configuration failed'))
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ const translation = {
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
clear: 'Clear',
|
clear: 'Clear',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
|
saving: 'Saving...',
|
||||||
yes: 'Yes',
|
yes: 'Yes',
|
||||||
no: 'No',
|
no: 'No',
|
||||||
deleteConfirmTitle: 'Delete?',
|
deleteConfirmTitle: 'Delete?',
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@ const translation = {
|
||||||
unauthorized: 'Manual',
|
unauthorized: 'Manual',
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
edit: {
|
||||||
|
title: 'Edit Subscription',
|
||||||
|
success: 'Subscription updated successfully',
|
||||||
|
error: 'Failed to update subscription',
|
||||||
|
},
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
deleteConfirm: {
|
deleteConfirm: {
|
||||||
title: 'Delete {{name}}?',
|
title: 'Delete {{name}}?',
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { FormOption } from '@/app/components/base/form/types'
|
||||||
import type {
|
import type {
|
||||||
TriggerLogEntity,
|
TriggerLogEntity,
|
||||||
TriggerOAuthClientParams,
|
TriggerOAuthClientParams,
|
||||||
|
|
@ -149,9 +150,9 @@ export const useUpdateTriggerSubscriptionBuilder = () => {
|
||||||
provider: string
|
provider: string
|
||||||
subscriptionBuilderId: string
|
subscriptionBuilderId: string
|
||||||
name?: string
|
name?: string
|
||||||
properties?: Record<string, any>
|
properties?: Record<string, unknown>
|
||||||
parameters?: Record<string, any>
|
parameters?: Record<string, unknown>
|
||||||
credentials?: Record<string, any>
|
credentials?: Record<string, unknown>
|
||||||
}) => {
|
}) => {
|
||||||
const { provider, subscriptionBuilderId, ...body } = payload
|
const { provider, subscriptionBuilderId, ...body } = payload
|
||||||
return post<TriggerSubscriptionBuilder>(
|
return post<TriggerSubscriptionBuilder>(
|
||||||
|
|
@ -162,17 +163,35 @@ export const useUpdateTriggerSubscriptionBuilder = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useVerifyTriggerSubscriptionBuilder = () => {
|
export const useVerifyAndUpdateTriggerSubscriptionBuilder = () => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: [NAME_SPACE, 'verify-subscription-builder'],
|
mutationKey: [NAME_SPACE, 'verify-and-update-subscription-builder'],
|
||||||
mutationFn: (payload: {
|
mutationFn: (payload: {
|
||||||
provider: string
|
provider: string
|
||||||
subscriptionBuilderId: string
|
subscriptionBuilderId: string
|
||||||
credentials?: Record<string, any>
|
credentials?: Record<string, unknown>
|
||||||
}) => {
|
}) => {
|
||||||
const { provider, subscriptionBuilderId, ...body } = payload
|
const { provider, subscriptionBuilderId, ...body } = payload
|
||||||
return post<{ verified: boolean }>(
|
return post<{ verified: boolean }>(
|
||||||
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/verify/${subscriptionBuilderId}`,
|
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/verify-and-update/${subscriptionBuilderId}`,
|
||||||
|
{ body },
|
||||||
|
{ silent: true },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useVerifyTriggerSubscription = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: [NAME_SPACE, 'verify-subscription'],
|
||||||
|
mutationFn: (payload: {
|
||||||
|
provider: string
|
||||||
|
subscriptionId: string
|
||||||
|
credentials?: Record<string, unknown>
|
||||||
|
}) => {
|
||||||
|
const { provider, subscriptionId, ...body } = payload
|
||||||
|
return post<{ verified: boolean }>(
|
||||||
|
`/workspaces/current/trigger-provider/${provider}/subscriptions/verify/${subscriptionId}`,
|
||||||
{ body },
|
{ body },
|
||||||
{ silent: true },
|
{ silent: true },
|
||||||
)
|
)
|
||||||
|
|
@ -184,7 +203,7 @@ export type BuildTriggerSubscriptionPayload = {
|
||||||
provider: string
|
provider: string
|
||||||
subscriptionBuilderId: string
|
subscriptionBuilderId: string
|
||||||
name?: string
|
name?: string
|
||||||
parameters?: Record<string, any>
|
parameters?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useBuildTriggerSubscription = () => {
|
export const useBuildTriggerSubscription = () => {
|
||||||
|
|
@ -211,6 +230,27 @@ export const useDeleteTriggerSubscription = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UpdateTriggerSubscriptionPayload = {
|
||||||
|
subscriptionId: string
|
||||||
|
name?: string
|
||||||
|
properties?: Record<string, unknown>
|
||||||
|
parameters?: Record<string, unknown>
|
||||||
|
credentials?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdateTriggerSubscription = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: [NAME_SPACE, 'update-subscription'],
|
||||||
|
mutationFn: (payload: UpdateTriggerSubscriptionPayload) => {
|
||||||
|
const { subscriptionId, ...body } = payload
|
||||||
|
return post<{ result: string, id: string }>(
|
||||||
|
`/workspaces/current/trigger-provider/${subscriptionId}/subscriptions/update`,
|
||||||
|
{ body },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const useTriggerSubscriptionBuilderLogs = (
|
export const useTriggerSubscriptionBuilderLogs = (
|
||||||
provider: string,
|
provider: string,
|
||||||
subscriptionBuilderId: string,
|
subscriptionBuilderId: string,
|
||||||
|
|
@ -290,20 +330,45 @@ export const useTriggerPluginDynamicOptions = (payload: {
|
||||||
action: string
|
action: string
|
||||||
parameter: string
|
parameter: string
|
||||||
credential_id: string
|
credential_id: string
|
||||||
extra?: Record<string, any>
|
credentials?: Record<string, unknown>
|
||||||
|
extra?: Record<string, unknown>
|
||||||
}, enabled = true) => {
|
}, enabled = true) => {
|
||||||
return useQuery<{ options: Array<{ value: string, label: any }> }>({
|
return useQuery<{ options: FormOption[] }>({
|
||||||
queryKey: [NAME_SPACE, 'dynamic-options', payload.plugin_id, payload.provider, payload.action, payload.parameter, payload.credential_id, payload.extra],
|
queryKey: [NAME_SPACE, 'dynamic-options', payload.plugin_id, payload.provider, payload.action, payload.parameter, payload.credential_id, payload.credentials, payload.extra],
|
||||||
queryFn: () => get<{ options: Array<{ value: string, label: any }> }>(
|
queryFn: () => {
|
||||||
'/workspaces/current/plugin/parameters/dynamic-options',
|
// Use new endpoint with POST when credentials provided (for edit mode)
|
||||||
|
if (payload.credentials) {
|
||||||
|
return post<{ options: FormOption[] }>(
|
||||||
|
'/workspaces/current/plugin/parameters/dynamic-options-with-credentials',
|
||||||
{
|
{
|
||||||
params: {
|
body: {
|
||||||
...payload,
|
plugin_id: payload.plugin_id,
|
||||||
provider_type: 'trigger', // Add required provider_type parameter
|
provider: payload.provider,
|
||||||
|
action: payload.action,
|
||||||
|
parameter: payload.parameter,
|
||||||
|
credential_id: payload.credential_id,
|
||||||
|
credentials: payload.credentials,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ silent: true },
|
{ silent: true },
|
||||||
),
|
)
|
||||||
|
}
|
||||||
|
// Use original GET endpoint for normal cases
|
||||||
|
return get<{ options: FormOption[] }>(
|
||||||
|
'/workspaces/current/plugin/parameters/dynamic-options',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
plugin_id: payload.plugin_id,
|
||||||
|
provider: payload.provider,
|
||||||
|
action: payload.action,
|
||||||
|
parameter: payload.parameter,
|
||||||
|
credential_id: payload.credential_id,
|
||||||
|
provider_type: 'trigger',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ silent: true },
|
||||||
|
)
|
||||||
|
},
|
||||||
enabled: enabled && !!payload.plugin_id && !!payload.provider && !!payload.action && !!payload.parameter && !!payload.credential_id,
|
enabled: enabled && !!payload.plugin_id && !!payload.provider && !!payload.action && !!payload.parameter && !!payload.credential_id,
|
||||||
retry: 0,
|
retry: 0,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue