mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-11 16:18:07 -05:00
Compare commits
6 Commits
update-ins
...
fixes-to-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b71c03d96 | ||
|
|
b20b00a441 | ||
|
|
84810ce0af | ||
|
|
2610c4579f | ||
|
|
0c09b0c459 | ||
|
|
1105e6c0d2 |
@@ -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
|
||||
|
||||
|
||||
@@ -60,21 +60,6 @@ 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
|
||||
|
||||
|
||||
@@ -1071,7 +1071,6 @@ 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]:
|
||||
@@ -1100,7 +1099,6 @@ 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]:
|
||||
@@ -1126,7 +1124,6 @@ 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,
|
||||
|
||||
@@ -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.server.model import Pagination
|
||||
from backend.util.models import Pagination
|
||||
|
||||
app = fastapi.FastAPI()
|
||||
app.include_router(credit_admin_routes.router)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from backend.data.model import UserTransaction
|
||||
from backend.server.model import Pagination
|
||||
from backend.util.models import Pagination
|
||||
|
||||
|
||||
class UserHistoryResponse(BaseModel):
|
||||
|
||||
@@ -9,7 +9,6 @@ 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
|
||||
@@ -23,6 +22,7 @@ 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=backend.server.model.Pagination(
|
||||
pagination=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=backend.server.model.Pagination(
|
||||
pagination=Pagination(
|
||||
total_items=total_items,
|
||||
total_pages=total_pages,
|
||||
current_page=page,
|
||||
|
||||
@@ -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: server_model.Pagination
|
||||
pagination: 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: server_model.Pagination
|
||||
pagination: Pagination
|
||||
|
||||
|
||||
class LibraryAgentFilter(str, Enum):
|
||||
|
||||
@@ -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=server_model.Pagination(
|
||||
pagination=Pagination(
|
||||
total_items=2, total_pages=1, current_page=1, page_size=50
|
||||
),
|
||||
)
|
||||
|
||||
@@ -466,6 +466,8 @@ 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)
|
||||
|
||||
@@ -546,7 +548,7 @@ async def create_store_submission(
|
||||
description: str = "",
|
||||
sub_heading: str = "",
|
||||
categories: list[str] = [],
|
||||
changes_summary: str = "Initial Submission",
|
||||
changes_summary: str | None = "Initial Submission",
|
||||
) -> backend.server.v2.store.model.StoreSubmission:
|
||||
"""
|
||||
Create the first (and only) store listing and thus submission as a normal user
|
||||
@@ -685,6 +687,160 @@ 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,
|
||||
@@ -696,7 +852,7 @@ async def create_store_version(
|
||||
description: str = "",
|
||||
sub_heading: str = "",
|
||||
categories: list[str] = [],
|
||||
changes_summary: str = "Update Submission",
|
||||
changes_summary: str | None = "Initial submission",
|
||||
) -> backend.server.v2.store.model.StoreSubmission:
|
||||
"""
|
||||
Create a new version for an existing store listing
|
||||
|
||||
@@ -94,3 +94,15 @@ 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
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import List
|
||||
import prisma.enums
|
||||
import pydantic
|
||||
|
||||
from backend.server.model import Pagination
|
||||
from backend.util.models import Pagination
|
||||
|
||||
|
||||
class MyAgent(pydantic.BaseModel):
|
||||
@@ -115,11 +115,9 @@ class StoreSubmission(pydantic.BaseModel):
|
||||
reviewed_at: datetime.datetime | None = None
|
||||
changes_summary: str | None = None
|
||||
|
||||
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
|
||||
# Additional fields for editing
|
||||
video_url: str | None = None
|
||||
categories: list[str] = []
|
||||
|
||||
|
||||
class StoreSubmissionsResponse(pydantic.BaseModel):
|
||||
@@ -161,6 +159,16 @@ 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
|
||||
|
||||
@@ -564,6 +564,47 @@ 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",
|
||||
|
||||
@@ -551,6 +551,8 @@ 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(
|
||||
|
||||
20
autogpt_platform/backend/backend/util/models.py
Normal file
20
autogpt_platform/backend/backend/util/models.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
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]
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
-- 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;
|
||||
@@ -659,6 +659,8 @@ view StoreSubmission {
|
||||
internal_comments String?
|
||||
reviewed_at DateTime?
|
||||
changes_summary String?
|
||||
video_url String?
|
||||
categories String[]
|
||||
|
||||
// Index or unique are not applied to views
|
||||
}
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
"review_comments": null,
|
||||
"internal_comments": null,
|
||||
"reviewed_at": null,
|
||||
"changes_summary": null
|
||||
"changes_summary": null,
|
||||
"video_url": "test.mp4",
|
||||
"categories": [
|
||||
"test-category"
|
||||
]
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
|
||||
@@ -39,20 +39,20 @@ faker = Faker()
|
||||
|
||||
|
||||
# Constants for data generation limits (reduced for E2E tests)
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
def get_image():
|
||||
@@ -76,6 +76,23 @@ 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."""
|
||||
|
||||
@@ -559,24 +576,37 @@ class TestDataCreator:
|
||||
submissions.append(test_submission.model_dump())
|
||||
print("✅ Created special test store submission for test123@gmail.com")
|
||||
|
||||
# Auto-approve the test submission
|
||||
# Randomly approve, reject, or leave pending the test submission
|
||||
if test_submission.store_listing_version_id:
|
||||
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")
|
||||
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")
|
||||
|
||||
# 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")
|
||||
# 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")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating test store submission: {e}")
|
||||
@@ -617,58 +647,87 @@ 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=[faker.word() for _ in range(3)],
|
||||
categories=[
|
||||
get_category()
|
||||
], # Single category from predefined list
|
||||
changes_summary="Initial E2E test submission",
|
||||
)
|
||||
submissions.append(submission.model_dump())
|
||||
print(f"✅ Created store submission: {submission.name}")
|
||||
|
||||
# Approve the submission so it appears in the store
|
||||
# Randomly approve, reject, or leave pending the submission
|
||||
if submission.store_listing_version_id:
|
||||
try:
|
||||
# Pick a random user as the reviewer (admin)
|
||||
reviewer_id = random.choice(self.users)["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"]
|
||||
|
||||
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:
|
||||
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%)
|
||||
print(
|
||||
f"Warning: Could not approve submission {submission.name}: {e}"
|
||||
f"⏳ Left submission pending for review: {submission.name}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -44,9 +44,9 @@ export function APIKeysSection() {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{apiKeys.map((key) => (
|
||||
<TableRow key={key.id}>
|
||||
<TableRow key={key.id} data-testid="api-key-row">
|
||||
<TableCell>{key.name}</TableCell>
|
||||
<TableCell>
|
||||
<TableCell data-testid="api-key-id">
|
||||
<div className="rounded-md border p-1 px-2 text-xs">
|
||||
{`${key.prefix}******************${key.postfix}`}
|
||||
</div>
|
||||
@@ -76,7 +76,11 @@ export function APIKeysSection() {
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Button
|
||||
data-testid="api-key-actions"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
AgentTableRow,
|
||||
AgentTableRowProps,
|
||||
} from "../AgentTableRow/AgentTableRow";
|
||||
import { StoreSubmissionEditRequest } from "@/app/api/__generated__/models/storeSubmissionEditRequest";
|
||||
|
||||
export interface AgentTableProps {
|
||||
agents: Omit<
|
||||
@@ -15,15 +16,23 @@ 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">
|
||||
@@ -62,6 +71,7 @@ export const AgentTable: React.FC<AgentTableProps> = ({
|
||||
{...agent}
|
||||
onViewSubmission={onViewSubmission}
|
||||
onDeleteSubmission={onDeleteSubmission}
|
||||
onEditSubmission={onEditSubmission}
|
||||
/>
|
||||
<div className="block md:hidden">
|
||||
<AgentTableCard
|
||||
|
||||
@@ -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: StatusType;
|
||||
status: SubmissionStatus;
|
||||
runs: number;
|
||||
rating: number;
|
||||
id: number;
|
||||
@@ -44,8 +44,7 @@ export const AgentTableCard = ({
|
||||
description,
|
||||
image_urls: imageSrc,
|
||||
date_submitted: dateSubmitted,
|
||||
// SafeCast: status is a string from the API...
|
||||
status: status.toUpperCase() as SubmissionStatus,
|
||||
status: status,
|
||||
runs,
|
||||
rating,
|
||||
});
|
||||
|
||||
@@ -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, StatusType } from "@/components/agptui/Status";
|
||||
import { Status } from "@/components/agptui/Status";
|
||||
import { useAgentTableRow } from "./useAgentTableRow";
|
||||
import { StoreSubmission } from "@/app/api/__generated__/models/storeSubmission";
|
||||
import {
|
||||
@@ -13,7 +13,10 @@ 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;
|
||||
@@ -23,13 +26,22 @@ export interface AgentTableRowProps {
|
||||
description: string;
|
||||
imageSrc: string[];
|
||||
date_submitted: string;
|
||||
status: StatusType;
|
||||
status: SubmissionStatus;
|
||||
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 = ({
|
||||
@@ -44,13 +56,18 @@ export const AgentTableRow = ({
|
||||
runs,
|
||||
rating,
|
||||
id,
|
||||
video_url,
|
||||
categories,
|
||||
store_listing_version_id,
|
||||
onViewSubmission,
|
||||
onDeleteSubmission,
|
||||
onEditSubmission,
|
||||
}: AgentTableRowProps) => {
|
||||
const { handleView, handleDelete } = useAgentTableRow({
|
||||
const { handleView, handleDelete, handleEdit } = useAgentTableRow({
|
||||
id,
|
||||
onViewSubmission,
|
||||
onDeleteSubmission,
|
||||
onEditSubmission,
|
||||
agent_id,
|
||||
agent_version,
|
||||
agentName,
|
||||
@@ -58,16 +75,23 @@ export const AgentTableRow = ({
|
||||
description,
|
||||
imageSrc,
|
||||
dateSubmitted,
|
||||
status: status.toUpperCase(),
|
||||
status,
|
||||
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-agent-name={agentName}
|
||||
data-submission-id={store_listing_version_id}
|
||||
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">
|
||||
@@ -140,13 +164,23 @@ 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">
|
||||
<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>
|
||||
{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.Separator className="my-1 h-px bg-gray-300 dark:bg-gray-600" />
|
||||
<DropdownMenu.Item
|
||||
onSelect={handleDelete}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
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;
|
||||
@@ -12,14 +19,18 @@ interface useAgentTableRowProps {
|
||||
description: string;
|
||||
imageSrc: string[];
|
||||
dateSubmitted: string;
|
||||
status: string;
|
||||
status: SubmissionStatus;
|
||||
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,
|
||||
@@ -30,6 +41,9 @@ export const useAgentTableRow = ({
|
||||
status,
|
||||
runs,
|
||||
rating,
|
||||
video_url,
|
||||
categories,
|
||||
store_listing_version_id,
|
||||
}: useAgentTableRowProps) => {
|
||||
const handleView = () => {
|
||||
onViewSubmission({
|
||||
@@ -41,16 +55,32 @@ export const useAgentTableRow = ({
|
||||
description,
|
||||
image_urls: imageSrc,
|
||||
date_submitted: dateSubmitted,
|
||||
// SafeCast: status is a string from the API...
|
||||
status: status.toUpperCase() as SubmissionStatus,
|
||||
status: status,
|
||||
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 };
|
||||
return { handleView, handleDelete, handleEdit };
|
||||
};
|
||||
|
||||
@@ -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,9 +13,13 @@ export const MainDashboardPage = () => {
|
||||
const {
|
||||
onDeleteSubmission,
|
||||
onViewSubmission,
|
||||
onEditSubmission,
|
||||
onEditSuccess,
|
||||
onEditClose,
|
||||
onOpenSubmitModal,
|
||||
onPublishStateChange,
|
||||
publishState,
|
||||
editState,
|
||||
// API data
|
||||
submissions,
|
||||
isLoading,
|
||||
@@ -89,17 +93,30 @@ export const MainDashboardPage = () => {
|
||||
dateSubmitted: new Date(
|
||||
submission.date_submitted,
|
||||
).toLocaleDateString(),
|
||||
status: submission.status.toLowerCase() as StatusType,
|
||||
status: submission.status,
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,10 +4,12 @@ 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";
|
||||
|
||||
@@ -17,6 +19,16 @@ 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();
|
||||
|
||||
@@ -28,6 +40,11 @@ export const useMainDashboardPage = () => {
|
||||
submissionData: null,
|
||||
});
|
||||
|
||||
const [editState, setEditState] = useState<EditState>({
|
||||
isOpen: false,
|
||||
submission: null,
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteSubmission } = useDeleteV2DeleteStoreSubmission({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
@@ -59,6 +76,43 @@ 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,
|
||||
@@ -83,7 +137,11 @@ export const useMainDashboardPage = () => {
|
||||
onPublishStateChange,
|
||||
onDeleteSubmission,
|
||||
onViewSubmission,
|
||||
onEditSubmission,
|
||||
onEditSuccess,
|
||||
onEditClose,
|
||||
publishState,
|
||||
editState,
|
||||
// API data
|
||||
submissions,
|
||||
isLoading: !isSuccess,
|
||||
|
||||
19
autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionEditRequest.ts
generated
Normal file
19
autogpt_platform/frontend/src/app/api/__generated__/models/storeSubmissionEditRequest.ts
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -2624,6 +2624,50 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/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"],
|
||||
@@ -6299,6 +6343,16 @@
|
||||
"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",
|
||||
@@ -6317,6 +6371,40 @@
|
||||
],
|
||||
"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" },
|
||||
|
||||
@@ -186,10 +186,16 @@ const FlowEditor: React.FC<{
|
||||
storage.clean(Key.SHEPHERD_TOUR);
|
||||
router.push(pathname);
|
||||
} else if (!storage.get(Key.SHEPHERD_TOUR)) {
|
||||
const emptyNodes = (forceRemove: boolean = false) =>
|
||||
forceRemove ? (setNodes([]), setEdges([]), true) : nodes.length === 0;
|
||||
startTutorial(emptyNodes, setPinBlocksPopover, setPinSavePopover);
|
||||
storage.set(Key.SHEPHERD_TOUR, "yes");
|
||||
// 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);
|
||||
}
|
||||
}, [router, pathname, params, setEdges, setNodes, nodes.length]);
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { SubmissionStatus } from "@/app/api/__generated__/models/submissionStatus";
|
||||
import * as React from "react";
|
||||
|
||||
export type StatusType = "draft" | "awaiting_review" | "approved" | "rejected";
|
||||
|
||||
interface StatusProps {
|
||||
status: StatusType;
|
||||
status: SubmissionStatus;
|
||||
}
|
||||
|
||||
const statusConfig: Record<
|
||||
StatusType,
|
||||
SubmissionStatus,
|
||||
{
|
||||
bgColor: string;
|
||||
dotColor: string;
|
||||
@@ -16,28 +15,28 @@ const statusConfig: Record<
|
||||
darkDotColor: string;
|
||||
}
|
||||
> = {
|
||||
draft: {
|
||||
[SubmissionStatus.DRAFT]: {
|
||||
bgColor: "bg-blue-50",
|
||||
dotColor: "bg-blue-500",
|
||||
text: "Draft",
|
||||
darkBgColor: "dark:bg-blue-900",
|
||||
darkDotColor: "dark:bg-blue-300",
|
||||
},
|
||||
awaiting_review: {
|
||||
[SubmissionStatus.PENDING]: {
|
||||
bgColor: "bg-amber-50",
|
||||
dotColor: "bg-amber-500",
|
||||
text: "Awaiting review",
|
||||
darkBgColor: "dark:bg-amber-900",
|
||||
darkDotColor: "dark:bg-amber-300",
|
||||
},
|
||||
approved: {
|
||||
[SubmissionStatus.APPROVED]: {
|
||||
bgColor: "bg-green-50",
|
||||
dotColor: "bg-green-500",
|
||||
text: "Approved",
|
||||
darkBgColor: "dark:bg-green-900",
|
||||
darkDotColor: "dark:bg-green-300",
|
||||
},
|
||||
rejected: {
|
||||
[SubmissionStatus.REJECTED]: {
|
||||
bgColor: "bg-red-50",
|
||||
dotColor: "bg-red-500",
|
||||
text: "Rejected",
|
||||
@@ -53,9 +52,9 @@ export const Status: React.FC<StatusProps> = ({ status }) => {
|
||||
* Valid values: 'draft', 'awaiting_review', 'approved', 'rejected'
|
||||
*/
|
||||
if (!status) {
|
||||
return <Status status="awaiting_review" />;
|
||||
return <Status status={SubmissionStatus.PENDING} />;
|
||||
} else if (!statusConfig[status]) {
|
||||
return <Status status="awaiting_review" />;
|
||||
return <Status status={SubmissionStatus.PENDING} />;
|
||||
}
|
||||
|
||||
const config = statusConfig[status];
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -190,7 +190,12 @@ export const startTutorial = (
|
||||
element: '[data-id="blocks-control-popover-content"]',
|
||||
on: "right",
|
||||
},
|
||||
buttons: [],
|
||||
buttons: [
|
||||
{
|
||||
text: "Back",
|
||||
action: tour.back,
|
||||
},
|
||||
],
|
||||
beforeShowPromise: () =>
|
||||
waitForElement('[data-id="blocks-control-popover-content"]').then(() => {
|
||||
disableOtherBlocks(
|
||||
@@ -202,7 +207,11 @@ export const startTutorial = (
|
||||
event: "click",
|
||||
},
|
||||
when: {
|
||||
show: () => setPinBlocksPopover(true),
|
||||
show: () => {
|
||||
setPinBlocksPopover(true);
|
||||
// Additional safeguard - ensure the popover stays pinned for this step
|
||||
setTimeout(() => setPinBlocksPopover(true), 50);
|
||||
},
|
||||
hide: enableAllBlocks,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -135,3 +135,105 @@ 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/);
|
||||
});
|
||||
|
||||
64
autogpt_platform/frontend/src/tests/api-keys.spec.ts
Normal file
64
autogpt_platform/frontend/src/tests/api-keys.spec.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user