Compare commits

..

5 Commits

Author SHA1 Message Date
Bently
82bddd885b Merge branch 'dev' into update-install-scripts 2025-08-19 15:57:45 +01:00
Bentlybro
c71406af8b Simplify setup scripts and remove Sentry prompts
Refactored Windows and Linux setup scripts to streamline prerequisite checks, repository detection, and service startup. Removed Sentry configuration and related prompts for a simpler setup experience. Updated user messaging and improved error handling for common Docker issues.
2025-08-13 18:07:22 +01:00
Bently
468d1af802 Merge branch 'dev' into update-install-scripts 2025-08-08 12:47:12 +01:00
Bentlybro
a2c88c7786 Refactor setup scripts for improved reliability and clarity
Reworked both Windows (.bat) and Unix (.sh) setup scripts to improve error handling, logging, and user prompts. The scripts now check for prerequisites, handle Sentry enablement more clearly, ensure environment files are copied with error checks, and consolidate service startup into a single docker compose command with log output. Unused or redundant code was removed for maintainability.
2025-08-07 10:35:48 +01:00
Bentlybro
e79b7a95dc Remove auto-start of frontend dev server in setup scripts
The setup-autogpt.bat and setup-autogpt.sh scripts no longer automatically start the frontend development server after setup. Users are now instructed to manually stop services with 'docker compose down', and the scripts prompt for exit while keeping services running.
2025-08-07 10:03:45 +01:00
35 changed files with 150 additions and 1307 deletions

View File

@@ -34,10 +34,10 @@ from backend.data.model import (
from backend.data.notifications import NotificationEventModel, RefundRequestData
from backend.data.user import get_user_by_id, get_user_email_by_id
from backend.notifications.notifications import queue_notification_async
from backend.server.model import Pagination
from backend.server.v2.admin.model import UserHistoryResponse
from backend.util.exceptions import InsufficientBalanceError
from backend.util.json import SafeJson
from backend.util.models import Pagination
from backend.util.retry import func_retry
from backend.util.settings import Settings

View File

@@ -60,6 +60,21 @@ class UpdatePermissionsRequest(pydantic.BaseModel):
permissions: list[APIKeyPermission]
class Pagination(pydantic.BaseModel):
total_items: int = pydantic.Field(
description="Total number of items.", examples=[42]
)
total_pages: int = pydantic.Field(
description="Total number of pages.", examples=[2]
)
current_page: int = pydantic.Field(
description="Current_page page number.", examples=[1]
)
page_size: int = pydantic.Field(
description="Number of items per page.", examples=[25]
)
class RequestTopUp(pydantic.BaseModel):
credit_amount: int

View File

@@ -1071,6 +1071,7 @@ async def get_api_key(
tags=["api-keys"],
dependencies=[Depends(auth_middleware)],
)
@feature_flag("api-keys-enabled")
async def delete_api_key(
key_id: str, user_id: Annotated[str, Depends(get_user_id)]
) -> Optional[APIKeyWithoutHash]:
@@ -1099,6 +1100,7 @@ async def delete_api_key(
tags=["api-keys"],
dependencies=[Depends(auth_middleware)],
)
@feature_flag("api-keys-enabled")
async def suspend_key(
key_id: str, user_id: Annotated[str, Depends(get_user_id)]
) -> Optional[APIKeyWithoutHash]:
@@ -1124,6 +1126,7 @@ async def suspend_key(
tags=["api-keys"],
dependencies=[Depends(auth_middleware)],
)
@feature_flag("api-keys-enabled")
async def update_permissions(
key_id: str,
request: UpdatePermissionsRequest,

View File

@@ -14,7 +14,7 @@ import backend.server.v2.admin.credit_admin_routes as credit_admin_routes
import backend.server.v2.admin.model as admin_model
from backend.data.model import UserTransaction
from backend.server.conftest import ADMIN_USER_ID, TARGET_USER_ID
from backend.util.models import Pagination
from backend.server.model import Pagination
app = fastapi.FastAPI()
app.include_router(credit_admin_routes.router)

View File

@@ -1,7 +1,7 @@
from pydantic import BaseModel
from backend.data.model import UserTransaction
from backend.util.models import Pagination
from backend.server.model import Pagination
class UserHistoryResponse(BaseModel):

View File

@@ -9,6 +9,7 @@ import prisma.models
import prisma.types
import backend.data.graph as graph_db
import backend.server.model
import backend.server.v2.library.model as library_model
import backend.server.v2.store.exceptions as store_exceptions
import backend.server.v2.store.image_gen as store_image_gen
@@ -22,7 +23,6 @@ from backend.integrations.creds_manager import IntegrationCredentialsManager
from backend.integrations.webhooks.graph_lifecycle_hooks import on_graph_activate
from backend.util.exceptions import NotFoundError
from backend.util.json import SafeJson
from backend.util.models import Pagination
from backend.util.settings import Config
logger = logging.getLogger(__name__)
@@ -131,7 +131,7 @@ async def list_library_agents(
# Return the response with only valid agents
return library_model.LibraryAgentResponse(
agents=valid_library_agents,
pagination=Pagination(
pagination=backend.server.model.Pagination(
total_items=agent_count,
total_pages=(agent_count + page_size - 1) // page_size,
current_page=page,
@@ -629,7 +629,7 @@ async def list_presets(
return library_model.LibraryAgentPresetResponse(
presets=presets,
pagination=Pagination(
pagination=backend.server.model.Pagination(
total_items=total_items,
total_pages=total_pages,
current_page=page,

View File

@@ -8,9 +8,9 @@ import pydantic
import backend.data.block as block_model
import backend.data.graph as graph_model
import backend.server.model as server_model
from backend.data.model import CredentialsMetaInput, is_credentials_field_name
from backend.integrations.providers import ProviderName
from backend.util.models import Pagination
class LibraryAgentStatus(str, Enum):
@@ -213,7 +213,7 @@ class LibraryAgentResponse(pydantic.BaseModel):
"""Response schema for a list of library agents and pagination info."""
agents: list[LibraryAgent]
pagination: Pagination
pagination: server_model.Pagination
class LibraryAgentPresetCreatable(pydantic.BaseModel):
@@ -317,7 +317,7 @@ class LibraryAgentPresetResponse(pydantic.BaseModel):
"""Response schema for a list of agent presets and pagination info."""
presets: list[LibraryAgentPreset]
pagination: Pagination
pagination: server_model.Pagination
class LibraryAgentFilter(str, Enum):

View File

@@ -7,9 +7,9 @@ import pytest
import pytest_mock
from pytest_snapshot.plugin import Snapshot
import backend.server.model as server_model
import backend.server.v2.library.model as library_model
from backend.server.v2.library.routes import router as library_router
from backend.util.models import Pagination
app = fastapi.FastAPI()
app.include_router(library_router)
@@ -77,7 +77,7 @@ async def test_get_library_agents_success(
updated_at=datetime.datetime(2023, 1, 1, 0, 0, 0),
),
],
pagination=Pagination(
pagination=server_model.Pagination(
total_items=2, total_pages=1, current_page=1, page_size=50
),
)

View File

@@ -466,8 +466,6 @@ async def get_store_submissions(
# internal_comments omitted for regular users
reviewed_at=sub.reviewed_at,
changes_summary=sub.changes_summary,
video_url=sub.video_url,
categories=sub.categories,
)
submission_models.append(submission_model)
@@ -548,7 +546,7 @@ async def create_store_submission(
description: str = "",
sub_heading: str = "",
categories: list[str] = [],
changes_summary: str | None = "Initial Submission",
changes_summary: str = "Initial Submission",
) -> backend.server.v2.store.model.StoreSubmission:
"""
Create the first (and only) store listing and thus submission as a normal user
@@ -687,160 +685,6 @@ async def create_store_submission(
) from e
async def edit_store_submission(
user_id: str,
store_listing_version_id: str,
name: str,
video_url: str | None = None,
image_urls: list[str] = [],
description: str = "",
sub_heading: str = "",
categories: list[str] = [],
changes_summary: str | None = "Update submission",
) -> backend.server.v2.store.model.StoreSubmission:
"""
Edit an existing store listing submission.
Args:
user_id: ID of the authenticated user editing the submission
store_listing_version_id: ID of the store listing version to edit
agent_id: ID of the agent being submitted
agent_version: Version of the agent being submitted
slug: URL slug for the listing (only changeable for PENDING submissions)
name: Name of the agent
video_url: Optional URL to video demo
image_urls: List of image URLs for the listing
description: Description of the agent
sub_heading: Optional sub-heading for the agent
categories: List of categories for the agent
changes_summary: Summary of changes made in this submission
Returns:
StoreSubmission: The updated store submission
Raises:
SubmissionNotFoundError: If the submission is not found
UnauthorizedError: If the user doesn't own the submission
InvalidOperationError: If trying to edit a submission that can't be edited
"""
try:
# Get the current version and verify ownership
current_version = await prisma.models.StoreListingVersion.prisma().find_first(
where=prisma.types.StoreListingVersionWhereInput(
id=store_listing_version_id
),
include={
"StoreListing": {
"include": {
"Versions": {"order_by": {"version": "desc"}, "take": 1}
}
}
},
)
if not current_version:
raise backend.server.v2.store.exceptions.SubmissionNotFoundError(
f"Store listing version not found: {store_listing_version_id}"
)
# Verify the user owns this submission
if (
not current_version.StoreListing
or current_version.StoreListing.owningUserId != user_id
):
raise backend.server.v2.store.exceptions.UnauthorizedError(
f"User {user_id} does not own submission {store_listing_version_id}"
)
# Currently we are not allowing user to update the agent associated with a submission
# If we allow it in future, then we need a check here to verify the agent belongs to this user.
# Check if we can edit this submission
if current_version.submissionStatus == prisma.enums.SubmissionStatus.REJECTED:
raise backend.server.v2.store.exceptions.InvalidOperationError(
"Cannot edit a rejected submission"
)
# For APPROVED submissions, we need to create a new version
if current_version.submissionStatus == prisma.enums.SubmissionStatus.APPROVED:
# Create a new version for the existing listing
return await create_store_version(
user_id=user_id,
agent_id=current_version.agentGraphId,
agent_version=current_version.agentGraphVersion,
store_listing_id=current_version.storeListingId,
name=name,
video_url=video_url,
image_urls=image_urls,
description=description,
sub_heading=sub_heading,
categories=categories,
changes_summary=changes_summary,
)
# For PENDING submissions, we can update the existing version
elif current_version.submissionStatus == prisma.enums.SubmissionStatus.PENDING:
# Update the existing version
updated_version = await prisma.models.StoreListingVersion.prisma().update(
where={"id": store_listing_version_id},
data=prisma.types.StoreListingVersionUpdateInput(
name=name,
videoUrl=video_url,
imageUrls=image_urls,
description=description,
categories=categories,
subHeading=sub_heading,
changesSummary=changes_summary,
),
)
logger.debug(
f"Updated existing version {store_listing_version_id} for agent {current_version.agentGraphId}"
)
if not updated_version:
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to update store listing version"
)
return backend.server.v2.store.model.StoreSubmission(
agent_id=current_version.agentGraphId,
agent_version=current_version.agentGraphVersion,
name=name,
sub_heading=sub_heading,
slug=current_version.StoreListing.slug,
description=description,
image_urls=image_urls,
date_submitted=updated_version.submittedAt or updated_version.createdAt,
status=updated_version.submissionStatus,
runs=0,
rating=0.0,
store_listing_version_id=updated_version.id,
changes_summary=changes_summary,
video_url=video_url,
categories=categories,
version=updated_version.version,
)
else:
raise backend.server.v2.store.exceptions.InvalidOperationError(
f"Cannot edit submission with status: {current_version.submissionStatus}"
)
except (
backend.server.v2.store.exceptions.SubmissionNotFoundError,
backend.server.v2.store.exceptions.UnauthorizedError,
backend.server.v2.store.exceptions.AgentNotFoundError,
backend.server.v2.store.exceptions.ListingExistsError,
backend.server.v2.store.exceptions.InvalidOperationError,
):
raise
except prisma.errors.PrismaError as e:
logger.error(f"Database error editing store submission: {e}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to edit store submission"
) from e
async def create_store_version(
user_id: str,
agent_id: str,
@@ -852,7 +696,7 @@ async def create_store_version(
description: str = "",
sub_heading: str = "",
categories: list[str] = [],
changes_summary: str | None = "Initial submission",
changes_summary: str = "Update Submission",
) -> backend.server.v2.store.model.StoreSubmission:
"""
Create a new version for an existing store listing

View File

@@ -94,15 +94,3 @@ class SubmissionNotFoundError(StoreError):
"""Raised when a submission is not found"""
pass
class InvalidOperationError(StoreError):
"""Raised when an operation is not valid for the current state"""
pass
class UnauthorizedError(StoreError):
"""Raised when a user is not authorized to perform an action"""
pass

View File

@@ -4,7 +4,7 @@ from typing import List
import prisma.enums
import pydantic
from backend.util.models import Pagination
from backend.server.model import Pagination
class MyAgent(pydantic.BaseModel):
@@ -115,9 +115,11 @@ class StoreSubmission(pydantic.BaseModel):
reviewed_at: datetime.datetime | None = None
changes_summary: str | None = None
# Additional fields for editing
video_url: str | None = None
categories: list[str] = []
reviewer_id: str | None = None
review_comments: str | None = None # External comments visible to creator
internal_comments: str | None = None # Private notes for admin use only
reviewed_at: datetime.datetime | None = None
changes_summary: str | None = None
class StoreSubmissionsResponse(pydantic.BaseModel):
@@ -159,16 +161,6 @@ class StoreSubmissionRequest(pydantic.BaseModel):
changes_summary: str | None = None
class StoreSubmissionEditRequest(pydantic.BaseModel):
name: str
sub_heading: str
video_url: str | None = None
image_urls: list[str] = []
description: str = ""
categories: list[str] = []
changes_summary: str | None = None
class ProfileDetails(pydantic.BaseModel):
name: str
username: str

View File

@@ -564,47 +564,6 @@ async def create_submission(
)
@router.put(
"/submissions/{store_listing_version_id}",
summary="Edit store submission",
tags=["store", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
response_model=backend.server.v2.store.model.StoreSubmission,
)
async def edit_submission(
store_listing_version_id: str,
submission_request: backend.server.v2.store.model.StoreSubmissionEditRequest,
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
):
"""
Edit an existing store listing submission.
Args:
store_listing_version_id (str): ID of the store listing version to edit
submission_request (StoreSubmissionRequest): The updated submission details
user_id (str): ID of the authenticated user editing the listing
Returns:
StoreSubmission: The updated store submission
Raises:
HTTPException: If there is an error editing the submission
"""
return await backend.server.v2.store.db.edit_store_submission(
user_id=user_id,
store_listing_version_id=store_listing_version_id,
name=submission_request.name,
video_url=submission_request.video_url,
image_urls=submission_request.image_urls,
description=submission_request.description,
sub_heading=submission_request.sub_heading,
categories=submission_request.categories,
changes_summary=submission_request.changes_summary,
)
@router.post(
"/submissions/media",
summary="Upload submission media",

View File

@@ -551,8 +551,6 @@ def test_get_submissions_success(
agent_version=1,
sub_heading="Test agent subheading",
slug="test-agent",
video_url="test.mp4",
categories=["test-category"],
)
],
pagination=backend.server.v2.store.model.Pagination(

View File

@@ -1,20 +0,0 @@
"""
Shared models and types used across the backend to avoid circular imports.
"""
import pydantic
class Pagination(pydantic.BaseModel):
total_items: int = pydantic.Field(
description="Total number of items.", examples=[42]
)
total_pages: int = pydantic.Field(
description="Total number of pages.", examples=[2]
)
current_page: int = pydantic.Field(
description="Current_page page number.", examples=[1]
)
page_size: int = pydantic.Field(
description="Number of items per page.", examples=[25]
)

View File

@@ -1,41 +0,0 @@
-- Drop the existing view
DROP VIEW IF EXISTS "StoreSubmission";
-- Recreate the view with the new fields
CREATE VIEW "StoreSubmission" AS
SELECT
sl.id AS listing_id,
sl."owningUserId" AS user_id,
slv."agentGraphId" AS agent_id,
slv.version AS agent_version,
sl.slug,
COALESCE(slv.name, '') AS name,
slv."subHeading" AS sub_heading,
slv.description,
slv."imageUrls" AS image_urls,
slv."submittedAt" AS date_submitted,
slv."submissionStatus" AS status,
COALESCE(ar.run_count, 0::bigint) AS runs,
COALESCE(avg(sr.score::numeric), 0.0)::double precision AS rating,
slv.id AS store_listing_version_id,
slv."reviewerId" AS reviewer_id,
slv."reviewComments" AS review_comments,
slv."internalComments" AS internal_comments,
slv."reviewedAt" AS reviewed_at,
slv."changesSummary" AS changes_summary,
-- Add the two new fields:
slv."videoUrl" AS video_url,
slv.categories
FROM "StoreListing" sl
JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id
LEFT JOIN "StoreListingReview" sr ON sr."storeListingVersionId" = slv.id
LEFT JOIN (
SELECT "AgentGraphExecution"."agentGraphId", count(*) AS run_count
FROM "AgentGraphExecution"
GROUP BY "AgentGraphExecution"."agentGraphId"
) ar ON ar."agentGraphId" = slv."agentGraphId"
WHERE sl."isDeleted" = false
GROUP BY sl.id, sl."owningUserId", slv.id, slv."agentGraphId", slv.version, sl.slug, slv.name,
slv."subHeading", slv.description, slv."imageUrls", slv."submittedAt",
slv."submissionStatus", slv."reviewerId", slv."reviewComments", slv."internalComments",
slv."reviewedAt", slv."changesSummary", slv."videoUrl", slv.categories, ar.run_count;

View File

@@ -659,8 +659,6 @@ view StoreSubmission {
internal_comments String?
reviewed_at DateTime?
changes_summary String?
video_url String?
categories String[]
// Index or unique are not applied to views
}

View File

@@ -20,11 +20,7 @@
"review_comments": null,
"internal_comments": null,
"reviewed_at": null,
"changes_summary": null,
"video_url": "test.mp4",
"categories": [
"test-category"
]
"changes_summary": null
}
],
"pagination": {

View File

@@ -39,20 +39,20 @@ faker = Faker()
# Constants for data generation limits (reduced for E2E tests)
NUM_USERS = 15
NUM_AGENT_BLOCKS = 30
MIN_GRAPHS_PER_USER = 15
MAX_GRAPHS_PER_USER = 15
MIN_NODES_PER_GRAPH = 3
MAX_NODES_PER_GRAPH = 6
MIN_PRESETS_PER_USER = 2
MAX_PRESETS_PER_USER = 3
MIN_AGENTS_PER_USER = 15
MAX_AGENTS_PER_USER = 15
MIN_EXECUTIONS_PER_GRAPH = 2
MAX_EXECUTIONS_PER_GRAPH = 8
MIN_REVIEWS_PER_VERSION = 2
MAX_REVIEWS_PER_VERSION = 5
NUM_USERS = 10
NUM_AGENT_BLOCKS = 20
MIN_GRAPHS_PER_USER = 10
MAX_GRAPHS_PER_USER = 10
MIN_NODES_PER_GRAPH = 2
MAX_NODES_PER_GRAPH = 4
MIN_PRESETS_PER_USER = 1
MAX_PRESETS_PER_USER = 2
MIN_AGENTS_PER_USER = 10
MAX_AGENTS_PER_USER = 10
MIN_EXECUTIONS_PER_GRAPH = 1
MAX_EXECUTIONS_PER_GRAPH = 5
MIN_REVIEWS_PER_VERSION = 1
MAX_REVIEWS_PER_VERSION = 3
def get_image():
@@ -76,23 +76,6 @@ def get_video_url():
return f"https://www.youtube.com/watch?v={video_id}"
def get_category():
"""Generate a random category from the predefined list."""
categories = [
"productivity",
"writing",
"development",
"data",
"marketing",
"research",
"creative",
"business",
"personal",
"other",
]
return random.choice(categories)
class TestDataCreator:
"""Creates test data using API functions for E2E tests."""
@@ -576,37 +559,24 @@ class TestDataCreator:
submissions.append(test_submission.model_dump())
print("✅ Created special test store submission for test123@gmail.com")
# Randomly approve, reject, or leave pending the test submission
# Auto-approve the test submission
if test_submission.store_listing_version_id:
random_value = random.random()
if random_value < 0.4: # 40% chance to approve
approved_submission = await review_store_submission(
store_listing_version_id=test_submission.store_listing_version_id,
is_approved=True,
external_comments="Test submission approved",
internal_comments="Auto-approved test submission",
reviewer_id=test_user["id"],
)
approved_submissions.append(approved_submission.model_dump())
print("✅ Approved test store submission")
approved_submission = await review_store_submission(
store_listing_version_id=test_submission.store_listing_version_id,
is_approved=True,
external_comments="Test submission approved",
internal_comments="Auto-approved test submission",
reviewer_id=test_user["id"],
)
approved_submissions.append(approved_submission.model_dump())
print("✅ Approved test store submission")
# Mark approved submission as featured
await prisma.storelistingversion.update(
where={"id": test_submission.store_listing_version_id},
data={"isFeatured": True},
)
print("🌟 Marked test agent as FEATURED")
elif random_value < 0.7: # 30% chance to reject (40% to 70%)
await review_store_submission(
store_listing_version_id=test_submission.store_listing_version_id,
is_approved=False,
external_comments="Test submission rejected - needs improvements",
internal_comments="Auto-rejected test submission for E2E testing",
reviewer_id=test_user["id"],
)
print("❌ Rejected test store submission")
else: # 30% chance to leave pending (70% to 100%)
print("⏳ Left test submission pending for review")
# Mark test submission as featured
await prisma.storelistingversion.update(
where={"id": test_submission.store_listing_version_id},
data={"isFeatured": True},
)
print("🌟 Marked test agent as FEATURED")
except Exception as e:
print(f"Error creating test store submission: {e}")
@@ -647,87 +617,58 @@ class TestDataCreator:
video_url=get_video_url() if random.random() < 0.3 else None,
image_urls=[get_image() for _ in range(3)],
description=faker.text(),
categories=[
get_category()
], # Single category from predefined list
categories=[faker.word() for _ in range(3)],
changes_summary="Initial E2E test submission",
)
submissions.append(submission.model_dump())
print(f"✅ Created store submission: {submission.name}")
# Randomly approve, reject, or leave pending the submission
# Approve the submission so it appears in the store
if submission.store_listing_version_id:
random_value = random.random()
if random_value < 0.4: # 40% chance to approve
try:
# Pick a random user as the reviewer (admin)
reviewer_id = random.choice(self.users)["id"]
try:
# Pick a random user as the reviewer (admin)
reviewer_id = random.choice(self.users)["id"]
approved_submission = await review_store_submission(
store_listing_version_id=submission.store_listing_version_id,
is_approved=True,
external_comments="Auto-approved for E2E testing",
internal_comments="Automatically approved by E2E test data script",
reviewer_id=reviewer_id,
)
approved_submissions.append(
approved_submission.model_dump()
)
print(
f"✅ Approved store submission: {submission.name}"
)
approved_submission = await review_store_submission(
store_listing_version_id=submission.store_listing_version_id,
is_approved=True,
external_comments="Auto-approved for E2E testing",
internal_comments="Automatically approved by E2E test data script",
reviewer_id=reviewer_id,
)
approved_submissions.append(
approved_submission.model_dump()
)
print(f"✅ Approved store submission: {submission.name}")
# Mark some agents as featured during creation (30% chance)
# More likely for creators and first submissions
is_creator = user["id"] in [
p.get("userId") for p in self.profiles
]
feature_chance = (
0.5 if is_creator else 0.2
) # 50% for creators, 20% for others
# Mark some agents as featured during creation (30% chance)
# More likely for creators and first submissions
is_creator = user["id"] in [
p.get("userId") for p in self.profiles
]
feature_chance = (
0.5 if is_creator else 0.2
) # 50% for creators, 20% for others
if random.random() < feature_chance:
try:
await prisma.storelistingversion.update(
where={
"id": submission.store_listing_version_id
},
data={"isFeatured": True},
)
print(
f"🌟 Marked agent as FEATURED: {submission.name}"
)
except Exception as e:
print(
f"Warning: Could not mark submission as featured: {e}"
)
if random.random() < feature_chance:
try:
await prisma.storelistingversion.update(
where={
"id": submission.store_listing_version_id
},
data={"isFeatured": True},
)
print(
f"🌟 Marked agent as FEATURED: {submission.name}"
)
except Exception as e:
print(
f"Warning: Could not mark submission as featured: {e}"
)
except Exception as e:
print(
f"Warning: Could not approve submission {submission.name}: {e}"
)
elif random_value < 0.7: # 30% chance to reject (40% to 70%)
try:
# Pick a random user as the reviewer (admin)
reviewer_id = random.choice(self.users)["id"]
await review_store_submission(
store_listing_version_id=submission.store_listing_version_id,
is_approved=False,
external_comments="Submission rejected - needs improvements",
internal_comments="Automatically rejected by E2E test data script",
reviewer_id=reviewer_id,
)
print(
f"❌ Rejected store submission: {submission.name}"
)
except Exception as e:
print(
f"Warning: Could not reject submission {submission.name}: {e}"
)
else: # 30% chance to leave pending (70% to 100%)
except Exception as e:
print(
f"⏳ Left submission pending for review: {submission.name}"
f"Warning: Could not approve submission {submission.name}: {e}"
)
except Exception as e:

View File

@@ -44,9 +44,9 @@ export function APIKeysSection() {
</TableHeader>
<TableBody>
{apiKeys.map((key) => (
<TableRow key={key.id} data-testid="api-key-row">
<TableRow key={key.id}>
<TableCell>{key.name}</TableCell>
<TableCell data-testid="api-key-id">
<TableCell>
<div className="rounded-md border p-1 px-2 text-xs">
{`${key.prefix}******************${key.postfix}`}
</div>
@@ -76,11 +76,7 @@ export function APIKeysSection() {
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
data-testid="api-key-actions"
variant="ghost"
size="sm"
>
<Button variant="ghost" size="sm">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>

View File

@@ -7,7 +7,6 @@ import {
AgentTableRow,
AgentTableRowProps,
} from "../AgentTableRow/AgentTableRow";
import { StoreSubmissionEditRequest } from "@/app/api/__generated__/models/storeSubmissionEditRequest";
export interface AgentTableProps {
agents: Omit<
@@ -16,23 +15,15 @@ export interface AgentTableProps {
| "selectedAgents"
| "onViewSubmission"
| "onDeleteSubmission"
| "onEditSubmission"
>[];
onViewSubmission: (submission: StoreSubmission) => void;
onDeleteSubmission: (submission_id: string) => void;
onEditSubmission: (
submission: StoreSubmissionEditRequest & {
store_listing_version_id: string | undefined;
agent_id: string;
},
) => void;
}
export const AgentTable: React.FC<AgentTableProps> = ({
agents,
onViewSubmission,
onDeleteSubmission,
onEditSubmission,
}) => {
return (
<div className="w-full" data-testid="agent-table">
@@ -71,7 +62,6 @@ export const AgentTable: React.FC<AgentTableProps> = ({
{...agent}
onViewSubmission={onViewSubmission}
onDeleteSubmission={onDeleteSubmission}
onEditSubmission={onEditSubmission}
/>
<div className="block md:hidden">
<AgentTableCard

View File

@@ -3,8 +3,8 @@
import Image from "next/image";
import { IconStarFilled, IconMore } from "@/components/ui/icons";
import { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
import { Status, StatusType } from "@/components/agptui/Status";
import { SubmissionStatus } from "@/app/api/__generated__/models/submissionStatus";
import { Status } from "@/components/agptui/Status";
export interface AgentTableCardProps {
agent_id: string;
@@ -14,7 +14,7 @@ export interface AgentTableCardProps {
description: string;
imageSrc: string[];
dateSubmitted: string;
status: SubmissionStatus;
status: StatusType;
runs: number;
rating: number;
id: number;
@@ -44,7 +44,8 @@ export const AgentTableCard = ({
description,
image_urls: imageSrc,
date_submitted: dateSubmitted,
status: status,
// SafeCast: status is a string from the API...
status: status.toUpperCase() as SubmissionStatus,
runs,
rating,
});

View File

@@ -4,7 +4,7 @@ import Image from "next/image";
import { Text } from "@/components/atoms/Text/Text";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { Status } from "@/components/agptui/Status";
import { Status, StatusType } from "@/components/agptui/Status";
import { useAgentTableRow } from "./useAgentTableRow";
import { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
import {
@@ -13,10 +13,7 @@ import {
ImageBroken,
Star,
Trash,
PencilSimple,
} from "@phosphor-icons/react/dist/ssr";
import { SubmissionStatus } from "@/app/api/__generated__/models/submissionStatus";
import { StoreSubmissionEditRequest } from "@/app/api/__generated__/models/storeSubmissionEditRequest";
export interface AgentTableRowProps {
agent_id: string;
@@ -26,22 +23,13 @@ export interface AgentTableRowProps {
description: string;
imageSrc: string[];
date_submitted: string;
status: SubmissionStatus;
status: StatusType;
runs: number;
rating: number;
dateSubmitted: string;
id: number;
video_url?: string;
categories?: string[];
store_listing_version_id?: string;
onViewSubmission: (submission: StoreSubmission) => void;
onDeleteSubmission: (submission_id: string) => void;
onEditSubmission: (
submission: StoreSubmissionEditRequest & {
store_listing_version_id: string | undefined;
agent_id: string;
},
) => void;
}
export const AgentTableRow = ({
@@ -56,18 +44,13 @@ export const AgentTableRow = ({
runs,
rating,
id,
video_url,
categories,
store_listing_version_id,
onViewSubmission,
onDeleteSubmission,
onEditSubmission,
}: AgentTableRowProps) => {
const { handleView, handleDelete, handleEdit } = useAgentTableRow({
const { handleView, handleDelete } = useAgentTableRow({
id,
onViewSubmission,
onDeleteSubmission,
onEditSubmission,
agent_id,
agent_version,
agentName,
@@ -75,23 +58,16 @@ export const AgentTableRow = ({
description,
imageSrc,
dateSubmitted,
status,
status: status.toUpperCase(),
runs,
rating,
video_url,
categories,
store_listing_version_id,
});
// Determine if we should show Edit or View button
const canEdit =
status === SubmissionStatus.APPROVED || status === SubmissionStatus.PENDING;
return (
<div
data-testid="agent-table-row"
data-agent-id={agent_id}
data-submission-id={store_listing_version_id}
data-agent-name={agentName}
className="hidden items-center border-b border-neutral-300 px-4 py-4 hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-800 md:flex"
>
<div className="grid w-full grid-cols-[minmax(400px,1fr),180px,140px,100px,100px,40px] items-center gap-4">
@@ -164,23 +140,13 @@ export const AgentTableRow = ({
<DotsThreeVerticalIcon className="h-5 w-5 text-neutral-800" />
</DropdownMenu.Trigger>
<DropdownMenu.Content className="z-10 rounded-xl border bg-white p-1 shadow-md dark:bg-gray-800">
{canEdit ? (
<DropdownMenu.Item
onSelect={handleEdit}
className="flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<PencilSimple className="mr-2 h-4 w-4 dark:text-gray-100" />
<span className="dark:text-gray-100">Edit</span>
</DropdownMenu.Item>
) : (
<DropdownMenu.Item
onSelect={handleView}
className="flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<Eye className="mr-2 h-4 w-4 dark:text-gray-100" />
<span className="dark:text-gray-100">View</span>
</DropdownMenu.Item>
)}
<DropdownMenu.Item
onSelect={handleView}
className="flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<Eye className="mr-2 h-4 w-4 dark:text-gray-100" />
<span className="dark:text-gray-100">View</span>
</DropdownMenu.Item>
<DropdownMenu.Separator className="my-1 h-px bg-gray-300 dark:bg-gray-600" />
<DropdownMenu.Item
onSelect={handleDelete}

View File

@@ -1,17 +1,10 @@
import { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
import { StoreSubmissionEditRequest } from "@/app/api/__generated__/models/storeSubmissionEditRequest";
import { SubmissionStatus } from "@/app/api/__generated__/models/submissionStatus";
interface useAgentTableRowProps {
id: number;
onViewSubmission: (submission: StoreSubmission) => void;
onDeleteSubmission: (submission_id: string) => void;
onEditSubmission: (
submission: StoreSubmissionEditRequest & {
store_listing_version_id: string | undefined;
agent_id: string;
},
) => void;
agent_id: string;
agent_version: number;
agentName: string;
@@ -19,18 +12,14 @@ interface useAgentTableRowProps {
description: string;
imageSrc: string[];
dateSubmitted: string;
status: SubmissionStatus;
status: string;
runs: number;
rating: number;
video_url?: string;
categories?: string[];
store_listing_version_id?: string;
}
export const useAgentTableRow = ({
onViewSubmission,
onDeleteSubmission,
onEditSubmission,
agent_id,
agent_version,
agentName,
@@ -41,9 +30,6 @@ export const useAgentTableRow = ({
status,
runs,
rating,
video_url,
categories,
store_listing_version_id,
}: useAgentTableRowProps) => {
const handleView = () => {
onViewSubmission({
@@ -55,32 +41,16 @@ export const useAgentTableRow = ({
description,
image_urls: imageSrc,
date_submitted: dateSubmitted,
status: status,
// SafeCast: status is a string from the API...
status: status.toUpperCase() as SubmissionStatus,
runs,
rating,
video_url,
categories,
store_listing_version_id,
} satisfies StoreSubmission);
};
const handleEdit = () => {
onEditSubmission({
name: agentName,
sub_heading,
description,
image_urls: imageSrc,
video_url,
categories,
changes_summary: "Update Submission",
store_listing_version_id,
agent_id,
});
};
const handleDelete = () => {
onDeleteSubmission(agent_id);
};
return { handleView, handleDelete, handleEdit };
return { handleView, handleDelete };
};

View File

@@ -1,8 +1,8 @@
import { useMainDashboardPage } from "./useMainDashboardPage";
import { Separator } from "@/components/ui/separator";
import { AgentTable } from "../AgentTable/AgentTable";
import { StatusType } from "@/components/agptui/Status";
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
import { EditAgentModal } from "@/components/contextual/EditAgentModal/EditAgentModal";
import { Button } from "@/components/atoms/Button/Button";
import { EmptySubmissions } from "./components/EmptySubmissions";
import { SubmissionLoadError } from "./components/SumbmissionLoadError";
@@ -13,13 +13,9 @@ export const MainDashboardPage = () => {
const {
onDeleteSubmission,
onViewSubmission,
onEditSubmission,
onEditSuccess,
onEditClose,
onOpenSubmitModal,
onPublishStateChange,
publishState,
editState,
// API data
submissions,
isLoading,
@@ -93,30 +89,17 @@ export const MainDashboardPage = () => {
dateSubmitted: new Date(
submission.date_submitted,
).toLocaleDateString(),
status: submission.status,
status: submission.status.toLowerCase() as StatusType,
runs: submission.runs,
rating: submission.rating,
video_url: submission.video_url || undefined,
categories: submission.categories,
slug: submission.slug,
store_listing_version_id:
submission.store_listing_version_id || undefined,
}))}
onViewSubmission={onViewSubmission}
onDeleteSubmission={onDeleteSubmission}
onEditSubmission={onEditSubmission}
/>
) : (
<EmptySubmissions />
)}
</div>
<EditAgentModal
isOpen={editState.isOpen}
onClose={onEditClose}
submission={editState.submission}
onSuccess={onEditSuccess}
/>
</main>
);
};

View File

@@ -4,12 +4,10 @@ import {
useGetV2ListMySubmissions,
} from "@/app/api/__generated__/endpoints/store/store";
import { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
import { StoreSubmissionEditRequest } from "@/app/api/__generated__/models/storeSubmissionEditRequest";
import { StoreSubmissionsResponse } from "@/app/api/__generated__/models/storeSubmissionsResponse";
import { getQueryClient } from "@/lib/react-query/queryClient";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { useState } from "react";
import * as Sentry from "@sentry/nextjs";
type PublishStep = "select" | "info" | "review";
@@ -19,16 +17,6 @@ type PublishState = {
submissionData: StoreSubmission | null;
};
type EditState = {
isOpen: boolean;
submission:
| (StoreSubmissionEditRequest & {
store_listing_version_id: string | undefined;
agent_id: string;
})
| null;
};
export const useMainDashboardPage = () => {
const queryClient = getQueryClient();
@@ -40,11 +28,6 @@ export const useMainDashboardPage = () => {
submissionData: null,
});
const [editState, setEditState] = useState<EditState>({
isOpen: false,
submission: null,
});
const { mutateAsync: deleteSubmission } = useDeleteV2DeleteStoreSubmission({
mutation: {
onSuccess: () => {
@@ -76,43 +59,6 @@ export const useMainDashboardPage = () => {
});
};
const onEditSubmission = (
submission: StoreSubmissionEditRequest & {
store_listing_version_id: string | undefined;
agent_id: string;
},
) => {
setEditState({
isOpen: true,
submission,
});
};
const onEditSuccess = async (submission: StoreSubmission) => {
try {
if (!submission.store_listing_version_id) {
Sentry.captureException(
new Error("No store listing version ID found for submission"),
);
return;
}
setEditState({
isOpen: false,
submission: null,
});
} catch (error) {
Sentry.captureException(error);
}
};
const onEditClose = () => {
setEditState({
isOpen: false,
submission: null,
});
};
const onDeleteSubmission = async (submission_id: string) => {
await deleteSubmission({
submissionId: submission_id,
@@ -137,11 +83,7 @@ export const useMainDashboardPage = () => {
onPublishStateChange,
onDeleteSubmission,
onViewSubmission,
onEditSubmission,
onEditSuccess,
onEditClose,
publishState,
editState,
// API data
submissions,
isLoading: !isSuccess,

View File

@@ -1,19 +0,0 @@
/**
* Generated by orval v7.11.2 🍺
* Do not edit manually.
* AutoGPT Agent Server
* This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1
*/
import type { StoreSubmissionEditRequestVideoUrl } from './storeSubmissionEditRequestVideoUrl';
import type { StoreSubmissionEditRequestChangesSummary } from './storeSubmissionEditRequestChangesSummary';
export interface StoreSubmissionEditRequest {
name: string;
sub_heading: string;
video_url?: StoreSubmissionEditRequestVideoUrl;
image_urls?: string[];
description?: string;
categories?: string[];
changes_summary?: StoreSubmissionEditRequestChangesSummary;
}

View File

@@ -2624,50 +2624,6 @@
}
}
},
"/api/store/submissions/{store_listing_version_id}": {
"put": {
"tags": ["v2", "store", "private"],
"summary": "Edit store submission",
"description": "Edit an existing store listing submission.\n\nArgs:\n store_listing_version_id (str): ID of the store listing version to edit\n submission_request (StoreSubmissionRequest): The updated submission details\n user_id (str): ID of the authenticated user editing the listing\n\nReturns:\n StoreSubmission: The updated store submission\n\nRaises:\n HTTPException: If there is an error editing the submission",
"operationId": "putV2Edit store submission",
"parameters": [
{
"name": "store_listing_version_id",
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Store Listing Version Id" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StoreSubmissionEditRequest"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/StoreSubmission" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/api/store/submissions/media": {
"post": {
"tags": ["v2", "store", "private"],
@@ -6343,16 +6299,6 @@
"changes_summary": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Changes Summary"
},
"video_url": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Video Url"
},
"categories": {
"items": { "type": "string" },
"type": "array",
"title": "Categories",
"default": []
}
},
"type": "object",
@@ -6371,40 +6317,6 @@
],
"title": "StoreSubmission"
},
"StoreSubmissionEditRequest": {
"properties": {
"name": { "type": "string", "title": "Name" },
"sub_heading": { "type": "string", "title": "Sub Heading" },
"video_url": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Video Url"
},
"image_urls": {
"items": { "type": "string" },
"type": "array",
"title": "Image Urls",
"default": []
},
"description": {
"type": "string",
"title": "Description",
"default": ""
},
"categories": {
"items": { "type": "string" },
"type": "array",
"title": "Categories",
"default": []
},
"changes_summary": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Changes Summary"
}
},
"type": "object",
"required": ["name", "sub_heading"],
"title": "StoreSubmissionEditRequest"
},
"StoreSubmissionRequest": {
"properties": {
"agent_id": { "type": "string", "title": "Agent Id" },

View File

@@ -186,16 +186,10 @@ const FlowEditor: React.FC<{
storage.clean(Key.SHEPHERD_TOUR);
router.push(pathname);
} else if (!storage.get(Key.SHEPHERD_TOUR)) {
// Add a small delay to ensure the component state is fully initialized
// This is especially important when resetTutorial=true caused a redirect
const timer = setTimeout(() => {
const emptyNodes = (forceRemove: boolean = false) =>
forceRemove ? (setNodes([]), setEdges([]), true) : nodes.length === 0;
startTutorial(emptyNodes, setPinBlocksPopover, setPinSavePopover);
storage.set(Key.SHEPHERD_TOUR, "yes");
}, 100);
return () => clearTimeout(timer);
const emptyNodes = (forceRemove: boolean = false) =>
forceRemove ? (setNodes([]), setEdges([]), true) : nodes.length === 0;
startTutorial(emptyNodes, setPinBlocksPopover, setPinSavePopover);
storage.set(Key.SHEPHERD_TOUR, "yes");
}
}, [router, pathname, params, setEdges, setNodes, nodes.length]);

View File

@@ -1,12 +1,13 @@
import { SubmissionStatus } from "@/app/api/__generated__/models/submissionStatus";
import * as React from "react";
export type StatusType = "draft" | "awaiting_review" | "approved" | "rejected";
interface StatusProps {
status: SubmissionStatus;
status: StatusType;
}
const statusConfig: Record<
SubmissionStatus,
StatusType,
{
bgColor: string;
dotColor: string;
@@ -15,28 +16,28 @@ const statusConfig: Record<
darkDotColor: string;
}
> = {
[SubmissionStatus.DRAFT]: {
draft: {
bgColor: "bg-blue-50",
dotColor: "bg-blue-500",
text: "Draft",
darkBgColor: "dark:bg-blue-900",
darkDotColor: "dark:bg-blue-300",
},
[SubmissionStatus.PENDING]: {
awaiting_review: {
bgColor: "bg-amber-50",
dotColor: "bg-amber-500",
text: "Awaiting review",
darkBgColor: "dark:bg-amber-900",
darkDotColor: "dark:bg-amber-300",
},
[SubmissionStatus.APPROVED]: {
approved: {
bgColor: "bg-green-50",
dotColor: "bg-green-500",
text: "Approved",
darkBgColor: "dark:bg-green-900",
darkDotColor: "dark:bg-green-300",
},
[SubmissionStatus.REJECTED]: {
rejected: {
bgColor: "bg-red-50",
dotColor: "bg-red-500",
text: "Rejected",
@@ -52,9 +53,9 @@ export const Status: React.FC<StatusProps> = ({ status }) => {
* Valid values: 'draft', 'awaiting_review', 'approved', 'rejected'
*/
if (!status) {
return <Status status={SubmissionStatus.PENDING} />;
return <Status status="awaiting_review" />;
} else if (!statusConfig[status]) {
return <Status status={SubmissionStatus.PENDING} />;
return <Status status="awaiting_review" />;
}
const config = statusConfig[status];

View File

@@ -1,51 +0,0 @@
"use client";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
import { EditAgentForm } from "./components/EditAgentForm";
import { StoreSubmissionEditRequest } from "@/app/api/__generated__/models/storeSubmissionEditRequest";
export interface EditAgentModalProps {
isOpen: boolean;
onClose: () => void;
submission:
| (StoreSubmissionEditRequest & {
store_listing_version_id: string | undefined;
agent_id: string;
})
| null;
onSuccess: (submission: StoreSubmission) => void;
}
export function EditAgentModal({
isOpen,
onClose,
submission,
onSuccess,
}: EditAgentModalProps) {
if (!submission) return null;
return (
<Dialog
styling={{
maxWidth: "45rem",
}}
controlled={{
isOpen,
set: (isOpen) => {
if (!isOpen) onClose();
},
}}
>
<Dialog.Content>
<div data-testid="edit-agent-modal">
<EditAgentForm
submission={submission}
onClose={onClose}
onSuccess={onSuccess}
/>
</div>
</Dialog.Content>
</Dialog>
);
}

View File

@@ -1,171 +0,0 @@
"use client";
import * as React from "react";
import { Button } from "@/components/atoms/Button/Button";
import { Input } from "@/components/atoms/Input/Input";
import { Select } from "@/components/atoms/Select/Select";
import { Form, FormField } from "@/components/ui/form";
import { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
import { ThumbnailImages } from "../../PublishAgentModal/components/AgentInfoStep/components/ThumbnailImages";
import { StoreSubmissionEditRequest } from "@/app/api/__generated__/models/storeSubmissionEditRequest";
import { StepHeader } from "../../PublishAgentModal/components/StepHeader";
import { useEditAgentForm } from "./useEditAgentForm";
interface EditAgentFormProps {
submission: StoreSubmissionEditRequest & {
store_listing_version_id: string | undefined;
agent_id: string;
};
onClose: () => void;
onSuccess: (submission: StoreSubmission) => void;
}
export function EditAgentForm({
submission,
onClose,
onSuccess,
}: EditAgentFormProps) {
const {
form,
categoryOptions,
isSubmitting,
handleFormSubmit,
handleImagesChange,
} = useEditAgentForm({ submission, onSuccess });
return (
<div className="mx-auto flex w-full flex-col rounded-3xl">
<StepHeader title="Edit Agent" description="Update your agent details" />
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleFormSubmit)}
className="flex-grow overflow-y-auto p-6"
>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<Input
id={field.name}
label="Title"
type="text"
placeholder="Agent name"
error={form.formState.errors.title?.message}
{...field}
/>
)}
/>
<FormField
control={form.control}
name="subheader"
render={({ field }) => (
<Input
id={field.name}
label="Subheader"
type="text"
placeholder="A tagline for your agent"
error={form.formState.errors.subheader?.message}
{...field}
/>
)}
/>
<ThumbnailImages
agentId={submission.agent_id}
onImagesChange={handleImagesChange}
initialImages={submission.image_urls || []}
initialSelectedImage={submission.image_urls?.[0] || null}
errorMessage={form.formState.errors.root?.message}
/>
<FormField
control={form.control}
name="youtubeLink"
render={({ field }) => (
<Input
id={field.name}
label="YouTube video link"
type="url"
placeholder="Paste a video link here"
error={form.formState.errors.youtubeLink?.message}
{...field}
/>
)}
/>
<FormField
control={form.control}
name="category"
render={({ field }) => {
console.log("Edit Category field value:", field.value);
return (
<Select
id={field.name}
label="Category"
placeholder="Select a category for your agent"
value={field.value}
onValueChange={field.onChange}
error={form.formState.errors.category?.message}
options={categoryOptions}
/>
);
}}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<Input
id={field.name}
label="Description"
type="textarea"
placeholder="Describe your agent and what it does"
error={form.formState.errors.description?.message}
{...field}
/>
)}
/>
<FormField
control={form.control}
name="changes_summary"
render={({ field }) => (
<Input
id={field.name}
label="Changes Summary"
type="text"
placeholder="Briefly describe what you changed"
error={form.formState.errors.changes_summary?.message}
{...field}
/>
)}
/>
<div className="flex justify-between gap-4 pt-6">
<Button
type="button"
onClick={onClose}
variant="secondary"
className="w-full"
>
Cancel
</Button>
<Button
type="submit"
className="w-full"
disabled={
Object.keys(form.formState.errors).length > 0 || isSubmitting
}
loading={isSubmitting}
>
Update submission
</Button>
</div>
</form>
</Form>
</div>
);
}

View File

@@ -1,169 +0,0 @@
import * as Sentry from "@sentry/nextjs";
import {
getGetV2ListMySubmissionsQueryKey,
usePutV2EditStoreSubmission,
} from "@/app/api/__generated__/endpoints/store/store";
import { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
import { StoreSubmissionEditRequest } from "@/app/api/__generated__/models/storeSubmissionEditRequest";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQueryClient } from "@tanstack/react-query";
import React from "react";
import { useForm } from "react-hook-form";
import z from "zod";
interface useEditAgentFormProps {
submission: StoreSubmissionEditRequest & {
store_listing_version_id: string | undefined;
agent_id: string;
};
onSuccess: (submission: StoreSubmission) => void;
}
export const useEditAgentForm = ({
submission,
onSuccess,
}: useEditAgentFormProps) => {
const editAgentSchema = z.object({
title: z
.string()
.min(1, "Title is required")
.max(100, "Title must be less than 100 characters"),
subheader: z
.string()
.min(1, "Subheader is required")
.max(200, "Subheader must be less than 200 characters"),
youtubeLink: z
.string()
.optional()
.refine((val) => {
if (!val) return true;
try {
const url = new URL(val);
const allowedHosts = [
"youtube.com",
"www.youtube.com",
"youtu.be",
"www.youtu.be",
];
return allowedHosts.includes(url.hostname);
} catch {
return false;
}
}, "Please enter a valid YouTube URL"),
category: z.string().min(1, "Category is required"),
description: z
.string()
.min(1, "Description is required")
.max(1000, "Description must be less than 1000 characters"),
changes_summary: z
.string()
.min(1, "Changes summary is required")
.max(200, "Changes summary must be less than 200 characters"),
});
type EditAgentFormData = z.infer<typeof editAgentSchema>;
const [images, setImages] = React.useState<string[]>(
submission.image_urls || [],
);
const [isSubmitting, setIsSubmitting] = React.useState(false);
const { mutateAsync: editSubmission } = usePutV2EditStoreSubmission({
mutation: {
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: getGetV2ListMySubmissionsQueryKey(),
});
},
},
});
const queryClient = useQueryClient();
const { toast } = useToast();
const form = useForm<EditAgentFormData>({
resolver: zodResolver(editAgentSchema),
defaultValues: {
title: submission.name,
subheader: submission.sub_heading,
youtubeLink: submission.video_url || "",
category: submission.categories?.[0] || "",
description: submission.description,
changes_summary: submission.changes_summary || "",
},
});
const categoryOptions = [
{ value: "productivity", label: "Productivity" },
{ value: "writing", label: "Writing & Content" },
{ value: "development", label: "Development" },
{ value: "data", label: "Data & Analytics" },
{ value: "marketing", label: "Marketing & SEO" },
{ value: "research", label: "Research & Learning" },
{ value: "creative", label: "Creative & Design" },
{ value: "business", label: "Business & Finance" },
{ value: "personal", label: "Personal Assistant" },
{ value: "other", label: "Other" },
];
const handleImagesChange = React.useCallback((newImages: string[]) => {
setImages(newImages);
}, []);
async function handleFormSubmit(data: EditAgentFormData) {
// Validate that at least one image is present
if (images.length === 0) {
form.setError("root", {
type: "manual",
message: "At least one image is required",
});
return;
}
const categories = data.category ? [data.category] : [];
const filteredCategories = categories.filter(Boolean);
setIsSubmitting(true);
try {
const response = await editSubmission({
storeListingVersionId: submission.store_listing_version_id!,
data: {
name: data.title,
sub_heading: data.subheader,
description: data.description,
image_urls: images,
video_url: data.youtubeLink || "",
categories: filteredCategories,
changes_summary: data.changes_summary,
},
});
// Extract the StoreSubmission from the response
if (response.status === 200 && response.data) {
onSuccess(response.data);
} else {
throw new Error("Failed to update submission");
}
} catch (error) {
Sentry.captureException(error);
toast({
title: "Edit Agent Error",
description:
"An error occurred while editing the agent. Please try again.",
duration: 3000,
variant: "destructive",
});
} finally {
setIsSubmitting(false);
}
}
return {
form,
isSubmitting,
handleFormSubmit,
handleImagesChange,
categoryOptions,
};
};

View File

@@ -190,12 +190,7 @@ export const startTutorial = (
element: '[data-id="blocks-control-popover-content"]',
on: "right",
},
buttons: [
{
text: "Back",
action: tour.back,
},
],
buttons: [],
beforeShowPromise: () =>
waitForElement('[data-id="blocks-control-popover-content"]').then(() => {
disableOtherBlocks(
@@ -207,11 +202,7 @@ export const startTutorial = (
event: "click",
},
when: {
show: () => {
setPinBlocksPopover(true);
// Additional safeguard - ensure the popover stays pinned for this step
setTimeout(() => setPinBlocksPopover(true), 50);
},
show: () => setPinBlocksPopover(true),
hide: enableAllBlocks,
},
});

View File

@@ -135,105 +135,3 @@ test("edit action is unavailable for rejected agents (view only)", async ({
await expect(page.getByRole("menuitem", { name: "View" })).toBeVisible();
await expect(page.getByRole("menuitem", { name: "Edit" })).toHaveCount(0);
});
test("editing an approved agent creates a new pending submission", async ({
page,
}) => {
await page.goto("/profile/dashboard");
const agentTable = page.getByTestId("agent-table");
await expect(agentTable).toBeVisible();
const rows = agentTable.getByTestId("agent-table-row");
const approvedRow = rows.filter({ hasText: "Approved" }).first();
if (!(await approvedRow.count())) {
console.log("No approved agents available; skipping approved edit test.");
return;
}
const beforeCount = await rows.count();
await approvedRow.scrollIntoViewIfNeeded();
const actionsButton = approvedRow.getByTestId("agent-table-row-actions");
await actionsButton.waitFor({ state: "visible", timeout: 10000 });
await actionsButton.scrollIntoViewIfNeeded();
await actionsButton.click();
const editButton = page.getByRole("menuitem", { name: "Edit" });
await expect(editButton).toBeVisible();
await editButton.click();
const editModal = page.getByTestId("edit-agent-modal");
await expect(editModal).toBeVisible();
const newTitle = `E2E Edit Approved ${Date.now()}`;
await page.getByRole("textbox", { name: "Title" }).fill(newTitle);
await page
.getByRole("textbox", { name: "Changes Summary" })
.fill("E2E change - approved -> new pending submission");
await page.getByRole("button", { name: "Update submission" }).click();
await expect(editModal).not.toBeVisible();
// A new submission should appear with pending state
await expect(async () => {
const afterCount = await rows.count();
expect(afterCount).toBeGreaterThan(beforeCount);
}).toPass();
const newRow = rows.filter({ hasText: newTitle }).first();
await expect(newRow).toBeVisible();
await expect(newRow).toContainText(/Awaiting review/);
});
test("editing a pending agent updates the same submission in place", async ({
page,
}) => {
await page.goto("/profile/dashboard");
const agentTable = page.getByTestId("agent-table");
await expect(agentTable).toBeVisible();
const rows = agentTable.getByTestId("agent-table-row");
const pendingRow = rows.filter({ hasText: /Awaiting review/ }).first();
if (!(await pendingRow.count())) {
console.log("No pending agents available; skipping pending edit test.");
return;
}
const beforeCount = await rows.count();
await pendingRow.scrollIntoViewIfNeeded();
const actionsButton = pendingRow.getByTestId("agent-table-row-actions");
await actionsButton.waitFor({ state: "visible", timeout: 10000 });
await actionsButton.scrollIntoViewIfNeeded();
await actionsButton.click();
const editButton = page.getByRole("menuitem", { name: "Edit" });
await expect(editButton).toBeVisible();
await editButton.click();
const editModal = page.getByTestId("edit-agent-modal");
await expect(editModal).toBeVisible();
const newTitle = `E2E Edit Pending ${Date.now()}`;
await page.getByRole("textbox", { name: "Title" }).fill(newTitle);
await page
.getByRole("textbox", { name: "Changes Summary" })
.fill("E2E change - pending -> same submission");
await page.getByRole("button", { name: "Update submission" }).click();
await expect(editModal).not.toBeVisible();
// Count should remain the same
await expect(async () => {
const afterCount = await rows.count();
expect(afterCount).toBe(beforeCount);
}).toPass();
const updatedRow = rows.filter({ hasText: newTitle }).first();
await expect(updatedRow).toBeVisible();
await expect(updatedRow).toContainText(/Awaiting review/);
});

View File

@@ -1,64 +0,0 @@
import { expect, test } from "@playwright/test";
import { LoginPage } from "./pages/login.page";
import { TEST_CREDENTIALS } from "./credentials";
import { hasUrl } from "./utils/assertion";
import { getSelectors } from "./utils/selectors";
test.describe("API Keys Page", () => {
test.beforeEach(async ({ page }) => {
const loginPage = new LoginPage(page);
await page.goto("/login");
await loginPage.login(TEST_CREDENTIALS.email, TEST_CREDENTIALS.password);
await hasUrl(page, "/marketplace");
});
test("should redirect to login page when user is not authenticated", async ({
browser,
}) => {
const context = await browser.newContext();
const page = await context.newPage();
try {
await page.goto("/profile/api_keys");
await hasUrl(page, "/login");
} finally {
await page.close();
await context.close();
}
});
test("should create a new API key successfully", async ({ page }) => {
const { getButton, getField } = getSelectors(page);
await page.goto("/profile/api_keys");
await getButton("Create Key").click();
await getField("Name").fill("Test Key");
await getButton("Create").click();
await expect(
page.getByText("AutoGPT Platform API Key Created"),
).toBeVisible();
await getButton("Close").first().click();
await expect(page.getByText("Test Key").first()).toBeVisible();
});
test("should revoke an existing API key", async ({ page }) => {
const { getRole, getId } = getSelectors(page);
await page.goto("/profile/api_keys");
const apiKeyRow = getId("api-key-row").first();
const apiKeyContent = await apiKeyRow
.getByTestId("api-key-id")
.textContent();
const apiKeyActions = apiKeyRow.getByTestId("api-key-actions").first();
await apiKeyActions.click();
await getRole("menuitem", "Revoke").click();
await expect(
page.getByText("AutoGPT Platform API key revoked successfully"),
).toBeVisible();
await expect(page.getByText(apiKeyContent!)).not.toBeVisible();
});
});