mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-07 22:33:57 -05:00
feat(platform): add Agent Output Demo field to marketplace submission form (#11538)
## Summary - Add Agent Output Demo field to marketplace agent submission form, positioned below the Description field - Store agent output demo URLs in database for future CoPilot integration - Implement proper video/image ordering on marketplace pages - Add shared YouTube URL validation utility to eliminate code duplication ## Changes Made ### Frontend - **Agent submission form**: Added Agent Output Demo field with YouTube URL validation - **Edit agent form**: Added Agent Output Demo field for existing submissions - **Marketplace display**: Implemented proper video/image ordering: 1. YouTube/Overview video (if exists) 2. First image (hero) 3. Agent Output Demo (if exists) 4. Additional images - **Shared utilities**: Created `validateYouTubeUrl` function in `src/lib/utils.ts` ### Backend - **Database schema**: Added `agentOutputDemoUrl` field to `StoreListingVersion` model - **Database views**: Updated `StoreAgent` view to include `agent_output_demo` field - **API models**: Added `agent_output_demo_url` to submission requests and `agent_output_demo` to responses - **Database migration**: Added migration to create new column and update view - **Test files**: Updated all test files to include the new required field ## Test Plan - [x] Frontend form validation works correctly for YouTube URLs - [x] Database migration applies successfully - [x] Backend API accepts and returns the new field - [x] Marketplace displays videos in correct order - [x] Both frontend and backend formatting/linting pass - [x] All test files include required field to prevent failures 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -432,6 +432,7 @@ async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]:
|
||||
slug=agent.slug,
|
||||
agent_name=agent.agent_name,
|
||||
agent_video=agent.agent_video or "",
|
||||
agent_output_demo=agent.agent_output_demo or "",
|
||||
agent_image=agent.agent_image,
|
||||
creator=agent.creator_username,
|
||||
creator_avatar=agent.creator_avatar,
|
||||
|
||||
@@ -327,6 +327,7 @@ async def get_store_agent_details(
|
||||
slug=agent.slug,
|
||||
agent_name=agent.agent_name,
|
||||
agent_video=agent.agent_video or "",
|
||||
agent_output_demo=agent.agent_output_demo or "",
|
||||
agent_image=agent.agent_image,
|
||||
creator=agent.creator_username or "",
|
||||
creator_avatar=agent.creator_avatar or "",
|
||||
@@ -397,6 +398,7 @@ async def get_store_agent_by_version_id(
|
||||
slug=agent.slug,
|
||||
agent_name=agent.agent_name,
|
||||
agent_video=agent.agent_video or "",
|
||||
agent_output_demo=agent.agent_output_demo or "",
|
||||
agent_image=agent.agent_image,
|
||||
creator=agent.creator_username or "",
|
||||
creator_avatar=agent.creator_avatar or "",
|
||||
@@ -683,6 +685,7 @@ async def create_store_submission(
|
||||
slug: str,
|
||||
name: str,
|
||||
video_url: str | None = None,
|
||||
agent_output_demo_url: str | None = None,
|
||||
image_urls: list[str] = [],
|
||||
description: str = "",
|
||||
instructions: str | None = None,
|
||||
@@ -777,6 +780,7 @@ async def create_store_submission(
|
||||
agentGraphVersion=agent_version,
|
||||
name=name,
|
||||
videoUrl=video_url,
|
||||
agentOutputDemoUrl=agent_output_demo_url,
|
||||
imageUrls=image_urls,
|
||||
description=description,
|
||||
instructions=instructions,
|
||||
@@ -849,6 +853,7 @@ async def edit_store_submission(
|
||||
store_listing_version_id: str,
|
||||
name: str,
|
||||
video_url: str | None = None,
|
||||
agent_output_demo_url: str | None = None,
|
||||
image_urls: list[str] = [],
|
||||
description: str = "",
|
||||
sub_heading: str = "",
|
||||
@@ -930,6 +935,7 @@ async def edit_store_submission(
|
||||
store_listing_id=current_version.storeListingId,
|
||||
name=name,
|
||||
video_url=video_url,
|
||||
agent_output_demo_url=agent_output_demo_url,
|
||||
image_urls=image_urls,
|
||||
description=description,
|
||||
sub_heading=sub_heading,
|
||||
@@ -947,6 +953,7 @@ async def edit_store_submission(
|
||||
data=prisma.types.StoreListingVersionUpdateInput(
|
||||
name=name,
|
||||
videoUrl=video_url,
|
||||
agentOutputDemoUrl=agent_output_demo_url,
|
||||
imageUrls=image_urls,
|
||||
description=description,
|
||||
categories=categories,
|
||||
@@ -1008,6 +1015,7 @@ async def create_store_version(
|
||||
store_listing_id: str,
|
||||
name: str,
|
||||
video_url: str | None = None,
|
||||
agent_output_demo_url: str | None = None,
|
||||
image_urls: list[str] = [],
|
||||
description: str = "",
|
||||
instructions: str | None = None,
|
||||
@@ -1077,6 +1085,7 @@ async def create_store_version(
|
||||
agentGraphVersion=agent_version,
|
||||
name=name,
|
||||
videoUrl=video_url,
|
||||
agentOutputDemoUrl=agent_output_demo_url,
|
||||
imageUrls=image_urls,
|
||||
description=description,
|
||||
instructions=instructions,
|
||||
|
||||
@@ -44,6 +44,7 @@ class StoreAgentDetails(pydantic.BaseModel):
|
||||
slug: str
|
||||
agent_name: str
|
||||
agent_video: str
|
||||
agent_output_demo: str
|
||||
agent_image: list[str]
|
||||
creator: str
|
||||
creator_avatar: str
|
||||
@@ -121,6 +122,7 @@ class StoreSubmission(pydantic.BaseModel):
|
||||
|
||||
# Additional fields for editing
|
||||
video_url: str | None = None
|
||||
agent_output_demo_url: str | None = None
|
||||
categories: list[str] = []
|
||||
|
||||
|
||||
@@ -157,6 +159,7 @@ class StoreSubmissionRequest(pydantic.BaseModel):
|
||||
name: str
|
||||
sub_heading: str
|
||||
video_url: str | None = None
|
||||
agent_output_demo_url: str | None = None
|
||||
image_urls: list[str] = []
|
||||
description: str = ""
|
||||
instructions: str | None = None
|
||||
@@ -169,6 +172,7 @@ class StoreSubmissionEditRequest(pydantic.BaseModel):
|
||||
name: str
|
||||
sub_heading: str
|
||||
video_url: str | None = None
|
||||
agent_output_demo_url: str | None = None
|
||||
image_urls: list[str] = []
|
||||
description: str = ""
|
||||
instructions: str | None = None
|
||||
|
||||
@@ -62,6 +62,7 @@ def test_store_agent_details():
|
||||
slug="test-agent",
|
||||
agent_name="Test Agent",
|
||||
agent_video="video.mp4",
|
||||
agent_output_demo="demo.mp4",
|
||||
agent_image=["image1.jpg", "image2.jpg"],
|
||||
creator="creator1",
|
||||
creator_avatar="avatar.jpg",
|
||||
|
||||
@@ -438,6 +438,7 @@ async def create_submission(
|
||||
slug=submission_request.slug,
|
||||
name=submission_request.name,
|
||||
video_url=submission_request.video_url,
|
||||
agent_output_demo_url=submission_request.agent_output_demo_url,
|
||||
image_urls=submission_request.image_urls,
|
||||
description=submission_request.description,
|
||||
instructions=submission_request.instructions,
|
||||
@@ -481,6 +482,7 @@ async def edit_submission(
|
||||
store_listing_version_id=store_listing_version_id,
|
||||
name=submission_request.name,
|
||||
video_url=submission_request.video_url,
|
||||
agent_output_demo_url=submission_request.agent_output_demo_url,
|
||||
image_urls=submission_request.image_urls,
|
||||
description=submission_request.description,
|
||||
instructions=submission_request.instructions,
|
||||
|
||||
@@ -378,6 +378,7 @@ def test_get_agent_details(
|
||||
slug="test-agent",
|
||||
agent_name="Test Agent",
|
||||
agent_video="video.mp4",
|
||||
agent_output_demo="demo.mp4",
|
||||
agent_image=["image1.jpg", "image2.jpg"],
|
||||
creator="creator1",
|
||||
creator_avatar="avatar1.jpg",
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "StoreListingVersion" ADD COLUMN "agentOutputDemoUrl" TEXT;
|
||||
|
||||
-- Drop and recreate the StoreAgent view with agentOutputDemoUrl field
|
||||
DROP VIEW IF EXISTS "StoreAgent";
|
||||
|
||||
CREATE OR REPLACE VIEW "StoreAgent" AS
|
||||
WITH latest_versions AS (
|
||||
SELECT
|
||||
"storeListingId",
|
||||
MAX(version) AS max_version
|
||||
FROM "StoreListingVersion"
|
||||
WHERE "submissionStatus" = 'APPROVED'
|
||||
GROUP BY "storeListingId"
|
||||
),
|
||||
agent_versions AS (
|
||||
SELECT
|
||||
"storeListingId",
|
||||
array_agg(DISTINCT version::text ORDER BY version::text) AS versions
|
||||
FROM "StoreListingVersion"
|
||||
WHERE "submissionStatus" = 'APPROVED'
|
||||
GROUP BY "storeListingId"
|
||||
)
|
||||
SELECT
|
||||
sl.id AS listing_id,
|
||||
slv.id AS "storeListingVersionId",
|
||||
slv."createdAt" AS updated_at,
|
||||
sl.slug,
|
||||
COALESCE(slv.name, '') AS agent_name,
|
||||
slv."videoUrl" AS agent_video,
|
||||
slv."agentOutputDemoUrl" AS agent_output_demo,
|
||||
COALESCE(slv."imageUrls", ARRAY[]::text[]) AS agent_image,
|
||||
slv."isFeatured" AS featured,
|
||||
p.username AS creator_username, -- Allow NULL for malformed sub-agents
|
||||
p."avatarUrl" AS creator_avatar, -- Allow NULL for malformed sub-agents
|
||||
slv."subHeading" AS sub_heading,
|
||||
slv.description,
|
||||
slv.categories,
|
||||
slv.search,
|
||||
COALESCE(ar.run_count, 0::bigint) AS runs,
|
||||
COALESCE(rs.avg_rating, 0.0)::double precision AS rating,
|
||||
COALESCE(av.versions, ARRAY[slv.version::text]) AS versions,
|
||||
slv."isAvailable" AS is_available,
|
||||
COALESCE(sl."useForOnboarding", false) AS "useForOnboarding"
|
||||
FROM "StoreListing" sl
|
||||
JOIN latest_versions lv
|
||||
ON sl.id = lv."storeListingId"
|
||||
JOIN "StoreListingVersion" slv
|
||||
ON slv."storeListingId" = lv."storeListingId"
|
||||
AND slv.version = lv.max_version
|
||||
AND slv."submissionStatus" = 'APPROVED'
|
||||
JOIN "AgentGraph" a
|
||||
ON slv."agentGraphId" = a.id
|
||||
AND slv."agentGraphVersion" = a.version
|
||||
LEFT JOIN "Profile" p
|
||||
ON sl."owningUserId" = p."userId"
|
||||
LEFT JOIN "mv_review_stats" rs
|
||||
ON sl.id = rs."storeListingId"
|
||||
LEFT JOIN "mv_agent_run_counts" ar
|
||||
ON a.id = ar."agentGraphId"
|
||||
LEFT JOIN agent_versions av
|
||||
ON sl.id = av."storeListingId"
|
||||
WHERE sl."isDeleted" = false
|
||||
AND sl."hasApprovedVersion" = true;
|
||||
@@ -701,10 +701,11 @@ view StoreAgent {
|
||||
storeListingVersionId String
|
||||
updated_at DateTime
|
||||
|
||||
slug String
|
||||
agent_name String
|
||||
agent_video String?
|
||||
agent_image String[]
|
||||
slug String
|
||||
agent_name String
|
||||
agent_video String?
|
||||
agent_output_demo String?
|
||||
agent_image String[]
|
||||
|
||||
featured Boolean @default(false)
|
||||
creator_username String?
|
||||
@@ -833,13 +834,14 @@ model StoreListingVersion {
|
||||
AgentGraph AgentGraph @relation(fields: [agentGraphId, agentGraphVersion], references: [id, version])
|
||||
|
||||
// Content fields
|
||||
name String
|
||||
subHeading String
|
||||
videoUrl String?
|
||||
imageUrls String[]
|
||||
description String
|
||||
instructions String?
|
||||
categories String[]
|
||||
name String
|
||||
subHeading String
|
||||
videoUrl String?
|
||||
agentOutputDemoUrl String?
|
||||
imageUrls String[]
|
||||
description String
|
||||
instructions String?
|
||||
categories String[]
|
||||
|
||||
isFeatured Boolean @default(false)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"slug": "test-agent",
|
||||
"agent_name": "Test Agent",
|
||||
"agent_video": "video.mp4",
|
||||
"agent_output_demo": "demo.mp4",
|
||||
"agent_image": [
|
||||
"image1.jpg",
|
||||
"image2.jpg"
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"reviewed_at": null,
|
||||
"changes_summary": null,
|
||||
"video_url": "test.mp4",
|
||||
"agent_output_demo_url": null,
|
||||
"categories": [
|
||||
"test-category"
|
||||
]
|
||||
|
||||
@@ -97,11 +97,31 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
|
||||
/>
|
||||
</div>
|
||||
<AgentImages
|
||||
images={
|
||||
agent.agent_video
|
||||
? [agent.agent_video, ...agent.agent_image]
|
||||
: agent.agent_image
|
||||
}
|
||||
images={(() => {
|
||||
const orderedImages: string[] = [];
|
||||
|
||||
// 1. YouTube/Overview video (if it exists)
|
||||
if (agent.agent_video) {
|
||||
orderedImages.push(agent.agent_video);
|
||||
}
|
||||
|
||||
// 2. First image (hero)
|
||||
if (agent.agent_image.length > 0) {
|
||||
orderedImages.push(agent.agent_image[0]);
|
||||
}
|
||||
|
||||
// 3. Agent Output Demo (if it exists)
|
||||
if ((agent as any).agent_output_demo) {
|
||||
orderedImages.push((agent as any).agent_output_demo);
|
||||
}
|
||||
|
||||
// 4. Additional images
|
||||
if (agent.agent_image.length > 1) {
|
||||
orderedImages.push(...agent.agent_image.slice(1));
|
||||
}
|
||||
|
||||
return orderedImages;
|
||||
})()}
|
||||
/>
|
||||
</div>
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
|
||||
@@ -8685,6 +8685,10 @@
|
||||
"slug": { "type": "string", "title": "Slug" },
|
||||
"agent_name": { "type": "string", "title": "Agent Name" },
|
||||
"agent_video": { "type": "string", "title": "Agent Video" },
|
||||
"agent_output_demo": {
|
||||
"type": "string",
|
||||
"title": "Agent Output Demo"
|
||||
},
|
||||
"agent_image": {
|
||||
"items": { "type": "string" },
|
||||
"type": "array",
|
||||
@@ -8735,6 +8739,7 @@
|
||||
"slug",
|
||||
"agent_name",
|
||||
"agent_video",
|
||||
"agent_output_demo",
|
||||
"agent_image",
|
||||
"creator",
|
||||
"creator_avatar",
|
||||
@@ -8902,6 +8907,10 @@
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Video Url"
|
||||
},
|
||||
"agent_output_demo_url": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Agent Output Demo Url"
|
||||
},
|
||||
"categories": {
|
||||
"items": { "type": "string" },
|
||||
"type": "array",
|
||||
@@ -8933,6 +8942,10 @@
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Video Url"
|
||||
},
|
||||
"agent_output_demo_url": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Agent Output Demo Url"
|
||||
},
|
||||
"image_urls": {
|
||||
"items": { "type": "string" },
|
||||
"type": "array",
|
||||
@@ -8978,6 +8991,10 @@
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Video Url"
|
||||
},
|
||||
"agent_output_demo_url": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Agent Output Demo Url"
|
||||
},
|
||||
"image_urls": {
|
||||
"items": { "type": "string" },
|
||||
"type": "array",
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
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 { validateYouTubeUrl } from "@/lib/utils";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
@@ -35,22 +36,7 @@ export const useEditAgentForm = ({
|
||||
.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"),
|
||||
.refine(validateYouTubeUrl, "Please enter a valid YouTube URL"),
|
||||
category: z.string().min(1, "Category is required"),
|
||||
description: z
|
||||
.string()
|
||||
@@ -60,6 +46,9 @@ export const useEditAgentForm = ({
|
||||
.string()
|
||||
.min(1, "Changes summary is required")
|
||||
.max(200, "Changes summary must be less than 200 characters"),
|
||||
agentOutputDemo: z
|
||||
.string()
|
||||
.refine(validateYouTubeUrl, "Please enter a valid YouTube URL"),
|
||||
});
|
||||
|
||||
type EditAgentFormData = z.infer<typeof editAgentSchema>;
|
||||
@@ -91,6 +80,7 @@ export const useEditAgentForm = ({
|
||||
category: submission.categories?.[0] || "",
|
||||
description: submission.description,
|
||||
changes_summary: submission.changes_summary || "",
|
||||
agentOutputDemo: submission.agent_output_demo_url || "",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -134,6 +124,7 @@ export const useEditAgentForm = ({
|
||||
description: data.description,
|
||||
image_urls: images,
|
||||
video_url: data.youtubeLink || "",
|
||||
agent_output_demo_url: data.agentOutputDemo || "",
|
||||
categories: filteredCategories,
|
||||
changes_summary: data.changes_summary,
|
||||
},
|
||||
|
||||
@@ -163,6 +163,21 @@ export function AgentInfoStep({
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="agentOutputDemo"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
id={field.name}
|
||||
label="Agent Output Demo"
|
||||
type="url"
|
||||
placeholder="Add a short video showing the agent's results in action."
|
||||
error={form.formState.errors.agentOutputDemo?.message}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="instructions"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import z from "zod";
|
||||
import { validateYouTubeUrl } from "@/lib/utils";
|
||||
|
||||
export const publishAgentSchema = z.object({
|
||||
title: z
|
||||
@@ -19,22 +20,7 @@ export const publishAgentSchema = z.object({
|
||||
),
|
||||
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"),
|
||||
.refine(validateYouTubeUrl, "Please enter a valid YouTube URL"),
|
||||
category: z.string().min(1, "Category is required"),
|
||||
description: z
|
||||
.string()
|
||||
@@ -48,6 +34,9 @@ export const publishAgentSchema = z.object({
|
||||
(val) => !val || val.length <= 2000,
|
||||
"Instructions must be less than 2000 characters",
|
||||
),
|
||||
agentOutputDemo: z
|
||||
.string()
|
||||
.refine(validateYouTubeUrl, "Please enter a valid YouTube URL"),
|
||||
});
|
||||
|
||||
export type PublishAgentFormData = z.infer<typeof publishAgentSchema>;
|
||||
@@ -64,4 +53,5 @@ export interface PublishAgentInfoInitialData {
|
||||
additionalImages?: string[];
|
||||
recommendedScheduleCron?: string;
|
||||
instructions?: string;
|
||||
agentOutputDemo?: string;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export function useAgentInfoStep({
|
||||
description: "",
|
||||
recommendedScheduleCron: "",
|
||||
instructions: "",
|
||||
agentOutputDemo: "",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -68,6 +69,7 @@ export function useAgentInfoStep({
|
||||
description: initialData.description,
|
||||
recommendedScheduleCron: initialData.recommendedScheduleCron || "",
|
||||
instructions: initialData.instructions || "",
|
||||
agentOutputDemo: initialData.agentOutputDemo || "",
|
||||
});
|
||||
}
|
||||
}, [initialData, form]);
|
||||
@@ -99,12 +101,13 @@ export function useAgentInfoStep({
|
||||
instructions: data.instructions || null,
|
||||
image_urls: images,
|
||||
video_url: data.youtubeLink || "",
|
||||
agent_output_demo_url: data.agentOutputDemo || "",
|
||||
agent_id: selectedAgentId || "",
|
||||
agent_version: selectedAgentVersion || 0,
|
||||
slug: data.slug.replace(/\s+/g, "-"),
|
||||
categories: filteredCategories,
|
||||
recommended_schedule_cron: data.recommendedScheduleCron || null,
|
||||
});
|
||||
} as any);
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListMySubmissionsQueryKey(),
|
||||
|
||||
@@ -428,3 +428,20 @@ export function isEmpty(value: any): boolean {
|
||||
export function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/** Validate YouTube URL */
|
||||
export function validateYouTubeUrl(val: string): boolean {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user