From 9f9a2f9b827f7f5eef7a0f2d73da986dc6d0b915 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:49:57 +0000 Subject: [PATCH 01/48] Initial plan From 155efe52aef9deb2d49b74d9b0c4488657264ffa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 17:02:39 +0000 Subject: [PATCH 02/48] Add GraphQL mutations for updateItemListing, pauseItemListing, and deleteItemListing Co-authored-by: dani-vaibhav <182140623+dani-vaibhav@users.noreply.github.com> --- .../src/contexts/listing/item/index.ts | 9 ++- .../src/contexts/listing/item/pause.ts | 31 ++++++++ .../src/contexts/listing/item/update.ts | 53 +++++++++++++- .../schema/types/listing/item-listing.graphql | 13 ++++ .../types/listing/item-listing.resolvers.ts | 72 +++++++++++++++++++ 5 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 packages/sthrift/application-services/src/contexts/listing/item/pause.ts diff --git a/packages/sthrift/application-services/src/contexts/listing/item/index.ts b/packages/sthrift/application-services/src/contexts/listing/item/index.ts index 54a359392..3150355a6 100644 --- a/packages/sthrift/application-services/src/contexts/listing/item/index.ts +++ b/packages/sthrift/application-services/src/contexts/listing/item/index.ts @@ -8,6 +8,7 @@ import { } from './query-by-sharer.ts'; import { type ItemListingQueryAllCommand, queryAll } from './query-all.ts'; import { type ItemListingCancelCommand, cancel } from './cancel.ts'; +import { type ItemListingPauseCommand, pause } from './pause.ts'; import { queryPaged } from './query-paged.ts'; import { type ItemListingUpdateCommand, update } from './update.ts'; @@ -29,6 +30,9 @@ export interface ItemListingApplicationService { cancel: ( command: ItemListingCancelCommand, ) => Promise; + pause: ( + command: ItemListingPauseCommand, + ) => Promise; queryPaged: (command: { page: number; pageSize: number; @@ -42,7 +46,9 @@ export interface ItemListingApplicationService { page: number; pageSize: number; }>; - update: (command: ItemListingUpdateCommand) => Promise; + update: ( + command: ItemListingUpdateCommand, + ) => Promise; } export const ItemListing = ( @@ -54,6 +60,7 @@ export const ItemListing = ( queryBySharer: queryBySharer(dataSources), queryAll: queryAll(dataSources), cancel: cancel(dataSources), + pause: pause(dataSources), queryPaged: queryPaged(dataSources), update: update(dataSources), }; diff --git a/packages/sthrift/application-services/src/contexts/listing/item/pause.ts b/packages/sthrift/application-services/src/contexts/listing/item/pause.ts new file mode 100644 index 000000000..1ba08dfab --- /dev/null +++ b/packages/sthrift/application-services/src/contexts/listing/item/pause.ts @@ -0,0 +1,31 @@ +import type { Domain } from '@sthrift/domain'; +import type { DataSources } from '@sthrift/persistence'; + +export interface ItemListingPauseCommand { + id: string; +} + +export const pause = (dataSources: DataSources) => { + return async ( + command: ItemListingPauseCommand, + ): Promise => { + let itemListingToReturn: + | Domain.Contexts.Listing.ItemListing.ItemListingEntityReference + | undefined; + await dataSources.domainDataSource.Listing.ItemListing.ItemListingUnitOfWork.withScopedTransaction( + async (repo) => { + const listing = await repo.getById(command.id); + if (!listing) { + throw new Error('Listing not found'); + } + + listing.pause(); + itemListingToReturn = await repo.save(listing); + }, + ); + if (!itemListingToReturn) { + throw new Error('ItemListing not paused'); + } + return itemListingToReturn; + }; +}; diff --git a/packages/sthrift/application-services/src/contexts/listing/item/update.ts b/packages/sthrift/application-services/src/contexts/listing/item/update.ts index a23538b9d..79937390d 100644 --- a/packages/sthrift/application-services/src/contexts/listing/item/update.ts +++ b/packages/sthrift/application-services/src/contexts/listing/item/update.ts @@ -1,13 +1,23 @@ +import type { Domain } from '@sthrift/domain'; import type { DataSources } from '@sthrift/persistence'; export interface ItemListingUpdateCommand { id: string; + title?: string; + description?: string; + category?: string; + location?: string; + sharingPeriodStart?: Date; + sharingPeriodEnd?: Date; + images?: string[]; isBlocked?: boolean; isDeleted?: boolean; } export const update = (datasources: DataSources) => { - return async (command: ItemListingUpdateCommand): Promise => { + return async ( + command: ItemListingUpdateCommand, + ): Promise => { const uow = datasources.domainDataSource.Listing.ItemListing.ItemListingUnitOfWork; if (!uow) @@ -15,9 +25,42 @@ export const update = (datasources: DataSources) => { 'ItemListingUnitOfWork not available on dataSources.domainDataSource.Listing.ItemListing', ); + let updatedListing: + | Domain.Contexts.Listing.ItemListing.ItemListingEntityReference + | undefined; + await uow.withScopedTransactionById(command.id, async (repo) => { const listing = await repo.get(command.id); + // Update listing fields + if (command.title !== undefined) { + listing.title = command.title; + } + + if (command.description !== undefined) { + listing.description = command.description; + } + + if (command.category !== undefined) { + listing.category = command.category; + } + + if (command.location !== undefined) { + listing.location = command.location; + } + + if (command.sharingPeriodStart !== undefined) { + listing.sharingPeriodStart = command.sharingPeriodStart; + } + + if (command.sharingPeriodEnd !== undefined) { + listing.sharingPeriodEnd = command.sharingPeriodEnd; + } + + if (command.images !== undefined) { + listing.images = command.images; + } + if (command.isBlocked !== undefined) { listing.setBlocked(command.isBlocked); } @@ -26,7 +69,13 @@ export const update = (datasources: DataSources) => { listing.setDeleted(command.isDeleted); } - await repo.save(listing); + updatedListing = await repo.save(listing); }); + + if (!updatedListing) { + throw new Error('ItemListing not updated'); + } + + return updatedListing; }; }; diff --git a/packages/sthrift/graphql/src/schema/types/listing/item-listing.graphql b/packages/sthrift/graphql/src/schema/types/listing/item-listing.graphql index a5e48b8f3..e10e9349b 100644 --- a/packages/sthrift/graphql/src/schema/types/listing/item-listing.graphql +++ b/packages/sthrift/graphql/src/schema/types/listing/item-listing.graphql @@ -69,8 +69,21 @@ input CreateItemListingInput { isDraft: Boolean } +input UpdateItemListingInput { + title: String + description: String + category: String + location: String + sharingPeriodStart: DateTime + sharingPeriodEnd: DateTime + images: [String!] +} + extend type Mutation { createItemListing(input: CreateItemListingInput!): ItemListing! + updateItemListing(id: ObjectID!, input: UpdateItemListingInput!): ItemListing! + pauseItemListing(id: ObjectID!): ItemListing! + deleteItemListing(id: ObjectID!): ItemListing! cancelItemListing(id: ObjectID!): ItemListing! removeListing(id: ObjectID!): Boolean! unblockListing(id: ObjectID!): Boolean! diff --git a/packages/sthrift/graphql/src/schema/types/listing/item-listing.resolvers.ts b/packages/sthrift/graphql/src/schema/types/listing/item-listing.resolvers.ts index 354bce333..d02574926 100644 --- a/packages/sthrift/graphql/src/schema/types/listing/item-listing.resolvers.ts +++ b/packages/sthrift/graphql/src/schema/types/listing/item-listing.resolvers.ts @@ -120,6 +120,78 @@ const itemListingResolvers: Resolvers = { ); }, + updateItemListing: async (_parent, args, context) => { + const userEmail = + context.applicationServices.verifiedUser?.verifiedJwt?.email; + if (!userEmail) { + throw new Error('Authentication required'); + } + + const command: { + id: string; + title?: string; + description?: string; + category?: string; + location?: string; + sharingPeriodStart?: Date; + sharingPeriodEnd?: Date; + images?: string[]; + } = { + id: args.id, + }; + + if (args.input.title !== undefined && args.input.title !== null) { + command.title = args.input.title; + } + if (args.input.description !== undefined && args.input.description !== null) { + command.description = args.input.description; + } + if (args.input.category !== undefined && args.input.category !== null) { + command.category = args.input.category; + } + if (args.input.location !== undefined && args.input.location !== null) { + command.location = args.input.location; + } + if (args.input.sharingPeriodStart !== undefined && args.input.sharingPeriodStart !== null) { + command.sharingPeriodStart = new Date(args.input.sharingPeriodStart); + } + if (args.input.sharingPeriodEnd !== undefined && args.input.sharingPeriodEnd !== null) { + command.sharingPeriodEnd = new Date(args.input.sharingPeriodEnd); + } + if (args.input.images !== undefined && args.input.images !== null) { + command.images = [...args.input.images]; + } + + return await context.applicationServices.Listing.ItemListing.update( + command, + ); + }, + + pauseItemListing: async (_parent, args, context) => { + const userEmail = + context.applicationServices.verifiedUser?.verifiedJwt?.email; + if (!userEmail) { + throw new Error('Authentication required'); + } + + return await context.applicationServices.Listing.ItemListing.pause({ + id: args.id, + }); + }, + + deleteItemListing: async (_parent, args, context) => { + const userEmail = + context.applicationServices.verifiedUser?.verifiedJwt?.email; + if (!userEmail) { + throw new Error('Authentication required'); + } + + return await context.applicationServices.Listing.ItemListing.update({ + id: args.id, + isDeleted: true, + }); + }, + removeListing: async (_parent, args, context) => { // Admin-note: role-based authorization should be implemented here (security) // Once implemented, use system-level permissions for admin operations From c6afcd019b111c14f3ecc0f585aeb6ef96be6aac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 17:09:30 +0000 Subject: [PATCH 03/48] Implement Edit Listing UI components with full CRUD operations Co-authored-by: dani-vaibhav <182140623+dani-vaibhav@users.noreply.github.com> --- .../edit-listing/edit-listing-form.tsx | 191 +++++++++++ .../edit-listing.container.graphql | 49 +++ .../edit-listing/edit-listing.container.tsx | 230 +++++++++++++ .../components/edit-listing/edit-listing.tsx | 307 ++++++++++++++++++ .../home/my-listings/pages/edit-listing.tsx | 7 +- 5 files changed, 783 insertions(+), 1 deletion(-) create mode 100644 apps/ui-sharethrift/src/components/layouts/home/components/edit-listing/edit-listing-form.tsx create mode 100644 apps/ui-sharethrift/src/components/layouts/home/components/edit-listing/edit-listing.container.graphql create mode 100644 apps/ui-sharethrift/src/components/layouts/home/components/edit-listing/edit-listing.container.tsx create mode 100644 apps/ui-sharethrift/src/components/layouts/home/components/edit-listing/edit-listing.tsx diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/edit-listing/edit-listing-form.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/edit-listing/edit-listing-form.tsx new file mode 100644 index 000000000..1304632e7 --- /dev/null +++ b/apps/ui-sharethrift/src/components/layouts/home/components/edit-listing/edit-listing-form.tsx @@ -0,0 +1,191 @@ +import { Row, Col, Button, Form, Input, Select, DatePicker, Space } from 'antd'; +import { + DeleteOutlined, + PauseCircleOutlined, + StopOutlined, +} from '@ant-design/icons'; +import type { ConfigType } from 'dayjs'; +import dayjs from 'dayjs'; + +const { TextArea } = Input; +const { RangePicker } = DatePicker; + +export interface EditListingFormProps { + categories: string[]; + isLoading: boolean; + maxCharacters: number; + handleFormSubmit: () => void; + onNavigateBack: () => void; + onPause: () => void; + onDelete: () => void; + onCancel: () => void; + canPause: boolean; + canCancel: boolean; +} + +export const EditListingForm: React.FC = ({ + categories, + isLoading, + maxCharacters, + handleFormSubmit, + onNavigateBack, + onPause, + onDelete, + onCancel, + canPause, + canCancel, +}) => { + const disabledDate = (current: ConfigType) => { + try { + const maybeDay = current as dayjs.Dayjs; + if (!maybeDay || typeof maybeDay.isBefore !== 'function') { + return false; + } + + return maybeDay.isBefore(dayjs(), 'day'); + } catch (_err) { + return false; + } + }; + + return ( +
+ + + + + + + + + + + + + + + + + + +